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/card-columns.css b/app/assets/stylesheets/card-columns.css index 4edb06c64..2c0c67ef9 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -849,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 8fef15ebb..006cbb985 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -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 { 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/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 420dbe5b6..55d8f06dd 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -8,7 +8,7 @@ class Users::AvatarsController < ApplicationController if @user.system? redirect_to view_context.image_path("system_user.png") elsif @user.avatar.attached? - redirect_to rails_blob_url(@user.avatar_thumbnail, disposition: "inline") + redirect_to rails_blob_path(@user.avatar_thumbnail, disposition: "inline") elsif stale? @user, cache_control: cache_control render_initials end diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 7c064a650..8ae4346ac 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -42,6 +42,6 @@ module AvatarsHelper end def avatar_image_tag(user, **options) - image_tag user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options + image_tag user_avatar_path(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options end end 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/active_storage/blobs/web/_representation.html.erb b/app/views/active_storage/blobs/web/_representation.html.erb index ca934f469..82a1a2d96 100644 --- a/app/views/active_storage/blobs/web/_representation.html.erb +++ b/app/views/active_storage/blobs/web/_representation.html.erb @@ -12,14 +12,14 @@ height: height %> <% elsif blob.audio? %> <% elsif blob.variable? %> - <%= link_to url_for(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %> - <%= image_tag url_for(blob.variant(variant)), width: width, height: height %> + <%= link_to rails_representation_path(blob.variant(variant)), data: { lightbox_target: "image", lightbox_caption_value: blob.filename.to_s } do %> + <%= image_tag rails_representation_path(blob.variant(variant)), width: width, height: height %> <% end %> <% elsif blob.previewable? %> - <%= image_tag url_for(blob.preview(variant)), width: width, height: height %> + <%= image_tag rails_representation_path(blob.preview(variant)), width: width, height: height %> <% else %> <%= blob.filename.extension&.downcase.presence || "unknown" %> <% end %> diff --git a/app/views/boards/edit/_auto_close.html.erb b/app/views/boards/edit/_auto_close.html.erb index c32c1c050..8993c132e 100644 --- a/app/views/boards/edit/_auto_close.html.erb +++ b/app/views/boards/edit/_auto_close.html.erb @@ -1,7 +1,7 @@ -<%= turbo_frame_tag @board, :entropy do %> +<%= turbo_frame_tag board, :entropy do %>

Auto close

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) %>
<% end %> diff --git a/app/views/cards/columns/edit.html.erb b/app/views/cards/columns/edit.html.erb index 586fac20f..162bfbbeb 100644 --- a/app/views/cards/columns/edit.html.erb +++ b/app/views/cards/columns/edit.html.erb @@ -5,6 +5,7 @@ <%= button_to "Not now", card_not_now_path(@card), class: [ "card__column-name btn", { "card__column-name--current": @card.postponed? } ], style: "--column-color: var(--color-card-complete)", + disabled: @card.postponed?, role: "radio", aria: { checked: @card.postponed? }, data: { scroll_to_target: @card.postponed? ? "target" : nil }, @@ -12,7 +13,8 @@ <%= button_to "Maybe?", card_triage_path(@card), method: :delete, class: [ "card__column-name card__column-name--stream btn", { "card__column-name--current": @card.awaiting_triage? } ], - style: "--column-color: var(--color-card-default)", + style: "--column-color: var(--color-card-default)", + disabled: @card.awaiting_triage?, role: "radio", aria: { checked: @card.awaiting_triage? }, data: { scroll_to_target: @card.awaiting_triage? ? "target" : nil }, @@ -25,6 +27,7 @@ <%= button_to "Done", card_closure_path(@card), class: [ "card__column-name btn", { "card__column-name--current": @card.closed? } ], style: "--column-color: var(--color-card-complete)", + disabled: @card.closed?, role: "radio", aria: { checked: @card.closed? }, data: { scroll_to_target: @card.closed? ? "target" : nil }, diff --git a/app/views/events/event/attachments/_attachment.html.erb b/app/views/events/event/attachments/_attachment.html.erb index c1eb254d0..fdb3b871b 100644 --- a/app/views/events/event/attachments/_attachment.html.erb +++ b/app/views/events/event/attachments/_attachment.html.erb @@ -3,9 +3,9 @@ <% height = attachment.metadata["height"] %> <% if attachment.previewable? %> - <%= image_tag url_for(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %> + <%= image_tag rails_representation_path(attachment.preview(variant)), class: "attachment attachment--image", width: width, height: height %> <% elsif attachment.variable? %> - <%= image_tag url_for(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %> + <%= image_tag rails_representation_path(attachment.variant(variant)), class: "attachment attachment--image", width: width, height: height %> <% else %>
<%= attachment.filename.extension&.downcase.presence || "unknown" %> diff --git a/app/views/my/menus/_accounts.html.erb b/app/views/my/menus/_accounts.html.erb index 2fdf9d8a5..6a25fb233 100644 --- a/app/views/my/menus/_accounts.html.erb +++ b/app/views/my/menus/_accounts.html.erb @@ -3,7 +3,7 @@ <%= collapsible_nav_section "Accounts" do %> <%# Bust cache 1 Dec 2025 %> <% accounts.each do |account| %> - <%= filter_place_menu_item landing_url(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %> + <%= filter_place_menu_item landing_path(script_name: account.slug), account.name, "marker", current: account == Current.account, turbo: false %> <% end %> <% end %> <% end %> diff --git a/app/views/my/menus/_settings.html.erb b/app/views/my/menus/_settings.html.erb index 150b6794a..d1251b7c0 100644 --- a/app/views/my/menus/_settings.html.erb +++ b/app/views/my/menus/_settings.html.erb @@ -6,7 +6,7 @@ <%= 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 %> + <%= button_to session_path(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %> Sign out <% end %> <% end %> diff --git a/app/views/prompts/cards/_card.html.erb b/app/views/prompts/cards/_card.html.erb index 2308bc0c7..1dd4494c4 100644 --- a/app/views/prompts/cards/_card.html.erb +++ b/app/views/prompts/cards/_card.html.erb @@ -3,6 +3,6 @@ #<%= card.number %> <%= card.title %> diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb index c4940e146..aba8411f5 100644 --- a/app/views/sessions/magic_links/show.html.erb +++ b/app/views/sessions/magic_links/show.html.erb @@ -3,7 +3,7 @@
">

<%= @page_title %>

-

+

Then enter the verification code included in the email below:

@@ -15,7 +15,7 @@ data: { magic_link_target: "input", action: "keydown.enter->magic-link#submitOnEnter paste->magic-link#submitOnPaste" } %> <% end %> -

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) %>.

<% if Rails.env.development? && flash[:magic_link_code].present? %> diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index 9c6867740..09383b72a 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -13,7 +13,7 @@ <% @accounts.each do |account| %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 7cf5bdf7c..771a8e924 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -41,7 +41,7 @@ <% if Current.user == @user %> - <%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %> + <%= button_to session_path(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %> Sign out of Fizzy on this device <% end %> <% end %> diff --git a/app/views/webhooks/event.html.erb b/app/views/webhooks/event.html.erb index f2920e8d0..7766020a1 100644 --- a/app/views/webhooks/event.html.erb +++ b/app/views/webhooks/event.html.erb @@ -1,4 +1,4 @@ <%= @event.description_for(Current.user).to_plain_text %> <% if @event.eventable %> -<%= link_to "↗︎", polymorphic_url(@event.eventable) %> +<%= link_to "↗︎", polymorphic_url(@event.eventable, only_path: true) %> <% end %> diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index 3edd75cc8..586c292a1 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -8,6 +8,17 @@ ActiveSupport.on_load(:active_storage_blob) do end end +ActiveSupport.on_load(:action_text_content) do + # Install our extensions after ActionText::Engine's + ActiveSupport.on_load(:active_storage_blob) do + # Ensure all s have a "url" attribute that's a relative + # path (for portability across host name changes, beta environments, etc). + def to_rich_text_attributes(*) + super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true) + end + end +end + # Don't configure replica connections for ActiveStorage::Record. # When ActiveStorage uses `connects_to`, it creates a separate connection pool # from ApplicationRecord. This causes after_commit callbacks to fire in diff --git a/script/migrations/convert-absolute-attachment-urls-to-relative.rb b/script/migrations/convert-absolute-attachment-urls-to-relative.rb new file mode 100644 index 000000000..009b6c4cb --- /dev/null +++ b/script/migrations/convert-absolute-attachment-urls-to-relative.rb @@ -0,0 +1,132 @@ +#!/usr/bin/env ruby + +# Convert absolute attachment URLs in rich text content to relative paths. +# This fixes URLs that were stored with full hostnames (e.g., https://app.fizzy.do/...) +# making them portable across beta environments and host changes. +# +# MUST BE RUN AFTER `decrypt!` when using ActiveRecord Encryption +# +# Run locally: +# bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help +# +# Run via Kamal: +# kamal app exec -d -p --reuse "bin/rails runner script/migrations/convert-absolute-attachment-urls-to-relative.rb --help" +# +# Safe to re-run: won't modify already-relative URLs + +class ConvertAbsoluteAttachmentUrlsToRelative + # Match absolute URLs pointing to Active Storage routes, keeping the account slug + ABSOLUTE_URL_PATTERN = %r{https?://[^/]+(/\d+/rails/active_storage/[^"']+)} + + attr_reader :account, :dry_run + + def initialize(account_id: nil, dry_run: false) + @account = Account.find_by(external_account_id: account_id) + @dry_run = dry_run + end + + def run + puts "Converting absolute attachment URLs to relative paths" + puts dry_run ? "DRY RUN MODE - no changes will be saved" : "LIVE MODE - changes will be saved" + puts account ? "Only account: #{account.external_account_id} - #{account.name}" : "For **ALL ACCOUNTS**" + + puts "\nPress ENTER to continue running or CTRL-C to bail..." + gets + + puts "\nRunning..." + + # Suppress SQL logs + Rails.event.debug_mode = false + + seconds = Benchmark.realtime do + suppressing_turbo_broadcasts do + convert_urls + end + end + + puts "\n\n" + puts "Finished in %.2f seconds." % seconds + end + + private + def suppressing_turbo_broadcasts + Board.suppressing_turbo_broadcasts do + Card.suppressing_turbo_broadcasts do + yield + end + end + end + + def convert_urls + scanned = 0 + fixed = 0 + urls_converted = 0 + + action_texts_scope.find_each do |rich_text| + scanned += 1 + + body = rich_text.body + + edited = false + conversions = 0 + + body.send(:attachment_nodes).each do |node| + url = node["url"] + next unless url + + if url.match?(ABSOLUTE_URL_PATTERN) + node["url"] = url.gsub(ABSOLUTE_URL_PATTERN, '\1') + edited = true + conversions += 1 + end + end + + if edited + record = rich_text.record + puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)" + + unless dry_run + rich_text.update! body: body.fragment.to_html + end + + fixed += 1 + urls_converted += conversions + end + end + + puts "\n\nConversion complete!" + puts " Rich texts examined: #{scanned}" + puts " Rich texts modified: #{fixed}" + puts " URLs converted: #{urls_converted}" + end + + def action_texts_scope + # Only examine rich texts that have embedded attachments + scope = ActionText::RichText.joins(:embeds_attachments) + scope = scope.where(account: account) if account + scope + end +end + +require "optparse" + +options = { account_id: nil, dry_run: true } + +OptionParser.new do |opts| + opts.banner = "Usage: bin/rails runner #{__FILE__} [options]" + + opts.on("-a", "--account ACCOUNT_ID", "Restrict to a specific account (external_account_id)") do |id| + options[:account_id] = id + end + + opts.on("--[no-]dry-run", "Run in dry-run mode (default: --dry-run)") do |v| + options[:dry_run] = v + end + + opts.on("-h", "--help", "Show this help message") do + puts opts + exit + end +end.parse! + +ConvertAbsoluteAttachmentUrlsToRelative.new(**options).run diff --git a/script/migrations/convert-relative-attachment-urls-to-absolute.rb b/script/migrations/convert-relative-attachment-urls-to-absolute.rb new file mode 100644 index 000000000..4b201f34f --- /dev/null +++ b/script/migrations/convert-relative-attachment-urls-to-absolute.rb @@ -0,0 +1,127 @@ +#!/usr/bin/env ruby + +# Rollback script: Convert relative attachment URLs back to absolute URLs. +# Use this if you need to rollback the relative URL changes and want to +# convert rich texts created during the rollout back to absolute URLs. +# +# Run locally: +# bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help +# +# Run via Kamal: +# kamal app exec -d -p --reuse "bin/rails runner script/migrations/convert-relative-attachment-urls-to-absolute.rb --help" + +class ConvertRelativeAttachmentUrlsToAbsolute + # Match relative URLs pointing to Active Storage routes (with account slug) + RELATIVE_URL_PATTERN = %r{\A(/\d+/rails/active_storage/[^"']+)\z} + + attr_reader :host, :since + + def initialize(host:, since:) + @host = host + @since = since + end + + def run + puts "Converting relative attachment URLs to absolute URLs" + puts "Host: #{host}" + puts "Processing rich texts created since: #{since}" + + puts "\nPress ENTER to continue running or CTRL-C to bail..." + gets + + puts "\nRunning..." + + seconds = Benchmark.realtime do + suppressing_turbo_broadcasts do + convert_urls + end + end + + puts "\n\n" + puts "Finished in %.2f seconds." % seconds + end + + private + def suppressing_turbo_broadcasts + Board.suppressing_turbo_broadcasts do + Card.suppressing_turbo_broadcasts do + yield + end + end + end + + def convert_urls + scanned = 0 + fixed = 0 + urls_converted = 0 + + action_texts_scope.find_each do |rich_text| + scanned += 1 + + body = rich_text.body + + edited = false + conversions = 0 + + body.send(:attachment_nodes).each do |node| + url = node["url"] + next unless url + + if url.match?(RELATIVE_URL_PATTERN) + node["url"] = "#{host}#{url}" + edited = true + conversions += 1 + end + end + + if edited + record = rich_text.record + puts " - modifying #{record.class.name} #{record.to_param} (account: #{record.account&.external_account_id}) - #{conversions} URL(s)" + + rich_text.update! body: body.fragment.to_html + + fixed += 1 + urls_converted += conversions + end + end + + puts "\n\nConversion complete!" + puts " Rich texts examined: #{scanned}" + puts " Rich texts modified: #{fixed}" + puts " URLs converted: #{urls_converted}" + end + + def action_texts_scope + ActionText::RichText.joins(:embeds_attachments).where("action_text_rich_texts.created_at >= ?", since) + end +end + +require "optparse" +require "time" + +options = {} + +OptionParser.new do |opts| + opts.banner = "Usage: bin/rails runner #{__FILE__} [options]" + + opts.on("--host HOST", "Host to prepend (e.g., https://app.fizzy.do)") do |host| + options[:host] = host + end + + opts.on("--since TIME", "Process rich texts created since this time (ISO 8601 format)") do |time| + options[:since] = Time.parse(time) + end + + opts.on("-h", "--help", "Show this help message") do + puts opts + exit + end +end.parse! + +if options[:host].nil? || options[:since].nil? + puts "Error: --host and --since are required" + puts "Example: bin/rails runner #{__FILE__} --host https://app.fizzy.do --since 2026-01-14T10:00:00Z" + exit 1 +end + +ConvertRelativeAttachmentUrlsToAbsolute.new(**options).run 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