diff --git a/Gemfile b/Gemfile index efd0166b4..17e33abd2 100644 --- a/Gemfile +++ b/Gemfile @@ -19,9 +19,7 @@ gem "solid_cache", "~> 1.0" gem "solid_queue", "~> 1.2" gem "sqlite3", ">= 2.0" gem "thruster", require: false -source "https://e95ae463b12de3f204526a44650f6ae0@gems.stanko.io/private" do - gem "beamer-rails", "~> 0.1.0.beta4" -end +gem "beamer-rails", "~> 0.1.0.beta4" # Features gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index cfbb22c7b..02c28a0e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ GIT remote: https://github.com/basecamp/activerecord-tenanted - revision: aaa4a3473eab400c4c8a6ca05a7207578d3ade53 + revision: e409f75dc2ea1c3355e98feb22b76b3cc0a01fe0 specs: - activerecord-tenanted (0.5.0) + activerecord-tenanted (0.6.0) activerecord (>= 8.1.beta) railties (>= 8.1.beta) zeitwerk @@ -36,7 +36,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: d3d17c297a70d3e255a34693a1fb0074712e9930 + revision: bc41ad696a76a151db56259938dccf64ef87b501 branch: main specs: actioncable (8.2.0.alpha) @@ -143,20 +143,6 @@ PATH rails (>= 8.1.0.beta1) rails_structured_logging -GEM - remote: https://gems.stanko.io/private/ - specs: - beamer-rails (0.1.0.beta4-arm64-darwin) - sqlite3 (>= 2.0) - beamer-rails (0.1.0.beta4-arm64-linux-gnu) - sqlite3 (>= 2.0) - beamer-rails (0.1.0.beta4-x86_64-darwin) - sqlite3 (>= 2.0) - beamer-rails (0.1.0.beta4-x86_64-linux-gnu) - sqlite3 (>= 2.0) - beamer-rails (0.1.0.beta4-x86_64-linux-musl) - sqlite3 (>= 2.0) - GEM remote: https://rubygems.org/ specs: @@ -198,6 +184,16 @@ GEM base64 (0.3.0) bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) + beamer-rails (0.1.0.beta4-arm64-darwin) + sqlite3 (>= 2.0) + beamer-rails (0.1.0.beta4-arm64-linux-gnu) + sqlite3 (>= 2.0) + beamer-rails (0.1.0.beta4-x86_64-darwin) + sqlite3 (>= 2.0) + beamer-rails (0.1.0.beta4-x86_64-linux-gnu) + sqlite3 (>= 2.0) + beamer-rails (0.1.0.beta4-x86_64-linux-musl) + sqlite3 (>= 2.0) benchmark (0.5.0) bigdecimal (3.3.1) bindex (0.8.1) @@ -294,7 +290,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.18.beta) + lexxy (0.1.20.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) @@ -537,7 +533,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.1.0) + uri (1.1.1) useragent (0.16.11) vcr (6.3.1) base64 @@ -604,7 +600,7 @@ DEPENDENCIES autotuner aws-sdk-s3 bcrypt (~> 3.1.7) - beamer-rails (~> 0.1.0.beta4)! + beamer-rails (~> 0.1.0.beta4) benchmark bootsnap brakeman diff --git a/app/assets/images/collapse.svg b/app/assets/images/collapse.svg new file mode 100644 index 000000000..90effb7cf --- /dev/null +++ b/app/assets/images/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/fizzy.svg b/app/assets/images/fizzy.svg new file mode 100644 index 000000000..fd9f5f8bf --- /dev/null +++ b/app/assets/images/fizzy.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/grid.svg b/app/assets/images/grid.svg new file mode 100644 index 000000000..eca0897e6 --- /dev/null +++ b/app/assets/images/grid.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/logo-color.svg b/app/assets/images/logo-color.svg deleted file mode 100644 index 32d315b5f..000000000 --- a/app/assets/images/logo-color.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png index 82632a317..92d4a8e74 100644 Binary files a/app/assets/images/logo.png and b/app/assets/images/logo.png differ diff --git a/app/assets/images/logo.webp b/app/assets/images/logo.webp new file mode 100644 index 000000000..20568de8b Binary files /dev/null and b/app/assets/images/logo.webp differ diff --git a/app/assets/images/maximize.svg b/app/assets/images/maximize.svg new file mode 100644 index 000000000..425fa32d0 --- /dev/null +++ b/app/assets/images/maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css index c18cf4090..9f1092f82 100644 --- a/app/assets/stylesheets/_global.css +++ b/app/assets/stylesheets/_global.css @@ -89,7 +89,7 @@ --lch-ink-darkest: 26% 0.05 264; --lch-ink-darker: 40% 0.026 262; - --lch-ink-dark: 57% 0.014 260; + --lch-ink-dark: 56% 0.014 260; --lch-ink-medium: 66% 0.008 258; --lch-ink-light: 84% 0.005 256; --lch-ink-lighter: 92% 0.003 254; diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css index e2710006d..5a5c910d1 100644 --- a/app/assets/stylesheets/buttons.css +++ b/app/assets/stylesheets/buttons.css @@ -40,7 +40,7 @@ pointer-events: none; } - [aria-busy] &:disabled { + form[aria-busy] &:disabled { position: relative; > * { diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index a74e8e3f8..675f3fe13 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -3,6 +3,10 @@ /* ------------------------------------------------------------------------ */ #main:has(.card-columns) { + --main-padding: 0; + } + + .card-columns { --bubble-size: 3.5rem; --cards-gap: min(1.2cqi, 1.7rem); --column-gap: 8px; @@ -14,10 +18,7 @@ --progress-max-cards: 20; --progress-max-height: 50dvh; - padding-inline: var(--column-gap); - } - - .card-columns { + -ms-overflow-style: none; /* Hide x-scrollbar on Edge */ container-type: inline-size; display: grid; gap: var(--column-gap); @@ -26,7 +27,14 @@ max-inline-size: var(--main-width); overflow-x: auto; overflow-y: hidden; + padding-block-end: var(--column-width-collapsed); position: relative; + scrollbar-width: none; /* Hide x-scrollbar on FF */ + + /* Hide x-scrollbar on Chrome/Safari/Opera */ + &::-webkit-scrollbar { + display: none; + } /* When it has something expanded */ &:has(.card-columns__left .cards:not(.is-collapsed), .card-columns__right .cards:not(.is-collapsed)) { @@ -51,10 +59,12 @@ .card-columns__left { justify-content: end; + padding-inline-start: var(--column-gap); } .card-columns__right { justify-content: start; + padding-inline-end: var(--column-gap); } /* Column @@ -63,20 +73,9 @@ .cards { --column-color: color-mix(in srgb, var(--card-color) 15%, var(--color-canvas)); - border-radius: calc(var(--column-width-collapsed) / 2); inline-size: var(--column-width-expanded); position: relative; scroll-snap-align: start; - translate: 0; - transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle); - - .no-transitions & { - transition: none; - } - - &:not(.is-collapsed) { - padding-inline: var(--column-padding-expanded); - } &.is-collapsed { inline-size: var(--column-width-collapsed); @@ -85,12 +84,41 @@ .card { display: none; } + } - &:not(.cards--considering) { - translate: 0 var(--column-width-collapsed); + &.drag-and-drop__hover-container { + --dnd-bg-color: transparent; + --dnd-border-color: transparent; + + &.is-off-screen { + &:after { + content: attr(data-column-name); + font-size: var(--text-x-small); + font-weight: 500; + line-height: var(--column-width-collapsed); + padding-inline: 1ch; + position: fixed; + text-transform: uppercase; + top: 0; + translate: -50%; + } + + &.is-collapsed { + &:after { + writing-mode: vertical-rl; + } + } + + &:not(.is-collapsed) { + &:after { + background-color: var(--column-color); + inline-size: calc(var(--column-width-expanded) - 4px); /* make room for the dnd border */ + } + } } } + @media (any-hover: hover) { .card:has(.card__background img:not([src=""])):hover .card__background img:not([src=""]) { filter: blur(3px) brightness(1.2); @@ -99,6 +127,36 @@ } } + .cards__transition-container { + block-size: 100%; + border-radius: calc(var(--column-width-collapsed) / 2); + translate: 0; + transition: translate var(--column-transition-duration) var(--ease-out-overshoot-subtle); + + .is-collapsed:not(.cards--considering) & { + translate: 0 var(--column-width-collapsed); + } + + .cards:not(.is-collapsed) & { + padding-inline: var(--column-padding-expanded); + } + + .drag-and-drop__hover-container & { + --dnd-bg-color: var(--column-color); + --dnd-border-color: var(--card-color); + + background-color: var(--dnd-bg-color); + outline: 2px dashed var(--dnd-border-color); + outline-offset: -2px; + transition: background-color 200ms; + z-index: 1; + } + + .no-transitions & { + transition: none; + } + } + /* The wrapper around the cards used to clip overflow while transitioning. * Also, don't resize cards while transitioning to avoid reflow. */ .cards__list { @@ -171,6 +229,46 @@ /* Column Elements /* ------------------------------------------------------------------------ */ + .cards__header { + .cards.is-collapsed & { + block-size: 100%; + } + + .cards:not(.is-collapsed) & { + display: grid; + grid-template-areas: "menu expander maximize"; + grid-template-columns: var(--column-width-collapsed) 1fr var(--column-width-collapsed); + margin-block-end: calc(0.5 * var(--cards-gap)); + } + } + + .cards__menu .btn--circle, + .cards__maximize-button { + --btn-background: transparent; + + block-size: var(--column-width-collapsed); + inline-size: var(--column-width-collapsed); + opacity: 0; + outline-offset: -2px; + + .cards:hover &, + &:focus-visible { + opacity: 1; + } + + .cards.is-collapsed & { + display: none; + } + } + + .cards__menu { + z-index: var(--z-popup); + } + + .cards__maximize-button { + grid-area: maximize; + } + .cards__expander { align-items: center; border-radius: 99rem; @@ -180,6 +278,8 @@ font-size: var(--text-x-small); font-weight: 600; gap: 0.5ch; + grid-area: expander; + outline-offset: -2px; position: relative; text-transform: uppercase; @@ -239,7 +339,6 @@ .cards:not(.is-collapsed) & { inline-size: 100%; - margin-block-end: calc(0.5 * var(--cards-gap)); justify-content: center; } @@ -247,39 +346,6 @@ cursor: unset; filter: none; } - - @media (any-hover: hover) { - .btn--circle, - ~ .btn--circle, - ~ .cards__expander-menu .btn--circle:not(label) { - transition: opacity 300ms ease-in-out; - opacity: 0; - - &:focus-visible, - .cards:hover & { - opacity: 1; - } - } - } - } - - .btn.cards__expander-button { - --btn-background: transparent; - - block-size: var(--column-width-collapsed); - inline-size: var(--column-width-collapsed); - inset: 0 0 auto auto; - position: absolute; - translate: calc(var(--column-gap) * -2); - z-index: var(--z-popup); - - .cards--considering & { - translate: none; - } - - .cards.is-collapsed & { - display: none; - } } .cards__expander-count { @@ -305,25 +371,28 @@ } .cards:not(.is-collapsed, .cards--considering) & { + align-items: center; + display: flex; + gap: 0.25ch; max-inline-size: calc(100% - var(--column-width-collapsed) * 2); } - } - .cards__expander-menu { - inset: 0 auto auto 0; - position: absolute; - translate: calc(var(--column-gap) * 2); - z-index: var(--z-popup); + .icon--collapse { + --icon-size: 1.15em; - .btn--circle { - --btn-background: transparent; + opacity: 0; + scale: 1.5; + transition: 150ms ease-out; + transition-property: opacity, scale; - block-size: var(--column-width-collapsed); - inline-size: var(--column-width-collapsed); - } + .cards.is-collapsed & { + display: none; + } - .cards.is-collapsed & { - display: none; + .cards:not(.is-collapsed) .cards__expander:hover & { + opacity: 0.66; + scale: 1; + } } } @@ -331,6 +400,10 @@ box-shadow: none !important; color: color-mix(in srgb, var(--card-color) 70%, var(--color-canvas)); + .cards--considering & { + background-color: var(--card-bg-color) !important; + } + p { font-size: var(--text-small); overflow: hidden; @@ -344,10 +417,6 @@ display: initial; } } - - .cards--grid &:not(.blank-slate-for-grid) { - display: none; - } } } @@ -450,8 +519,8 @@ @media (any-hover: hover) { .card:hover & { - opacity: 1; - } + opacity: 1; + } } } } @@ -473,11 +542,9 @@ /* ------------------------------------------------------------------------ */ .cards--considering { - position: relative; + --card-color: oklch(var(--lch-blue-medium)); - @media (min-width: 640px) { - padding-inline: var(--cards-gap); - } + position: relative; .card { --avatar-size: 2.75em; @@ -604,24 +671,26 @@ /* -------------------------------------------------------------------------- */ /* Surface a mini bubble if there are cards with bubbles inside */ - .cards--considering:has(.bubble:not([hidden])) .cards__expander-title, + .cards--considering:has(.bubble:not([hidden])), .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%; + .cards__transition-container { + --bubble-color: var(--card-color, oklch(var(--lch-blue-medium))); + --bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%; - &: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; + &: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; + } } } diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 0160fdaaf..fd506aa17 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -240,8 +240,9 @@ translate: 0 calc(-1 * var(--half-btn-height)); } - &:has([open]) { - z-index: 1; + form { + background-color: var(--color-canvas); + border-radius: 99rem; } .btn:not(.popup__btn, .btn--plain, .btn--reversed) { diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 2b61ac4ce..2591332d9 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -179,17 +179,20 @@ display: none; } - & ~ .btn { - --btn-background: var(--card-color); - --btn-color: var(--color-ink-inverted); - } - } - - .card-field__description { lexxy-toolbar { border-block-start: 1px solid var(--color-ink-light); margin-block-start: var(--block-space-half) !important; } + + & ~ .btn.btn--reversed { + --btn-background: var(--card-color); + --btn-color: var(--color-ink-inverted); + } + + & ~ .btn { + --btn-border-color: var(--card-color); + --btn-color: var(--card-color); + } } .card__stages { @@ -253,6 +256,21 @@ } } + .card__assignees-trigger { + background: transparent; + border: none; + padding: 0; + display: flex; + + &:focus-visible { + outline: none; + + .btn { + box-shadow: 0 0 0 var(--focus-ring-size) var(--focus-ring-color); + } + } + } + .card__meta-text { line-height: 1; white-space: nowrap; @@ -498,5 +516,9 @@ margin-inline: auto; text-align: start; } + + code { + text-align: left; + } } } diff --git a/app/assets/stylesheets/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css index 99b3089eb..2ada036b4 100644 --- a/app/assets/stylesheets/drag_and_drop.css +++ b/app/assets/stylesheets/drag_and_drop.css @@ -7,8 +7,11 @@ } .drag-and-drop__hover-container { - background-color: var(--color-selected-light); - outline: 2px dashed var(--color-selected-dark); + --dnd-bg-color: var(--color-selected-light); + --dnd-border-color: var(--color-selected-dark); + + background-color: var(--dnd-bg-color); + outline: 2px dashed var(--dnd-border-color); outline-offset: -2px; transition: background-color 200ms; z-index: 1; diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css index d8b5a2a43..be5448991 100644 --- a/app/assets/stylesheets/header.css +++ b/app/assets/stylesheets/header.css @@ -13,7 +13,8 @@ "menu menu menu" "actions-start title actions-end"; max-inline-size: 100dvw; - padding: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--main-padding); + padding-block: calc(var(--block-space-half) + env(safe-area-inset-top)) var(--block-space-half); + padding-inline: var(--main-padding); /* Change the grid size depending on how many buttons are present */ &:has(.header__actions > *:nth-child(1)) { --header-button-count: 1; } @@ -78,4 +79,16 @@ --left-offset: var(--inline-space); } } + + .header__logo { + color: var(--color-ink); + inline-size: auto; + margin-block-start: 0.1em; + + img { + block-size: auto; + inline-size: 1.15em; + margin-inline-end: 0.8ch; + } + } } diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index 8575b72f2..8e9f19e89 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -42,6 +42,7 @@ .icon--clipboard { --svg: url("clipboard.svg "); } .icon--close { --svg: url("close.svg "); } .icon--close-circle { --svg: url("close-circle.svg "); } + .icon--collapse { --svg: url("collapse.svg "); } .icon--board { --svg: url("board.svg "); } .icon--board-add { --svg: url("board-add.svg "); } .icon--column-left { --svg: url("column-left.svg "); } @@ -53,7 +54,9 @@ .icon--everyone { --svg: url("everyone.svg "); } .icon--expand { --svg: url("expand.svg "); } .icon--gear { --svg: url("gear.svg "); } + .icon--grid { --svg: url("grid.svg "); } .icon--filter { --svg: url("filter.svg "); } + .icon--fizzy { --svg: url("fizzy.svg"); } .icon--globe { --svg: url("globe.svg "); } .icon--golden-ticket { --svg: url("golden-ticket.svg "); } .icon--home { --svg: url("home.svg "); } @@ -64,6 +67,7 @@ .icon--lock { --svg: url("lock.svg "); } .icon--logout { --svg: url("logout.svg "); } .icon--marker { --svg: url("marker.svg "); } + .icon--maximize { --svg: url("maximize.svg "); } .icon--menu { --svg: url("menu.svg "); } .icon--menu-dots-horizontal { --svg: url("menu-dots-horizontal.svg "); } .icon--menu-dots-vertical { --svg: url("menu-dots-vertical.svg "); } diff --git a/app/assets/stylesheets/boxcar-menu.css b/app/assets/stylesheets/nav.css similarity index 75% rename from app/assets/stylesheets/boxcar-menu.css rename to app/assets/stylesheets/nav.css index e60a56bb9..2aa7be278 100644 --- a/app/assets/stylesheets/boxcar-menu.css +++ b/app/assets/stylesheets/nav.css @@ -1,17 +1,8 @@ @layer components { - .boxcar-header-logo { - color: var(--color-ink); - inline-size: auto; - margin-block-start: 0.1em; + /* Trigger + /* ------------------------------------------------------------------------ */ - img { - block-size: auto; - inline-size: 1.15em; - margin-inline-end: 0.8ch; - } - } - - .input:is(.boxcar-menu-trigger) { + .nav__trigger { --input-background: var(--color-canvas); --input-border-color: transparent; --input-padding: 0.3em 2em 0.3em 0.75em; @@ -58,37 +49,45 @@ } } - .boxcar-menu:is(.panel) { + /* Dialog + /* ------------------------------------------------------------------------ */ + + .nav__menu.popup { --panel-border-color: var(--color-selected-dark); --panel-border-radius: 1.4em; + --panel-padding: var(--block-space) var(--block-space) 0 var(--block-space); --panel-size: 45ch; + --popup-display: grid; + --nav-section-gap: 2px; - align-items: stretch; + block-size: auto; box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%), 0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%), 0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%), 0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%); - flex-direction: column; - gap: 2px; + grid-template-rows: auto 1fr auto; + gap: var(--nav-section-gap); + overflow: hidden; scrollbar-gutter: stable both-edges; - - /* Safari doesn't position the sticky footer properly during the - * transition, so we need to delay the footer entrance. */ - .popup__footer { - animation: safari-sticky-footer-hack 0s; - animation-delay: var(--dialog-duration); - animation-fill-mode: both; - inset-inline: var(--block-space); - } } - .boxcar-menu__close { + .nav__scroll-container { + display: flex; + flex-direction: column; + gap: var(--nav-section-gap); + margin-inline: calc(-1 * var(--block-space)); /* space for scrollbar */ + overflow: auto; + padding-block-end: var(--nav-section-gap); + padding-inline: var(--block-space); + } + + .nav__close { @media (any-hover: hover) { display: none; } } - .boxcar-menu__hotkeys { + .nav__hotkeys { --gap: 8px; align-items: center; @@ -152,4 +151,15 @@ } } } + + .nav__footer { + background-color: var(--color-canvas); + border-block-start: 1px solid var(--color-ink-lighter); + font-size: var(--text-small); + line-height: 1.6; + margin-block-start: calc(-1 * var(--nav-section-gap)); + padding: 1.5ch; + text-align: center; + z-index: 1; + } } diff --git a/app/assets/stylesheets/panels.css b/app/assets/stylesheets/panels.css index 9b0345096..a82dc1665 100644 --- a/app/assets/stylesheets/panels.css +++ b/app/assets/stylesheets/panels.css @@ -6,12 +6,22 @@ color: var(--color-ink); inline-size: var(--panel-size, 60ch); max-inline-size: 100%; - padding: var(--panel-padding, calc(var(--block-space) * 2)); + padding: var(--panel-padding, var(--block-space)); + + @media (min-width: 640px) { + --panel-size: 100%; + + padding: var(--panel-padding, var(--block-space-double)); + } } .panel--centered { --panel-border-size: 0; - --panel-size: 42ch; + --panel-size: 100%; + + @media (min-width: 640px) { + --panel-size: 42ch; + } #main:has(&) { display: grid; diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 825414807..c0803326d 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -6,6 +6,7 @@ --panel-size: auto; --popup-icon-size: 24px; --popup-item-padding-inline: 0.5rem; + --popup-display: flex; inset: 0 auto auto 50%; min-inline-size: min(25ch, calc(100dvw - var(--block-space-double))); @@ -14,39 +15,9 @@ position: absolute; transform: translateX(-50%); z-index: var(--z-popup); - opacity: 0; - transform: scale(0.2) translateX(-50%); - transform-origin: top left; - transition: var(--dialog-duration) allow-discrete; - transition-property: display, opacity, overlay, transform; - - &::backdrop { - background-color: var(--color-always-black); - opacity: 0; - transform: scale(1); - transition: var(--dialog-duration) allow-discrete; - transition-property: display, opacity, overlay; - } &[open] { - display: flex; - opacity: 1; - transform: scale(1) translateX(-50%); - - &::backdrop { - opacity: 0.5; - } - } - - @starting-style { - &[open] { - opacity: 0; - transform: scale(0.2) translateX(-50%); - } - - &[open]::backdrop { - opacity: 0; - } + display: var(--popup-display); } &.orient-left { @@ -59,10 +30,6 @@ transform: translateX(0); } - &:has(.popup__footer) { - --panel-padding: var(--block-space) var(--block-space) 0 var(--block-space); - } - form { display: contents; } @@ -86,11 +53,11 @@ background: var(--color-canvas); font-size: var(--text-small); font-weight: bold; + inset-block-start: 0; list-style: none; padding: var(--block-space-half) var(--inline-space-half); position: sticky; text-transform: uppercase; - top: calc(-1 * var(--block-space)); z-index: 1; &:is(summary) { @@ -196,21 +163,42 @@ } } - .popup__footer { - background-color: var(--color-canvas); - border-block-start: 1px solid var(--color-ink-lighter); - font-size: var(--text-small); - inset: auto 0 0; - line-height: 1.6; - margin-block-start: var(--block-space-half); - padding: 1.5ch; - position: sticky; - text-align: center; - z-index: 1; - } + /* Animated + /* -------------------------------------------------------------------------- */ - @keyframes safari-sticky-footer-hack { - 0% { position: absolute; } - 100% { position: sticky; } + .popup--animated { + opacity: 0; + transform: scale(0.2) translateX(-50%); + transform-origin: top left; + transition: var(--dialog-duration) allow-discrete; + transition-property: display, opacity, overlay, transform; + + &::backdrop { + background-color: var(--color-always-black); + opacity: 0; + transform: scale(1); + transition: var(--dialog-duration) allow-discrete; + transition-property: display, opacity, overlay; + } + + &[open] { + opacity: 1; + transform: scale(1) translateX(-50%); + + &::backdrop { + opacity: 0.5; + } + } + + @starting-style { + &[open] { + opacity: 0; + transform: scale(0.2) translateX(-50%); + } + + &[open]::backdrop { + opacity: 0; + } + } } } diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index df7f8d3a8..890e27ac3 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -35,8 +35,8 @@ print-color-adjust: exact; } - .boxcar-menu, - .boxcar-menu-trigger, + .nav__menu, + .nav__trigger, .header__actions { display: none; } diff --git a/app/assets/stylesheets/reactions.css b/app/assets/stylesheets/reactions.css index 664aa7941..80bde798a 100644 --- a/app/assets/stylesheets/reactions.css +++ b/app/assets/stylesheets/reactions.css @@ -29,6 +29,7 @@ } .reactions__trigger { + --btn-border-color: var(--color-ink-lightest); background-color: var(--color-ink-lightest); &:not(:hover) { @@ -38,10 +39,23 @@ } } + .reactions__trigger { + --btn-border-color: var(--reaction-border-color); + + img { + block-size: 65%; + inline-size: 65%; + } + } + .reactions__list { display: inline-flex; flex-wrap: wrap; gap: var(--inline-space-half); + + &:not(:has(.reaction)) { + display: none; + } } /* Single reaction diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 72cba909b..5e871260f 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -1,13 +1,13 @@ @layer components { .rich-text-content { - --block-margin: 0; + --block-margin: 0.5lh; :where(h1, h2, h3, h4, h5, h6) { display: block; font-weight: 800; letter-spacing: -0.02ch; line-height: 1.1; - margin-block: var(--block-margin); + margin-block: 0 var(--block-margin); overflow-wrap: break-word; text-wrap: balance; } @@ -62,14 +62,17 @@ } :where(hr) { + color: currentColor; block-size: 0; - border-block-end: 1px solid currentColor; + border: none; + border-block-end: 2px solid currentColor; inline-size: 20%; - margin: var(--block-margin) 0; + margin: calc(var(--block-margin) * 2) 0; } .horizontal-divider { - margin-inline: 0; + margin: var(--block-margin) 0; + padding-block: var(--block-margin); } .lexxy-content__strikethrough { @@ -372,6 +375,12 @@ } } + action-text-attachment[content-type^='application/vnd.actiontext.mention'] { + img { + object-fit: cover; + } + } + [data-lexical-cursor] { animation: blink 1s step-end infinite; block-size: 1lh; diff --git a/app/assets/stylesheets/tooltips.css b/app/assets/stylesheets/tooltips.css index 20a02985b..419664fc6 100644 --- a/app/assets/stylesheets/tooltips.css +++ b/app/assets/stylesheets/tooltips.css @@ -1,5 +1,5 @@ @layer components { - [data-controller^="tooltip"] { + [data-controller~="tooltip"] { --tooltip-delay: 750ms; --tooltip-duration: 150ms; diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 99bac2252..6ad6b2089 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -16,7 +16,7 @@ .txt-ink { color: var(--color-ink); } .txt-reversed { color: var(--color-ink-inverted); } .txt-negative { color: var(--color-negative); } - .txt-subtle { color: var(--color-ink-medium); } + .txt-subtle { color: var(--color-ink-dark); } .txt-alert { color: var(--color-marker); } .txt-undecorated { text-decoration: none; } .txt-underline { text-decoration: underline; } diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb index f45f3ef5f..00d195584 100644 --- a/app/controllers/account/entropies_controller.rb +++ b/app/controllers/account/entropies_controller.rb @@ -1,4 +1,6 @@ class Account::EntropiesController < ApplicationController + before_action :ensure_admin + def update Account.sole.entropy.update!(entropy_params) redirect_to account_settings_path, notice: "Account updated" diff --git a/app/controllers/account/join_codes_controller.rb b/app/controllers/account/join_codes_controller.rb index 026c692f9..562ffaa0d 100644 --- a/app/controllers/account/join_codes_controller.rb +++ b/app/controllers/account/join_codes_controller.rb @@ -1,5 +1,6 @@ class Account::JoinCodesController < ApplicationController before_action :set_join_code + before_action :ensure_admin, only: %i[ update destroy ] def show end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dc70236ef..235b9c2a0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base include Authorization include CurrentRequest, CurrentTimezone, SetPlatform include TurboFlash, ViewTransitions + include Saas stale_when_importmap_changes allow_browser versions: :modern diff --git a/app/controllers/boards/entropies_controller.rb b/app/controllers/boards/entropies_controller.rb index 153dafe5f..e07272e9b 100644 --- a/app/controllers/boards/entropies_controller.rb +++ b/app/controllers/boards/entropies_controller.rb @@ -1,6 +1,8 @@ class Boards::EntropiesController < ApplicationController include BoardScoped + before_action :ensure_permission_to_admin_board + def update @board.entropy.update!(entropy_params) end diff --git a/app/controllers/boards/publications_controller.rb b/app/controllers/boards/publications_controller.rb index b7cda07ee..9435bd8bf 100644 --- a/app/controllers/boards/publications_controller.rb +++ b/app/controllers/boards/publications_controller.rb @@ -1,6 +1,8 @@ class Boards::PublicationsController < ApplicationController include BoardScoped + before_action :ensure_permission_to_admin_board + def create @board.publish end diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 4bff8dde4..70383e139 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -2,7 +2,7 @@ class BoardsController < ApplicationController include FilterScoped before_action :set_board, except: %i[ new create ] - before_action :ensure_permission_to_change_accesses, only: %i[ update ] + before_action :ensure_permission_to_admin_board, only: %i[ update ] def show if @filter.used?(ignore_boards: true) @@ -48,20 +48,12 @@ class BoardsController < ApplicationController @board = Current.user.boards.find params[:id] end - def ensure_permission_to_change_accesses - if trying_to_change_accesses? && !Current.user.can_administer_board?(@board) + def ensure_permission_to_admin_board + unless Current.user.can_administer_board?(@board) head :forbidden end end - def trying_to_change_accesses? - all_access_changed? || grantees_changed? - end - - def all_access_changed? - params[:board]&.key?(:all_access) - end - def grantees_changed? params.key?(:user_ids) end diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index cac32efde..fdd2a86dc 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -3,7 +3,7 @@ class Cards::AssignmentsController < ApplicationController def new @users = @board.users.active.alphabetically - fresh_when @users + fresh_when etag: [ @users, @card.assignees ] end def create diff --git a/app/controllers/cards/columns_controller.rb b/app/controllers/cards/columns_controller.rb index f250f2dd6..3e32b1cb8 100644 --- a/app/controllers/cards/columns_controller.rb +++ b/app/controllers/cards/columns_controller.rb @@ -1,7 +1,7 @@ class Cards::ColumnsController < ApplicationController def edit @card = Current.user.accessible_cards.find(params[:card_id]) - @columns = @card.board.columns + @columns = @card.board.columns.sorted fresh_when etag: [ @card, @columns ] end diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index 50f58e34b..23bac85e3 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -3,7 +3,7 @@ class Cards::TaggingsController < ApplicationController def new @tags = Tag.all.alphabetically - fresh_when @tags + fresh_when etag: [ @tags, @card.tags ] end def create diff --git a/app/controllers/concerns/board_scoped.rb b/app/controllers/concerns/board_scoped.rb index 2772c7d01..f4f3a3e2b 100644 --- a/app/controllers/concerns/board_scoped.rb +++ b/app/controllers/concerns/board_scoped.rb @@ -9,4 +9,10 @@ module BoardScoped def set_board @board = Current.user.boards.find(params[:board_id]) end + + def ensure_permission_to_admin_board + unless Current.user.can_administer_board?(@board) + head :forbidden + end + end end diff --git a/app/controllers/concerns/saas.rb b/app/controllers/concerns/saas.rb new file mode 100644 index 000000000..fd4ca8491 --- /dev/null +++ b/app/controllers/concerns/saas.rb @@ -0,0 +1,11 @@ +module Saas + extend ActiveSupport::Concern + + included do + helper_method :signups_allowed? + end + + def signups_allowed? + defined?(Signup) && defined?(saas) + end +end diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index f9980a8db..a4b6768b9 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -4,6 +4,8 @@ class JoinCodesController < ApplicationController before_action :set_join_code before_action :ensure_join_code_is_valid + layout "public" + def new @account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name } end @@ -14,7 +16,8 @@ class JoinCodesController < ApplicationController identity.memberships.find_or_create_by!(tenant: tenant) do |membership| membership.join_code = code end - identity.send_magic_link + magic_link = identity.send_magic_link + flash[:magic_link_code] = magic_link&.code if Rails.env.development? end session[:return_to_after_authenticating] = landing_url(script_name: "/#{tenant}") diff --git a/app/controllers/memberships/email_addresses_controller.rb b/app/controllers/memberships/email_addresses_controller.rb index c75783ac1..0870410a9 100644 --- a/app/controllers/memberships/email_addresses_controller.rb +++ b/app/controllers/memberships/email_addresses_controller.rb @@ -1,6 +1,8 @@ class Memberships::EmailAddressesController < ApplicationController require_untenanted_access + layout "public" + before_action :set_membership rate_limit to: 5, within: 1.hour, only: :create diff --git a/app/controllers/my/menus_controller.rb b/app/controllers/my/menus_controller.rb index 900cf7c0a..2b20d2188 100644 --- a/app/controllers/my/menus_controller.rb +++ b/app/controllers/my/menus_controller.rb @@ -1,7 +1,10 @@ class My::MenusController < ApplicationController - include FilterScoped - def show - fresh_when etag: [ @user_filtering, Current.session ] + @filters = Current.user.filters.all + @boards = Current.user.boards.ordered_by_recently_accessed + @tags = Tag.all.alphabetically + @users = User.active.alphabetically + + # fresh_when etag: [ @user_filtering, Current.session ] end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 10a3c9773..8f8222ed9 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,4 +1,9 @@ class SessionsController < ApplicationController + # FIXME: Remove this before launch! + SIGNUP_USERNAME = Rails.env.local? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name + SIGNUP_PASSWORD = Rails.env.local? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password + http_basic_authenticate_with name: SIGNUP_USERNAME, password: SIGNUP_PASSWORD, realm: "Fizzy Signup", only: :create, unless: -> { Identity.exists?(email_address: email_address) } + require_untenanted_access require_unauthenticated_access except: :destroy rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." } @@ -9,8 +14,15 @@ class SessionsController < ApplicationController end def create - Identity.find_by_email_address(email_address)&.send_magic_link - redirect_to session_magic_link_path + if identity = Identity.find_by_email_address(email_address) + magic_link = identity.send_magic_link + flash[:magic_link_code] = magic_link&.code if Rails.env.development? + redirect_to session_magic_link_path + elsif signups_allowed? + Signup.new(email_address: email_address).create_identity + session[:return_to_after_authenticating] = saas.new_signup_membership_path + redirect_to session_magic_link_path + end end def destroy diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb index d811a6203..73a9ae296 100644 --- a/app/controllers/users/joins_controller.rb +++ b/app/controllers/users/joins_controller.rb @@ -3,6 +3,8 @@ class Users::JoinsController < ApplicationController before_action :set_join_code, :ensure_join_code_is_valid + layout "public" + def new end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f6afd0ba4..fc37d1635 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,9 @@ module ApplicationHelper def page_title_tag - tag.title @page_title || "Fizzy" + account_name = if ApplicationRecord.current_tenant && Current.session&.identity&.memberships&.many? + Account.sole&.name + end + tag.title [ @page_title, account_name, "Fizzy" ].compact.join(" | ") end def icon_tag(name, **options) diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 3dccac678..f572434ec 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -19,6 +19,16 @@ module AvatarsHelper end end + def mail_avatar_tag(user, size: 48, **options) + if user.avatar.attached? + image_tag user_avatar_url(user), alt: user.name, class: "avatar", size: size, **options + else + tag.span class: "avatar", style: "background-color: #{avatar_background_color(user)};" do + user.initials + end + end + end + def avatar_preview_tag(user, hidden_for_screen_reader: false, **options) tag.span class: class_names("avatar", options.delete(:class)), aria: { hidden: hidden_for_screen_reader, label: user.name }, diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index ae8739d05..3fde0d2ad 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -1,8 +1,8 @@ module BoardsHelper def link_back_to_board(board) link_to board, class: "btn borderless txt-medium", - data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click click->turbo-navigation#backIfSamePath" } do - tag.span ("←" + tag.strong(board.name, class: "overflow-ellipsis")).html_safe + data: { controller: "hotkey", action: "keydown.left@document->hotkey#click click->turbo-navigation#backIfSamePath" } do + tag.span(tag.strong("Back to #{board.name}", class: "overflow-ellipsis") + tag.kbd("←", class: "txt-x-small margin-inline-start hide-on-touch"), class: "flex align-center").html_safe end end diff --git a/app/helpers/clipboard_helper.rb b/app/helpers/clipboard_helper.rb index b079dc6a9..972856b15 100644 --- a/app/helpers/clipboard_helper.rb +++ b/app/helpers/clipboard_helper.rb @@ -1,7 +1,7 @@ module ClipboardHelper def button_to_copy_to_clipboard(url, &) tag.button class: "btn", data: { - controller: "copy-to-clipboard", action: "copy-to-clipboard#copy", + controller: "copy-to-clipboard tooltip", action: "copy-to-clipboard#copy", copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url }, & end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 992463740..501abf2dd 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,138 +1,22 @@ module EventsHelper - def event_columns(event_type, day_timeline) - case event_type - when "added" - events = day_timeline.events.where(action: [ "card_published", "card_reopened" ]) - full_events_count = events.count - { - title: event_column_title("Added", full_events_count, day_timeline.day), - index: 1, - events: events.limit(100).load, - full_events_count: full_events_count - } - when "closed" - events = day_timeline.events.where(action: "card_closed") - full_events_count = events.count - { - title: event_column_title("Closed", full_events_count, day_timeline.day), - index: 3, - events: events.limit(100).load, - full_events_count: full_events_count - } + def event_action_icon(event) + case event.action + when "card_assigned" + "assigned" + when "card_unassigned" + "minus" + when "comment_created" + "comment" + when "card_title_changed" + "rename" + when "card_board_changed" + "move" else - events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ]) - full_events_count = events.count - { - title: event_column_title("Updated", full_events_count, day_timeline.day), - index: 2, - events: events.limit(100).load, - full_events_count: full_events_count - } + "person" end end - private - def event_column_title(base_title, count, day) - date_tag = local_datetime_tag day, style: :agoorweekday - parts = [ base_title, date_tag ] - parts << tag.span("(#{count})", class: "font-weight-normal") if count > 0 - safe_join(parts, " ") - end - - def event_column(event) - case event.action - when "card_closed" - 3 - when "card_published", "card_reopened" - 1 - else - 2 - end - end - - def event_cluster_tag(hour, col, &) - row = 25 - hour - tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", & - end - - def event_action_sentence(event) - if event.action.comment_created? - comment_event_action_sentence(event) - else - card_event_action_sentence(event) - end - end - - def comment_event_action_sentence(event) - "#{event_creator_name(event)} commented on #{event_card_title(event.eventable.card)}".html_safe - end - - def event_creator_name(event) - tag.span data: { creator_id: event.creator.id } do - safe_join([ - tag.span("You", data: { only_visible_to_you: true }), - tag.span(h(event.creator.name), data: { only_visible_to_others: true }) - ]) - end - end - - def card_event_action_sentence(event) - card = event.eventable - - case event.action - when "card_assigned" - if event.assignees.include?(Current.user) - "#{event_creator_name(event)} will handle #{event_card_title(card)}" - else - "#{event_creator_name(event)} assigned #{h event.assignees.pluck(:name).to_sentence} to #{event_card_title(card)}" - end - when "card_unassigned" - "#{event_creator_name(event)} unassigned #{h(event.assignees.include?(Current.user) ? "yourself" : event.assignees.pluck(:name).to_sentence)} from #{event_card_title(card)}" - when "card_published" - "#{event_creator_name(event)} added #{event_card_title(card)}" - when "card_closed" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “Done”" - when "card_reopened" - "#{event_creator_name(event)} reopened #{event_card_title(card)}" - when "card_postponed" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “Not Now”" - when "card_auto_postponed" - "#{event_card_title(card)} was closed as “Not Now” due to inactivity" - when "card_resumed" - "#{event_creator_name(event)} resumed #{event_card_title(card)}" - when "card_title_changed" - "#{event_creator_name(event)} renamed #{event_card_title(card)} (was: “#{h event.particulars.dig('particulars', 'old_title')}”)" - when "card_board_changed", "card_collection_changed" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h (event.particulars.dig('particulars', 'new_board') || event.particulars.dig('particulars', 'new_collection'))}”" - when "card_triaged" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'column')}”" - when "card_sent_back_to_triage" - "#{event_creator_name(event)} moved #{event_card_title(card)} back to “Maybe?”" - end&.html_safe - end - - def event_card_title(card) - tag.span card.title, class: "txt-underline" - end - - def event_due_date(event) - event.particulars.dig("particulars", "due_date").to_date.strftime("%B %-d") - end - - def event_action_icon(event) - case event.action - when "card_assigned" - "assigned" - when "card_unassigned" - "minus" - when "comment_created" - "comment" - when "card_title_changed" - "rename" - when "card_board_changed" - "move" - else - "person" - end - end + def events_at_hour_container(column, hour, &block) + tag.div class: "events__time-block", style: "grid-area: #{25 - hour}/#{column.index}", &block + end end diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb index 2962a49ac..7924c4d97 100644 --- a/app/helpers/my/menu_helper.rb +++ b/app/helpers/my/menu_helper.rb @@ -16,4 +16,39 @@ module My::MenuHelper navigable_list_target: "input", action: "input->filter#filter" } end + + def my_menu_board_item(board) + my_menu_item("board", board) do + link_to(tag.span(board.name, class: "overflow-ellipsis"), board, class: "popup__btn btn") + end + end + + def my_menu_tag_item(the_tag) + my_menu_item("tag", tag) do + link_to(tag.span("#{the_tag.title} (#{the_tag.cards_count})", class: "overflow-ellipsis"), cards_path(tag_ids: [ the_tag ]), class: "popup__btn btn") + end + end + + def my_menu_user_item(user) + my_menu_item("person", user) do + link_to(tag.span(user.name, class: "overflow-ellipsis"), user, class: "popup__btn btn") + end + end + + def my_menu_filter_item(filter) + my_menu_item("bookmark", filter) do + link_to(cards_path(filter_id: filter.id), class: "popup__btn btn") do + tag.div(class: "txt-tight-lines min-width txt-small overflow-ellipsis") do + tag.div(tag.strong(filter.boards_label)) + + tag.div(filter.summary, class: "txt-capitalize") + end + end + end + end + + def my_menu_item(item, record) + tag.li(class: "popup__item", data: { filter_target: "item", navigable_list_target: "item", id: "filter-#{item}-#{record.id}" }) do + icon_tag(item, class: "popup__icon") + yield + end + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 51ff30da0..dda94684d 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -28,7 +28,7 @@ module NotificationsHelper } do link_to(notification, class: [ "card card--notification", { "card--closed": notification.card.closed? }, { "unread": !notification.read? } ], - data: { turbo_frame: "_top", badge_target: "unread", action: "badge#update" }, + data: { turbo_frame: "_top", badge_target: "unread", action: "badge#update dialog#close" }, style: { "--card-color:": notification.card.color }, &) end diff --git a/app/javascript/controllers/clicker_controller.js b/app/javascript/controllers/clicker_controller.js index d09cb9ede..6d3571942 100644 --- a/app/javascript/controllers/clicker_controller.js +++ b/app/javascript/controllers/clicker_controller.js @@ -6,6 +6,10 @@ export default class extends Controller { async click() { await nextFrame() - this.clickableTarget.click() + this.#clickable.click() + } + + get #clickable() { + return this.hasClickableTarget ? this.clickableTarget : this.element } } diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 74dd1ff65..b11821f3f 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -2,8 +2,8 @@ import { Controller } from "@hotwired/stimulus" import { nextFrame, debounce } from "helpers/timing_helpers"; export default class extends Controller { - static classes = [ "collapsed", "noTransitions" ] - static targets = [ "column", "button" ] + static classes = [ "collapsed", "noTransitions", "titleNotVisible" ] + static targets = [ "column", "button", "title" ] static values = { board: String } @@ -14,6 +14,14 @@ export default class extends Controller { async connect() { await this.#restoreColumnsDisablingTransitions() + this.#setupIntersectionObserver() + } + + disconnect() { + if (this._intersectionObserver) { + this._intersectionObserver.disconnect() + this._intersectionObserver = null + } } toggle({ target }) { @@ -106,4 +114,23 @@ export default class extends Controller { #localStorageKeyFor(column) { return `expand-${this.boardValue}-${column.getAttribute("id")}` } + + #setupIntersectionObserver() { + if (typeof IntersectionObserver === "undefined") return + if (this._intersectionObserver) this._intersectionObserver.disconnect() + + this._intersectionObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + const title = entry.target + const column = title.closest(".cards") + + if (!column) return + + const offscreen = entry.intersectionRatio === 0 + column.classList.toggle(this.titleNotVisibleClass, offscreen) + }) + }, { threshold: [0] }) + + this.titleTargets.forEach(title => this._intersectionObserver.observe(title)) + } } diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index 16204bd28..4fbf25dc5 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -24,6 +24,7 @@ export default class extends Controller { limitHeightToViewport(this.dialogTarget, true) + this.#loadLazyFrames() this.dialogTarget.setAttribute("aria-hidden", "false") this.dispatch("show") } @@ -54,4 +55,8 @@ export default class extends Controller { event.stopPropagation() } } + + #loadLazyFrames() { + Array.from(this.dialogTarget.querySelectorAll("turbo-frame")).forEach(frame => { frame.loading = "eager" }) + } } diff --git a/app/javascript/controllers/dialog_manager_controller.js b/app/javascript/controllers/dialog_manager_controller.js new file mode 100644 index 000000000..71552e2c8 --- /dev/null +++ b/app/javascript/controllers/dialog_manager_controller.js @@ -0,0 +1,24 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.element.addEventListener("dialog:show", this.handleDialogShow.bind(this)) + } + + disconnect() { + this.element.removeEventListener("dialog:show", this.handleDialogShow.bind(this)) + } + + handleDialogShow(event) { + this.#dialogControllers.forEach(dialogController => { + if (dialogController !== event.target) { + const dialog = dialogController.querySelector("dialog") + dialog.removeAttribute("open") + } + }) + } + + get #dialogControllers() { + return this.element.querySelectorAll('[data-controller~="dialog"]') + } +} diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js index a5aa1a94f..94acaf28b 100644 --- a/app/javascript/controllers/navigable_list_controller.js +++ b/app/javascript/controllers/navigable_list_controller.js @@ -80,6 +80,10 @@ export default class extends Controller { selectedItem.setAttribute(this.selectionAttributeValue, "true") this.currentItem = selectedItem await nextFrame() + try { + this.currentItem.scrollIntoView({ block: "nearest", inline: "nearest" }) + } catch (e) {} + if (this.focusOnSelectionValue) { this.currentItem.focus() } if (this.hasInputTarget && id) { this.inputTarget.setAttribute("aria-activedescendant", id) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 4e7878b62..abeb3ae09 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base - default from: "The Fizzy team " + default from: "Fizzy " layout "mailer" append_view_path Rails.root.join("app/views/mailers") diff --git a/app/mailers/magic_link_mailer.rb b/app/mailers/magic_link_mailer.rb index 391e64f6d..f23cc4fbf 100644 --- a/app/mailers/magic_link_mailer.rb +++ b/app/mailers/magic_link_mailer.rb @@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer @magic_link = magic_link @identity = @magic_link.identity - mail to: @identity.email_address, subject: "Sign in to Fizzy" + mail to: @identity.email_address, subject: "Your Fizzy verification code" end private diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb deleted file mode 100644 index 09926c09b..000000000 --- a/app/mailers/user_mailer.rb +++ /dev/null @@ -1,8 +0,0 @@ -class UserMailer < ApplicationMailer - def email_change_confirmation(user:, email_address:, token:) - @user = user - @token = token - - mail to: email_address, subject: "Confirm your new email address" - end -end diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 991877833..28835041c 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -31,49 +31,49 @@ class Account::Seeder playground = Board.create! name: "Playground", creator: creator, all_access: true # Cards - playground.cards.create! creator: creator, title: "Watch this: Fizzy orientation video", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Finally, watch this Fizzy orientation video", status: "published", description: <<~HTML

