diff --git a/Gemfile.lock b/Gemfile.lock index 4b3ce6e44..0e48985b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,8 +123,8 @@ GEM ast (2.4.3) autotuner (1.1.0) aws-eventstream (1.4.0) - aws-partitions (1.1197.0) - aws-sdk-core (3.240.0) + aws-partitions (1.1203.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -132,28 +132,28 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.208.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bcrypt (3.1.20) - bcrypt_pbkdf (1.1.1) + bcrypt (3.1.21) + bcrypt_pbkdf (1.1.2) benchmark (0.5.0) bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.19.0) + bootsnap (1.20.1) msgpack (~> 1.2) - brakeman (7.1.1) + brakeman (7.1.2) racc builder (3.3.0) - bundler-audit (0.9.2) - bundler (>= 1.2.0, < 3) + bundler-audit (0.9.3) + bundler (>= 1.2.0) thor (~> 1.0) capybara (3.40.0) addressable @@ -168,23 +168,23 @@ GEM logger (~> 1.5) chunky_png (1.4.0) concurrent-ruby (1.3.6) - connection_pool (2.5.5) + connection_pool (3.0.2) crack (1.0.1) bigdecimal rexml crass (1.0.6) date (3.5.1) - debug (1.11.0) + debug (1.11.1) irb (~> 1.10) reline (>= 0.3.8) - dotenv (3.1.8) + dotenv (3.2.0) drb (2.2.3) ed25519 (1.4.0) erb (6.0.1) erubi (1.13.1) et-orbi (1.4.0) tzinfo - faker (3.5.2) + faker (3.5.3) i18n (>= 1.8.11, < 2) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) @@ -224,7 +224,7 @@ GEM json (2.18.0) jwt (3.1.2) base64 - kamal (2.9.0) + kamal (2.10.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -271,11 +271,11 @@ GEM stimulus-rails turbo-rails mittens (0.3.1) - mocha (2.8.2) + mocha (3.0.1) ruby2_keywords (>= 0.0.5) msgpack (1.8.0) - net-http-persistent (4.0.6) - connection_pool (~> 2.2, >= 2.2.4) + net-http-persistent (4.0.8) + connection_pool (>= 2.2.4, < 4) net-imap (0.6.2) date net-protocol @@ -307,7 +307,7 @@ GEM racc (~> 1.4) nokogiri (1.19.0-x86_64-linux-musl) racc (~> 1.4) - openssl (3.3.2) + openssl (4.0.0) ostruct (0.6.3) parallel (1.27.0) parser (3.3.10.0) @@ -319,7 +319,7 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.6.0) + prism (1.8.0) propshaft (1.3.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -360,11 +360,11 @@ GEM reline (0.6.3) io-console (~> 0.5) rexml (3.4.4) - rouge (4.6.1) - rqrcode (3.1.0) + rouge (4.7.0) + rqrcode (3.2.0) chunky_png (~> 1.0) rqrcode_core (~> 2.0) - rqrcode_core (2.0.0) + rqrcode_core (2.1.0) rubocop (1.81.7) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -400,7 +400,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (3.2.2) securerandom (0.4.1) - selenium-webdriver (4.38.0) + selenium-webdriver (4.39.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -430,7 +430,7 @@ GEM sqlite3 (2.8.0-x86_64-darwin) sqlite3 (2.8.0-x86_64-linux-gnu) sqlite3 (2.8.0-x86_64-linux-musl) - sshkit (1.24.0) + sshkit (1.25.0) base64 logger net-scp (>= 1.1.2) @@ -440,7 +440,7 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) - thor (1.4.0) + thor (1.5.0) thruster (0.1.17) thruster (0.1.17-aarch64-linux) thruster (0.1.17-arm64-darwin) @@ -458,16 +458,15 @@ GEM unicode-emoji (~> 4.1) unicode-emoji (4.1.0) uri (1.1.1) - vcr (6.3.1) - base64 + vcr (6.4.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - web-push (3.0.2) + web-push (3.1.0) jwt (~> 3.0) - openssl (~> 3.0) + openssl (>= 3.0) webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock index 98cf833bc..f340b4ae4 100644 --- a/Gemfile.saas.lock +++ b/Gemfile.saas.lock @@ -201,8 +201,8 @@ GEM ast (2.4.3) autotuner (1.1.0) aws-eventstream (1.4.0) - aws-partitions (1.1197.0) - aws-sdk-core (3.240.0) + aws-partitions (1.1203.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -210,28 +210,28 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.208.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bcrypt (3.1.20) - bcrypt_pbkdf (1.1.1) + bcrypt (3.1.21) + bcrypt_pbkdf (1.1.2) benchmark (0.5.0) bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.19.0) + bootsnap (1.20.1) msgpack (~> 1.2) - brakeman (7.1.1) + brakeman (7.1.2) racc builder (3.3.0) - bundler-audit (0.9.2) - bundler (>= 1.2.0, < 3) + bundler-audit (0.9.3) + bundler (>= 1.2.0) thor (~> 1.0) capybara (3.40.0) addressable @@ -246,16 +246,16 @@ GEM logger (~> 1.5) chunky_png (1.4.0) concurrent-ruby (1.3.6) - connection_pool (2.5.5) + connection_pool (3.0.2) crack (1.0.1) bigdecimal rexml crass (1.0.6) date (3.5.1) - debug (1.11.0) + debug (1.11.1) irb (~> 1.10) reline (>= 0.3.8) - dotenv (3.1.8) + dotenv (3.2.0) drb (2.2.3) dry-initializer (3.2.0) ed25519 (1.4.0) @@ -263,7 +263,7 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo - faker (3.5.2) + faker (3.5.3) i18n (>= 1.8.11, < 2) ffi (1.17.2-aarch64-linux-gnu) ffi (1.17.2-aarch64-linux-musl) @@ -302,7 +302,7 @@ GEM json (2.18.0) jwt (3.1.2) base64 - kamal (2.9.0) + kamal (2.10.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -349,11 +349,11 @@ GEM stimulus-rails turbo-rails mittens (0.3.1) - mocha (2.8.2) + mocha (3.0.1) ruby2_keywords (>= 0.0.5) msgpack (1.8.0) - net-http-persistent (4.0.6) - connection_pool (~> 2.2, >= 2.2.4) + net-http-persistent (4.0.8) + connection_pool (>= 2.2.4, < 4) net-imap (0.6.2) date net-protocol @@ -383,7 +383,7 @@ GEM racc (~> 1.4) nokogiri (1.19.0-x86_64-linux-musl) racc (~> 1.4) - openssl (3.3.2) + openssl (4.0.0) ostruct (0.6.3) parallel (1.27.0) parser (3.3.10.0) @@ -395,7 +395,7 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.6.0) + prism (1.8.0) prometheus-client-mmap (1.4.0) base64 bigdecimal @@ -470,11 +470,11 @@ GEM io-console (~> 0.5) rexml (3.4.4) rinku (2.0.6) - rouge (4.6.1) - rqrcode (3.1.0) + rouge (4.7.0) + rqrcode (3.2.0) chunky_png (~> 1.0) rqrcode_core (~> 2.0) - rqrcode_core (2.0.0) + rqrcode_core (2.1.0) rubocop (1.81.7) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -511,7 +511,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (3.2.2) securerandom (0.4.1) - selenium-webdriver (4.38.0) + selenium-webdriver (4.39.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -549,7 +549,7 @@ GEM sqlite3 (2.8.0-arm64-darwin) sqlite3 (2.8.0-x86_64-linux-gnu) sqlite3 (2.8.0-x86_64-linux-musl) - sshkit (1.24.0) + sshkit (1.25.0) base64 logger net-scp (>= 1.1.2) @@ -560,7 +560,7 @@ GEM railties (>= 6.0.0) stringio (3.2.0) stripe (18.0.1) - thor (1.4.0) + thor (1.5.0) thruster (0.1.17) thruster (0.1.17-aarch64-linux) thruster (0.1.17-arm64-darwin) @@ -577,16 +577,15 @@ GEM unicode-emoji (~> 4.1) unicode-emoji (4.1.0) uri (1.1.1) - vcr (6.3.1) - base64 + vcr (6.4.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - web-push (3.0.2) + web-push (3.1.0) jwt (~> 3.0) - openssl (~> 3.0) + openssl (>= 3.0) webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) diff --git a/app/assets/stylesheets/blank-slates.css b/app/assets/stylesheets/blank-slates.css index aa221c452..5db13ef7f 100644 --- a/app/assets/stylesheets/blank-slates.css +++ b/app/assets/stylesheets/blank-slates.css @@ -1,46 +1,21 @@ +/* Styles for the blank slate. To manage when they are shown/hidden, do so in context */ + @layer components { - .blank-slate--drag { - box-shadow: none !important; - color: color-mix(in srgb, var(--card-color) 70%, var(--color-canvas)); - - p { - font-size: var(--text-small); - overflow: hidden; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; - } - - .cards--maybe & { - background-color: var(--card-bg-color) !important; - } - - .cards--grid & { - display: none; - } - } - - .blank-slate--empty { - border: 2px dashed var(--color-ink-light); - border-radius: 1ch; + .blank-slate { + border-radius: 0.5ch; + border: 2px dashed var(--color-ink-lighter); color: var(--color-ink-dark); - margin-block-start: 2dvh; - padding: 1ch 2ch; - rotate: -3deg; font-weight: 500; - - .cards:has(.card) & { - display: none; - } + margin-block-start: 2dvh; + padding: 1.5ch 2ch; + rotate: -3deg; } - .blank-slate--filters { - .cards--grid:has(.card) & { - display: none; - } - } - - .cards__list:has(> :not(.blank-slate)) .card--hide-unless-empty { - display: none; + .blank-slate--drag { + background-color: color-mix(in srgb, transparent, var(--card-color) 5%); + border-color: color-mix(in srgb, transparent, var(--card-color) 10%); + color: color-mix(in srgb, transparent, var(--card-color) 75%); + margin-block-start: 0; + rotate: 0deg; } } diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 6aaacaf8b..2c0c67ef9 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -255,6 +255,27 @@ } } } + + &:has(.card) { + .blank-slate { + display: none; + } + } + + /* Use the default blank-slate on small viewports since drag-and-drop isn't available */ + [data-controller~="drag-and-drop"] & { + @media (max-width: 639px) { + .blank-slate--drag { + display: none; + } + } + + @media (min-width: 640px) { + .blank-slate--default { + display: none; + } + } + } } .cards__new-column { @@ -315,6 +336,10 @@ inline-size: fit-content; margin: 0 0 0 auto; } + + .blank-slate--drag { + display: none; + } } /* Column Elements @@ -731,7 +756,8 @@ .cards--on-deck { --card-color: var(--color-ink-light) !important; - .card { + .card, + .blank-slate { --card-color: var(--color-card-complete) !important; } @@ -748,12 +774,13 @@ .cards--maybe.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container, .cards--doing.is-collapsed:has(.bubble:not([hidden])) .cards__transition-container { --bubble-color: var(--card-color, oklch(var(--lch-blue-medium))); + --bubble-opacity: 75%; --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% + color-mix(in srgb, var(--bubble-color) calc(var(--bubble-opacity) / 5), var(--color-canvas)) 50%, + color-mix(in srgb, var(--bubble-color) var(--bubble-opacity), var(--color-canvas)) 100% ); block-size: 1em; border-radius: var(--bubble-shape); @@ -777,6 +804,22 @@ z-index: -1; } } + + @media (max-width: 639px) { + &.cards__expander-title:before { + display: none; + } + } + + html[data-theme="dark"] & { + --bubble-opacity: 100%; + } + + @media (prefers-color-scheme: dark) { + html:not([data-theme]) & { + --bubble-opacity: 100%; + } + } } /* Card column indicators @@ -806,6 +849,7 @@ --btn-background: var(--card-color); color: var(--color-ink-inverted); + opacity: 1 !important; @media (hover: hover) { &:hover { diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index a8956a98e..006cbb985 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -4,7 +4,7 @@ --comment-padding-block: var(--block-space-half); --comment-padding-inline: var(--inline-space-double); --comment-max: 70ch; - --reaction-size: 2rem; + --reaction-size: 2.25rem; display: flex; flex-direction: column; @@ -109,6 +109,13 @@ &:not:has(lexxy-editor) { padding-inline-end: var(--reaction-size); } + + /* Add an empty space so the last line of text doesn't overlap with the reaction button */ + .action-text-content > p:last-child::after { + content: ""; + display: inline-block; + inline-size: var(--reaction-size); + } } .comment__content { @@ -130,6 +137,10 @@ .comment__edit { background-color: var(--color-ink-lightest); + + &:hover { + z-index: 1; + } } .comment__permalink-title { diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index ada7324d2..91ff5750d 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -30,7 +30,7 @@ } &:has(.card--notification) { - .notifications-list__empty-message { + .notifications-list__blank-slate { display: none; } } diff --git a/app/assets/stylesheets/search.css b/app/assets/stylesheets/search.css index fcd4563ca..5691ac5e5 100644 --- a/app/assets/stylesheets/search.css +++ b/app/assets/stylesheets/search.css @@ -71,7 +71,7 @@ summary { text-align: start; } - .search__empty { + .search__blank-slate { margin-block: 3em; margin-inline: auto; inline-size: fit-content; diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index aa19f0f58..76dedfe12 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -24,6 +24,7 @@ .txt-underline { text-decoration: underline; } .txt-tight-lines { line-height: 1.2; } .txt-nowrap { white-space: nowrap; } + .txt-balance { text-wrap: balance; } .txt-break { word-break: break-word; } .txt-uppercase { text-transform: uppercase; } .txt-capitalize { text-transform: capitalize; } diff --git a/app/controllers/notifications/unsubscribes_controller.rb b/app/controllers/notifications/unsubscribes_controller.rb index f7486d91e..d1842b169 100644 --- a/app/controllers/notifications/unsubscribes_controller.rb +++ b/app/controllers/notifications/unsubscribes_controller.rb @@ -1,6 +1,6 @@ class Notifications::UnsubscribesController < ApplicationController allow_unauthenticated_access - skip_before_action :verify_authenticity_token + skip_forgery_protection before_action :set_user diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb index a92fb2824..b3e98b30e 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -5,6 +5,7 @@ module ColumnsHelper card_triage_path(card, column_id: column), method: :post, class: [ "card__column-name btn", { "card__column-name--current": column == card.column && card.open? } ], + disabled: column == card.column, style: "--column-color: #{column.color}", form_class: "flex gap-half", data: { turbo_frame: "_top", scroll_to_target: column == card.column && card.open? ? "target" : nil } diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 1a9ba4174..e432a054f 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -14,13 +14,12 @@ export default class extends Controller { } async connect() { - await this.#restoreColumnsDisablingTransitions() - this.#setupIntersectionObserver() - this.mediaQuery = window.matchMedia(this.desktopBreakpointValue) this.handleDesktop = this.#handleDesktop.bind(this) this.mediaQuery.addEventListener("change", this.handleDesktop) - this.handleDesktop(this.mediaQuery) + + await this.#restoreColumnsDisablingTransitions() + this.#setupIntersectionObserver() } disconnect() { @@ -57,6 +56,7 @@ export default class extends Controller { async #restoreColumnsDisablingTransitions() { this.#disableTransitions() this.#restoreColumns() + this.#handleDesktop() await nextFrame() this.#enableTransitions() diff --git a/app/models/board/accessible.rb b/app/models/board/accessible.rb index 2f668ae4f..e5b86dbe1 100644 --- a/app/models/board/accessible.rb +++ b/app/models/board/accessible.rb @@ -46,6 +46,7 @@ module Board::Accessible mentions_for_user(user).destroy_all notifications_for_user(user).destroy_all watches_for(user).destroy_all + pins_for(user).destroy_all end def watchers @@ -99,4 +100,8 @@ module Board::Accessible def watches_for(user) Watch.where(card: cards, user: user) end + + def pins_for(user) + Pin.where(card: cards, user: user) + end end diff --git a/app/views/boards/columns/_empty_placeholder.html.erb b/app/views/boards/columns/_empty_placeholder.html.erb index d9da8a896..21e709b84 100644 --- a/app/views/boards/columns/_empty_placeholder.html.erb +++ b/app/views/boards/columns/_empty_placeholder.html.erb @@ -1,4 +1,2 @@ -
- +Fizzy doesn’t let stale cards stick around forever. Cards automatically move to “Not now” if no one updates, comments, or moves a card for…
- <%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(@board) %> + <%= render "entropy/auto_close", model: board, url: board_entropy_path(board), disabled: !Current.user.can_administer_board?(board) %>+
Then enter the verification code included in the email below:
The code sent to <%= email_address_pending_authentication %> will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.
+The code sent to <%= email_address_pending_authentication %> will work for <%= distance_of_time_in_words(MagicLink::EXPIRATION_TIME) %>.
To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <%= format_currency(Plan.paid.price) %>/month for unlimited cards, unlimited users, and <%= storage_to_human_size(Plan.paid.storage_limit) %> of storage.
+To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <%= format_currency(Plan.paid.price) %>/month for unlimited cards, unlimited users, and <%= storage_to_human_size(Plan.paid.storage_limit) %> of storage.
<%= button_to "Upgrade to #{Plan.paid.name} for #{ format_currency(Plan.paid.price) }/month", account_subscription_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %> diff --git a/saas/config/deploy.beta.yml b/saas/config/deploy.beta.yml index 14bf89698..d5fce9d32 100644 --- a/saas/config/deploy.beta.yml +++ b/saas/config/deploy.beta.yml @@ -75,6 +75,7 @@ ssh: env: clear: APP_FQDN: beta<%= @beta_number %>.fizzy-beta.com + CACHE_NAMESPACE: <%= @beta_number %> RAILS_ENV: beta RAILS_LOG_LEVEL: fatal # suppress unstructured log lines MYSQL_DATABASE_HOST: fizzy-mysql-primary diff --git a/script/migrations/backfill-storage-ledger.rb b/script/migrations/backfill-storage-ledger.rb index 5d89342fc..aa3ec793f 100644 --- a/script/migrations/backfill-storage-ledger.rb +++ b/script/migrations/backfill-storage-ledger.rb @@ -10,17 +10,19 @@ # # Safe to re-run: skips attachments that already have entries (by blob_id + recordable). # -# IMPORTANT: Before running in production, verify zero historic blob reuse in tracked contexts: +# OPTIONAL: If you want to enforce no-reuse for direct attachments, verify there are +# no existing violations (ActionText embeds may legitimately reuse blobs): # # ActiveStorage::Attachment # .joins(:blob) # .where(record_type: Storage::TRACKED_RECORD_TYPES) +# .where.not(record_type: "ActionText::RichText") # .where.not(active_storage_blobs: { account_id: Storage::TEMPLATE_ACCOUNT_ID }) # .select(:blob_id) # .group(:blob_id) # .having("COUNT(*) > 1") # .count -# # Should return empty hash if no reuse exists in tracked contexts +# # Should return empty hash if no direct-attachment reuse exists # # If reuse exists (excluding template blobs), fix the data first. class BackfillStorageLedger diff --git a/test/controllers/searches_controller_test.rb b/test/controllers/searches_controller_test.rb index 14bd91e64..70ec15da9 100644 --- a/test/controllers/searches_controller_test.rb +++ b/test/controllers/searches_controller_test.rb @@ -39,7 +39,7 @@ class SearchesControllerTest < ActionDispatch::IntegrationTest # Searching with non-existent card id get search_path(q: "999999", script_name: "/#{@account.external_account_id}") assert_select "form[data-controller='auto-submit']", count: 0 - assert_select ".search__empty", text: "No matches" + assert_select ".search__blank-slate", text: "No matches" end test "search highlights matched terms with proper HTML marks" do diff --git a/test/models/access_test.rb b/test/models/access_test.rb index 10787e5d3..b4580420d 100644 --- a/test/models/access_test.rb +++ b/test/models/access_test.rb @@ -80,4 +80,27 @@ class AccessTest < ActiveSupport::TestCase assert_not card.watched_by?(kevin) end + + test "pins are destroyed when access is lost" do + kevin = users(:kevin) + board = boards(:writebook) + card = cards(:logo) # Kevin has pinned this card + + other_board = boards(:miltons_wish_list) + other_card = cards(:radio) + other_board.accesses.grant_to(kevin) + other_card.pin_by(kevin) + + assert card.pinned_by?(kevin) + assert other_card.pinned_by?(kevin) + + kevin_access = accesses(:writebook_kevin) + + perform_enqueued_jobs only: Board::CleanInaccessibleDataJob do + kevin_access.destroy + end + + assert_not card.pinned_by?(kevin) + assert other_card.pinned_by?(kevin), "Pin on other board should not be destroyed" + end end diff --git a/test/models/storage/no_reuse_test.rb b/test/models/storage/no_reuse_test.rb index 5af07511f..e876fe50c 100644 --- a/test/models/storage/no_reuse_test.rb +++ b/test/models/storage/no_reuse_test.rb @@ -33,6 +33,83 @@ class Storage::NoReuseTest < ActiveSupport::TestCase assert_equal 1, ActiveStorage::Attachment.where(blob_id: blob.id).count end + test "allows reuse for ActionText embeds" do + file = file_fixture("moon.jpg") + blob = ActiveStorage::Blob.create_and_upload! \ + io: file.open, + filename: "embed.jpg", + content_type: "image/jpeg" + + embed_html = ActionText::Attachment.from_attachable(blob).to_html + + card1 = @board.cards.create!(title: "Card 1", creator: users(:david)) + card1.update!(description: "#{embed_html}
") + card1.reload + + card2 = @board.cards.create!(title: "Card 2", creator: users(:david)) + card2.update!(description: "#{embed_html}
") + card2.reload + + assert_equal 2, ActiveStorage::Attachment.where( + record_type: "ActionText::RichText", + name: "embeds", + blob_id: blob.id + ).count + end + + test "purge_later does not purge blob when still attached elsewhere" do + file = file_fixture("moon.jpg") + blob = ActiveStorage::Blob.create_and_upload! \ + io: file.open, + filename: "embed.jpg", + content_type: "image/jpeg" + + embed_html = ActionText::Attachment.from_attachable(blob).to_html + + card1 = @board.cards.create!(title: "Card 1", creator: users(:david)) + card1.update!(description: "#{embed_html}
") + + card2 = @board.cards.create!(title: "Card 2", creator: users(:david)) + card2.update!(description: "#{embed_html}
") + + attachment = ActiveStorage::Attachment.find_by( + record: card1.rich_text_description, + name: "embeds", + blob_id: blob.id + ) + + assert_no_enqueued_jobs only: ActiveStorage::PurgeJob do + attachment.purge_later + end + + assert ActiveStorage::Blob.exists?(blob.id) + assert_equal 1, ActiveStorage::Attachment.where(blob_id: blob.id).count + end + + test "purge_later enqueues purge when last attachment is removed" do + file = file_fixture("moon.jpg") + blob = ActiveStorage::Blob.create_and_upload! \ + io: file.open, + filename: "embed.jpg", + content_type: "image/jpeg" + + embed_html = ActionText::Attachment.from_attachable(blob).to_html + + card = @board.cards.create!(title: "Card", creator: users(:david)) + card.update!(description: "#{embed_html}
") + card.reload + + attachment = ActiveStorage::Attachment.find_by( + record: card.rich_text_description, + name: "embeds", + blob_id: blob.id + ) + + assert_enqueued_with job: ActiveStorage::PurgeJob, args: [ blob ] do + attachment.purge_later + end + end + test "rejects cross-account blob attachment" do other_account = Account.create!(name: "Other") other_board = other_account.boards.create!(name: "Other Board", creator: users(:david))