diff --git a/Gemfile b/Gemfile index c5ac64538..c72bd8326 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem "rqrcode" gem "redcarpet" gem "rouge" gem "jbuilder" -gem "lexxy", bc: "lexxy" +gem "lexxy" gem "image_processing", "~> 1.14" gem "platform_agent" gem "aws-sdk-s3", require: false @@ -40,10 +40,13 @@ gem "sentry-ruby" gem "sentry-rails" gem "rails_structured_logging", bc: "rails-structured-logging" gem "yabeda" -gem "yabeda-rails" -gem "yabeda-puma-plugin" -gem "webrick" # required for yabeda-prometheus metrics server +gem "yabeda-activejob", github: "rosa/yabeda-activejob", branch: "support-end-time-in-seconds" +gem "yabeda-gc" +gem "yabeda-http_requests" gem "yabeda-prometheus-mmap" +gem "yabeda-puma-plugin" +gem "yabeda-rails" +gem "webrick" # required for yabeda-prometheus metrics server gem "prometheus-client-mmap", "~> 1.1" # AI diff --git a/Gemfile.lock b/Gemfile.lock index 0f362ba60..2a01957cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,13 +5,6 @@ GIT specs: file_repository (1.4.6) -GIT - remote: https://github.com/basecamp/lexxy - revision: 3babb794da31cd947f9316f5dac78a718d586554 - specs: - lexxy (0.1.7.beta) - rails (>= 8.0.2) - GIT remote: https://github.com/basecamp/portfolio revision: 86411a53d19a42ce44705a50ebe7c22828597991 @@ -197,6 +190,15 @@ GIT tsort (>= 0.2) zeitwerk (~> 2.6) +GIT + remote: https://github.com/rosa/yabeda-activejob.git + revision: 08207e7dddf6f4dd1d039c1ddf792b35655a3feb + branch: support-end-time-in-seconds + specs: + yabeda-activejob (0.5.0) + rails (>= 5.2) + yabeda (~> 0.6) + PATH remote: gems/fizzy-saas specs: @@ -357,6 +359,8 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) + lexxy (0.1.8.beta) + rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) loofah (2.24.1) @@ -548,6 +552,9 @@ GEM sentry-ruby (5.27.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + sniffer (0.5.0) + anyway_config (>= 1.0) + dry-initializer (~> 3) solid_cable (3.0.12) actioncable (>= 7.2) activejob (>= 7.2) @@ -596,7 +603,7 @@ GEM tiktoken_ruby (0.0.12-x86_64-linux) timeout (0.4.3) tsort (0.2.0) - turbo-rails (2.0.16) + turbo-rails (2.0.17) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -627,6 +634,11 @@ GEM anyway_config (>= 1.0, < 3) concurrent-ruby dry-initializer + yabeda-gc (0.4.0) + yabeda (~> 0.6) + yabeda-http_requests (0.2.1) + sniffer + yabeda yabeda-prometheus-mmap (0.4.0) prometheus-client-mmap yabeda (~> 0.10) @@ -666,7 +678,7 @@ DEPENDENCIES jbuilder kamal letter_opener - lexxy! + lexxy mission_control-jobs mocha mysql2! @@ -704,6 +716,9 @@ DEPENDENCIES webmock webrick yabeda + yabeda-activejob! + yabeda-gc + yabeda-http_requests yabeda-prometheus-mmap yabeda-puma-plugin yabeda-rails diff --git a/app/assets/images/tag-outline.svg b/app/assets/images/tag-outline.svg new file mode 100644 index 000000000..1c8fab206 --- /dev/null +++ b/app/assets/images/tag-outline.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/tag.svg b/app/assets/images/tag.svg index 76591fe36..aa0368613 100644 --- a/app/assets/images/tag.svg +++ b/app/assets/images/tag.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index 2f9511f90..1f1ebb51b 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -43,6 +43,10 @@ 0 0.4em 0.4em oklch(var(--lch-black) / 5%), 0 0.8em 0.8em oklch(var(--lch-black) / 5%); + /* Patterns */ + --wavy-background-mask: radial-gradient(6px at 50% calc(100% + 3px), transparent calc(99% - 2px), black calc(101% - 2px) 99%, transparent 101%) calc(50% - 8px) calc(50% - 3px + .5px)/16px 6px, + radial-gradient(6px at 50% -3px, transparent calc(99% - 2px), black calc(101% - 2px) 99%, transparent 101%) 50% calc(50% + 3px)/16px 6px; + /* Components */ --btn-size: 2.65em; --footer-height: 3.5rem; @@ -58,6 +62,8 @@ /* Easing functions from https://easingwizard.com/ */ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-out-overshoot: cubic-bezier(0.25, 1.75, 0.5, 1); + --ease-out-overshoot-subtle: cubic-bezier(0.25, 1.25, 0.5, 1); @media (max-width: 799px) { --tray-size: var(--footer-height); @@ -205,7 +211,7 @@ /* Colors: Cards */ --color-card-default: oklch(var(--lch-blue-dark)); - --color-card-complete: var(--color-ink); + --color-card-complete: var(--color-ink-darker); --color-card-1: oklch(var(--lch-ink-medium)); --color-card-2: oklch(var(--lch-uncolor-medium)); --color-card-3: oklch(var(--lch-yellow-medium)); diff --git a/app/assets/stylesheets/bubble.css b/app/assets/stylesheets/bubble.css index e334b8582..8bcf6f4d6 100644 --- a/app/assets/stylesheets/bubble.css +++ b/app/assets/stylesheets/bubble.css @@ -52,24 +52,4 @@ position: absolute; text-align: center; } - - .bubble--doing, - .bubble--on_deck { - inset-inline-start: 100%; - translate: -60% -60%; - - .card-perma__bg & { - --bubble-number-max: 42px; - --bubble-size: 6rem; - - inset: calc(var(--bubble-size) / -4) calc(var(--bubble-size) / 1.5) auto auto; - translate: 0 0 !important; - - @media (max-width: 800px) { - --bubble-size: 4.5rem; - - inset: calc(var(--bubble-size) / 1.5) 0 auto auto; - } - } - } } diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index cc33e509a..80573c4d6 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -1,113 +1,100 @@ @layer components { - /* Columns + /* Column container /* ------------------------------------------------------------------------ */ - /* Card contents are resized via container queries. We're using #main here as - the primary container so all cards have the same container reference */ - #main:has(.card-columns) { - --cards-gap: min(1.5cqi, 1.5rem); - --gradient-border: linear-gradient(to right,var(--color-canvas), var(--color-ink) 3%, var(--color-ink) 97%, var(--color-canvas)); - --reserved-bubble-space: 0; - --bubble-gap: 0.5rem; - --bubble-size: 3.5rem; - - container-type: inline-size; - max-inline-size: var(--main-width); - - @media (max-width: 639px) { - --bubble-gap: -0.5rem; - --bubble-size: 3rem; - } - } - .card-columns { - margin: auto; - max-inline-size: 100%; - padding-inline: var(--reserved-bubble-space); + --bubble-size: 3.5rem; + --cards-gap: min(1.7cqi, 1.7rem); + --column-gap: 8px; + --column-width-collapsed: 40px; + --column-width-expanded: 450px; + --column-transition: 350ms var(--ease-out-expo); + --progress-increment: var(--progress-max-height) / var(--progress-max-cards); + --progress-max-cards: 20; + --progress-max-height: 50dvh; + + align-items: stretch; + container-type: inline-size; + display: grid; + gap: var(--column-gap); + grid-template-columns: 1fr var(--column-width-expanded) 1fr; + justify-content: center; + margin: var(--cards-gap) auto 0; + max-inline-size: var(--main-width); position: relative; - transition: 300ms ease-in-out; + + &:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) { + grid-template-columns: auto auto auto; + } &:has(.cards) { min-block-size: 20lh; } - &:before { - background: var(--gradient-border); - block-size: 1px; - content: ""; - inset: 0 -0.6em 100% -0.6em; - opacity: 0.15; - position: absolute; - z-index: 1; - } - - @media (min-width: 640px) { - display: grid; - grid-template-columns: repeat(3, 1fr); + @media (max-width: 639px) { + --bubble-size: 3rem; } } - /* Cards + .card-columns__left, + .card-columns__right { + align-items: stretch; + display: flex; + gap: var(--column-gap); + } + + .card-columns__left { + justify-content: end; + } + + .card-columns__right { + justify-content: start; + } + + /* Column /* ------------------------------------------------------------------------ */ .cards { + --column-color: color-mix(in srgb, var(--card-color) 15%, var(--color-canvas)); + align-items: stretch; display: flex; flex-direction: column; gap: var(--cards-gap); justify-content: start; - min-inline-size: 0; - padding: 0 var(--cards-gap) var(--cards-gap); + max-inline-size: var(--column-width-expanded); + min-inline-size: var(--column-width-expanded); position: relative; + scroll-snap-align: start; + transition: var(--column-transition); /* Make sure filter dialogs don't end up behind sticky headers */ &:has([open]) { z-index: 3; } - @media (max-width: 639px) { - padding-block-end: calc(var(--cards-gap) * 2); - padding-inline-start: var(--cards-gap); - } + &.is-collapsed { + border-radius: 99rem; + flex: 0 0 var(--column-width-collapsed); + inline-size: var(--column-width-collapsed); + min-inline-size: 0; - .card { - --block-space: 1em; - --block-space-half: 0.5em; - --text-xx-large: 1.6em; - --text-x-small: 1em; - - /* Set lower limit for font size */ - font-size: clamp(0.6rem, 0.85cqi, 100px); - } - - .card__title { - line-height: 1.2; - pointer-events: none; - } - - .card__link { - z-index: 1; - } - - .card__hide-on-index { - display: none; - } - - .card:has(.card__background img:not([src=""])) { - .card__content, - .card__meta, - .workflow-stage:not(.workflow-stage--current) { - opacity: 0; - transition: opacity 0.2s ease-in-out; - - @media (any-hover: hover) { - .card:hover & { - opacity: 1; - } - } + .blank-slate, + .pagination-link, + .card { + display: none; } } + &:not(.is-collapsed) { + padding-inline: calc(var(--column-gap) * 2); + } + + .bubble { + inset-inline-start: 100%; + translate: -60% -60%; + } + @media (any-hover: hover) { .card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) { filter: blur(3px) brightness(1.2); @@ -120,22 +107,151 @@ } } - .cards--related:not(:hover):not(.drag-and-drop__hover-container) { - .card { - box-shadow: 0 0 1px var(--color-ink-medium); - opacity: 0.9; - transition: box-shadow 450ms ease-in-out, opacity 150ms ease-in-out; + /* Used when filtering */ + .cards--grid { + --cards-gap: 1rem; + --card-grid-columns: 1; - @media (prefers-color-scheme: dark) { - opacity: 0.8; - transition: opacity 150ms ease-in-out; + container-type: inline-size; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + margin-inline: auto; + max-inline-size: var(--main-width); + + @media (min-width: 640px) { + --card-grid-columns: 2; + } + + @media (min-width: 960px) { + --card-grid-columns: 3; + } + + .card { + inline-size: calc((100% - var(--cards-gap) * (var(--card-grid-columns) - 1) ) / var(--card-grid-columns)); + } + } + + /* Column Elements + /* ------------------------------------------------------------------------ */ + + .cards__expander { + align-items: center; + border-radius: 99rem; + cursor: pointer; + display: flex; + flex-direction: row-reverse; + font-size: var(--text-x-small); + font-weight: 600; + gap: 0.5ch; + position: relative; + text-transform: uppercase; + transition: 100ms ease-out; + + @media (any-hover: hover) { + &:hover { + filter: brightness(0.9); + } + } + + .cards.is-collapsed & { + block-size: 100%; + flex-direction: column; + inline-size: var(--column-width-collapsed); + justify-content: start; + letter-spacing: 0.05em; + + /* Guitar string */ + &:before { + background-color: var(--column-color); + block-size: 100%; + content: ""; + inline-size: 1px; + inset: 0 auto; + position: absolute; + z-index: -2; + } + + /* Progress */ + &:after { + background: var(--column-color); + block-size: calc(var(--column-width-collapsed) + var(--card-count) * var(--progress-increment)); + border-radius: 99rem; + content: ""; + inline-size: 100%; + inset: 0 0 auto; + max-block-size: var(--progress-max-height); + min-block-size: var(--column-width-collapsed); + position: absolute; + transition: block-size 500ms var(--ease-out-overshoot); + z-index: -1; + } + } + + .cards:not(.is-collapsed) & { + display: grid; + grid-template-columns: auto 1fr auto; + inline-size: 100%; + margin-block-end: calc(-0.5 * var(--cards-gap)); + + &:hover, + &:has(~ .cards__expander-menu:hover) { + background-color: var(--column-color); + filter: none; } } } - /* Column elements - /* ------------------------------------------------------------------------ */ + .cards__expander-count { + line-height: var(--column-width-collapsed); + inline-size: var(--column-width-collapsed); + .cards:not(.is-collapsed) & { + opacity: 0; + } + } + + .cards__expander-title { + font-weight: inherit; + font-size: inherit; + grid-column: 2; + line-height: var(--column-width-collapsed); + + .cards.is-collapsed & { + writing-mode: vertical-rl; + } + } + + .cards__expander-menu { + inset: 0 0 auto auto; + position: absolute; + translate: calc(var(--column-gap) * -2); + z-index: var(--z-popup); + + .btn--circle { + --btn-background: transparent; + + block-size: var(--column-width-collapsed); + inline-size: var(--column-width-collapsed); + } + + .panel { + --panel-padding: 1ch; + } + + .cards.is-collapsed & { + display: none; + } + } + + .blank-slate--card { + box-shadow: none !important; + border: 2px dashed color-mix(in srgb, var(--card-color) 40%, var(--color-canvas));; + font-weight: 600; + rotate: 2deg; + } + + /* TODO: I think this is legacy now? */ .cards__heading { display: flex; font-size: var(--text-medium); @@ -150,7 +266,7 @@ /* Cover bubbles and cards on small viewports */ @media (max-width: 639px) { &:not(.cards--closed &) { - margin-inline: calc(-1 * var(--cards-gap) - var(--reserved-bubble-space) - var(--main-padding)); + margin-inline: calc(-1 * var(--cards-gap) - var(--main-padding)); } } @@ -175,6 +291,84 @@ } } + /* Override card styles within columns + /* ------------------------------------------------------------------------ */ + + .cards { + .card { + --block-space: 1em; + --block-space-half: 0.5em; + --card-padding-inline: 1em; + --text-xx-large: 1.6em; + --text-x-small: 1em; + + /* Set lower limit for font size */ + font-size: clamp(0.6rem, 0.85cqi, 100px); + } + + .card__tags { + gap: calc(var(--card-header-space) / 2); + } + + .card__title { + line-height: 1.2; + pointer-events: none; + } + + .card__link { + z-index: 1; + } + + .card__stages, + .card__hide-on-index { + display: none; + } + + .card__body { + padding-block: calc(var(--card-padding-block) * 0.75) var(--card-padding-block); + } + + .card__meta { + font-weight: 600; + + strong, + .local-time-value { + font-weight: inherit; + } + } + + .card:has(.card__background img:not([src=""])) { + .card__content, + .card__meta, + .workflow-stage:not(.workflow-stage--current) { + opacity: 0; + transition: opacity 0.2s ease-in-out; + + @media (any-hover: hover) { + .card:hover & { + opacity: 1; + } + } + } + } + + .bubble { + inset-inline-start: 100%; + translate: -60% -60%; + } + + @media (any-hover: hover) { + .card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) { + filter: blur(3px) brightness(1.2); + opacity: 0.2; + } + } + + .card__steps { + display: flex; + } + } + /* Considering /* ------------------------------------------------------------------------ */ @@ -187,7 +381,6 @@ color-mix(in srgb, var(--color-considering) var(--gradient-opacity), transparent) 33%, color-mix(in srgb, var(--color-considering) var(--gradient-opacity), transparent) 66%, transparent 100%); - padding-block-start: 2em; @supports(mask: radial-gradient(black)) { &:before { @@ -198,8 +391,7 @@ color-mix(in oklch, var(--color-considering) 0%, transparent) 100%); content: ""; inset: 0; - mask: radial-gradient(6px at 50% calc(100% + 3px), transparent calc(99% - 2px), black calc(101% - 2px) 99%, transparent 101%) calc(50% - 8px) calc(50% - 3px + .5px)/16px 6px, - radial-gradient(6px at 50% -3px, transparent calc(99% - 2px), black calc(101% - 2px) 99%, transparent 101%) 50% calc(50% + 3px)/16px 6px; + mask: var(--wavy-background-mask); position: absolute; z-index: 0; } @@ -269,52 +461,35 @@ &:nth-child(4n) { rotate: 0.5deg; } } - .card--new { - --border-color: var(--color-selected-dark); - --border-size: 1px; - --card-padding-block: var(--block-space); + .card__collection { + background-color: transparent; + color: var(--card-text-color); + } - border: 1px solid var(--color-selected-dark); - text-align: center; + /* The following groups the header and title on considering cards */ + .card__header { + color: var(--color-ink); + padding-block-start: calc(var(--card-padding-block) / 2); + } - .btn { - &.btn--link { - font-size: 1.2em; - margin-block-start: 0.1em; - } - - &:not(.btn--link) { - border: 0; - color: var(--color-link); - } - } - - footer { - font-size: var(--text-x-small); - margin-block-end: calc(var(--block-space-half) * -1); - } - - .overflow-count { - font-size: 1.2em; - font-weight: 500; - padding: 0.5em 0.3em; - } + .card__collection, + .card__tags { + color: inherit; } .card__body { - padding-block-start: 0; - padding-inline: 0; + padding-block: 0 var(--card-padding-block); } + /* End */ + /* The Uncomment the following to add borders and such */ + /* .card__collection { - background-color: transparent; - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - } - - .card__collection-name { - border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + border-block-end: var(--card-border); + border-inline-end: var(--card-border); } + */ + /* End */ .card--drafted { --card-color: var(--color-ink-medium) !important; @@ -324,11 +499,6 @@ box-shadow: none !important; } - .card__header { - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - margin-inline: calc(-0.5 * var(--card-padding-inline)) calc(-0.25 * var(--card-padding-inline)); - } - .card__people-label { display: none; } @@ -336,38 +506,74 @@ .card__title { min-block-size: 0; } + } - .card__tags { - font-size: var(--text-small); + + .card.card--new { + --border-color: var(--color-selected-dark); + --border-size: 1px; + --card-padding-block: var(--block-space); + + border: 1px solid var(--color-selected-dark); + text-align: center; + + .btn--link { + font-size: 1.2em; + } + + .btn:not(.btn--link, .btn--circle) { + border: 0; + color: var(--color-link); + } + + footer { + font-size: var(--text-x-small); + margin-block-end: calc(var(--block-space-half) * -1); + } + + .overflow-count { + font-size: 1.2em; + font-weight: 500; + padding: 0.5em 0.3em; } } - /* On Deck + /* On Deck (Not Now) /* ------------------------------------------------------------------------ */ .cards--on-deck { - @media (min-width: 640px) { - padding-inline: var(--cards-gap); + --column-color: var(--color-ink-lighter); + + .card { + --card-color: var(--color-card-complete) !important; + } + + .bubble { + display: none !important; } } /* Doing - /* ------------------------------------------------------------------------ */ + /* -------------------------------------------------------------------------- */ - .cards--doing { - @media (min-width: 640px) { - padding-inline: var(--cards-gap); - } + /* Surface a mini bubble if there are cards with bubbles inside */ + .cards--doing.is-collapsed:has(.bubble:not([hidden])) { + --bubble-color: var(--card-color, oklch(var(--lch-blue-medium))); + --bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%; - @media (max-width: 639px) { - &::before { - background: var(--gradient-border); - block-size: 1px; - content: ""; - inset: 0 0 auto; - opacity: 0.15; - position: absolute; - } + &:before { + background: radial-gradient( + color-mix(in srgb, var(--bubble-color) 8%, var(--color-canvas)) 50%, + color-mix(in srgb, var(--bubble-color) 48%, var(--color-canvas)) 100% + ); + block-size: 1em; + border-radius: var(--bubble-shape); + content: ""; + inline-size: 1em; + inset: 0 0 auto auto; + position: absolute; + translate: 25% -25%; + z-index: 1; } } @@ -375,83 +581,6 @@ /* ------------------------------------------------------------------------ */ .cards--closed { - justify-content: center; - flex-direction: row; - flex-wrap: wrap; - inline-size: 100%; - margin: 0 auto; - position: relative; - - &:before { - background: var(--gradient-border); - block-size: 1px; - content: ""; - inset: 0 -0.6em 100% -0.6em; - opacity: 0.15; - position: absolute; - } - - @media (max-width: 639px) { - flex-direction: column; - padding-inline: var(--reserved-bubble-space); - } - - .cards__heading { - min-inline-size: 100%; - - @media (max-width: 639px) { - margin-inline-end: calc(-1 * var(--cards-gap)); - } - } - - .card { - @media (min-width: 640px) { - inline-size: calc((100% - var(--cards-gap)) / 2); - } - - @media (min-width: 960px) { - inline-size: calc((100% - var(--cards-gap) * 2) / 3); - } - - &:nth-of-type(7) { - margin-block-start: var(--cards-gap); - } - - &:nth-of-type(n+7) { - background-color: var(--color-canvas); - border: none; - border-block-end: 1px solid var(--color-ink-lighter); - box-shadow: none; - inline-size: 100%; - margin-inline: auto; - max-inline-size: 110ch; - padding: 0 0 var(--cards-gap); - - .card__meta, - .card__closed-by-line, - .card__closed-date { - display: none; - } - - .card__body { - padding-block-end: 0; - } - - .card__closed { - border-width: 0.5ch; - font-size: 0.75em; - inset: auto 0 var(--cards-gap) auto; - rotate: 0deg; - } - - .card__header { - margin: 0; - } - - .card__title { - min-block-size: 0; - } - } - } + --column-color: var(--color-ink-lighter); } } diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index e589d4333..4cfaafc2a 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -17,7 +17,7 @@ "notch-bottom notch-bottom notch-bottom"; grid-template-columns: 48px minmax(0, 1120px) 48px; inline-size: fit-content; - margin-block-start: 2rem; + margin-block-start: var(--block-space); max-inline-size: 100%; margin-inline: auto; position: relative; @@ -91,6 +91,20 @@ } } + .bubble { + --bubble-number-max: 42px; + --bubble-size: 6rem; + + inset: calc(var(--bubble-size) / -4) calc(var(--bubble-size) / 1.5) auto auto; + translate: 0 0; + + @media (max-width: 800px) { + --bubble-size: 4.5rem; + + inset: calc(var(--bubble-size) / 1.5) 0 auto auto; + } + } + @media (max-width: 800px) { --padding-inline: var(--main-padding); diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index f0ae80af0..84046527f 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -5,6 +5,9 @@ .card { --avatar-size: 2.75em; --card-bg-color: color-mix(in srgb, var(--card-color) 4%, var(--color-canvas)); + --card-text-color: color-mix(in srgb, var(--card-color) 75%, var(--color-ink)); + --card-border: 1px solid color-mix(in srgb, var(--card-color) 33%, var(--color-ink-inverted)); + --card-header-space: 1ch; --card-padding-inline: var(--inline-space-double); --card-padding-block: var(--block-space); --border-color: transparent; @@ -15,7 +18,6 @@ background-color: var(--card-bg-color); border-radius: var(--border-radius); box-shadow: var(--shadow); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); display: flex; flex-direction: column; inline-size: 100%; @@ -41,7 +43,7 @@ border-radius: var(--border-radius) 0 0 0; display: flex; flex-wrap: nowrap; - gap: var(--inline-space); + gap: var(--card-header-space); margin-block-start: calc(-1 * var(--card-padding-block)); margin-inline: calc(-1 * var(--card-padding-inline)) calc(-0.5 * var(--card-padding-inline)); max-inline-size: unset; @@ -52,25 +54,16 @@ } } - .card__id { - flex-shrink: 0; - margin-inline-start: calc(var(--inline-space) * -1); - - .card-perma &:before { - content: "No."; - opacity: 0.5; - } - } - .card__collection { background-color: var(--card-color); border-radius: var(--border-radius) 0 var(--border-radius) 0; color: var(--color-ink-inverted); display: inline-flex; - font-weight: 800; + font-weight: 600; max-inline-size: 100%; min-inline-size: 0; - padding: var(--block-space-half) var(--inline-space) var(--block-space-half) var(--card-padding-inline); + padding-block: 0.25lh; + padding-inline: var(--card-padding-inline) 1ch; &:has(.collection-picker__button) { cursor: pointer; @@ -89,34 +82,43 @@ } } - .card__collection-name { - border-inline-start: 1px solid color-mix(in srgb, var(--color-ink-inverted) 33%, var(--card-color)); - color: currentcolor; - display: flex; - margin-inline-start: var(--inline-space-half); - max-inline-size: 100%; - min-inline-size: 0; - padding-inline-start: calc(var(--inline-space) * 0.75); - text-transform: uppercase; + .card__id { + flex-shrink: 0; - .cards--considering & { - border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + .card-perma &:before { + content: "No."; + opacity: 0.5; } } - .card__tag { - color: var(--card-color); - font-weight: 700; + .card__collection-name { + border-inline-start: 1px solid color-mix(in hsl, transparent 75%, currentColor); + color: currentColor; + display: flex; + margin-inline-start: var(--card-header-space); + max-inline-size: 100%; + min-inline-size: 0; + padding-inline-start: var(--card-header-space); + text-transform: uppercase; } .card__tags { + --btn-color: var(--card-color); + align-items: center; - color: var(--card-color); + color: var(--card-text-color); display: flex; + gap: var(--card-header-space); min-inline-size: 0; } + .card__tag { + color: inherit; + font-weight: 600; + min-width: 0; + text-transform: uppercase; + } + /* Body /* ------------------------------------------------------------------------ */ @@ -133,6 +135,7 @@ } .card__content { + color: var(--color-ink); contain: inline-size; flex: 2 1 auto; max-inline-size: 100%; @@ -140,13 +143,12 @@ .card__title { --input-border-radius: 0; - --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); --lines: 3; - color: inherit; + color: var(--color-ink); font-size: var(--text-xx-large); font-weight: 900; - line-height: 1.3; + line-height: 1.1; text-wrap: balance; @supports not (field-sizing: content) { @@ -187,9 +189,11 @@ } .card__stages { + color: var(--card-text-color); display: flex; flex: 0 1 auto; flex-direction: column; + gap: 2px; justify-self: end; max-inline-size: 20ch; padding-block: var(--block-space-half); @@ -199,11 +203,11 @@ /* ------------------------------------------------------------------------ */ .card__meta { - --meta-border: 1px solid color-mix(in srgb, var(--card-color) 50%, transparent); --meta-spacer-block: 0.5ch; --meta-spacer-inline: 0.75ch; align-items: center; + color: var(--card-text-color); display: grid; font-size: var(--text-x-small); font-weight: 500; @@ -259,7 +263,7 @@ /* Top */ .card__meta-text:nth-of-type(odd) { - border-block-end: var(--meta-border); + border-block-end: var(--card-border); padding-block-end: var(--meta-spacer-block); } @@ -270,7 +274,7 @@ /* Left */ .card__meta-text:nth-of-type(-n+2) { - border-inline-end: var(--meta-border); + border-inline-end: var(--card-border); padding-inline-end: var(--meta-spacer-inline); } @@ -376,6 +380,7 @@ --card-padding-block: 1ch; background-color: var(--color-canvas); + color: var(--color-ink); &.card--closed { --card-color: var(--color-card-complete) !important; diff --git a/app/assets/stylesheets/dividers.css b/app/assets/stylesheets/dividers.css index 83759c1ba..8583b7f15 100644 --- a/app/assets/stylesheets/dividers.css +++ b/app/assets/stylesheets/dividers.css @@ -14,4 +14,9 @@ flex: 1; } } + + .divider--fade { + &:before { background: linear-gradient(to right, transparent, var(--divider-color) 50%); } + &:after { background: linear-gradient(to left, transparent, var(--divider-color) 50%); } + } } diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index e10454be5..7eeedc3a5 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -1,13 +1,15 @@ @layer components { .filters { + display: flex; + align-items: center; + gap: 1ch; + justify-content: center; position: relative; view-transition-name: "filters"; - z-index: var(--z-popup); } #header:has(.filters) { position: relative; - z-index: var(--z-popup); } .filters { @@ -104,7 +106,9 @@ .filter__terms { --input-background: var(--color-canvas); - min-inline-size: 30ch; + .filters:not(.filters--expanded) & { + min-inline-size: 40ch; + } } .quick-filter { diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index 6eab28b1b..0f2e8177a 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -63,6 +63,8 @@ .icon--move { --svg: url("move.svg "); } .icon--notification-bell-access-only { --svg: url("bell.svg "); } .icon--notification-bell-watching { --svg: url("bell-off.svg "); } + .icon--notification-bell-reverse-access-only { --svg: url("bell-off.svg "); } + .icon--notification-bell-reverse-watching { --svg: url("bell.svg "); } .icon--password { --svg: url("password.svg "); } .icon--pencil { --svg: url("pencil.svg "); } .icon--person { --svg: url("person.svg "); } @@ -85,6 +87,7 @@ .icon--sliders { --svg: url("sliders.svg "); } .icon--switch { --svg: url("switch.svg "); } .icon--tag { --svg: url("tag.svg "); } + .icon--tag-outline { --svg: url("tag-outline.svg "); } .icon--thumb-up { --svg: url("thumb-up.svg "); } .icon--trash { --svg: url("trash.svg "); } .icon--unpinned { --svg: url("unpinned.svg"); } diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index f45420c30..6dc2b049e 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -6,7 +6,6 @@ --tray-duration: 350ms; --tray-margin: 0.5rem; --tray-radius: 0.25rem; - --tray-timing-function: cubic-bezier(0.25, 1.25, 0.5, 1); align-items: end; block-size: var(--footer-height); @@ -14,7 +13,7 @@ inset-block: auto env(safe-area-inset-bottom); inline-size: var(--tray-size); position: fixed; - transition: inset var(--tray-duration) var(--tray-timing-function); + transition: inset var(--tray-duration) var(--ease-out-overshoot-subtle); z-index: var(--z-tray); /* Make the dialog, expander, and actions inhabit the same space */ @@ -43,7 +42,7 @@ inline-size: auto; inset: auto 0 0 0; position: absolute; - transition: var(--tray-duration) var(--tray-timing-function); + transition: var(--tray-duration) var(--ease-out-overshoot-subtle); transition-property: background-color, box-shadow, inset-block-end; &[open] { @@ -159,7 +158,7 @@ .tray__dialog & { font-size: 10px; margin-block-end: var(--tray-item-margin); - transition: var(--tray-duration) var(--tray-timing-function); + transition: var(--tray-duration) var(--ease-out-overshoot-subtle); transition-delay: var(--tray-item-delay); transition-property: margin, opacity, scale; diff --git a/app/assets/stylesheets/workflows.css b/app/assets/stylesheets/workflows.css index 2dc043584..5baee3422 100644 --- a/app/assets/stylesheets/workflows.css +++ b/app/assets/stylesheets/workflows.css @@ -7,6 +7,8 @@ color: inherit; inline-size: 100%; + justify-content: flex-start; + text-transform: uppercase; @media (hover: hover) { &:hover { @@ -27,6 +29,18 @@ } } + @supports(mask: radial-gradient(black)) { + .workflow-stage--stream:before { + background: white; + content: ""; + inset: 0; + mask: var(--wavy-background-mask); + position: absolute; + z-index: 0; + opacity: 0.25; + } + } + /* Workflow settings /* ------------------------------------------------------------------------ */ diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb index f3e1b7ebd..53b1f54e8 100644 --- a/app/controllers/account/entropy_configurations_controller.rb +++ b/app/controllers/account/entropy_configurations_controller.rb @@ -7,6 +7,6 @@ class Account::EntropyConfigurationsController < ApplicationController private def entropy_configuration_params - params.expect(entropy_configuration: [ :auto_close_period, :auto_reconsider_period ]) + params.expect(entropy_configuration: [ :auto_postpone_period ]) end end diff --git a/app/controllers/cards/drops_controller.rb b/app/controllers/cards/drops_controller.rb deleted file mode 100644 index 468290fd9..000000000 --- a/app/controllers/cards/drops_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -class Cards::DropsController < ApplicationController - include FilterScoped - - before_action :set_card, :set_drop_target - - def create - perform_drop_action - render_column_replacement - end - - private - VALID_DROP_TARGETS = %w[ considering on_deck doing ] - - def set_card - @card = Current.user.accessible_cards.find(params[:dropped_item_id]) - end - - def set_drop_target - if params[:drop_target].in?(VALID_DROP_TARGETS) - @drop_target = params[:drop_target].to_sym - else - head :bad_request - end - end - - def perform_drop_action - case @drop_target - when :considering - @card.reconsider - when :on_deck - @card.move_to_on_deck - when :doing - @card.engage - end - end - - def render_column_replacement - columns = Cards::Columns.new(user_filtering: @user_filtering, page_size: CardsController::PAGE_SIZE) - column = columns.public_send(@drop_target) - - render \ - turbo_stream: turbo_stream.replace("#{@drop_target.to_s.gsub('_', '-')}-cards", - method: :morph, - partial: "cards/index/engagement/#{@drop_target}", - locals: { column: column }) - end -end diff --git a/app/controllers/cards/not_nows_controller.rb b/app/controllers/cards/not_nows_controller.rb new file mode 100644 index 000000000..8f43db1be --- /dev/null +++ b/app/controllers/cards/not_nows_controller.rb @@ -0,0 +1,8 @@ +class Cards::NotNowsController < ApplicationController + include CardScoped + + def create + @card.postpone + render_card_replacement + end +end diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb deleted file mode 100644 index 714427ecf..000000000 --- a/app/controllers/cards/stagings_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -class Cards::StagingsController < ApplicationController - include CardScoped - - def update - @card.change_stage_to @collection.workflow.stages.find(params[:stage_id]) - render_card_replacement - end -end diff --git a/app/controllers/cards/triages_controller.rb b/app/controllers/cards/triages_controller.rb new file mode 100644 index 000000000..12fe1a5fb --- /dev/null +++ b/app/controllers/cards/triages_controller.rb @@ -0,0 +1,15 @@ +class Cards::TriagesController < ApplicationController + include CardScoped + + def create + column = @card.collection.columns.find(params[:column_id]) + @card.triage_into(column) + + render_card_replacement + end + + def destroy + @card.send_back_to_triage + render_card_replacement + end +end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e30867991..174202742 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -11,8 +11,7 @@ class CardsController < ApplicationController PAGE_SIZE = 25 def index - @columns = Cards::Columns.new(user_filtering: @user_filtering, page_size: PAGE_SIZE) - + set_page_and_extract_portion_from @filter.cards fresh_when etag: @columns end diff --git a/app/controllers/collections/columns/closeds_controller.rb b/app/controllers/collections/columns/closeds_controller.rb new file mode 100644 index 000000000..2ffd7c253 --- /dev/null +++ b/app/controllers/collections/columns/closeds_controller.rb @@ -0,0 +1,8 @@ +class Collections::Columns::ClosedsController < ApplicationController + include CollectionScoped + + def show + set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first + fresh_when etag: [ @collection, @page.records ] + end +end diff --git a/app/controllers/collections/columns/not_nows_controller.rb b/app/controllers/collections/columns/not_nows_controller.rb new file mode 100644 index 000000000..03922664c --- /dev/null +++ b/app/controllers/collections/columns/not_nows_controller.rb @@ -0,0 +1,8 @@ +class Collections::Columns::NotNowsController < ApplicationController + include CollectionScoped + + def show + set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically + fresh_when etag: [ @collection, @page.records ] + end +end diff --git a/app/controllers/collections/columns/streams_controller.rb b/app/controllers/collections/columns/streams_controller.rb new file mode 100644 index 000000000..ebc4c2404 --- /dev/null +++ b/app/controllers/collections/columns/streams_controller.rb @@ -0,0 +1,8 @@ +class Collections::Columns::StreamsController < ApplicationController + include CollectionScoped + + def show + set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically + fresh_when etag: [ @collection, @page.records ] + end +end diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb new file mode 100644 index 000000000..4d6f4ee4c --- /dev/null +++ b/app/controllers/collections/columns_controller.rb @@ -0,0 +1,31 @@ +class Collections::ColumnsController < ApplicationController + include ActionView::RecordIdentifier, CollectionScoped + + before_action :set_column, only: [ :show, :update, :destroy ] + + def show + set_page_and_extract_portion_from @column.cards.active.reverse_chronologically + fresh_when etag: [ @column, @page.records ] + end + + def create + @column = @collection.columns.create!(column_params) + end + + def update + @column.update!(column_params) + end + + def destroy + @column.destroy + end + + private + def set_column + @column = @collection.columns.find(params[:id]) + end + + def column_params + params.require(:column).permit(:name) + end +end diff --git a/app/controllers/collections/entropy_configurations_controller.rb b/app/controllers/collections/entropy_configurations_controller.rb index cabdc8c22..76957627d 100644 --- a/app/controllers/collections/entropy_configurations_controller.rb +++ b/app/controllers/collections/entropy_configurations_controller.rb @@ -12,6 +12,6 @@ class Collections::EntropyConfigurationsController < ApplicationController private def entropy_configuration_params - params.expect(collection: [ :auto_close_period, :auto_reconsider_period ]) + params.expect(collection: [ :auto_postpone_period ]) end end diff --git a/app/controllers/collections/workflows_controller.rb b/app/controllers/collections/workflows_controller.rb deleted file mode 100644 index 7b79baad4..000000000 --- a/app/controllers/collections/workflows_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Collections::WorkflowsController < ApplicationController - include CollectionScoped - - before_action :set_workflow - - def update - @collection.update! workflow: @workflow - render turbo_stream: [ - turbo_stream.replace([ @collection, :workflows ], partial: "collections/edit/workflows", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end - - private - def set_workflow - workflow_id = params[:collection][:workflow_id] - @workflow = workflow_id.present? ? Workflow.find(workflow_id) : nil - end -end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 2bc7fa08d..94b1ff9bd 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -7,9 +7,14 @@ class CollectionsController < ApplicationController @collection = Collection.new end + def show + set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically + fresh_when etag: [ @collection, @page.records, @user_filtering ] + end + def create @collection = Collection.create! collection_params.with_defaults(all_access: true) - redirect_to cards_path(collection_ids: [ @collection ]) + redirect_to collection_path(@collection) end def edit @@ -38,7 +43,7 @@ class CollectionsController < ApplicationController end def collection_params - params.expect(collection: [ :name, :all_access, :auto_close_period, :auto_reconsider_period, :public_description ]) + params.expect(collection: [ :name, :all_access, :auto_postpone_period, :public_description ]) end def grantees diff --git a/app/controllers/columns/cards/drops/closures_controller.rb b/app/controllers/columns/cards/drops/closures_controller.rb new file mode 100644 index 000000000..ba0dc67df --- /dev/null +++ b/app/controllers/columns/cards/drops/closures_controller.rb @@ -0,0 +1,9 @@ +class Columns::Cards::Drops::ClosuresController < ApplicationController + include CardScoped + + def create + @card.close + + render turbo_stream: turbo_stream.replace("closed-cards", partial: "collections/show/closed", method: :morph, locals: { collection: @card.collection }) + end +end diff --git a/app/controllers/columns/cards/drops/columns_controller.rb b/app/controllers/columns/cards/drops/columns_controller.rb new file mode 100644 index 000000000..cb2c3b4a8 --- /dev/null +++ b/app/controllers/columns/cards/drops/columns_controller.rb @@ -0,0 +1,10 @@ +class Columns::Cards::Drops::ColumnsController < ApplicationController + include ActionView::RecordIdentifier, CardScoped + + def create + column = @card.collection.columns.find(params[:column_id]) + @card.triage_into(column) + + render turbo_stream: turbo_stream.replace(dom_id(column), partial: "collections/show/column", method: :morph, locals: { column: column }) + end +end diff --git a/app/controllers/columns/cards/drops/not_nows_controller.rb b/app/controllers/columns/cards/drops/not_nows_controller.rb new file mode 100644 index 000000000..a8fe74298 --- /dev/null +++ b/app/controllers/columns/cards/drops/not_nows_controller.rb @@ -0,0 +1,9 @@ +class Columns::Cards::Drops::NotNowsController < ApplicationController + include CardScoped + + def create + @card.postpone + + render turbo_stream: turbo_stream.replace("not-now", partial: "collections/show/not_now", method: :morph, locals: { collection: @card.collection }) + end +end diff --git a/app/controllers/columns/cards/drops/streams_controller.rb b/app/controllers/columns/cards/drops/streams_controller.rb new file mode 100644 index 000000000..d9141af7a --- /dev/null +++ b/app/controllers/columns/cards/drops/streams_controller.rb @@ -0,0 +1,10 @@ +class Columns::Cards::Drops::StreamsController < ApplicationController + include CardScoped + + def create + @card.send_back_to_triage + set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically + + render turbo_stream: turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page }) + end +end diff --git a/app/controllers/concerns/public_collection_scoped.rb b/app/controllers/concerns/public_collection_scoped.rb index 3f5edb35d..11ecacfca 100644 --- a/app/controllers/concerns/public_collection_scoped.rb +++ b/app/controllers/concerns/public_collection_scoped.rb @@ -7,6 +7,6 @@ module PublicCollectionScoped private def set_collection - @collection = Collection.find_by_published_key(params[:id] || params[:collection_id]) + @collection = Collection.find_by_published_key(params[:collection_id] || params[:id]) end end diff --git a/app/controllers/concerns/workflow_scoped.rb b/app/controllers/concerns/workflow_scoped.rb deleted file mode 100644 index 8e358733a..000000000 --- a/app/controllers/concerns/workflow_scoped.rb +++ /dev/null @@ -1,12 +0,0 @@ -module WorkflowScoped - extend ActiveSupport::Concern - - included do - before_action :set_workflow - end - - private - def set_workflow - @workflow = Workflow.find(params[:workflow_id]) - end -end diff --git a/app/controllers/public/collections/columns/closeds_controller.rb b/app/controllers/public/collections/columns/closeds_controller.rb new file mode 100644 index 000000000..2b9467504 --- /dev/null +++ b/app/controllers/public/collections/columns/closeds_controller.rb @@ -0,0 +1,14 @@ +class Public::Collections::Columns::ClosedsController < ApplicationController + include PublicCollectionScoped + + allow_unauthenticated_access only: :show + + layout "public" + + def show + set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first + + # To enable caching at intermediate proxies during traffic spikes + expires_in 5.seconds, public: true + end +end diff --git a/app/controllers/public/collections/columns/not_nows_controller.rb b/app/controllers/public/collections/columns/not_nows_controller.rb new file mode 100644 index 000000000..67c0cd5bf --- /dev/null +++ b/app/controllers/public/collections/columns/not_nows_controller.rb @@ -0,0 +1,14 @@ +class Public::Collections::Columns::NotNowsController < ApplicationController + include PublicCollectionScoped + + allow_unauthenticated_access only: :show + + layout "public" + + def show + set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically + + # To enable caching at intermediate proxies during traffic spikes + expires_in 5.seconds, public: true + end +end diff --git a/app/controllers/public/collections/columns/streams_controller.rb b/app/controllers/public/collections/columns/streams_controller.rb new file mode 100644 index 000000000..c60144799 --- /dev/null +++ b/app/controllers/public/collections/columns/streams_controller.rb @@ -0,0 +1,14 @@ +class Public::Collections::Columns::StreamsController < ApplicationController + include PublicCollectionScoped + + allow_unauthenticated_access only: :show + + layout "public" + + def show + set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically + + # To enable caching at intermediate proxies during traffic spikes + expires_in 5.seconds, public: true + end +end diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb new file mode 100644 index 000000000..f074b8188 --- /dev/null +++ b/app/controllers/public/collections/columns_controller.rb @@ -0,0 +1,21 @@ +class Public::Collections::ColumnsController < ApplicationController + include ActionView::RecordIdentifier, PublicCollectionScoped + + allow_unauthenticated_access only: :show + + layout "public" + + before_action :set_column, only: :show + + def show + set_page_and_extract_portion_from @column.cards.active.reverse_chronologically + + # To enable caching at intermediate proxies during traffic spikes + expires_in 5.seconds, public: true + end + + private + def set_column + @column = @collection.columns.find(params[:id]) + end +end diff --git a/app/controllers/public/collections_controller.rb b/app/controllers/public/collections_controller.rb index 5a97f0194..1529df607 100644 --- a/app/controllers/public/collections_controller.rb +++ b/app/controllers/public/collections_controller.rb @@ -6,10 +6,7 @@ class Public::CollectionsController < ApplicationController layout "public" def show - @considering = current_page_from @collection.cards.considering.latest, per_page: CardsController::PAGE_SIZE - @on_deck = current_page_from @collection.cards.on_deck.latest, per_page: CardsController::PAGE_SIZE - @doing = current_page_from @collection.cards.doing.latest, per_page: CardsController::PAGE_SIZE - @closed = current_page_from @collection.cards.closed.recently_closed_first, per_page: CardsController::PAGE_SIZE + set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/controllers/workflows/stages_controller.rb b/app/controllers/workflows/stages_controller.rb deleted file mode 100644 index 5973948d2..000000000 --- a/app/controllers/workflows/stages_controller.rb +++ /dev/null @@ -1,36 +0,0 @@ -class Workflows::StagesController < ApplicationController - include WorkflowScoped - - before_action :set_stage, only: %i[ edit update destroy ] - - def new - @stage = @workflow.stages.new - end - - def create - @stage = @workflow.stages.create! stage_params - redirect_to @workflow - end - - def edit - end - - def update - @stage.update! stage_params - redirect_to @workflow - end - - def destroy - @stage.destroy - redirect_to @workflow - end - - private - def set_stage - @stage = @workflow.stages.find params[:id] - end - - def stage_params - params.require(:workflow_stage).permit(:name, :color) - end -end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb deleted file mode 100644 index 24aab333b..000000000 --- a/app/controllers/workflows_controller.rb +++ /dev/null @@ -1,43 +0,0 @@ -class WorkflowsController < ApplicationController - before_action :set_workflow, only: %i[ show edit update destroy ] - - include FilterScoped - - def index - @workflows = Workflow.all - end - - def new - @workflow = Workflow.new - end - - def create - @workflow = Workflow.create! workflow_params - redirect_to workflows_path - end - - def show - end - - def edit - end - - def update - @workflow.update! workflow_params - redirect_to @workflow - end - - def destroy - @workflow.destroy - redirect_to workflows_path - end - - private - def set_workflow - @workflow = Workflow.find params[:id] - end - - def workflow_params - params.expect workflow: :name - end -end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 833a1a2b0..8e3dee6a3 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -51,10 +51,10 @@ module AccessesHelper method: :put, aria: { labelledby: involvement_label_id }, class: "btn", - params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement) } + params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement), icon_only: icon_only } ) do safe_join([ - icon_tag("notification-bell-#{access.involvement.dasherize}"), + icon_tag("notification-bell-#{icon_only ? 'reverse-' : nil}#{access.involvement.dasherize}"), tag.span( involvement_access_label(collection, access.involvement), class: class_names("txt-nowrap txt-uppercase", "for-screen-reader": icon_only), diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 0fba06f5f..dac427eb8 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -1,42 +1,10 @@ module CardsHelper - def cards_next_page_link(target, page:, filter:, fetch_on_visible: false, data: {}, **options) - url = cards_previews_path(target: target, page: page.next_param, **filter.as_params) - - if fetch_on_visible - data[:controller] = "#{data[:controller]} fetch-on-visible" - data[:fetch_on_visible_url_value] = url - end - - link_to "Load more", - url, - id: "#{target}-load-page-#{page.next_param}", - data: { turbo_stream: true, **data }, - class: "btn txt-small", - **options - end - - def public_collection_cards_next_page_link(collection, target, fetch_on_visible: false, page:, data: {}, **options) - url = public_collection_card_previews_path(collection.publication.key, target: target, page: page.next_param) - - if fetch_on_visible - data[:controller] = "#{data[:controller]} fetch-on-visible" - data[:fetch_on_visible_url_value] = url - end - - link_to "Load more", - url, - id: "#{target}-load-page-#{page.next_param}", - data: { turbo_stream: true, **data }, - class: "btn txt-small", - **options - end - def card_article_tag(card, id: dom_id(card, :article), **options, &block) classes = [ options.delete(:class), ("golden-effect" if card.golden?), - ("card--considering" if card.considering?), - ("card--doing" if card.doing?), + ("card--postponed" if card.postponed?), + ("card--active" if card.active?), ("card--drafted" if card.drafted?) ].compact.join(" ") @@ -65,4 +33,8 @@ module CardsHelper title << "assigned to #{card.assignees.map(&:name).to_sentence}" if card.assignees.any? title.join(" ") end + + def cacheable_preview_parts_for(card, *options) + [ card, card.workflow, card.collection.entropy_configuration, card.collection.publication, *options ] + end end diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb new file mode 100644 index 000000000..008cc0404 --- /dev/null +++ b/app/helpers/columns_helper.rb @@ -0,0 +1,20 @@ +module ColumnsHelper + def button_to_set_column(card, column) + button_to \ + tag.span(column.name, class: "overflow-ellipsis"), + card_triage_path(card, column_id: column), + method: :post, + class: [ "workflow-stage btn", { "workflow-stage--current": column == card.column } ], + form_class: "flex align-center gap-half", + data: { turbo_frame: "_top" } + end + + def column_frame_tag(id, src: nil, data: {}, **options, &block) + data = data.reverse_merge \ + "drag-and-drop-refresh": true, + controller: "frame", + action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload" + + turbo_frame_tag(id, src: src, data: data, **options, &block) + end +end diff --git a/app/helpers/entropy_helper.rb b/app/helpers/entropy_helper.rb index bc4dbbc28..ee69d30be 100644 --- a/app/helpers/entropy_helper.rb +++ b/app/helpers/entropy_helper.rb @@ -7,7 +7,7 @@ module EntropyHelper { daysBeforeReminder: card.entropy.days_before_reminder, closesAt: card.entropy.auto_clean_at.iso8601, - action: card_entropy_action(card) + action: "Postpones" } end @@ -20,12 +20,4 @@ module EntropyHelper } end end - - def card_entropy_action(card) - if card.stagnated? - "Falls Back" - elsif card.considering? - "Closes" - end - end end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb new file mode 100644 index 000000000..06599fc36 --- /dev/null +++ b/app/helpers/pagination_helper.rb @@ -0,0 +1,47 @@ +module PaginationHelper + def pagination_frame_tag(namespace, page, data: {}, **attributes, &) + turbo_frame_tag pagination_frame_id_for(namespace, page.number), data: { timeline_target: "frame", **data }, role: "presentation", **attributes, & + end + + def link_to_next_page(namespace, page, activate_when_observed: false, label: "Load more...", data: {}, **attributes) + if page.before_last? && !params[:previous] + pagination_link(namespace, page.number + 1, label: label, activate_when_observed: activate_when_observed, data: data, class: "margin-block btn txt-small", **attributes) + end + end + + def pagination_link(namespace, page_number, label: spinner_tag, activate_when_observed: false, url_params: {}, data: {}, **attributes) + link_to label, url_for(page: page_number, **url_params), + "aria-label": "Load page #{page_number}", + class: class_names(attributes.delete(:class), "pagination-link", { "pagination-link--active-when-observed" => activate_when_observed }), + data: { + frame: pagination_frame_id_for(namespace, page_number), + pagination_target: "paginationLink", + action: ("click->pagination#loadPage:prevent" unless activate_when_observed), + **data + }, + **attributes + end + + def pagination_frame_id_for(namespace, page_number) + "#{namespace}-pagination-contents-#{page_number}" + end + + def with_manual_pagination(name, page, **properties) + pagination_list name, **properties do + concat(pagination_frame_tag(name, page) do + yield + concat link_to_next_page(name, page) + end) + end + end + + private + def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block) + classes = properties.delete(:class) + tag.public_send tag_element, + class: token_list(name, "display-contents", classes), + role: "list", + data: { controller: "pagination", pagination_paginate_on_intersection_value: paginate_on_scroll }, + &block + end +end diff --git a/app/helpers/webhooks_helper.rb b/app/helpers/webhooks_helper.rb new file mode 100644 index 000000000..7fe91d8c0 --- /dev/null +++ b/app/helpers/webhooks_helper.rb @@ -0,0 +1,27 @@ +module WebhooksHelper + ACTION_LABELS = { + card_assigned: "Card assigned", + card_closed: "Card closed", + card_collection_changed: "Card collection changed", + card_due_date_added: "Card due date added", + card_due_date_changed: "Card due date changed", + card_due_date_removed: "Card due date removed", + card_published: "Card published", + card_reopened: "Card reopened", + card_staged: "Card staged", + card_title_changed: "Card title changed", + card_unassigned: "Card unassigned", + card_unstaged: "Card unstaged", + comment_created: "Comment created" + }.with_indifferent_access.freeze + + def webhook_action_options(actions = Webhook::PERMITTED_ACTIONS) + actions.each_with_object({}) do |action, hash| + hash[action.to_s] = webhook_action_label(action) + end + end + + def webhook_action_label(action) + ACTION_LABELS[action] || action.to_s.humanize + end +end diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb deleted file mode 100644 index f60e9d7f8..000000000 --- a/app/helpers/workflows_helper.rb +++ /dev/null @@ -1,35 +0,0 @@ -module WorkflowsHelper - def button_to_set_stage(card, stage) - 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--current": stage == card.stage } ], - form_class: "flex align-center gap-half", - data: { turbo_frame: "_top" } - end - - def stage_color(stage) - stage.color.presence || Card::DEFAULT_COLOR - end - - def dependent_collections_sentence(workflow) - if workflow.collections.many? - "It will be removed from #{ workflow.collections.count } collections that are using it." - elsif workflow.collections.one? - "It will be removed from the only collection using it." - else - "It's not being used in any collections." - end - end - - def workflow_switch_confirmation_message(workflow, collection) - if workflow == collection.workflow - "Stop using #{workflow.name}? Cards you're working on will lose their current stage." - elsif collection.workflow.present? - "Switch to #{workflow.name}? This will return all cards you're working on to the first stage." - else - nil - end - end -end diff --git a/app/javascript/application.js b/app/javascript/application.js index dbac91d0a..bdafb95b0 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -6,4 +6,3 @@ import "controllers" import "lexxy" import "@rails/actiontext" -window.fetch = Turbo.fetch // TODO: We need to make @rails/request.js use Turbo's fetch when it's present. diff --git a/app/javascript/controllers/clicker_controller.js b/app/javascript/controllers/clicker_controller.js new file mode 100644 index 000000000..d09cb9ede --- /dev/null +++ b/app/javascript/controllers/clicker_controller.js @@ -0,0 +1,11 @@ +import { Controller } from "@hotwired/stimulus" +import { nextFrame } from "helpers/timing_helpers"; + +export default class extends Controller { + static targets = [ "clickable" ] + + async click() { + await nextFrame() + this.clickableTarget.click() + } +} diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js new file mode 100644 index 000000000..9c4898994 --- /dev/null +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -0,0 +1,79 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static classes = [ "collapsed" ] + static targets = [ "column", "button" ] + + connect() { + this.#restoreColumns() + } + + toggle({ target }) { + const column = target.closest('[data-collapsible-columns-target="column"]') + this.#toggleColumn(column); + } + + preventToggle(event) { + if (event.detail.attributeName === "class") { + event.preventDefault() + } + } + + #toggleColumn(column) { + this.#collapseAllExcept(column) + + if (this.#isCollapsed(column)) { + this.#expand(column) + } else { + this.#collapse(column) + } + + console.log("TOGGLE") + console.log(localStorage) + } + + #collapseAllExcept(clickedColumn) { + this.columnTargets.forEach(column => { + if (column !== clickedColumn) { + this.#collapse(column) + } + }) + } + + #isCollapsed(column) { + return column.classList.contains(this.collapsedClass) + } + + #collapse(column) { + const key = this.#localStorageKeyFor(column) + + this.#buttonFor(column).setAttribute("aria-expanded", "false") + column.classList.add(this.collapsedClass) + localStorage.removeItem(key) + } + + #expand(column) { + const key = this.#localStorageKeyFor(column) + + this.#buttonFor(column).setAttribute("aria-expanded", "true") + column.classList.remove(this.collapsedClass) + localStorage.setItem(key, true) + } + + #buttonFor(column) { + return this.buttonTargets.find(button => column.contains(button)) + } + + #restoreColumns() { + this.columnTargets.forEach(column => { + const key = this.#localStorageKeyFor(column) + if (localStorage.getItem(key)) { + this.#expand(column) + } + }) + } + + #localStorageKeyFor(column) { + return `expand-${column.getAttribute("id")}` + } +} diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index 9341f4c25..44876b231 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -8,7 +8,7 @@ export default class extends Controller { } connect() { - this.dialogTarget.setAttribute('aria-hidden', 'true') + this.dialogTarget.setAttribute("aria-hidden", "true") } open() { @@ -21,7 +21,7 @@ export default class extends Controller { orient(this.dialogTarget) } - this.dialogTarget.setAttribute('aria-hidden', 'false') + this.dialogTarget.setAttribute("aria-hidden", "false") this.dispatch("show") } @@ -35,7 +35,7 @@ export default class extends Controller { close() { this.dialogTarget.close() - this.dialogTarget.setAttribute('aria-hidden', 'true') + this.dialogTarget.setAttribute("aria-hidden", "true") this.dialogTarget.blur() orient(this.dialogTarget, false) } diff --git a/app/javascript/controllers/drag_and_drop_controller.js b/app/javascript/controllers/drag_and_drop_controller.js index 01ccc593d..5c03853be 100644 --- a/app/javascript/controllers/drag_and_drop_controller.js +++ b/app/javascript/controllers/drag_and_drop_controller.js @@ -4,7 +4,6 @@ import { nextFrame } from "helpers/timing_helpers" export default class extends Controller { static targets = [ "item", "container" ] - static values = { url: String } static classes = [ "draggedItem", "hoverContainer" ] // Actions @@ -38,8 +37,10 @@ export default class extends Controller { if (!container || container === this.sourceContainer) { return } this.wasDropped = true - + this.#decreaseCounter(this.sourceContainer) + const sourceContainer = this.sourceContainer await this.#submitDropRequest(this.dragItem, container) + this.#reloadSourceFrame(sourceContainer); } dragEnd() { @@ -67,15 +68,30 @@ export default class extends Controller { this.containerTargets.forEach(container => container.classList.remove(this.hoverContainerClass)) } - // Private - async #submitDropRequest(item, container) { const body = new FormData() const id = item.dataset.id - const containerTarget = container.dataset.dropTarget + const url = container.dataset.dragAndDropUrl.replaceAll("__id__", id) - body.append("dropped_item_id", id) - body.append("drop_target", containerTarget) - return post(this.urlValue, { body, headers: { Accept: "text/vnd.turbo-stream.html" } }) + return post(url, { body, headers: { Accept: "text/vnd.turbo-stream.html" } }) + } + + #reloadSourceFrame(sourceContainer) { + const frame = sourceContainer.querySelector("[data-drag-and-drop-refresh]") + if (frame) frame.reload() + } + + #decreaseCounter(sourceContainer) { + const counterElement = sourceContainer.querySelector("[data-drag-and-drop-counter]") + if (counterElement) { + const currentValue = counterElement.textContent.trim() + + if (!/^\d+$/.test(currentValue)) return + + const count = parseInt(currentValue) + if (count > 0) { + counterElement.textContent = count - 1 + } + } } } diff --git a/app/javascript/controllers/drag_and_strum_controller.js b/app/javascript/controllers/drag_and_strum_controller.js new file mode 100644 index 000000000..8946f360a --- /dev/null +++ b/app/javascript/controllers/drag_and_strum_controller.js @@ -0,0 +1,109 @@ +import { Controller } from "@hotwired/stimulus" + +const INSTRUMENTS = [ + [ + "/audio/vibes/B3.mp3", + "/audio/vibes/C3.mp3", + "/audio/vibes/D4.mp3", + "/audio/vibes/E3.mp3", + "/audio/vibes/Fsharp4.mp3", + "/audio/vibes/G3.mp3" + ], + [ + "/audio/banjo/B3.mp3", + "/audio/banjo/C3.mp3", + "/audio/banjo/D4.mp3", + "/audio/banjo/E3.mp3", + "/audio/banjo/Fsharp4.mp3", + "/audio/banjo/G3.mp3" + ], + [ + "/audio/harpsichord/B3.mp3", + "/audio/harpsichord/C3.mp3", + "/audio/harpsichord/D4.mp3", + "/audio/harpsichord/E3.mp3", + "/audio/harpsichord/Fsharp4.mp3", + "/audio/harpsichord/G3.mp3" + ], + [ + "/audio/mandolin/B3.mp3", + "/audio/mandolin/C3.mp3", + "/audio/mandolin/D4.mp3", + "/audio/mandolin/E3.mp3", + "/audio/mandolin/Fsharp4.mp3", + "/audio/mandolin/G3.mp3" + ], + [ + "/audio/piano/B3.mp3", + "/audio/piano/C3.mp3", + "/audio/piano/D4.mp3", + "/audio/piano/E3.mp3", + "/audio/piano/Fsharp4.mp3", + "/audio/piano/G3.mp3" + ], +] + +export default class extends Controller { + static targets = [ "container" ] + + connect() { + this.instrumentIndex = 0 + this.preloadedAudioFiles = [] + document.addEventListener("keydown", this.handleKeyDown.bind(this)); + } + + disconnect() { + document.removeEventListener("keydown", this.handleKeyDown.bind(this)); + } + + handleKeyDown(event) { + if (event.shiftKey) { + this.instrumentIndex = this.#getInstrumentIndex(event) + + if (this.instrumentIndex < INSTRUMENTS.length) { + this.#preloadAudioFiles(this.instrumentIndex) + } + } + } + + dragEnter(event) { + event.preventDefault() + const container = this.#containerContaining(event.target) + + if (!container) { return } + + if (container !== this.sourceContainer && event.shiftKey) { + this.#playSound() + } + } + + #getInstrumentIndex(event) { + const number = Number(event.code.replace("Digit", "")) + return isNaN(number) ? 0 : number + } + + #preloadAudioFiles(instrumentIndex) { + this.preloadedAudioFiles = [] + const audioFiles = INSTRUMENTS[instrumentIndex]; + + if (audioFiles) { + this.preloadedAudioFiles = audioFiles.map(file => { + const audio = new Audio(file) + audio.load() + return audio + }) + } + } + + #containerContaining(element) { + return this.containerTargets.find(container => container.contains(element) || container === element) + } + + #playSound() { + const randomIndex = Math.floor(Math.random() * this.preloadedAudioFiles.length) + const audio = this.preloadedAudioFiles[randomIndex] + const audioInstance = new Audio(audio.src) + + audioInstance.play() + } +} diff --git a/app/javascript/controllers/filter_settings_controller.js b/app/javascript/controllers/filter_settings_controller.js index 85784583a..7a207da80 100644 --- a/app/javascript/controllers/filter_settings_controller.js +++ b/app/javascript/controllers/filter_settings_controller.js @@ -5,7 +5,7 @@ import { post } from "@rails/request.js" export default class extends Controller { static classes = ["filtersSet"] static targets = ["field", "form"] - static values = { refreshUrl: String } + static values = { refreshUrl: String, noFilteringUrl: String } initialize() { this.debouncedToggle = debounce(this.#toggle.bind(this), 50) @@ -15,11 +15,18 @@ export default class extends Controller { this.#toggle() } - change() { + change(event) { this.#toggle() this.#refreshSaveToggleButton() } + resetIfNoFiltering(event) { + if (!this.#hasFiltersSet) { + this.#showNoFilteringUrl() + event.stopImmediatePropagation() + } + } + async fieldTargetConnected(field) { this.debouncedToggle() } @@ -65,4 +72,8 @@ export default class extends Controller { return formData } + + #showNoFilteringUrl() { + Turbo.visit(this.noFilteringUrlValue, { frame: "cards_container", action: "advance" }) + } } diff --git a/app/javascript/controllers/form_controller.js b/app/javascript/controllers/form_controller.js index eaf246fea..4e59f6055 100644 --- a/app/javascript/controllers/form_controller.js +++ b/app/javascript/controllers/form_controller.js @@ -25,6 +25,10 @@ export default class extends Controller { this.submit() } + reset() { + this.element.reset() + } + cancel() { this.cancelTarget?.click() } diff --git a/app/javascript/controllers/frame_controller.js b/app/javascript/controllers/frame_controller.js new file mode 100644 index 000000000..7684a43ed --- /dev/null +++ b/app/javascript/controllers/frame_controller.js @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" +import { Turbo } from "@hotwired/turbo-rails" + +export default class extends Controller { + morphRender({ detail }) { + detail.render = function (currentElement, newElement) { + Turbo.morphChildren(currentElement, newElement) + } + } + + morphReload(event) { + const newElement = event.detail.newElement + if (newElement && newElement.tagName === "TURBO-FRAME" && newElement.matches('[data-controller~="frame"]')) { + event.preventDefault() + this.element.reload() + } + } +} diff --git a/app/javascript/controllers/pagination_controller.js b/app/javascript/controllers/pagination_controller.js new file mode 100644 index 000000000..01d93d909 --- /dev/null +++ b/app/javascript/controllers/pagination_controller.js @@ -0,0 +1,124 @@ +import { Controller } from "@hotwired/stimulus" +import { createElement } from "helpers/html_helpers" +import { delay, nextEvent } from "helpers/timing_helpers" +import { keepingScrollPosition } from "helpers/scroll_helpers" +import { get } from "@rails/request.js" + +const DELAY_BEFORE_OBSERVING = 400 + +export default class extends Controller { + static targets = [ "paginationLink" ] + static values = { + paginateOnIntersection: { type: Boolean, default: false }, + discardFrame: Boolean, + manualActivation: Boolean + } + + initialize() { + if (!this.manualActivation) { + this.activate() + } + } + + disconnect() { + this.observer?.disconnect() + } + + async activate() { + await delay(DELAY_BEFORE_OBSERVING) + + if (this.paginateOnIntersectionValue) { + this.observer = new IntersectionObserver(this.#intersect, { rootMargin: "300px", threshold: 1 }) + } + } + + async paginationLinkTargetConnected(linkElement) { + if (this.paginateOnIntersectionValue) { + await delay(DELAY_BEFORE_OBSERVING) + this.observer?.observe(linkElement) + } + } + + // Actions + + loadPage({ target }) { + this.#loadPaginationLink(target) + } + + // Private + + #intersect = ([ entry ]) => { + if (entry?.isIntersecting && entry.intersectionRatio === 1) { + this.#loadPaginationLink(entry.target) + } + } + + #loadPaginationLink(linkElement) { + this.observer?.unobserve(linkElement) + + keepingScrollPosition(this.#closestSiblingTo(linkElement) || linkElement.parentNode, this.#expandPaginationLink(linkElement)) + } + + #closestSiblingTo(element) { + return element.nextElementSibling || element.previousElementSibling + } + + async #expandPaginationLink(linkElement) { + linkElement.setAttribute("aria-busy", "true") + + if (this.discardFrameValue) { + await this.#replacePaginationLinkWithFrameContents(linkElement) + } else { + await this.#replacePaginationLinkWithFrame(linkElement) + } + + linkElement.removeAttribute("aria-busy") + } + + async #replacePaginationLinkWithFrameContents(linkElement) { + linkElement.outerHTML = await this.#loadHtmlFrom(linkElement) + } + + async #loadHtmlFrom(linkElement) { + const response = await get(linkElement.href, { responseKind: "html" }) + const html = await response.text + const doc = new DOMParser().parseFromString(html, "text/html") + const element = doc.querySelector(`turbo-frame#${linkElement.dataset.frame}`) + return element ? element.innerHTML.trim() : "" + } + + #replacePaginationLinkWithFrame(linkElement) { + const turboFrame = this.#buildTurboFrameFor(linkElement) + this.#insertTurboFrameAtPosition(linkElement, turboFrame) + linkElement.remove() + } + + #buildTurboFrameFor(linkElement) { + const turboFrame = createElement("turbo-frame", { + id: linkElement.dataset.frame, + src: linkElement.href, + refresh: "morph", + target: "_top" + }) + + this.#keepScrollPositionOnFrameRender(turboFrame, linkElement) + + return turboFrame + } + + async #keepScrollPositionOnFrameRender(turboFrame, linkElement) { + await nextEvent(turboFrame, "turbo:before-frame-render") + + keepingScrollPosition(linkElement, nextEvent(turboFrame, "turbo:frame-render")) + } + + #insertTurboFrameAtPosition(linkElement, turboFrame) { + const container = linkElement.parentNode.parentNode + + if (linkElement.parentNode.firstElementChild === linkElement) { + container.prepend(turboFrame) + } else { + container.append(turboFrame) + } + } +} diff --git a/app/javascript/helpers/html_helpers.js b/app/javascript/helpers/html_helpers.js new file mode 100644 index 000000000..609594b88 --- /dev/null +++ b/app/javascript/helpers/html_helpers.js @@ -0,0 +1,9 @@ +export function createElement(name, properties) { + const element = document.createElement(name) + + for (var key in properties) { + element.setAttribute(key, properties[key]) + } + + return element +} diff --git a/app/javascript/helpers/scroll_helpers.js b/app/javascript/helpers/scroll_helpers.js new file mode 100644 index 000000000..603b1e3f2 --- /dev/null +++ b/app/javascript/helpers/scroll_helpers.js @@ -0,0 +1,71 @@ +export async function keepingScrollPosition(element, promise) { + const originalPosition = element.getBoundingClientRect() + + await promise + + const currentPosition = element.getBoundingClientRect() + + const yDiff = currentPosition.top - originalPosition.top + const xDiff = currentPosition.left - originalPosition.left + + findNearestScrollableYAncestor(element).scrollTop += yDiff + findNearestScrollableXAncestor(element).scrollLeft += xDiff +} + +export function isFullyVisible(element, container = document.documentElement) { + const elementRect = element.getBoundingClientRect() + const containerRect = container.getBoundingClientRect() + + return elementRect.top >= containerRect.top && + elementRect.bottom <= containerRect.bottom && + elementRect.left >= containerRect.left && + elementRect.right <= containerRect.right +} + +export function isScrolledToBottom(element, threshold = 100) { + return (element.scrollHeight - element.scrollTop - element.clientHeight) < threshold +} + +export function scrollToBottom(element) { + element.scrollTop = element.scrollHeight +} + +export function scrollIntoView(element, options = { inline: "center", block: "center", behavior: "instant" }) { + element.scrollIntoView(options) +} + +// Private + +function findNearestScrollableYAncestor(refElement) { + return findNearest(refElement, (element) => { + const largerThanVisibleArea = element.scrollHeight > element.clientHeight + + const overflowY = getComputedStyle(element).overflowY + const scrollableStyle = overflowY === "scroll" || overflowY === "auto" + + return largerThanVisibleArea && scrollableStyle + }) +} + +function findNearestScrollableXAncestor(refElement) { + return findNearest(refElement, (element) => { + const largerThanVisibleArea = element.scrollWidth > element.clientWidth + + const overflowX = getComputedStyle(element).overflowX + const scrollableStyle = overflowX === "scroll" || overflowX === "auto" + + return largerThanVisibleArea && scrollableStyle + }) +} + +function findNearest(element, fn) { + while (element) { + if (fn(element)) { + return element + } else { + element = element.parentElement + } + } + + return document.documentElement +} diff --git a/app/javascript/helpers/timing_helpers.js b/app/javascript/helpers/timing_helpers.js index f83e6719c..95312e19a 100644 --- a/app/javascript/helpers/timing_helpers.js +++ b/app/javascript/helpers/timing_helpers.js @@ -26,6 +26,10 @@ export function onNextEventLoopTick(callback) { setTimeout(callback, 0) } +export function nextEvent(element, eventName) { + return new Promise(resolve => element.addEventListener(eventName, resolve, { once: true })) +} + export function nextFrame() { return new Promise(requestAnimationFrame) } diff --git a/app/jobs/card/auto_close_all_due_job.rb b/app/jobs/card/auto_close_all_due_job.rb deleted file mode 100644 index d964dae23..000000000 --- a/app/jobs/card/auto_close_all_due_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Card::AutoCloseAllDueJob < ApplicationJob - def perform - ApplicationRecord.with_each_tenant do |tenant| - Card.auto_close_all_due - end - end -end diff --git a/app/jobs/card/auto_postpone_all_due_job.rb b/app/jobs/card/auto_postpone_all_due_job.rb new file mode 100644 index 000000000..2b75f0736 --- /dev/null +++ b/app/jobs/card/auto_postpone_all_due_job.rb @@ -0,0 +1,7 @@ +class Card::AutoPostponeAllDueJob < ApplicationJob + def perform + ApplicationRecord.with_each_tenant do |tenant| + Card.auto_postpone_all_due + end + end +end diff --git a/app/jobs/card/auto_reconsider_all_stagnated_job.rb b/app/jobs/card/auto_reconsider_all_stagnated_job.rb deleted file mode 100644 index b7178f916..000000000 --- a/app/jobs/card/auto_reconsider_all_stagnated_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Card::AutoReconsiderAllStagnatedJob < ApplicationJob - def perform - ApplicationRecord.with_each_tenant do |tenant| - Card.auto_reconsider_all_stagnated - end - end -end diff --git a/app/models/account.rb b/app/models/account.rb index 3473de25e..950c0a124 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -12,7 +12,5 @@ class Account < ApplicationRecord Closure::Reason.create_defaults Collection.create!(name: "Cards", creator: user, all_access: true) - workflow = Workflow.create!(name: "Basic") - Collection.first.update!(workflow: workflow) end end diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index a60af2b4b..b82b1051b 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -12,7 +12,6 @@ module Account::Entropic def set_default_entropy_configuration self.default_entropy_configuration ||= build_default_entropy_configuration \ - auto_close_period: DEFAULT_ENTROPY_PERIOD, - auto_reconsider_period: DEFAULT_ENTROPY_PERIOD + auto_postpone_period: DEFAULT_ENTROPY_PERIOD end end diff --git a/app/models/ai/prompts.rb b/app/models/ai/prompts.rb index 628ebd4fd..a9280f647 100644 --- a/app/models/ai/prompts.rb +++ b/app/models/ai/prompts.rb @@ -21,6 +21,10 @@ module Ai::Prompts * A card can have comments. - User can posts comments. - The system user can post comments in cards relative to certain events. + * An open card can be: + - Postponed (Not now) + - Pending triage in the Stream + - Triaged into a column * Both card and comments generate events relative to their lifecycle or to what the user do with them. * The system user can close cards due to inactivity. Refer to these as *auto-closed cards*. * Don't include the system user in the summaries. Include the outcomes (e.g: cards were autoclosed due to inactivity). diff --git a/app/models/card.rb b/app/models/card.rb index 02394131c..9175d6b96 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,7 +1,7 @@ class Card < ApplicationRecord - include Assignable, Attachments, Cacheable, Closeable, Colored, Engageable, Entropic, Eventable, - Golden, Mentions, Multistep, Pinnable, Promptable, Readable, Searchable, - Staged, Stallable, Statuses, Taggable, Watchable + include Assignable, Attachments, Cacheable, Closeable, Colored, Entropic, Eventable, + Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable, + Staged, Stallable, Statuses, Taggable, Triageable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } @@ -21,7 +21,7 @@ class Card < ApplicationRecord scope :indexed_by, ->(index) do case index when "stalled" then stalled - when "closing_soon" then closing_soon + when "postponing_soon" then postponing_soon when "falling_back_soon" then falling_back_soon when "closed" then closed.recently_closed_first when "golden" then golden diff --git a/app/models/card/colored.rb b/app/models/card/colored.rb index e6ccc2c80..78152bab8 100644 --- a/app/models/card/colored.rb +++ b/app/models/card/colored.rb @@ -14,11 +14,11 @@ module Card::Colored DEFAULT_COLOR = "var(--color-card-default)" def color - color_from_stage || DEFAULT_COLOR + color_from_column || DEFAULT_COLOR end private - def color_from_stage - stage&.color&.presence if doing? + def color_from_column + column&.color&.presence end end diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb deleted file mode 100644 index 0a006fd79..000000000 --- a/app/models/card/engageable.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Card::Engageable - extend ActiveSupport::Concern - - included do - has_one :engagement, dependent: :destroy, class_name: "Card::Engagement" - - scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) } - scope :on_deck, -> { published.open.joins(:engagement).where(engagement: { status: "on_deck" }) } - scope :doing, -> { published.open.joins(:engagement).where(engagement: { status: "doing" }) } - - scope :by_engagement_status, ->(status) do - case status.to_s - when "considering" then considering.with_golden_first - when "on_deck" then on_deck.with_golden_first - when "doing" then doing.with_golden_first - end - end - end - - def doing? - open? && published? && engagement&.status == "doing" - end - - def on_deck? - open? && published? && engagement&.status == "on_deck" - end - - def considering? - open? && published? && engagement.blank? - end - - def engagement_status - if doing? - "doing" - elsif on_deck? - "on_deck" - elsif considering? - "considering" - end - end - - def engage - unless doing? - reengage(status: "doing") - end - end - - def move_to_on_deck - unless on_deck? - reengage(status: "on_deck") - end - end - - def reconsider - transaction do - reopen - engagement&.destroy - activity_spike&.destroy - touch_last_active_at - end - end - - private - def reengage(status:) - transaction do - reopen - engagement&.destroy - create_engagement!(status:) - end - end -end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index a9dd1a5e9..bbf681835 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -2,43 +2,29 @@ module Card::Entropic extend ActiveSupport::Concern included do - scope :entropic_by, ->(period_name) do - left_outer_joins(collection: :entropy_configuration) - .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.#{period_name}, ?) || ' seconds')", - Entropy::Configuration.default.public_send(period_name)) - end - - scope :stagnated, -> { doing.or(on_deck).entropic_by(:auto_reconsider_period) } - - scope :due_to_be_closed, -> { considering.entropic_by(:auto_close_period) } - - scope :closing_soon, -> do - considering + scope :due_to_be_postponed, -> do + active .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_close_period, ?) || ' seconds')", Entropy::Configuration.default.auto_close_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_close_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_close_period) + .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", + Entropy::Configuration.default.auto_postpone_period) end - scope :falling_back_soon, -> do - doing.or(on_deck) + scope :postponing_soon, -> do + active .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_reconsider_period, ?) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_reconsider_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) end - delegate :auto_close_period, :auto_reconsider_period, to: :collection + delegate :auto_postpone_period, to: :collection end class_methods do - def auto_close_all_due - due_to_be_closed.find_each do |card| - card.close(user: User.system, reason: "Closed") + def auto_postpone_all_due + due_to_be_postponed.find_each do |card| + card.postpone end end - - def auto_reconsider_all_stagnated - stagnated.find_each(&:reconsider) - end end def entropy @@ -48,8 +34,4 @@ module Card::Entropic def entropic? entropy.present? end - - def stagnated? - card.doing? || card.on_deck? - end end diff --git a/app/models/card/entropy.rb b/app/models/card/entropy.rb index a7ab7597d..62de5fcf4 100644 --- a/app/models/card/entropy.rb +++ b/app/models/card/entropy.rb @@ -5,11 +5,7 @@ class Card::Entropy def for(card) return unless card.last_active_at - if card.considering? - new(card, card.auto_close_period) - elsif card.stagnated? - new(card, card.auto_reconsider_period) - end + new(card, card.auto_postpone_period) end end diff --git a/app/models/card/not_now.rb b/app/models/card/not_now.rb new file mode 100644 index 000000000..456ff2751 --- /dev/null +++ b/app/models/card/not_now.rb @@ -0,0 +1,3 @@ +class Card::NotNow < ApplicationRecord + belongs_to :card, class_name: "::Card", touch: true +end diff --git a/app/models/card/postponable.rb b/app/models/card/postponable.rb new file mode 100644 index 000000000..82e30a7bc --- /dev/null +++ b/app/models/card/postponable.rb @@ -0,0 +1,35 @@ +module Card::Postponable + extend ActiveSupport::Concern + + included do + has_one :not_now, dependent: :destroy, class_name: "Card::NotNow" + + scope :postponed, -> { open.published.joins(:not_now) } + scope :active, -> { open.published.where.missing(:not_now) } + end + + def postponed? + open? && published? && not_now.present? + end + + def active? + open? && published? && !postponed? + end + + def postpone + transaction do + send_back_to_triage + reopen + activity_spike&.destroy + create_not_now! unless postponed? + end + end + + def resume + transaction do + reopen + activity_spike&.destroy + not_now&.destroy + end + end +end diff --git a/app/models/card/promptable.rb b/app/models/card/promptable.rb index c0374759b..526368740 100644 --- a/app/models/card/promptable.rb +++ b/app/models/card/promptable.rb @@ -19,11 +19,8 @@ module Card::Promptable * Id: #{id} * Created by: #{creator.name}} * Assigned to: #{assignees.map(&:name).join(", ")}} - * Workflow stage: #{stage&.name} + * Column: #{column_prompt_label} * Created at: #{created_at}} - * Closed: #{closed?} - * Closed by: #{closed_by&.name} - * Closed at: #{closed_at} * Collection id: #{collection_id} * Collection name: #{collection.name} * Number of comments: #{comments.count} @@ -32,4 +29,19 @@ module Card::Promptable END OF CARD #{id} PROMPT end + + private + def column_prompt_label + if open? + if postponed? + "Not now" + elsif triaged? + "#{column&.name}" + else + "The stream" + end + else + "Closed (by #{closed_by&.name} at #{closed_at})" + end + end end diff --git a/app/models/card/stallable.rb b/app/models/card/stallable.rb index 30bed9a99..bc935b5f6 100644 --- a/app/models/card/stallable.rb +++ b/app/models/card/stallable.rb @@ -7,7 +7,7 @@ module Card::Stallable has_one :activity_spike, class_name: "Card::ActivitySpike", dependent: :destroy scope :with_activity_spikes, -> { joins(:activity_spike) } - scope :stalled, -> { open.with_activity_spikes.where("card_activity_spikes.updated_at": ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) } + scope :stalled, -> { open.active.with_activity_spikes.where("card_activity_spikes.updated_at": ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) } before_update :remember_to_detect_activity_spikes after_update_commit :detect_activity_spikes_later, if: :should_detect_activity_spikes? diff --git a/app/models/card/triageable.rb b/app/models/card/triageable.rb new file mode 100644 index 000000000..e310e1bce --- /dev/null +++ b/app/models/card/triageable.rb @@ -0,0 +1,34 @@ +module Card::Triageable + extend ActiveSupport::Concern + + included do + belongs_to :column, optional: true, touch: true + + scope :awaiting_triage, -> { active.where.missing(:column) } + scope :triaged, -> { active.joins(:column) } + end + + def triaged? + active? && column.present? + end + + def awaiting_triage? + active? && !triaged? + end + + def triage_into(column) + raise "The column must belong to the card collection" unless collection == column.collection + + transaction do + resume + update! column: column + end + end + + def send_back_to_triage + transaction do + resume + update! column: nil + end + end +end diff --git a/app/models/cards/columns.rb b/app/models/cards/columns.rb deleted file mode 100644 index 2ef9b9957..000000000 --- a/app/models/cards/columns.rb +++ /dev/null @@ -1,45 +0,0 @@ -class Cards::Columns - attr_reader :user_filtering, :page_size - - delegate :filter, to: :user_filtering - - def initialize(user_filtering:, page_size:) - @user_filtering = user_filtering - @page_size = page_size - end - - def considering - @considering ||= build_column_for "considering" - end - - def on_deck - @on_deck ||= build_column_for "on_deck" - end - - def doing - @doing ||= build_column_for "doing" - end - - def closed - @closed ||= if filter.indexed_by.stalled? - build_column(filter) { |cards| cards.recently_closed_first } - else - build_column(filter.with(indexed_by: "closed")) { |cards| cards.recently_closed_first } - end - end - - def cache_key - ActiveSupport::Cache.expand_cache_key([ considering, on_deck, doing, closed, Workflow.all, user_filtering ]) - end - - private - def build_column_for(engagement_status) - build_column(filter.with(engagement_status: engagement_status)) - end - - def build_column(filter, &block) - cards = block ? yield(filter.cards) : filter.cards - - Column.new(page: GearedPagination::Recordset.new(cards, per_page: page_size).page(1), filter: filter, user_filtering: user_filtering) - end -end diff --git a/app/models/cards/columns/column.rb b/app/models/cards/columns/column.rb deleted file mode 100644 index c48c29fb8..000000000 --- a/app/models/cards/columns/column.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Cards::Columns::Column - attr_reader :page, :filter, :user_filtering - - def initialize(page:, filter:, user_filtering:) - @page = page - @filter = filter - @user_filtering = user_filtering - end - - def cards - page.records - end - - def cache_key - ActiveSupport::Cache.expand_cache_key([ cards ]) - end -end diff --git a/app/models/closure/reason.rb b/app/models/closure/reason.rb index 9dca74fc3..f071901c1 100644 --- a/app/models/closure/reason.rb +++ b/app/models/closure/reason.rb @@ -3,7 +3,8 @@ class Closure::Reason < ApplicationRecord "Completed", "Duplicate", "Maybe later", - "Working as intended" + "Working as intended", + "Not now" ] FALLBACK_LABEL = "Done" diff --git a/app/models/collection.rb b/app/models/collection.rb index 0d8fdeb37..bd4d403c7 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -1,5 +1,5 @@ class Collection < ApplicationRecord - include AutoClosing, Accessible, Broadcastable, Entropic, Filterable, Publishable, Workflowing + include AutoClosing, Accessible, Broadcastable, Entropic, Filterable, Publishable, Triageable, Workflowing belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/collection/auto_closing.rb b/app/models/collection/auto_closing.rb index 5f000c48b..98cf21385 100644 --- a/app/models/collection/auto_closing.rb +++ b/app/models/collection/auto_closing.rb @@ -2,13 +2,13 @@ module Collection::AutoClosing extend ActiveSupport::Concern included do - before_create :set_default_auto_close_period + before_create :set_default_auto_postpone_period end private - DEFAULT_AUTO_CLOSE_PERIOD = 30.days + DEFAULT_auto_postpone_period = 30.days - def set_default_auto_close_period - self.auto_close_period ||= DEFAULT_AUTO_CLOSE_PERIOD unless attribute_present?(:auto_close_period) + def set_default_auto_postpone_period + self.auto_postpone_period ||= DEFAULT_auto_postpone_period unless attribute_present?(:auto_postpone_period) end end diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index 9a4a6b362..a3879955b 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -2,20 +2,15 @@ module Collection::Entropic extend ActiveSupport::Concern included do - delegate :auto_close_period, :auto_reconsider_period, to: :entropy_configuration + delegate :auto_postpone_period, to: :entropy_configuration end def entropy_configuration super || Entropy::Configuration.default end - def auto_close_period=(new_value) + def auto_postpone_period=(new_value) entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration - entropy_configuration.update auto_close_period: new_value - end - - def auto_reconsider_period=(new_value) - entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration - entropy_configuration.update auto_reconsider_period: new_value + entropy_configuration.update auto_postpone_period: new_value end end diff --git a/app/models/collection/triageable.rb b/app/models/collection/triageable.rb new file mode 100644 index 000000000..a314868c8 --- /dev/null +++ b/app/models/collection/triageable.rb @@ -0,0 +1,7 @@ +module Collection::Triageable + extend ActiveSupport::Concern + + included do + has_many :columns, dependent: :destroy + end +end diff --git a/app/models/column.rb b/app/models/column.rb new file mode 100644 index 000000000..f62a728ce --- /dev/null +++ b/app/models/column.rb @@ -0,0 +1,14 @@ +class Column < ApplicationRecord + belongs_to :collection + has_many :cards, dependent: :nullify + + validates :name, presence: true + validates :color, presence: true + + before_validation :set_default_color + + private + def set_default_color + self.color ||= Card::DEFAULT_COLOR + end +end diff --git a/app/models/filter.rb b/app/models/filter.rb index 6bef679e5..b848a43bb 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -23,12 +23,10 @@ class Filter < ApplicationRecord result = result.sorted_by(sorted_by) result = result.where(id: card_ids) if card_ids.present? result = result.open unless include_closed_cards? - 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.where(collection: collections.ids) if collections.present? - result = result.in_stage(stages.ids) if stages.present? && engagement_status&.doing? result = result.tagged_with(tags.ids) if tags.present? result = result.where("cards.created_at": creation_window) if creation_window result = result.closed_at_window(closure_window) if closure_window diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index af0966d2a..fc11752fc 100644 --- a/app/models/filter/fields.rb +++ b/app/models/filter/fields.rb @@ -1,7 +1,7 @@ module Filter::Fields extend ActiveSupport::Concern - INDEXES = %w[ all stalled closing_soon falling_back_soon golden draft ] + INDEXES = %w[ all stalled postponing_soon falling_back_soon golden draft ] SORTED_BY = %w[ newest oldest latest ] delegate :default_value?, to: :class @@ -18,7 +18,7 @@ module Filter::Fields included do store_accessor :fields, :assignment_status, :indexed_by, :sorted_by, :terms, - :engagement_status, :card_ids, :creation, :closure + :card_ids, :creation, :closure def assignment_status super.to_s.inquiry @@ -32,10 +32,6 @@ module Filter::Fields (super || default_sorted_by).inquiry end - def engagement_status - super&.inquiry - end - def creation_window TimeWindowParser.parse(creation) end diff --git a/app/models/filter/params.rb b/app/models/filter/params.rb index 81ba67c99..6992516f5 100644 --- a/app/models/filter/params.rb +++ b/app/models/filter/params.rb @@ -5,7 +5,6 @@ module Filter::Params :assignment_status, :indexed_by, :sorted_by, - :engagement_status, :creation, :closure, card_ids: [], @@ -44,7 +43,7 @@ module Filter::Params def used?(ignore_collections: false) tags.any? || assignees.any? || creators.any? || closers.any? || - stages.any? || terms.any? || card_ids&.any? || (!ignore_collections && collections.present?) || + terms.any? || card_ids&.any? || (!ignore_collections && collections.present?) || assignment_status.unassigned? || !indexed_by.all? || !sorted_by.latest? end @@ -54,14 +53,12 @@ module Filter::Params @as_params ||= {}.tap do |params| params[:indexed_by] = indexed_by params[:sorted_by] = sorted_by - params[:engagement_status] = engagement_status params[:creation] = creation params[:closure] = closure params[:assignment_status] = assignment_status params[:terms] = terms params[:tag_ids] = tags.ids params[:collection_ids] = collections.ids - params[:stage_ids] = stages.ids params[:card_ids] = card_ids params[:assignee_ids] = assignees.ids params[:creator_ids] = creators.ids diff --git a/app/models/filter/resources.rb b/app/models/filter/resources.rb index da71ce982..a12ed368f 100644 --- a/app/models/filter/resources.rb +++ b/app/models/filter/resources.rb @@ -4,7 +4,6 @@ module Filter::Resources included do has_and_belongs_to_many :tags has_and_belongs_to_many :collections - has_and_belongs_to_many :stages, class_name: "Workflow::Stage", join_table: "filters_stages" has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id" has_and_belongs_to_many :creators, class_name: "User", join_table: "creators_filters", association_foreign_key: "creator_id" has_and_belongs_to_many :closers, class_name: "User", join_table: "closers_filters", association_foreign_key: "closer_id" diff --git a/app/models/filter/summarized.rb b/app/models/filter/summarized.rb index 22bc6bc5e..01f21799c 100644 --- a/app/models/filter/summarized.rb +++ b/app/models/filter/summarized.rb @@ -1,6 +1,6 @@ module Filter::Summarized def summary - [ index_summary, sort_summary, tag_summary, assignee_summary, creator_summary, stage_summary, terms_summary ].compact.to_sentence + [ index_summary, sort_summary, tag_summary, assignee_summary, creator_summary, terms_summary ].compact.to_sentence end private @@ -30,12 +30,6 @@ module Filter::Summarized end end - def stage_summary - if stages.any? - "staged in #{stages.pluck(:name).to_choice_sentence}" - end - end - def terms_summary if terms.any? "matching #{terms.map { |term| %Q("#{term}") }.to_sentence}" diff --git a/app/models/user/highlights.rb b/app/models/user/highlights.rb index 8383e6a44..43a6745f8 100644 --- a/app/models/user/highlights.rb +++ b/app/models/user/highlights.rb @@ -15,6 +15,7 @@ module User::Highlights def generate_weekly_highlights(date = Time.current) in_time_zone do + date = date - 1.day if date.sunday? PeriodHighlights.create_or_find_for collections, starts_at: highlights_starts_at(date), duration: 1.week end end @@ -27,7 +28,7 @@ module User::Highlights private def highlights_starts_at(date = Time.current) - date = date - 1.day if date.sunday? + date = date.in_time_zone(timezone) date.beginning_of_week(:sunday) end end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index f385c5dd0..196af1cc3 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -7,7 +7,7 @@ class Workflow < ApplicationRecord has_many :stages, dependent: :destroy has_many :collections, dependent: :nullify - after_create_commit :create_default_stages + after_create_commit :create_default_columns private def create_default_stages diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 6d41d439d..72bca26cb 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -3,12 +3,6 @@ <% content_for :header do %> <%= render "filters/menu", user_filtering: @user_filtering %>

<%= @page_title %>

-
- <%= link_to workflows_path, class: "btn" do %> - <%= icon_tag "bolt" %> - Workflows - <% end %> -
<% end %>
diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index f067781b1..057282d85 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,7 +1,6 @@ <% cache card.cache_invalidation_parts.for_perma do %>
- <%= render "cards/container/engagement", card: card %> <%= render "cards/container/status", card: card %>