There’s a whole lot more you can do in Fizzy. In the video below 37signals founder and CEO, Jason Fried, will walk you through the basics in just 8 minutes.


HTML - playground.cards.create! creator: creator, title: "Grab the invite link to invite someone else", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Now, grab the invite link to invite someone else", status: "published", description: <<~HTML

Open Fizzy menu, select “+ Add people”, then copy the invite link. You can give this link to someone else so they can make an login for themselves in your account.


HTML - playground.cards.create! creator: creator, title: "Head back home to check out activity", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Then, head back home to check out activity", status: "published", description: <<~HTML

Hit “1” or pull down the Fizzy menu and select “Home”.


HTML - playground.cards.create! creator: creator, title: "Check out all cards assigned to you", status: "published", description: <<~HTML -

Pull down the Fizzy menu at the top of the screen, and select “Assigned to me” or just hit “3” on your keyboard any time.

+ playground.cards.create! creator: creator, title: "Now,check out all cards assigned to you", status: "published", description: <<~HTML +

Pull down the Fizzy menu at the top of the screen, and select “Assigned to me” or just hit “2” on your keyboard any time.


HTML - playground.cards.create! creator: creator, title: "Open the Fizzy menu", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Then, open the Fizzy menu", status: "published", description: <<~HTML

The Fizzy menu is how you get around the app. Click “Fizzy” at the top of the screen or hit the “J” key on your keyboard to pop it open.


