diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000..78434a1e0 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[env] +ARTENANT = "development-tenant" diff --git a/Gemfile.lock b/Gemfile.lock index 8fbd0f756..44c91583d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 8758240be33a6f6f7e7f56486a2e78008c193030 + revision: f04f4f0a153ae5da65b400021aa97fdb28ac86b5 specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -18,7 +18,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: 32d96235810e268eb5335a7837b13cda3f8b653d + revision: 0ed3cd46d0038f8108ed92edb308c81fa6cc3730 branch: main specs: actioncable (8.1.0.alpha) @@ -145,7 +145,7 @@ GEM xpath (~> 3.2) chunky_png (1.4.0) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.2) crass (1.0.6) date (3.4.1) debug (1.10.0) @@ -208,7 +208,7 @@ GEM mini_portile2 (2.8.8) minitest (5.25.5) msgpack (1.8.0) - net-imap (0.5.6) + net-imap (0.5.7) date net-protocol net-pop (0.1.2) @@ -223,24 +223,24 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.4) - nokogiri (1.18.7) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.7-aarch64-linux-gnu) + nokogiri (1.18.8-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-aarch64-linux-musl) + nokogiri (1.18.8-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-gnu) + nokogiri (1.18.8-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-musl) + nokogiri (1.18.8-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm64-darwin) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-darwin) + nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-gnu) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-musl) + nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.1) parallel (1.26.3) @@ -264,7 +264,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.12) + rack (3.1.13) rack-contrib (2.5.0) rack (< 4) rack-session (2.1.0) @@ -373,7 +373,7 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.6) + stringio (3.1.7) thor (1.3.2) thruster (0.1.12) thruster (0.1.12-aarch64-linux) diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 5497c7aa5..b00fa22ab 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -11,8 +11,8 @@ /* Text */ --text-xx-small: 0.55rem; - --text-x-small: 0.7rem; - --text-small: 0.8rem; + --text-x-small: 0.75rem; + --text-small: 0.85rem; --text-normal: 1rem; --text-medium: 1.1rem; --text-large: 1.5rem; diff --git a/app/assets/stylesheets/avatars.css b/app/assets/stylesheets/avatars.css index a55112b8c..d30bb984f 100644 --- a/app/assets/stylesheets/avatars.css +++ b/app/assets/stylesheets/avatars.css @@ -10,7 +10,8 @@ margin: 0; place-items: center; - img { + img, + .icon { aspect-ratio: 1; block-size: auto; border-radius: var(--avatar-border-radius); diff --git a/app/assets/stylesheets/base.css b/app/assets/stylesheets/base.css index 6bebd44d5..faaba06c9 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css @@ -85,6 +85,19 @@ list-style: none; } + kbd { + border: 1px solid; + border-radius: .3em; + box-shadow: 0 0.1em 0 currentColor; + font-family: var(--font-mono); + font-size: var(--text-x-small); + font-weight: 600; + opacity: 0.7; + padding: 0em 0.4em; + text-transform: uppercase; + white-space: nowrap; + } + video { max-inline-size: 100%; } diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index ad64cda2d..4311f1007 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -92,6 +92,7 @@ border-color: var(--card-color) !important; border-style: dashed !important; + box-shadow: none !important; } .card__background { diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 77be4096d..cd4973c7f 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -15,7 +15,7 @@ "qw notch-top we" "actions-left card actions-right" "er notch-bottom rt"; - grid-template-columns: 48px minmax(0, 960px) 48px; + grid-template-columns: 48px minmax(0, 1120px) 48px; inline-size: fit-content; max-inline-size: 100%; margin-inline: auto; @@ -31,6 +31,15 @@ border: none; } + + .card__meta, + .card__stages { + font-size: var(--text-small); + } + + .card__stages { + max-inline-size: 32ch; + } } /* Children @@ -100,7 +109,7 @@ z-index: 1; } - .btn:not(.popup__item, .btn--plain) { + .btn:not(.popup__item, .btn--plain, .btn--reversed) { --btn-background: var(--card-color); --btn-color: var(--color-ink-inverted); } @@ -110,6 +119,11 @@ text-decoration: underline; } + + .btn--reversed { + --btn-background: var(--color-bg); + --btn-color: var(--card-color); + } } .card-perma__closure-message { @@ -119,29 +133,4 @@ padding-left: 1ch; } } - - /* Variants - /* ------------------------------------------------------------------------ */ - - .card-perma--pointing { - --arrow-size: 20em; - --aspect-ratio: 7; - - position: relative; - margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio)); - - &::after { - aspect-ratio: var(--aspect-ratio); - background-color: var(--color-container); - clip-path: polygon(50% 100%, 100% 0, 0 0); - inline-size: var(--arrow-size); - content: ""; - display: block; - inset-block-start: 99%; - inset-inline-start: 50%; - position: absolute; - transform: translateX(-50%); - z-index: -1; - } - } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index f61b37ed0..fb8465e31 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -105,18 +105,45 @@ padding-block: var(--card-padding-block); } + .card__description { + house-md-toolbar, + .house-md-content { + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + } + + .house-md-content { + min-block-size: 3lh; + padding: var(--block-space-half) var(--inline-space) 0 0; + } + + & ~ .btn { + --btn-background: var(--card-color); + --btn-color: var(--color-ink-reversed); + } + } + + .card__content { + flex: 2 1 auto; + } + .card__title { --hover-size: 0; --input-border-radius: 0; + --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --lines: 3; - flex: 2 1 75%; + color: inherit; font-size: var(--text-xx-large); font-weight: 900; - line-height: 1.1; + line-height: 1.2; min-block-size: 3lh; text-wrap: balance; - .card__title-field { + .card-perma__bg & { + min-block-size: unset; + } + + .card-field__title { &:is(textarea)::placeholder { color: inherit; opacity: 0.5; @@ -124,23 +151,34 @@ } .card__title-link { - --lines: 3; - color: inherit; } } .card__stages { display: flex; - flex: 1 1 25%; + flex: 0 1 auto; flex-direction: column; - max-width: 100%; - min-width: 0; - padding-block: var(--block-space); + justify-self: end; + max-inline-size: 20ch; + padding-block: var(--block-space-half); + } + + .card--new-card-button:is(.btn) { + --btn-padding: var(--block-space) var(--inline-space-double); + --btn-border-size: 0; + --btn-color: var(--color-link); + --btn-icon-size: 1em; + --card-padding-block: calc(var(--block-space) * 1.25); + + box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-link) 20%, var(--color-bg)); + color: var(--btn-color); + font-weight: 700; + } .card__move-button { - --btn-background: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --btn-background: color-mix(in srgb, inherit 40%, var(--color-ink)); margin-block-start: calc(var(--block-space) * 1.2); margin-inline-end: calc(var(--inline-space) * -4); @@ -192,16 +230,25 @@ .card__closed { align-items: center; - aspect-ratio: 7/4; + aspect-ratio: 8/5; + background-color: var(--color-bg); border: var(--block-space-half) solid var(--card-color); + border-radius: 0.2em; + color: var(--card-color); display: flex; flex-direction: column; inset: auto var(--block-space) var(--block-space-double) auto; justify-content: space-between; + max-inline-size: 25ch; + min-inline-size: 18ch; + mix-blend-mode: multiply; padding: var(--block-space-half) calc(var(--inline-space) * 1.5); position: absolute; transform: rotate(-10deg); - z-index: -1; + + @media (prefers-color-scheme: dark) { + mix-blend-mode: screen; + } } .card__closed-by { @@ -210,8 +257,10 @@ .card__closed-title { color: var(--color-ink-inverted); - font-size: var(--text-rel-large); + font-size: 1.5em; font-weight: 900; + line-height: 1.2; + text-align: center; text-shadow: -2px -2px 0 var(--card-color), 2px -2px 0 var(--card-color), diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index a22069fc9..9fc3f9ac2 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -12,7 +12,7 @@ text-align: center; @media (min-width: 160ch) { - padding-inline: calc(var(--tray-size) + var(--inline-space) * 1.5); + padding-inline: var(--tray-size); } } @@ -68,6 +68,7 @@ padding: var(--comment-padding-block) var(--comment-padding-inline); + word-wrap: break-word; } .comment__edit { diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index dd2e8dc80..849540e5b 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -11,9 +11,8 @@ --column-gap: clamp(var(--inline-space), 3cqmin, calc(var(--inline-space) * 3)); --grid-lines: 0.1rem; - display: grid; - grid-template-columns: 1fr 5fr 1fr; margin: auto; + max-inline-size: min(105ch, 100%); } .events__day-header { @@ -61,16 +60,7 @@ } .events__popup { - .panel:is(&) { - max-block-size: calc(100dvh - (2 * var(--block-space-double))); - max-inline-size: calc(100dvw - (2 * var(--block-space-double))); - } - - #header:has(&) { - max-inline-size: 100%; - position: relative; - z-index: 5; - } + margin-block-start: var(--block-space-double); } .event { @@ -93,6 +83,12 @@ z-index: 2; } + .event__collection { + font-size: var(--text-x-small); + font-weight: 500; + opacity: 0.66; + } + .event__grid-item { background-color: var(--color-container); block-size: 100%; diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index 612a1b4a1..139a3a8ce 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -107,3 +107,11 @@ } } } + +.filter--active li { + display: none; + + &.selected { + display: flex; + } +} diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css index 810fc7b17..4ab37ba0e 100644 --- a/app/assets/stylesheets/house.css +++ b/app/assets/stylesheets/house.css @@ -1,6 +1,8 @@ @layer components { /* Editor */ house-md { + --placeholder: attr(placeholder); + display: flex; flex-direction: column; flex-grow: 1; @@ -58,7 +60,6 @@ /* Markdown Content */ .house-md-content { - caret-color: var(--color-link); flex-grow: 1; font-family: var(--font-sans); text-align: left; @@ -66,8 +67,8 @@ padding: 1rem; &.house-md-content-empty::before { - content: attr(placeholder); - color: var(--color-ink); + content: var(--placeholder); + color: currentColor; opacity: 0.5; pointer-events: none; display: block; diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css index 341d9a059..e8d396a59 100644 --- a/app/assets/stylesheets/inputs.css +++ b/app/assets/stylesheets/inputs.css @@ -94,6 +94,11 @@ @media (prefers-color-scheme: dark) { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 19.5c-.7 0-1.3-.3-1.7-.8l-9.8-11.1c-.7-.8-.6-1.9.2-2.6.8-.6 1.9-.6 2.5.2l8.6 9.8c0 .1.2.1.4 0l8.6-9.8c.7-.8 1.8-.9 2.6-.2s.9 1.8.2 2.6l-9.8 11.1c-.4.5-1.1.8-1.7.8z' fill='%23fff'/%3E%3C/svg%3E"); } + + option { + background-color: var(--color-bg); + color: var(--color-ink); + } } .input--textarea { diff --git a/app/assets/stylesheets/comment-content.css b/app/assets/stylesheets/markdown-content.css similarity index 98% rename from app/assets/stylesheets/comment-content.css rename to app/assets/stylesheets/markdown-content.css index a11b8dbf8..7a67ec288 100644 --- a/app/assets/stylesheets/comment-content.css +++ b/app/assets/stylesheets/markdown-content.css @@ -1,5 +1,5 @@ @layer components { - .comments { + .markdown-content { :where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) { font-weight: 800; hyphens: auto; diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index a1bb0f911..43dd95d5e 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -94,3 +94,17 @@ } } } + +.notifications__empty-message { + .notificiations-list:has(.notification) & { + display: none; + } +} + +.notificiations-list--read { + display: none; + + &:has(.notification) { + display: flex; + } +} diff --git a/app/assets/stylesheets/panels.css b/app/assets/stylesheets/panels.css index 6ce5e10b6..19609aced 100644 --- a/app/assets/stylesheets/panels.css +++ b/app/assets/stylesheets/panels.css @@ -8,4 +8,20 @@ max-inline-size: 100%; padding: var(--panel-padding, calc(var(--block-space) * 2)); } + + .panels--two-up { + --panel-size: auto; + + display: flex; + flex-wrap: wrap; + gap: var(--block-space-double); + align-content: start; + max-inline-size: min(100ch, 100%); + margin: auto; + + .panel { + flex: 1 1 33%; + min-inline-size: 36ch; + } + } } diff --git a/app/assets/stylesheets/pins.css b/app/assets/stylesheets/pins.css index b8e44d7e2..8756e2391 100644 --- a/app/assets/stylesheets/pins.css +++ b/app/assets/stylesheets/pins.css @@ -31,6 +31,7 @@ --text-xx-large: 2em; border: 1px solid var(--color-canvas); + view-transition-name: unset !important; } .card__collection { @@ -42,16 +43,18 @@ padding-block-end: 0; } + .card__link { + z-index: 1; + } + .card__title { + --lines: 1; + font-size: var(--text-small); font-weight: bold; min-block-size: 0; } - .card__title-link { - --lines: 1; - } - .card__stages { padding: 0; } diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index a9bccdf4d..3013ac534 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -1,19 +1,42 @@ @layer components { .popup { --panel-border-radius: 0.5em; - --panel-padding: 0.4em 0.5em 0.5em; + --panel-padding: var(--block-space); --panel-size: auto; inline-size: auto; - inset: 0 auto auto 0; - min-inline-size: 20ch; - max-inline-size: 40ch; + inset: 0 auto auto 50%; + min-inline-size: min(25ch, 90vw); + max-inline-size: min(50ch, 90vw); position: absolute; + transform: translateX(-50%); z-index: 2; &[open] { display: flex; } + + #header:has(&) { + max-inline-size: 100%; + position: relative; + z-index: 5; + } + } + + .popup__group { + background: var(--color-subtle-light); + max-inline-size: 100%; + padding-inline: var(--inline-space); + + .btn { + --btn-background: transparent; + } + + @media (any-hover: hover) { + &:hover { + background: var(--color-selected); + } + } } .popup__item { @@ -25,17 +48,10 @@ --hover-size: 0; --outline-color: transparent; + flex: 1 1 auto; justify-content: start; - - &::after { - border-radius: var(--btn-border-radius); - border: var(--btn-border-size) solid var(--btn-border-color, var(--color-ink-lighter)); - content: ""; - display: block; - inline-size: calc(100% - var(--btn-padding) * 2); - block-size: calc(100% - var(--btn-padding) * 2); - - } + max-inline-size: 100%; + text-align: start; @media (any-hover: hover) { &:where(:not(:active):hover) { @@ -75,10 +91,67 @@ } .popup__list { + max-block-size: 60dvh; + overflow: auto; row-gap: 2px; } + .popup__new { + --btn-icon-size: 1em; + + .icon { + margin-inline-end: 0.5em; + } + + padding-inline-start: var(--inline-space-half); + } + .popup__title { font-weight: 800; } } + +.popup--animated { + --backdrop-speed: 150ms; + --panel-size: max-content; + --speed: 150ms; + + opacity: 0; + transform: scale(0.2) translateX(-50%); + transform-origin: top left; + transition: + display var(--speed) allow-discrete, + opacity var(--speed), + overlay var(--speed) allow-discrete, + transform var(--speed); + + &::backdrop { + background-color: var(--color-always-black); + opacity: 0; + transform: scale(1); + transition: + display var(--backdrop-speed) allow-discrete, + opacity var(--backdrop-speed), + overlay var(--backdrop-speed) allow-discrete; + } + + &[open] { + opacity: 1; + transform: scale(1) translateX(-50%); + + &::backdrop { + opacity: 0.5; + } + } + + @starting-style { + &[open] { + opacity: 0; + transform: scale(0.2) translateX(-50%); + } + + &[open]::backdrop { + opacity: 0; + } + } +} diff --git a/app/assets/stylesheets/tags.css b/app/assets/stylesheets/tags.css index 2b667e771..90d51e011 100644 --- a/app/assets/stylesheets/tags.css +++ b/app/assets/stylesheets/tags.css @@ -22,8 +22,9 @@ } .tag-picker__button { - font-size: 0.7em; - margin-inline-start: 0.5em; + font-size: 0.6em; + margin-block-start: 0.1em; + margin-inline-start: 0.2em; + .panel { --panel-size: 18ch; diff --git a/app/assets/stylesheets/users.css b/app/assets/stylesheets/users.css new file mode 100644 index 000000000..e3e427779 --- /dev/null +++ b/app/assets/stylesheets/users.css @@ -0,0 +1,4 @@ +.users-list { + max-block-size: 40dvh; + overflow: auto; +} diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 94a2badd7..a07d98422 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -23,6 +23,7 @@ .txt-nowrap { white-space: nowrap; } .txt-break { word-break: break-word; } .txt-uppercase { text-transform: uppercase; } + .txt-capitalize { text-transform: capitalize; } .txt-link { color: var(--color-link); text-decoration: underline; } /* Flexbox and Grid */ @@ -107,6 +108,7 @@ .pad-inline-double { padding-inline: var(--inline-space-double); } .unpad { padding: 0; } + .unpad-block-end { padding-block-end: 0; } .unpad-inline { padding-inline: 0; } /* Margins */ diff --git a/app/assets/stylesheets/workflows.css b/app/assets/stylesheets/workflows.css index ce36985de..f69b7ff7c 100644 --- a/app/assets/stylesheets/workflows.css +++ b/app/assets/stylesheets/workflows.css @@ -27,13 +27,42 @@ background-color: color(from var(--card-color) srgb r g b / 0.65); } } + } - .ticket & { - --btn-background: var(--color-ink); + .workflow-preview { + --border-size: 3px; - background: var(--card-color); - border-radius: 0.2em; - color: var(--color-ink-inverted); + aspect-ratio: 1; + flex: 1 1 30%; + max-inline-size: 33%; + padding: 0.5em 1em; + + button { + --hover-size: 0; + + appearance: none; + background: transparent; + border: none; + cursor: pointer; + inline-size: auto; + margin: 0; + max-inline-size: 100%; + outline: 0; + padding: 0; + text-align: left; } } + + .workflow-preview--selected { + --border-color: var(--color-link); + } + + .workflow-preview__swatch { + --btn-size: 1em; + --btn-border-radius: 50%; + --btn-border-size: 0; + --hover-size: 0; + + cursor: auto; + } } diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index 63dce613c..0dd3d460e 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -6,5 +6,6 @@ class Cards::AssignmentsController < ApplicationController def create @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) + render_card_replacement end end diff --git a/app/controllers/cards/closures_controller.rb b/app/controllers/cards/closures_controller.rb index 3682552ee..29dcfd30c 100644 --- a/app/controllers/cards/closures_controller.rb +++ b/app/controllers/cards/closures_controller.rb @@ -3,11 +3,11 @@ class Cards::ClosuresController < ApplicationController def create @card.close(user: Current.user, reason: params[:reason]) - redirect_to @card + render_card_replacement end def destroy @card.reopen - redirect_to @card + render_card_replacement end end diff --git a/app/controllers/cards/engagements_controller.rb b/app/controllers/cards/engagements_controller.rb index 72025d16a..50a6daa83 100644 --- a/app/controllers/cards/engagements_controller.rb +++ b/app/controllers/cards/engagements_controller.rb @@ -3,11 +3,11 @@ class Cards::EngagementsController < ApplicationController def create @card.engage - redirect_to @card + render_card_replacement end def destroy @card.reconsider - redirect_to @card + render_card_replacement end end diff --git a/app/controllers/cards/goldnesses_controller.rb b/app/controllers/cards/goldnesses_controller.rb index 4be611d14..1a0912a24 100644 --- a/app/controllers/cards/goldnesses_controller.rb +++ b/app/controllers/cards/goldnesses_controller.rb @@ -3,11 +3,11 @@ class Cards::GoldnessesController < ApplicationController def create @card.gild - redirect_to @card + render_card_replacement end def destroy @card.ungild - redirect_to @card + render_card_replacement end end diff --git a/app/controllers/cards/images_controller.rb b/app/controllers/cards/images_controller.rb index fad419c52..f5a3f02b6 100644 --- a/app/controllers/cards/images_controller.rb +++ b/app/controllers/cards/images_controller.rb @@ -3,6 +3,6 @@ class Cards::ImagesController < ApplicationController def destroy @card.image.purge_later - redirect_to @card + render_card_replacement end end diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index 76c5aa723..6ac118543 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -5,25 +5,29 @@ class Cards::PinsController < ApplicationController end def create - pin = @card.pin_by Current.user + @pin = @card.pin_by Current.user - broadcast_my_new pin - redirect_to card_pin_path(@card) + broadcast_add_pin_to_tray + render_pin_button_replacement end def destroy - pin = @card.unpin_by Current.user + @pin = @card.unpin_by Current.user - broadcast_my_removed pin - redirect_to card_pin_path(@card) + broadcast_remove_pin_from_tray + render_pin_button_replacement end private - def broadcast_my_new(pin) - pin.card.broadcast_prepend_later_to [ Current.user, :pins ], target: "pins", partial: "cards/display/preview" + def broadcast_add_pin_to_tray + @pin.broadcast_prepend_to [ Current.user, :pins_tray ], target: "pins", partial: "my/pins/pin" end - def broadcast_my_removed(pin) - pin.broadcast_remove_to [ Current.user, :pins ] + def broadcast_remove_pin_from_tray + @pin.broadcast_remove_to [ Current.user, :pins_tray ] + end + + def render_pin_button_replacement + render turbo_stream: turbo_stream.replace([ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card }) end end diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index bbc4d53c0..2334cb8f5 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -2,7 +2,6 @@ class Cards::ReadingsController < ApplicationController include CardScoped def create - @notification = Current.user.notifications.find_by(card: @card) - @notification.read + @notifications = @card.read_by(Current.user) end end diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb index 071d19f5a..714427ecf 100644 --- a/app/controllers/cards/stagings_controller.rb +++ b/app/controllers/cards/stagings_controller.rb @@ -1,18 +1,8 @@ class Cards::StagingsController < ApplicationController include CardScoped - before_action :set_stage - - def create - if @stage - @card.toggle_stage @stage - else - @card.update!(stage: nil) - end + def update + @card.change_stage_to @collection.workflow.stages.find(params[:stage_id]) + render_card_replacement end - - private - def set_stage - @stage = Workflow::Stage.find_by(id: params[:stage_id]) - end end diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index 710b59fdd..a8fe7ecf9 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -7,6 +7,7 @@ class Cards::TaggingsController < ApplicationController def create @card.toggle_tag_with sanitized_tag_title_param + render_card_replacement end private diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e62146d4e..d535f26d9 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -25,16 +25,21 @@ class CardsController < ApplicationController def edit end + def update + @card.update! card_params + + if @card.published? + render_card_replacement + else + redirect_to @card + end + end + def destroy @card.destroy! redirect_to cards_path(collection_ids: [ @card.collection ]), notice: ("Card deleted" unless @card.creating?) end - def update - @card.update! card_params - redirect_to @card - end - private def set_card @card = @collection.cards.find params[:id] @@ -49,6 +54,10 @@ class CardsController < ApplicationController end def card_params - params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) + params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ]) + end + + def render_card_replacement + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) end end diff --git a/app/controllers/collections/workflows_controller.rb b/app/controllers/collections/workflows_controller.rb index ae50b90f9..28b691f11 100644 --- a/app/controllers/collections/workflows_controller.rb +++ b/app/controllers/collections/workflows_controller.rb @@ -10,6 +10,6 @@ class Collections::WorkflowsController < ApplicationController private def set_workflow - @workflow = Workflow.find(params.expect(collection: [ :workflow_id ]).require(:workflow_id)) + @workflow = Workflow.find(params[:collection][:workflow_id]) end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 36698afdf..f5eb8c9fc 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -6,7 +6,7 @@ class CollectionsController < ApplicationController end def create - @collection = Collection.create! collection_params + @collection = Collection.create! collection_params.with_defaults(all_access: true) redirect_to cards_path(collection_ids: [ @collection ]) end @@ -19,7 +19,7 @@ class CollectionsController < ApplicationController @collection.update! collection_params @collection.accesses.revise granted: grantees, revoked: revokees - redirect_to cards_path(collection_ids: [ @collection ]) + redirect_to edit_collection_path(@collection), notice: "Collection updated" end def destroy @@ -33,7 +33,7 @@ class CollectionsController < ApplicationController end def collection_params - params.expect(collection: [ :name, :all_access ]).with_defaults(all_access: true) + params.expect(collection: [ :name, :all_access, :auto_close_period ]) end def grantees diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index ef40d56e3..5438871c1 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -13,4 +13,8 @@ module CardScoped def set_collection @collection = @card.collection end + + def render_card_replacement + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) + end end diff --git a/app/controllers/concerns/collection_filterable.rb b/app/controllers/concerns/collection_filterable.rb deleted file mode 100644 index 4bd5ff9f1..000000000 --- a/app/controllers/concerns/collection_filterable.rb +++ /dev/null @@ -1,24 +0,0 @@ -module CollectionFilterable - extend ActiveSupport::Concern - - included do - before_action :set_collection_filter - end - - private - def set_collection_filter - params[:collection_ids] ||= cookies[:collection_filter]&.split(",") unless params[:clear_filter] - end - - def collection_filter - params[:collection_ids].presence || Current.user.collection_ids - end - - def update_collection_filter - if params[:clear_filter] - cookies.delete(:collection_filter) - elsif params[:collection_ids].present? - cookies[:collection_filter] = params[:collection_ids].join(",") - end - end -end diff --git a/app/controllers/concerns/day_timelines_scoped.rb b/app/controllers/concerns/day_timelines_scoped.rb new file mode 100644 index 000000000..d7bf80a5a --- /dev/null +++ b/app/controllers/concerns/day_timelines_scoped.rb @@ -0,0 +1,48 @@ +module DayTimelinesScoped + extend ActiveSupport::Concern + + included do + include FilterScoped + + before_action :restore_collections_filter_from_cookie + before_action :set_day_timeline + after_action :save_collections_filter_to_cookie + end + + private + def restore_collections_filter_from_cookie + if params[:clear_filter] + delete_collections_filter_cookie + else + set_collections_filter_from_cookie + end + end + + def delete_collections_filter_cookie + cookies.delete(:collection_filter) + end + + def set_collections_filter_from_cookie + if cookies[:collection_filter].present? && @filter.collections.blank? + @filter.collection_ids = cookies[:collection_filter].split(",") + end + end + + def save_collections_filter_to_cookie + cookies[:collection_filter] = @filter.collection_ids.join(",") + end + + def set_day_timeline + @day_timeline = Current.user.timeline_for(day, filter: @filter) + end + + def day + if params[:day].present? + Time.zone.parse(params[:day]) + else + Time.current + end + rescue ArgumentError + head :not_found + end +end diff --git a/app/controllers/concerns/events_timeline.rb b/app/controllers/concerns/events_timeline.rb deleted file mode 100644 index 0d50d2093..000000000 --- a/app/controllers/concerns/events_timeline.rb +++ /dev/null @@ -1,40 +0,0 @@ -module EventsTimeline - extend ActiveSupport::Concern - - include CollectionFilterable - - included do - before_action :set_activity_day - end - - private - def set_activity_day - @activity_day = if params[:day].present? - Time.zone.parse(params[:day]) - else - Time.zone.now - end - rescue ArgumentError - raise ActionController::RoutingError - end - - def events_for_activity_day - user_events.where(created_at: @activity_day.all_day) - .group_by { |event| [ event.created_at.hour, helpers.event_column(event) ] } - .map { |hour_col, events| [ hour_col, events.uniq { |e| e.id } ] } - end - - def latest_event_before_activity_day - user_events.where(created_at: ...@activity_day.beginning_of_day).chronologically.last - end - - def user_events - Event.where(card: user_cards) - end - - def user_cards - Current.user.accessible_cards - .published_or_drafted_by(Current.user) - .where(collection_id: collection_filter) - end -end diff --git a/app/controllers/events/days_controller.rb b/app/controllers/events/days_controller.rb index c2966357d..31ec80d39 100644 --- a/app/controllers/events/days_controller.rb +++ b/app/controllers/events/days_controller.rb @@ -1,8 +1,6 @@ class Events::DaysController < ApplicationController - include EventsTimeline + include DayTimelinesScoped def index - @events = events_for_activity_day - @next_day = latest_event_before_activity_day&.created_at end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index a3c8a9157..1340def1d 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,13 +1,6 @@ class EventsController < ApplicationController - include EventsTimeline + include DayTimelinesScoped def index - update_collection_filter - - @collections = Current.user.collections.alphabetically - @filters = Current.user.filters.all - - @events = events_for_activity_day - @next_day = latest_event_before_activity_day&.created_at end end diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index 4a01a1ee9..7c7a52bb3 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -1,5 +1,5 @@ class Notifications::TraysController < ApplicationController def show - @notifications = Current.user.notifications.unread.ordered.limit(20) + @notifications = Current.user.notifications.unread.ordered.limit(100) end end diff --git a/app/controllers/terminals_controller.rb b/app/controllers/terminals_controller.rb index 728ff9983..552d285cd 100644 --- a/app/controllers/terminals_controller.rb +++ b/app/controllers/terminals_controller.rb @@ -1,6 +1,6 @@ class TerminalsController < ApplicationController def show - @events = Event.where(card: Current.user.accessible_cards, creator: Current.user).chronologically.reverse_order.limit(20) + @events = Current.user.accessible_events.chronologically.reverse_order.limit(20) end def edit diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 49abcd26f..f8b54254d 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -11,8 +11,6 @@ class WorkflowsController < ApplicationController def create @workflow = Workflow.create! workflow_params - # FIXME: this should definitely change. - [ "Triage", "In progress", "On Hold", "Review" ].each { |name| @workflow.stages.create! name: name } redirect_to workflows_path end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index a139ac3d4..8715b8165 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -1,6 +1,6 @@ module AccessesHelper def access_menu_tag(collection, &) - tag.menu class: [ "flex flex-column gap margin-none pad txt-medium", { "toggler--toggled": collection.all_access? } ], data: { + tag.menu class: [ "flex flex-column gap-half unpad margin-none txt-medium", { "toggler--toggled": collection.all_access? } ], data: { controller: "filter toggle-class", filter_active_class: "filter--active", filter_selected_class: "selected", toggle_class_toggle_class: "toggler--toggled" }, & diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index e13defa96..79338d622 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -15,11 +15,12 @@ module CardsHelper **options end - def card_article_tag(card, id: dom_id(card, :ticket), **options, &block) + def card_article_tag(card, id: dom_id(card, :article), **options, &block) classes = [ options.delete(:class), ("card--golden" if card.golden?), - ("card--doing" if card.doing?) + ("card--doing" if card.doing?), + ("card--drafted" if card.drafted?) ].compact.join(" ") tag.article \ @@ -29,4 +30,12 @@ module CardsHelper **options, &block end + + def button_to_delete_card(card) + button_to collection_card_path(card.collection, card), + method: :delete, class: "btn txt-negative borderless txt-small", data: { turbo_frame: "_top", turbo_confirm: "Are you sure you want to permanently delete this card?" } do + concat(icon_tag("trash")) + concat(tag.span("Delete this card")) + end + end end diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb new file mode 100644 index 000000000..5f872079c --- /dev/null +++ b/app/helpers/collections_helper.rb @@ -0,0 +1,12 @@ +module CollectionsHelper + def collection_auto_close_options + [ + [ "30 days", 30.days ], + [ "60 days", 60.days ], + [ "90 days", 90.days ], + [ "6 months", 180.days ], + [ "1 year", 365.days ], + [ "Never", nil ] + ] + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 53732b327..b00408cc3 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,20 +1,9 @@ module EventsHelper - def event_day_title(day) - case - when day.today? - "Today" - when day.yesterday? - "Yesterday" - else - day.strftime("%A, %B %e") - end - end - def event_column(event) case event.action - when "closed" + when "card_closed" 3 - when "published" + when "card_published" 1 else 2 @@ -33,7 +22,7 @@ module EventsHelper end end - def render_event_grid_cells(day, columns: 4, rows: 24) + def render_event_grid_cells(columns: 4, rows: 24) safe_join((2..rows + 1).map do |row| (1..columns).map do |col| tag.div class: class_names("event__grid-item"), style: "grid-area: #{row}/#{col};" @@ -41,19 +30,11 @@ module EventsHelper end.flatten) end - def render_column_headers(day = Date.current) - start_time = day.beginning_of_day - end_time = day.end_of_day - - accessible_events = Event.joins(card: :collection) - .merge(Current.user.collections) - .where(created_at: start_time..end_time) - .where(cards: { collection_id: params[:collection_ids].presence || Current.user.collection_ids }) - + def render_column_headers(day_timeline) headers = { - "Added" => accessible_events.where(action: "published").count, + "Added" => day_timeline.events.where(action: "card_published").count, "Updated" => nil, - "Closed" => accessible_events.where(action: "closed").count + "Closed" => day_timeline.events.where(action: "card_closed").count } headers.map do |header, count| @@ -63,47 +44,64 @@ module EventsHelper end def event_action_sentence(event) + if event.action.comment_created? + comment_event_action_sentence(event) + else + card_event_action_sentence(event) + end + end + + def comment_event_action_sentence(event) + "#{ event_creator_name(event) } commented on #{ event.eventable.card.title }".html_safe + end + + def event_creator_name(event) + event.creator == Current.user ? "You" : event.creator.name + end + + def card_event_action_sentence(event) + card = event.eventable + title = card.title + case event.action - when "assigned" + when "card_assigned" if event.assignees.include?(Current.user) - "#{ event.creator == Current.user ? "You" : event.creator.name } will handle #{ event.card.title }".html_safe + "#{ event_creator_name(event) } will handle #{ title }".html_safe else - "#{ event.creator == Current.user ? "You" : event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to #{ event.card.title }".html_safe + "#{ event_creator_name(event) } assigned #{ event.assignees.pluck(:name).to_sentence } to #{ title }".html_safe end - when "unassigned" - "#{ event.creator == Current.user ? "You" : event.creator.name } unassigned #{ event.assignees.include?(Current.user) ? "yourself" : event.assignees.pluck(:name).to_sentence } from #{ event.card.title }".html_safe - when "commented" - "#{ event.creator == Current.user ? "You" : event.creator.name } commented on #{ event.card.title }".html_safe - when "published" - "#{ event.creator == Current.user ? "You" : event.creator.name } added #{ event.card.title }".html_safe - when "closed" - "#{ event.creator == Current.user ? "You" : event.creator.name } closed #{ event.card.title }".html_safe - when "staged" - "#{event.creator == Current.user ? "You" : event.creator.name} moved #{ event.card.title } to the #{event.stage_name} stage".html_safe - when "unstaged" - "#{event.creator == Current.user ? "You" : event.creator.name} moved #{ event.card.title } out ofthe #{event.stage_name} stage".html_safe - when "due_date_added" - "#{event.creator == Current.user ? "You" : event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.card.title }".html_safe - when "due_date_changed" - "#{event.creator == Current.user ? "You" : event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ event.card.title }".html_safe - when "due_date_removed" - "#{event.creator == Current.user ? "You" : event.creator.name} removed the date on #{ event.card.title }" - when "title_changed" - "#{event.creator == Current.user ? "You" : event.creator.name} renamed #{ event.card.title } (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe + when "card_unassigned" + "#{ event_creator_name(event) } unassigned #{ event.assignees.include?(Current.user) ? "yourself" : event.assignees.pluck(:name).to_sentence } from #{ title }".html_safe + when "card_published" + "#{ event_creator_name(event) } added #{ title }".html_safe + when "card_closed" + "#{ event_creator_name(event) } closed #{ title }".html_safe + when "card_staged" + "#{event_creator_name(event)} moved #{ title } to the #{event.stage_name} stage".html_safe + when "card_unstaged" + "#{event_creator_name(event)} moved #{ title } out ofthe #{event.stage_name} stage".html_safe + when "card_due_date_added" + "#{event_creator_name(event)} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ title }".html_safe + when "card_due_date_changed" + "#{event_creator_name(event)} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on #{ title }".html_safe + when "card_due_date_removed" + "#{event_creator_name(event)} removed the date on #{ title }" + when "card_title_changed" + "#{event_creator_name(event)} renamed #{ title } (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe end end def event_action_icon(event) case event.action - when "assigned" + when "card_assigned" "assigned" - when "staged" + when "card_staged" "bolt" - when "unstaged" + when "card_unstaged" "bolt" - when "commented" + when "comment_created" "comment" - when "title_changed" + when "card_title_changed" "rename" else "person" diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index c2c1d0dc5..aae65bf6b 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -21,9 +21,19 @@ module FiltersHelper hidden_field_tag name, value, id: nil end + def filter_selected_collections_label(filter) + selected_collections = if filter.collections.any? + filter.collections.collect { "#{it.name}" }.uniq.sort.to_sentence + else + "all collections" + end + + "Activity in #{selected_collections}".html_safe + end + def any_filters?(filter) filter.tags.any? || filter.assignees.any? || filter.creators.any? || - filter.stages.any? || filter.terms.any? || - filter.assignment_status.unassigned? || !filter.default_indexed_by? + filter.stages.any? || filter.terms.any? || + filter.assignment_status.unassigned? || !filter.default_indexed_by? end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 656edad54..3c36cc51a 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,20 +1,19 @@ module NotificationsHelper - def notification_title(notification) - if notification.resource.is_a? Comment - "RE: " + notification.card.title - elsif notification_event_action(notification) == "assigned" - "Assigned to you: " + notification.card.title - else - notification.card.title + def event_notification_title(event) + case event_notification_action(event) + when "comment_created" then "RE: " + event.eventable.card.title + when "card_assigned" then "Assigned to you: " + event.eventable.title + else event.eventable.title end end - def notification_body(notification) - name = notification.creator.name + def event_notification_body(event) + name = event.creator.name - case notification_event_action(notification) - when "closed" then "Closed by #{name}" - when "published" then "Added by #{name}" + case event_notification_action(event) + when "card_closed" then "Closed by #{name}" + when "card_published" then "Added by #{name}" + when "comment_created" then comment_notification_body(event) else name end end @@ -22,7 +21,7 @@ module NotificationsHelper def notification_tag(notification, &) tag.div id: dom_id(notification), class: "notification tray__item border-radius txt-normal" do concat( - link_to(notification.resource, + link_to(notification, class: "notification__content border-radius shadow fill-white flex align-start txt-align-start gap flex-item-grow max-width border txt-ink", data: { action: "click->dialog#close", turbo_frame: "_top" }, &) @@ -33,11 +32,11 @@ module NotificationsHelper def notification_mark_read_button(notification) button_to read_notification_path(notification), - class: "notification__unread_indicator btn borderless", - title: "Mark as read", - data: { turbo_frame: "_top" } do - concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true })) - concat(tag.span("Mark as read", class: "for-screen-reader")) + class: "notification__unread_indicator btn borderless", + title: "Mark as read", + data: { turbo_frame: "_top" } do + concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true })) + concat(tag.span("Mark as read", class: "for-screen-reader")) end end @@ -48,15 +47,16 @@ module NotificationsHelper end private - def notification_event_action(notification) - if notification_is_for_initial_assignement?(notification) - "assigned" + def event_notification_action(event) + if event.action.card_published? && event.eventable.assigned_to?(event.creator) + "card_assigned" else - notification.event.action + event.action end end - def notification_is_for_initial_assignement?(notification) - notification.event.action == "published" && notification.card.assigned_to?(notification.user) + def comment_notification_body(event) + comment = event.eventable + "#{strip_tags(comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(comment.body_html).truncate(200)}" end end diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index b78482c9f..560e1aebf 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -1,16 +1,15 @@ module WorkflowsHelper def button_to_set_stage(card, stage) - button_to card_stagings_path(card, stage_id: stage), - method: :post, class: [ "btn justify-start workflow-stage txt-uppercase workflow-stage", { "workflow-stage--current": stage == card.stage } ], - form_class: "flex align-center gap-half", - data: { turbo_frame: "_top" } do - tag.span class: "overflow-ellipsis" do - stage.name - end - end + button_to \ + tag.span(stage.name, class: "overflow-ellipsis"), + card_staging_path(card, stage_id: stage), + method: :put, + class: [ "btn justify-start workflow-stage txt-uppercase workflow-stage", { "workflow-stage--current": stage == card.stage } ], + form_class: "flex align-center gap-half", + data: { turbo_frame: "_top" } end def stage_color(stage) - stage.color.presence || Colorable::DEFAULT_COLOR + stage.color.presence || Card::DEFAULT_COLOR end end diff --git a/app/javascript/controllers/animation_controller.js b/app/javascript/controllers/animation_controller.js deleted file mode 100644 index f63d36e97..000000000 --- a/app/javascript/controllers/animation_controller.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Controller } from "@hotwired/stimulus" -import { nextFrame } from "helpers/timing_helpers" - -export default class extends Controller { - static classes = [ "play" ] - static values = { playOnLoad: { type: Boolean, default: false } } - - connect() { - if (this.playOnLoadValue) { - this.play() - } - } - - async play() { - await nextFrame() - this.element.classList.remove(this.playClass) - this.#forceReflow() - this.element.classList.add(this.playClass) - } - - #forceReflow() { - this.element.offsetWidth - } -} diff --git a/app/javascript/controllers/autofocus_controller.js b/app/javascript/controllers/autofocus_controller.js deleted file mode 100644 index 75e5c4de9..000000000 --- a/app/javascript/controllers/autofocus_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = [ "element" ] - - connect() { - this.observer = new IntersectionObserver(([entry]) => { - if (entry.isIntersecting) this.elementTarget.focus() - }) - this.observer.observe(this.elementTarget) - } - - disconnect() { - this.observer.disconnect() - } -} diff --git a/app/javascript/controllers/card_size_controller.js b/app/javascript/controllers/card_size_controller.js deleted file mode 100644 index e688358da..000000000 --- a/app/javascript/controllers/card_size_controller.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -const SIZES = [ "one", "two", "three", "four", "five" ] - -export default class extends Controller { - static targets = [ "card" ] - - connect() { - this.resize() - } - - resize() { - const [ min, max ] = this.#getScoreRange() - - this.cardTargets.forEach(card => { - const score = this.#currentCardScore(card) - const idx = Math.round((score - min) / (max - min) * (SIZES.length - 1)) - - card.style.setProperty("--card-size", `var(--card-size-${SIZES[idx]})`) - }) - } - - #getScoreRange() { - var min = 0, max = 1; - - this.cardTargets.forEach(card => { - const score = this.#currentCardScore(card) - - min = Math.min(min, score) - max = Math.max(max, score) - }) - - return [ min, max ] - } - - #currentCardScore(el) { - const score = el.dataset.activityScore - const scoreAt = el.dataset.activityScoreAt - const daysAgo = (Date.now() / 1000 - scoreAt) / (60 * 60 * 24) - - return score / (2**daysAgo) - } -} diff --git a/app/javascript/controllers/local_save_controller.js b/app/javascript/controllers/local_save_controller.js index c0c5892aa..5d9b89a72 100644 --- a/app/javascript/controllers/local_save_controller.js +++ b/app/javascript/controllers/local_save_controller.js @@ -47,7 +47,7 @@ export default class extends Controller { #triggerChangeEvent(newValue) { if (this.inputTarget.tagName === "HOUSE-MD") { this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', { - cards: true, + bubbles: true, detail: { previousContent: '', newContent: newValue diff --git a/app/javascript/controllers/local_time_controller.js b/app/javascript/controllers/local_time_controller.js index 3bc77b769..7fe903c85 100644 --- a/app/javascript/controllers/local_time_controller.js +++ b/app/javascript/controllers/local_time_controller.js @@ -2,7 +2,9 @@ import { Controller } from "@hotwired/stimulus" import { differenceInDays } from "helpers/date_helpers" export default class extends Controller { - static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago" ] + static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago", "agoorweekday" ] + static values = { refreshInterval: Number } + static classes = [ "local-time-value"] #timer @@ -13,7 +15,10 @@ export default class extends Controller { this.dateTimeFormatter = new Intl.DateTimeFormat(undefined, { timeStyle: "short", dateStyle: "short" }) this.agoFormatter = new AgoFormatter() this.daysagoFormatter = new DaysAgoFormatter() + this.datewithweekdayFormatter = new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric" }) + this.datewithweekdayFormatter = new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric" }) this.indaysFormatter = new InDaysFormatter() + this.agoorweekdayFormatter = new DaysAgoOrWeekdayFormatter() } connect() { @@ -60,6 +65,10 @@ export default class extends Controller { this.#formatTime(this.daysagoFormatter, target) } + agoorweekdayTargetConnected(target) { + this.#formatTime(this.agoorweekdayFormatter, target) + } + #refreshRelativeTimes() { this.agoTargets.forEach(target => { this.#formatTime(this.agoFormatter, target) @@ -111,6 +120,18 @@ class DaysAgoFormatter { } } +class DaysAgoOrWeekdayFormatter { + format(date) { + const days = differenceInDays(date, new Date()) + + if (days <= 1) { + return new DaysAgoFormatter().format(date) + } else { + return new Intl.DateTimeFormat(undefined, { weekday: "long", month: "long", day: "numeric" }).format(date) + } + } +} + class InDaysFormatter { format(date) { const days = differenceInDays(new Date(), date) diff --git a/app/javascript/controllers/paste_controller.js b/app/javascript/controllers/paste_controller.js index 41fc77d2c..79db7d7db 100644 --- a/app/javascript/controllers/paste_controller.js +++ b/app/javascript/controllers/paste_controller.js @@ -12,7 +12,7 @@ export default class extends Controller { for (const file of files) { const uploadEvent = new CustomEvent("house-md:before-upload", { - cards: true, + bubbles: true, detail: { file }, cancelable: true }) diff --git a/app/javascript/controllers/soft_keyboard_controller.js b/app/javascript/controllers/soft_keyboard_controller.js index becc36c52..75fb4f2ee 100644 --- a/app/javascript/controllers/soft_keyboard_controller.js +++ b/app/javascript/controllers/soft_keyboard_controller.js @@ -1,10 +1,10 @@ import { Controller } from "@hotwired/stimulus" import { nextEventNamed } from "helpers/timing_helpers" -import { isTouchDevice } from "helpers/navigator_helpers" export default class extends Controller { + // Only load for touch devices static get shouldLoad() { - return isTouchDevice() + return "ontouchstart" in window && navigator.maxTouchPoints > 0 } // Use a fake input to trigger the soft keyboard on actions that load async content diff --git a/app/javascript/controllers/toggle_class_controller.js b/app/javascript/controllers/toggle_class_controller.js index 694106ea2..ca29ad771 100644 --- a/app/javascript/controllers/toggle_class_controller.js +++ b/app/javascript/controllers/toggle_class_controller.js @@ -6,4 +6,12 @@ export default class extends Controller { toggle() { this.element.classList.toggle(this.toggleClass) } + + add() { + this.element.classList.add(this.toggleClass) + } + + remove() { + this.element.classList.remove(this.toggleClass) + } } diff --git a/app/javascript/helpers/navigator_helpers.js b/app/javascript/helpers/navigator_helpers.js deleted file mode 100644 index b75f62875..000000000 --- a/app/javascript/helpers/navigator_helpers.js +++ /dev/null @@ -1,3 +0,0 @@ -export function isTouchDevice() { - return "ontouchstart" in window && navigator.maxTouchPoints > 0 -} diff --git a/app/javascript/initializers/custom_stream_actions.js b/app/javascript/initializers/custom_stream_actions.js deleted file mode 100644 index fecb25ff5..000000000 --- a/app/javascript/initializers/custom_stream_actions.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Turbo } from "@hotwired/turbo-rails" - -Turbo.StreamActions.set_css_variable = function() { - const name = this.getAttribute("name") - const value = this.getAttribute("value") - - this.targetElements.forEach(element => element.style.setProperty(name, value)) -} diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js index 01fdd9220..10fb36975 100644 --- a/app/javascript/initializers/index.js +++ b/app/javascript/initializers/index.js @@ -1,2 +1 @@ import "initializers/current" -import "initializers/custom_stream_actions" diff --git a/app/jobs/card/auto_close_all_due_job.rb b/app/jobs/card/auto_close_all_due_job.rb index 6cb4f646b..d964dae23 100644 --- a/app/jobs/card/auto_close_all_due_job.rb +++ b/app/jobs/card/auto_close_all_due_job.rb @@ -1,6 +1,4 @@ class Card::AutoCloseAllDueJob < ApplicationJob - queue_as :default - def perform ApplicationRecord.with_each_tenant do |tenant| Card.auto_close_all_due diff --git a/app/jobs/card/auto_reconsider_all_stagnated_job.rb b/app/jobs/card/auto_reconsider_all_stagnated_job.rb index 83f87a648..b7178f916 100644 --- a/app/jobs/card/auto_reconsider_all_stagnated_job.rb +++ b/app/jobs/card/auto_reconsider_all_stagnated_job.rb @@ -1,6 +1,4 @@ class Card::AutoReconsiderAllStagnatedJob < ApplicationJob - queue_as :default - def perform ApplicationRecord.with_each_tenant do |tenant| Card.auto_reconsider_all_stagnated diff --git a/app/jobs/generate_notifications_job.rb b/app/jobs/generate_notifications_job.rb deleted file mode 100644 index e386b7b37..000000000 --- a/app/jobs/generate_notifications_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class GenerateNotificationsJob < ApplicationJob - queue_as :default - - def perform(event) - event.generate_notifications - end -end diff --git a/app/jobs/mention/create_job.rb b/app/jobs/mention/create_job.rb new file mode 100644 index 000000000..816782754 --- /dev/null +++ b/app/jobs/mention/create_job.rb @@ -0,0 +1,5 @@ +class Mention::CreateJob < ApplicationJob + def perform(record, mentioner:) + record.create_mentions(mentioner:) + end +end diff --git a/app/jobs/notify_recipients_job.rb b/app/jobs/notify_recipients_job.rb new file mode 100644 index 000000000..6083622fd --- /dev/null +++ b/app/jobs/notify_recipients_job.rb @@ -0,0 +1,5 @@ +class NotifyRecipientsJob < ApplicationJob + def perform(notifiable) + notifiable.notify_recipients + end +end diff --git a/app/jobs/remove_abandoned_creations_job.rb b/app/jobs/remove_abandoned_creations_job.rb index 1444313a2..6ca63169d 100644 --- a/app/jobs/remove_abandoned_creations_job.rb +++ b/app/jobs/remove_abandoned_creations_job.rb @@ -1,6 +1,4 @@ class RemoveAbandonedCreationsJob < ApplicationJob - queue_as :default - def perform ApplicationRecord.with_each_tenant do |tenant| Card.remove_abandoned_creations diff --git a/app/models/card.rb b/app/models/card.rb index e2191aa41..4344d89b1 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,39 +1,26 @@ class Card < ApplicationRecord - include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, - Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged, + include Assignable, Colored, Engageable, Eventable, Golden, Mentions, + Messages, Pinnable, Closeable, Readable, Searchable, Staged, Statuses, Taggable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :notifications, dependent: :destroy - has_one_attached :image, dependent: :purge_later - after_save :track_due_date_change, if: :saved_change_to_due_on? - after_save :track_title_change, if: :saved_change_to_title? - after_create :assign_initial_stage + has_markdown :description scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } - scope :in_collection, ->(collection) { where collection: collection } scope :indexed_by, ->(index) do case index - when "most_active" then ordered_by_activity - when "newest" then reverse_chronologically - when "oldest" then chronologically - when "latest" then latest - when "stalled" then ordered_by_staleness - when "closed" then closed - end - end - - scope :by_engagement_status, ->(status) do - case status.to_s - when "considering" then considering - when "doing" then doing.with_golden_first + when "newest" then reverse_chronologically + when "oldest" then chronologically + when "latest" then latest + when "stalled" then chronologically + when "closed" then closed end end @@ -42,36 +29,6 @@ class Card < ApplicationRecord end def cache_key - [ super, collection&.name ].compact.join("/") + [ super, collection.name ].compact.join("/") end - - private - def track_due_date_change - if due_on.present? - if due_on_before_last_save.nil? - track_event("due_date_added", particulars: { due_date: due_on }) - else - track_event("due_date_changed", particulars: { due_date: due_on }) - end - elsif due_on_before_last_save.present? - track_event("due_date_removed") - end - end - - def track_title_change - if title_before_last_save.present? - track_event("title_changed", particulars: { - old_title: title_before_last_save, - new_title: title - }) - end - end - - def assign_initial_stage - if workflow_stage = collection.workflow&.stages&.first - self.stage = workflow_stage - save! touch: false - track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name - end - end end diff --git a/app/models/card/closeable.rb b/app/models/card/closeable.rb index ce0a63e33..b599b6a03 100644 --- a/app/models/card/closeable.rb +++ b/app/models/card/closeable.rb @@ -1,16 +1,17 @@ module Card::Closeable extend ActiveSupport::Concern - AUTO_CLOSE_AFTER = 30.days - included do has_one :closure, dependent: :destroy scope :closed, -> { joins(:closure) } - scope :active, -> { where.missing(:closure) } + scope :open, -> { where.missing(:closure) } scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) } - scope :due_to_be_closed, -> { considering.where(last_active_at: ..AUTO_CLOSE_AFTER.ago) } + scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) } + scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") } + + delegate :auto_closing?, :auto_close_period, to: :collection end class_methods do @@ -22,14 +23,14 @@ module Card::Closeable end def auto_close_at - last_active_at + AUTO_CLOSE_AFTER if last_active_at + last_active_at + auto_close_period if auto_closing? && last_active_at end def closed? closure.present? end - def active? + def open? !closed? end diff --git a/app/models/card/colored.rb b/app/models/card/colored.rb index 99e934a94..9a16ee4a4 100644 --- a/app/models/card/colored.rb +++ b/app/models/card/colored.rb @@ -1,8 +1,11 @@ module Card::Colored extend ActiveSupport::Concern + COLORS = %w[ #3b3633 #67695e #eb7a32 #bf7c2b #c09c6f #746b1e #2c6da8 #5d618f #663251 #ff63a8 ] + DEFAULT_COLOR = "#2c6da8" + def color - color_from_stage || Colorable::DEFAULT_COLOR + color_from_stage || DEFAULT_COLOR end private diff --git a/app/models/card/draft_commenting.rb b/app/models/card/draft_commenting.rb deleted file mode 100644 index 615d23916..000000000 --- a/app/models/card/draft_commenting.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Card::DraftCommenting - extend ActiveSupport::Concern - - included do - after_save :capture_draft_comment - end - - def draft_comment - find_or_build_initial_comment.body.content - end - - def draft_comment=(body) - if body.present? - @draft_comment = body - else - messages.comments.destroy_all - end - end - - private - def find_or_build_initial_comment - message = messages.comments.first || messages.new(messageable: Comment.new) - message.comment - end - - def capture_draft_comment - if @draft_comment.present? - find_or_build_initial_comment.update! body: @draft_comment, creator: creator - end - - @draft_comment = nil - end -end diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb index 800088f72..6671bc5a1 100644 --- a/app/models/card/engageable.rb +++ b/app/models/card/engageable.rb @@ -6,9 +6,16 @@ module Card::Engageable included do has_one :engagement, dependent: :destroy, class_name: "Card::Engagement" - scope :considering, -> { published_or_drafted_by(Current.user).active.where.missing(:engagement) } - scope :doing, -> { published.active.joins(:engagement) } + scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) } + scope :doing, -> { published.open.joins(:engagement) } scope :stagnated, -> { doing.where(last_active_at: ..STAGNATED_AFTER.ago) } + + scope :by_engagement_status, ->(status) do + case status.to_s + when "considering" then considering + when "doing" then doing.with_golden_first + end + end end class_methods do @@ -22,11 +29,11 @@ module Card::Engageable end def doing? - active? && published? && engagement.present? + open? && published? && engagement.present? end def considering? - active? && published? && engagement.blank? + open? && published? && engagement.blank? end def engage diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index c8098708b..67c8fcff9 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -1,26 +1,46 @@ module Card::Eventable extend ActiveSupport::Concern + include ::Eventable + included do - has_many :events, dependent: :destroy before_create { self.last_active_at = Time.current } + + after_save :track_due_date_change, if: :saved_change_to_due_on? + after_save :track_title_change, if: :saved_change_to_title? end - def touch_last_active_at - touch :last_active_at - end - - def track_event(action, creator: Current.user, **particulars) - if published? - event = find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars - event.generate_notifications_later + def event_was_created(event) + transaction do + find_or_capture_event_summary.events << event + touch(:last_active_at) end end private + def should_track_event? + published? + end + def find_or_capture_event_summary - transaction do - messages.last&.event_summary || capture(EventSummary.new).event_summary + messages.last&.event_summary || capture(EventSummary.new).event_summary + end + + def track_due_date_change + if due_on.present? + if due_on_before_last_save.nil? + track_event("card_due_date_added", particulars: { due_date: due_on }) + else + track_event("card_due_date_changed", particulars: { due_date: due_on }) + end + elsif due_on_before_last_save.present? + track_event("card_due_date_removed") + end + end + + def track_title_change + if title_before_last_save.present? + track_event "card_title_changed", particulars: { old_title: title_before_last_save, new_title: title } end end end diff --git a/app/models/card/golden.rb b/app/models/card/golden.rb index 759179415..4f2f52a60 100644 --- a/app/models/card/golden.rb +++ b/app/models/card/golden.rb @@ -2,10 +2,9 @@ module Card::Golden extend ActiveSupport::Concern included do - scope :golden, -> { joins(:goldness) } - has_one :goldness, dependent: :destroy, class_name: "Card::Goldness" + scope :golden, -> { joins(:goldness) } scope :with_golden_first, -> { left_outer_joins(:goldness).prepend_order("card_goldnesses.id IS NULL") } end diff --git a/app/models/card/readable.rb b/app/models/card/readable.rb new file mode 100644 index 000000000..b37538b9b --- /dev/null +++ b/app/models/card/readable.rb @@ -0,0 +1,38 @@ +module Card::Readable + extend ActiveSupport::Concern + + def read_by(user) + notifications_for(user).tap do |notifications| + notifications.each(&:read) + end + end + + private + def notifications_for(user) + user.notifications.unread.where(source: notification_sources) + end + + def notification_sources + event_notification_sources + mention_notification_sources + end + + def event_notification_sources + events + comment_creation_events + end + + def comment_creation_events + Event.where(eventable: comments) + end + + def mention_notification_sources + mentions + comment_mentions + end + + def comment_mentions + Mention.where(source: comments) + end + + def comments + @comments ||= Comment.where(id: messages.comments.pluck(:messageable_id)) + end +end diff --git a/app/models/card/scorable.rb b/app/models/card/scorable.rb deleted file mode 100644 index 91b0856b5..000000000 --- a/app/models/card/scorable.rb +++ /dev/null @@ -1,64 +0,0 @@ -module Card::Scorable - extend ActiveSupport::Concern - - REFERENCE_DATE = Time.utc(2025, 1, 1) - - included do - scope :ordered_by_activity, -> { order activity_score_order: :desc } - - # Staleness is measured by the amount of activity_score lost since it was last updated. - # The factor 0.9 is chosen to make the decay curve closer to linear; an exponential - # curve would make recent items appear stale very quickly, due to the sharp dropoff. - scope :ordered_by_staleness, -> { order Arel.sql( - "coalesce(activity_score * (1 - power(0.9, julianday('now') - julianday(activity_score_at))), 0) desc" - ) } - end - - def rescore - score = calculate_activity_score - score_at = last_scorable_activity_at - - update! \ - activity_score: score, - activity_score_at: score_at, - activity_score_order: event_score_reference(score, score_at) - end - - private - def calculate_activity_score - scorable_events.sum { |event| event_score(event) } - end - - def event_score(event) - days_ago = (last_scorable_activity_at - event.created_at) / 1.day - event_weight(event) / (2**days_ago) - end - - def event_weight(event) - case - when event.comment&.first_by_author_on_card? then 20 - when event.comment&.follows_comment_by_another_author? then 15 - when event.commented? then 10 - else 0 - end - end - - def event_score_reference(score, activity_at) - # The reference score is used to make the activity score comparable - # across different cards, since it represents the card's activity - # level at a consistent point in time. - # - # We store this as log2 to tame the huge/tiny numbers we'd otherwise get - # when activity is far from the reference date. - days_diff = (activity_at - REFERENCE_DATE) / 1.day - Math.log2(1.0 + score) + days_diff - end - - def last_scorable_activity_at - scorable_events.maximum(:created_at) || created_at - end - - def scorable_events - events.where(action: :commented) - end -end diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index 3627a555b..2e01fef9b 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -4,7 +4,7 @@ module Card::Searchable included do include ::Searchable - searchable_by :title, using: :cards_search_index + searchable_by :title_and_description, using: :cards_search_index, as: :title scope :mentioning, ->(query) do if query = sanitize_query_syntax(query) @@ -18,25 +18,31 @@ module Card::Searchable end end - class_methods do - def sanitize_query_syntax(terms) - terms = terms.to_s - terms = remove_invalid_search_characters(terms) - terms = remove_unbalanced_quotes(terms) - terms.presence + class_methods do + def sanitize_query_syntax(terms) + terms = terms.to_s + terms = remove_invalid_search_characters(terms) + terms = remove_unbalanced_quotes(terms) + terms.presence + end + + private + def remove_invalid_search_characters(terms) + terms.gsub(/[^\w"]/, " ") end - private - def remove_invalid_search_characters(terms) - terms.gsub(/[^\w"]/, " ") + def remove_unbalanced_quotes(terms) + if terms.count("\"").even? + terms + else + terms.gsub("\"", " ") end + end + end - def remove_unbalanced_quotes(terms) - if terms.count("\"").even? - terms - else - terms.gsub("\"", " ") - end - end + private + # TODO: Temporary until we stabilize the search API + def title_and_description + [ title, description.to_plain_text ].join(" ") end end diff --git a/app/models/card/staged.rb b/app/models/card/staged.rb index b669aeddd..f8036d3e3 100644 --- a/app/models/card/staged.rb +++ b/app/models/card/staged.rb @@ -4,6 +4,8 @@ module Card::Staged included do belongs_to :stage, class_name: "Workflow::Stage", optional: true + before_create :assign_initial_stage + scope :in_stage, ->(stage) { where stage: stage } end @@ -11,12 +13,15 @@ module Card::Staged stage&.workflow end - def toggle_stage(stage) - new_stage, event = self.stage_id == stage.id ? [ nil, :unstaged ] : [ stage, :staged ] - + def change_stage_to(new_stage) transaction do update! stage: new_stage - track_event event, stage_id: stage.id, stage_name: stage.name + track_event :staged, stage_id: new_stage.id, stage_name: new_stage.name end end + + private + def assign_initial_stage + self.stage = collection.initial_workflow_stage + end end diff --git a/app/models/collection.rb b/app/models/collection.rb index 319b2bb7d..29e07a98b 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -1,20 +1,11 @@ class Collection < ApplicationRecord - include Accessible, Broadcastable, Filterable + include AutoClosing, Accessible, Broadcastable, Filterable, Workflowing belongs_to :creator, class_name: "User", default: -> { Current.user } - belongs_to :workflow, optional: true has_many :cards, dependent: :destroy has_many :tags, -> { distinct }, through: :cards - - validates_presence_of :name - - after_save :update_cards_workflow, if: :saved_change_to_workflow_id? + has_many :events scope :alphabetically, -> { order("lower(name)") } - - private - def update_cards_workflow - cards.update_all(stage_id: workflow&.stages&.first&.id) - end end diff --git a/app/models/collection/auto_closing.rb b/app/models/collection/auto_closing.rb new file mode 100644 index 000000000..e3a9c8990 --- /dev/null +++ b/app/models/collection/auto_closing.rb @@ -0,0 +1,19 @@ +module Collection::AutoClosing + extend ActiveSupport::Concern + + included do + scope :auto_closing, -> { where.not(auto_close_period: nil) } + before_create :set_default_auto_close_period + end + + def auto_closing? + auto_close_period.present? + end + + private + DEFAULT_AUTO_CLOSE_PERIOD = 30.days + + def set_default_auto_close_period + self.auto_close_period ||= DEFAULT_AUTO_CLOSE_PERIOD unless attribute_present?(:auto_close_period) + end +end diff --git a/app/models/collection/workflowing.rb b/app/models/collection/workflowing.rb new file mode 100644 index 000000000..658087fe2 --- /dev/null +++ b/app/models/collection/workflowing.rb @@ -0,0 +1,18 @@ +module Collection::Workflowing + extend ActiveSupport::Concern + + included do + belongs_to :workflow, optional: true + + after_update_commit :set_all_cards_to_initial_workflow_stage, if: :saved_change_to_workflow_id? + end + + def initial_workflow_stage + workflow&.stages&.first + end + + private + def set_all_cards_to_initial_workflow_stage + cards.update_all(stage_id: initial_workflow_stage&.id) + end +end diff --git a/app/models/colorable.rb b/app/models/colorable.rb deleted file mode 100644 index fed768a27..000000000 --- a/app/models/colorable.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Colorable - extend ActiveSupport::Concern - - # COLORS = %w[ #3b3633 #67695e #eb7a32 #bf7c2b #c09c6f #746b1e #2c6da8 #5d618f #663251 #ff63a8 ] - # DEFAULT_COLOR = "#2c6da8" - - COLORS = [ - "var(--color-card-1)", - "var(--color-card-2)", - "var(--color-card-3)", - "var(--color-card-4)", - "var(--color-card-5)", - "var(--color-card-6)", - "var(--color-card-7)", - "var(--color-card-8)", - "var(--color-card-9)", - ] - DEFAULT_COLOR = "var(--color-card-6)" - - included do - attribute :color, default: DEFAULT_COLOR - end -end diff --git a/app/models/comment.rb b/app/models/comment.rb index 8922b30b7..bdc674a9c 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,5 @@ class Comment < ApplicationRecord - include Messageable, Searchable + include Eventable, Mentions, Messageable, Searchable belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :reactions, dependent: :delete_all @@ -10,9 +10,11 @@ class Comment < ApplicationRecord # FIXME: Not a fan of this. Think all references to comment should come directly from the message. scope :belonging_to_card, ->(card) { joins(:message).where(messages: { card_id: card.id }) } - after_create_commit :watch_card_by_creator, :track_commented_card + after_create_commit :watch_card_by_creator after_destroy_commit :cleanup_events + delegate :watch_by, to: :card + def to_partial_path "cards/#{super}" end @@ -22,10 +24,6 @@ class Comment < ApplicationRecord card.watch_by creator end - def track_commented_card - card.track_event :commented, comment_id: id - end - # FIXME: This isn't right. We need to introduce an eventable polymorphic association for this. def cleanup_events # Delete events that reference directly in particulars diff --git a/app/models/comment/eventable.rb b/app/models/comment/eventable.rb new file mode 100644 index 000000000..4e90777e0 --- /dev/null +++ b/app/models/comment/eventable.rb @@ -0,0 +1,18 @@ +module Comment::Eventable + extend ActiveSupport::Concern + + include ::Eventable + + included do + after_create_commit :track_creation + end + + def event_was_created(event) + card.touch(:last_active_at) + end + + private + def track_creation + track_event("created", collection: card.collection) + end +end diff --git a/app/models/concerns/filterable.rb b/app/models/concerns/filterable.rb index b3cb07c46..85a33a015 100644 --- a/app/models/concerns/filterable.rb +++ b/app/models/concerns/filterable.rb @@ -9,7 +9,9 @@ module Filterable end private + # FIXME: This is too inefficient to have part of a destroy transaction. + # Need to find a way to use a job or a single query. def remove_from_filters - filters.each { |filter| filter.resource_removed self } + filters.each { it.resource_removed self } end end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb new file mode 100644 index 000000000..08e0362cb --- /dev/null +++ b/app/models/concerns/mentions.rb @@ -0,0 +1,46 @@ +module Mentions + extend ActiveSupport::Concern + + included do + has_many :mentions, as: :source, dependent: :destroy + has_many :mentionees, through: :mentions + after_save_commit :create_mentions_later, if: :mentionable_content_changed? + end + + def create_mentions(mentioner: Current.user) + scan_mentionees.each do |mentionee| + mentionee.mentioned_by mentioner, at: self + end + end + + def mentionable_content + markdown_associations.collect { send(it.name)&.to_plain_text }.compact.join(" ") + end + + private + def scan_mentionees + scan_mentioned_handles.filter_map do |mention| + mentionable_users.find { |user| user.mentionable_handles.include?(mention) } + end + end + + def scan_mentioned_handles + mentionable_content.scan(/(? { order created_at: :asc, id: :desc } - after_create -> { card.touch_last_active_at } + after_create -> { eventable.event_was_created(self) } - def commented? - action == "commented" + def action + super.inquiry end - def generate_notifications - Notifier.for(self)&.generate - end - - def generate_notifications_later - GenerateNotificationsJob.perform_later self + def notifiable_target + eventable end end diff --git a/app/models/event/particulars.rb b/app/models/event/particulars.rb index 389eda8ea..dd92fa4da 100644 --- a/app/models/event/particulars.rb +++ b/app/models/event/particulars.rb @@ -2,14 +2,10 @@ module Event::Particulars extend ActiveSupport::Concern included do - store_accessor :particulars, :assignee_ids, :comment_id, :stage_id, :stage_name + store_accessor :particulars, :assignee_ids, :stage_id, :stage_name end def assignees @assignees ||= User.where id: assignee_ids end - - def comment - @comment ||= Comment.find_by id: comment_id - end end diff --git a/app/models/event_summary.rb b/app/models/event_summary.rb index fcfaabd83..f04841e66 100644 --- a/app/models/event_summary.rb +++ b/app/models/event_summary.rb @@ -13,25 +13,23 @@ class EventSummary < ApplicationRecord def summarize(event) case event.action - when "published" - "Added by #{event.creator.name}." - when "assigned" + when "card_assigned" "Assigned to #{event.assignees.pluck(:name).to_sentence}." - when "unassigned" + when "card_unassigned" "Unassigned from #{event.assignees.pluck(:name).to_sentence}." - when "staged" + when "card_staged" "#{event.creator.name} moved this to '#{event.stage_name}'." - when "closed" + when "card_closed" "Closed by #{ event.creator.name }" - when "unstaged" + when "card_unstaged" "#{event.creator.name} removed this from '#{event.stage_name}'." - when "due_date_added" + when "card_due_date_added" "#{event.creator.name} set due date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')}." - when "due_date_changed" + when "card_due_date_changed" "#{event.creator.name} changed due date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')}." - when "due_date_removed" + when "card_due_date_removed" "#{event.creator.name} removed the date." - when "title_changed" + when "card_title_changed" "#{event.creator.name} changed title from '#{event.particulars.dig('particulars', 'old_title')}' to '#{event.particulars.dig('particulars', 'new_title')}'." end end diff --git a/app/models/eventable.rb b/app/models/eventable.rb new file mode 100644 index 000000000..e14803f09 --- /dev/null +++ b/app/models/eventable.rb @@ -0,0 +1,25 @@ +module Eventable + extend ActiveSupport::Concern + + included do + has_many :events, as: :eventable, dependent: :destroy + end + + def track_event(action, creator: Current.user, collection: self.collection, **particulars) + if should_track_event? + collection.events.create!(action: "#{eventable_prefix}_#{action}", creator:, collection:, eventable: self, particulars:) + end + end + + def event_was_created(event) + end + + private + def should_track_event? + true + end + + def eventable_prefix + self.class.name.demodulize.underscore + end +end diff --git a/app/models/filter.rb b/app/models/filter.rb index 1ec1a5e6c..e5ce39383 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -19,12 +19,12 @@ class Filter < ApplicationRecord def cards @cards ||= begin result = creator.accessible_cards.indexed_by(indexed_by) - result = result.active unless indexed_by.closed? + result = result.open unless indexed_by.closed? result = result.by_engagement_status(engagement_status) if engagement_status.present? result = result.unassigned if assignment_status.unassigned? result = result.assigned_to(assignees.ids) if assignees.present? result = result.where(creator_id: creators.ids) if creators.present? - result = result.in_collection(collections.ids) if collections.present? + result = result.where(collection: collections.ids) if collections.present? result = result.in_stage(stages.ids) if stages.present? result = result.tagged_with(tags.ids) if tags.present? result = terms.reduce(result) do |result, term| diff --git a/app/models/mention.rb b/app/models/mention.rb new file mode 100644 index 000000000..ac5cb336c --- /dev/null +++ b/app/models/mention.rb @@ -0,0 +1,22 @@ +class Mention < ApplicationRecord + include Notifiable + + belongs_to :source, polymorphic: true + belongs_to :mentioner, class_name: "User" + belongs_to :mentionee, class_name: "User", inverse_of: :mentions + + after_create_commit :watch_source_by_mentionee + + def self_mention? + mentioner == mentionee + end + + def notifiable_target + source + end + + private + def watch_source_by_mentionee + source.watch_by(mentionee) + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 9fbb94a5b..01444ce61 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,16 +1,16 @@ class Notification < ApplicationRecord belongs_to :user - belongs_to :event - belongs_to :card - belongs_to :resource, polymorphic: true + belongs_to :creator, class_name: "User" + belongs_to :source, polymorphic: true scope :unread, -> { where(read_at: nil) } scope :read, -> { where.not(read_at: nil) } scope :ordered, -> { order(read_at: :desc, created_at: :desc) } - delegate :creator, to: :event after_create_commit :broadcast_unread + delegate :notifiable_target, to: :source + def self.read_all update!(read_at: Time.current) end diff --git a/app/models/notifier.rb b/app/models/notifier.rb index 19626be5f..81e69ce07 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -1,40 +1,31 @@ class Notifier - attr_reader :event - - delegate :creator, to: :event + attr_reader :source class << self - def for(event) - "Notifier::#{event.action.classify}".safe_constantize&.new(event) + def for(source) + case source + when Event + "Notifier::#{source.eventable.class}EventNotifier".safe_constantize&.new(source) + when Mention + MentionNotifier.new(source) + end end end - def generate + def notify if should_notify? recipients.map do |recipient| - Notification.create! user: recipient, event: event, card: card, resource: resource + Notification.create! user: recipient, source: source, creator: creator end end end private - def initialize(event) - @event = event + def initialize(source) + @source = source end def should_notify? - !event.creator.system? - end - - def recipients - card.watchers_and_subscribers.without(creator) - end - - def resource - card - end - - def card - event.summary.message.card + !creator.system? end end diff --git a/app/models/notifier/assigned.rb b/app/models/notifier/assigned.rb deleted file mode 100644 index 843376435..000000000 --- a/app/models/notifier/assigned.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Notifier::Assigned < Notifier - private - def recipients - event.assignees.excluding(card.collection.access_only_users) - end -end diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb new file mode 100644 index 000000000..899ee53fb --- /dev/null +++ b/app/models/notifier/card_event_notifier.rb @@ -0,0 +1,22 @@ +class Notifier::CardEventNotifier < Notifier + delegate :creator, to: :source + delegate :watchers_and_subscribers, to: :card + + private + def recipients + case source.action + when "card_assigned" + source.assignees.excluding(source.collection.access_only_users) + when "card_published" + watchers_and_subscribers(include_only_watching: true).without(creator, *card.mentionees) + when "comment_created" + watchers_and_subscribers.without(creator, *source.eventable.mentionees) + else + watchers_and_subscribers.without(creator) + end + end + + def card + source.eventable + end +end diff --git a/app/models/notifier/closed.rb b/app/models/notifier/closed.rb deleted file mode 100644 index f090b0308..000000000 --- a/app/models/notifier/closed.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Notifier::Closed < Notifier -end diff --git a/app/models/notifier/comment_event_notifier.rb b/app/models/notifier/comment_event_notifier.rb new file mode 100644 index 000000000..40a40f4e5 --- /dev/null +++ b/app/models/notifier/comment_event_notifier.rb @@ -0,0 +1,12 @@ +class Notifier::CommentEventNotifier < Notifier + delegate :creator, to: :source + + private + def recipients + card.watchers_and_subscribers.without(creator, *source.eventable.mentionees) + end + + def card + source.eventable.card + end +end diff --git a/app/models/notifier/commented.rb b/app/models/notifier/commented.rb deleted file mode 100644 index 42ad4e5c8..000000000 --- a/app/models/notifier/commented.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Notifier::Commented < Notifier - private - def resource - event.comment - end -end diff --git a/app/models/notifier/mention_notifier.rb b/app/models/notifier/mention_notifier.rb new file mode 100644 index 000000000..c3920df8a --- /dev/null +++ b/app/models/notifier/mention_notifier.rb @@ -0,0 +1,16 @@ +class Notifier::MentionNotifier < Notifier + alias mention source + + private + def recipients + if mention.self_mention? + [] + else + [ mention.mentionee ] + end + end + + def creator + mention.mentioner + end +end diff --git a/app/models/notifier/published.rb b/app/models/notifier/published.rb deleted file mode 100644 index ce3917e8f..000000000 --- a/app/models/notifier/published.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Notifier::Published < Notifier - private - def recipients - card.watchers_and_subscribers(include_only_watching: true).without(creator) - end -end diff --git a/app/models/user.rb b/app/models/user.rb index a22841c42..370aae05c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,8 @@ class User < ApplicationRecord - include Accessor, Assignee, Avatar, Role, Transferable + include Accessor, Assignee, Mentionable, Named, Role, Transferable + include Timelined # Depends on Accessor + + has_one_attached :avatar has_many :sessions, dependent: :destroy has_secure_password validations: false @@ -15,18 +18,10 @@ class User < ApplicationRecord normalizes :email_address, with: ->(value) { value.strip.downcase } - scope :alphabetically, -> { order("lower(name)") } - - def initials - name.to_s.scan(/\b\p{L}/).join.upcase - end - def deactivate - transaction do - sessions.delete_all - accesses.destroy_all - update! active: false, email_address: deactived_email_address - end + sessions.delete_all + accesses.destroy_all + update! active: false, email_address: deactived_email_address end private diff --git a/app/models/user/avatar.rb b/app/models/user/avatar.rb deleted file mode 100644 index a9d2ad12e..000000000 --- a/app/models/user/avatar.rb +++ /dev/null @@ -1,17 +0,0 @@ -module User::Avatar - extend ActiveSupport::Concern - - included do - has_one_attached :avatar - end - - class_methods do - def from_avatar_token(sid) - find_signed!(sid, purpose: :avatar) - end - end - - def avatar_token - signed_id(purpose: :avatar) - end -end diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb new file mode 100644 index 000000000..32aad0814 --- /dev/null +++ b/app/models/user/day_timeline.rb @@ -0,0 +1,44 @@ +class User::DayTimeline + attr_reader :user, :day, :filter + + def initialize(user, day, filter) + @user, @day, @filter = user, day, filter + end + + def has_activity? + events.any? + end + + def events + filtered_events.where(created_at: window).order(created_at: :desc) + end + + def next_day + latest_event_before&.created_at + end + + def earliest_time + next_day&.tomorrow&.beginning_of_day + end + + def latest_time + day.yesterday.beginning_of_day + end + + private + def filtered_events + @filtered_events ||= Event.where(collection: collections) + end + + def collections + filter.collections.presence || user.collections + end + + def latest_event_before + filtered_events.where(created_at: ...day.beginning_of_day).chronologically.last + end + + def window + day.all_day + end +end diff --git a/app/models/user/mentionable.rb b/app/models/user/mentionable.rb new file mode 100644 index 000000000..7a930ab02 --- /dev/null +++ b/app/models/user/mentionable.rb @@ -0,0 +1,20 @@ +module User::Mentionable + extend ActiveSupport::Concern + + included do + has_many :mentions, dependent: :destroy, inverse_of: :mentionee + end + + def mentioned_by(mentioner, at:) + mentions.find_or_create_by! source: at, mentioner: mentioner + end + + def mentionable_handles + [ initials, first_name, first_name_with_last_name_initial ].collect(&:downcase) + end + + private + def first_name_with_last_name_initial + "#{first_name}#{last_name&.first}" + end +end diff --git a/app/models/user/named.rb b/app/models/user/named.rb new file mode 100644 index 000000000..67420ec72 --- /dev/null +++ b/app/models/user/named.rb @@ -0,0 +1,19 @@ +module User::Named + extend ActiveSupport::Concern + + included do + scope :alphabetically, -> { order("lower(name)") } + end + + def first_name + name.split(/\s/).first + end + + def last_name + name.split(/\s/, 2).last + end + + def initials + name.scan(/\b\p{L}/).join.upcase + end +end diff --git a/app/models/user/timelined.rb b/app/models/user/timelined.rb new file mode 100644 index 000000000..1f5f2c8fe --- /dev/null +++ b/app/models/user/timelined.rb @@ -0,0 +1,11 @@ +module User::Timelined + extend ActiveSupport::Concern + + included do + has_many :accessible_events, through: :collections, source: :events + end + + def timeline_for(day, filter:) + User::DayTimeline.new(self, day, filter) + end +end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index df896cf09..3174289dd 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -1,3 +1,12 @@ class Workflow < ApplicationRecord + DEFAULT_STAGES = [ "Triage", "In progress", "On Hold", "Review" ] + has_many :stages, dependent: :delete_all + + after_create_commit :create_default_stages + + private + def create_default_stages + Workflow::Stage.insert_all(DEFAULT_STAGES.map { { name: it, workflow_id: id } }) + end end diff --git a/app/presenters/collection_filter_presenter.rb b/app/presenters/collection_filter_presenter.rb deleted file mode 100644 index 730e38e83..000000000 --- a/app/presenters/collection_filter_presenter.rb +++ /dev/null @@ -1,29 +0,0 @@ -class CollectionFilterPresenter - def initialize(collections, params, cookies) - @collections = collections - @params = params - @cookies = cookies - end - - def filter_text - if selected_collection_ids.present? - "Showing activity for #{selected_collection_names_bold}".html_safe - else - "Showing everything" - end - end - - private - def selected_collection_ids - @params[:collection_ids].presence || @cookies[:collection_filter]&.split(",") - end - - def selected_collection_names - @collections.where(id: selected_collection_ids).pluck(:name).to_sentence - end - - def selected_collection_names_bold - names = @collections.where(id: selected_collection_ids).pluck(:name) - names.map { |name| "#{name}" }.to_sentence - end -end diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb new file mode 100644 index 000000000..ce7482d9c --- /dev/null +++ b/app/views/cards/_container.html.erb @@ -0,0 +1,37 @@ +
+ <%= render "cards/container/engagement", card: card %> + + + +
+ <%= card_article_tag card, class: "card" do %> +
+ <%= link_to card.collection.name, cards_path(collection_ids: [ card.collection ]), + class: "card__collection txt-uppercase overflow-ellipsis txt-reversed" %> + + <%= render "cards/display/perma/tags", card: card %> +
+ +
+ <%= render "cards/container/title", card: card %> + <%= render "cards/stagings/stages", card: card if card.doing? %> +
+ + + + <%= render "cards/display/common/background", card: card %> + <% end %> +
+ + <% if card.published? %> + <%= render "cards/container/footer/published", card: card %> + <% elsif card.creating? %> + <%= render "cards/container/footer/draft", card: card %> + <% end %> +
diff --git a/app/views/cards/_messages.html.erb b/app/views/cards/_messages.html.erb index 4b4298759..35f0cd586 100644 --- a/app/views/cards/_messages.html.erb +++ b/app/views/cards/_messages.html.erb @@ -14,5 +14,10 @@ <%= avatar_tag watcher %> <% end %> + +
+
+ <%= button_to_delete_card(@card) %> +
<% end %> diff --git a/app/views/cards/assignments/create.turbo_stream.erb b/app/views/cards/assignments/create.turbo_stream.erb deleted file mode 100644 index f05f15d8d..000000000 --- a/app/views/cards/assignments/create.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= turbo_stream.replace([ @card, :meta ], partial: "cards/display/perma/meta", locals: { card: @card }) %> diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index 1be2c77a2..22c8f9c2d 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -1,8 +1,8 @@ <%= turbo_frame_tag @card, :assignment do %> - Assign this to… + Assign this to… -
+
<%= sanitize comment.body_html %>
diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index 2352d1797..c4645fe86 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -4,7 +4,7 @@
-
+
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width", data: { controller: "form paste local-save", local_save_key_value: "comment-#{card.id}", diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index e3a2e783d..d07b50334 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -1,6 +1,6 @@ <%= turbo_frame_tag dom_id(@comment) do %>
-
+
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width", data: { controller: "form paste", action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } do |form| %> <%= form.markdown_area :body, class: "input comment__input", required: true, autofocus: true, placeholder: new_comment_placeholder(@card) %> @@ -9,7 +9,7 @@ Save changes <% end %> <%= link_to card_comment_path(@card, @comment), class: "btn", data: { form_target: "cancel" } do %> - Cancel + Cancel <% end %> <%= tag.button type: :submit, class: "btn btn--negative flex-item-justify-end", form: dom_id(@comment, :delete_form), diff --git a/app/views/cards/_closure_toggle.html.erb b/app/views/cards/container/_closure.html.erb similarity index 65% rename from app/views/cards/_closure_toggle.html.erb rename to app/views/cards/container/_closure.html.erb index 7958b0887..f69cab694 100644 --- a/app/views/cards/_closure_toggle.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -1,9 +1,11 @@ -
"> +
<% if card.closed? %> - Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. - <%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %> - Un-do - <% end %> +
+ Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. + <%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %> + Un-do + <% end %> +
<% else %>
@@ -22,7 +24,7 @@ <% if card.doing? %> Returns to Considering if no activity <%= local_datetime_tag(card.auto_reconsider_at, style: :indays) -%>. - <% else %> + <% elsif card.auto_closing? %> Auto-closes if no activity <%= local_datetime_tag(card.auto_close_at, style: :indays) -%>. <% end %> diff --git a/app/views/cards/_doing_toggle.html.erb b/app/views/cards/container/_engagement.html.erb similarity index 100% rename from app/views/cards/_doing_toggle.html.erb rename to app/views/cards/container/_engagement.html.erb diff --git a/app/views/cards/_golden_toggle.html.erb b/app/views/cards/container/_gild.html.erb similarity index 100% rename from app/views/cards/_golden_toggle.html.erb rename to app/views/cards/container/_gild.html.erb diff --git a/app/views/cards/_image.html.erb b/app/views/cards/container/_image.html.erb similarity index 100% rename from app/views/cards/_image.html.erb rename to app/views/cards/container/_image.html.erb diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb new file mode 100644 index 000000000..4e4c2c9f5 --- /dev/null +++ b/app/views/cards/container/_title.html.erb @@ -0,0 +1,26 @@ +<% if card.published? %> + <%= turbo_frame_tag card, :edit do %> +
+

+ <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %> +

+
+ <%= sanitize card.description_html %> +
+
+ <% end %> +<% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "card__content", data: { controller: "paste auto-save" } do |form| %> +

+ <%= form.text_area :title, placeholder: "Name it…", + class: "input input--textarea full-width borderless txt-align-start", + autofocus: card.title.blank?, + data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
+ <% end %> +<% end %> diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb new file mode 100644 index 000000000..f46e08b41 --- /dev/null +++ b/app/views/cards/container/footer/_draft.html.erb @@ -0,0 +1,4 @@ +
+ <%= button_to "Create card", card_publish_path(card), class: "btn" %> + <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> +
diff --git a/app/views/cards/container/footer/_published.html.erb b/app/views/cards/container/footer/_published.html.erb new file mode 100644 index 000000000..c4e0a0cdf --- /dev/null +++ b/app/views/cards/container/footer/_published.html.erb @@ -0,0 +1,7 @@ +<%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> + + +<%= render "cards/container/closure", card: card %> diff --git a/app/views/cards/display/_perma.html.erb b/app/views/cards/display/_perma.html.erb deleted file mode 100644 index f70b9d2c8..000000000 --- a/app/views/cards/display/_perma.html.erb +++ /dev/null @@ -1,39 +0,0 @@ -<% cache card do %> - <%= card_article_tag card, class: "card" do %> -
- <%= link_to card.collection.name, cards_path(collection_ids: [card.collection]), - class: "card__collection txt-uppercase overflow-ellipsis txt-reversed" %> - - <%= render "cards/display/perma/tags", card: card %> -
- -
-

- <% if card.published? %> - <%= turbo_frame_tag card, :edit do %> - <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> - <% end %> - <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> - <%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", - autofocus: card.title.blank?, - data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> - <% end %> - <% end %> -

- - <% if card.doing? %> - <%= turbo_frame_tag dom_id(@card, :stages) do %> - <%= render "cards/stagings/stages", card: card %> - <% end %> - <% end %> -
- -
- <%= render "cards/display/perma/meta", card: card %> -
- - <%= render "cards/display/common/background", card: card %> - <% end %> -<% end %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 7d673308c..a909089cb 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -1,18 +1,20 @@ -<% cache [ card ] do %> +<% cache card do %> <%= card_article_tag card, class: "card" do %>
<%= card.collection.name %> <%= render "cards/display/preview/tags", card: card %>
-
-

- <%= card.title %> -

+
+
+

+ <%= card.title %> +

- <%= link_to collection_card_path(card.collection, card), class: "card__link" do %> - <%= card.title %> - <% end %> + <%= link_to collection_card_path(card.collection, card), class: "card__link", data: { turbo_frame: "_top" } do %> + <%= card.title %> + <% end %> +
<%= render "cards/stagings/stages", card: card if card.doing? %>
diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index 7cfa48447..c89cdcc57 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -4,7 +4,7 @@ <% end %>
> - diff --git a/app/views/cards/display/common/_background.html.erb b/app/views/cards/display/common/_background.html.erb index 08a3c557a..8852fb765 100644 --- a/app/views/cards/display/common/_background.html.erb +++ b/app/views/cards/display/common/_background.html.erb @@ -5,7 +5,7 @@ <% if card.closed? %>
<%= card.closure.reason %> - <%= card.closed_at.strftime("%b %d, %Y") %> - by <%= card.closed_by.name %> + <%= card.closed_at.strftime("%b %d, %Y") %> + by <%= familiar_name_for(card.closed_by) %>
<% end %> diff --git a/app/views/cards/display/common/_meta.html.erb b/app/views/cards/display/common/_meta.html.erb index 94c5f477a..9219e44df 100644 --- a/app/views/cards/display/common/_meta.html.erb +++ b/app/views/cards/display/common/_meta.html.erb @@ -20,7 +20,8 @@ Reported by <%= familiar_name_for(card.creator) %> - <%= card.assignees.any? ? card.assignees.map { |assignee| "Assigned to #{familiar_name_for(assignee)}" }.to_sentence.html_safe : "Not assigned" %> + <%= card.assignees.any? ? "Assigned to" : "Not assigned" %> + <%= card.assignees.map { |assignee| "#{familiar_name_for(assignee)}" }.to_sentence.html_safe %>
diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index cecde410e..ff6c97b8e 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -1,7 +1,7 @@ -
+
<% if card.tags.any? || card.creating? %> - # + # <% card.tags.each_with_index do |tag, index| %> <%= link_to cards_path(collection_ids: [ card.collection ], tag_ids: [ tag.id ]), class: "card__tag btn btn--plain min-width txt-uppercase fill-transparent" do %> @@ -12,7 +12,7 @@
> - diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 5e5e0d1d9..499563bb4 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,13 +1,19 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %> - <%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card__title-field", - required: true, autofocus: true, placeholder: "Name it…", - rows: 3, - data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" }, - style: "color: var(--spat-color); --input-border-radius: 0" %> - <% end %> - <%= form.button "Save", type: :submit, hidden: true %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "card__content", data: { controller: "form paste" } do |form| %> +

+ <%= form.label :title, class: "flex flex-column align-center" do %> + <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", + required: true, autofocus: true, placeholder: "Name it…", + rows: 1, + data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> + <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
+ <%= form.button "Save changes", type: :submit, class: "btn" %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> diff --git a/app/views/cards/index/_collections_filter.html.erb b/app/views/cards/index/_collections_filter.html.erb index b81481ad7..c61ef98d0 100644 --- a/app/views/cards/index/_collections_filter.html.erb +++ b/app/views/cards/index/_collections_filter.html.erb @@ -1,23 +1,44 @@ -<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list", +<%= form_with url: cards_path, method: :get, class: "flex flex-column max-width popup__list full-width", data: { controller: "form" } do |form| %> <% filter.as_params.except(:collection_ids).each do |key, value| %> <%= filter_hidden_field_tag key, value %> <% end %> - <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "btn popup__item" do %> - All collections + <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center", style: "--hover-size: 0" do %> +
+ > + + + See everything in all collections + + <%= icon_tag "check", size: 18, class: "checked" %> +
+ <%= tag.div class: "btn popup__item min-width flex-item-grow" do %> + All collections + <% end %> + GO TO <% end %> <% Current.user.collections.order(:name).each do |collection| %> - + + <%= link_to cards_path(collection_ids: [ collection ]), class: "btn popup__item min-width flex-item-grow" do %> + <%= collection.name %> + <% end %> + + + <% end %> <% end %> diff --git a/app/views/cards/index/_header.html.erb b/app/views/cards/index/_header.html.erb index 0eef62cce..b5b9fb59d 100644 --- a/app/views/cards/index/_header.html.erb +++ b/app/views/cards/index/_header.html.erb @@ -1,22 +1,46 @@
- -
- Choose a workflow -

Use a workflow to track progress in this collection.

- <%= form_with model: @collection, url: collection_workflow_path(@collection), local: true, method: :patch, data: { controller: "form" } do |form| %> - <%= form.select :workflow_id, Workflow.all.map { |w| [w.name, w.id] }, { include_blank: "Choose…" }, class: "input input--select full-width", data: { action: "change->form#submit" } %> - <% end %> -

<%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %>

-
+ <% end %> +
+ diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb new file mode 100644 index 000000000..84be06644 --- /dev/null +++ b/app/views/collections/edit/_auto_close.html.erb @@ -0,0 +1,6 @@ +Auto-Close Cards + + +<%= form_with model: collection, data: { controller: "form" } do |form| %> + <%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %> +<% end %> diff --git a/app/views/collections/edit/_name.html.erb b/app/views/collections/edit/_name.html.erb new file mode 100644 index 000000000..e08e885ce --- /dev/null +++ b/app/views/collections/edit/_name.html.erb @@ -0,0 +1,7 @@ +
+ +
diff --git a/app/views/collections/edit/_users.html.erb b/app/views/collections/edit/_users.html.erb new file mode 100644 index 000000000..bc3530419 --- /dev/null +++ b/app/views/collections/edit/_users.html.erb @@ -0,0 +1,42 @@ +
+ <%= access_menu_tag collection do %> +
  • +
    + <%= icon_tag "everyone" %> + Everyone +
    + +
    +
    Everyone
    +
    + + + + +
  • + + + + <% if User.active.count > 20 %> +
    + +
    + <% end %> + +
    + <% 20.times do %> + <%= access_toggles_for selected_users, selected: true %> + <% end %> + + <% if selected_users.any? && unselected_users.any? %> +
    + <% end %> + + <%= access_toggles_for unselected_users, selected: false %> +
    + <% end %> +
    diff --git a/app/views/collections/edit/_workflows.html.erb b/app/views/collections/edit/_workflows.html.erb new file mode 100644 index 000000000..5e958cd19 --- /dev/null +++ b/app/views/collections/edit/_workflows.html.erb @@ -0,0 +1,21 @@ +Workflows +
    Use a Workflow to track progress in this Collection.
    +
    + <% Workflow.all.each do |workflow| %> +
    + <%= button_to collection_workflow_path(collection), method: :patch do %> + <%= hidden_field_tag "collection[workflow_id]", workflow.id %> + <%= workflow.name %> +
      + <% workflow.stages.each do |stage| %> +
    • + + <%= stage.name %> +
    • + <% end %> +
    + <% end %> +
    + <% end %> +
    +

    <%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %>

    diff --git a/app/views/events/_day.html.erb b/app/views/events/_day.html.erb index 416d67e63..91058f79f 100644 --- a/app/views/events/_day.html.erb +++ b/app/views/events/_day.html.erb @@ -1,25 +1,20 @@
    - <% if events.any? %> + data-related-element-highlight-class="event--related"> + <% if day_timeline.has_activity? %>

    - <%= event_day_title(activity_day) %> + + <%= local_datetime_tag day_timeline.day, style: :agoorweekday, class: "txt-ink" %> +

    - <%= render_column_headers(activity_day) %> - <%= render_event_grid_cells(activity_day) %> - - <% events.each do | hour_column, cluster| %> - <%= event_cluster_tag(*hour_column) do %> - <%= render partial: "events/event", collection: cluster.sort_by(&:created_at).reverse %> - - <%= local_datetime_tag cluster.first.created_at, class: "event__timestamp txt-small translucent" %> - <% end %> - <% end %> + <%= render_column_headers(day_timeline) %> + <%= render_event_grid_cells %> + <%= render "events/day_timeline", day_timeline: day_timeline %>
    - <%= render "events/empty_days", earliest: next_day&.tomorrow&.beginning_of_day, latest: activity_day.yesterday.beginning_of_day %> + <%= render "events/empty_days", day_timeline: day_timeline %> <% else %> - <%= render "events/empty_days", earliest: next_day&.tomorrow&.beginning_of_day, latest: activity_day.beginning_of_day %> + <%= render "events/empty_days", day_timeline: day_timeline %> <% end %>
    diff --git a/app/views/events/_day_timeline.html.erb b/app/views/events/_day_timeline.html.erb new file mode 100644 index 000000000..91933a709 --- /dev/null +++ b/app/views/events/_day_timeline.html.erb @@ -0,0 +1,9 @@ +<% day_timeline.events.group_by { it.created_at.hour }.each do |hour, events_grouped_by_hour| %> + <% events_grouped_by_hour.group_by { event_column(it) }.each do |column, events| %> + <%= event_cluster_tag(hour, column) do %> + <%= render partial: "events/event", collection: events %> + + <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/events/_empty_days.html.erb b/app/views/events/_empty_days.html.erb index d67dea429..93be21058 100644 --- a/app/views/events/_empty_days.html.erb +++ b/app/views/events/_empty_days.html.erb @@ -1,12 +1,14 @@ +<% earliest, latest = day_timeline.earliest_time, day_timeline.latest_time %> + <% if earliest.present? %> <% if earliest == latest %>
    -

    <%= event_day_title(earliest) %>

    +

    <%= local_datetime_tag earliest, style: :agoorweekday, class: "txt-ink txt-capitalize" %>

    No activity

    <% elsif earliest < latest %>
    -

    <%= event_day_title(earliest) %> – <%= event_day_title(latest) %>

    +

    <%= local_datetime_tag earliest, style: :agoorweekday, class: "txt-ink txt-capitalize" %> – <%= local_datetime_tag latest, style: :agoorweekday, class: "txt-ink txt-capitalize" %>

    No activity for <%= (latest - earliest).seconds.in_days.round + 1 %> days

    <% end %> diff --git a/app/views/events/_event.html.erb b/app/views/events/_event.html.erb index 0f55d47be..8554ea400 100644 --- a/app/views/events/_event.html.erb +++ b/app/views/events/_event.html.erb @@ -1,36 +1 @@ -<% card = event.summary.card %> -<% link = event.action == "commented" ? collection_card_path(card.collection, card, anchor: dom_id(event.comment)) : collection_card_path(card.collection, card) %> - -<%= link_to link, - class: "event event--#{ event.action } panel center center-block flex-inline align-start justify-start gap position-relative", - style: "--card-color: #{ card.color }; background-color: color-mix(in srgb, var(--card-color) 10%, var(--color-canvas)); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));", - data: { related_element_target: "related", - related_element_group_value: card.id, - action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %> -
    - <%= avatar_image_tag(event.creator) %> -
    - -
    -
    - - <%= event_action_sentence(event) %> - - - <% if event_column(event) == 2 %> - - <%= icon_tag event_action_icon(event) %> - - <% end %> -
    - - <% if event.action == "commented" && !strip_tags(event&.comment&.body_html).blank? %> - - <%= strip_tags(event.comment.body_html).truncate(200) -%> - - <% end %> - - <%= card.collection.name %> -
    -<% end %> +<%= render "events/event/#{event.eventable_type.demodulize.underscore}", event: event %> diff --git a/app/views/events/_filter.html.erb b/app/views/events/_filter.html.erb index 94a9a756a..ee19a3112 100644 --- a/app/views/events/_filter.html.erb +++ b/app/views/events/_filter.html.erb @@ -1,30 +1,37 @@ -<% presenter = CollectionFilterPresenter.new(@collections, params, cookies) %> -
    - - - - Filter by Collection… - <%= form_with url: events_path, method: :get, class: "flex flex-column full-width popup__list", - data: { controller: "form" } do |form| %> - <%= link_to "Show everything", events_path(clear_filter: true), class: "btn popup__item txt-nowrap" %> - <% @collections.each do |collection| %> - - <% end %> <% end %> - + + <%= tag.dialog class: "events__popup popup popup--animated panel flex-column align-start gap-half fill-white shadow", + style: "z-index: 5;", + data: { + action: "turbo:before-cache@document->dialog#close", + controller: "filter", + dialog_target: "dialog", + filter_active_class: "filter--active", + filter_selected_class: "selected" } do %> + <%= render "events/filter/header" %> + <%= render "events/filter/new_collection_option" %> + <% if Current.user.collections.count > 15 %> + + <% end %> + <%= form_with url: events_path, method: :get, class: "flex flex-column max-width popup__list full-width", + data: { controller: "form" } do |form| %> + <%= render "events/filter/all_collections_option", form: form, filter: filter %> + + <%= render partial: "events/filter/collection_option", collection: Current.user.collections, as: :collection, locals: { form: form, filter: filter } %> + + <% end %> + +

    + Press ⌘j anytime to open this, esc to close. +

    + <% end %>
    diff --git a/app/views/events/days/index.turbo_stream.erb b/app/views/events/days/index.turbo_stream.erb index 984b01ea3..cceb2d352 100644 --- a/app/views/events/days/index.turbo_stream.erb +++ b/app/views/events/days/index.turbo_stream.erb @@ -1,7 +1,7 @@ <%= turbo_stream.append :activity do %> - <%= render "events/day", activity_day: @activity_day, next_day: @next_day, events: @events %> + <%= render "events/day", day_timeline: @day_timeline %> <% end %> <%= turbo_stream.replace :next_page do %> - <%= event_next_page_link(@next_day) %> + <%= event_next_page_link(@day_timeline.next_day) %> <% end %> diff --git a/app/views/events/event/_card.html.erb b/app/views/events/event/_card.html.erb new file mode 100644 index 000000000..74864434f --- /dev/null +++ b/app/views/events/event/_card.html.erb @@ -0,0 +1 @@ +<%= render "events/event/layout", card: event.eventable, event: event %> diff --git a/app/views/events/event/_comment.html.erb b/app/views/events/event/_comment.html.erb new file mode 100644 index 000000000..22f4596e8 --- /dev/null +++ b/app/views/events/event/_comment.html.erb @@ -0,0 +1,5 @@ +<%= render "events/event/layout", card: event.eventable.card, event: event do %> + + <%= strip_tags(event&.eventable.body_html).truncate(200) -%> + +<% end %> diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb new file mode 100644 index 000000000..acfbf7494 --- /dev/null +++ b/app/views/events/event/_layout.html.erb @@ -0,0 +1,29 @@ +<%= link_to event.notifiable_target, + class: "event event--#{ event.action } panel center center-block flex-inline align-start justify-start gap position-relative", + style: "--card-color: #{ card.color }; background-color: color-mix(in srgb, var(--card-color) 10%, var(--color-bg)); + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));", + data: { related_element_target: "related", + related_element_group_value: card.id, + action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %> +
    + <%= avatar_image_tag(event.creator) %> +
    + +
    +
    + + <%= event_action_sentence(event) %> + + + <% if event_column(event) == 2 %> + + <%= icon_tag event_action_icon(event) %> + + <% end %> +
    + + <%= yield %> + + <%= event.collection.name %> +
    +<% end %> diff --git a/app/views/events/filter/_all_collections_option.html.erb b/app/views/events/filter/_all_collections_option.html.erb new file mode 100644 index 000000000..71f94c20e --- /dev/null +++ b/app/views/events/filter/_all_collections_option.html.erb @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/app/views/events/filter/_collection_option.html.erb b/app/views/events/filter/_collection_option.html.erb new file mode 100644 index 000000000..6661dc5c2 --- /dev/null +++ b/app/views/events/filter/_collection_option.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/views/events/filter/_header.html.erb b/app/views/events/filter/_header.html.erb new file mode 100644 index 000000000..48039a51a --- /dev/null +++ b/app/views/events/filter/_header.html.erb @@ -0,0 +1,10 @@ +
    + + <%= icon_tag "filter" %> + + Collections + +
    diff --git a/app/views/events/filter/_new_collection_option.html.erb b/app/views/events/filter/_new_collection_option.html.erb new file mode 100644 index 000000000..3064516cd --- /dev/null +++ b/app/views/events/filter/_new_collection_option.html.erb @@ -0,0 +1,7 @@ + +
    diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 86ba20d61..2c4678fde 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -6,7 +6,7 @@

    <%= Account.sole.name %> / Latest activity

    - <%= render "events/filter" %> + <%= render "events/filter", filter: @filter %>
    <%= link_to users_path, class: "btn flex-item-justify-end" do %> @@ -17,36 +17,10 @@ <% end %>
    -
    -

    Custom views

    -
    - <% @filters.each do |filter| %> -

    - <%= link_to filter.summary, cards_path(**filter.as_params) %> -

    - <% end %> -
    -
    - <%= render "events/day", activity_day: @activity_day, next_day: @next_day, events: @events %> -
    - <%= event_next_page_link(@next_day) %> -
    - -
    -
    -

    Collections

    - <%= link_to new_collection_path, class: "btn txt-xx-small flex-item-justify-end flex-item-no-shrink", style: "view-transition-name: new-collection" do %> - <%= icon_tag "add" %> - New - <% end %> -
    - -
    - <% @collections.each do |collection| %> -

    <%= link_to collection.name, cards_path(collection_ids: [ collection ]) %>

    - <% end %> + <%= render "events/day", day_timeline: @day_timeline %>
    + <%= event_next_page_link(@day_timeline.next_day) %>
    diff --git a/app/views/my/pins/_pin.html.erb b/app/views/my/pins/_pin.html.erb new file mode 100644 index 000000000..66d177b28 --- /dev/null +++ b/app/views/my/pins/_pin.html.erb @@ -0,0 +1,3 @@ +
    + <%= render "cards/display/preview", card: pin.card %> +
    diff --git a/app/views/my/pins/_tray.html.erb b/app/views/my/pins/_tray.html.erb index 70c1d5a16..ddd39fdfe 100644 --- a/app/views/my/pins/_tray.html.erb +++ b/app/views/my/pins/_tray.html.erb @@ -1,4 +1,4 @@ -<%= turbo_stream_from Current.user, :pins %> +<%= turbo_stream_from Current.user, :pins_tray %> <%= tag.dialog id: "pin-tray", class: "tray pin-tray", data: { controller: "dialog", turbo_permanent: true, dialog_modal_value: false, dialog_target: "dialog", action: "keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" } do %> diff --git a/app/views/my/pins/index.html.erb b/app/views/my/pins/index.html.erb index 519070aec..bee9fe10b 100644 --- a/app/views/my/pins/index.html.erb +++ b/app/views/my/pins/index.html.erb @@ -1,7 +1,3 @@ <%= turbo_frame_tag "pins" do %> - <% @pins.map(&:card).each do |card| %> -
    - <%= render partial: "cards/display/preview", locals: { card: card } %> -
    - <% end %> + <%= render partial: "my/pins/pin", collection: @pins %> <% end %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 8cb22eeb1..36ec74370 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -5,15 +5,7 @@
    - <%= notification_title(notification) %> -
    - <% if notification.event.action == "commented" %> - <%= "#{strip_tags(notification.event.comment.body_html).blank? ? "#{notification.event.creator.name} replied" : "#{notification.event.creator.name}:" } #{strip_tags(notification.event.comment.body_html).truncate(200)}" %> - <% else %> - <%= notification_body(notification) %> - <% end %> -
    -
    <%= notification.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
    + <%= render "notifications/notification/#{notification.source_type.underscore}", notification: notification %>
    <% end %> <% end %> diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index bf83657ee..4b76c0779 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -13,7 +13,7 @@ <% end %> -
    +
    <% if @unread.any? %>

    New for you

    @@ -22,15 +22,13 @@ <%= render partial: "notifications/notification", collection: @unread, cached: true %> <% else %> - <%# FIXME: Need to reveal this section if there are unread notification records in it %> -
    +
    Nothing new for you.
    <% end %>
    -<%# FIXME: Need to hide this section if there are no notification records in it, but then it needs to appear when any are added %> -
    +

    Previously seen

    diff --git a/app/views/notifications/notification/_event.html.erb b/app/views/notifications/notification/_event.html.erb new file mode 100644 index 000000000..2dd8d16ee --- /dev/null +++ b/app/views/notifications/notification/_event.html.erb @@ -0,0 +1,7 @@ +<% event = notification.source %> + +<%= event_notification_title(event) %> +
    + <%= event_notification_body(event) %> +
    +
    <%= notification.source.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
    diff --git a/app/views/notifications/notification/_mention.html.erb b/app/views/notifications/notification/_mention.html.erb new file mode 100644 index 000000000..7ed8ffdc5 --- /dev/null +++ b/app/views/notifications/notification/_mention.html.erb @@ -0,0 +1,7 @@ +<% mention = notification.source %> + +<%= mention.mentioner.first_name %> mentioned you +
    + <%= mention.source.mentionable_content.truncate(200) %> +
    +
    <%= local_datetime_tag(notification.created_at, style: :ago) %>
    diff --git a/app/views/terminals/_event.html.erb b/app/views/terminals/_event.html.erb index fd67cd405..041ca94c4 100644 --- a/app/views/terminals/_event.html.erb +++ b/app/views/terminals/_event.html.erb @@ -1,5 +1,5 @@
  • - <%= link_to event_action_sentence(event), collection_card_path(event.card.collection, event.card), - class: "terminal__link", - data: { turbo_frame: "_top" } %> -
  • \ No newline at end of file + <%= link_to event_action_sentence(event), collection_card_path(event.collection, event.eventable), + class: "terminal__link", + data: { action: "toggle-class#remove", turbo_frame: "_top" } %> + diff --git a/app/views/terminals/edit.html.erb b/app/views/terminals/edit.html.erb index 38496e662..735162d56 100644 --- a/app/views/terminals/edit.html.erb +++ b/app/views/terminals/edit.html.erb @@ -12,8 +12,7 @@ autocapitalize: "off", class: "terminal__input input fill-transparent unpad", data: { - controller: "filter", - action: "keydown.up->toggle-class#toggle:prevent input->filter#filter keydown.esc->form#cancel:prevent" + action: "keydown.up->toggle-class#add:prevent keydown.down->toggle-class#remove:prevent keydown.esc->form#cancel:prevent" }, id: "fizzy_do", placeholder: "Search or type commands…" %> diff --git a/app/views/terminals/show.html.erb b/app/views/terminals/show.html.erb index 58a7f663d..843d79039 100644 --- a/app/views/terminals/show.html.erb +++ b/app/views/terminals/show.html.erb @@ -5,7 +5,7 @@ <%= turbo_frame_tag "terminal_form" do %> - <%= link_to edit_terminal_path, class: "terminal__link flex align-center gap-half min-width", data: { action: "keydown.meta+j@document->hotkey#click", controller: "hotkey" } do %> + <%= link_to edit_terminal_path, class: "terminal__link flex align-center gap-half min-width", data: { action: "keydown.meta+shift+p@document->hotkey#click", controller: "hotkey" } do %> Fizzy do > Search or type commands… <% end %> diff --git a/app/views/workflows/stages/_color.html.erb b/app/views/workflows/stages/_color.html.erb index a5e1657b5..b51ae516d 100644 --- a/app/views/workflows/stages/_color.html.erb +++ b/app/views/workflows/stages/_color.html.erb @@ -13,7 +13,7 @@ <%= form_with model: stage, url: workflow_stage_path(stage.workflow, stage), class: "flex gap-half", data: { controller: "form", action: "change->form#submit" } do |form| %> - <% Colorable::COLORS.each do |color| %> + <% Card::COLORS.each do |color| %>