HTML - playground.cards.create! creator: creator, title: "Assign this card to yourself", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Next, assign this card to yourself", status: "published", description: <<~HTML

Click the little head with the + next to it, pick yourself.


HTML - playground.cards.create! creator: creator, title: "Tag this card “Design” then move it to YES", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Now, tag this card “Design” then move it to YES", status: "published", description: <<~HTML

Click the little Tag icon, type “design”, then “Create tag”. Then, move the card to the new “YES” column you created in the previous step.


HTML - playground.cards.create! creator: creator, title: "Make two more columns", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Next, make two more columns", status: "published", description: <<~HTML
  1. Make one called "Yes"
  2. Make another called "Working on"
  3. @@ -86,13 +86,13 @@ class Account::Seeder HTML - playground.cards.create! creator: creator, title: "Move this card to NOT NOW", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "Second, move this card to NOT NOW", status: "published", description: <<~HTML

    You can either select “NOT NOW” over in the sidebar, or you can go back out to the board view and drag this card into the “NOT NOW” column on the left side.


    HTML - playground.cards.create! creator: creator, title: "Rename this card", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "First, rename this card", status: "published", description: <<~HTML
    1. Click the title and you can rename the card, change the description, or add more information to the card.
    2. Then, hit "Mark as Done" at the bottom of the card.
    3. diff --git a/app/models/board.rb b/app/models/board.rb index 9e259d652..71ff693fb 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -1,5 +1,5 @@ class Board < ApplicationRecord - include AutoPostponing, Accessible, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable + include Accessible, AutoPostponing, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/board/accessible.rb b/app/models/board/accessible.rb index 6d62c0b21..75b2e982e 100644 --- a/app/models/board/accessible.rb +++ b/app/models/board/accessible.rb @@ -45,6 +45,7 @@ module Board::Accessible mentions_for_user(user).destroy_all notifications_for_user(user).destroy_all + watches_for(user).destroy_all end def watchers @@ -89,4 +90,8 @@ module Board::Accessible (notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.board_id = ?)", id, id) end + + def watches_for(user) + Watch.where(card: cards, user: user) + end end diff --git a/app/models/card.rb b/app/models/card.rb index 65c7cab18..859f4b1a7 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,6 +1,6 @@ class Card < ApplicationRecord - include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, - Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, + include Assignable, Attachments, Closeable, Colored, Entropic, Eventable, + Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable belongs_to :board, touch: true diff --git a/app/models/card/broadcastable.rb b/app/models/card/broadcastable.rb deleted file mode 100644 index aa24b776c..000000000 --- a/app/models/card/broadcastable.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Card::Broadcastable - extend ActiveSupport::Concern - - included do - broadcasts_refreshes - end -end diff --git a/app/models/card/mentions.rb b/app/models/card/mentions.rb index cb3a34f17..3ce74726b 100644 --- a/app/models/card/mentions.rb +++ b/app/models/card/mentions.rb @@ -7,5 +7,9 @@ module Card::Mentions def mentionable? published? end + + def should_check_mentions? + saved_change_to_status? && published? + end end end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index 055c114fc..df8563ba3 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -19,20 +19,6 @@ module Mentions private def scan_mentionees - mentionees_from_plain_text | mentionees_from_rich_text - end - - def mentionees_from_plain_text - scan_mentioned_handles.filter_map do |mention| - mentionable_users.find { |user| user.mentionable_handles.include?(mention) } - end - end - - def scan_mentioned_handles - mentionable_content.scan(/(?(value) { value.strip.downcase } + normalizes :email_address, with: ->(value) { value.strip.downcase.presence } def send_magic_link magic_links.create!.tap do |magic_link| diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb index 570989566..6179a4404 100644 --- a/app/models/notifier/card_event_notifier.rb +++ b/app/models/notifier/card_event_notifier.rb @@ -8,7 +8,7 @@ class Notifier::CardEventNotifier < Notifier when "card_assigned" source.assignees.excluding(creator) when "card_published" - board.watchers.without(creator, *card.mentionees) + board.watchers.without(creator, *card.mentionees).including(*card.assignees).uniq when "comment_created" card.watchers.without(creator, *source.eventable.mentionees) else diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb index 367eccdcb..a5f98cb8a 100644 --- a/app/models/user/day_timeline.rb +++ b/app/models/user/day_timeline.rb @@ -29,6 +29,18 @@ class User::DayTimeline day.yesterday.beginning_of_day end + def added_column + @added_column ||= build_column("Added", 1, events.where(action: %w[card_published card_reopened])) + end + + def updated_column + @updated_column ||= build_column("Updated", 2, events.where.not(action: %w[card_published card_closed card_reopened])) + end + + def closed_column + @closed_column ||= build_column("Closed", 3, events.where(action: "card_closed")) + end + def cache_key ActiveSupport::Cache.expand_cache_key [ user, filter, day.to_date, events ], "day-timeline" end @@ -70,6 +82,10 @@ class User::DayTimeline filtered_events.where(created_at: ...day.beginning_of_day).chronologically.last end + def build_column(base_title, index, events) + Column.new(self, base_title, index, events) + end + def window day.all_day end diff --git a/app/models/user/day_timeline/column.rb b/app/models/user/day_timeline/column.rb new file mode 100644 index 000000000..fe42c68dc --- /dev/null +++ b/app/models/user/day_timeline/column.rb @@ -0,0 +1,42 @@ +class User::DayTimeline::Column + include ActionView::Helpers::TagHelper, ActionView::Helpers::OutputSafetyHelper, TimeHelper + + attr_reader :index + + def initialize(day_timeline, base_title, index, events) + @day_timeline = day_timeline + @base_title = base_title + @index = index + @events = events + end + + def title + date_tag = local_datetime_tag(day_timeline.day, style: :agoorweekday) + parts = [ base_title, date_tag ] + parts << tag.span("(#{full_events_count})", class: "font-weight-normal") if full_events_count > 0 + safe_join(parts, " ") + end + + def events_by_hour + limited_events.group_by { it.created_at.hour } + end + + def has_more_events? + limited_events.count < full_events_count + end + + def hidden_events_count + full_events_count - limited_events.count + end + + private + attr_reader :base_title, :day_timeline, :events + + def limited_events + @limited_events ||= events.limit(100).load + end + + def full_events_count + @full_events_count ||= events.count + end +end diff --git a/app/models/user/role.rb b/app/models/user/role.rb index a327ffcf6..e12bf0191 100644 --- a/app/models/user/role.rb +++ b/app/models/user/role.rb @@ -18,7 +18,7 @@ module User::Role admin? || other == self end - def can_administer?(other = nil) + def can_administer?(other) admin? && other != self end diff --git a/app/views/account/join_codes/edit.html.erb b/app/views/account/join_codes/edit.html.erb index de53bf8c8..14958a44c 100644 --- a/app/views/account/join_codes/edit.html.erb +++ b/app/views/account/join_codes/edit.html.erb @@ -2,8 +2,11 @@ <% content_for :header do %>
      - <%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - Invite link + <%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to Invite link + + <% end %>
      <% end %> diff --git a/app/views/account/join_codes/show.html.erb b/app/views/account/join_codes/show.html.erb index f3142b091..c528bb07d 100644 --- a/app/views/account/join_codes/show.html.erb +++ b/app/views/account/join_codes/show.html.erb @@ -2,8 +2,11 @@ <% content_for :header do %>
      - <%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - Account Settings + <%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to Account Settings + + <% end %>
      <% end %> @@ -17,10 +20,13 @@ <% url = join_url(code: @join_code.code, tenant: @join_code.tenant, script_name: nil) %>
      - <%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: { - turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %> - <%= icon_tag "refresh" %> - Generate a new code + + <% if Current.user.admin? %> + <%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: { + turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %> + <%= icon_tag "refresh" %> + Generate a new code + <% end %> <% end %>
      @@ -59,8 +65,10 @@ This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit %> times (<%= @join_code.active? ? @join_code.usage_limit - @join_code.usage_count : "none" %> remaining) - <%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %> - Change limit + <% if Current.user.admin? %> + <%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %> + Change limit + <% end %> <% end %>

      diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb index f6056ab37..11ff9b326 100644 --- a/app/views/account/settings/_entropy.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -4,5 +4,5 @@

      Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not Now” without activity for specific period of time. This is the default, global setting — you can override it on each board.

      - <%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %> + <%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path, disabled: !Current.user.admin? %> diff --git a/app/views/account/settings/_name.html.erb b/app/views/account/settings/_name.html.erb index 3c4f68f03..e3b12f325 100644 --- a/app/views/account/settings/_name.html.erb +++ b/app/views/account/settings/_name.html.erb @@ -1,7 +1,7 @@ <%= form_with model: account, url: account_settings_path, method: :put, scope: :account, data: { controller: "form" }, class: "flex gap-half" do |form| %> - <%= form.text_field :name, required: true, class: "input input--transparent full-width txt-medium", placeholder: "Account name…", data: { action: "input->form#disableSubmitWhenInvalid" }, readonly: !Current.user.can_administer? %> + <%= form.text_field :name, required: true, class: "input input--transparent full-width txt-medium", placeholder: "Account name…", data: { action: "input->form#disableSubmitWhenInvalid" }, readonly: !Current.user.admin? %> - <% if Current.user.can_administer? %> + <% if Current.user.admin? %> <%= form.button class: "btn btn--circle btn--link txt-medium", data: { form_target: "submit" }, disabled: form.object do %> <%= icon_tag "arrow-right" %> Save changes diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 7e2dbbfbf..57f3913bf 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -1,7 +1,12 @@ <% @page_title = "Account Settings" %> <% content_for :header do %> -

      <%= @page_title %>

      +

      + <%= @page_title %> + <% unless Current.user.admin? %> +
      Only admins can change these settings
      + <% end %> +

      <% end %>
      diff --git a/app/views/boards/columns/closeds/show.html.erb b/app/views/boards/columns/closeds/show.html.erb index 9aeb21044..a67c45c3b 100644 --- a/app/views/boards/columns/closeds/show.html.erb +++ b/app/views/boards/columns/closeds/show.html.erb @@ -2,12 +2,7 @@ <% content_for :header do %>
      - <%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @board.name %> - - <% end %> + <%= link_back_to_board(@board) %>

      diff --git a/app/views/boards/columns/not_nows/show.html.erb b/app/views/boards/columns/not_nows/show.html.erb index 4bd6a0524..379d92dea 100644 --- a/app/views/boards/columns/not_nows/show.html.erb +++ b/app/views/boards/columns/not_nows/show.html.erb @@ -2,12 +2,7 @@ <% content_for :header do %>
      - <%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @board.name %> - - <% end %> + <%= link_back_to_board(@board) %>

      diff --git a/app/views/boards/columns/show.html.erb b/app/views/boards/columns/show.html.erb index 4c29f5099..83931aaff 100644 --- a/app/views/boards/columns/show.html.erb +++ b/app/views/boards/columns/show.html.erb @@ -2,12 +2,7 @@ <% content_for :header do %>
      - <%= link_to @column.board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @column.board.name %> - - <% end %> + <%= link_back_to_board(@column.board) %>

      @@ -16,7 +11,7 @@ <% end %>
      - <%= turbo_frame_tag dom_id(@column, :cards) do %> + <%= turbo_frame_tag @column, :cards do %> <% if @page.used? %> <%= with_automatic_pagination dom_id(@column, :cards), @page do %> <%= render "boards/columns/list", cards: @page.records, draggable: true %> diff --git a/app/views/boards/columns/streams/show.html.erb b/app/views/boards/columns/streams/show.html.erb index 59e929464..141584283 100644 --- a/app/views/boards/columns/streams/show.html.erb +++ b/app/views/boards/columns/streams/show.html.erb @@ -2,12 +2,7 @@ <% content_for :header do %>
      - <%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @board.name %> - - <% end %> + <%= link_back_to_board(@board) %>

      diff --git a/app/views/boards/edit.html.erb b/app/views/boards/edit.html.erb index 206bc5ed5..36beee3c8 100644 --- a/app/views/boards/edit.html.erb +++ b/app/views/boards/edit.html.erb @@ -2,15 +2,15 @@ <% content_for :header do %>
      - <%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @board.name %> - - <% end %> + <%= link_back_to_board(@board) %>
      -

      <%= @page_title %>

      +

      +
      <%= @page_title %>
      + <% unless Current.user.can_administer_board?(@board) %> +
      Only admins can change these settings
      + <% end %> +

      <% end %>
      @@ -19,10 +19,10 @@ controller: "form boards-form", boards_form_self_removal_prompt_message_value: "Are you sure you want to remove yourself from this board? You won’t be able to get back in unless someone invites you.", action: "turbo:submit-start->boards-form#submitWithWarning" } do |form| %> - <%= render "boards/edit/name", form: form %> + <%= render "boards/edit/name", form: form, board: @board %> <%= render "boards/edit/users", board: @board, selected_users: @selected_users, unselected_users: @unselected_users, form: form %> - @@ -33,6 +33,6 @@
      <%= render "boards/edit/auto_close", board: @board %> <%= render "boards/edit/publication", board: @board %> - <%= render "boards/edit/delete", board: @board %> + <%= render "boards/edit/delete", board: @board if Current.user.can_administer_board?(@board) %>
      diff --git a/app/views/boards/edit/_auto_close.html.erb b/app/views/boards/edit/_auto_close.html.erb index d903ddacd..2e4dc55a2 100644 --- a/app/views/boards/edit/_auto_close.html.erb +++ b/app/views/boards/edit/_auto_close.html.erb @@ -2,6 +2,6 @@

      Auto close

      Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…

      - <%= render "entropy/auto_close", model: board, url: board_entropy_path(board) %> + <%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(@board) %>
      <% end %> diff --git a/app/views/boards/edit/_knob.html.erb b/app/views/boards/edit/_knob.html.erb deleted file mode 100644 index 5dda6727e..000000000 --- a/app/views/boards/edit/_knob.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% name = label.parameterize %> - -
      - -
      - <% knob_options.each_with_index do |value, i| %> - - <% end %> - - -
      -
      - -
      Days
      -
      diff --git a/app/views/boards/edit/_name.html.erb b/app/views/boards/edit/_name.html.erb index e238a838e..e1578a944 100644 --- a/app/views/boards/edit/_name.html.erb +++ b/app/views/boards/edit/_name.html.erb @@ -2,6 +2,7 @@ diff --git a/app/views/boards/edit/_publication.html.erb b/app/views/boards/edit/_publication.html.erb index 68902e19e..8ff98d87e 100644 --- a/app/views/boards/edit/_publication.html.erb +++ b/app/views/boards/edit/_publication.html.erb @@ -10,7 +10,7 @@ <%= icon_tag "lock" %>
      diff --git a/app/views/boards/show/_column.html.erb b/app/views/boards/show/_column.html.erb index 7630ce3fe..c763479ef 100644 --- a/app/views/boards/show/_column.html.erb +++ b/app/views/boards/show/_column.html.erb @@ -4,17 +4,20 @@ data-drag-and-strum-target="container" data-collapsible-columns-target="column" data-controller="clicker" + data-column-name="<%= column.name %>" data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle turbo:before-stream-render@document->collapsible-columns#restoreState" data-drag-and-drop-url="<%= columns_card_drops_column_path("__id__", column_id: column.id) %>" > - <%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) do %> - <%= render "boards/show/menu/column", column: column %> - <% end %> +
      +
      + <%= render "boards/show/menu/column", column: column %> + <%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) %> - <%= link_to board_column_path(column.board, column), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> - <%= icon_tag "expand", class: "translucent" %> - Expand column - <% end %> - - <%= column_frame_tag dom_id(column, :cards), src: board_column_path(column.board, column) %> + <%= link_to board_column_path(column.board, column), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Maximize column + <% end %> +
      + <%= column_frame_tag dom_id(column, :cards), src: board_column_path(column.board, column) %> +

      diff --git a/app/views/boards/show/_columns.html.erb b/app/views/boards/show/_columns.html.erb index a2a406aff..5223b9254 100644 --- a/app/views/boards/show/_columns.html.erb +++ b/app/views/boards/show/_columns.html.erb @@ -1,26 +1,28 @@ -<%= tag.div data: { - controller: "drag-and-drop drag-and-strum", +<%= tag.div class: "card-columns", data: { + controller: "collapsible-columns drag-and-drop drag-and-strum", drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item", drag_and_drop_hover_container_class: "drag-and-drop__hover-container", + collapsible_columns_board_value: board.id, + collapsible_columns_collapsed_class: "is-collapsed", + collapsible_columns_no_transitions_class: "no-transitions", + collapsible_columns_title_not_visible_class: "is-off-screen", action: " dragstart->drag-and-drop#dragStart dragover->drag-and-drop#dragOver dragenter->drag-and-strum#dragEnter drop->drag-and-drop#drop dragend->drag-and-drop#dragEnd" } do %> -
      -
      - <%= render "boards/show/not_now", board: board %> -
      +
      + <%= render "boards/show/not_now", board: board %> +
      - <%= render "boards/show/stream", board: board, page: page %> + <%= render "boards/show/stream", board: board, page: page %> -
      - <%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %> - <%= render "boards/show/closed", board: board %> +
      + <%= render partial: "boards/show/column", collection: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %> + <%= render "boards/show/closed", board: board %> - <%= render "boards/show/menu/columns", board: board %> -
      + <%= render "boards/show/menu/columns", board: board %>
      <% end %> diff --git a/app/views/boards/show/_expander.html.erb b/app/views/boards/show/_expander.html.erb index 91e6466da..d01debadf 100644 --- a/app/views/boards/show/_expander.html.erb +++ b/app/views/boards/show/_expander.html.erb @@ -1,6 +1,9 @@ <%= yield if block_given? %> diff --git a/app/views/boards/show/_not_now.html.erb b/app/views/boards/show/_not_now.html.erb index cdb40fe79..414b608c9 100644 --- a/app/views/boards/show/_not_now.html.erb +++ b/app/views/boards/show/_not_now.html.erb @@ -2,13 +2,15 @@ data-collapsible-columns-target="column" data-drag-and-drop-target="container" data-drag-and-strum-target="container" + data-column-name="Not Now" data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle" data-drag-and-drop-url="<%= columns_card_drops_not_now_path("__id__") %>" > - - <%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" do %> - <%= render "boards/show/menu/expand", column_path: board_columns_not_now_path(board) %> - <% end %> - - <%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %> +
      +
      + <%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" %> + <%= render "boards/show/menu/maximize", column_path: board_columns_not_now_path(board) %> +
      + <%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %> +
      diff --git a/app/views/boards/show/_stream.html.erb b/app/views/boards/show/_stream.html.erb index f8b9cd81f..f4ea59ea1 100644 --- a/app/views/boards/show/_stream.html.erb +++ b/app/views/boards/show/_stream.html.erb @@ -1,20 +1,23 @@
      "> -
      -

      Maybe?

      - <%= link_to board_columns_stream_path(board), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> - <%= icon_tag "expand", class: "translucent" %> - Expand column +
      +
      +
      +

      Maybe?

      +
      + <%= link_to board_columns_stream_path(board), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Expand column + <% end %> +
      + <%= render "columns/show/add_card_button", board: board %> + <% if page.used? %> + <%= with_automatic_pagination "the-stream", @page do %> + <%= render "boards/columns/list", cards: page.records, draggable: true %> + <% end %> <% end %> -
      - - <%= render "columns/show/add_card_button", board: board %> - - <% if page.used? %> - <%= with_automatic_pagination "the-stream", @page do %> - <%= render "boards/columns/list", cards: page.records, draggable: true %> - <% end %> - <% end %> +
      diff --git a/app/views/boards/show/menu/_column.html.erb b/app/views/boards/show/menu/_column.html.erb index 52637cce4..fa3ab38ff 100644 --- a/app/views/boards/show/menu/_column.html.erb +++ b/app/views/boards/show/menu/_column.html.erb @@ -1,4 +1,4 @@ -
      +
      <%= render "boards/show/menu/column_form", board: board, column: Column.new, label: "Add column" %> diff --git a/app/views/boards/show/menu/_expand.html.erb b/app/views/boards/show/menu/_maximize.html.erb similarity index 54% rename from app/views/boards/show/menu/_expand.html.erb rename to app/views/boards/show/menu/_maximize.html.erb index 4413739d1..0194dbea5 100644 --- a/app/views/boards/show/menu/_expand.html.erb +++ b/app/views/boards/show/menu/_maximize.html.erb @@ -1,4 +1,4 @@ -<%= link_to column_path, class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> - <%= icon_tag "expand", class: "translucent" %> +<%= link_to column_path, class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> Expand column <% end %> diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index c5b1c43ea..07ad6531b 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -5,7 +5,11 @@ dialog_target: "dialog", navigable_list_focus_on_selection_value: false, navigable_list_actionable_items_value: true } do %> - Assign this to… + +
      + Assign this to… + a +
      <% if @users.many? %> <%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true, diff --git a/app/views/cards/columns/edit.html.erb b/app/views/cards/columns/edit.html.erb index ae38fed17..e8061f9d6 100644 --- a/app/views/cards/columns/edit.html.erb +++ b/app/views/cards/columns/edit.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag dom_id(@card, :columns) do %> +<%= turbo_frame_tag @card, :columns do %>
      Choose a column for this card diff --git a/app/views/cards/comments/_new.html.erb b/app/views/cards/comments/_new.html.erb index ca5e731b8..cd46d7cb4 100644 --- a/app/views/cards/comments/_new.html.erb +++ b/app/views/cards/comments/_new.html.erb @@ -13,9 +13,16 @@ data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %> <%= general_prompts(@card.board) %> <% end %> - <%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %> - Post this comment - <% end %> + + + <%= form.button class: "comment__submit btn btn--reversed", + data: { form_target: "submit" }, disabled: true do %> + Post + <% end %> + + <%= hotkey_label([ "ctrl", "enter" ]) %> + + <% end %>
      diff --git a/app/views/cards/comments/reactions/_reactions.html.erb b/app/views/cards/comments/reactions/_reactions.html.erb index bf3ff746a..6de217842 100644 --- a/app/views/cards/comments/reactions/_reactions.html.erb +++ b/app/views/cards/comments/reactions/_reactions.html.erb @@ -6,7 +6,7 @@ <%= turbo_frame_tag comment, :new_reaction do %> <%= link_to new_card_comment_reaction_path(comment.card, comment), role: "button", - class: "reactions__trigger btn btn--circle borderless", action: "soft-keyboard#open", + class: "reactions__trigger btn btn--circle", action: "soft-keyboard#open", data: { turbo_frame: dom_id(comment, :new_reaction), action: "dialog#close" } do %> <%= image_tag "boost-color.svg", aria: { hidden: true } %> Add your own reaction diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index c40da47b9..941122578 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -7,8 +7,11 @@ <% end %>
      <% else %> - <%= button_to card_closure_path(card), class: "btn borderless" do %> + <%= button_to card_closure_path(card), class: "btn borderless", + data: { controller: "hotkey", form_target: "submit", action: "keydown.d@document->hotkey#click" }, + form: { data: { controller: "form" } } do %> Mark as Done + d <% end %>
      diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index ee18dec9f..98b0b3bb6 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -4,9 +4,16 @@ <%= link_to card.title, edit_card_path(card), class: "card__title-link" %> -
      - <%= card.description %> -
      + <% unless card.description.blank? %> +
      + <%= card.description %> +
      + <% end %> + + <%= link_to edit_card_path(card), class: "btn fit-content txt-small", data: { controller: "hotkey", action: "keydown.e@document->hotkey#click" } do %> + Edit + e + <% end %> <% end %> <% else %> <%= form_with model: card, id: "card_form", data: { controller: "autoresize auto-save" } do |form| %> @@ -19,7 +26,7 @@ <% end %> - <%= form.rich_textarea :description, class: "card-field__description rich-text-content", + <%= form.rich_textarea :description, class: "card__description rich-text-content", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "lexxy:change->auto-save#change focusout->auto-save#submit" } do %> <%= general_prompts(card.board) %> diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb index a2317ce08..1c898c30d 100644 --- a/app/views/cards/container/footer/_draft.html.erb +++ b/app/views/cards/container/footer/_draft.html.erb @@ -1,7 +1,13 @@
      - <%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn", - form: { data: { controller: "form", action: "keydown.ctrl+enter@document->form#submit:prevent keydown.meta+enter@document->form#submit:prevent" } } %> + <%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn", + form: { data: { controller: "form" } }, + data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %> + Create card + <% end %> - <%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another", - title: "Create and add another (ctrl+shift+enter)", form: { data: { turbo: false } }, data: { controller: "hotkey", action: "keydown.ctrl+shift+enter@document->hotkey#click" } %> + <%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another", + title: "Create and add another (ctrl+shift+enter)", form: { data: { controller: "form" } }, + data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %> + Create and add another + <% end %>
      diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index c07ec9e6d..d4455dcbe 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -1,16 +1,16 @@ -
      - <% card.assignees.each do |assignee| %> - <%= avatar_tag assignee, tabindex: (local_assigns.key?(:preview) && local_assigns[:preview]) ? -1 : 0 %> - <% end %> +
      > + + + - - <%= yield %> - -
      + + <%= yield %> +
      diff --git a/app/views/cards/display/perma/_assignees.html.erb b/app/views/cards/display/perma/_assignees.html.erb index a115f39b2..1292573d4 100644 --- a/app/views/cards/display/perma/_assignees.html.erb +++ b/app/views/cards/display/perma/_assignees.html.erb @@ -1,3 +1,3 @@ <%= render "cards/display/common/assignees", card: card do %> - <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %> + <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), loading: :lazy, refresh: "morph" %> <% end %> diff --git a/app/views/cards/display/perma/_board.html.erb b/app/views/cards/display/perma/_board.html.erb index f816683b9..e78a33579 100644 --- a/app/views/cards/display/perma/_board.html.erb +++ b/app/views/cards/display/perma/_board.html.erb @@ -6,7 +6,7 @@ - <%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", refresh: "morph" %> + <%= turbo_frame_tag "board_picker", src: edit_card_board_path(card), target: "_top", loading: :lazy, refresh: "morph" %>
      <% end %> diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index 015f7990a..0a5bbb1bf 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -1,12 +1,16 @@
      -
      > - - - <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %> + + <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), loading: :lazy, refresh: :morph %>
      diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 1a1eda843..58b2d7bde 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -9,13 +9,16 @@ <% end %> - <%= form.rich_textarea :description, class: "card-field__description rich-text-content", + <%= form.rich_textarea :description, class: "card__description rich-text-content", placeholder: "Add some notes, context, pictures, or video about this…", data: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do %> <%= general_prompts(@card.board) %> <% end %> - <%= form.button "Save changes", type: :submit, class: "btn btn--reversed fit-content", style: "--btn-background: #{@card.color}" %> + <%= form.button type: :submit, class: "btn btn--reversed fit-content txt-small", data: { form_target: "submit" } do %> + Save changes + <%= hotkey_label([ "ctrl", "enter" ]) %> + <% end %> <%= link_to "Close editor and discard changes", @card, data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> diff --git a/app/views/cards/pins/show.html.erb b/app/views/cards/pins/show.html.erb index c8ac69d7e..37f06232a 100644 --- a/app/views/cards/pins/show.html.erb +++ b/app/views/cards/pins/show.html.erb @@ -1,3 +1,3 @@ -<%= turbo_frame_tag dom_id(@card, :pin) do %> +<%= turbo_frame_tag @card, :pin do %> <%= render "cards/pins/pin_button", card: @card %> <% end %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index b96a3a7cb..596006ce8 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -11,6 +11,7 @@
      <% end %> +<%= turbo_stream_from @card %> <%= turbo_stream_from @card, :activity %>
      diff --git a/app/views/cards/taggings/new.html.erb b/app/views/cards/taggings/new.html.erb index d629cfb88..e348f531f 100644 --- a/app/views/cards/taggings/new.html.erb +++ b/app/views/cards/taggings/new.html.erb @@ -5,24 +5,19 @@ data-navigable-list-actionable-items-value="true" data-action="keydown->navigable-list#navigate filter:changed->navigable-list#reset dialog:show@document->navigable-list#reset" > - Tag this… +
      + Tag this…t +
      <%= form_with url: card_taggings_path(@card), id: dom_id(@card, :tags_form), data: { controller: "form", action: "submit->form#preventEmptySubmit" }, class: "flex flex-column gap-half full-width margin-block-half" do |form| %> <%= form.text_field :tag_title, placeholder: @tags.any? ? "Add a new tag or filter…" : "Name this tag…", class: "input txt-small full-width", - autocomplete: "off", data: { filter_target: "input", form_target: "input", action: "input->filter#filter" } %> + autocomplete: "off", autofocus: true, data: { filter_target: "input", form_target: "input", action: "input->filter#filter" } %> <% end %>
      <% end %> diff --git a/app/views/cards/triage/_columns.html.erb b/app/views/cards/triage/_columns.html.erb index 591a8e256..5a92a0126 100644 --- a/app/views/cards/triage/_columns.html.erb +++ b/app/views/cards/triage/_columns.html.erb @@ -1 +1 @@ -<%= turbo_frame_tag dom_id(card, :columns), src: edit_card_column_path(card), target: "_top", refresh: "morph" %> +<%= turbo_frame_tag card, :columns, src: edit_card_column_path(card), target: "_top", refresh: "morph" %> diff --git a/app/views/cards/watches/show.html.erb b/app/views/cards/watches/show.html.erb index 44892190d..5c62559bb 100644 --- a/app/views/cards/watches/show.html.erb +++ b/app/views/cards/watches/show.html.erb @@ -1,3 +1,3 @@ -<%= turbo_frame_tag dom_id(@card, :watch) do %> +<%= turbo_frame_tag @card, :watch do %> <%= render "cards/watches/watch_button", card: @card %> <% end %> diff --git a/app/views/entropy/_auto_close.html.erb b/app/views/entropy/_auto_close.html.erb index 73655d0f6..6ccb5d205 100644 --- a/app/views/entropy/_auto_close.html.erb +++ b/app/views/entropy/_auto_close.html.erb @@ -1,4 +1,5 @@ <% url = local_assigns[:url] %> +<% disabled = local_assigns[:disabled] %>
      @@ -7,7 +8,8 @@ form: form, name: :auto_postpone_period, knob_options: entropy_auto_close_options, - label: "Days until auto-close" %> + label: "Days until auto-close", + disabled: disabled %> <% end %>
      diff --git a/app/views/entropy/_knob.html.erb b/app/views/entropy/_knob.html.erb index bfe92ed71..69bbb5fb0 100644 --- a/app/views/entropy/_knob.html.erb +++ b/app/views/entropy/_knob.html.erb @@ -13,7 +13,8 @@ action: "change->knob#optionChanged change->form#submit", index: index, knob_target: "option" - } %> + }, + disabled: disabled %> <%= value %> <% end %> @@ -23,7 +24,8 @@ data: { action: "input->knob#sliderChanged change->form#submit", knob_target: "slider" }, "aria-hidden": true, max: knob_options.length - 1, - min: 0 + min: 0, + disabled: disabled %>
      diff --git a/app/views/events/_day_timeline.html.erb b/app/views/events/_day_timeline.html.erb deleted file mode 100644 index 244414d2a..000000000 --- a/app/views/events/_day_timeline.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% day_timeline.events.group_by { it.created_at.hour }.each do |hour, events_grouped_by_hour| %> - <% events_grouped_by_hour.group_by { event_column(it) }.each do |column, events| %> - <%= event_cluster_tag(hour, column) do %> - <%= render partial: "events/event", collection: events %> - <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> - <% end %> - <% end %> -<% end %> diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index aefe9258e..6e036a412 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -1,17 +1,15 @@ -<% column = event_columns(event_type, day_timeline) %> -
      -

      <%= column[:title] %>

      - <% column[:events].group_by { it.created_at.hour }.each do |hour, events| %> - <%= event_cluster_tag(hour, column[:index]) do %> +

      <%= column.title %>

      + <% column.events_by_hour.each do |hour, events| %> + <%= events_at_hour_container(column, hour) do %> <%= render partial: "events/event", collection: events, cached: true %> <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> <% end %> <% end %> - <% if column[:events].count < column[:full_events_count] %> - diff --git a/app/views/events/day_timeline/_columns.html.erb b/app/views/events/day_timeline/_columns.html.erb index 5391736e9..580c7312c 100644 --- a/app/views/events/day_timeline/_columns.html.erb +++ b/app/views/events/day_timeline/_columns.html.erb @@ -1,9 +1,9 @@ <% cache [ day_timeline.events ] do %> <% if day_timeline.has_activity? %>
      - <%= render "events/day_timeline/column", event_type: "added", day_timeline: day_timeline %> - <%= render "events/day_timeline/column", event_type: "updated", day_timeline: day_timeline %> - <%= render "events/day_timeline/column", event_type: "closed", day_timeline: day_timeline %> + <%= render "events/day_timeline/column", column: day_timeline.added_column %> + <%= render "events/day_timeline/column", column: day_timeline.updated_column %> + <%= render "events/day_timeline/column", column: day_timeline.closed_column %>
      <% end %> <% end %> diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb index 0fc767862..08c406002 100644 --- a/app/views/events/event/_layout.html.erb +++ b/app/views/events/event/_layout.html.erb @@ -18,7 +18,7 @@ - <% if event_column(event) == 2 %> + <% unless event.action.in?(%w[ card_closed card_published card_reopened ]) %> <%= icon_tag event_action_icon(event) %> @@ -32,7 +32,7 @@
      - <%= event_action_sentence(event) %> + <%= event.description_for(Current.user).to_html %> <%= yield %> diff --git a/app/views/events/index/_add_card_button.html.erb b/app/views/events/index/_add_card_button.html.erb index c22841cf5..028407b79 100644 --- a/app/views/events/index/_add_card_button.html.erb +++ b/app/views/events/index/_add_card_button.html.erb @@ -2,8 +2,16 @@ <% if board = user_filtering.single_board_or_first %> <%= button_to board_cards_path(board), method: :post, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %> <%= icon_tag "add", class: "show-on-touch" %> - Add a card + Add a card C <% end %> <% end %>
      + +
      + <%= link_to new_board_path, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.b@document->hotkey#click" } do %> + <%= icon_tag "board", class: "show-on-touch" %> + Add a board + B + <% end %> +
      diff --git a/app/views/filters/_filter_toggle.html.erb b/app/views/filters/_filter_toggle.html.erb index 76f599384..09e65c583 100644 --- a/app/views/filters/_filter_toggle.html.erb +++ b/app/views/filters/_filter_toggle.html.erb @@ -1,13 +1,13 @@
      <% if filter.persisted? %> - <%= button_to filter_path(filter), method: :delete, class: "btn txt-x-small btn--reversed", form_class: "inline" do %> + <%= button_to filter_path(filter), method: :delete, class: "btn txt-x-small btn--reversed", data: { controller: "tooltip" }, form_class: "inline" do %> <%= icon_tag "bookmark-outline" %> - Remove saved filter + Delete custom view <% end %> <% else %> - <%= button_to filters_path(filter.as_params), method: :post, class: "btn txt-x-small", form_class: "inline" do %> + <%= button_to filters_path(filter.as_params), method: :post, class: "btn txt-x-small", data: { controller: "tooltip" }, form_class: "inline" do %> <%= icon_tag "bookmark-outline" %> - Save filter + Save custom view <% end %> <% end %>
      diff --git a/app/views/filters/_settings.html.erb b/app/views/filters/_settings.html.erb index ae604011c..52137b5de 100644 --- a/app/views/filters/_settings.html.erb +++ b/app/views/filters/_settings.html.erb @@ -1,7 +1,7 @@ <%= tag.aside \ class: class_names("filters margin-block-end", { "filters--expanded": user_filtering.expanded? }), data: { - controller: "toggle-enable toggle-class filter-settings", + controller: "toggle-enable toggle-class filter-settings dialog-manager", toggle_class_toggle_class: "filters--expanded", filter_settings_filters_set_class: "filters--has-filters-set", filter_settings_no_filtering_url_value: no_filtering_url, diff --git a/app/views/filters/create.turbo_stream.erb b/app/views/filters/create.turbo_stream.erb index 6bcd97ee4..c0fc91e7c 100644 --- a/app/views/filters/create.turbo_stream.erb +++ b/app/views/filters/create.turbo_stream.erb @@ -1,2 +1 @@ <%= turbo_stream.replace("filter-settings-save-toggle", partial: "filters/filter_toggle", locals: { filter: @filter }) %> -<%= turbo_stream.replace("my-filters", partial: "my/menus/saved_filter", locals: { filters: @filters }) %> diff --git a/app/views/filters/destroy.turbo_stream.erb b/app/views/filters/destroy.turbo_stream.erb index 6bcd97ee4..c0fc91e7c 100644 --- a/app/views/filters/destroy.turbo_stream.erb +++ b/app/views/filters/destroy.turbo_stream.erb @@ -1,2 +1 @@ <%= turbo_stream.replace("filter-settings-save-toggle", partial: "filters/filter_toggle", locals: { filter: @filter }) %> -<%= turbo_stream.replace("my-filters", partial: "my/menus/saved_filter", locals: { filters: @filters }) %> diff --git a/app/views/filters/settings/_assignees.html.erb b/app/views/filters/settings/_assignees.html.erb index 996116d63..0a60a4878 100644 --- a/app/views/filters/settings/_assignees.html.erb +++ b/app/views/filters/settings/_assignees.html.erb @@ -1,5 +1,5 @@ <% filter = user_filtering.filter %> -<%= tag.div class: "quick-filter position-relative", +<%= tag.div class: "quick-filter", data: { controller: "dialog multi-selection-combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", diff --git a/app/views/filters/settings/_boards.html.erb b/app/views/filters/settings/_boards.html.erb index 5caf96d65..97a21618d 100644 --- a/app/views/filters/settings/_boards.html.erb +++ b/app/views/filters/settings/_boards.html.erb @@ -1,6 +1,6 @@ <% filter = user_filtering.filter %> -<%= tag.div class: "quick-filter position-relative", +<%= tag.div class: "quick-filter", data: { controller: "dialog multi-selection-combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", diff --git a/app/views/filters/settings/_closers.html.erb b/app/views/filters/settings/_closers.html.erb index 5da789245..24e542b1e 100644 --- a/app/views/filters/settings/_closers.html.erb +++ b/app/views/filters/settings/_closers.html.erb @@ -1,6 +1,6 @@ <% filter = user_filtering.filter %> -<%= tag.div class: "quick-filter position-relative", +<%= tag.div class: "quick-filter", data: { controller: "dialog multi-selection-combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", diff --git a/app/views/filters/settings/_creators.html.erb b/app/views/filters/settings/_creators.html.erb index 92385f7a2..8453fe745 100644 --- a/app/views/filters/settings/_creators.html.erb +++ b/app/views/filters/settings/_creators.html.erb @@ -1,6 +1,6 @@ <% filter = user_filtering.filter %> -<%= tag.div class: "quick-filter position-relative", +<%= tag.div class: "quick-filter", data: { controller: "dialog multi-selection-combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", diff --git a/app/views/filters/settings/_indexed_by.html.erb b/app/views/filters/settings/_indexed_by.html.erb index 48bf476da..8175fea9e 100644 --- a/app/views/filters/settings/_indexed_by.html.erb +++ b/app/views/filters/settings/_indexed_by.html.erb @@ -1,6 +1,6 @@ <% filter = user_filtering.filter %> -<%= tag.div class: class_names("quick-filter position-relative"), +<%= tag.div class: class_names("quick-filter"), data: { controller: "dialog combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", filter_show: user_filtering.show_indexed_by?, diff --git a/app/views/filters/settings/_manage.html.erb b/app/views/filters/settings/_manage.html.erb index 8a13490ad..333e9973e 100644 --- a/app/views/filters/settings/_manage.html.erb +++ b/app/views/filters/settings/_manage.html.erb @@ -3,7 +3,7 @@
      <%= render "filters/filter_toggle", filter: filter %> - <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click"} do %> + <%= link_to no_filtering_url, class: "btn btn--remove txt-x-small", data: { controller: "hotkey tooltip", action: "keydown.esc@document->hotkey#click"} do %> <%= icon_tag "close" %> Clear all <% end %> diff --git a/app/views/filters/settings/_sorted_by.html.erb b/app/views/filters/settings/_sorted_by.html.erb index 5379cfd17..e5c0adc4e 100644 --- a/app/views/filters/settings/_sorted_by.html.erb +++ b/app/views/filters/settings/_sorted_by.html.erb @@ -1,6 +1,6 @@ <% filter = user_filtering.filter %> -<%= tag.div class: class_names("quick-filter position-relative"), +<%= tag.div class: class_names("quick-filter"), data: { controller: "dialog combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", diff --git a/app/views/filters/settings/_time_window.html.erb b/app/views/filters/settings/_time_window.html.erb index cfc827ecc..b747384ac 100644 --- a/app/views/filters/settings/_time_window.html.erb +++ b/app/views/filters/settings/_time_window.html.erb @@ -1,5 +1,5 @@ <% filter = user_filtering.filter %> -<%= tag.div class: "quick-filter position-relative", +<%= tag.div class: "quick-filter", data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", filter_show: filter.public_send("#{name}_window").present? } do %> <% else %> - <% end %> diff --git a/app/views/join_codes/new.html.erb b/app/views/join_codes/new.html.erb index 5974c16ba..c4d933b8b 100644 --- a/app/views/join_codes/new.html.erb +++ b/app/views/join_codes/new.html.erb @@ -1,12 +1,7 @@ -<% @page_title = "Join #{@account_name}" %> +<% @page_title = "Join #{@account_name} in Fizzy" %>
      -
      -

      - <%= @page_title %> -

      -

      Enter your email address to continue

      -
      +

      <%= @page_title %>

      <%= form_with url: join_path(code: params[:code], tenant: params[:tenant]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
      @@ -16,7 +11,7 @@
      <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 31a162329..d2af3e959 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,7 +5,7 @@ diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 5e3c6e69f..cd7502ec1 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -43,27 +43,29 @@ width: 100% !important; } - .avatar { - border-radius: 50%; - height: 2.75em; - margin-right: 0.2.75em; - max-height: 2.75em; - min-height: 2.75em; - overflow: hidden; - width: 2.75em; - } - .avatar__container { - padding-top: 1em; vertical-align: top; width: 3.25em; } + .avatar { + border-radius: 50%; + color: white; + display: block; + font-weight: 600; + height: 2.75em; + line-height: 2.75em; + mso-line-height-rule: exactly; + overflow: hidden; + text-align: center; + width: 2.75em; + } + .btn { background: #2d71e5; border-color: #2d71e5; border-radius: 3rem; - color: #ffffff; + color: white !important; font-weight: 500; padding: 0.5em 1em; text-decoration: none; diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index c217f44bb..2f1e2be3d 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -1,16 +1,18 @@ - + <%= render "layouts/shared/head" %>
      diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb index 997bc48db..fd4b7cab7 100644 --- a/app/views/layouts/shared/_head.html.erb +++ b/app/views/layouts/shared/_head.html.erb @@ -1,8 +1,7 @@ <%= page_title_tag %> - - + <% unless @disable_view_transition %> <% end %> diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb index 438dc18e9..e07fdedf1 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb @@ -1,9 +1,8 @@ -

      Confirm your email address change

      -

      Hit the button below to use this email address in Fizzy.

      +

      Confirm your email address change

      <%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>

      If you didn’t request this change, you can ignore this email. Your email address WILL NOT be changed unless you hit the button.

      - diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb index b966ecfa6..8ba613348 100644 --- a/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb +++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb @@ -1,12 +1,14 @@ -

      Sign in to Fizzy

      -

      Hit the button below to sign in to Fizzy on this device

      -<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %> +<% if @identity.memberships.any? %> +

      Fizzy verification code

      +

      Please enter this 6-character verification code on the Fizzy sign-in page:

      +<% else %> +

      Welcome to Fizzy!

      +

      Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:

      +<% end %> -

      If you’re signing in on another device, enter this special code: -
      <%= @magic_link.code %> -

      - - diff --git a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb index f44e1c908..2de3e5533 100644 --- a/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb +++ b/app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb @@ -1,8 +1,7 @@ -Sign in to Fizzy -<%= "=" * 80 %> +<% if @identity.memberships.any? %> +Please enter this 6-character verification code on the Fizzy sign-in page: +<% else %> +Please enter this 6-character verification code on the Fizzy sign-up page to create your new account: +<% end %> -Open this link in your browser to login on this device: -<%= session_magic_link_url(code: @magic_link.code) %> - -If you're signing in on another device, enter this code: <%= @magic_link.code %> diff --git a/app/views/mailers/notification/bundle_mailer/_notification.html.erb b/app/views/mailers/notification/bundle_mailer/_notification.html.erb index 5dfddcf31..543f1515c 100644 --- a/app/views/mailers/notification/bundle_mailer/_notification.html.erb +++ b/app/views/mailers/notification/bundle_mailer/_notification.html.erb @@ -1,8 +1,15 @@ - + + + + + diff --git a/app/views/memberships/email_addresses/create.html.erb b/app/views/memberships/email_addresses/create.html.erb index 53851105d..44711b5cf 100644 --- a/app/views/memberships/email_addresses/create.html.erb +++ b/app/views/memberships/email_addresses/create.html.erb @@ -1,14 +1,17 @@ <% @page_title = "Confirm your new email address" %> <% content_for :header do %> -
      - <%= link_to edit_user_path(@membership.user, script_name: "/#{@tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - My profile +
      + <%= link_to edit_user_path(@membership.user, script_name: "/#{@tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to My profile + + <% end %>
      <% end %> -
      +

      Check your email

      We just sent an email to <%= params[:email_address] %>

      Hit the link in the email to confirm this is the email address you want to use with Fizzy going forward.

      diff --git a/app/views/memberships/email_addresses/new.html.erb b/app/views/memberships/email_addresses/new.html.erb index f88456679..4b493d667 100644 --- a/app/views/memberships/email_addresses/new.html.erb +++ b/app/views/memberships/email_addresses/new.html.erb @@ -1,30 +1,32 @@ <% @page_title = "Change your email" %> <% content_for :header do %> -
      - <%= link_to edit_user_path(@membership.user, script_name: "/#{@membership.tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - My profile +
      + <%= link_to edit_user_path(@membership.user, script_name: "/#{@membership.tenant}"), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to My profile + + <% end %>
      <% end %> -
      -
      -

      - <%= @page_title %> -

      -

      Enter your new email address. We'll send you a confirmation to verify it.

      -
      +
      +

      + <%= @page_title %> +

      - <%= form_with url: email_addresses_path(membership_id: @membership.id), method: :post, class: "flex flex-column gap", data: { controller: "form", turbo: false } do |form| %> -
      + <%= form_with url: email_addresses_path(membership_id: @membership.id), method: :post, class: "flex flex-column gap-half", data: { controller: "form", turbo: false } do |form| %> +
      - <% end %> diff --git a/app/views/my/_menu.html.erb b/app/views/my/_menu.html.erb new file mode 100644 index 000000000..48570e1ff --- /dev/null +++ b/app/views/my/_menu.html.erb @@ -0,0 +1,21 @@ + diff --git a/app/views/my/menus/_accounts.html.erb b/app/views/my/menus/_accounts.html.erb deleted file mode 100644 index e8c850907..000000000 --- a/app/views/my/menus/_accounts.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= turbo_frame_tag dom_id(Current.identity, :account_menu), src: session_menu_url(script_name: nil, menu_section: true, without: ApplicationRecord.current_tenant) do %> - Loading... -<% end %> diff --git a/app/views/my/menus/_boards.html.erb b/app/views/my/menus/_boards.html.erb deleted file mode 100644 index c881d1f88..000000000 --- a/app/views/my/menus/_boards.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<% # Using method: :get inside Turbo frame triggers some special Turbo handling where the script_name is lost %> -<%= collapsible_nav_section "Boards" do %> - - <% if user_filtering.boards.any? %> - <%= render partial: "my/menus/boards/board", collection: user_filtering.boards, as: :board %> - <% end %> -<% end %> diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb deleted file mode 100644 index 85b2c9a93..000000000 --- a/app/views/my/menus/_button.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline align-center txt-normal", - data: { - action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click", - controller: "hotkey" } do %> - <%= image_tag "logo.png" %> - Fizzy - J -<% end %> diff --git a/app/views/my/menus/_footer.html.erb b/app/views/my/menus/_footer.html.erb deleted file mode 100644 index 9bcea3d54..000000000 --- a/app/views/my/menus/_footer.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
      - Fizzy™ is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals™ -
      diff --git a/app/views/my/menus/_hotkeys.html.erb b/app/views/my/menus/_hotkeys.html.erb deleted file mode 100644 index 198f766f9..000000000 --- a/app/views/my/menus/_hotkeys.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -
      - <%= filter_hotkey_link "Home", root_path, 1, "home" %> - <%= filter_hotkey_link "New board", new_board_path, 2, "board-add" %> - <%= filter_hotkey_link "Assigned to me", cards_path(assignee_ids: [Current.user.id]), 3, "clipboard" %> - <%= filter_hotkey_link "Added by me", cards_path(creator_ids: [Current.user.id]), 4, "person-add" %> - <%= filter_hotkey_link "Notifications", notifications_path, 5, "bell" %> - <%= filter_hotkey_link "Settings", account_settings_path, 6, "settings" %> -
      diff --git a/app/views/my/menus/_jump.html.erb b/app/views/my/menus/_jump.html.erb new file mode 100644 index 000000000..281f45c90 --- /dev/null +++ b/app/views/my/menus/_jump.html.erb @@ -0,0 +1,23 @@ +
      +
      + <% if Current.user.identity.memberships.many? %> + <%= Account.sole.name %> + <% end %> + <%= jump_field_tag %> +
      + + +
      + + diff --git a/app/views/my/menus/_menu.html.erb b/app/views/my/menus/_menu.html.erb deleted file mode 100644 index 26b0c291c..000000000 --- a/app/views/my/menus/_menu.html.erb +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/app/views/my/menus/_places.html.erb b/app/views/my/menus/_places.html.erb deleted file mode 100644 index 2d0213f89..000000000 --- a/app/views/my/menus/_places.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<%= collapsible_nav_section "Jump to…" do %> - <%= filter_place_menu_item account_settings_path, "Account Settings", "settings" %> - <%= filter_place_menu_item user_path(Current.user), "My Profile", "person" %> - <%= filter_place_menu_item notifications_path, "Notifications", "bell" %> - <%= filter_place_menu_item notifications_settings_path, "Notification Settings", "settings" %> - - <%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %> - <%= icon_tag "logout", class: "popup__icon" %> - <%= button_to session_url(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %> - Sign out - <% end %> - <% end %> -<% end %> diff --git a/app/views/my/menus/_saved_filter.html.erb b/app/views/my/menus/_saved_filter.html.erb deleted file mode 100644 index 46dbf2f56..000000000 --- a/app/views/my/menus/_saved_filter.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= collapsible_nav_section "Saved filters", id: "my-filters" do %> - <%= form_with url: cards_path, method: :get, data: { controller: "form" } do |form| %> - <%= render "my/menus/saved_filter/create" %> - <%= render partial: "my/menus/saved_filter/filter", collection: filters, as: :filter %> - <% end %> -<% end %> diff --git a/app/views/my/menus/_tags.html.erb b/app/views/my/menus/_tags.html.erb deleted file mode 100644 index 9f2d27cf6..000000000 --- a/app/views/my/menus/_tags.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= collapsible_nav_section "Tags" do %> - <%= render partial: "my/menus/tags/tag", collection: user_filtering.tags, as: :tag %> -<% end %> diff --git a/app/views/my/menus/_users.html.erb b/app/views/my/menus/_users.html.erb deleted file mode 100644 index b8ca2cb59..000000000 --- a/app/views/my/menus/_users.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%= collapsible_nav_section "People" do %> - - - <%= render partial: "my/menus/users/user", collection: user_filtering.users, as: :user %> -<% end %> diff --git a/app/views/my/menus/boards/_board.html.erb b/app/views/my/menus/boards/_board.html.erb deleted file mode 100644 index 365028a55..000000000 --- a/app/views/my/menus/boards/_board.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/views/my/menus/saved_filter/_create.html.erb b/app/views/my/menus/saved_filter/_create.html.erb deleted file mode 100644 index e10d5313f..000000000 --- a/app/views/my/menus/saved_filter/_create.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/views/my/menus/saved_filter/_filter.html.erb b/app/views/my/menus/saved_filter/_filter.html.erb deleted file mode 100644 index 6dd20bf9c..000000000 --- a/app/views/my/menus/saved_filter/_filter.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index 924dbf485..94e14a642 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -1,19 +1,70 @@ <%= turbo_frame_tag "my_menu", target: "_top" do %> -
      - <%= jump_field_tag %> + <%= render "my/menus/jump" do %> + <%= collapsible_nav_section "Boards" do %> + - -
      + <% @boards.each do |board| %> + <%= my_menu_board_item(board) %> + <% end %> + <% end %> - <%= render "hotkeys" %> - <%= render "saved_filter", filters: @user_filtering.filters %> - <%= render "boards", user_filtering: @user_filtering %> - <%= render "tags", user_filtering: @user_filtering %> - <%= render "users", user_filtering: @user_filtering %> - <%= render "places", user_filtering: @user_filtering %> - <%= render "accounts", user_filtering: @user_filtering %> - <%= render "footer" %> + <%= collapsible_nav_section "Custom views", id: "my-filters" do %> + <%= form_with url: cards_path, method: :get, data: { controller: "form" } do |form| %> + + + <% @filters.each do |filter| %> + <%= my_menu_filter_item(filter) %> + <% end %> + <% end %> + <% end %> + + <%= collapsible_nav_section "Tags" do %> + <% @tags.each do |tag| %> + <%= my_menu_tag_item(tag) %> + <% end %> + <% end %> + + <%= collapsible_nav_section "People" do %> + + + <% @users.each do |user| %> + <%= my_menu_user_item(user) %> + <% end %> + <% end %> + + <%= collapsible_nav_section "Settings" do %> + <%= filter_place_menu_item account_settings_path, "Account Settings", "settings" %> + <%= filter_place_menu_item user_path(Current.user), "My Profile", "person" %> + <%= filter_place_menu_item notifications_path, "All notifications", "bell" %> + <%= filter_place_menu_item notifications_settings_path, "Notification Settings", "settings" %> + + <%= tag.li class: "popup__item", data: { filter_target: "item", navigable_list_target: "item" } do %> + <%= icon_tag "logout", class: "popup__icon" %> + <%= button_to session_url(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %> + Sign out + <% end %> + <% end %> + <% end %> + + <%= turbo_frame_tag Current.identity, :account_menu, src: session_menu_url(script_name: nil, menu_section: true, without: ApplicationRecord.current_tenant) %> + <% end %> + + <% end %> diff --git a/app/views/my/menus/tags/_tag.html.erb b/app/views/my/menus/tags/_tag.html.erb deleted file mode 100644 index 3f502ba6b..000000000 --- a/app/views/my/menus/tags/_tag.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/views/my/menus/users/_user.html.erb b/app/views/my/menus/users/_user.html.erb deleted file mode 100644 index e4e3ea9da..000000000 --- a/app/views/my/menus/users/_user.html.erb +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/app/views/notifications/settings/_board.html.erb b/app/views/notifications/settings/_board.html.erb index fe6d5b7d4..a5f485752 100644 --- a/app/views/notifications/settings/_board.html.erb +++ b/app/views/notifications/settings/_board.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag dom_id(board, :involvement) do %> +<%= turbo_frame_tag board, :involvement do %>

      <%= board.name %> diff --git a/app/views/public/_footer.html.erb b/app/views/public/_footer.html.erb new file mode 100644 index 000000000..a9b5d1853 --- /dev/null +++ b/app/views/public/_footer.html.erb @@ -0,0 +1,3 @@ +
      + <%= icon_tag "fizzy", class: "v-align-middle" %> Fizzy™ is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. +
      diff --git a/app/views/public/boards/columns/closeds/show.html.erb b/app/views/public/boards/columns/closeds/show.html.erb index 02e8aa46c..b3212aa81 100644 --- a/app/views/public/boards/columns/closeds/show.html.erb +++ b/app/views/public/boards/columns/closeds/show.html.erb @@ -1,11 +1,34 @@ -<%= turbo_frame_tag :closed_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :closed_column, @page do %> - <%= render "public/boards/columns/list", cards: @page.records %> +<% @page_title = "Column: Closed" %> + +<% content_for :header do %> +
      + <%= link_to published_board_url(@board), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @board.name %> + + <% end %> - <% else %> -
      -

      No cards here

      -
      - <% end %> +
      + +

      + <%= @page_title %> +

      +<% end %> + +
      + <%= turbo_frame_tag :closed_column do %> + <% if @page.used? %> + <%= with_automatic_pagination :closed_column, @page do %> + <%= render "public/boards/columns/list", cards: @page.records %> + <% end %> + <% else %> +
      +

      No cards here

      +
      + <% end %> + <% end %> +
      + +<% content_for :footer do %> + <%= render "public/footer" %> <% end %> diff --git a/app/views/public/boards/columns/not_nows/show.html.erb b/app/views/public/boards/columns/not_nows/show.html.erb index 8f818cfe0..d6ceb0da5 100644 --- a/app/views/public/boards/columns/not_nows/show.html.erb +++ b/app/views/public/boards/columns/not_nows/show.html.erb @@ -1,11 +1,34 @@ -<%= turbo_frame_tag :not_now_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :not_now_column, @page do %> - <%= render "public/boards/columns/list", cards: @page.records %> +<% @page_title = "Column: Not now" %> + +<% content_for :header do %> +
      + <%= link_to published_board_url(@board), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @board.name %> + + <% end %> - <% else %> -
      -

      No cards here

      -
      - <% end %> +
      + +

      + <%= @page_title %> +

      +<% end %> + +
      + <%= turbo_frame_tag :not_now_column do %> + <% if @page.used? %> + <%= with_automatic_pagination :not_now_column, @page do %> + <%= render "public/boards/columns/list", cards: @page.records %> + <% end %> + <% else %> +
      +

      No cards here

      +
      + <% end %> + <% end %> +
      + +<% content_for :footer do %> + <%= render "public/footer" %> <% end %> diff --git a/app/views/public/boards/columns/show.html.erb b/app/views/public/boards/columns/show.html.erb index 2ebd44e2d..e23e4e370 100644 --- a/app/views/public/boards/columns/show.html.erb +++ b/app/views/public/boards/columns/show.html.erb @@ -1,11 +1,35 @@ -<%= turbo_frame_tag dom_id(@column, :cards) do %> - <% if @page.used? %> - <%= with_automatic_pagination dom_id(@column, :cards), @page do %> - <%= render "public/boards/columns/list", cards: @page.records %> +<% @page_title = "Column: #{ @column.name }" %> + +<% content_for :header do %> +
      + <%= link_to published_board_url(@column.board), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @column.board.name %> + + <% end %> - <% else %> -
      -

      No cards here

      -
      - <% end %> +
      + +

      + <%= @page_title %> +

      <% end %> + +
      + <%= turbo_frame_tag @column, :cards do %> + <% if @page.used? %> + <%= with_automatic_pagination dom_id(@column, :cards), @page do %> + <%= render "public/boards/columns/list", cards: @page.records %> + <% end %> + <% else %> +
      +

      No cards here

      +
      + <% end %> + <% end %> +
      + +<% content_for :footer do %> + <%= render "public/footer" %> +<% end %> + diff --git a/app/views/public/boards/columns/streams/show.html.erb b/app/views/public/boards/columns/streams/show.html.erb index f165d6250..0a75e1179 100644 --- a/app/views/public/boards/columns/streams/show.html.erb +++ b/app/views/public/boards/columns/streams/show.html.erb @@ -1,11 +1,34 @@ -<%= turbo_frame_tag :stream_column do %> - <% if @page.used? %> - <%= with_automatic_pagination :stream_column, @page do %> - <%= render "public/boards/columns/list", cards: @page.records %> +<% @page_title = "Column: Maybe?" %> + +<% content_for :header do %> +
      + <%= link_to published_board_url(@board), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @board.name %> + + <% end %> - <% else %> -
      -

      No cards here

      -
      - <% end %> +
      + +

      + <%= @page_title %> +

      <% end %> + +
      + <%= turbo_frame_tag :stream_column do %> + <% if @page.used? %> + <%= with_automatic_pagination :stream_column, @page do %> + <%= render "public/boards/columns/list", cards: @page.records %> + <% end %> + <% else %> +
      +

      No cards here

      +
      + <% end %> + <% end %> +
      + +<% content_for :footer do %> + <%= render "public/footer" %> +<% end %> \ No newline at end of file diff --git a/app/views/public/boards/show.html.erb b/app/views/public/boards/show.html.erb index 0436f00cb..f0e91cfd2 100644 --- a/app/views/public/boards/show.html.erb +++ b/app/views/public/boards/show.html.erb @@ -13,15 +13,19 @@ <% end %> <% content_for :header do %> -

      - <%= @page_title %> +

      + <%= @page_title %>

      <% end %> <% if @board.public_description.present? %> -
      +
      <%= @board.public_description %>
      <% end %> <%= render "public/boards/show/columns", page: @page, board: @board %> + +<% content_for :footer do %> + <%= render "public/footer" %> +<% end %> diff --git a/app/views/public/boards/show/_closed.html.erb b/app/views/public/boards/show/_closed.html.erb index 717f81324..9f1e1c930 100644 --- a/app/views/public/boards/show/_closed.html.erb +++ b/app/views/public/boards/show/_closed.html.erb @@ -3,7 +3,16 @@ data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle" > - <%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" %> +
      +
      + <%= render "boards/show/expander", title: "Done", count: board.cards.closed.count, column_id: "closed-cards" %> - <%= column_frame_tag :closed_column, src: public_board_columns_closed_path(board.publication.key) %> + <%= link_to public_board_columns_closed_url(board.publication.key), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Maximize column + <% end %> +
      + + <%= column_frame_tag :closed_column, src: public_board_columns_closed_path(board.publication.key) %> +
      diff --git a/app/views/public/boards/show/_column.html.erb b/app/views/public/boards/show/_column.html.erb index 99e59de0d..b95c49ebd 100644 --- a/app/views/public/boards/show/_column.html.erb +++ b/app/views/public/boards/show/_column.html.erb @@ -4,7 +4,16 @@ data-controller="clicker" data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle" > - <%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) %> +
      +
      + <%= render "boards/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) %> - <%= column_frame_tag dom_id(column, :cards), src: public_board_column_path(column.board.publication.key, column) %> + <%= link_to public_board_column_url(column.board.publication.key, column), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Maximize column + <% end %> +
      + + <%= column_frame_tag dom_id(column, :cards), src: public_board_column_path(column.board.publication.key, column) %> +
      diff --git a/app/views/public/boards/show/_considering.html.erb b/app/views/public/boards/show/_considering.html.erb deleted file mode 100644 index 057871543..000000000 --- a/app/views/public/boards/show/_considering.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -
      - <% if page.used? %> - <%= render "cards/display/public_previews", cards: page.records %> - - <% unless page.last? %> -
      - <%= public_board_cards_next_page_link board, "considering-cards", page: page %> -
      - <% end %> - <% else %> -

      Nothing here

      - <% end %> -
      diff --git a/app/views/public/boards/show/_doing.html.erb b/app/views/public/boards/show/_doing.html.erb deleted file mode 100644 index b6b3cdc8a..000000000 --- a/app/views/public/boards/show/_doing.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
      -

      - Working On -

      - - <% if page.used? %> - <%= render "cards/display/public_previews", cards: page.records %> - - <% unless page.last? %> -
      - <%= public_board_cards_next_page_link board, "doing-cards", page: page %> -
      - <% end %> - <% else %> -

      Nothing here

      - <% end %> -
      diff --git a/app/views/public/boards/show/_not_now.html.erb b/app/views/public/boards/show/_not_now.html.erb index dad3ca0e4..d66103117 100644 --- a/app/views/public/boards/show/_not_now.html.erb +++ b/app/views/public/boards/show/_not_now.html.erb @@ -3,7 +3,16 @@ data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle" > - <%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" %> +
      +
      + <%= render "boards/show/expander", title: "Not Now", count: board.cards.postponed.count, column_id: "not-now" %> - <%= column_frame_tag :not_now_column, src: public_board_columns_not_now_path(board.publication.key) %> + <%= link_to public_board_columns_not_now_url(board.publication.key), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Maximize column + <% end %> +
      + + <%= column_frame_tag :not_now_column, src: public_board_columns_not_now_path(board.publication.key) %> +
      diff --git a/app/views/public/boards/show/_on_deck.html.erb b/app/views/public/boards/show/_on_deck.html.erb deleted file mode 100644 index 539efa64c..000000000 --- a/app/views/public/boards/show/_on_deck.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
      -

      - On Deck -

      - - <% if page.used? %> - <%= render partial: "cards/display/public_preview", collection: page.records, as: :card, cached: true %> - - <% unless page.last? %> -
      - <%= public_board_cards_next_page_link board, "on-deck-cards", page: page %> -
      - <% end %> - <% else %> -

      Nothing here

      - <% end %> -
      diff --git a/app/views/public/boards/show/_stream.html.erb b/app/views/public/boards/show/_stream.html.erb index abdc0fc33..8fea976b6 100644 --- a/app/views/public/boards/show/_stream.html.erb +++ b/app/views/public/boards/show/_stream.html.erb @@ -1,12 +1,17 @@ -
      -
      -

      Maybe?

      -
      +
      +
      +
      +

      Maybe?

      +
      - <% if page.used? %> - <%= with_automatic_pagination "the-stream", @page do %> - <%= render "public/boards/columns/list", cards: page.records %> - <% end %> - <% end %> + <%= link_to public_board_columns_stream_url(board.publication.key), class: "cards__maximize-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= icon_tag "grid", class: "translucent" %> + Maximize column + <% end %> +
      + + <%= column_frame_tag :stream_column, src: public_board_columns_stream_path(board.publication.key) %> +
      diff --git a/app/views/public/cards/show.html.erb b/app/views/public/cards/show.html.erb index b1201b1f3..b6446ea7b 100644 --- a/app/views/public/cards/show.html.erb +++ b/app/views/public/cards/show.html.erb @@ -13,12 +13,14 @@ <% end %> <% content_for :header do %> - <%= link_to published_board_url(@card.board), class: "header__title btn borderless txt-large", style: "--btn-padding: 0.25ch 1ch 0.25ch 0.75ch; view-transistion-name: card-board-title;", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @card.board.name %> - - <% end %> +
      + <%= link_to published_board_url(@card.board), class: "btn borderless txt-medium", style: "view-transistion-name: card-board-title;", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @card.board.name %> + + + <% end %> +
      <% end %>
      + +<% content_for :footer do %> + <%= render "public/footer" %> +<% end %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index ff66cb7b3..8938626a1 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -1,5 +1,5 @@ { - "name": "<%= [ Account.sole.name, "Fizzy", Rails.env.production? ? nil : Rails.env ].compact.join(" - ") %>", + "name": <%= [ "Fizzy", Account.sole.name, Rails.env.production? ? nil : Rails.env ].compact.join(" - ").to_json.html_safe %>, "icons": [ { "src": "/app-icon-192.png", diff --git a/app/views/sessions/_footer.html.erb b/app/views/sessions/_footer.html.erb index 33d604824..e538532d7 100644 --- a/app/views/sessions/_footer.html.erb +++ b/app/views/sessions/_footer.html.erb @@ -1,3 +1,4 @@ -
      - Fizzy beta. <%= mail_to "support@37signals.com", "Need help?", class: "txt-link" %> +
      + <%= icon_tag "fizzy", class: "v-align-middle" %> Fizzy™ is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals™. + Need help? <%= mail_to "support@fizzy.do", "Send us an email", class: "txt-link" %>.
      \ No newline at end of file diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb index cf81f458d..e358f3284 100644 --- a/app/views/sessions/magic_links/show.html.erb +++ b/app/views/sessions/magic_links/show.html.erb @@ -3,19 +3,23 @@
      ">

      <%= @page_title %>

      -

      We just emailed you a link to sign in.

      +

      Then enter the verification code included in the email below:

      -

      If your email is on a different device, enter the code included in the email below.

      - <%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: token_list("form", "auto-submit" => params[:code].present?)} } do |form| %> - <%= form.text_field :code, required: true, class: "input center txt-align-enter txt-normal", - autofocus: false, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true, + <%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large", + autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true, autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code], data: { form_target: "input", action: "paste->form#debouncedSubmit" } %> <% end %>
      +<% if Rails.env.development? %> + +<% end %> + <% content_for :footer do %> <%= render "sessions/footer" %> <% end %> diff --git a/app/views/sessions/menus/show.html+menu_section.erb b/app/views/sessions/menus/show.html+menu_section.erb index 6a0a41346..ed72443cc 100644 --- a/app/views/sessions/menus/show.html+menu_section.erb +++ b/app/views/sessions/menus/show.html+menu_section.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag dom_id(Current.identity, :account_menu) do %> +<%= turbo_frame_tag Current.identity, :account_menu do %> <% cache [ Current.identity, @memberships ] do %> <%= collapsible_nav_section "Accounts" do %> <% @memberships.each do |membership| %> diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index 096ea7d5b..19f190eb3 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -24,7 +24,7 @@

      You don’t have any Fizzy accounts.

      <% end %> - <% if defined?(saas) %> + <% if signups_allowed? %>
      <%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %> Sign up for a new Fizzy account diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index be35c17b1..68f0c7e83 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -1,17 +1,19 @@ -<% @page_title = "Sign in" %> +<% @page_title = "Enter your email" %>
      -

      <%= @page_title %>

      +

      Let’s get you in to Fizzy

      - <%= form_with url: session_path, class: "flex flex-column gap txt-medium" do |form| %> + <%= form_with url: session_path, class: "flex flex-column gap-half txt-medium" do |form| %>
      - <% end %> diff --git a/app/views/users/_transfer.html.erb b/app/views/users/_transfer.html.erb index b260a152e..8befea9ce 100644 --- a/app/views/users/_transfer.html.erb +++ b/app/views/users/_transfer.html.erb @@ -12,18 +12,18 @@
      - <%= tag.button class: "btn", data: { action: "dialog#open" } do %> + <%= tag.button class: "btn", data: { action: "dialog#open", controller: "tooltip" } do %> <%= icon_tag "qr-code" %> - Show auto-login QR code + Display auto-login QR code <% end %> <%= qr_code_image(url) %>
      -
      diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 19736ac2d..c3e3aa09a 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -1,5 +1,16 @@ <% @page_title = "Edit your profile" %> +<% content_for :header do %> +
      + <%= link_to user_path(@user), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to profile + + + <% end %> +
      +<% end %> +
      <%= form_with model: @user, method: :patch, class: "flex flex-column gap", data: { controller: "form upload-preview" } do |form| %> diff --git a/app/views/users/joins/new.html.erb b/app/views/users/joins/new.html.erb index adf795ece..a5601b7a7 100644 --- a/app/views/users/joins/new.html.erb +++ b/app/views/users/joins/new.html.erb @@ -1,24 +1,17 @@ -<% @page_title = "Join #{Account.sole.name}" %> +<% @page_title = "Great! Now enter your name" %>
      -
      -

      - <%= @page_title %> -

      - -

      Enter your name to continue

      -
      +

      <%= @page_title %>

      <%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
      <% end %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index d5cbd8865..eec6444a0 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -5,9 +5,9 @@
      <% if Current.user == @user %> - <%= link_to edit_user_path(@user), class: "user-edit-link btn" do %> + <%= link_to edit_user_path(@user), class: "user-edit-link btn", data: { controller: "tooltip" } do %> <%= icon_tag "pencil" %> - Edit + Edit profile <% end %> <% end %> @@ -27,9 +27,9 @@
      - <%= link_to "Which cards are assigned to #{me_or_you}?", + <%= link_to "Which cards are assigned to #{me_or_you}?", cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> - <%= link_to "Which cards were added by #{me_or_you}?", + <%= link_to "Which cards were added by #{me_or_you}?", cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
      diff --git a/app/views/webhooks/edit.html.erb b/app/views/webhooks/edit.html.erb index adb7c27c8..26196e90c 100644 --- a/app/views/webhooks/edit.html.erb +++ b/app/views/webhooks/edit.html.erb @@ -2,10 +2,10 @@ <% content_for :header do %>
      - <%= link_to @webhook, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @page_title %> + <%= link_to @webhook, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to <%= @page_title %> + <% end %>
      diff --git a/app/views/webhooks/event.html.erb b/app/views/webhooks/event.html.erb index 91500a17b..f2920e8d0 100644 --- a/app/views/webhooks/event.html.erb +++ b/app/views/webhooks/event.html.erb @@ -1,4 +1,4 @@ -<%= event_action_sentence(@event) %> +<%= @event.description_for(Current.user).to_plain_text %> <% if @event.eventable %> <%= link_to "↗︎", polymorphic_url(@event.eventable) %> <% end %> diff --git a/app/views/webhooks/index.html.erb b/app/views/webhooks/index.html.erb index 778598381..0652eba7c 100644 --- a/app/views/webhooks/index.html.erb +++ b/app/views/webhooks/index.html.erb @@ -2,35 +2,24 @@ <% content_for :header do %>
      - <%= link_to @board, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - <%= @board.name %> - - <% end %> + <%= link_back_to_board(@board) %>

      <%= @page_title %>

      - -
      - <%= link_to new_board_webhook_path, class: "btn" do %> - <%= icon_tag "add" %> - Create a new webhook - <% end %> -
      <% end %> <%= tag.section class: "panel shadow center webhooks" do %> <% if @page.records.any? %> -
        +
          <%= render partial: "webhooks/webhook", collection: @page.records %>
        <% else %>

        Webhooks can notify another application when something happens in this Fizzy board. You’ll choose which events to subscribe to and provide a URL to receive the data.

        For example, you could create a webhook that posts to a Campfire chat in Basecamp when new cards are added to Fizzy.

        - <%= link_to new_board_webhook_path, class: "btn btn--link" do %> - <%= icon_tag "add" %> - Set up a webhook - <% end %> + <% end %> + + <%= link_to new_board_webhook_path, class: "btn btn--link" do %> + <%= icon_tag "add" %> + Set up a new webhook <% end %> <% end %> diff --git a/app/views/webhooks/new.html.erb b/app/views/webhooks/new.html.erb index 40178418f..c722ff9db 100644 --- a/app/views/webhooks/new.html.erb +++ b/app/views/webhooks/new.html.erb @@ -2,12 +2,7 @@ <% content_for :header do %>
        - <%= link_to board_webhooks_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - Webhooks - - <% end %> + <%= link_back_to_board(@board) %>

        <%= @page_title %>

        diff --git a/app/views/webhooks/show.html.erb b/app/views/webhooks/show.html.erb index 95f0a2f0e..4dbef81d3 100644 --- a/app/views/webhooks/show.html.erb +++ b/app/views/webhooks/show.html.erb @@ -2,10 +2,10 @@ <% content_for :header do %>
        - <%= link_to board_webhooks_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %> - - ← - Webhooks + <%= link_to board_webhooks_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.left@document->hotkey#click" } do %> + + Back to Webhooks + <% end %>
        diff --git a/config/application.rb b/config/application.rb index 6fe42e9ef..7f52b3117 100644 --- a/config/application.rb +++ b/config/application.rb @@ -27,7 +27,8 @@ module Fizzy # enable load_async config.active_record.async_query_executor = :global_thread_pool - # use routes for error pages instead of static files - config.exceptions_app = self.routes + # include the tenant in query logs + config.active_record.query_log_tags_enabled = true + config.active_record.query_log_tags = [ :tenant ] end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 62a5b4cb0..8ca445d76 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -20,8 +20,10 @@ Rails.application.configure do # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true - # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - # config.public_file_server.enabled = false + config.public_file_server.enabled = true + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{1.year.to_i}" + } # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" diff --git a/config/routes.rb b/config/routes.rb index 0ac566f2b..640e82843 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + root "events#index" + namespace :account do resource :join_code resource :settings @@ -15,8 +17,6 @@ Rails.application.routes.draw do end end - resource :landing - resources :boards do scope module: :boards do resource :subscriptions @@ -66,21 +66,21 @@ Rails.application.routes.draw do resources :cards do scope module: :cards do + resource :board + resource :closure + resource :column resource :goldness resource :image - resource :pin - resource :closure resource :not_now - resource :triage + resource :pin resource :publish resource :reading + resource :triage resource :watch - resource :board - resource :column resources :assignments - resources :taggings resources :steps + resources :taggings resources :comments do resources :reactions, module: :comments @@ -144,13 +144,7 @@ Rails.application.routes.draw do end end - resources :commands - - resource :conversation do - scope module: :conversations do - resources :messages - end - end + resource :landing scope module: :memberships, path: "memberships/:membership_id" do resource :unlink, only: %i[ show create ], controller: :unlink, as: :unlink_membership @@ -168,8 +162,8 @@ Rails.application.routes.draw do namespace :prompts do resources :cards - resources :users resources :tags + resources :users resources :boards do scope module: :boards do @@ -194,9 +188,6 @@ Rails.application.routes.draw do end end - namespace :admin do - end - direct :published_board do |board, options| route_for :public_board, board.publication.key end @@ -230,8 +221,6 @@ Rails.application.routes.draw do get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "service-worker" => "pwa#service_worker" - root "events#index" - unless Rails.application.config.x.oss_config mount Fizzy::Saas::Engine, at: "/", as: "saas" end diff --git a/db/migrate/20251106154151_add_user_and_card_index_to_watches.rb b/db/migrate/20251106154151_add_user_and_card_index_to_watches.rb new file mode 100644 index 000000000..1ad20894a --- /dev/null +++ b/db/migrate/20251106154151_add_user_and_card_index_to_watches.rb @@ -0,0 +1,5 @@ +class AddUserAndCardIndexToWatches < ActiveRecord::Migration[8.2] + def change + add_index :watches, %i[ user_id card_id ] + end +end diff --git a/db/schema.rb b/db/schema.rb index fbf5f98df..9516f3206 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_11_05_122933) do +ActiveRecord::Schema[8.2].define(version: 2025_11_06_154151) do create_table "accesses", force: :cascade do |t| t.datetime "accessed_at" t.integer "board_id", null: false @@ -404,6 +404,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_05_122933) do t.integer "user_id", null: false t.boolean "watching", default: true, null: false t.index ["card_id"], name: "index_watches_on_card_id" + t.index ["user_id", "card_id"], name: "index_watches_on_user_id_and_card_id" t.index ["user_id"], name: "index_watches_on_user_id" end diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 60b8b29da..f818b58bf 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -2760,6 +2760,23 @@ indexes: nulls_not_distinct: comment: valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: watches + name: index_watches_on_user_id_and_card_id + unique: false + columns: + - user_id + - card_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true webhook_delinquency_trackers: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: webhook_delinquency_trackers @@ -2843,4 +2860,4 @@ indexes: nulls_not_distinct: comment: valid: true -version: 20251105122933 +version: 20251106154151 diff --git a/gems/fizzy-saas/app/controllers/concerns/restricted.rb b/gems/fizzy-saas/app/controllers/concerns/restricted.rb deleted file mode 100644 index b117810ba..000000000 --- a/gems/fizzy-saas/app/controllers/concerns/restricted.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Restricted - extend ActiveSupport::Concern - - included do - unless Rails.env.development? - http_basic_authenticate_with \ - name: Rails.env.test? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name, - password: Rails.env.test? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password - end - end -end diff --git a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb b/gems/fizzy-saas/app/controllers/signup/completions_controller.rb similarity index 85% rename from gems/fizzy-saas/app/controllers/signups/completions_controller.rb rename to gems/fizzy-saas/app/controllers/signup/completions_controller.rb index 58dd528d2..5641ce418 100644 --- a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb +++ b/gems/fizzy-saas/app/controllers/signup/completions_controller.rb @@ -1,6 +1,4 @@ -class Signups::CompletionsController < ApplicationController - include Restricted - +class Signup::CompletionsController < ApplicationController require_untenanted_access layout "public" diff --git a/gems/fizzy-saas/app/controllers/signups/memberships_controller.rb b/gems/fizzy-saas/app/controllers/signup/memberships_controller.rb similarity index 87% rename from gems/fizzy-saas/app/controllers/signups/memberships_controller.rb rename to gems/fizzy-saas/app/controllers/signup/memberships_controller.rb index c75b63950..799382d58 100644 --- a/gems/fizzy-saas/app/controllers/signups/memberships_controller.rb +++ b/gems/fizzy-saas/app/controllers/signup/memberships_controller.rb @@ -1,6 +1,4 @@ -class Signups::MembershipsController < ApplicationController - include Restricted - +class Signup::MembershipsController < ApplicationController require_untenanted_access layout "public" diff --git a/gems/fizzy-saas/app/controllers/signups_controller.rb b/gems/fizzy-saas/app/controllers/signups_controller.rb deleted file mode 100644 index 967a4b571..000000000 --- a/gems/fizzy-saas/app/controllers/signups_controller.rb +++ /dev/null @@ -1,33 +0,0 @@ -class SignupsController < ApplicationController - include Restricted - - require_untenanted_access - require_unauthenticated_access - - layout "public" - - rate_limit only: :create, name: "short-term", to: 5, within: 3.minutes, - with: -> { redirect_to saas.new_signup_path, alert: "Try again later." } - rate_limit only: :create, name: "long-term", to: 10, within: 30.minutes, - with: -> { redirect_to saas.new_signup_path, alert: "Try again later." } - - def new - @signup = Signup.new - end - - def create - @signup = Signup.new(signup_params) - - if @signup.create_identity - session[:return_to_after_authenticating] = saas.new_signup_membership_path - redirect_to session_magic_link_path - else - render :new, status: :unprocessable_entity - end - end - - private - def signup_params - params.expect(signup: %i[ email_address ]) - end -end diff --git a/gems/fizzy-saas/app/models/signup.rb b/gems/fizzy-saas/app/models/signup.rb index ed9b94528..b03ce730a 100644 --- a/gems/fizzy-saas/app/models/signup.rb +++ b/gems/fizzy-saas/app/models/signup.rb @@ -8,10 +8,6 @@ class Signup attr_accessor :full_name, :email_address, :identity, :membership_id, :account_name attr_reader :queenbee_account, :account, :user, :tenant, :membership - with_options on: :identity_creation do - validates_presence_of :email_address - end - with_options on: :membership_creation do validates_presence_of :full_name, :identity end @@ -42,8 +38,6 @@ class Signup end def create_identity - return false unless valid?(:identity_creation) - @identity = Identity.find_or_create_by!(email_address: email_address) @identity.send_magic_link end diff --git a/gems/fizzy-saas/app/views/signups/completions/new.html.erb b/gems/fizzy-saas/app/views/signup/completions/new.html.erb similarity index 83% rename from gems/fizzy-saas/app/views/signups/completions/new.html.erb rename to gems/fizzy-saas/app/views/signup/completions/new.html.erb index e350c36fc..51a442bd3 100644 --- a/gems/fizzy-saas/app/views/signups/completions/new.html.erb +++ b/gems/fizzy-saas/app/views/signup/completions/new.html.erb @@ -1,7 +1,7 @@ -<% @page_title = "Creating your account" %> +<% @page_title = "Creating your account…" %>
        "> -

        <%= @page_title %>

        +

        <%= @page_title %>

        <%= form_with model: @signup, url: saas.signup_completion_url(script_name: "/#{@signup.tenant}"), scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: @signup.errors.blank? ? "form auto-submit" : "form" } do |form| %> <%= form.hidden_field :membership_id %> @@ -19,8 +19,7 @@ <% end %> <% end %>
        diff --git a/gems/fizzy-saas/app/views/signups/memberships/new.html.erb b/gems/fizzy-saas/app/views/signup/memberships/new.html.erb similarity index 69% rename from gems/fizzy-saas/app/views/signups/memberships/new.html.erb rename to gems/fizzy-saas/app/views/signup/memberships/new.html.erb index 416e9d8f2..dc60fd8a8 100644 --- a/gems/fizzy-saas/app/views/signups/memberships/new.html.erb +++ b/gems/fizzy-saas/app/views/signup/memberships/new.html.erb @@ -1,12 +1,15 @@ -<% @page_title = "Create your account" %> +<% @page_title = "Complete your sign-up" %>
        "> -

        <%= @page_title %>

        +

        <%= @page_title %>

        <%= form_with model: @signup, url: saas.signup_membership_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %> <%= form.hidden_field :new_user %> - <%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %> + <%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name…", autofocus: true, required: true %> + +

        You’re one step away. Just enter your name to get your own Fizzy account.

        + <% if @signup.errors.any? %>
        diff --git a/gems/fizzy-saas/app/views/signups/new.html.erb b/gems/fizzy-saas/app/views/signup/new.html.erb similarity index 100% rename from gems/fizzy-saas/app/views/signups/new.html.erb rename to gems/fizzy-saas/app/views/signup/new.html.erb diff --git a/gems/fizzy-saas/config/routes.rb b/gems/fizzy-saas/config/routes.rb index 04f8faca9..b99f172ff 100644 --- a/gems/fizzy-saas/config/routes.rb +++ b/gems/fizzy-saas/config/routes.rb @@ -1,11 +1,9 @@ Fizzy::Saas::Engine.routes.draw do - resource :signup, only: %i[ new create ] do - scope module: :signups, as: :signup do - collection do - resource :membership, only: %i[ new create ] - resource :completion, only: %i[ new create ] - end - end + get "/signup/new", to: redirect("/session/new") + + namespace :signup do + resource :membership, only: %i[ new create ] + resource :completion, only: %i[ new create ] end Queenbee.routes(self) diff --git a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb index 35861722d..aa18721fd 100644 --- a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb +++ b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest +class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest setup do @signup = Signup.new(email_address: "newuser@example.com", full_name: "New User") @@ -16,7 +16,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest get saas.new_signup_completion_path(signup: { membership_id: @signup.membership_id, full_name: @signup.full_name, - account_name: @signup.account_name }), headers: http_basic_auth_headers + account_name: @signup.account_name }) end assert_response :success @@ -30,7 +30,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest full_name: @signup.full_name, account_name: @signup.account_name } - }, headers: http_basic_auth_headers + } end assert_redirected_to landing_path(script_name: "/#{@signup.tenant}"), "Successful completion should redirect to root in new tenant" @@ -42,14 +42,9 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest full_name: "", account_name: "" } - }, headers: http_basic_auth_headers + } end assert_response :unprocessable_entity, "Invalid params should return unprocessable entity" end - - private - def http_basic_auth_headers - { "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") } - end end diff --git a/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb b/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb index f1ca13093..57010d2bd 100644 --- a/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb +++ b/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb @@ -1,6 +1,6 @@ require "test_helper" -class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest +class Signup::MembershipsControllerTest < ActionDispatch::IntegrationTest setup do @identity = Identity.create!(email_address: "newuser@example.com") magic_link = @identity.send_magic_link @@ -16,7 +16,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest test "new" do untenanted do - get saas.new_signup_membership_path, headers: http_basic_auth_headers + get saas.new_signup_membership_path assert_response :success end @@ -24,7 +24,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest test "new with new_user param" do untenanted do - get saas.new_signup_membership_path(signup: { new_user: true }), headers: http_basic_auth_headers + get saas.new_signup_membership_path(signup: { new_user: true }) assert_response :success end @@ -37,7 +37,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest signup: { full_name: "New User" } - }, headers: http_basic_auth_headers + } end membership = Membership.last @@ -58,15 +58,10 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest signup: { full_name: "" } - }, headers: http_basic_auth_headers + } end assert_response :unprocessable_entity, "Invalid params should return unprocessable entity" end end - - private - def http_basic_auth_headers - { "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") } - end end diff --git a/gems/fizzy-saas/test/controllers/signups_controller_test.rb b/gems/fizzy-saas/test/controllers/signups_controller_test.rb deleted file mode 100644 index afb773031..000000000 --- a/gems/fizzy-saas/test/controllers/signups_controller_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require "test_helper" - -class SignupsControllerTest < ActionDispatch::IntegrationTest - setup do - @signup_params = { - full_name: "Brian Wilson", - email_address: "brian@example.com", - company_name: "Beach Boys" - } - @starting_tenants = ApplicationRecord.tenants - - # Clear script_name for untenanted signup tests - integration_session.default_url_options[:script_name] = nil - end - - test "should require http basic authentication" do - get saas.new_signup_url - - assert_response :unauthorized - end - - test "should get new" do - get saas.new_signup_url, headers: http_basic_auth_headers - - assert_response :success - assert_select "h1", "Sign up" - assert_select "input[name='signup[email_address]']" - end - - test "should create signup and redirect to magic link page" do - assert_no_difference -> { ApplicationRecord.tenants.count } do - post saas.signup_url, params: { signup: { email_address: @signup_params[:email_address] } }, headers: http_basic_auth_headers - end - - assert_redirected_to session_magic_link_path - end - - test "should render new with errors when signup fails validation" do - invalid_params = { email_address: "" } - - assert_no_difference -> { ApplicationRecord.tenants.count } do - post saas.signup_url, params: { signup: invalid_params }, headers: http_basic_auth_headers - end - - assert_response :unprocessable_entity - assert_select ".txt-negative" - end - - private - def http_basic_auth_headers - credentials = ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") - { "HTTP_AUTHORIZATION" => credentials } - end -end diff --git a/gems/fizzy-saas/test/models/signup_test.rb b/gems/fizzy-saas/test/models/signup_test.rb index 685b25b38..e5364c141 100644 --- a/gems/fizzy-saas/test/models/signup_test.rb +++ b/gems/fizzy-saas/test/models/signup_test.rb @@ -27,8 +27,9 @@ class SignupTest < ActiveSupport::TestCase end signup_invalid = Signup.new(email_address: "") - assert_not signup_invalid.create_identity, "Should fail with invalid email" - assert_not_empty signup_invalid.errors[:email_address], "Should have validation error for email_address" + assert_raises do + signup_invalid.create_identity + end end test "#create_membership" do @@ -56,7 +57,6 @@ class SignupTest < ActiveSupport::TestCase test "#complete" do Account.any_instance.expects(:setup_customer_template).once - # First create the membership signup_for_membership = Signup.new( full_name: "Kevin", identity: identities(:kevin) diff --git a/lib/web_push/notification.rb b/lib/web_push/notification.rb index 891f53799..d2865ff46 100644 --- a/lib/web_push/notification.rb +++ b/lib/web_push/notification.rb @@ -15,7 +15,7 @@ class WebPush::Notification private def vapid_identification - { subject: "mailto:support@37signals.com" }.merge \ + { subject: "mailto:support@fizzy.do" }.merge \ Rails.configuration.x.vapid.symbolize_keys end diff --git a/public/app-icon-192-maskable.png b/public/app-icon-192-maskable.png index f3286645c..42d39f88a 100644 Binary files a/public/app-icon-192-maskable.png and b/public/app-icon-192-maskable.png differ diff --git a/public/app-icon-192.png b/public/app-icon-192.png index f3286645c..93c0a4f50 100644 Binary files a/public/app-icon-192.png and b/public/app-icon-192.png differ diff --git a/public/app-icon.png b/public/app-icon.png index 8fe3d2094..44efbd781 100644 Binary files a/public/app-icon.png and b/public/app-icon.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index e2266fcb3..5204feb44 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico index f48235b97..288329979 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png index 79de761f2..e470ed15c 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/test/controllers/accounts/entropies_controller_test.rb b/test/controllers/accounts/entropies_controller_test.rb index 9fda2c1ed..ef23851de 100644 --- a/test/controllers/accounts/entropies_controller_test.rb +++ b/test/controllers/accounts/entropies_controller_test.rb @@ -12,4 +12,11 @@ class Account::EntropiesControllerTest < ActionDispatch::IntegrationTest assert_redirected_to account_settings_path end + + test "update requires admin" do + logout_and_sign_in_as :david + + put account_entropy_path, params: { entropy: { auto_postpone_period: 1.day } } + assert_response :forbidden + end end diff --git a/test/controllers/accounts/join_codes_controller_test.rb b/test/controllers/accounts/join_codes_controller_test.rb index 438904d2a..ee80e4193 100644 --- a/test/controllers/accounts/join_codes_controller_test.rb +++ b/test/controllers/accounts/join_codes_controller_test.rb @@ -23,4 +23,18 @@ class Account::JoinCodesControllerTest < ActionDispatch::IntegrationTest assert_equal 5, Account::JoinCode.sole.usage_limit assert_redirected_to account_join_code_path end + + test "update requires admin" do + logout_and_sign_in_as :david + + put account_join_code_path, params: { account_join_code: { usage_limit: 5 } } + assert_response :forbidden + end + + test "destroy requires admin" do + logout_and_sign_in_as :david + + delete account_join_code_path + assert_response :forbidden + end end diff --git a/test/controllers/accounts/settings_controller_test.rb b/test/controllers/accounts/settings_controller_test.rb index 9a9a45d3b..50078dd32 100644 --- a/test/controllers/accounts/settings_controller_test.rb +++ b/test/controllers/accounts/settings_controller_test.rb @@ -15,4 +15,11 @@ class Account::SettingsControllerTest < ActionDispatch::IntegrationTest assert_equal "New Account Name", Account.sole.name assert_redirected_to account_settings_path end + + test "update requires admin" do + logout_and_sign_in_as :david + + put account_settings_path, params: { account: { name: "New Account Name" } } + assert_response :forbidden + end end diff --git a/test/controllers/boards/entropies_controller_test.rb b/test/controllers/boards/entropies_controller_test.rb index 02338f393..111fe1513 100644 --- a/test/controllers/boards/entropies_controller_test.rb +++ b/test/controllers/boards/entropies_controller_test.rb @@ -13,4 +13,15 @@ class Boards::EntropiesControllerTest < ActionDispatch::IntegrationTest assert_turbo_stream action: :replace, target: dom_id(@board, :entropy) end + + test "update requires board admin permission" do + logout_and_sign_in_as :jz + + original_period = @board.entropy.auto_postpone_period + + put board_entropy_path(@board, format: :turbo_stream), params: { board: { auto_postpone_period: 1.day } } + + assert_response :forbidden + assert_equal original_period, @board.entropy.reload.auto_postpone_period + end end diff --git a/test/controllers/boards/publications_controller_test.rb b/test/controllers/boards/publications_controller_test.rb index 6ce770858..568c21571 100644 --- a/test/controllers/boards/publications_controller_test.rb +++ b/test/controllers/boards/publications_controller_test.rb @@ -21,9 +21,32 @@ class Boards::PublicationsControllerTest < ActionDispatch::IntegrationTest assert @board.published? assert_changes -> { @board.reload.published? }, from: true, to: false do - delete board_publication_path(@board, format: :turbo_streamn) + delete board_publication_path(@board, format: :turbo_stream) end assert_turbo_stream action: :replace, target: dom_id(@board, :publication) end + + test "publish requires board admin permission" do + logout_and_sign_in_as :jz + + assert_not @board.published? + + post board_publication_path(@board, format: :turbo_stream) + + assert_response :forbidden + assert_not @board.reload.published? + end + + test "unpublish requires board admin permission" do + logout_and_sign_in_as :jz + + @board.publish + assert @board.published? + + delete board_publication_path(@board, format: :turbo_stream) + + assert_response :forbidden + assert @board.reload.published? + end end diff --git a/test/controllers/boards_controller_test.rb b/test/controllers/boards_controller_test.rb index 14f573780..41cbb557e 100644 --- a/test/controllers/boards_controller_test.rb +++ b/test/controllers/boards_controller_test.rb @@ -122,4 +122,18 @@ class BoardsControllerTest < ActionDispatch::IntegrationTest assert_response :forbidden assert_equal original_users, board.reload.users.sort end + + test "non-admin cannot change board name on board they don't own" do + logout_and_sign_in_as :jz + + board = boards(:writebook) + original_name = board.name + + patch board_path(board), params: { + board: { name: "Hacked Board Name" } + } + + assert_response :forbidden + assert_equal original_name, board.reload.name + end end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 2923734b7..623423fdf 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -1,10 +1,46 @@ require "test_helper" class SessionsControllerTest < ActionDispatch::IntegrationTest - test "destroy" do + test "new" do untenanted do - sign_in_as :kevin + get new_session_path + end + assert_response :success + end + + test "create" do + identity = identities(:kevin) + + untenanted do + assert_difference -> { MagicLink.count }, 1 do + post session_path, params: { email_address: identity.email_address } + end + + assert_redirected_to session_magic_link_path + end + end + + unless Bootstrap.oss_config? + test "create for a new user" do + untenanted do + assert_difference -> { Identity.count }, +1 do + assert_difference -> { MagicLink.count }, +1 do + post session_path, + params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" }, + headers: http_basic_auth_headers("testname", "testpassword") + end + end + + assert_redirected_to session_magic_link_path + end + end + end + + test "destroy" do + sign_in_as :kevin + + untenanted do delete session_path assert_redirected_to new_session_path @@ -12,31 +48,8 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest end end - test "new" do - untenanted do - get new_session_path - - assert_response :success + private + def http_basic_auth_headers(user, password) + { "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } end - end - - test "create" do - untenanted do - identity = identities(:kevin) - - assert_difference -> { MagicLink.count }, 1 do - post session_path, params: { email_address: identity.email_address } - end - - assert_redirected_to session_magic_link_path - end - - untenanted do - assert_no_difference -> { MagicLink.count } do - post session_path, params: { email_address: "nonexistent@example.com" } - end - - assert_redirected_to session_magic_link_path - end - end end diff --git a/test/fixtures/files/avatar.png b/test/fixtures/files/avatar.png new file mode 100644 index 000000000..82632a317 Binary files /dev/null and b/test/fixtures/files/avatar.png differ diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb new file mode 100644 index 000000000..fc48e48a1 --- /dev/null +++ b/test/helpers/application_helper_test.rb @@ -0,0 +1,49 @@ +require "test_helper" + +class ApplicationHelperTest < ActionView::TestCase + def parse(html) + Nokogiri::HTML::DocumentFragment.parse(html) + end + + test "page_title_tag on untenanted page" do + ApplicationRecord.without_tenant do + assert_select parse(page_title_tag), "title", text: "Fizzy" + end + end + + test "page_title_tag on untenanted page with a page title" do + @page_title = "Holodeck" + + ApplicationRecord.without_tenant do + assert_select parse(page_title_tag), "title", text: "Holodeck | Fizzy" + end + end + + test "page_title_tag on tenanted page when user has a single membership" do + Current.session = sessions(:david) + + assert_select parse(page_title_tag), "title", text: "Fizzy" + end + + test "page_title_tag on tenanted page when user has multiple memberships" do + Current.session = sessions(:david) + identities(:david).memberships.create!(tenant: "dangling-tenant") + + assert_select parse(page_title_tag), "title", text: "37signals | Fizzy" + end + + test "page_title_tag on tenanted page with a page title when user has a single membership" do + Current.session = sessions(:david) + @page_title = "Holodeck" + + assert_select parse(page_title_tag), "title", text: "Holodeck | Fizzy" + end + + test "page_title_tag on tenanted page with a page title when user has multiple memberships" do + Current.session = sessions(:david) + identities(:david).memberships.create!(tenant: "dangling-tenant") + @page_title = "Holodeck" + + assert_select parse(page_title_tag), "title", text: "Holodeck | 37signals | Fizzy" + end +end diff --git a/test/mailers/magic_link_mailer_test.rb b/test/mailers/magic_link_mailer_test.rb index ee3d56abc..7d389587f 100644 --- a/test/mailers/magic_link_mailer_test.rb +++ b/test/mailers/magic_link_mailer_test.rb @@ -10,7 +10,7 @@ class MagicLinkMailerTest < ActionMailer::TestCase end assert_equal [ "kevin@37signals.com" ], email.to - assert_equal "Sign in to Fizzy", email.subject + assert_equal "Your Fizzy verification code", email.subject assert_match magic_link.code, email.body.encoded end end diff --git a/test/mailers/notification/bundle_mailer_test.rb b/test/mailers/notification/bundle_mailer_test.rb new file mode 100644 index 000000000..fbc4274c9 --- /dev/null +++ b/test/mailers/notification/bundle_mailer_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +class Notification::BundleMailerTest < ActionMailer::TestCase + setup do + @user = users(:david) + + @bundle = Notification::Bundle.create!( + user: @user, + starts_at: 1.hour.ago, + ends_at: 1.hour.from_now + ) + end + + test "renders avatar with initials in span when avatar is not attached" do + create_notification(@user) + + email = Notification::BundleMailer.notification(@bundle) + + assert_match /]*class="avatar"[^>]*>/, email.html_part.body.to_s + assert_match /#{@user.initials}/, email.html_part.body.to_s + assert_match /style="background-color: #[A-F0-9]{6};?"/, email.html_part.body.to_s + end + + test "renders avatar with external image URL when avatar is attached" do + @user.avatar.attach( + io: File.open(Rails.root.join("test", "fixtures", "files", "avatar.png")), + filename: "avatar.png", + content_type: "image/png" + ) + + create_notification(@user) + + email = Notification::BundleMailer.notification(@bundle) + + assert_match /]*class="avatar"[^>]*>/, email.html_part.body.to_s + assert_match /]*class="avatar"[^>]*src="[^"]*"/, email.html_part.body.to_s + assert_match /alt="#{@user.name}"/, email.html_part.body.to_s + end + + private + def create_notification(user) + Notification.create!(user: user, creator: user, source: events(:logo_published), created_at: 30.minutes.ago) + end +end diff --git a/test/mailers/previews/identity_mailer_preview.rb b/test/mailers/previews/identity_mailer_preview.rb new file mode 100644 index 000000000..8d29667bc --- /dev/null +++ b/test/mailers/previews/identity_mailer_preview.rb @@ -0,0 +1,17 @@ +class IdentityMailerPreview < ActionMailer::Preview + def email_change_confirmation + ApplicationRecord.current_tenant = "897362094" + + identity = Identity.find_by(email_address: "david@37signals.com") + membership = identity&.memberships&.find_by(tenant: ApplicationRecord.current_tenant) + + new_email_address = "david.new@example.com" + token = membership.send(:generate_email_address_change_token, to: new_email_address) + + IdentityMailer.email_change_confirmation( + email_address: new_email_address, + token: token, + membership: membership + ) + end +end diff --git a/test/mailers/previews/notification/bundle_mailer_preview.rb b/test/mailers/previews/notification/bundle_mailer_preview.rb index 3c279e916..9242aaa60 100644 --- a/test/mailers/previews/notification/bundle_mailer_preview.rb +++ b/test/mailers/previews/notification/bundle_mailer_preview.rb @@ -1,6 +1,5 @@ class Notification::BundleMailerPreview < ActionMailer::Preview def notification - ApplicationRecord.current_tenant = "1065895976" Notification::BundleMailer.notification Notification::Bundle.take! end end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb deleted file mode 100644 index d36b8929b..000000000 --- a/test/mailers/previews/user_mailer_preview.rb +++ /dev/null @@ -1,18 +0,0 @@ -class UserMailerPreview < ActionMailer::Preview - def email_change_confirmation - ApplicationRecord.current_tenant = "897362094" - user = User.find_by(email_address: "david@37signals.com") || User.new( - name: "David", - email_address: "david@37signals.com" - ) - - new_email_address = "david.new@example.com" - token = user.generate_email_address_change_token(to: new_email_address) - - UserMailer.email_change_confirmation( - user: user, - email_address: new_email_address, - token: token - ) - end -end diff --git a/test/models/access_test.rb b/test/models/access_test.rb index 0ea14a250..1b9ea2d12 100644 --- a/test/models/access_test.rb +++ b/test/models/access_test.rb @@ -64,4 +64,20 @@ class AccessTest < ActiveSupport::TestCase assert_empty remaining_mentions end + + test "watches are destroyed when access is lost" do + kevin = users(:kevin) + board = boards(:writebook) + card = board.cards.first + + assert card.watched_by?(kevin) + + kevin_access = accesses(:writebook_kevin) + + perform_enqueued_jobs only: Board::CleanInaccessibleDataJob do + kevin_access.destroy + end + + assert_not card.watched_by?(kevin) + end end diff --git a/test/models/board/accessible_test.rb b/test/models/board/accessible_test.rb index a384efad3..8c3beef5e 100644 --- a/test/models/board/accessible_test.rb +++ b/test/models/board/accessible_test.rb @@ -38,4 +38,6 @@ class Board::AccessibleTest < ActiveSupport::TestCase boards(:writebook).access_for(users(:kevin)).access_only! assert_not_includes boards(:writebook).reload.watchers, users(:kevin) end + + # NOTE: The tests for clearing inaccessible data are in +AccessTest+ end diff --git a/test/models/concerns/mentions_test.rb b/test/models/concerns/mentions_test.rb index d2edb073a..833d6df0d 100644 --- a/test/models/concerns/mentions_test.rb +++ b/test/models/concerns/mentions_test.rb @@ -8,8 +8,8 @@ class MentionsTest < ActiveSupport::TestCase test "don't create mentions when creating or updating drafts" do assert_no_difference -> { Mention.count } do perform_enqueued_jobs only: Mention::CreateJob do - card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?" - card.update description: "Any thoughts here @jz" + card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{mention_html_for(users(:david))}?" + card.update description: "Any thoughts here #{mention_html_for(users(:jz))}" end end end @@ -17,9 +17,11 @@ class MentionsTest < ActiveSupport::TestCase test "create mentions from plain text mentions when publishing cards" do perform_enqueued_jobs only: Mention::CreateJob do card = assert_no_difference -> { Mention.count } do - boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?" + boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{mention_html_for(users(:david))}?" end + card = Card.find(card.id) + assert_difference -> { Mention.count }, +1 do card.published! end @@ -29,10 +31,11 @@ class MentionsTest < ActiveSupport::TestCase test "create mentions from rich text mentions when publishing cards" do perform_enqueued_jobs only: Mention::CreateJob do card = assert_no_difference -> { Mention.count } do - attachment = ActionText::Attachment.from_attachable(users(:david)) - boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{attachment.to_html}?" + boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{mention_html_for(users(:david))}?" end + card = Card.find(card.id) + assert_difference -> { Mention.count }, +1 do card.published! end @@ -41,18 +44,18 @@ class MentionsTest < ActiveSupport::TestCase test "don't create repeated mentions when updating cards" do perform_enqueued_jobs only: Mention::CreateJob do - card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?" + card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{mention_html_for(users(:david))}?" assert_difference -> { Mention.count }, +1 do card.published! end assert_no_difference -> { Mention.count } do - card.update description: "Any thoughts here @david" + card.update description: "Any thoughts here #{mention_html_for(users(:david))}" end assert_difference -> { Mention.count }, +1 do - card.update description: "Any thoughts here @jz" + card.update description: "Any thoughts here #{mention_html_for(users(:jz))}" end end end @@ -62,7 +65,7 @@ class MentionsTest < ActiveSupport::TestCase card = boards(:writebook).cards.create title: "Cleanup", description: "Some initial content", status: :published assert_difference -> { Mention.count }, +1 do - card.comments.create!(body: "Great work on this @david!") + card.comments.create!(body: "Great work on this #{mention_html_for(users(:david))}!") end end end @@ -72,7 +75,7 @@ class MentionsTest < ActiveSupport::TestCase card = boards(:writebook).cards.create title: "Cleanup", description: "Some initial content" assert_no_difference -> { Mention.count } do - card.comments.create!(body: "Great work on this @david!") + card.comments.create!(body: "Great work on this #{mention_html_for(users(:david))}!") end end end @@ -83,17 +86,21 @@ class MentionsTest < ActiveSupport::TestCase assert_no_difference -> { Mention.count }, +1 do perform_enqueued_jobs only: Mention::CreateJob do - attachment = ActionText::Attachment.from_attachable(users(:david)) - boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{attachment.to_html}?" + boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, #{mention_html_for(users(:david))}?" end end end test "mentionees are added as watchers of the card" do perform_enqueued_jobs only: Mention::CreateJob do - card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup @kevin?" + card = boards(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup #{mention_html_for(users(:kevin))}?" card.published! assert card.watchers.include?(users(:kevin)) end end + + private + def mention_html_for(user) + ActionText::Attachment.from_attachable(user).to_html + end end diff --git a/test/models/event/description_test.rb b/test/models/event/description_test.rb new file mode 100644 index 000000000..e5bd583a7 --- /dev/null +++ b/test/models/event/description_test.rb @@ -0,0 +1,35 @@ +require "test_helper" + +class Event::DescriptionTest < ActiveSupport::TestCase + test "generates html description for card published event" do + description = events(:logo_published).description_for(users(:david)) + + assert_includes description.to_html, "added" + assert_includes description.to_html, "logo" + end + + test "generates plain text description for card published event" do + description = events(:logo_published).description_for(users(:david)) + + assert_includes description.to_plain_text, "David added" + assert_includes description.to_plain_text, "logo" + end + + test "generates description for comment event" do + description = events(:layout_commented).description_for(users(:jz)) + + assert_includes description.to_plain_text, "David commented on" + end + + test "uses always the name even when the event creator is the current user" do + description = events(:logo_published).description_for(users(:david)) + + assert_includes description.to_plain_text, "David added" + end + + test "uses creator name when event creator is not the current user" do + description = events(:logo_published).description_for(users(:jz)) + + assert_includes description.to_plain_text, "David added" + end +end diff --git a/test/models/notifier/event_notifier_test.rb b/test/models/notifier/event_notifier_test.rb index 886551efa..bc68b2a6c 100644 --- a/test/models/notifier/event_notifier_test.rb +++ b/test/models/notifier/event_notifier_test.rb @@ -68,10 +68,19 @@ class Notifier::EventNotifierTest < ActiveSupport::TestCase assert_empty notifications end - test "don't create notifications on publish for mentionees" do + test "create notifications on publish for mentionees" do users(:kevin).mentioned_by(users(:david), at: cards(:logo)) - assert_no_difference -> { users(:kevin).notifications.count } do + assert_difference -> { users(:kevin).notifications.count }, +1 do + Notifier.for(events(:logo_published)).notify + end + end + + test "don'create notifications on publish for mentionees that are not watching" do + users(:kevin).mentioned_by(users(:david), at: cards(:logo)) + cards(:logo).unwatch_by(users(:kevin)) + + assert_difference -> { users(:kevin).notifications.count }, +1 do Notifier.for(events(:logo_published)).notify end end diff --git a/test/models/webhook/delivery_test.rb b/test/models/webhook/delivery_test.rb index 244a9a2cc..e803526ac 100644 --- a/test/models/webhook/delivery_test.rb +++ b/test/models/webhook/delivery_test.rb @@ -218,4 +218,44 @@ class Webhook::DeliveryTest < ActiveSupport::TestCase assert Webhook::Delivery.exists?(fresh_delivery.id) assert_not Webhook::Delivery.exists?(stale_delivery.id) end + + test "renders the creator name when event creator is current user" do + webhook = Webhook.create!( + board: boards(:writebook), + name: "Basecamp", + url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/789/lines" + ) + event = events(:logo_published) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + Current.session = sessions(:david) + + request_stub = stub_request(:post, webhook.url) + .with { |request| CGI.parse(request.body)["content"].first.include?("David added") } + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + end + + test "renders creator name when event creator is not current user" do + webhook = Webhook.create!( + board: boards(:writebook), + name: "Basecamp", + url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/789/lines" + ) + event = events(:logo_published) + delivery = Webhook::Delivery.create!(webhook: webhook, event: event) + + Current.session = sessions(:kevin) + + request_stub = stub_request(:post, webhook.url) + .with { |request| CGI.parse(request.body)["content"].first.include?("David added") } + .to_return(status: 200) + + delivery.deliver + + assert_requested request_stub + end end diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 76c601024..d899aa450 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -27,7 +27,7 @@ class SmokeTest < ApplicationSystemTestCase assert_selector "figcaption input[placeholder='moon.jpg']" end - click_on "Post this comment" + click_on "Post" within("action-text-attachment") do assert_selector "a img[src*='/rails/active_storage']"
      <%= image_tag user_avatar_url(notification.creator), alt: notification.creator.name, class: "avatar", size: 48 %>  <%= render "notification/bundle_mailer/header", notification: notification %> +
      + <%= mail_avatar_tag(notification.creator) %> + <%= render "notification/bundle_mailer/#{notification.source_type.underscore}/body", notification: notification %>