From 91263556a9df83e3998dfe4efca556b982839894 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 31 Oct 2025 21:57:17 +0100 Subject: [PATCH 001/173] Ensure there is a limit of 100 events per column per day Otherwise these pages can get very heavy --- app/helpers/events_helper.rb | 18 ++++++++++++------ app/views/events/day_timeline/_column.html.erb | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 14c12c5cd..8bd479784 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,24 +3,30 @@ module EventsHelper 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", events.count, day_timeline.day), + title: event_column_title("Added", full_events_count, day_timeline.day), index: 1, - events: events + 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", events.count, day_timeline.day), + title: event_column_title("Closed", full_events_count, day_timeline.day), index: 3, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } else events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ]) + full_events_count = events.count { - title: event_column_title("Updated", events.count, day_timeline.day), + title: event_column_title("Updated", full_events_count, day_timeline.day), index: 2, - events: events + events: events.limit(100).load, + full_events_count: full_events_count } end end diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index a1186da6b..2249766c6 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -8,4 +8,9 @@ <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> <% end %> <% end %> + + <%# FIXME: Need to style this cut-off %> + <% if column[:events].count < column[:full_events_count] %> +

...and <%= column[:full_events_count] - column[:events].count %> more

+ <% end %> From 4b7cd8828fb1c4a7ad68af5fb347aeb9ec3aef21 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Fri, 31 Oct 2025 16:58:21 -0500 Subject: [PATCH 002/173] Style notice --- app/views/events/day_timeline/_column.html.erb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index 2249766c6..ab7f0aa15 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -9,8 +9,7 @@ <% end %> <% end %> - <%# FIXME: Need to style this cut-off %> <% if column[:events].count < column[:full_events_count] %> -

...and <%= column[:full_events_count] - column[:events].count %> more

+
Showing the first 100 items (<%= column[:full_events_count] - column[:events].count %> are hidden)
From e726335833db3f89e0241a2540d2dc24cd471ba3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 00:05:38 +0100 Subject: [PATCH 003/173] WIP for seeding accounts --- Gemfile.lock | 2 +- app/models/account.rb | 4 ++ app/models/account/seeder.rb | 77 ++++++++++++++++++++++++++++ gems/fizzy-saas/app/models/signup.rb | 2 +- lib/tasks/seed.rake | 13 +++++ 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 app/models/account/seeder.rb create mode 100644 lib/tasks/seed.rake diff --git a/Gemfile.lock b/Gemfile.lock index eb6ae5aa8..3c76704a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -296,7 +296,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.13.beta) + lexxy (0.1.14.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) diff --git a/app/models/account.rb b/app/models/account.rb index 357d0cc29..edd75655c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -31,6 +31,10 @@ class Account < ApplicationRecord Collection.create!(name: "Cards", creator: user, all_access: true) end + def setup_customer_template + Account::Seeder.new(self, User.first).seed + end + private def create_join_code Account::JoinCode.create! diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb new file mode 100644 index 000000000..f2d01919e --- /dev/null +++ b/app/models/account/seeder.rb @@ -0,0 +1,77 @@ +class Account::Seeder + attr_reader :account, :creator + + def initialize(account, creator) + @account = account + @creator = creator + + puts creator.inspect + end + + def seed + Current.set session: session do + populate + end + end + + def seed! + raise "You can't run in production environments" unless Rails.env.local? + + delete_everything + seed + end + + private + def session + creator.identity.sessions.last + end + + def populate + # --------------- + # Bugs Collection + # --------------- + bug_tracker = Collection.create!(name: "Bugs", creator: creator, all_access: true) + + # Columns + triage_column = bug_tracker.columns.create!(name: "Triage", color: "#ef4444") + in_progress_column = bug_tracker.columns.create!(name: "In Progress", color: "#f97316") + resolved_column = bug_tracker.columns.create!(name: "Resolved", color: "#22c55e") + + # Cards + bug_tracker.cards.create!(creator: creator, column: triage_column, title: "Login button not responding on mobile", status: "published") + bug_tracker.cards.create!(creator: creator, column: triage_column, title: "Search results showing duplicates", status: "published") + profile_crash_card = bug_tracker.cards.create!(creator: creator, column: in_progress_column, title: "Profile page crashes when uploading large images", status: "published") + bug_tracker.cards.create!(creator: creator, column: in_progress_column, title: "Email notifications not being sent", status: "published") + bug_tracker.cards.create!(creator: creator, column: resolved_column, title: "Fix broken links in footer", status: "published") + + # Comments + profile_crash_card.comments.create!(creator: creator, body: "I can reproduce this with images over 5MB") + profile_crash_card.comments.create!(creator: creator, body: "Looking into adding client-side image compression before upload") + + # ---------------------------- + # Feature Requests Collection + # ---------------------------- + feature_requests = Collection.create!(name: "Feature Requests", creator: creator, all_access: true) + + # Columns + backlog_column = feature_requests.columns.create!(name: "Backlog", color: "#6366f1") + planning_column = feature_requests.columns.create!(name: "Planning", color: "#8b5cf6") + + # Cards + feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Add dark mode support", status: "published") + feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Export data to CSV", status: "published") + feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Add keyboard shortcuts for navigation", status: "published") + two_factor_card = feature_requests.cards.create!(creator: creator, column: planning_column, title: "Implement two-factor authentication", status: "published") + feature_requests.cards.create!(creator: creator, column: planning_column, title: "Add bulk actions for managing items", status: "published") + + # Comments + two_factor_card.comments.create!(creator: creator, body: "Should we support SMS and authenticator apps?") + two_factor_card.comments.create!(creator: creator, body: "Let's start with authenticator apps only to keep it simple") + end + + def delete_everything + Current.set session: session do + Collection.destroy_all + end + end +end diff --git a/gems/fizzy-saas/app/models/signup.rb b/gems/fizzy-saas/app/models/signup.rb index 58ee5775a..ed9b94528 100644 --- a/gems/fizzy-saas/app/models/signup.rb +++ b/gems/fizzy-saas/app/models/signup.rb @@ -116,7 +116,7 @@ class Signup } ) @user = User.find_by!(role: :admin) - @account.setup_basic_template + @account.setup_customer_template end end diff --git a/lib/tasks/seed.rake b/lib/tasks/seed.rake new file mode 100644 index 000000000..97f28f233 --- /dev/null +++ b/lib/tasks/seed.rake @@ -0,0 +1,13 @@ +namespace :seed do + desc "Seed customer data for a specific account (e.g., rails seed:customer 1234)" + task :customer, [:tenant_id] => :environment do |t, args| + raise "Please provide a tenant ID: rails seed:customer[1234]" unless args[:tenant_id] + + tenant_id = args[:tenant_id].to_i + ApplicationRecord.current_tenant = tenant_id + account = Account.sole + Account::Seeder.new(account, User.first).seed! + + puts "✓ Seeded account #{account.name} (tenant: #{tenant_id})" + end +end From 3bba72bf191fed66c0d158b4124f317c9bdf470f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:19:22 +0100 Subject: [PATCH 004/173] Wait a little to ensure attachments have loaded --- test/system/smoke_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 870438f72..8c80c165e 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -61,8 +61,8 @@ class SmokeTest < ApplicationSystemTestCase def assert_image_figure_attachment(content_type: "image/png", caption:) assert_figure_attachment(content_type: content_type) do - assert_selector("img[src*='/rails/active_storage']") - assert_selector "figcaption input[placeholder='#{caption}']" + assert_selector "img[src*='/rails/active_storage']", wait: 5 + assert_selector "figcaption input[placeholder='#{caption}']", wait: 5 end end end From d6deb3ac5f7b672512e604ffd95360653692ac3d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:22:32 +0100 Subject: [PATCH 005/173] Setup signoff too --- bin/setup | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/setup b/bin/setup index 90d68e323..61ab69995 100755 --- a/bin/setup +++ b/bin/setup @@ -62,6 +62,9 @@ if which pacman >/dev/null 2>&1; then fi fi +# Ensure gh-signoff is installed and up to date +step "Set up gh-signoff" bash -c "gh extension install basecamp/gh-signoff || gh extension upgrade basecamp/gh-signoff" + bundle config set --local auto_install true step "Installing RubyGems" bundle install From e6459cede3bbcda6a91795771349859397163b39 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 08:34:04 +0100 Subject: [PATCH 006/173] Use existing authorization method --- app/controllers/account/settings_controller.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index 9acebbae4..c2f9672c9 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -1,16 +1,14 @@ class Account::SettingsController < ApplicationController + before_action :ensure_admin, only: :update + def show @account = Account.sole @users = User.active.alphabetically end def update - if Current.user.can_administer? - Account.sole.update!(account_params) - redirect_to account_settings_path - else - head :forbidden - end + Account.sole.update!(account_params) + redirect_to account_settings_path end private From ac507aa754f80ad77b4c4f4f32b0516e33273e56 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 09:24:38 +0100 Subject: [PATCH 007/173] Add support for remote images in the seeding system --- app/models/account/seeder.rb | 70 +++++++++++++------ app/models/concerns/attachments.rb | 8 +++ .../attachables/_remote_image.html.erb | 8 +++ app/views/events/event/_attachments.html.erb | 6 +- lib/tasks/seed.rake | 2 +- 5 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 app/views/action_text/attachables/_remote_image.html.erb diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index f2d01919e..989b56852 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -30,43 +30,71 @@ class Account::Seeder # --------------- # Bugs Collection # --------------- - bug_tracker = Collection.create!(name: "Bugs", creator: creator, all_access: true) + bug_tracker = Collection.create! name: "Bugs", creator: creator, all_access: true # Columns - triage_column = bug_tracker.columns.create!(name: "Triage", color: "#ef4444") - in_progress_column = bug_tracker.columns.create!(name: "In Progress", color: "#f97316") - resolved_column = bug_tracker.columns.create!(name: "Resolved", color: "#22c55e") + triage_column = bug_tracker.columns.create! name: "Triage", color: "#ef4444" + in_progress_column = bug_tracker.columns.create! name: "In Progress", color: "#f97316" + resolved_column = bug_tracker.columns.create! name: "Resolved", color: "#22c55e" # Cards - bug_tracker.cards.create!(creator: creator, column: triage_column, title: "Login button not responding on mobile", status: "published") - bug_tracker.cards.create!(creator: creator, column: triage_column, title: "Search results showing duplicates", status: "published") - profile_crash_card = bug_tracker.cards.create!(creator: creator, column: in_progress_column, title: "Profile page crashes when uploading large images", status: "published") - bug_tracker.cards.create!(creator: creator, column: in_progress_column, title: "Email notifications not being sent", status: "published") - bug_tracker.cards.create!(creator: creator, column: resolved_column, title: "Fix broken links in footer", status: "published") + bug_tracker.cards.create! creator: creator, column: triage_column, title: "Login button not responding on mobile", status: "published", description: <<~HTML +

Users are reporting that the login button is not responding on mobile devices.

+ +

Steps to reproduce:

+
    +
  • Open the app on mobile browser
  • +
  • Navigate to login page
  • +
  • Tap the login button
  • +
  • Nothing happens
  • +
+ +

This appears to be affecting iOS devices primarily.

+ + + HTML + bug_tracker.cards.create! creator: creator, column: triage_column, title: "Search results showing duplicates", status: "published" + profile_crash_card = bug_tracker.cards.create! creator: creator, column: in_progress_column, title: "Profile page crashes when uploading large images", status: "published" + bug_tracker.cards.create! creator: creator, column: in_progress_column, title: "Email notifications not being sent", status: "published" + bug_tracker.cards.create! creator: creator, column: resolved_column, title: "Fix broken links in footer", status: "published" # Comments - profile_crash_card.comments.create!(creator: creator, body: "I can reproduce this with images over 5MB") - profile_crash_card.comments.create!(creator: creator, body: "Looking into adding client-side image compression before upload") + profile_crash_card.comments.create! creator: creator, body: "I can reproduce this with images over 5MB" + profile_crash_card.comments.create! creator: creator, body: "Looking into adding client-side image compression before upload" # ---------------------------- # Feature Requests Collection # ---------------------------- - feature_requests = Collection.create!(name: "Feature Requests", creator: creator, all_access: true) + feature_requests = Collection.create! name: "Feature Requests", creator: creator, all_access: true # Columns - backlog_column = feature_requests.columns.create!(name: "Backlog", color: "#6366f1") - planning_column = feature_requests.columns.create!(name: "Planning", color: "#8b5cf6") + backlog_column = feature_requests.columns.create! name: "Backlog", color: "#6366f1" + planning_column = feature_requests.columns.create! name: "Planning", color: "#8b5cf6" # Cards - feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Add dark mode support", status: "published") - feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Export data to CSV", status: "published") - feature_requests.cards.create!(creator: creator, column: backlog_column, title: "Add keyboard shortcuts for navigation", status: "published") - two_factor_card = feature_requests.cards.create!(creator: creator, column: planning_column, title: "Implement two-factor authentication", status: "published") - feature_requests.cards.create!(creator: creator, column: planning_column, title: "Add bulk actions for managing items", status: "published") + feature_requests.cards.create! creator: creator, column: backlog_column, title: "Add dark mode support", status: "published", description: <<~HTML +

Many users have requested a dark mode option for better usability in low-light environments.

+ +

Proposed features:

+
    +
  • Toggle in user settings
  • +
  • Respect system preferences automatically
  • +
  • Smooth transition between themes
  • +
  • Persistent theme selection across sessions
  • +
+ +

This would improve accessibility and reduce eye strain for our users.

+ + + HTML + feature_requests.cards.create! creator: creator, column: backlog_column, title: "Export data to CSV", status: "published" + feature_requests.cards.create! creator: creator, column: backlog_column, title: "Add keyboard shortcuts for navigation", status: "published" + two_factor_card = feature_requests.cards.create! creator: creator, column: planning_column, title: "Implement two-factor authentication", status: "published" + feature_requests.cards.create! creator: creator, column: planning_column, title: "Add bulk actions for managing items", status: "published" # Comments - two_factor_card.comments.create!(creator: creator, body: "Should we support SMS and authenticator apps?") - two_factor_card.comments.create!(creator: creator, body: "Let's start with authenticator apps only to keep it simple") + two_factor_card.comments.create! creator: creator, body: "Should we support SMS and authenticator apps?" + two_factor_card.comments.create! creator: creator, body: "Let's start with authenticator apps only to keep it simple" end def delete_everything diff --git a/app/models/concerns/attachments.rb b/app/models/concerns/attachments.rb index f4328600e..1813e61e9 100644 --- a/app/models/concerns/attachments.rb +++ b/app/models/concerns/attachments.rb @@ -28,6 +28,14 @@ module Attachments attachments.any? end + def remote_images + rich_text_record&.body&.attachables&.grep(ActionText::Attachables::RemoteImage) || [] + end + + def has_remote_images? + remote_images.any? + end + private def rich_text_record @rich_text_record ||= begin diff --git a/app/views/action_text/attachables/_remote_image.html.erb b/app/views/action_text/attachables/_remote_image.html.erb new file mode 100644 index 000000000..174a95c5e --- /dev/null +++ b/app/views/action_text/attachables/_remote_image.html.erb @@ -0,0 +1,8 @@ +
+ <%= image_tag remote_image.url, width: remote_image.width, height: remote_image.height %> + <% if caption = remote_image.try(:caption) %> +
+ <%= caption %> +
+ <% end %> +
diff --git a/app/views/events/event/_attachments.html.erb b/app/views/events/event/_attachments.html.erb index 9a255e8cb..05b88e6ba 100644 --- a/app/views/events/event/_attachments.html.erb +++ b/app/views/events/event/_attachments.html.erb @@ -1,4 +1,4 @@ -<% if eventable&.has_attachments? %> +<% if eventable&.has_attachments? || eventable&.has_remote_images? %> <% eventable.attachments.each do |attachment| %> <% variant = Attachments::VARIANTS[:small] %> @@ -15,5 +15,9 @@ <% end %> <% end %> + + <% eventable.remote_images.each do |remote_image| %> + <%= image_tag remote_image.url, class: "attachment attachment--image", width: remote_image.width, height: remote_image.height, alt: remote_image.alt %> + <% end %> <% end %> diff --git a/lib/tasks/seed.rake b/lib/tasks/seed.rake index 97f28f233..31fada3a6 100644 --- a/lib/tasks/seed.rake +++ b/lib/tasks/seed.rake @@ -6,7 +6,7 @@ namespace :seed do tenant_id = args[:tenant_id].to_i ApplicationRecord.current_tenant = tenant_id account = Account.sole - Account::Seeder.new(account, User.first).seed! + Account::Seeder.new(account, User.active.first).seed! puts "✓ Seeded account #{account.name} (tenant: #{tenant_id})" end From 2ec5d3794780a22a9ac103c1753ad915ab76d302 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 09:38:48 +0100 Subject: [PATCH 008/173] Add support for remote videos too --- app/models/account/seeder.rb | 6 +++++- app/models/concerns/attachments.rb | 8 ++++++++ .../action_text/attachables/_remote_video.html.erb | 10 ++++++++++ app/views/events/event/_attachments.html.erb | 10 ++++++++-- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 app/views/action_text/attachables/_remote_video.html.erb diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 989b56852..f9f06cb98 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -59,7 +59,11 @@ class Account::Seeder bug_tracker.cards.create! creator: creator, column: resolved_column, title: "Fix broken links in footer", status: "published" # Comments - profile_crash_card.comments.create! creator: creator, body: "I can reproduce this with images over 5MB" + profile_crash_card.comments.create! creator: creator, body: <<~HTML + I can reproduce this with images over 5MB + + + HTML profile_crash_card.comments.create! creator: creator, body: "Looking into adding client-side image compression before upload" # ---------------------------- diff --git a/app/models/concerns/attachments.rb b/app/models/concerns/attachments.rb index 1813e61e9..d30496165 100644 --- a/app/models/concerns/attachments.rb +++ b/app/models/concerns/attachments.rb @@ -36,6 +36,14 @@ module Attachments remote_images.any? end + def remote_videos + rich_text_record&.body&.attachables&.grep(ActionText::Attachables::RemoteVideo) || [] + end + + def has_remote_videos? + remote_videos.any? + end + private def rich_text_record @rich_text_record ||= begin diff --git a/app/views/action_text/attachables/_remote_video.html.erb b/app/views/action_text/attachables/_remote_video.html.erb new file mode 100644 index 000000000..5233d062a --- /dev/null +++ b/app/views/action_text/attachables/_remote_video.html.erb @@ -0,0 +1,10 @@ +
+ <%= tag.video controls: true, width: remote_video.width, height: remote_video.height do %> + <%= tag.source src: remote_video.url, type: remote_video.content_type %> + <% end %> + <% if caption = remote_video.try(:caption) %> +
+ <%= caption %> +
+ <% end %> +
diff --git a/app/views/events/event/_attachments.html.erb b/app/views/events/event/_attachments.html.erb index 05b88e6ba..aff8316da 100644 --- a/app/views/events/event/_attachments.html.erb +++ b/app/views/events/event/_attachments.html.erb @@ -1,4 +1,4 @@ -<% if eventable&.has_attachments? || eventable&.has_remote_images? %> +<% if eventable&.has_attachments? || eventable&.has_remote_images? || eventable&.has_remote_videos? %> <% eventable.attachments.each do |attachment| %> <% variant = Attachments::VARIANTS[:small] %> @@ -17,7 +17,13 @@ <% end %> <% eventable.remote_images.each do |remote_image| %> - <%= image_tag remote_image.url, class: "attachment attachment--image", width: remote_image.width, height: remote_image.height, alt: remote_image.alt %> + <%= image_tag remote_image.url, class: "attachment attachment--image", width: remote_image.width, height: remote_image.height %> + <% end %> + + <% eventable.remote_videos.each do |remote_video| %> + <%= tag.video controls: true, class: "attachment attachment--video", width: remote_video.width, height: remote_video.height do %> + <%= tag.source src: remote_video.url, type: remote_video.content_type %> + <% end %> <% end %> <% end %> From 4e2f56f4c0b553c88a493a05c0f141cd3ac352d0 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 09:44:50 +0100 Subject: [PATCH 009/173] Extract concern, add tests --- app/models/account.rb | 12 +----------- app/models/account/seedeable.rb | 13 +++++++++++++ app/models/account/seeder.rb | 2 -- test/models/account/seedeable_test.rb | 23 +++++++++++++++++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 app/models/account/seedeable.rb create mode 100644 test/models/account/seedeable_test.rb diff --git a/app/models/account.rb b/app/models/account.rb index edd75655c..e32f0bcdb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -1,5 +1,5 @@ class Account < ApplicationRecord - include Entropic + include Entropic, Seedeable has_many_attached :uploads @@ -25,16 +25,6 @@ class Account < ApplicationRecord "/#{tenant}" end - def setup_basic_template - user = User.first - - Collection.create!(name: "Cards", creator: user, all_access: true) - end - - def setup_customer_template - Account::Seeder.new(self, User.first).seed - end - private def create_join_code Account::JoinCode.create! diff --git a/app/models/account/seedeable.rb b/app/models/account/seedeable.rb new file mode 100644 index 000000000..a4096c28e --- /dev/null +++ b/app/models/account/seedeable.rb @@ -0,0 +1,13 @@ +module Account::Seedeable + extend ActiveSupport::Concern + + def setup_basic_template + user = User.first + + Collection.create!(name: "Cards", creator: user, all_access: true) + end + + def setup_customer_template + Account::Seeder.new(self, User.active.first).seed + end +end diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index f9f06cb98..28e977c29 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -4,8 +4,6 @@ class Account::Seeder def initialize(account, creator) @account = account @creator = creator - - puts creator.inspect end def seed diff --git a/test/models/account/seedeable_test.rb b/test/models/account/seedeable_test.rb new file mode 100644 index 000000000..432482876 --- /dev/null +++ b/test/models/account/seedeable_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class Account::SedeableTest < ActiveSupport::TestCase + setup do + @account = Account.sole + end + + test "setup_basic_template changes collection count" do + assert_changes -> { Collection.count } do + @account.setup_basic_template + end + end + + test "setup_customer_template changes collections, cards, and comments count" do + assert_changes -> { Collection.count } do + assert_changes -> { Card.count } do + assert_changes -> { Comment.count } do + @account.setup_customer_template + end + end + end + end +end From 348e3a890b6664f00945f3e1a3498e6b1d8aee29 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 09:45:11 +0100 Subject: [PATCH 010/173] Fix test --- gems/fizzy-saas/test/models/signup_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/fizzy-saas/test/models/signup_test.rb b/gems/fizzy-saas/test/models/signup_test.rb index 44d801017..685b25b38 100644 --- a/gems/fizzy-saas/test/models/signup_test.rb +++ b/gems/fizzy-saas/test/models/signup_test.rb @@ -54,7 +54,7 @@ class SignupTest < ActiveSupport::TestCase end test "#complete" do - Account.any_instance.expects(:setup_basic_template).once + Account.any_instance.expects(:setup_customer_template).once # First create the membership signup_for_membership = Signup.new( From 72d0008df0c65ed039a015fc3032082c25b15689 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 12:49:17 +0100 Subject: [PATCH 011/173] Only restrict resource actions when there's a specific story to tell --- config/routes.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 9a663d70d..9b7e1ddb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Rails.application.routes.draw do namespace :account do post :enter, to: "entries#create" - resource :join_code, only: %i[ show edit update destroy ] + resource :join_code resource :settings resource :entropy_configuration end @@ -27,11 +27,11 @@ Rails.application.routes.draw do resources :columns end - resources :cards, only: %i[ create ] + resources :cards, only: :create resources :webhooks do scope module: "webhooks" do - resource :activation, only: %i[ create ] + resource :activation, only: :create end end end @@ -58,7 +58,7 @@ Rails.application.routes.draw do resources :previews end - resources :cards, only: %i[ index show edit update destroy ] do + resources :cards do scope module: :cards do resource :goldness resource :image @@ -95,7 +95,7 @@ Rails.application.routes.draw do scope module: :notifications do get "tray", to: "trays#show", on: :collection - resource :reading, only: %i[ create destroy ] + resource :reading collection do resource :bulk_reading, only: :create end @@ -130,9 +130,9 @@ Rails.application.routes.draw do resource :session do scope module: "sessions" do - resources :transfers, only: %i[ show update ] - resource :magic_link, only: %i[ show create ] - resource :menu, only: %i[ show create ] + resources :transfers + resource :magic_link + resource :menu end end @@ -144,17 +144,17 @@ Rails.application.routes.draw do resources :commands - resource :conversation, only: %i[ show create ] do + resource :conversation do scope module: :conversations do - resources :messages, only: %i[ index create ] + resources :messages end end scope module: :memberships, path: "memberships/:membership_id" do - resource :unlink, only: %i[ show create ], controller: :unlink, as: :unlink_membership + resource :unlink, controller: :unlink, as: :unlink_membership - resources :email_addresses, only: %i[ new create ], param: :token do - resource :confirmation, only: %i[ show create ], module: :email_addresses + resources :email_addresses, param: :token do + resource :confirmation, module: :email_addresses end end From d29e4dcdcf44e21d8a3924363392a547579564af Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 12:49:54 +0100 Subject: [PATCH 012/173] No CR when there are only two statements in total --- app/controllers/account/entropy_configurations_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb index 53b1f54e8..9de3ff04c 100644 --- a/app/controllers/account/entropy_configurations_controller.rb +++ b/app/controllers/account/entropy_configurations_controller.rb @@ -1,7 +1,6 @@ class Account::EntropyConfigurationsController < ApplicationController def update Entropy::Configuration.default.update!(entropy_configuration_params) - redirect_to account_settings_path, notice: "Account updated" end From b3474ec97d555a83fc015ec8be4fdf7f44598aa4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:11:41 +0100 Subject: [PATCH 013/173] Rename Entropy::Configuration to just Entropy --- .../account/entropies_controller.rb | 11 +++++++++ .../entropy_configurations_controller.rb | 11 --------- .../collections/entropies_controller.rb | 17 +++++++++++++ .../entropy_configurations_controller.rb | 17 ------------- app/models/account.rb | 2 +- app/models/account/entropic.rb | 8 +++---- app/models/card/entropic.rb | 12 +++++----- app/models/collection.rb | 2 +- app/models/collection/entropic.rb | 10 ++++---- app/models/entropy.rb | 17 ++++++++++--- app/models/entropy/configuration.rb | 16 ------------- ...nfiguration.html.erb => _entropy.html.erb} | 2 +- app/views/account/settings/show.html.erb | 2 +- .../collections/edit/_auto_close.html.erb | 4 ++-- config/routes.rb | 4 ++-- ...ame_entropy_configurations_to_entropies.rb | 5 ++++ db/schema.rb | 6 ++--- db/schema_cache.yml | 16 ++++++------- .../accounts/entropies_controller_test.rb | 15 ++++++++++++ .../entropy_configurations_controller_test.rb | 15 ------------ .../collections/entropies_controller_test.rb | 16 +++++++++++++ .../entropy_configurations_controller_test.rb | 16 ------------- .../collections_controller_test.rb | 2 +- .../configurations.yml => entropies.yml} | 0 test/models/card/entropic_test.rb | 24 +++++++++---------- test/models/card/stallable_test.rb | 2 +- test/models/entropy/configuration_test.rb | 20 ---------------- test/models/entropy_test.rb | 20 ++++++++++++++++ test/routes_test.rb | 4 ++-- 29 files changed, 148 insertions(+), 148 deletions(-) create mode 100644 app/controllers/account/entropies_controller.rb delete mode 100644 app/controllers/account/entropy_configurations_controller.rb create mode 100644 app/controllers/collections/entropies_controller.rb delete mode 100644 app/controllers/collections/entropy_configurations_controller.rb delete mode 100644 app/models/entropy/configuration.rb rename app/views/account/settings/{_entropy_configuration.html.erb => _entropy.html.erb} (89%) create mode 100644 db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb create mode 100644 test/controllers/accounts/entropies_controller_test.rb delete mode 100644 test/controllers/accounts/entropy_configurations_controller_test.rb create mode 100644 test/controllers/collections/entropies_controller_test.rb delete mode 100644 test/controllers/collections/entropy_configurations_controller_test.rb rename test/fixtures/{entropy/configurations.yml => entropies.yml} (100%) delete mode 100644 test/models/entropy/configuration_test.rb create mode 100644 test/models/entropy_test.rb diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb new file mode 100644 index 000000000..75fa01e20 --- /dev/null +++ b/app/controllers/account/entropies_controller.rb @@ -0,0 +1,11 @@ +class Account::EntropiesController < ApplicationController + def update + Entropy.default.update!(entropy_params) + redirect_to account_settings_path, notice: "Account updated" + end + + private + def entropy_params + params.expect(entropy: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/account/entropy_configurations_controller.rb b/app/controllers/account/entropy_configurations_controller.rb deleted file mode 100644 index 9de3ff04c..000000000 --- a/app/controllers/account/entropy_configurations_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Account::EntropyConfigurationsController < ApplicationController - def update - Entropy::Configuration.default.update!(entropy_configuration_params) - redirect_to account_settings_path, notice: "Account updated" - end - - private - def entropy_configuration_params - params.expect(entropy_configuration: [ :auto_postpone_period ]) - end -end diff --git a/app/controllers/collections/entropies_controller.rb b/app/controllers/collections/entropies_controller.rb new file mode 100644 index 000000000..bb0d2940a --- /dev/null +++ b/app/controllers/collections/entropies_controller.rb @@ -0,0 +1,17 @@ +class Collections::EntropiesController < ApplicationController + include CollectionScoped + + def update + @collection.entropy.update!(entropy_params) + + render turbo_stream: [ + turbo_stream.replace([ @collection, :entropy], partial: "collections/edit/auto_close", locals: { collection: @collection }), + turbo_stream_flash(notice: "Saved") + ] + end + + private + def entropy_params + params.expect(collection: [ :auto_postpone_period ]) + end +end diff --git a/app/controllers/collections/entropy_configurations_controller.rb b/app/controllers/collections/entropy_configurations_controller.rb deleted file mode 100644 index 76957627d..000000000 --- a/app/controllers/collections/entropy_configurations_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Collections::EntropyConfigurationsController < ApplicationController - include CollectionScoped - - def update - @collection.entropy_configuration.update!(entropy_configuration_params) - - render turbo_stream: [ - turbo_stream.replace([ @collection, :entropy_configuration ], partial: "collections/edit/auto_close", locals: { collection: @collection }), - turbo_stream_flash(notice: "Saved") - ] - end - - private - def entropy_configuration_params - params.expect(collection: [ :auto_postpone_period ]) - end -end diff --git a/app/models/account.rb b/app/models/account.rb index 357d0cc29..6c42bcdfb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -16,7 +16,7 @@ class Account < ApplicationRecord end end - # To use the account as a generic card container. See +Entropy::Configuration+. + # To use the account as a generic card container. See +Entropy+. def cards Card.all end diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index b82b1051b..de6e3a7e6 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -2,16 +2,16 @@ module Account::Entropic extend ActiveSupport::Concern included do - has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy + has_one :default_entropy, class_name: "Entropy", as: :container, dependent: :destroy - before_save :set_default_entropy_configuration + before_save :set_default_entropy end private DEFAULT_ENTROPY_PERIOD = 30.days - def set_default_entropy_configuration - self.default_entropy_configuration ||= build_default_entropy_configuration \ + def set_default_entropy + self.default_entropy ||= build_default_entropy \ auto_postpone_period: DEFAULT_ENTROPY_PERIOD end end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index 46acd8da8..e938e232e 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -4,16 +4,16 @@ module Card::Entropic included do scope :due_to_be_postponed, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", - Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(collection: :entropy) + .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", + Entropy.default.auto_postpone_period) end scope :postponing_soon, -> do active - .left_outer_joins(collection: :entropy_configuration) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_postpone_period) + .left_outer_joins(collection: :entropy) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Entropy.default.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy.default.auto_postpone_period) end delegate :auto_postpone_period, to: :collection diff --git a/app/models/collection.rb b/app/models/collection.rb index 504439e2e..f1aef0fb7 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,7 +8,7 @@ class Collection < ApplicationRecord has_many :tags, -> { distinct }, through: :cards has_many :events has_many :webhooks, dependent: :destroy - has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy + has_one :entropy, as: :container, dependent: :destroy scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index a3879955b..dbfb6a981 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -2,15 +2,15 @@ module Collection::Entropic extend ActiveSupport::Concern included do - delegate :auto_postpone_period, to: :entropy_configuration + delegate :auto_postpone_period, to: :entropy end - def entropy_configuration - super || Entropy::Configuration.default + def entropy + super || Entropy.default end def auto_postpone_period=(new_value) - entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration - entropy_configuration.update auto_postpone_period: new_value + entropy ||= association(:entropy).reader || self.build_entropy + entropy.update auto_postpone_period: new_value end end diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 42e730a09..613d1a315 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -1,5 +1,16 @@ -module Entropy - def self.table_name_prefix - "entropy_" +class Entropy < ApplicationRecord + belongs_to :container, polymorphic: true + + after_commit :touch_all_cards_later + + class << self + def default + Account.sole.default_entropy + end end + + private + def touch_all_cards_later + Card::TouchAllJob.perform_later(container) + end end diff --git a/app/models/entropy/configuration.rb b/app/models/entropy/configuration.rb deleted file mode 100644 index 1884cb112..000000000 --- a/app/models/entropy/configuration.rb +++ /dev/null @@ -1,16 +0,0 @@ -class Entropy::Configuration < ApplicationRecord - belongs_to :container, polymorphic: true - - after_commit :touch_all_cards_later - - class << self - def default - Account.sole.default_entropy_configuration - end - end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(container) - end -end diff --git a/app/views/account/settings/_entropy_configuration.html.erb b/app/views/account/settings/_entropy.html.erb similarity index 89% rename from app/views/account/settings/_entropy_configuration.html.erb rename to app/views/account/settings/_entropy.html.erb index 9ecc64db6..316097898 100644 --- a/app/views/account/settings/_entropy_configuration.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -4,5 +4,5 @@

BOXCAR 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.default_entropy_configuration, url: account_entropy_configuration_path %> + <%= render "entropy/auto_close", model: account.default_entropy, url: account_entropy_path %> diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb index 53533bba4..7e2dbbfbf 100644 --- a/app/views/account/settings/show.html.erb +++ b/app/views/account/settings/show.html.erb @@ -10,5 +10,5 @@ <%= render "account/settings/users", users: @users %> - <%= render "account/settings/entropy_configuration", account: @account %> + <%= render "account/settings/entropy", account: @account %> diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index fba5f8771..4a86005a8 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -1,7 +1,7 @@ -<%= turbo_frame_tag @collection, :entropy_configuration do %> +<%= turbo_frame_tag @collection, :entropy do %>

Auto close

BOXCAR 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: collection, url: collection_entropy_configuration_path(collection) %> + <%= render "entropy/auto_close", model: collection, url: collection_entropy_path(collection) %>
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 9b7e1ddb6..b8c481b53 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,7 @@ Rails.application.routes.draw do post :enter, to: "entries#create" resource :join_code resource :settings - resource :entropy_configuration + resource :entropy end resources :users do @@ -16,7 +16,7 @@ Rails.application.routes.draw do resource :subscriptions resource :involvement resource :publication - resource :entropy_configuration + resource :entropy namespace :columns do resource :not_now diff --git a/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb b/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb new file mode 100644 index 000000000..2e1e5d1e8 --- /dev/null +++ b/db/migrate/20251102115338_rename_entropy_configurations_to_entropies.rb @@ -0,0 +1,5 @@ +class RenameEntropyConfigurationsToEntropies < ActiveRecord::Migration[8.2] + def change + rename_table :entropy_configurations, :entropies + end +end diff --git a/db/schema.rb b/db/schema.rb index 987f8eb9d..3a6bf3b4c 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_10_29_161222) do +ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do create_table "accesses", force: :cascade do |t| t.datetime "accessed_at" t.integer "collection_id", null: false @@ -220,13 +220,13 @@ ActiveRecord::Schema[8.2].define(version: 2025_10_29_161222) do t.index ["filter_id"], name: "index_creators_filters_on_filter_id" end - create_table "entropy_configurations", force: :cascade do |t| + create_table "entropies", force: :cascade do |t| t.bigint "auto_postpone_period", default: 2592000, null: false t.integer "container_id", null: false t.string "container_type", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["container_type", "container_id", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73" + t.index ["container_type", "container_id", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_3d79b50517" t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true end diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 704ed83a2..755b7cf07 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -606,7 +606,7 @@ columns: creators_filters: - *25 - *19 - entropy_configurations: + entropies: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: auto_postpone_period @@ -1254,7 +1254,7 @@ primary_keys: columns: id comments: id creators_filters: - entropy_configurations: id + entropies: id events: id filters: id filters_tags: @@ -1301,7 +1301,7 @@ data_sources: columns: true comments: true creators_filters: true - entropy_configurations: true + entropies: true events: true filters: true filters_tags: true @@ -2031,10 +2031,10 @@ indexes: nulls_not_distinct: comment: valid: true - entropy_configurations: + entropies: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: entropy_configurations - name: idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73 + table: entropies + name: idx_on_container_type_container_id_auto_postpone_pe_3d79b50517 unique: false columns: - container_type @@ -2051,7 +2051,7 @@ indexes: comment: valid: true - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: entropy_configurations + table: entropies name: index_entropy_configurations_on_container unique: true columns: @@ -2879,4 +2879,4 @@ indexes: nulls_not_distinct: comment: valid: true -version: 20251029161222 +version: 20251102115338 diff --git a/test/controllers/accounts/entropies_controller_test.rb b/test/controllers/accounts/entropies_controller_test.rb new file mode 100644 index 000000000..9fda2c1ed --- /dev/null +++ b/test/controllers/accounts/entropies_controller_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +class Account::EntropiesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "update" do + put account_entropy_path, params: { entropy: { auto_postpone_period: 1.day } } + + assert_equal 1.day, entropies("37s_account").auto_postpone_period + + assert_redirected_to account_settings_path + end +end diff --git a/test/controllers/accounts/entropy_configurations_controller_test.rb b/test/controllers/accounts/entropy_configurations_controller_test.rb deleted file mode 100644 index 309e6712e..000000000 --- a/test/controllers/accounts/entropy_configurations_controller_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "test_helper" - -class Account::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - end - - test "update" do - put account_entropy_configuration_path, params: { entropy_configuration: { auto_postpone_period: 1.day } } - - assert_equal 1.day, entropy_configurations("37s_account").auto_postpone_period - - assert_redirected_to account_settings_path - end -end diff --git a/test/controllers/collections/entropies_controller_test.rb b/test/controllers/collections/entropies_controller_test.rb new file mode 100644 index 000000000..118ba0b7c --- /dev/null +++ b/test/controllers/collections/entropies_controller_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class Collections::EntropiesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + @collection = collections(:writebook) + end + + test "update" do + put collection_entropy_path(@collection), params: { collection: { auto_postpone_period: 1.day } } + + assert_equal 1.day, @collection.entropy.reload.auto_postpone_period + + assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy) + end +end diff --git a/test/controllers/collections/entropy_configurations_controller_test.rb b/test/controllers/collections/entropy_configurations_controller_test.rb deleted file mode 100644 index 3fc24fe8a..000000000 --- a/test/controllers/collections/entropy_configurations_controller_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -require "test_helper" - -class Collections::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :kevin - @collection = collections(:writebook) - end - - test "update" do - put collection_entropy_configuration_path(@collection), params: { collection: { auto_postpone_period: 1.day } } - - assert_equal 1.day, @collection.entropy_configuration.reload.auto_postpone_period - - assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy_configuration) - end -end diff --git a/test/controllers/collections_controller_test.rb b/test/controllers/collections_controller_test.rb index 4193cbbdd..92bb2237c 100644 --- a/test/controllers/collections_controller_test.rb +++ b/test/controllers/collections_controller_test.rb @@ -44,7 +44,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to edit_collection_path(collections(:writebook)) assert_equal "Writebook bugs", collections(:writebook).reload.name assert_equal users(:kevin, :jz).sort, collections(:writebook).users.sort - assert_equal 1.day, entropy_configurations(:writebook_collection).auto_postpone_period + assert_equal 1.day, entropies(:writebook_collection).auto_postpone_period assert_not collections(:writebook).all_access? end diff --git a/test/fixtures/entropy/configurations.yml b/test/fixtures/entropies.yml similarity index 100% rename from test/fixtures/entropy/configurations.yml rename to test/fixtures/entropies.yml diff --git a/test/models/card/entropic_test.rb b/test/models/card/entropic_test.rb index 4953ee24e..0512b1c2b 100644 --- a/test/models/card/entropic_test.rb +++ b/test/models/card/entropic_test.rb @@ -8,8 +8,8 @@ class Card::EntropicTest < ActiveSupport::TestCase test "auto_postpone_at uses the period defined in the account by default" do freeze_time - entropy_configurations(:writebook_collection).destroy - entropy_configurations("37s_account").reload.update! auto_postpone_period: 456.days + entropies(:writebook_collection).destroy + entropies("37s_account").reload.update! auto_postpone_period: 456.days cards(:layout).update! last_active_at: 2.day.ago assert_equal (456 - 2).days.from_now, cards(:layout).entropy.auto_clean_at end @@ -17,16 +17,16 @@ class Card::EntropicTest < ActiveSupport::TestCase test "auto_postpone_at infers the period from the collection when present" do freeze_time - entropy_configurations(:writebook_collection).update! auto_postpone_period: 123.days + entropies(:writebook_collection).update! auto_postpone_period: 123.days cards(:layout).update! last_active_at: 2.day.ago assert_equal (123 - 2).days.from_now, cards(:layout).entropy.auto_clean_at end - test "auto postpone all due using the default account entropy configuration" do - entropy_configurations(:writebook_collection).destroy + test "auto postpone all due using the default account entropy" do + entropies(:writebook_collection).destroy - cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("37s_account").auto_postpone_period) - cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("37s_account").auto_postpone_period) + cards(:logo).update!(last_active_at: 1.day.ago - entropies("37s_account").auto_postpone_period) + cards(:shipping).update!(last_active_at: 1.day.from_now - entropies("37s_account").auto_postpone_period) assert_difference -> { Card.postponed.count }, +1 do Card.auto_postpone_all_due @@ -37,9 +37,9 @@ class Card::EntropicTest < ActiveSupport::TestCase assert_not cards(:shipping).reload.postponed? end - test "auto postpone all due using entropy configuration defined at the collection level" do - cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations(:writebook_collection).auto_postpone_period) - cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations(:writebook_collection).auto_postpone_period) + test "auto postpone all due using entropy defined at the collection level" do + cards(:logo).update!(last_active_at: 1.day.ago - entropies(:writebook_collection).auto_postpone_period) + cards(:shipping).update!(last_active_at: 1.day.from_now - entropies(:writebook_collection).auto_postpone_period) assert_difference -> { Card.postponed.count }, +1 do Card.auto_postpone_all_due @@ -52,8 +52,8 @@ class Card::EntropicTest < ActiveSupport::TestCase test "postponing soon scope" do cards(:logo, :shipping).each(&:published!) - cards(:logo).update!(last_active_at: entropy_configurations(:writebook_collection).auto_postpone_period.seconds.ago + 2.days) - cards(:shipping).update!(last_active_at: entropy_configurations(:writebook_collection).auto_postpone_period.seconds.ago - 2.days) + cards(:logo).update!(last_active_at: entropies(:writebook_collection).auto_postpone_period.seconds.ago + 2.days) + cards(:shipping).update!(last_active_at: entropies(:writebook_collection).auto_postpone_period.seconds.ago - 2.days) assert_includes Card.postponing_soon, cards(:logo) assert_not_includes Card.postponing_soon, cards(:shipping) diff --git a/test/models/card/stallable_test.rb b/test/models/card/stallable_test.rb index a76ff1d69..13ae49972 100644 --- a/test/models/card/stallable_test.rb +++ b/test/models/card/stallable_test.rb @@ -28,7 +28,7 @@ class Card::StallableTest < ActiveSupport::TestCase test "a card with an old activity spike is not stalled after being postponed" do card = cards(:logo) - card.update!(last_active_at: 1.day.ago - card.collection.entropy_configuration.auto_postpone_period) + card.update!(last_active_at: 1.day.ago - card.collection.entropy.auto_postpone_period) card.create_activity_spike!(updated_at: 3.months.ago) assert card.stalled? diff --git a/test/models/entropy/configuration_test.rb b/test/models/entropy/configuration_test.rb deleted file mode 100644 index 015e8b1de..000000000 --- a/test/models/entropy/configuration_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "test_helper" - -class Entropy::ConfigurationTest < ActiveSupport::TestCase - test "touch cards when configuration changes for collection container" do - collection = collections(:writebook) - config = collection.entropy_configuration || collection.create_entropy_configuration!(auto_postpone_period: 30.days) - - assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do - config.update!(auto_postpone_period: 15.days) - end - end - - test "touch cards when configuration changes for account container" do - account = Account.sole - - assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do - account.default_entropy_configuration.update!(auto_postpone_period: 45.days) - end - end -end diff --git a/test/models/entropy_test.rb b/test/models/entropy_test.rb new file mode 100644 index 000000000..08fbf8600 --- /dev/null +++ b/test/models/entropy_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class Entropy::Test < ActiveSupport::TestCase + test "touch cards when entropy changes for collection container" do + collection = collections(:writebook) + config = collection.entropy || collection.create_entropy!(auto_postpone_period: 30.days) + + assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do + config.update!(auto_postpone_period: 15.days) + end + end + + test "touch cards when entropy changes for account container" do + account = Account.sole + + assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do + account.default_entropy.update!(auto_postpone_period: 45.days) + end + end +end diff --git a/test/routes_test.rb b/test/routes_test.rb index d1663037b..1bdc1ae5c 100644 --- a/test/routes_test.rb +++ b/test/routes_test.rb @@ -9,7 +9,7 @@ class RouteTest < ActionDispatch::IntegrationTest assert_recognizes({ controller: "account/settings", action: "show" }, "/account/settings") end - test "account/entropy_configuration" do - assert_recognizes({ controller: "account/entropy_configurations", action: "show" }, "/account/entropy_configuration") + test "account/entropy" do + assert_recognizes({ controller: "account/entropies", action: "show" }, "/account/entropy") end end From fb395c8b079213450a5cc59aec65f2e7d3099d08 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:17:55 +0100 Subject: [PATCH 014/173] Keep all entropy definitions together in the concern --- app/models/collection.rb | 1 - app/models/collection/entropic.rb | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index f1aef0fb7..d9ed3d3ec 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,7 +8,6 @@ class Collection < ApplicationRecord has_many :tags, -> { distinct }, through: :cards has_many :events has_many :webhooks, dependent: :destroy - has_one :entropy, as: :container, dependent: :destroy scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index dbfb6a981..851da2e37 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -3,6 +3,7 @@ module Collection::Entropic included do delegate :auto_postpone_period, to: :entropy + has_one :entropy, as: :container, dependent: :destroy end def entropy From 2c4159d11ae673d6ba5ca1a8baf3686316ef9809 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:29:07 +0100 Subject: [PATCH 015/173] Style --- app/models/collection.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index 504439e2e..756d987cf 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -17,8 +17,6 @@ class Collection < ApplicationRecord private def ensure_default_collection - if Collection.count.zero? - Collection.create!(name: "Cards") - end + Collection.create!(name: "Cards") if Collection.none? end end From 7052d6ecec446ec9148f228f844e0b4db7ca2e24 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:54:46 +0100 Subject: [PATCH 016/173] Fix style --- app/controllers/collections/entropies_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/collections/entropies_controller.rb b/app/controllers/collections/entropies_controller.rb index bb0d2940a..d9ed904f8 100644 --- a/app/controllers/collections/entropies_controller.rb +++ b/app/controllers/collections/entropies_controller.rb @@ -5,7 +5,7 @@ class Collections::EntropiesController < ApplicationController @collection.entropy.update!(entropy_params) render turbo_stream: [ - turbo_stream.replace([ @collection, :entropy], partial: "collections/edit/auto_close", locals: { collection: @collection }), + turbo_stream.replace([ @collection, :entropy ], partial: "collections/edit/auto_close", locals: { collection: @collection }), turbo_stream_flash(notice: "Saved") ] end From b1c8dc1e64d26f6999eedc52c79ae4bc2155ed35 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 13:56:31 +0100 Subject: [PATCH 017/173] Need even more grace to ensure success --- test/system/smoke_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 8c80c165e..7e0b5e74b 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -61,8 +61,8 @@ class SmokeTest < ApplicationSystemTestCase def assert_image_figure_attachment(content_type: "image/png", caption:) assert_figure_attachment(content_type: content_type) do - assert_selector "img[src*='/rails/active_storage']", wait: 5 - assert_selector "figcaption input[placeholder='#{caption}']", wait: 5 + assert_selector "img[src*='/rails/active_storage']", wait: 10 + assert_selector "figcaption input[placeholder='#{caption}']", wait: 10 end end end From 966aa51a37cd32928bec988c5d1238285d7393cb Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:15:53 +0100 Subject: [PATCH 018/173] Get rid of unnecessary touch jobs touch_all on 10K cards took 6ms in testing --- app/jobs/card/touch_all_job.rb | 7 ------- app/models/collection/cards.rb | 7 +------ app/models/column.rb | 16 ++-------------- app/models/entropy.rb | 7 +------ test/models/collection/cards_test.rb | 4 ++-- test/models/column_test.rb | 8 ++++---- test/models/entropy_test.rb | 13 +++++-------- 7 files changed, 15 insertions(+), 47 deletions(-) delete mode 100644 app/jobs/card/touch_all_job.rb diff --git a/app/jobs/card/touch_all_job.rb b/app/jobs/card/touch_all_job.rb deleted file mode 100644 index 7b96fa5b7..000000000 --- a/app/jobs/card/touch_all_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Card::TouchAllJob < ApplicationJob - queue_as :backend - - def perform(container) - container.cards.in_batches(&:touch_all) - end -end diff --git a/app/models/collection/cards.rb b/app/models/collection/cards.rb index 17ed592aa..105f4912b 100644 --- a/app/models/collection/cards.rb +++ b/app/models/collection/cards.rb @@ -4,11 +4,6 @@ module Collection::Cards included do has_many :cards, dependent: :destroy - after_update_commit :touch_all_cards_later, if: :saved_change_to_name? + after_update_commit -> { cards.touch_all }, if: :saved_change_to_name? end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end end diff --git a/app/models/column.rb b/app/models/column.rb index 0285ab185..d0c1ce102 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -8,23 +8,11 @@ class Column < ApplicationRecord validates :color, presence: true before_validation :set_default_color - after_save_commit :touch_all_cards_later, if: :should_invalidate_cards? - after_destroy_commit :touch_all_collection_cards + after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } + after_destroy_commit -> { collection.cards.touch_all } private def set_default_color self.color ||= Card::DEFAULT_COLOR end - - def touch_all_cards_later - Card::TouchAllJob.perform_later(self) - end - - def should_invalidate_cards? - saved_change_to_name? || saved_change_to_color? - end - - def touch_all_collection_cards - Card::TouchAllJob.perform_later(collection) - end end diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 613d1a315..6940ed813 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -1,16 +1,11 @@ class Entropy < ApplicationRecord belongs_to :container, polymorphic: true - after_commit :touch_all_cards_later + after_commit -> { container.cards.touch_all } class << self def default Account.sole.default_entropy end end - - private - def touch_all_cards_later - Card::TouchAllJob.perform_later(container) - end end diff --git a/test/models/collection/cards_test.rb b/test/models/collection/cards_test.rb index f2ec9b9db..34bed8d8c 100644 --- a/test/models/collection/cards_test.rb +++ b/test/models/collection/cards_test.rb @@ -4,11 +4,11 @@ class Collection::CardsTest < ActiveSupport::TestCase test "touch cards when the name changes" do collection = collections(:writebook) - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { collection.cards.first.updated_at } do collection.update!(name: "New Name") end - assert_no_enqueued_jobs(only: Card::TouchAllJob) do + assert_no_changes -> { collection.cards.first.updated_at } do collection.update!(updated_at: 1.hour.from_now) end end diff --git a/test/models/column_test.rb b/test/models/column_test.rb index d924de308..e4786abfb 100644 --- a/test/models/column_test.rb +++ b/test/models/column_test.rb @@ -10,15 +10,15 @@ class ColumnTest < ActiveSupport::TestCase test "touch all the cards when the name or color changes" do column = columns(:writebook_triage) - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { column.cards.first.updated_at } do column.update!(name: "New Name") end - assert_enqueued_with(job: Card::TouchAllJob) do + assert_changes -> { column.cards.first.updated_at } do column.update!(color: "#FF0000") end - assert_no_enqueued_jobs(only: Card::TouchAllJob) do + assert_no_changes -> { column.cards.first.updated_at } do column.update!(updated_at: 1.hour.from_now) end end @@ -26,7 +26,7 @@ class ColumnTest < ActiveSupport::TestCase test "touch all collection cards when column is destroyed" do column = columns(:writebook_triage) - assert_enqueued_with(job: Card::TouchAllJob, args: [ column.collection ]) do + assert_changes -> { column.collection.cards.first.updated_at } do column.destroy end end diff --git a/test/models/entropy_test.rb b/test/models/entropy_test.rb index 08fbf8600..f5056cf76 100644 --- a/test/models/entropy_test.rb +++ b/test/models/entropy_test.rb @@ -1,20 +1,17 @@ require "test_helper" class Entropy::Test < ActiveSupport::TestCase - test "touch cards when entropy changes for collection container" do - collection = collections(:writebook) - config = collection.entropy || collection.create_entropy!(auto_postpone_period: 30.days) - - assert_enqueued_with(job: Card::TouchAllJob, args: [ collection ]) do - config.update!(auto_postpone_period: 15.days) + test "touch cards when entropy changes for collection" do + assert_changes -> { collections(:writebook).cards.first.updated_at } do + collections(:writebook).entropy.update!(auto_postpone_period: 15.days) end end test "touch cards when entropy changes for account container" do account = Account.sole - assert_enqueued_with(job: Card::TouchAllJob, args: [ account ]) do - account.default_entropy.update!(auto_postpone_period: 45.days) + assert_changes -> { account.cards.first.updated_at } do + collections(:writebook).entropy.update!(auto_postpone_period: 15.days) end end end From 0af74bfa11830a9b38d9582db37df690a4740fef Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:17:46 +0100 Subject: [PATCH 019/173] Style --- app/controllers/collections/columns_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index b8f4d4de1..ca06d825f 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -1,7 +1,7 @@ class Collections::ColumnsController < ApplicationController include ActionView::RecordIdentifier, CollectionScoped - before_action :set_column, only: [ :show, :update, :destroy ] + before_action :set_column, only: %i[ show update destroy ] def show set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first From d6c88736bd19b233b39ac38df5ae7df733ca92cf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:19:26 +0100 Subject: [PATCH 020/173] No need for validation when we have rely on update!/create! on db constraints --- app/models/column.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/column.rb b/app/models/column.rb index d0c1ce102..10ab6f3df 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -4,9 +4,6 @@ class Column < ApplicationRecord belongs_to :collection, touch: true has_many :cards, dependent: :nullify - validates :name, presence: true - validates :color, presence: true - before_validation :set_default_color after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } after_destroy_commit -> { collection.cards.touch_all } From a1543f2660c7e62151b40048cb8e84d4128fb170 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 14:20:07 +0100 Subject: [PATCH 021/173] Inline anemic helper method with no additional explaining purpose --- app/models/column.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/models/column.rb b/app/models/column.rb index 10ab6f3df..87772367f 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -4,12 +4,7 @@ class Column < ApplicationRecord belongs_to :collection, touch: true has_many :cards, dependent: :nullify - before_validation :set_default_color + before_validation -> { self.color ||= Card::DEFAULT_COLOR } after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } after_destroy_commit -> { collection.cards.touch_all } - - private - def set_default_color - self.color ||= Card::DEFAULT_COLOR - end end From 2c9845eb62e08b288c4199a667054f9d9f22f21b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:24:55 +0100 Subject: [PATCH 022/173] No need to hide that the top-level entropy is tied to the Account --- app/controllers/account/entropies_controller.rb | 2 +- app/models/account/entropic.rb | 15 ++++----------- app/models/card/entropic.rb | 6 +++--- app/models/collection/entropic.rb | 2 +- app/models/entropy.rb | 6 ------ app/views/account/settings/_entropy.html.erb | 2 +- 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/controllers/account/entropies_controller.rb b/app/controllers/account/entropies_controller.rb index 75fa01e20..f45f3ef5f 100644 --- a/app/controllers/account/entropies_controller.rb +++ b/app/controllers/account/entropies_controller.rb @@ -1,6 +1,6 @@ class Account::EntropiesController < ApplicationController def update - Entropy.default.update!(entropy_params) + Account.sole.entropy.update!(entropy_params) redirect_to account_settings_path, notice: "Account updated" end diff --git a/app/models/account/entropic.rb b/app/models/account/entropic.rb index de6e3a7e6..e54af1432 100644 --- a/app/models/account/entropic.rb +++ b/app/models/account/entropic.rb @@ -1,17 +1,10 @@ module Account::Entropic extend ActiveSupport::Concern + DEFAULT_ENTROPY_PERIOD = 30.days + included do - has_one :default_entropy, class_name: "Entropy", as: :container, dependent: :destroy - - before_save :set_default_entropy + has_one :entropy, as: :container, dependent: :destroy + after_create -> { create_entropy!(auto_postpone_period: DEFAULT_ENTROPY_PERIOD) } end - - private - DEFAULT_ENTROPY_PERIOD = 30.days - - def set_default_entropy - self.default_entropy ||= build_default_entropy \ - auto_postpone_period: DEFAULT_ENTROPY_PERIOD - end end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index e938e232e..68c0c6aa1 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -6,14 +6,14 @@ module Card::Entropic active .left_outer_joins(collection: :entropy) .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", - Entropy.default.auto_postpone_period) + Account.sole.entropy.auto_postpone_period) end scope :postponing_soon, -> do active .left_outer_joins(collection: :entropy) - .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Entropy.default.auto_postpone_period) - .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy.default.auto_postpone_period) + .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period) + .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Account.sole.entropy.auto_postpone_period) end delegate :auto_postpone_period, to: :collection diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index 851da2e37..a84e7eb62 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -7,7 +7,7 @@ module Collection::Entropic end def entropy - super || Entropy.default + super || Account.sole.entropy end def auto_postpone_period=(new_value) diff --git a/app/models/entropy.rb b/app/models/entropy.rb index 6940ed813..6be6675ee 100644 --- a/app/models/entropy.rb +++ b/app/models/entropy.rb @@ -2,10 +2,4 @@ class Entropy < ApplicationRecord belongs_to :container, polymorphic: true after_commit -> { container.cards.touch_all } - - class << self - def default - Account.sole.default_entropy - end - end end diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb index 316097898..fb3709e4d 100644 --- a/app/views/account/settings/_entropy.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -4,5 +4,5 @@

BOXCAR 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.default_entropy, url: account_entropy_path %> + <%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %> From f09183bc9ac50b89200452953de0695ded1bf3a8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:46:05 +0100 Subject: [PATCH 023/173] Don't want to balloon txn, but also don't want to end up with a state with broken data --- app/models/collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index ac9003ed1..1bfc4be2a 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -12,7 +12,7 @@ class Collection < ApplicationRecord scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } - after_destroy_commit :ensure_default_collection + after_destroy :ensure_default_collection private def ensure_default_collection From c834cce504e31149e2266a30c01e1e9220933e3c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 15:58:33 +0100 Subject: [PATCH 024/173] Simplify join code creation and validation and test controller cc @monorkin --- .../account/join_codes_controller.rb | 7 ++--- app/models/account/join_code.rb | 6 +---- .../accounts/join_codes_controller_test.rb | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 test/controllers/accounts/join_codes_controller_test.rb diff --git a/app/controllers/account/join_codes_controller.rb b/app/controllers/account/join_codes_controller.rb index b67246b80..026c692f9 100644 --- a/app/controllers/account/join_codes_controller.rb +++ b/app/controllers/account/join_codes_controller.rb @@ -8,11 +8,8 @@ class Account::JoinCodesController < ApplicationController end def update - if @join_code.update(join_code_params) - redirect_to account_join_code_path - else - render :edit, status: :unprocessable_entity - end + @join_code.update!(join_code_params) + redirect_to account_join_code_path end def destroy diff --git a/app/models/account/join_code.rb b/app/models/account/join_code.rb index ddf4cf822..69d681197 100644 --- a/app/models/account/join_code.rb +++ b/app/models/account/join_code.rb @@ -3,11 +3,7 @@ class Account::JoinCode < ApplicationRecord scope :active, -> { where("usage_count < usage_limit") } - before_validation :generate_code, on: :create, if: -> { code.blank? } - - validates :code, presence: true, uniqueness: true - validates :usage_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :usage_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + before_create :generate_code, if: -> { code.blank? } def redeem transaction do diff --git a/test/controllers/accounts/join_codes_controller_test.rb b/test/controllers/accounts/join_codes_controller_test.rb new file mode 100644 index 000000000..438904d2a --- /dev/null +++ b/test/controllers/accounts/join_codes_controller_test.rb @@ -0,0 +1,26 @@ +require "test_helper" + +class Account::JoinCodesControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "reset" do + get account_join_code_path + assert_response :success + + assert_changes -> { Account::JoinCode.sole.code } do + delete account_join_code_path + assert_redirected_to account_join_code_path + end + end + + test "update" do + get edit_account_join_code_path + assert_response :success + + put account_join_code_path, params: { account_join_code: { usage_limit: 5 } } + assert_equal 5, Account::JoinCode.sole.usage_limit + assert_redirected_to account_join_code_path + end +end From 02bd57089dabb49fd4efea5674c52418f31bc1db Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:04:44 +0100 Subject: [PATCH 025/173] Use before_action to pull up account assignment --- app/controllers/account/settings_controller.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/account/settings_controller.rb b/app/controllers/account/settings_controller.rb index c2f9672c9..42a6a60f7 100644 --- a/app/controllers/account/settings_controller.rb +++ b/app/controllers/account/settings_controller.rb @@ -1,17 +1,21 @@ class Account::SettingsController < ApplicationController before_action :ensure_admin, only: :update + before_action :set_account def show - @account = Account.sole @users = User.active.alphabetically end def update - Account.sole.update!(account_params) + @account.update!(account_params) redirect_to account_settings_path end private + def set_account + @account = Account.sole + end + def account_params params.expect account: %i[ name ] end From 76c87f6b67e059f83fe20e8400d36cad6829e002 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:04:51 +0100 Subject: [PATCH 026/173] Test updating account settings --- test/controllers/accounts/settings_controller_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/controllers/accounts/settings_controller_test.rb b/test/controllers/accounts/settings_controller_test.rb index d475ad40d..78b9f13ac 100644 --- a/test/controllers/accounts/settings_controller_test.rb +++ b/test/controllers/accounts/settings_controller_test.rb @@ -9,4 +9,10 @@ class Account::SettingsControllerTest < ActionDispatch::IntegrationTest get account_settings_path assert_response :success end + + test "update" do + put account_settings_path, params: { account: { name: "New Account Name" }} + assert_equal "New Account Name", Account.sole.name + assert_redirected_to account_settings_path + end end From 877e62dec5bc52774b1eb0b8268913073f39d210 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:30:49 +0100 Subject: [PATCH 027/173] No CR when there are only two statements --- app/controllers/sessions_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 25cb98408..10a3c9773 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -10,7 +10,6 @@ class SessionsController < ApplicationController def create Identity.find_by_email_address(email_address)&.send_magic_link - redirect_to session_magic_link_path end From 6a62df470cfcdacbe193716344a3b26bbb766fba Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 16:34:29 +0100 Subject: [PATCH 028/173] No longer used --- app/controllers/uploads_controller.rb | 29 --------------------------- config/routes.rb | 3 --- 2 files changed, 32 deletions(-) delete mode 100644 app/controllers/uploads_controller.rb diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb deleted file mode 100644 index 3a240af09..000000000 --- a/app/controllers/uploads_controller.rb +++ /dev/null @@ -1,29 +0,0 @@ -class UploadsController < ApplicationController - include ActiveStorage::SetCurrent - - before_action :set_file, only: :create - before_action :set_attachment, only: :show - - def create - # FIXME: Try to get upload attachments on root - @upload = Account.sole.uploads_attachments.create! blob: create_blob! - end - - def show - expires_in 5.minutes, public: true - redirect_to @attachment.url, allow_other_host: true - end - - private - def set_file - @file = params[:file] - end - - def set_attachment - @attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}" - end - - def create_blob! - ActiveStorage::Blob.create_and_upload! io: @file, filename: @file.original_filename, content_type: @file.content_type - end -end diff --git a/config/routes.rb b/config/routes.rb index b8c481b53..dc8df0d44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,9 +120,6 @@ Rails.application.routes.draw do resources :days end - resources :uploads, only: :create - get "/u/*slug" => "uploads#show", as: :upload - resources :qr_codes get "join/:tenant/:code", to: "join_codes#new", as: :join From bced7405dfd587e94fef43841090f8255b372dd6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:31:28 +0100 Subject: [PATCH 029/173] Extract events loading from overloaded UsersController --- app/controllers/users/events_controller.rb | 23 +++++++++++++++++++ app/controllers/users_controller.rb | 17 +------------- app/views/users/events/show.html.erb | 14 +++++++++++ app/views/users/show.html.erb | 19 +++++---------- config/routes.rb | 7 ++++-- .../users/events_controller_test.rb | 17 ++++++++++++++ 6 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 app/controllers/users/events_controller.rb create mode 100644 app/views/users/events/show.html.erb create mode 100644 test/controllers/users/events_controller_test.rb diff --git a/app/controllers/users/events_controller.rb b/app/controllers/users/events_controller.rb new file mode 100644 index 000000000..76b85dd58 --- /dev/null +++ b/app/controllers/users/events_controller.rb @@ -0,0 +1,23 @@ +class Users::EventsController < ApplicationController + include FilterScoped + + before_action :set_user, :set_filter, :set_user_filtering + + def show + @filter = Current.user.filters.new(creator_ids: [ @user.id ]) + @day_timeline = Current.user.timeline_for(day_param, filter: @filter) + end + + private + def set_user + @user = User.active.find(params[:user_id]) + end + + def day_param + if params[:day].present? + Time.zone.parse(params[:day]) + else + Time.current + end + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 811261c87..8b89fc0e4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,14 +1,9 @@ class UsersController < ApplicationController - include FilterScoped - require_access_without_a_user only: %i[ new create ] - before_action :set_join_code, only: %i[ new create] - before_action :ensure_join_code_is_valid, only: %i[ new create ] + before_action :set_join_code, :ensure_join_code_is_valid, only: %i[ new create ] before_action :set_user, only: %i[ show edit update destroy ] before_action :ensure_permission_to_change_user, only: %i[ update destroy ] - before_action :set_filter, only: %i[ edit show ] - before_action :set_user_filtering, only: %i[ edit show] def new end @@ -25,8 +20,6 @@ class UsersController < ApplicationController end def show - @filter = Current.user.filters.new(creator_ids: [ @user.id ]) - @day_timeline = Current.user.timeline_for(day_param, filter: @filter) end def update @@ -58,14 +51,6 @@ class UsersController < ApplicationController head :forbidden unless Current.user.can_change?(@user) end - def day_param - if params[:day].present? - Time.zone.parse(params[:day]) - else - Time.current - end - end - def user_params params.expect(user: [ :name, :avatar ]) end diff --git a/app/views/users/events/show.html.erb b/app/views/users/events/show.html.erb new file mode 100644 index 000000000..17ec2c1b3 --- /dev/null +++ b/app/views/users/events/show.html.erb @@ -0,0 +1,14 @@ + +

<%= "What #{Current.user == @user ? "have you" : "has #{@user.first_name}"} been up to?" %>

+ +
+ <%= day_timeline_pagination_frame_tag @day_timeline do %> + <%= render "events/day", day_timeline: @day_timeline %> + + <% if @day_timeline.next_day %> + <%= link_to "Load more…", user_events_path(@user, day: @day_timeline.next_day.strftime("%Y-%m-%d"), **@filter.as_params), + class: "day-timeline-pagination-link", data: { frame: day_timeline_pagination_frame_id_for(@day_timeline.next_day), pagination_target: "paginationLink" } %> + <% end %> + <% end %> +
+
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 9e49972f1..1ae06e2c0 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,8 +1,5 @@ <% @page_title = @user.name %> - -<% content_for :header do %> - <%= render "my/menus/menu" %> -<% end %> +<% me_or_you = Current.user == @user ? "me" : @user.first_name %>
@@ -30,8 +27,10 @@
- <%= link_to "Which cards are assigned to #{ Current.user == @user ? "me" : @user.first_name }?", cards_path(assignee_ids: [@user.id], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> - <%= link_to "Which cards were added by #{ Current.user == @user ? "me" : @user.first_name }?", cards_path(creator_ids: [@user.id], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %> + <%= 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}?", + cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
@@ -49,10 +48,4 @@ <% end %> - - - - -

<%= "What #{ Current.user == @user ? "have you" : "has #{ @user.first_name }" } been up to?" %>

- -<%= render "users/activity_timeline", user: @user, day_timeline: @day_timeline, filter: @filter %> +<%= turbo_frame_tag "user_events", src: user_events_path(@user) %> diff --git a/config/routes.rb b/config/routes.rb index dc8df0d44..15c162207 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,8 +7,11 @@ Rails.application.routes.draw do end resources :users do - resource :role, module: :users - resources :push_subscriptions, module: :users + scope module: :users do + resource :role + resource :events + resources :push_subscriptions + end end resources :collections do diff --git a/test/controllers/users/events_controller_test.rb b/test/controllers/users/events_controller_test.rb new file mode 100644 index 000000000..97cb85b2e --- /dev/null +++ b/test/controllers/users/events_controller_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +class Users::EventsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "show self" do + get user_events_path(users(:kevin)) + assert_in_body "What have you been up to?" + end + + test "show other" do + get user_events_path(users(:david)) + assert_in_body "What has David been up to?" + end +end From ada599f6a21f54a9988f60705905284aef748f92 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:31:35 +0100 Subject: [PATCH 030/173] Use latest Rails --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb6ae5aa8..32494ed59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,7 +36,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: def13087d55a6dbbfa80d114c847555927ce2ed7 + revision: d3d17c297a70d3e255a34693a1fb0074712e9930 branch: main specs: actioncable (8.2.0.alpha) @@ -267,7 +267,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.1) - irb (1.15.2) + irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -345,7 +345,7 @@ GEM net-smtp (0.5.1) net-protocol net-ssh (7.3.0) - nio4r (2.7.4) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -407,7 +407,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.3) + rack (3.2.4) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -427,7 +427,7 @@ GEM rake-compiler-dock (1.9.1) rb_sys (0.9.117) rake-compiler-dock (= 1.9.1) - rdoc (6.15.0) + rdoc (6.15.1) erb psych (>= 4.0.0) tsort @@ -543,7 +543,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.0.4) + uri (1.1.0) useragent (0.16.11) vcr (6.3.1) base64 From 43069b1ea4b4c229a87793a4815e66a6042ee4c1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 2 Nov 2025 17:49:25 +0100 Subject: [PATCH 031/173] Extract Users::JoinsController from overloaded UsersController Good code smell is when the before_action callbacks stack up but can't be shared across actions --- app/controllers/concerns/authorization.rb | 2 +- app/controllers/users/joins_controller.rb | 32 +++++++++++++++++++ app/controllers/users_controller.rb | 24 -------------- app/views/users/{ => joins}/new.html.erb | 3 +- config/routes.rb | 4 +++ .../users/joins_controller_test.rb | 32 +++++++++++++++++++ test/controllers/users_controller_test.rb | 29 ----------------- 7 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 app/controllers/users/joins_controller.rb rename app/views/users/{ => joins}/new.html.erb (84%) create mode 100644 test/controllers/users/joins_controller_test.rb diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index df48f62f5..0e5e9240d 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -29,7 +29,7 @@ module Authorization if Current.membership.blank? redirect_to session_menu_url(script_name: nil) elsif Current.user.nil? && Current.membership.join_code.present? - redirect_to new_user_path + redirect_to new_users_join_path elsif !Current.user&.active? redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) end diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb new file mode 100644 index 000000000..630c579bc --- /dev/null +++ b/app/controllers/users/joins_controller.rb @@ -0,0 +1,32 @@ +class Users::JoinsController < ApplicationController + require_access_without_a_user + + before_action :set_join_code, :ensure_join_code_is_valid + + def new + end + + def create + @join_code.redeem do + User.create!(user_params.merge(membership: Current.membership)) + end + + redirect_to root_path + end + + private + def set_join_code + @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) + end + + def ensure_join_code_is_valid + unless @join_code&.active? + redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) + end + end + + def user_params + params.expect(user: [ :name, :avatar ]) + end +end + diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8b89fc0e4..2ba535fef 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,21 +1,7 @@ class UsersController < ApplicationController - require_access_without_a_user only: %i[ new create ] - - before_action :set_join_code, :ensure_join_code_is_valid, only: %i[ new create ] before_action :set_user, only: %i[ show edit update destroy ] before_action :ensure_permission_to_change_user, only: %i[ update destroy ] - def new - end - - def create - @join_code.redeem do - User.create!(user_params.merge(membership: Current.membership)) - end - - redirect_to root_path - end - def edit end @@ -33,16 +19,6 @@ class UsersController < ApplicationController end private - def set_join_code - @join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code) - end - - def ensure_join_code_is_valid - unless @join_code&.active? - redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking)) - end - end - def set_user @user = User.active.find(params[:id]) end diff --git a/app/views/users/new.html.erb b/app/views/users/joins/new.html.erb similarity index 84% rename from app/views/users/new.html.erb rename to app/views/users/joins/new.html.erb index 9239786fe..adf795ece 100644 --- a/app/views/users/new.html.erb +++ b/app/views/users/joins/new.html.erb @@ -5,10 +5,11 @@

<%= @page_title %>

+

Enter your name to continue

- <%= form_with scope: "user", url: users_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %> + <%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
- - <% if card.tags.any? || card.creating? %> + <% if card.tags.any? %>
<% card.tags.each_with_index do |tag, index| %> <%= link_to cards_path(collection_ids: [ card.collection ], tag_ids: [ tag.id ]), diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 5c5549fa4..a300bafff 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -15,7 +15,7 @@
<%= render "cards/container", card: @card %> - <%= render "cards/messages", card: @card unless @card.creating? %> + <%= render "cards/messages", card: @card unless @card.drafted? %> <%= render "layouts/lightbox" do %> <%= button_to_remove_card_image(@card) if @card.image.attached? %> diff --git a/config/recurring.yml b/config/recurring.yml index 89b43c567..15067065b 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -8,9 +8,6 @@ production: &production auto_postpone_all_due: class: Card::AutoPostponeAllDueJob schedule: every hour at minute 50 - remove_abandoned_creations: - class: RemoveAbandonedCreationsJob - schedule: every hour at minute 59 delete_unused_tags: class: DeleteUnusedTagsJob schedule: every day at 04:02 diff --git a/config/routes.rb b/config/routes.rb index 043c48a03..c8e89fb66 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,7 +71,6 @@ Rails.application.routes.draw do resource :triage resource :publish resource :reading - resource :recover resource :watch resource :collection resource :column diff --git a/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb b/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb new file mode 100644 index 000000000..b6b960298 --- /dev/null +++ b/db/migrate/20251102095019_change_cards_status_default_to_drafted.rb @@ -0,0 +1,5 @@ +class ChangeCardsStatusDefaultToDrafted < ActiveRecord::Migration[8.2] + def change + change_column_default :cards, :status, from: "creating", to: "drafted" + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a6bf3b4c..049e45a1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -144,7 +144,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do t.integer "creator_id", null: false t.date "due_on" t.datetime "last_active_at", null: false - t.text "status", default: "creating", null: false + t.text "status", default: "drafted", null: false t.string "title" t.datetime "updated_at", null: false t.index ["collection_id"], name: "index_cards_on_collection_id" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 755b7cf07..e4c5ab87c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -496,7 +496,7 @@ columns: cast_type: *15 sql_type_metadata: *16 'null': false - default: creating + default: drafted default_function: collation: comment: diff --git a/test/controllers/cards/publishes_controller_test.rb b/test/controllers/cards/publishes_controller_test.rb index bddb6e088..c2129d161 100644 --- a/test/controllers/cards/publishes_controller_test.rb +++ b/test/controllers/cards/publishes_controller_test.rb @@ -21,11 +21,12 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest card.drafted! assert_changes -> { card.reload.published? }, from: false, to: true do - assert_difference -> { Card.creating.count }, +1 do + assert_difference -> { Card.count }, +1 do post card_publish_path(card, creation_type: "add_another") end end - assert_redirected_to Card.creating.last + assert Card.last.drafted? + assert_redirected_to Card.last end end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 4ee37cc2c..507aac5ed 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -15,11 +15,23 @@ class CardsControllerTest < ActionDispatch::IntegrationTest assert_response :success end - test "create" do + test "create a new draft" do assert_difference -> { Card.count }, 1 do post collection_cards_path(collections(:writebook)) end - assert_redirected_to card_path(Card.last) + + assert Card.last.drafted? + assert_redirected_to Card.last + end + + test "create resumes existing draft if it exists" do + draft = collections(:writebook).cards.create!(creator: users(:kevin), status: :drafted) + + assert_no_difference -> { Card.count } do + post collection_cards_path(collections(:writebook)) + end + + assert_redirected_to draft end test "show" do diff --git a/test/controllers/recovers_controller_test.rb b/test/controllers/recovers_controller_test.rb deleted file mode 100644 index 91fbf4cf2..000000000 --- a/test/controllers/recovers_controller_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require "test_helper" - -class Cards::RecoversControllerTest < ActionDispatch::IntegrationTest - setup do - sign_in_as :jz - end - - test "create" do - abandoned_card = collections(:writebook).cards.create! creator: users(:kevin) - abandoned_card.update!(title: "An edited title") - unsaved_card = collections(:writebook).cards.create! creator: users(:kevin) - - post card_recover_path(unsaved_card) - - assert_redirected_to abandoned_card - assert_equal [ abandoned_card ], Card.creating - end -end diff --git a/test/models/card/statuses_test.rb b/test/models/card/statuses_test.rb index 5a3e2cb8b..2ad983790 100644 --- a/test/models/card/statuses_test.rb +++ b/test/models/card/statuses_test.rb @@ -1,12 +1,10 @@ require "test_helper" class Card::StatusesTest < ActiveSupport::TestCase - test "cards start out in a `creating` state" do + test "cards start out in a `drafted` state" do card = collections(:writebook).cards.create! creator: users(:kevin), title: "Newly created card" - assert card.creating? - assert_not_includes Card.published_or_drafted_by(users(:kevin)), card - assert_not_includes Card.published_or_drafted_by(users(:jz)), card + assert card.drafted? end test "cards are only visible to the creator when drafted" do @@ -51,38 +49,4 @@ class Card::StatusesTest < ActiveSupport::TestCase assert_equal card, Event.last.eventable assert_equal "card_published", Event.last.action end - - test "can_recover_abandoned_creation?" do - card = collections(:writebook).cards.create! creator: users(:kevin) - unsaved_card = collections(:writebook).cards.new creator: users(:kevin) - - assert_not unsaved_card.can_recover_abandoned_creation? - - card.update!(title: "Something worth keeping") - assert unsaved_card.can_recover_abandoned_creation? - end - - test "recover_abandoned_creation" do - card_edited = collections(:writebook).cards.create! creator: users(:kevin) - card_edited.update!(title: "Something worth keeping") - - card_not_edited = collections(:writebook).cards.create! creator: users(:kevin) - - assert card_not_edited.can_recover_abandoned_creation? - - assert_equal card_edited, card_not_edited.recover_abandoned_creation - - assert_raises(ActiveRecord::RecordNotFound) { card_not_edited.reload } - end - - test "remove_abandoned_creations" do - card_old = collections(:writebook).cards.create! creator: users(:kevin), updated_at: 2.days.ago - card_recent = collections(:writebook).cards.create! creator: users(:kevin) - - assert_equal 2, Card.creating.count - - Card.remove_abandoned_creations - - assert_equal [ card_recent ], Card.creating - end end From 1df8d428114160a1900204579104c295d32ab990 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Sun, 2 Nov 2025 11:29:35 +0100 Subject: [PATCH 047/173] Create cards with CTRL/CMD enter --- app/views/cards/container/footer/_draft.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb index 1c39e6ad0..a2317ce08 100644 --- a/app/views/cards/container/footer/_draft.html.erb +++ b/app/views/cards/container/footer/_draft.html.erb @@ -1,5 +1,6 @@
- <%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn" %> + <%= 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 "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" } %> From 1d6cc37bb4dc50978cc9b840e031611184d60b9d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 10:57:35 +0100 Subject: [PATCH 048/173] Add missing blank content validation --- app/models/step.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/step.rb b/app/models/step.rb index c4e35f33e..c2063dfaa 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -3,6 +3,8 @@ class Step < ApplicationRecord scope :completed, -> { where(completed: true) } + validates :content, presence: true + def completed? completed end From da6d1011fd60cfeef3ff9c6b5c3377851e123b12 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 10:57:58 +0100 Subject: [PATCH 049/173] Replace card replacement with stream actions to prevent flickering when managing steps --- app/controllers/cards/steps_controller.rb | 3 --- app/views/cards/display/perma/_steps.html.erb | 4 ++-- app/views/cards/steps/create.turbo_stream.erb | 3 +++ app/views/cards/steps/destroy.turbo_stream.erb | 1 + app/views/cards/steps/update.turbo_stream.erb | 3 +++ test/controllers/cards/steps_controller_test.rb | 10 +++++----- 6 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 app/views/cards/steps/create.turbo_stream.erb create mode 100644 app/views/cards/steps/destroy.turbo_stream.erb create mode 100644 app/views/cards/steps/update.turbo_stream.erb diff --git a/app/controllers/cards/steps_controller.rb b/app/controllers/cards/steps_controller.rb index 4f3434632..305890508 100644 --- a/app/controllers/cards/steps_controller.rb +++ b/app/controllers/cards/steps_controller.rb @@ -5,7 +5,6 @@ class Cards::StepsController < ApplicationController def create @step = @card.steps.create!(step_params) - render_card_replacement end def show @@ -16,12 +15,10 @@ class Cards::StepsController < ApplicationController def update @step.update!(step_params) - render_card_replacement end def destroy @step.destroy! - render_card_replacement end private diff --git a/app/views/cards/display/perma/_steps.html.erb b/app/views/cards/display/perma/_steps.html.erb index 5ce71f721..6cca251d2 100644 --- a/app/views/cards/display/perma/_steps.html.erb +++ b/app/views/cards/display/perma/_steps.html.erb @@ -1,9 +1,9 @@
    <%= render partial: "cards/steps/step", collection: card.steps, as: :step %> -
  1. +
  2. - <%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %> + <%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit turbo:submit-end->form#reset" } do |form| %> <%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", autocomplete: "off", data: { form_target: "input" }, aria: { label: "Add a step" } %> <% end %>
  3. diff --git a/app/views/cards/steps/create.turbo_stream.erb b/app/views/cards/steps/create.turbo_stream.erb new file mode 100644 index 000000000..399593809 --- /dev/null +++ b/app/views/cards/steps/create.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.before dom_id(@card, :new_step) do %> + <%= render "cards/steps/step", step: @step %> +<% end %> diff --git a/app/views/cards/steps/destroy.turbo_stream.erb b/app/views/cards/steps/destroy.turbo_stream.erb new file mode 100644 index 000000000..a5c5deefb --- /dev/null +++ b/app/views/cards/steps/destroy.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.remove @step %> diff --git a/app/views/cards/steps/update.turbo_stream.erb b/app/views/cards/steps/update.turbo_stream.erb new file mode 100644 index 000000000..24fd7f934 --- /dev/null +++ b/app/views/cards/steps/update.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.replace @step do %> + <%= render "cards/steps/step", step: @step %> +<% end %> diff --git a/test/controllers/cards/steps_controller_test.rb b/test/controllers/cards/steps_controller_test.rb index 677279a2a..72c24f071 100644 --- a/test/controllers/cards/steps_controller_test.rb +++ b/test/controllers/cards/steps_controller_test.rb @@ -10,7 +10,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { card.steps.count }, +1 do post card_steps_path(card), params: { step: { content: "Research alternatives" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :before, target: dom_id(card, :new_step) end assert_equal "Research alternatives", card.steps.last.content @@ -22,7 +22,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_changes -> { step.reload.content }, from: "Original content", to: "Updated content" do put card_step_path(card, step), params: { step: { content: "Updated content" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end end @@ -32,7 +32,7 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { card.steps.count }, -1 do delete card_step_path(card, step), as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :remove, target: dom_id(step) end end @@ -43,13 +43,13 @@ class Cards::StepsControllerTest < ActionDispatch::IntegrationTest # Toggle to completed assert_changes -> { step.reload.completed? }, from: false, to: true do put card_step_path(card, step), params: { step: { completed: "1" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end # Toggle back to incomplete assert_changes -> { step.reload.completed? }, from: true, to: false do put card_step_path(card, step), params: { step: { completed: "0" } }, as: :turbo_stream - assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(step) end end end From 5f7feed6c4b98b26c83c426f74d985f6e4245cf1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 12:48:57 +0100 Subject: [PATCH 050/173] Refresh activity-related bits in the card perma dynamically: - Remove entropy bubble - Refresh the auto close notice https://app.box-car.com/5986089/cards/2683 --- app/models/card/eventable.rb | 5 +++++ .../card/display/_refresh_activity.turbo_stream.erb | 4 ++++ app/views/cards/container/_closure.html.erb | 13 ++++++++----- app/views/cards/display/preview/_bubble.html.erb | 5 ++++- app/views/cards/show.html.erb | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 app/views/card/display/_refresh_activity.turbo_stream.erb diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index 520fd95d4..c52fb7a29 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -19,6 +19,7 @@ module Card::Eventable def touch_last_active_at # Not using touch so that we can detect attribute change on callbacks update!(last_active_at: Time.current) + broadcast_activity end private @@ -35,4 +36,8 @@ module Card::Eventable def create_system_comment_for(event) SystemCommenter.new(self, event).comment end + + def broadcast_activity + broadcast_render_later_to self, :activity, partial: "card/display/refresh_activity", locals: { card: self } + end end diff --git a/app/views/card/display/_refresh_activity.turbo_stream.erb b/app/views/card/display/_refresh_activity.turbo_stream.erb new file mode 100644 index 000000000..a5a603252 --- /dev/null +++ b/app/views/card/display/_refresh_activity.turbo_stream.erb @@ -0,0 +1,4 @@ +<%= turbo_stream.remove dom_id(card, "bubble") %> +<%= turbo_stream.replace dom_id(card, :card_closure_toggle) do %> + <%= render "cards/container/closure", card: card %> +<% end %> diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index bad02f3cd..07d227c4f 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -10,10 +10,13 @@ <%= button_to card_closure_path(card), class: "btn borderless" do %> Mark as Done <% end %> - <% if card.entropic? && card.open? %> -
    - Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there’s no activity. -
    - <% end %> + +
    + <% if card.entropic? && card.open? %> +
    + Closes as "Not Now" <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there's no activity. +
    + <% end %> +
    <% end %>
diff --git a/app/views/cards/display/preview/_bubble.html.erb b/app/views/cards/display/preview/_bubble.html.erb index af2d71d04..c39772090 100644 --- a/app/views/cards/display/preview/_bubble.html.erb +++ b/app/views/cards/display/preview/_bubble.html.erb @@ -1,4 +1,7 @@ -<%= tag.div hidden: true, class: "bubble", +<%= tag.div \ + id: dom_id(card, "bubble"), + hidden: true, + class: "bubble", data: { controller: "bubble", action: "turbo:morph-element->bubble#update", diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index a300bafff..9ce4cf342 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -11,7 +11,7 @@
<% end %> -<%= turbo_stream_from @card %> +<%= turbo_stream_from @card, :activity %>
<%= render "cards/container", card: @card %> From 37ce2c98d10d573c2ba1cceb27738a4375ab7600 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 13:04:21 +0100 Subject: [PATCH 051/173] Refresh the cards perma dynamically with page refreshes --- app/models/card.rb | 6 +++--- app/models/card/broadcastable.rb | 7 +++++++ app/views/cards/display/common/_assignees.html.erb | 2 +- app/views/cards/display/perma/_assignees.html.erb | 2 +- app/views/cards/display/perma/_collection.html.erb | 4 ++-- app/views/cards/display/perma/_tags.html.erb | 4 ++-- app/views/cards/show.html.erb | 1 + app/views/cards/triage/_columns.html.erb | 2 +- 8 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 app/models/card/broadcastable.rb diff --git a/app/models/card.rb b/app/models/card.rb index 3b455a424..cdc32824d 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,7 +1,7 @@ class Card < ApplicationRecord - include Assignable, Attachments, Closeable, Colored, Entropic, Eventable, - Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, - Searchable, Stallable, Statuses, Taggable, Triageable, Watchable + include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic, + Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, + Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/card/broadcastable.rb b/app/models/card/broadcastable.rb new file mode 100644 index 000000000..aa24b776c --- /dev/null +++ b/app/models/card/broadcastable.rb @@ -0,0 +1,7 @@ +module Card::Broadcastable + extend ActiveSupport::Concern + + included do + broadcasts_refreshes + end +end diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index 19aebd6a9..c07ec9e6d 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -9,7 +9,7 @@ Assign - + <%= yield %>
diff --git a/app/views/cards/display/perma/_assignees.html.erb b/app/views/cards/display/perma/_assignees.html.erb index 1c530263e..a115f39b2 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) %> + <%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %> <% end %> diff --git a/app/views/cards/display/perma/_collection.html.erb b/app/views/cards/display/perma/_collection.html.erb index a9e0cbbfa..d660f252d 100644 --- a/app/views/cards/display/perma/_collection.html.erb +++ b/app/views/cards/display/perma/_collection.html.erb @@ -6,8 +6,8 @@ Choose a collection for this card - - <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top" %> + + <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %>
<% end %> diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index 91c112540..e72041211 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -5,8 +5,8 @@ Add a tag - - <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card) %> + + <%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 9ce4cf342..c33a2c40b 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/triage/_columns.html.erb b/app/views/cards/triage/_columns.html.erb index 2ceaab98b..591a8e256 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" %> +<%= turbo_frame_tag dom_id(card, :columns), src: edit_card_column_path(card), target: "_top", refresh: "morph" %> From 38a7a1446603e935560beba1bc0ce5cadc05b48a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:40:02 +0100 Subject: [PATCH 052/173] Remove vector extension that we are no longer using --- Gemfile | 3 --- Gemfile.lock | 5 ----- config/database.yml | 4 ---- 3 files changed, 12 deletions(-) diff --git a/Gemfile b/Gemfile index 533a87e9b..30f8a6483 100644 --- a/Gemfile +++ b/Gemfile @@ -55,9 +55,6 @@ gem "prometheus-client-mmap", "~> 1.1" gem "autotuner" gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings -# AI -gem "sqlite-vec", "0.1.7.alpha.2" - group :development, :test do gem "debug" gem "bundler-audit", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 32494ed59..09e284b0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -508,10 +508,6 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite-vec (0.1.7.alpha.2-arm64-darwin) - sqlite-vec (0.1.7.alpha.2-arm64-linux) - sqlite-vec (0.1.7.alpha.2-x86_64-darwin) - sqlite-vec (0.1.7.alpha.2-x86_64-linux) sqlite3 (2.7.4) mini_portile2 (~> 2.8.0) sqlite3 (2.7.4-arm64-darwin) @@ -646,7 +642,6 @@ DEPENDENCIES solid_cable (>= 3.0) solid_cache (~> 1.0) solid_queue (~> 1.2) - sqlite-vec (= 0.1.7.alpha.2) sqlite3 (>= 2.0) stimulus-rails thruster diff --git a/config/database.yml b/config/database.yml index b4b295cce..de3b50a9b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -8,8 +8,6 @@ default: &default adapter: sqlite3 pool: 50 timeout: 5000 - extensions: - - <%= SqliteVec.loadable_path %> development: primary: @@ -54,8 +52,6 @@ production: &production adapter: beamer database: storage/tenants/<%= Rails.env %>/%{tenant}/db/main.sqlite3 tenanted: true - extensions: - - <%= SqliteVec.loadable_path %> untenanted: <<: *default adapter: beamer From 225a70fc527a7b2de9895b1cdc9afddaded7b325 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:40:20 +0100 Subject: [PATCH 053/173] Remove here too --- config/application.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/application.rb b/config/application.rb index c39007fb2..6fe42e9ef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,8 +2,6 @@ require_relative "boot" require "rails/all" -require "sqlite_vec" # Referenced in database.yml - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) From 0a9e1620a0fd0bf916e7165a50c80ea2b8017175 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:46:22 +0100 Subject: [PATCH 054/173] No longer needed --- app/controllers/collections/columns_controller.rb | 2 +- app/controllers/public/collections/columns_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index 461b5bd1a..a6c9103c5 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -1,5 +1,5 @@ class Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CollectionScoped + include CollectionScoped before_action :set_column, only: %i[ show update destroy ] diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb index dcc2b46a3..6d882bc16 100644 --- a/app/controllers/public/collections/columns_controller.rb +++ b/app/controllers/public/collections/columns_controller.rb @@ -1,5 +1,5 @@ class Public::Collections::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CachedPublicly, PublicCollectionScoped + include CachedPublicly, PublicCollectionScoped allow_unauthenticated_access only: :show allow_unauthorized_access only: :show From aeb23b691c2a2d3e79a66961db739e5c5f4105d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 13:46:34 +0100 Subject: [PATCH 055/173] Always use templates for turbo streaming to stay consistentn --- app/controllers/columns/cards/drops/columns_controller.rb | 8 +++----- .../columns/cards/drops/columns/create.turbo_stream.erb | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 app/views/columns/cards/drops/columns/create.turbo_stream.erb diff --git a/app/controllers/columns/cards/drops/columns_controller.rb b/app/controllers/columns/cards/drops/columns_controller.rb index cb2c3b4a8..3de47992e 100644 --- a/app/controllers/columns/cards/drops/columns_controller.rb +++ b/app/controllers/columns/cards/drops/columns_controller.rb @@ -1,10 +1,8 @@ class Columns::Cards::Drops::ColumnsController < ApplicationController - include ActionView::RecordIdentifier, CardScoped + include CardScoped def create - column = @card.collection.columns.find(params[:column_id]) - @card.triage_into(column) - - render turbo_stream: turbo_stream.replace(dom_id(column), partial: "collections/show/column", method: :morph, locals: { column: column }) + @column = @card.collection.columns.find(params[:column_id]) + @card.triage_into(@column) end end diff --git a/app/views/columns/cards/drops/columns/create.turbo_stream.erb b/app/views/columns/cards/drops/columns/create.turbo_stream.erb new file mode 100644 index 000000000..5ab9915a0 --- /dev/null +++ b/app/views/columns/cards/drops/columns/create.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.replace(dom_id(@column), partial: "collections/show/column", method: :morph, locals: { column: @column }) %> From 2f1ee30a2b4cd1fb6a91004483c8d14139bab96f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:20:57 +0100 Subject: [PATCH 056/173] Remove duplicated nesting --- config/routes.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c8e89fb66..bff2cfcfd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ Rails.application.routes.draw do resources :users do scope module: :users do + resource :avatar resource :role resource :events resources :push_subscriptions @@ -139,12 +140,6 @@ Rails.application.routes.draw do end end - resources :users do - scope module: :users do - resource :avatar - end - end - resources :commands resource :conversation do From 3ec9308345a7f901e9edac7b9fa915727a238a0c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:06 +0100 Subject: [PATCH 057/173] Always bang for exceptions --- app/controllers/users/roles_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users/roles_controller.rb b/app/controllers/users/roles_controller.rb index 8cffa232b..f9eb7d232 100644 --- a/app/controllers/users/roles_controller.rb +++ b/app/controllers/users/roles_controller.rb @@ -3,7 +3,7 @@ class Users::RolesController < ApplicationController before_action :ensure_permission_to_administer_user def update - @user.update(role_params) + @user.update!(role_params) redirect_to users_path end From 4c2f3ef0b84e474e320207342ac0d974528c34b5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:25 +0100 Subject: [PATCH 058/173] Style --- app/controllers/users/avatars_controller.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 180a7c506..5e29250c0 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -6,25 +6,17 @@ class Users::AvatarsController < ApplicationController before_action :set_user def show - if stale? @user, cache_control: { max_age: cache_max_age, stale_while_revalidate: 1.week } + if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact render_avatar_or_initials end end def destroy @user.avatar.destroy - redirect_to user_path(@user) + redirect_to @user end private - def cache_max_age - if Current.user == @user - 0 - else - 30.minutes - end - end - def set_user @user = User.find(params[:user_id]) end From b05e7c04e16104bc6779bf36e2e7c29692cc058f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:48 +0100 Subject: [PATCH 059/173] Ensure you can only delete your own avatar or else you have to be admin --- app/controllers/users/avatars_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 5e29250c0..8be049cc3 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -4,6 +4,7 @@ class Users::AvatarsController < ApplicationController allow_unauthenticated_access only: :show before_action :set_user + before_action :ensure_permission_to_administer_user, only: :destroy def show if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact @@ -21,6 +22,10 @@ class Users::AvatarsController < ApplicationController @user = User.find(params[:user_id]) end + def ensure_permission_to_administer_user + head :forbidden unless Current.user.admin? || Current.user == @user + end + def render_avatar_or_initials if @user.avatar.attached? send_blob_stream @user.avatar From c25fcc087a876aab1c0518510dca528791c80314 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:21:53 +0100 Subject: [PATCH 060/173] Test avatars controller --- .../users/avatars_controller_test.rb | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/controllers/users/avatars_controller_test.rb diff --git a/test/controllers/users/avatars_controller_test.rb b/test/controllers/users/avatars_controller_test.rb new file mode 100644 index 000000000..4fa45c2c9 --- /dev/null +++ b/test/controllers/users/avatars_controller_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :david + end + + test "show self without caching" do + get user_avatar_path(users(:david)) + assert_match "image/svg+xml", @response.content_type + assert_nil @response.cache_control[:max_age] + end + + test "show other with caching" do + get user_avatar_path(users(:kevin)) + assert_match "image/svg+xml", @response.content_type + assert_equal 30.minutes.to_s, @response.cache_control[:max_age] + end + + test "delete self" do + delete user_avatar_path(users(:david)) + assert_redirected_to users(:david) + end + + test "unable to delete other" do + delete user_avatar_path(users(:kevin)) + assert_response :forbidden + end +end From f9ff13cdc4181413984915641041abe471e0e1f5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:25:32 +0100 Subject: [PATCH 061/173] Add http caching for user events --- app/controllers/users/events_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/users/events_controller.rb b/app/controllers/users/events_controller.rb index 76b85dd58..2f1e3afb0 100644 --- a/app/controllers/users/events_controller.rb +++ b/app/controllers/users/events_controller.rb @@ -6,6 +6,8 @@ class Users::EventsController < ApplicationController def show @filter = Current.user.filters.new(creator_ids: [ @user.id ]) @day_timeline = Current.user.timeline_for(day_param, filter: @filter) + + fresh_when @day_timeline end private From 992b149395ee96f51639b09dd5b79e170fc8ac3c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:29:01 +0100 Subject: [PATCH 062/173] No longer used --- app/models/user/filtering.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index c9f049ab3..fdc7f2bcb 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -1,6 +1,4 @@ class User::Filtering - include Rails.application.routes.url_helpers - attr_reader :user, :filter, :expanded delegate :as_params, :single_collection, to: :filter From 61d05ee073cabcb029fa93a64be09ca7a2ecb82f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:31:16 +0100 Subject: [PATCH 063/173] No longer used --- app/helpers/ai_helper.rb | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 app/helpers/ai_helper.rb diff --git a/app/helpers/ai_helper.rb b/app/helpers/ai_helper.rb deleted file mode 100644 index 171dcda01..000000000 --- a/app/helpers/ai_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -require "zlib" - -module AiHelper - LLM_MODELS = %w[ - gpt-5-chat-latest - gpt-5-mini - gpt-5-nano - gpt-5 - chatgpt-4o-latest - gpt-4.1 - gpt-3.5-turbo - gpt-4.1-mini - gpt-4.1-nano - gpt-4o-mini - ] - - def llm_model_options - LLM_MODELS.map { |model| [ model, model ] } - end -end From c007176ed046aaf59459c5f26bc9035cf2254769 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 3 Nov 2025 14:35:06 +0100 Subject: [PATCH 064/173] Don't broadcast card changes when touching activity spikes We do this from jobs, so there is no request id to discard the page refresh in the client. We don't want page refreshes in these cases, as those are normally handled via the change that originated this detection, in any case. --- app/models/card/activity_spike/detector.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/card/activity_spike/detector.rb b/app/models/card/activity_spike/detector.rb index 6721b946f..63f246ca0 100644 --- a/app/models/card/activity_spike/detector.rb +++ b/app/models/card/activity_spike/detector.rb @@ -20,10 +20,12 @@ class Card::ActivitySpike::Detector end def register_activity_spike - if card.activity_spike - card.activity_spike.touch - else - card.create_activity_spike! + Card.suppressing_turbo_broadcasts do + if card.activity_spike + card.activity_spike.touch + else + card.create_activity_spike! + end end end From 1277ec53475684447fdd4974ea8132933e326091 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 14:34:08 +0100 Subject: [PATCH 065/173] Extract jump_field_tag helper --- app/helpers/my/menu_helper.rb | 19 +++++++++++++++++++ app/views/my/menus/show.html.erb | 17 ++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 app/helpers/my/menu_helper.rb diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb new file mode 100644 index 000000000..a972a3408 --- /dev/null +++ b/app/helpers/my/menu_helper.rb @@ -0,0 +1,19 @@ +module My::MenuHelper + def jump_field_tag + text_field_tag :search, nil, + type: "search", + role: "combobox", + placeholder: "Type to jump to a collection, person, place, or tag…", + class: "input input--transparent txt-small", + autofocus: true, + autocorrect: "off", + autocomplete: "off", + aria: { activedescendant: "" }, + data: { + "1p-ignore": "true", + filter_target: "input", + nav_section_expander_target: "input", + navigable_list_target: "input", + action: "input->filter#filter" } + end +end diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index 077139170..4490d1088 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -1,20 +1,7 @@ <%= turbo_frame_tag "my_menu", target: "_top" do %>
- <%= text_field_tag :search, nil, - type: "search", - role: "combobox", - placeholder: "Type to jump to a collection, person, place, or tag…", - class: "input input--transparent txt-small", - autofocus: true, - autocorrect: "off", - autocomplete: "off", - aria: { activedescendant: "" }, - data: { - "1p-ignore": "true", - filter_target: "input", - nav_section_expander_target: "input", - navigable_list_target: "input", - action: "input->filter#filter" } %> + <%= jump_field_tag %> + +
> + - - <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %> - -
- <% end %> + + <%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %> + +
<% end %> From d3cb3a3a2da55349d4e24b6fba0de287993647ed Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 17:17:44 +0100 Subject: [PATCH 095/173] Use etags for my pins --- app/controllers/my/pins_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index f361b6af7..27e14535b 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -1,5 +1,6 @@ class My::PinsController < ApplicationController def index @pins = Current.user.pins.includes(:card).ordered.limit(20) + fresh_when @pins end end From 6570f5fc04ebf60d3b18e69c1e10034ef64755f3 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 3 Nov 2025 17:22:38 +0100 Subject: [PATCH 096/173] Use etag for assignments/new --- app/controllers/cards/assignments_controller.rb | 2 ++ app/views/cards/assignments/new.html.erb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index 9516ebc2f..80b6c8149 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -2,6 +2,8 @@ class Cards::AssignmentsController < ApplicationController include CardScoped def new + @users = @collection.users.active.alphabetically + fresh_when @users end def create diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index ee7f8a74c..8642e2c0a 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -7,13 +7,13 @@ navigable_list_actionable_items_value: true } do %> Assign this to… - <% if @collection.users.active.count > 1 %> + <% if @users.many? %> <%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true, type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> <% end %>
<% end %> From 886e38547e347fa4907cf878ce7765a245fef1d4 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 11:50:36 -0600 Subject: [PATCH 102/173] This shouldn't be visible when the card is already postponed Fixes: https://app.box-car.com/5986089/cards/128 --- app/views/cards/container/_closure.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index 07d227c4f..c40da47b9 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -12,9 +12,9 @@ <% end %>
- <% if card.entropic? && card.open? %> + <% if card.entropic? && card.open? && !card.postponed? %>
- Closes as "Not Now" <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there's no activity. + Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there’s no activity.
<% end %>
From 030ff2b11c4a856e30657f9af5c0d920c7860455 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 12:10:07 -0600 Subject: [PATCH 103/173] Stands in for `` while editing --- app/assets/stylesheets/rich-text-content.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 9832dbc93..4046157e7 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -61,6 +61,10 @@ display: none; } + .lexxy-content__strikethrough { + text-decoration: line-through; + } + /* Code */ :where(code, pre) { background-color: var(--color-canvas); From 5cac52fd721e595e4be1788c9f7bf2fec5eb1feb Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 12:16:03 -0600 Subject: [PATCH 104/173] Space between undo/redo and the rest of the toolbar buttons --- app/assets/stylesheets/lexxy.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/lexxy.css b/app/assets/stylesheets/lexxy.css index 5d0f37dc3..e3f8f8cbd 100644 --- a/app/assets/stylesheets/lexxy.css +++ b/app/assets/stylesheets/lexxy.css @@ -163,6 +163,10 @@ z-index: 1; } + :where(.lexxy-editor__toolbar-spacer) { + flex: 1; + } + /* Based on .input, .input--select */ .lexxy-code-language-picker { -webkit-appearance: none; From 9cd8d2fbf4e8b3304141b052ea4e10813d07f078 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 12:40:16 -0600 Subject: [PATCH 105/173] Style dividers --- app/assets/stylesheets/rich-text-content.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 4046157e7..72cba909b 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -61,6 +61,17 @@ display: none; } + :where(hr) { + block-size: 0; + border-block-end: 1px solid currentColor; + inline-size: 20%; + margin: var(--block-margin) 0; + } + + .horizontal-divider { + margin-inline: 0; + } + .lexxy-content__strikethrough { text-decoration: line-through; } From 9ca4d17a03db438386931c366b4a1b6f37ddb136 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 12:53:01 -0600 Subject: [PATCH 106/173] We won't need this now that we have a playground account --- app/assets/stylesheets/events.css | 11 ----------- app/views/events/index.html.erb | 10 ---------- app/views/my/menus/_places.html.erb | 1 - 3 files changed, 22 deletions(-) diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 660fda3bd..42d129125 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -225,17 +225,6 @@ text-align: center; } - .events__welcome-video { - .events:has(.day-timeline-pagination-link) + & { - display: none !important; - } - - iframe { - aspect-ratio: 16 / 9; - inline-size: 100%; - } - } - /* Event /* ------------------------------------------------------------------------ */ diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 4bdc6b8c7..ca071f189 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -23,13 +23,3 @@ <%= day_timeline_pagination_link(@day_timeline, @filter) %> <% end %> <% end %> - -
- -
diff --git a/app/views/my/menus/_places.html.erb b/app/views/my/menus/_places.html.erb index 8b7be5b73..53802ce7d 100644 --- a/app/views/my/menus/_places.html.erb +++ b/app/views/my/menus/_places.html.erb @@ -1,5 +1,4 @@ <%= collapsible_nav_section "Jump to…" do %> - <%= filter_place_menu_item "https://www.youtube.com/watch?v=rNKRdk7DLHM", "BOXCAR beta orientation", "youtube", new_window: true %> <%= 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" %> From 8538c21d4ec667a1bfc835dd1d816bec8aa6a561 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 13:39:55 -0500 Subject: [PATCH 107/173] Copy Cloudflare's True-Client-IP header to X-Forwarded-For ref: https://3.basecamp.com/2914079/buckets/37331921/todos/9243121525 --- config/initializers/true_client_ip.rb | 24 +++++++++++++++++++ test/lib/true_client_ip_test.rb | 34 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 config/initializers/true_client_ip.rb create mode 100644 test/lib/true_client_ip_test.rb diff --git a/config/initializers/true_client_ip.rb b/config/initializers/true_client_ip.rb new file mode 100644 index 000000000..cac696107 --- /dev/null +++ b/config/initializers/true_client_ip.rb @@ -0,0 +1,24 @@ +# +# Cloudflare sets a True-Client-IP header, which for most 37signals apps gets copied to +# X-Forwarded-For by an iRule on the F5 load balancers: +# +# https://github.com/basecamp/f5-tf/blob/1543f7bfa3961a79e397f80cf041d75567f1b2f8/ams-base/iRules/manage_x_forwarded.tcl +# +# However, for Fizzy the F5s are configured to do passthrough, so the header value isn't being +# copied for us. Let's do that bit of work here, before Rails' RemoteIp middleware. +# +class TrackTrueClientIp + def initialize(app) + @app = app + end + + def call(env) + if env["HTTP_TRUE_CLIENT_IP"].present? + env["HTTP_X_FORWARDED_FOR"] = env["HTTP_TRUE_CLIENT_IP"] + end + + @app.call(env) + end +end + +Rails.application.config.middleware.insert_before ActionDispatch::RemoteIp, TrackTrueClientIp diff --git a/test/lib/true_client_ip_test.rb b/test/lib/true_client_ip_test.rb new file mode 100644 index 000000000..5c8c2e4d9 --- /dev/null +++ b/test/lib/true_client_ip_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class TrackTrueClientIpTest < ActiveSupport::TestCase + setup do + @app = ->(env) { [ 200, {}, [ "OK" ] ] } + @middleware = TrackTrueClientIp.new(@app) + end + + test "sets X-Forwarded-For header when True-Client-IP header is present" do + env = { "HTTP_TRUE_CLIENT_IP" => "123.123.123.123" } + @middleware.call(env) + assert_equal "123.123.123.123", env["HTTP_X_FORWARDED_FOR"] + end + + test "does not modify environment when True-Client-IP header is absent" do + env = {} + @middleware.call(env) + assert_nil env["HTTP_X_FORWARDED_FOR"] + + env = { "HTTP_X_FORWARDED_FOR" => "234.234.234.234" } + @middleware.call(env) + assert_equal "234.234.234.234", env["HTTP_X_FORWARDED_FOR"] + end + + test "calls the next middleware in the stack" do + called = false + app = ->(env) { called = true; [ 200, {}, [ "OK" ] ] } + middleware = TrackTrueClientIp.new(app) + + middleware.call({}) + + assert called + end +end From e682f2742d38fc585c594135e67975fef7aec6c3 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 15:12:39 -0500 Subject: [PATCH 108/173] test: Fix the flaky smoke test on attachments --- test/system/smoke_test.rb | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index 7e0b5e74b..cc84bfef2 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -22,11 +22,17 @@ class SmokeTest < ApplicationSystemTestCase click_on "Upload file" end - assert_image_figure_attachment content_type: "image/jpeg", caption: "moon.jpg" + within("form lexxy-editor figure.attachment[data-content-type='image/jpeg']") do + assert_selector "img[src*='/rails/active_storage']" + assert_selector "figcaption input[placeholder='moon.jpg']" + end click_on "Post this comment" - assert_image_figure_attachment content_type: "image/jpeg", caption: "moon.jpg" + within("action-text-attachment") do + assert_selector "a img[src*='/rails/active_storage']" + assert_selector "figcaption span.attachment__name", text: "moon.jpg" + end end test "dismissing notifications" do @@ -53,16 +59,4 @@ class SmokeTest < ApplicationSystemTestCase editor_element.set with page.execute_script("arguments[0].value = '#{with}'", editor_element) end - - def assert_figure_attachment(content_type:, &block) - figure = find("figure.attachment[data-content-type='#{content_type}']") - within(figure, &block) if block_given? - end - - def assert_image_figure_attachment(content_type: "image/png", caption:) - assert_figure_attachment(content_type: content_type) do - assert_selector "img[src*='/rails/active_storage']", wait: 10 - assert_selector "figcaption input[placeholder='#{caption}']", wait: 10 - end - end end From 75e27c3e24344b5f8dda1ccbf09a2cf217b6959e Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 14:18:12 -0600 Subject: [PATCH 109/173] First pass a seeds based on JF's playground project Videos TBD --- app/models/account/seeder.rb | 104 ++++++++++++++--------------------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 28e977c29..7480ca88f 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -26,77 +26,57 @@ class Account::Seeder def populate # --------------- - # Bugs Collection + # Playground Collection # --------------- - bug_tracker = Collection.create! name: "Bugs", creator: creator, all_access: true - - # Columns - triage_column = bug_tracker.columns.create! name: "Triage", color: "#ef4444" - in_progress_column = bug_tracker.columns.create! name: "In Progress", color: "#f97316" - resolved_column = bug_tracker.columns.create! name: "Resolved", color: "#22c55e" + playground = Collection.create! name: "Playground", creator: creator, all_access: true # Cards - bug_tracker.cards.create! creator: creator, column: triage_column, title: "Login button not responding on mobile", status: "published", description: <<~HTML -

Users are reporting that the login button is not responding on mobile devices.

- -

Steps to reproduce:

-
    -
  • Open the app on mobile browser
  • -
  • Navigate to login page
  • -
  • Tap the login button
  • -
  • Nothing happens
  • -
- -

This appears to be affecting iOS devices primarily.

- - + have_fun_card = playground.cards.create! creator: creator, title: "Have fun!", status: "published", description: <<~HTML +

Mess around, make more boards, add more cards, and get your work, issues, or ideas organized! Include a video of the full product walkthrough.

HTML - bug_tracker.cards.create! creator: creator, column: triage_column, title: "Search results showing duplicates", status: "published" - profile_crash_card = bug_tracker.cards.create! creator: creator, column: in_progress_column, title: "Profile page crashes when uploading large images", status: "published" - bug_tracker.cards.create! creator: creator, column: in_progress_column, title: "Email notifications not being sent", status: "published" - bug_tracker.cards.create! creator: creator, column: resolved_column, title: "Fix broken links in footer", status: "published" - # Comments - profile_crash_card.comments.create! creator: creator, body: <<~HTML - I can reproduce this with images over 5MB - - + playground.cards.create! creator: creator, title: "Head back home to check out activity", status: "published", description: <<~HTML +

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

HTML - profile_crash_card.comments.create! creator: creator, body: "Looking into adding client-side image compression before upload" - # ---------------------------- - # Feature Requests Collection - # ---------------------------- - feature_requests = Collection.create! name: "Feature Requests", creator: creator, all_access: true - - # Columns - backlog_column = feature_requests.columns.create! name: "Backlog", color: "#6366f1" - planning_column = feature_requests.columns.create! name: "Planning", color: "#8b5cf6" - - # Cards - feature_requests.cards.create! creator: creator, column: backlog_column, title: "Add dark mode support", status: "published", description: <<~HTML -

Many users have requested a dark mode option for better usability in low-light environments.

- -

Proposed features:

-
    -
  • Toggle in user settings
  • -
  • Respect system preferences automatically
  • -
  • Smooth transition between themes
  • -
  • Persistent theme selection across sessions
  • -
- -

This would improve accessibility and reduce eye strain for our users.

- - + 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 type “3” any time.

HTML - feature_requests.cards.create! creator: creator, column: backlog_column, title: "Export data to CSV", status: "published" - feature_requests.cards.create! creator: creator, column: backlog_column, title: "Add keyboard shortcuts for navigation", status: "published" - two_factor_card = feature_requests.cards.create! creator: creator, column: planning_column, title: "Implement two-factor authentication", status: "published" - feature_requests.cards.create! creator: creator, column: planning_column, title: "Add bulk actions for managing items", status: "published" - # Comments - two_factor_card.comments.create! creator: creator, body: "Should we support SMS and authenticator apps?" - two_factor_card.comments.create! creator: creator, body: "Let's start with authenticator apps only to keep it simple" + playground.cards.create! creator: creator, title: "Grab the invite link to invite someone else", status: "published", description: <<~HTML +

Pull down the Fizzy menu at the top of the screen, select “Account” or just hit 6, then grab the invite link over on the left side. 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: "Grab the invite link to invite someone else", status: "published", description: <<~HTML +

Pull down the Fizzy menu at the top of the screen, select “Account” or just hit 6, then grab the invite link over on the left side. 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: "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” the move it to YES", status: "published", description: <<~HTML +

Click the little Tag icon, type Design, then save. 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 +
    +
  1. Make one called "Yes"
  2. +
  3. Make another called "Working on"
  4. +
+


+

Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.

+


+

After that, drag this card to “DONE” or select “DONE” in the sidebar.

+ HTML + + playground.cards.create! creator: creator, title: "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 +

Click the title and you can rename the card, change the description, or add more information to the card.

+ HTML end def delete_everything From 2df8e159ae1b389642fb2afdc43c3e6fa9bdd497 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 14:18:24 -0600 Subject: [PATCH 110/173] Typo --- app/models/account/seeder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 7480ca88f..350651615 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -55,7 +55,7 @@ class Account::Seeder

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

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

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

HTML From fbf7b6cda709c2f8594428d978aa840194628a74 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 14:27:20 -0600 Subject: [PATCH 111/173] No need to create a default collection The playground is enough --- app/models/account/seedeable.rb | 2 -- app/models/collection.rb | 9 --------- app/views/collections/edit/_delete.html.erb | 11 +++++------ test/models/account/seedeable_test.rb | 10 +--------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/app/models/account/seedeable.rb b/app/models/account/seedeable.rb index a4096c28e..3cceae251 100644 --- a/app/models/account/seedeable.rb +++ b/app/models/account/seedeable.rb @@ -3,8 +3,6 @@ module Account::Seedeable def setup_basic_template user = User.first - - Collection.create!(name: "Cards", creator: user, all_access: true) end def setup_customer_template diff --git a/app/models/collection.rb b/app/models/collection.rb index 504439e2e..7b756409d 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -12,13 +12,4 @@ class Collection < ApplicationRecord scope :alphabetically, -> { order("lower(name)") } scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) } - - after_destroy_commit :ensure_default_collection - - private - def ensure_default_collection - if Collection.count.zero? - Collection.create!(name: "Cards") - end - end end diff --git a/app/views/collections/edit/_delete.html.erb b/app/views/collections/edit/_delete.html.erb index 7f13e84b1..627bfd1bd 100644 --- a/app/views/collections/edit/_delete.html.erb +++ b/app/views/collections/edit/_delete.html.erb @@ -1,8 +1,7 @@ -<% if Current.user.collections.many? %> - <%= form_with model: collection, class: "txt-align-center margin-block-start-auto", method: :delete do |form| %> - <%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this board?" } do %> - <%= icon_tag "trash" %> - Delete this board - <% end %> +<%= form_with model: collection, class: "txt-align-center margin-block-start-auto", method: :delete do |form| %> + <%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this board?" } do %> + <%= icon_tag "trash" %> + Delete this board <% end %> <% end %> + diff --git a/test/models/account/seedeable_test.rb b/test/models/account/seedeable_test.rb index 010df5661..aba88a2a5 100644 --- a/test/models/account/seedeable_test.rb +++ b/test/models/account/seedeable_test.rb @@ -5,18 +5,10 @@ class Account::SedeableTest < ActiveSupport::TestCase @account = Account.sole end - test "setup_basic_template adds collections" do - assert_changes -> { Collection.count } do - @account.setup_basic_template - end - end - test "setup_customer_template adds collections, cards, and comments" do assert_changes -> { Collection.count } do assert_changes -> { Card.count } do - assert_changes -> { Comment.count } do - @account.setup_customer_template - end + @account.setup_customer_template end end end From 370137919bd9e48be1f65e2a5013fc62639d0125 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 16:16:50 -0600 Subject: [PATCH 112/173] Copy edits --- app/models/account/seeder.rb | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 350651615..d89c1287b 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -31,8 +31,12 @@ class Account::Seeder playground = Collection.create! name: "Playground", creator: creator, all_access: true # Cards - have_fun_card = playground.cards.create! creator: creator, title: "Have fun!", status: "published", description: <<~HTML -

Mess around, make more boards, add more cards, and get your work, issues, or ideas organized! Include a video of the full product walkthrough.

+ have_fun_card = playground.cards.create! creator: creator, title: "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 +

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 @@ -40,15 +44,11 @@ class Account::Seeder 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 type “3” any time.

+

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.

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

Pull down the Fizzy menu at the top of the screen, select “Account” or just hit 6, then grab the invite link over on the left side. 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: "Grab the invite link to invite someone else", status: "published", description: <<~HTML -

Pull down the Fizzy menu at the top of the screen, select “Account” or just hit 6, then grab the invite link over on the left side. You can give this link to someone else so they can make an login for themselves in your account.

+ playground.cards.create! creator: creator, title: "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 @@ -56,7 +56,7 @@ class Account::Seeder HTML playground.cards.create! creator: creator, title: "Tag this card “Design” then move it to YES", status: "published", description: <<~HTML -

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

+

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 @@ -71,11 +71,15 @@ class Account::Seeder HTML playground.cards.create! creator: creator, title: "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.

+

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 -

Click the title and you can rename the card, change the description, or add more information to the card.

+
    +
  1. Click the title and you can rename the card, change the description, or add more information to the card.
  2. +
  3. Then, hit "Mark as Done" at the bottom of the card.
  4. +
  5. Finally, hit “←Playground” in the top left of the screen to go back to the board.
  6. +
HTML end From 605955fdaffebfe6d3505e6a493f9f52b2f1d754 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 3 Nov 2025 17:29:25 -0500 Subject: [PATCH 113/173] Make old migration that depends on the sqlite vec0 module a no-op Once the sqlite 'vec0' module was removed from the codebase in 38a7a144 (following the table removal in 875a298f), this migration became unrunnable. So to make sure we can reconstruct the schema if necessary by running all the migrations, I'm replacing it with the creation of an empty table. --- .../20250723165724_create_search_embeddings.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/db/migrate/20250723165724_create_search_embeddings.rb b/db/migrate/20250723165724_create_search_embeddings.rb index 688d4a082..ad91e507e 100644 --- a/db/migrate/20250723165724_create_search_embeddings.rb +++ b/db/migrate/20250723165724_create_search_embeddings.rb @@ -1,10 +1,16 @@ class CreateSearchEmbeddings < ActiveRecord::Migration[7.1] def change - create_virtual_table :search_embeddings, :vec0, [ - "id INTEGER PRIMARY KEY", - "record_type TEXT NOT NULL", - "record_id INTEGER NOT NULL", - "embedding FLOAT[1536] distance_metric=cosine" - ] + # create_virtual_table :search_embeddings, :vec0, [ + # "id INTEGER PRIMARY KEY", + # "record_type TEXT NOT NULL", + # "record_id INTEGER NOT NULL", + # "embedding FLOAT[1536] distance_metric=cosine" + # ] + + # Above is the original migration. Once the sqlite 'vec0' module was removed from the codebase + # in 38a7a144 (following the table removal in 875a298f), this migration became unrunnable. So to + # make sure we can reconstruct the schema if necessary by running all the migrations, I'm + # replacing it with this empty table. + create_table :search_embeddings end end From 7d154b682eb4e7f42530bfeaeb1b1e333a952e25 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 16:29:57 -0600 Subject: [PATCH 114/173] Rename 'BOXCAR' to 'Fizzy' Missed files --- app/helpers/application_helper.rb | 2 +- app/mailers/application_mailer.rb | 2 +- app/mailers/magic_link_mailer.rb | 2 +- app/mailers/notification/bundle_mailer.rb | 2 +- app/views/account/settings/_entropy.html.erb | 2 +- app/views/collections/edit/_auto_close.html.erb | 2 +- app/views/collections/edit/_publication.html.erb | 2 +- app/views/events/index.html.erb | 2 +- app/views/layouts/public.html.erb | 2 +- .../email_change_confirmation.html.erb | 2 +- .../email_change_confirmation.text.erb | 2 +- .../sign_in_instructions.html.erb | 6 +++--- .../sign_in_instructions.text.erb | 2 +- .../bundle_mailer/notification.html.erb | 2 +- .../bundle_mailer/notification.text.erb | 2 +- app/views/my/menus/_button.html.erb | 2 +- app/views/my/menus/_places.html.erb | 2 +- app/views/my/menus/show.html.erb | 2 +- app/views/notifications/settings/_install.html.erb | 8 ++++---- app/views/notifications/settings/_system.html.erb | 12 ++++++------ app/views/notifications/unsubscribes/show.html.erb | 4 ++-- app/views/pwa/manifest.json.erb | 2 +- app/views/searches/_form.html.erb | 2 +- app/views/sessions/_footer.html.erb | 2 +- app/views/sessions/menus/show.html.erb | 6 +++--- app/views/webhooks/index.html.erb | 4 ++-- app/views/webhooks/new.html.erb | 2 +- .../app/models/signup/account_name_generator.rb | 2 +- gems/fizzy-saas/app/views/signups/new.html.erb | 2 +- .../signups/memberships_controller_test.rb | 2 +- .../models/signup/account_name_generator_test.rb | 14 +++++++------- test/mailers/magic_link_mailer_test.rb | 2 +- 32 files changed, 52 insertions(+), 52 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8447f024d..f6afd0ba4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,6 +1,6 @@ module ApplicationHelper def page_title_tag - tag.title @page_title || "BOXCAR" + tag.title @page_title || "Fizzy" end def icon_tag(name, **options) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d6db07d61..4e7878b62 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base - default from: "The BOXCAR team " + default from: "The Fizzy team " 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 52b9be031..391e64f6d 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 BOXCAR" + mail to: @identity.email_address, subject: "Sign in to Fizzy" end private diff --git a/app/mailers/notification/bundle_mailer.rb b/app/mailers/notification/bundle_mailer.rb index 32f883efd..c821a1274 100644 --- a/app/mailers/notification/bundle_mailer.rb +++ b/app/mailers/notification/bundle_mailer.rb @@ -11,6 +11,6 @@ class Notification::BundleMailer < ApplicationMailer mail \ to: bundle.user.identity.email_address, - subject: "Latest Activity in BOXCAR" + subject: "Latest Activity in Fizzy" end end diff --git a/app/views/account/settings/_entropy.html.erb b/app/views/account/settings/_entropy.html.erb index fb3709e4d..f6056ab37 100644 --- a/app/views/account/settings/_entropy.html.erb +++ b/app/views/account/settings/_entropy.html.erb @@ -1,7 +1,7 @@

Auto close

-

BOXCAR 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.

+

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 %> diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb index 4a86005a8..e7e135d8d 100644 --- a/app/views/collections/edit/_auto_close.html.erb +++ b/app/views/collections/edit/_auto_close.html.erb @@ -1,7 +1,7 @@ <%= turbo_frame_tag @collection, :entropy do %>

Auto close

-

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

+

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: collection, url: collection_entropy_path(collection) %>
<% end %> diff --git a/app/views/collections/edit/_publication.html.erb b/app/views/collections/edit/_publication.html.erb index 6f90bb0b0..0673bd043 100644 --- a/app/views/collections/edit/_publication.html.erb +++ b/app/views/collections/edit/_publication.html.erb @@ -1,7 +1,7 @@ <%= turbo_frame_tag @collection, :publication do %>

Public link

-
Turn on the Public link to share this board with anyone in the world. They won’t need to log in and they won’t be able to see anything else in BOXCAR.
+
Turn on the Public link to share this board with anyone in the world. They won’t need to log in and they won’t be able to see anything else in Fizzy.
<% if collection.published? %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 4bdc6b8c7..1355ca229 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -28,7 +28,7 @@ diff --git a/app/views/layouts/public.html.erb b/app/views/layouts/public.html.erb index 6df1eabeb..c217f44bb 100644 --- a/app/views/layouts/public.html.erb +++ b/app/views/layouts/public.html.erb @@ -8,7 +8,7 @@ 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 0c46017ff..438dc18e9 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.html.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.html.erb @@ -1,5 +1,5 @@

Confirm your email address change

-

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

+

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

<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %> diff --git a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb index 7d669a4d4..265c9cbf2 100644 --- a/app/views/mailers/identity_mailer/email_change_confirmation.text.erb +++ b/app/views/mailers/identity_mailer/email_change_confirmation.text.erb @@ -1,7 +1,7 @@ Confirm your email address change <%= "=" * 80 %> -Hit the link below to use this email address in BOXCAR: +Hit the link below to use this email address in Fizzy: <%= email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token) %> 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 7a193eb08..b966ecfa6 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,7 +1,7 @@ -

Sign in to BOXCAR

-

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

+

Sign in to Fizzy

+

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

-<%= link_to "Sign in to BOXCAR", session_magic_link_url(code: @magic_link.code), class: "btn" %> +<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %>

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 037cda365..f44e1c908 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,4 +1,4 @@ -Sign in to BOXCAR +Sign in to Fizzy <%= "=" * 80 %> Open this link in your browser to login on this device: diff --git a/app/views/mailers/notification/bundle_mailer/notification.html.erb b/app/views/mailers/notification/bundle_mailer/notification.html.erb index 5983a2bc1..e7478b6b7 100644 --- a/app/views/mailers/notification/bundle_mailer/notification.html.erb +++ b/app/views/mailers/notification/bundle_mailer/notification.html.erb @@ -3,7 +3,7 @@

Everything since <%= @bundle.starts_at.strftime("%-l%P on %A, %B %-d") %>

You have <%= link_to pluralize(@notifications.count, "new notification"), notifications_url %>.

<%= render partial: "notification/bundle_mailer/notification", collection: @notifications, as: :notification %> - diff --git a/app/views/mailers/notification/bundle_mailer/notification.text.erb b/app/views/mailers/notification/bundle_mailer/notification.text.erb index ce383e249..0149767e6 100644 --- a/app/views/mailers/notification/bundle_mailer/notification.text.erb +++ b/app/views/mailers/notification/bundle_mailer/notification.text.erb @@ -4,7 +4,7 @@ You have <%= pluralize @notifications.count, "new notification" %>. <%= render partial: "notification/bundle_mailer/notification", collection: @notifications, as: :notification %> -------------------------------------------------------------------------------- -BOXCAR emails you about new notifications every few hours. +Fizzy emails you about new notifications every few hours. Change how often you get these: <%= notifications_settings_url %> diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index 589c69bf2..2034f6a40 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -3,5 +3,5 @@ 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" %> - BOXCAR + Fizzy <% end %> diff --git a/app/views/my/menus/_places.html.erb b/app/views/my/menus/_places.html.erb index 8b7be5b73..ee7b23de0 100644 --- a/app/views/my/menus/_places.html.erb +++ b/app/views/my/menus/_places.html.erb @@ -1,5 +1,5 @@ <%= collapsible_nav_section "Jump to…" do %> - <%= filter_place_menu_item "https://www.youtube.com/watch?v=rNKRdk7DLHM", "BOXCAR beta orientation", "youtube", new_window: true %> + <%= filter_place_menu_item "https://www.youtube.com/watch?v=rNKRdk7DLHM", "Fizzy beta orientation", "youtube", new_window: true %> <%= 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" %> diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index 4490d1088..bac6a9422 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -11,7 +11,7 @@ <%= render "hotkeys" %>
- BOXCAR is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. + Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals.
<%= render "saved_filter", filters: @user_filtering.filters %> diff --git a/app/views/notifications/settings/_install.html.erb b/app/views/notifications/settings/_install.html.erb index be618ae0d..f8d664043 100644 --- a/app/views/notifications/settings/_install.html.erb +++ b/app/views/notifications/settings/_install.html.erb @@ -1,10 +1,10 @@ <% unless (platform.chrome? && !platform.ios?) || (platform.firefox? && !platform.android?) %>
-

<%= platform.safari? && platform.desktop? ? "…or install" : "Install " -%> BOXCAR as a web app.

+

<%= platform.safari? && platform.desktop? ? "…or install" : "Install " -%> Fizzy as a web app.

<% case when platform.edge? %>
    -
  1. Click <%= icon_tag "install-edge", alt: "the app available - install BOXCAR button" %>in the address bar.
  2. +
  3. Click <%= icon_tag "install-edge", alt: "the app available - install Fizzy button" %>in the address bar.
  4. Click Install.
<% when platform.chrome? && platform.android? %> @@ -23,13 +23,13 @@
  • Click Add to Dock….
  • <% when (platform.safari? || platform.chrome?) && platform.ios? %> -

    To receive push notifications in <%= platform.browser.capitalize %> for <%= platform.operating_system %>, you must first install BOXCAR as a web app.

    +

    To receive push notifications in <%= platform.browser.capitalize %> for <%= platform.operating_system %>, you must first install Fizzy as a web app.

    1. Tap <%= icon_tag "share", alt: "the share button" %>
    2. Tap Add to Home Screen.
    <% else %> -

    Some platforms require you to install BOXCAR as a web app to receive push notifications.

    +

    Some platforms require you to install Fizzy as a web app to receive push notifications.

    <% end %>
    <% end %> diff --git a/app/views/notifications/settings/_system.html.erb b/app/views/notifications/settings/_system.html.erb index 23741ee66..afe0efd45 100644 --- a/app/views/notifications/settings/_system.html.erb +++ b/app/views/notifications/settings/_system.html.erb @@ -12,19 +12,19 @@
    1. Click Start, then Settings.
    2. Go to System > Notification.
    3. -
    4. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for BOXCAR.
    5. +
    6. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for Fizzy.
    <% when (platform.firefox? || platform.chrome?) && platform.desktop? %>
      <% if platform.windows? %>
    1. Click Start, then Settings.
    2. Go to System > Notification.
    3. -
    4. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for BOXCAR.
    5. +
    6. Click <%= icon_tag "switch", alt: "the toggle button" %> ON for Fizzy.
    7. <% else %>
    8. Click in the top left.
    9. Click System Settings….
    10. Click Notifications.
    11. -
    12. Click BOXCAR.
    13. +
    14. Click Fizzy.
    15. Click <%= icon_tag "switch", alt: "the allow notifications switch" %> to Allow notifications.
    16. <% end %>
    @@ -33,13 +33,13 @@
  • Click in the top left.
  • Click System Settings….
  • Click Notifications.
  • -
  • Click BOXCAR.
  • +
  • Click Fizzy.
  • Click <%= icon_tag "switch", alt: "the allow notifications switch" %> to Allow notifications.
  • <% when (platform.safari? || platform.chrome?) && platform.ios? %>
    1. Open the <%= icon_tag "gear", aria: { hidden: "true" } %> Settings app.
    2. -
    3. Scroll to and tap BOXCAR.
    4. +
    5. Scroll to and tap Fizzy.
    6. Tap Notifications.
    7. Tap <%= icon_tag "switch", alt: "the allow notifications switch button" %> to Allow Notifications.
    @@ -48,7 +48,7 @@
  • Open the <%= icon_tag "gear", aria: { hidden: "true" } %> Settings app.
  • Tap Notifications.
  • Tap App notifications.
  • -
  • Scroll to BOXCAR.
  • +
  • Scroll to Fizzy.
  • Tap <%= icon_tag "switch", alt: "the switch" %> to Allow Notifications.
  • <% else %> diff --git a/app/views/notifications/unsubscribes/show.html.erb b/app/views/notifications/unsubscribes/show.html.erb index b41fa6431..3e100b14d 100644 --- a/app/views/notifications/unsubscribes/show.html.erb +++ b/app/views/notifications/unsubscribes/show.html.erb @@ -1,9 +1,9 @@

    You’re unsubscribed

    -

    Thanks, <%= @user.first_name %>! BOXCAR won’t send you any more email notifications. If you change your mind you can turn them back on in <%= link_to "Notifications Settings", notifications_settings_path %>.

    +

    Thanks, <%= @user.first_name %>! Fizzy won’t send you any more email notifications. If you change your mind you can turn them back on in <%= link_to "Notifications Settings", notifications_settings_path %>.

    <%= link_to root_path, class: "btn btn--link margin-block-start" do %> <%= icon_tag "arrow-left" %> - Back to BOXCAR + Back to Fizzy <% end %>
    diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index c697f8eba..ff66cb7b3 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -1,5 +1,5 @@ { - "name": "<%= [ Account.sole.name, "BOXCAR", Rails.env.production? ? nil : Rails.env ].compact.join(" - ") %>", + "name": "<%= [ Account.sole.name, "Fizzy", Rails.env.production? ? nil : Rails.env ].compact.join(" - ") %>", "icons": [ { "src": "/app-icon-192.png", diff --git a/app/views/searches/_form.html.erb b/app/views/searches/_form.html.erb index 379126465..f9a1b778a 100644 --- a/app/views/searches/_form.html.erb +++ b/app/views/searches/_form.html.erb @@ -1,5 +1,5 @@ <%= form_with url: search_path, method: :get, class: "search__form flex align-center justify-center gap-half", data: { bar_target: "form", turbo_frame: "bar_content" } do |form| %> - <%= form.label :q, "Search BOXCAR", class: "font-weight-black txt-nowrap" %> + <%= form.label :q, "Search Fizzy", class: "font-weight-black txt-nowrap" %> <%= text_field_tag :q, query_terms, class: "search__input input", type: "search", diff --git a/app/views/sessions/_footer.html.erb b/app/views/sessions/_footer.html.erb index 208f47a3f..33d604824 100644 --- a/app/views/sessions/_footer.html.erb +++ b/app/views/sessions/_footer.html.erb @@ -1,3 +1,3 @@
    - BOXCAR beta. <%= mail_to "support@37signals.com", "Need help?", class: "txt-link" %> + Fizzy beta. <%= mail_to "support@37signals.com", "Need help?", class: "txt-link" %>
    \ No newline at end of file diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index a61c415ca..535d2b903 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -7,7 +7,7 @@ > <% if @memberships.present? %>

    - Your BOXCAR accounts + Your Fizzy accounts

    <% @memberships.each do |membership| %> @@ -21,13 +21,13 @@ <% else %>

    Hmm...

    -

    You don’t have any BOXCAR accounts.

    +

    You don’t have any Fizzy accounts.

    <% end %> <% if defined?(saas) %>
    <%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small" do %> - Sign up for a new BOXCAR account + Sign up for a new Fizzy account <% end %>
    <% end %> diff --git a/app/views/webhooks/index.html.erb b/app/views/webhooks/index.html.erb index 8c0387510..a891b8bc8 100644 --- a/app/views/webhooks/index.html.erb +++ b/app/views/webhooks/index.html.erb @@ -26,8 +26,8 @@ <%= render partial: "webhooks/webhook", collection: @page.records %> <% else %> -

    Webhooks can notify another application when something happens in this BOXCAR 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 BOXCAR.

    +

    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_collection_webhook_path, class: "btn btn--link" do %> <%= icon_tag "add" %> Set up a webhook diff --git a/app/views/webhooks/new.html.erb b/app/views/webhooks/new.html.erb index aafe035e6..f71781935 100644 --- a/app/views/webhooks/new.html.erb +++ b/app/views/webhooks/new.html.erb @@ -22,7 +22,7 @@
    <%= form.label :url do %> Payload URL
    -

    This is the URL for the app that will receive payloads from BOXCAR.

    +

    This is the URL for the app that will receive payloads from Fizzy.

    <% end %> <%= form.url_field :url, required: true, diff --git a/gems/fizzy-saas/app/models/signup/account_name_generator.rb b/gems/fizzy-saas/app/models/signup/account_name_generator.rb index 90104fe63..2ec57d534 100644 --- a/gems/fizzy-saas/app/models/signup/account_name_generator.rb +++ b/gems/fizzy-saas/app/models/signup/account_name_generator.rb @@ -1,5 +1,5 @@ class Signup::AccountNameGenerator - SUFFIX = "BOXCAR".freeze + SUFFIX = "Fizzy".freeze attr_reader :identity, :name diff --git a/gems/fizzy-saas/app/views/signups/new.html.erb b/gems/fizzy-saas/app/views/signups/new.html.erb index 7512ec120..93166dd2f 100644 --- a/gems/fizzy-saas/app/views/signups/new.html.erb +++ b/gems/fizzy-saas/app/views/signups/new.html.erb @@ -1,4 +1,4 @@ -<% @page_title = "Sign up for BOXCAR" %> +<% @page_title = "Sign up for Fizzy" %>
    ">

    Sign up

    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 b0eb590fe..f1ca13093 100644 --- a/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb +++ b/gems/fizzy-saas/test/controllers/signups/memberships_controller_test.rb @@ -45,7 +45,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest signup: { membership_id: membership.signed_id(purpose: :account_creation), full_name: "New User", - account_name: "New's BOXCAR" + account_name: "New's Fizzy" } ) end diff --git a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb b/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb index 4c5516c61..fdaac96ce 100644 --- a/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb +++ b/gems/fizzy-saas/test/models/signup/account_name_generator_test.rb @@ -9,38 +9,38 @@ class Signup::AccountNameGeneratorTest < ActiveSupport::TestCase test "generate" do account_name = @generator.generate - assert_equal "Newart's BOXCAR", account_name, "The 1st account doesn't have 1st in the name" + assert_equal "Newart's Fizzy", account_name, "The 1st account doesn't have 1st in the name" first_membership = @identity.memberships.create(tenant: "1st") first_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 2nd BOXCAR", account_name + assert_equal "Newart's 2nd Fizzy", account_name second_membership = @identity.memberships.create(tenant: "2nd") second_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 3rd BOXCAR", account_name + assert_equal "Newart's 3rd Fizzy", account_name third_membership = @identity.memberships.create(tenant: "3nd") third_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 4th BOXCAR", account_name + assert_equal "Newart's 4th Fizzy", account_name fourth_membership = @identity.memberships.create(tenant: "4th") fourth_membership.stubs(:account_name).returns(account_name) account_name = @generator.generate - assert_equal "Newart's 5th BOXCAR", account_name + assert_equal "Newart's 5th Fizzy", account_name end test "generate continues from the previous highest index" do membership = @identity.memberships.create(tenant: "12th") - membership.stubs(:account_name).returns("Newart's 12th BOXCAR") + membership.stubs(:account_name).returns("Newart's 12th Fizzy") account_name = @generator.generate - assert_equal "Newart's 13th BOXCAR", account_name + assert_equal "Newart's 13th Fizzy", account_name end end diff --git a/test/mailers/magic_link_mailer_test.rb b/test/mailers/magic_link_mailer_test.rb index 34ba265f9..ee3d56abc 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 BOXCAR", email.subject + assert_equal "Sign in to Fizzy", email.subject assert_match magic_link.code, email.body.encoded end end From 38ead57f4e0980d35e8c5cec1fb6bf429f532beb Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 16:55:24 -0600 Subject: [PATCH 115/173] Just put the hotkey on the button No need to over explain the keyboard navigation, anyone who cares about this will figure it out --- app/views/my/menus/_button.html.erb | 3 ++- app/views/my/menus/_footer.html.erb | 3 +++ app/views/my/menus/_hotkeys.html.erb | 2 +- app/views/my/menus/_shortcut_notice.html.erb | 5 ----- app/views/my/menus/show.html.erb | 8 +------- 5 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 app/views/my/menus/_footer.html.erb delete mode 100644 app/views/my/menus/_shortcut_notice.html.erb diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index 2034f6a40..52305cf60 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -1,7 +1,8 @@ -<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline align-center txt-normal", +<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline gap-half 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 new file mode 100644 index 000000000..22cfc379d --- /dev/null +++ b/app/views/my/menus/_footer.html.erb @@ -0,0 +1,3 @@ +
    + 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 index b722cf3fa..1f6494c0d 100644 --- a/app/views/my/menus/_hotkeys.html.erb +++ b/app/views/my/menus/_hotkeys.html.erb @@ -1,4 +1,4 @@ -
    +
    <%= filter_hotkey_link "Home", root_path, 1, "home" %> <%= filter_hotkey_link "New board", new_collection_path, 2, "collection-add" %> <%= filter_hotkey_link "Assigned to me", cards_path(assignee_ids: [Current.user.id]), 3, "clipboard" %> diff --git a/app/views/my/menus/_shortcut_notice.html.erb b/app/views/my/menus/_shortcut_notice.html.erb deleted file mode 100644 index 70f5f5fb7..000000000 --- a/app/views/my/menus/_shortcut_notice.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% if platform.desktop? %> -
    - Press J anytime to open this, esc to close, to move, enter to navigate. -
    -<% end %> diff --git a/app/views/my/menus/show.html.erb b/app/views/my/menus/show.html.erb index bac6a9422..d5eaa1dd5 100644 --- a/app/views/my/menus/show.html.erb +++ b/app/views/my/menus/show.html.erb @@ -9,17 +9,11 @@
    <%= render "hotkeys" %> - -
    - Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. -
    - <%= render "saved_filter", filters: @user_filtering.filters %> <%= render "collections", 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 "shortcut_notice" %> + <%= render "footer" %> <% end %> From 9b7d724b88a7a1b6ae927543c7856bfea3526152 Mon Sep 17 00:00:00 2001 From: Jason Fried Date: Mon, 3 Nov 2025 15:24:07 -0800 Subject: [PATCH 116/173] Added trademarks for Fizzy and 37signals in the menu footer. --- app/views/my/menus/_footer.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/my/menus/_footer.html.erb b/app/views/my/menus/_footer.html.erb index 22cfc379d..9bcea3d54 100644 --- a/app/views/my/menus/_footer.html.erb +++ b/app/views/my/menus/_footer.html.erb @@ -1,3 +1,3 @@
    - Fizzy is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals. + Fizzy™ is designed, built, and backed by <%= icon_tag "37signals", class: "v-align-middle" %> 37signals™
    From 2d197601b713cc9f0bde032b76262dd5228d2799 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Mon, 3 Nov 2025 17:47:30 -0600 Subject: [PATCH 117/173] Add JF's fizzy orientation video --- app/models/account/seeder.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index d89c1287b..540273993 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -33,6 +33,8 @@ class Account::Seeder # Cards have_fun_card = playground.cards.create! creator: creator, title: "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 From f104e31f3ffc5adff9200a9a1ff555e2ab0acef1 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 05:06:18 +0100 Subject: [PATCH 118/173] Remove unused method --- app/models/account/seedeable.rb | 4 ---- db/seeds.rb | 1 - script/create-account.rb | 3 --- 3 files changed, 8 deletions(-) diff --git a/app/models/account/seedeable.rb b/app/models/account/seedeable.rb index 3cceae251..7e080d6ba 100644 --- a/app/models/account/seedeable.rb +++ b/app/models/account/seedeable.rb @@ -1,10 +1,6 @@ module Account::Seedeable extend ActiveSupport::Concern - def setup_basic_template - user = User.first - end - def setup_customer_template Account::Seeder.new(self, User.active.first).seed end diff --git a/db/seeds.rb b/db/seeds.rb index f70c87ccd..2d1002463 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -27,7 +27,6 @@ def create_tenant(signal_account_name) membership: membership } ) - account.setup_basic_template end ApplicationRecord.current_tenant = tenant_id diff --git a/script/create-account.rb b/script/create-account.rb index f0cf2b16a..1a3a7cca4 100755 --- a/script/create-account.rb +++ b/script/create-account.rb @@ -68,9 +68,6 @@ Current.set( email_address: owner_email } ) - - # Setup basic template - account.setup_basic_template end puts "✓ Tenant created" From 445ee48b0d4cfb5a3490aa18148a68e189d2d957 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 05:07:26 +0100 Subject: [PATCH 119/173] Format --- lib/tasks/seed.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/seed.rake b/lib/tasks/seed.rake index 4c7dd3937..71483eaf8 100644 --- a/lib/tasks/seed.rake +++ b/lib/tasks/seed.rake @@ -1,6 +1,6 @@ namespace :seed do desc "Seed customer data for a specific account (e.g: rails seed:customer ARTENANT=1234)" - task :customer, [:tenant_id] => "db:tenant" do |t, args| + task :customer, [ :tenant_id ] => "db:tenant" do |t, args| raise "Please provide a tenant ID: rails seed:customer ARTENANT=1234" unless ApplicationRecord.current_tenant account = Account.sole From ed518bac71f5d7e954ad476305e5b106c81e1de7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 05:09:48 +0100 Subject: [PATCH 120/173] Remove unused var --- app/models/account/seeder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 540273993..80af1251a 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -31,7 +31,7 @@ class Account::Seeder playground = Collection.create! name: "Playground", creator: creator, all_access: true # Cards - have_fun_card = playground.cards.create! creator: creator, title: "Watch this: Fizzy orientation video", status: "published", description: <<~HTML + playground.cards.create! creator: creator, title: "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.


    From 8aead312002f6b68f63cfc1627f7eba226f71462 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 06:12:30 +0100 Subject: [PATCH 121/173] Fix: mark all as read wasn't refreshing in the notifications screen --- .../notifications/bulk_readings_controller.rb | 11 +++++++++++ app/views/notifications/_tray.html.erb | 2 +- .../notifications/bulk_readings_controller_test.rb | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/controllers/notifications/bulk_readings_controller.rb b/app/controllers/notifications/bulk_readings_controller.rb index 71b387101..5f80938fe 100644 --- a/app/controllers/notifications/bulk_readings_controller.rb +++ b/app/controllers/notifications/bulk_readings_controller.rb @@ -1,5 +1,16 @@ class Notifications::BulkReadingsController < ApplicationController def create Current.user.notifications.unread.read_all + + if from_tray? + head :ok + else + redirect_to notifications_path + end end + + private + def from_tray? + params[:from_tray] + end end diff --git a/app/views/notifications/_tray.html.erb b/app/views/notifications/_tray.html.erb index a009835a4..b81e72b44 100644 --- a/app/views/notifications/_tray.html.erb +++ b/app/views/notifications/_tray.html.erb @@ -28,7 +28,7 @@ <% end %>
    - <%= button_to bulk_reading_path, + <%= button_to bulk_reading_path(from_tray: true), class: "btn borderless tray__clear-notifications", title: "Mark all notifications as read", data: { action: "dialog#close badge#clear", turbo_frame: "notifications" }, diff --git a/test/controllers/notifications/bulk_readings_controller_test.rb b/test/controllers/notifications/bulk_readings_controller_test.rb index 405d1f4a0..124490f53 100644 --- a/test/controllers/notifications/bulk_readings_controller_test.rb +++ b/test/controllers/notifications/bulk_readings_controller_test.rb @@ -5,11 +5,21 @@ class Notifications::BulkReadingsControllerTest < ActionDispatch::IntegrationTes sign_in_as :kevin end - test "create" do + test "create marks all notifications as read" do assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do assert_changes -> { notifications(:layout_commented_kevin).reload.read? }, from: false, to: true do post bulk_reading_path end end end + + test "create redirects to notifications path when not from tray" do + post bulk_reading_path + assert_redirected_to notifications_path + end + + test "create returns ok when from tray" do + post bulk_reading_path, params: { from_tray: true } + assert_response :ok + end end From 1c4c271fd885312b656e9172e6463d1dbedfb35a Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 3 Nov 2025 13:39:42 +0100 Subject: [PATCH 122/173] Remove unused route --- config/routes.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 790815c75..89699f4dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ Rails.application.routes.draw do namespace :account do - post :enter, to: "entries#create" resource :join_code resource :settings resource :entropy From fd7a9330e8b299ca1f9a759216ed1d799276544b Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Mon, 3 Nov 2025 15:18:37 +0100 Subject: [PATCH 123/173] Remove email_address & password_digest from User --- app/models/identity.rb | 1 - app/models/user.rb | 4 -- app/views/account/settings/_user.html.erb | 2 +- app/views/collections/_access_toggle.erb | 2 +- app/views/users/_user.json.jbuilder | 3 +- app/views/users/show.html.erb | 2 +- ..._address_and_password_digest_from_users.rb | 6 +++ db/schema.rb | 5 +-- db/schema_cache.yml | 38 +------------------ db/seeds.rb | 2 +- ...email_addresses_mandatory_on_identities.rb | 5 +++ db/untenanted_schema.rb | 4 +- test/models/account_test.rb | 6 ++- test/models/user/accessor_test.rb | 2 +- test/models/user/configurable_test.rb | 2 +- test/models/user_test.rb | 6 +-- 16 files changed, 29 insertions(+), 61 deletions(-) create mode 100644 db/migrate/20251103125353_remove_email_address_and_password_digest_from_users.rb create mode 100644 db/untenanted_migrate/20251103125952_make_email_addresses_mandatory_on_identities.rb diff --git a/app/models/identity.rb b/app/models/identity.rb index 70bc089eb..f5daf3be1 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -6,7 +6,6 @@ class Identity < UntenantedRecord has_many :sessions, dependent: :destroy normalizes :email_address, with: ->(value) { value.strip.downcase } - validates :email_address, presence: true def send_magic_link magic_links.create!.tap do |magic_link| diff --git a/app/models/user.rb b/app/models/user.rb index 2046b64d5..fabb0224a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,8 +3,6 @@ class User < ApplicationRecord Mentionable, Named, Notifiable, Role, Searcher, Watcher include Timelined # Depends on Accessor - self.ignored_columns = %i[ password_digest ] - has_one_attached :avatar belongs_to :membership, optional: true @@ -18,8 +16,6 @@ class User < ApplicationRecord has_many :pins, dependent: :destroy has_many :pinned_cards, through: :pins, source: :card - normalizes :email_address, with: ->(value) { value.strip.downcase } - delegate :staff?, to: :identity, allow_nil: true def deactivate diff --git a/app/views/account/settings/_user.html.erb b/app/views/account/settings/_user.html.erb index 78395ff1c..45fc77c07 100644 --- a/app/views/account/settings/_user.html.erb +++ b/app/views/account/settings/_user.html.erb @@ -3,7 +3,7 @@ <%= avatar_preview_tag user, hidden_for_screen_reader: true %>
    <%= user.name %> -
    <%= user.email_address %>
    +
    <%= user.identity.email_address %>
    <% end %> diff --git a/app/views/collections/_access_toggle.erb b/app/views/collections/_access_toggle.erb index 824cace65..829efa05e 100644 --- a/app/views/collections/_access_toggle.erb +++ b/app/views/collections/_access_toggle.erb @@ -3,7 +3,7 @@ <%= avatar_preview_tag user, hidden_for_screen_reader: true %>
    <%= user.name %> -
    <%= user.email_address %>
    +
    <%= user.identity.email_address %>
    <% end %> diff --git a/app/views/users/_user.json.jbuilder b/app/views/users/_user.json.jbuilder index 6b11459f3..6a49bd1de 100644 --- a/app/views/users/_user.json.jbuilder +++ b/app/views/users/_user.json.jbuilder @@ -1,6 +1,7 @@ json.cache! user do - json.(user, :id, :name, :email_address, :role, :active) + json.(user, :id, :name, :role, :active) + json.email_address user.identity.email_address json.created_at user.created_at.utc json.url user_url(user) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 1ae06e2c0..d5cbd8865 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -19,7 +19,7 @@

    <%= @user.name %>

    <% if @user.active? %> - <%= mail_to @user.email_address %> + <%= mail_to @user.identity.email_address %> <% else %> <%= @user.name %> is no longer on this account <% end %> diff --git a/db/migrate/20251103125353_remove_email_address_and_password_digest_from_users.rb b/db/migrate/20251103125353_remove_email_address_and_password_digest_from_users.rb new file mode 100644 index 000000000..c336b5b9c --- /dev/null +++ b/db/migrate/20251103125353_remove_email_address_and_password_digest_from_users.rb @@ -0,0 +1,6 @@ +class RemoveEmailAddressAndPasswordDigestFromUsers < ActiveRecord::Migration[8.2] + def change + remove_column :users, :email_address, :string + remove_column :users, :password_digest, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 049e45a1a..4c6901d26 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_02_115338) do +ActiveRecord::Schema[8.2].define(version: 2025_11_03_125353) do create_table "accesses", force: :cascade do |t| t.datetime "accessed_at" t.integer "collection_id", null: false @@ -389,13 +389,10 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do create_table "users", force: :cascade do |t| t.boolean "active", default: true, null: false t.datetime "created_at", null: false - t.string "email_address" t.integer "membership_id" t.string "name", null: false - t.string "password_digest" t.string "role", default: "member", null: false t.datetime "updated_at", null: false - t.index ["email_address"], name: "index_users_on_email_address", unique: true t.index ["membership_id"], name: "index_users_on_membership_id" t.index ["role"], name: "index_users_on_role" end diff --git a/db/schema_cache.yml b/db/schema_cache.yml index e4c5ab87c..78c8e718f 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -1045,16 +1045,6 @@ columns: collation: comment: - *7 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: email_address - cast_type: *5 - sql_type_metadata: *6 - 'null': true - default: - default_function: - collation: - comment: - *8 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -1067,16 +1057,6 @@ columns: collation: comment: - *10 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: password_digest - cast_type: *5 - sql_type_metadata: *6 - 'null': true - default: - default_function: - collation: - comment: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: role @@ -2715,22 +2695,6 @@ indexes: comment: valid: true users: - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: users - name: index_users_on_email_address - unique: true - columns: - - email_address - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: users name: index_users_on_membership_id @@ -2879,4 +2843,4 @@ indexes: nulls_not_distinct: comment: valid: true -version: 20251102115338 +version: 20251103125353 diff --git a/db/seeds.rb b/db/seeds.rb index 2d1002463..bf40aaf0a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -33,7 +33,7 @@ def create_tenant(signal_account_name) end def find_or_create_user(full_name, email_address) - if user = User.find_by(email_address: email_address) + if user = Identity.find_by(email_address: email_address)&.memberships&.find_by(tenant: ApplicationRecord.current_tenant)&.user user else identity = Identity.find_or_create_by!(email_address: email_address) diff --git a/db/untenanted_migrate/20251103125952_make_email_addresses_mandatory_on_identities.rb b/db/untenanted_migrate/20251103125952_make_email_addresses_mandatory_on_identities.rb new file mode 100644 index 000000000..615f0320f --- /dev/null +++ b/db/untenanted_migrate/20251103125952_make_email_addresses_mandatory_on_identities.rb @@ -0,0 +1,5 @@ +class MakeEmailAddressesMandatoryOnIdentities < ActiveRecord::Migration[8.2] + def change + change_column_null :identities, :email_address, false + end +end diff --git a/db/untenanted_schema.rb b/db/untenanted_schema.rb index 9e38879e1..b4582291a 100644 --- a/db/untenanted_schema.rb +++ b/db/untenanted_schema.rb @@ -10,10 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.2].define(version: 2025_10_27_131911) do +ActiveRecord::Schema[8.2].define(version: 2025_11_03_125952) do create_table "identities", force: :cascade do |t| t.datetime "created_at", null: false - t.string "email_address" + t.string "email_address", null: false t.datetime "updated_at", null: false t.index ["email_address"], name: "index_identities_on_email_address", unique: true end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 99711439d..785799c90 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -13,6 +13,8 @@ class AccountTest < ActiveSupport::TestCase end test ".create_with_admin_user creates a new local account" do + membership = memberships(:david_in_37signals) + ApplicationRecord.create_tenant("account-create-with-dependents") do account = Account.create_with_admin_user( account: { @@ -21,7 +23,7 @@ class AccountTest < ActiveSupport::TestCase }, owner: { name: "David", - email_address: "david@37signals.com" + membership: membership } ) assert_not_nil account @@ -37,7 +39,7 @@ class AccountTest < ActiveSupport::TestCase admin = User.find_by(role: "admin") assert_equal "David", admin.name - assert_equal "david@37signals.com", admin.email_address + assert_equal "david@37signals.com", admin.identity.email_address assert_equal "admin", admin.role end end diff --git a/test/models/user/accessor_test.rb b/test/models/user/accessor_test.rb index 5ca1f834f..3305c37c9 100644 --- a/test/models/user/accessor_test.rb +++ b/test/models/user/accessor_test.rb @@ -2,7 +2,7 @@ require "test_helper" class User::AccessorTest < ActiveSupport::TestCase test "new users get added to all_access collections on creation" do - user = User.create!(name: "Jorge", email_address: "testregular@example.com") + user = User.create!(name: "Jorge") assert_includes user.collections, collections(:writebook) assert_equal Collection.all_access.count, user.collections.count diff --git a/test/models/user/configurable_test.rb b/test/models/user/configurable_test.rb index dbc353936..b49b78c1b 100644 --- a/test/models/user/configurable_test.rb +++ b/test/models/user/configurable_test.rb @@ -2,7 +2,7 @@ require "test_helper" class User::ConfigurableTest < ActiveSupport::TestCase test "should create settings for new users" do - user = User.create! name: "Some new user", email_address: "some.new@user.com" + user = User.create! name: "Some new user" assert user.settings.present? end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 23f604ffb..2f3e71dca 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -4,8 +4,7 @@ class UserTest < ActiveSupport::TestCase test "create" do user = User.create! \ role: "member", - name: "Victor Cooper", - email_address: "victor@hey.com" + name: "Victor Cooper" assert_equal [ collections(:writebook) ], user.collections assert user.settings.present? @@ -14,8 +13,7 @@ class UserTest < ActiveSupport::TestCase test "creation gives access to all_access collections" do user = User.create! \ role: "member", - name: "Victor Cooper", - email_address: "victor@hey.com" + name: "Victor Cooper" assert_equal [ collections(:writebook) ], user.collections end From 5f26690aa1490ef163beef05a4a5a6d301fb762f Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 4 Nov 2025 07:18:02 +0100 Subject: [PATCH 124/173] Redirect to the account you joined after sign in --- app/controllers/join_codes_controller.rb | 6 ++ .../controllers/join_codes_controller_test.rb | 62 +++++++++++++++++++ test/fixtures/account/join_codes.yml | 2 - 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/controllers/join_codes_controller_test.rb diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 972420439..4bd5293ea 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -2,6 +2,7 @@ class JoinCodesController < ApplicationController require_untenanted_access allow_unauthenticated_access before_action :set_join_code + before_action :ensure_join_code_is_valid def new @account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name } @@ -14,10 +15,15 @@ class JoinCodesController < ApplicationController identity.send_magic_link end + session[:return_to_after_authenticating] = root_url(script_name: "/#{tenant}") redirect_to session_magic_link_path end private + def ensure_join_code_is_valid + head :not_found unless @join_code&.active? + end + def set_join_code @join_code ||= ApplicationRecord.with_tenant(tenant) { Account::JoinCode.active.find_by(code: code) } end diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb new file mode 100644 index 000000000..de8c5daa5 --- /dev/null +++ b/test/controllers/join_codes_controller_test.rb @@ -0,0 +1,62 @@ +require "test_helper" + +class JoinCodesControllerTest < ActionDispatch::IntegrationTest + setup do + @tenant = ApplicationRecord.current_tenant + @join_code = account_join_codes(:sole) + end + + test "new" do + untenanted do + get join_path(tenant: @tenant, code: @join_code.code) + end + + assert_response :success + assert_in_body "37signals" + end + + test "new with an invalid code" do + untenanted do + get join_path(tenant: @tenant, code: "INVALID-CODE") + end + + assert_response :not_found + end + + test "new with an inactive code" do + @join_code.update!(usage_count: @join_code.usage_limit) + + untenanted do + get join_path(tenant: @tenant, code: @join_code.code) + end + + assert_response :not_found + end + + test "create" do + untenanted do + assert_difference -> { Identity.count }, 1 do + assert_difference -> { Membership.count }, 1 do + post join_path(tenant: @tenant, code: @join_code.code), params: { email_address: "new_user@example.com" } + end + end + + assert_redirected_to session_magic_link_path + assert_equal root_url(script_name: "/#{@tenant}"), session[:return_to_after_authenticating] + end + end + + test "create for existing identity" do + identity = identities(:jz) + + untenanted do + assert_no_difference -> { Identity.count } do + assert_difference -> { Membership.count }, 1 do + post join_path(tenant: @tenant, code: @join_code.code), params: { email_address: identity.email_address } + end + end + + assert_redirected_to session_magic_link_path + end + end +end diff --git a/test/fixtures/account/join_codes.yml b/test/fixtures/account/join_codes.yml index d346d7e65..daba73e66 100644 --- a/test/fixtures/account/join_codes.yml +++ b/test/fixtures/account/join_codes.yml @@ -1,5 +1,3 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - sole: code: 1234-5678-9XYZ usage_count: 0 From a21502e83aba06e8001f757665acb590a4bb8bfa Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 07:33:29 +0100 Subject: [PATCH 125/173] Update to latest Lexxy (Lexical 38.2) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 59c1891fd..094571676 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -296,7 +296,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.14.beta) + lexxy (0.1.15.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) From 87d9c3cd010483f233ddc906bde50b0162d0d7e3 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 4 Nov 2025 09:02:36 +0100 Subject: [PATCH 126/173] Make unauthenticated access permit unauthorized access --- app/controllers/concerns/authentication.rb | 1 + app/controllers/concerns/authorization.rb | 2 +- app/controllers/notifications/unsubscribes_controller.rb | 1 - app/controllers/public/base_controller.rb | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 6acbd92b6..daedbb582 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -23,6 +23,7 @@ module Authentication def allow_unauthenticated_access(**options) skip_before_action :require_authentication, **options before_action :resume_session, **options + allow_unauthorized_access **options end def require_untenanted_access(**options) diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb index 0e5e9240d..6d31f844e 100644 --- a/app/controllers/concerns/authorization.rb +++ b/app/controllers/concerns/authorization.rb @@ -2,7 +2,7 @@ module Authorization extend ActiveSupport::Concern included do - before_action :ensure_can_access_account, if: -> { ApplicationRecord.current_tenant && Current.session } + before_action :ensure_can_access_account, if: -> { ApplicationRecord.current_tenant && authenticated? } end class_methods do diff --git a/app/controllers/notifications/unsubscribes_controller.rb b/app/controllers/notifications/unsubscribes_controller.rb index ca15e78d4..f7486d91e 100644 --- a/app/controllers/notifications/unsubscribes_controller.rb +++ b/app/controllers/notifications/unsubscribes_controller.rb @@ -1,6 +1,5 @@ class Notifications::UnsubscribesController < ApplicationController allow_unauthenticated_access - allow_unauthorized_access skip_before_action :verify_authenticity_token before_action :set_user diff --git a/app/controllers/public/base_controller.rb b/app/controllers/public/base_controller.rb index 71209224f..7fa2e69fb 100644 --- a/app/controllers/public/base_controller.rb +++ b/app/controllers/public/base_controller.rb @@ -1,6 +1,5 @@ class Public::BaseController < ApplicationController allow_unauthenticated_access - allow_unauthorized_access before_action :set_collection, :set_card, :set_public_cache_expiration From 417acab12a48a7e5300854a232692971dd4291bd Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 4 Nov 2025 09:57:00 +0000 Subject: [PATCH 127/173] Upgrade Beamer to zone-aware version --- Gemfile | 2 +- Gemfile.lock | 14 ++++++-------- config/deploy.beta.yml | 2 +- config/deploy.production.yml | 2 +- config/deploy.staging.yml | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 30f8a6483..efd0166b4 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem "solid_queue", "~> 1.2" gem "sqlite3", ">= 2.0" gem "thruster", require: false source "https://e95ae463b12de3f204526a44650f6ae0@gems.stanko.io/private" do - gem "beamer-rails" + gem "beamer-rails", "~> 0.1.0.beta4" end # Features diff --git a/Gemfile.lock b/Gemfile.lock index 094571676..3641d900c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,17 +146,15 @@ PATH GEM remote: https://gems.stanko.io/private/ specs: - beamer-rails (0.0.6-arm64-darwin) + beamer-rails (0.1.0.beta4-arm64-darwin) sqlite3 (>= 2.0) - beamer-rails (0.0.6-arm64-linux) + beamer-rails (0.1.0.beta4-arm64-linux-gnu) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-darwin) + beamer-rails (0.1.0.beta4-x86_64-darwin) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux) + beamer-rails (0.1.0.beta4-x86_64-linux-gnu) sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux-gnu) - sqlite3 (>= 2.0) - beamer-rails (0.0.6-x86_64-linux-musl) + beamer-rails (0.1.0.beta4-x86_64-linux-musl) sqlite3 (>= 2.0) GEM @@ -606,7 +604,7 @@ DEPENDENCIES autotuner aws-sdk-s3 bcrypt (~> 3.1.7) - beamer-rails! + beamer-rails (~> 0.1.0.beta4)! benchmark bootsnap brakeman diff --git a/config/deploy.beta.yml b/config/deploy.beta.yml index 8dfb783ff..59956263e 100644 --- a/config/deploy.beta.yml +++ b/config/deploy.beta.yml @@ -23,7 +23,7 @@ env: accessories: beamer: - image: basecamp/beamer:vfs + image: basecamp/beamer:zones registry: server: registry.37signals.com username: robot$harbor-bot diff --git a/config/deploy.production.yml b/config/deploy.production.yml index 5035e128d..b2e7b2bbb 100644 --- a/config/deploy.production.yml +++ b/config/deploy.production.yml @@ -31,7 +31,7 @@ env: accessories: beamer: - image: basecamp/beamer:vfs + image: basecamp/beamer:zones registry: server: registry.37signals.com username: robot$harbor-bot diff --git a/config/deploy.staging.yml b/config/deploy.staging.yml index 4943aeb20..e0cb7fbce 100644 --- a/config/deploy.staging.yml +++ b/config/deploy.staging.yml @@ -27,7 +27,7 @@ env: accessories: beamer: - image: basecamp/beamer:vfs + image: basecamp/beamer:zones registry: server: registry.37signals.com username: robot$harbor-bot From 2f3c1699c9173e6ac370e90ec330fe420c30b209 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 12:45:03 +0100 Subject: [PATCH 128/173] Update lexxy --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 094571676..c2d9a1fbe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -296,7 +296,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.15.beta) + lexxy (0.1.16.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) From 912f7d179a722679695977c07357613c63e971f5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 4 Nov 2025 12:45:09 +0100 Subject: [PATCH 129/173] We already have a button to create the card --- app/views/cards/container/_status.html.erb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/cards/container/_status.html.erb b/app/views/cards/container/_status.html.erb index 492130b9e..043e35dc0 100644 --- a/app/views/cards/container/_status.html.erb +++ b/app/views/cards/container/_status.html.erb @@ -1,8 +1,5 @@ <% if card.drafted? && card.filled? %>
    - This is a draft, it’s only visible to you. - <%= button_to card_publish_path(@card), class: "btn txt-small", style: "--btn-background: #{card.color}" do %> - Post to project - <% end %> + Restored the draft you were working on
    <% end %> From 514c0f45a201b2fe5605f6177f1c58c8bb82b089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20=C3=81lvarez?= Date: Tue, 4 Nov 2025 13:17:10 +0100 Subject: [PATCH 130/173] Migrate to fizzy.do --- README.md | 2 +- app/assets/stylesheets/bubble.css | 2 +- app/models/account/seeder.rb | 2 +- config/environments/production.rb | 6 ++-- ...20251014204033_ensure_consistent_schema.rb | 2 +- .../active_storage_blob_previewable.rb | 2 +- script/configure-lb-production.sh | 30 +++++++++---------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index cb7c40f78..d05487602 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,6 @@ This environment uses a FlashBlade bucket for blob storage, and shares nothing w ### Production -- https://app.box-car.com/ +- https://app.fizzy.do/ This environment uses a FlashBlade bucket for blob storage. diff --git a/app/assets/stylesheets/bubble.css b/app/assets/stylesheets/bubble.css index 808fefe31..3eb595b65 100644 --- a/app/assets/stylesheets/bubble.css +++ b/app/assets/stylesheets/bubble.css @@ -45,7 +45,7 @@ .bubble__number { display: grid; - font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.box-car.com/5986089/collections/2/cards/1373 */ + font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.fizzy.do/5986089/collections/2/cards/1373 */ font-weight: 900; inset: 0; place-content: center; diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 80af1251a..04ae290e5 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -42,7 +42,7 @@ class Account::Seeder HTML playground.cards.create! creator: creator, title: "Head back home to check out activity", status: "published", description: <<~HTML -

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

    +

    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 diff --git a/config/environments/production.rb b/config/environments/production.rb index 3ccf2fce3..62a5b4cb0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -74,10 +74,10 @@ Rails.application.configure do # config.action_mailer.raise_delivery_errors = false # Set host to be used by links generated in mailer and notification view templates. - config.action_controller.default_url_options = { host: "app.box-car.com", protocol: "https" } - config.action_mailer.default_url_options = { host: "app.box-car.com", protocol: "https" } + config.action_controller.default_url_options = { host: "app.fizzy.do", protocol: "https" } + config.action_mailer.default_url_options = { host: "app.fizzy.do", protocol: "https" } - config.action_mailer.smtp_settings = { domain: "app.box-car.com", address: "smtp-outbound", port: 25, enable_starttls_auto: false } + config.action_mailer.smtp_settings = { domain: "app.fizzy.do", address: "smtp-outbound", port: 25, enable_starttls_auto: false } # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). diff --git a/db/migrate/20251014204033_ensure_consistent_schema.rb b/db/migrate/20251014204033_ensure_consistent_schema.rb index 216b0cca6..ec1bdac7f 100644 --- a/db/migrate/20251014204033_ensure_consistent_schema.rb +++ b/db/migrate/20251014204033_ensure_consistent_schema.rb @@ -1,5 +1,5 @@ class EnsureConsistentSchema < ActiveRecord::Migration[8.1] - # ref: https://app.box-car.com/5986089/cards/2322 + # ref: https://app.fizzy.do/5986089/cards/2322 def change change_column_default :accesses, :involvement, "access_only" diff --git a/lib/rails_ext/active_storage_blob_previewable.rb b/lib/rails_ext/active_storage_blob_previewable.rb index aa6a46fd2..c3ceb31ae 100644 --- a/lib/rails_ext/active_storage_blob_previewable.rb +++ b/lib/rails_ext/active_storage_blob_previewable.rb @@ -1,5 +1,5 @@ # -# see https://app.box-car.com/5986089/collections/7/cards/1302 +# see https://app.fizzy.do/5986089/collections/7/cards/1302 # # Large previewable uploads may take longer than the "pin reads" interval. So we pick a small-ish # number and turn off previews for anything larger, at least until we can come up with a permanent diff --git a/script/configure-lb-production.sh b/script/configure-lb-production.sh index f69656dce..a6bf7fd24 100755 --- a/script/configure-lb-production.sh +++ b/script/configure-lb-production.sh @@ -4,14 +4,14 @@ set -e # fizzy-lb-101.df-iad-int.37signals.com # -# Service Host Path Target State TLS -# boxcar fizzy.37signals.com,box-car.com,app.box-car.com / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-102.df-iad-int.37signals.com running yes -# boxcar-admin fizzy.37signals.com,box-car.com,app.box-car.com /admin fizzy-app-101.df-iad-int.37signals.com running yes +# Service Host Path Target State TLS +# fizzy app.box-car.com,app.fizzy.do / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-102.df-iad-int.37signals.com running yes +# fizzy-admin app.box-car.com,app.fizzy.do /admin fizzy-app-101.df-iad-int.37signals.com running yes ssh app@fizzy-lb-101.df-iad-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar \ --tls \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --target=fizzy-app-101.df-iad-int.37signals.com \ --read-target=fizzy-app-102.df-iad-int.37signals.com \ --tls-acme-cache-path=/certificates @@ -19,7 +19,7 @@ ssh app@fizzy-lb-101.df-iad-int.37signals.com \ ssh app@fizzy-lb-101.df-iad-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar-admin \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --path-prefix /admin \ --strip-path-prefix=false \ --target=fizzy-app-101.df-iad-int.37signals.com @@ -27,14 +27,14 @@ ssh app@fizzy-lb-101.df-iad-int.37signals.com \ # fizzy-lb-01.sc-chi-int.37signals.com # -# Service Host Path Target State TLS -# boxcar fizzy.37signals.com,box-car.com,app.box-car.com / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-02.sc-chi-int.37signals.com running yes -# boxcar-admin fizzy.37signals.com,box-car.com,app.box-car.com /admin fizzy-app-101.df-iad-int.37signals.com running yes +# Service Host Path Target State TLS +# fizzy app.box-car.com,app.fizzy.do / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-02.sc-chi-int.37signals.com running yes +# fizzy-admin app.box-car.com,app.fizzy.do /admin fizzy-app-101.df-iad-int.37signals.com running yes ssh app@fizzy-lb-01.sc-chi-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar \ --tls \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --target=fizzy-app-101.df-iad-int.37signals.com \ --read-target=fizzy-app-02.sc-chi-int.37signals.com \ --tls-acme-cache-path=/certificates @@ -42,7 +42,7 @@ ssh app@fizzy-lb-01.sc-chi-int.37signals.com \ ssh app@fizzy-lb-01.sc-chi-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar-admin \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --path-prefix /admin \ --strip-path-prefix=false \ --target=fizzy-app-101.df-iad-int.37signals.com @@ -50,14 +50,14 @@ ssh app@fizzy-lb-01.sc-chi-int.37signals.com \ # fizzy-lb-401.df-ams-int.37signals.com # -# Service Host Path Target State TLS -# boxcar fizzy.37signals.com,box-car.com,app.box-car.com / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-402.df-ams-int.37signals.com running yes -# boxcar-admin fizzy.37signals.com,box-car.com,app.box-car.com /admin fizzy-app-101.df-iad-int.37signals.com running yes +# Service Host Path Target State TLS +# fizzy app.box-car.com,app.fizzy.do / fizzy-app-101.df-iad-int.37signals.com,fizzy-app-402.df-ams-int.37signals.com running yes +# fizzy-admin app.box-car.com,app.fizzy.do /admin fizzy-app-101.df-iad-int.37signals.com running yes ssh app@fizzy-lb-401.df-ams-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar \ --tls \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --target=fizzy-app-101.df-iad-int.37signals.com \ --read-target=fizzy-app-402.df-ams-int.37signals.com \ --tls-acme-cache-path=/certificates @@ -65,7 +65,7 @@ ssh app@fizzy-lb-401.df-ams-int.37signals.com \ ssh app@fizzy-lb-401.df-ams-int.37signals.com \ docker exec fizzy-load-balancer \ kamal-proxy deploy boxcar-admin \ - --host=fizzy.37signals.com,box-car.com,app.box-car.com \ + --host=app.box-car.com,app.fizzy.do \ --path-prefix /admin \ --strip-path-prefix=false \ --target=fizzy-app-101.df-iad-int.37signals.com From 20596fa97e3fe18d4b27f2b0e80afb3864d5c883 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 13:42:32 +0100 Subject: [PATCH 131/173] Update lexxy --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 32ee21ff9..cc1518514 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,7 +294,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.16.beta) + lexxy (0.1.17.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) From 4146dc2b920eeb8165ff963b7bdcbd0fc2562ae0 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 4 Nov 2025 12:46:01 +0000 Subject: [PATCH 132/173] Disable Turbo prefetch on signup link This is because we have basic auth enabled there, so prefetching will trigger the popup. --- app/views/sessions/menus/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index 535d2b903..1c9af55ed 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -26,7 +26,7 @@ <% if defined?(saas) %>
    - <%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small" do %> + <%= 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 <% end %>
    From 9ca63db91cd974029b03e707ddf6f81d21ce3f72 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 4 Nov 2025 07:56:09 -0500 Subject: [PATCH 133/173] bin/setup no longer resets when migrations are pending --- bin/setup | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/setup b/bin/setup index 61ab69995..b32c2d96c 100755 --- a/bin/setup +++ b/bin/setup @@ -76,10 +76,12 @@ fi if [[ $* == *--reset* ]]; then step "Resetting the database" rails db:reset -elif needs_seeding; then - step "Setting up the database" rails db:reset else step "Preparing the database" rails db:prepare + + if needs_seeding; then + step "Seeding the database" rails db:seed + fi fi step "Cleaning up logs and tempfiles" rails log:clear tmp:clear From 9a2af5025fa1f8f70d3a3cd1468d9a21720aa6ed Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 4 Nov 2025 15:32:41 +0100 Subject: [PATCH 134/173] Fix the menu sing out button --- app/views/my/menus/_places.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/my/menus/_places.html.erb b/app/views/my/menus/_places.html.erb index 53802ce7d..2d0213f89 100644 --- a/app/views/my/menus/_places.html.erb +++ b/app/views/my/menus/_places.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_path, method: :delete, class: "popup__btn btn", data: { turbo: false } do %> + <%= button_to session_url(script_name: nil), method: :delete, class: "popup__btn btn", data: { turbo: false } do %> Sign out <% end %> <% end %> From 2bb90eab13a6f743992b6b1a0e0b8d0ee20c53be Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 16:59:40 +0100 Subject: [PATCH 135/173] Use the sole collection as the root for a better onboarding experience for new accounts --- app/controllers/concerns/authentication.rb | 2 +- app/controllers/join_codes_controller.rb | 2 +- app/controllers/landings_controller.rb | 9 +++++++++ app/controllers/users/joins_controller.rb | 2 +- .../sessions/menus/show.html+menu_section.erb | 2 +- app/views/sessions/menus/show.html.erb | 2 +- config/routes.rb | 2 ++ .../signups/completions_controller.rb | 2 +- .../signups/completions_controller_test.rb | 2 +- .../controllers/join_codes_controller_test.rb | 2 +- test/controllers/landings_controller_test.rb | 20 +++++++++++++++++++ .../users/joins_controller_test.rb | 2 +- 12 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 app/controllers/landings_controller.rb create mode 100644 test/controllers/landings_controller_test.rb diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index daedbb582..5afbcccf8 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -73,7 +73,7 @@ module Authentication end def after_authentication_url - session.delete(:return_to_after_authenticating) || root_url + session.delete(:return_to_after_authenticating) || landing_url end def redirect_authenticated_user diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 4bd5293ea..0ddbcdd7b 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -15,7 +15,7 @@ class JoinCodesController < ApplicationController identity.send_magic_link end - session[:return_to_after_authenticating] = root_url(script_name: "/#{tenant}") + session[:return_to_after_authenticating] = landing_url(script_name: "/#{tenant}") redirect_to session_magic_link_path end diff --git a/app/controllers/landings_controller.rb b/app/controllers/landings_controller.rb new file mode 100644 index 000000000..5e8fce46d --- /dev/null +++ b/app/controllers/landings_controller.rb @@ -0,0 +1,9 @@ +class LandingsController < ApplicationController + def show + if Current.user.collections.many? + redirect_to events_path + else + redirect_to collection_path(Current.user.collections.first) + end + end +end diff --git a/app/controllers/users/joins_controller.rb b/app/controllers/users/joins_controller.rb index 493887452..d811a6203 100644 --- a/app/controllers/users/joins_controller.rb +++ b/app/controllers/users/joins_controller.rb @@ -11,7 +11,7 @@ class Users::JoinsController < ApplicationController User.create!(user_params.merge(membership: Current.membership)) end - redirect_to root_path + redirect_to landing_path end private diff --git a/app/views/sessions/menus/show.html+menu_section.erb b/app/views/sessions/menus/show.html+menu_section.erb index 6aab21fe8..6a0a41346 100644 --- a/app/views/sessions/menus/show.html+menu_section.erb +++ b/app/views/sessions/menus/show.html+menu_section.erb @@ -2,7 +2,7 @@ <% cache [ Current.identity, @memberships ] do %> <%= collapsible_nav_section "Accounts" do %> <% @memberships.each do |membership| %> - <%= filter_place_menu_item root_url(script_name: "/#{membership.tenant}"), "#{membership.account_name}", "marker", new_window: true %> + <%= filter_place_menu_item landing_url(script_name: "/#{membership.tenant}"), "#{membership.account_name}", "marker", new_window: true %> <% end %> <% end %> <% end %> diff --git a/app/views/sessions/menus/show.html.erb b/app/views/sessions/menus/show.html.erb index 1c9af55ed..096ea7d5b 100644 --- a/app/views/sessions/menus/show.html.erb +++ b/app/views/sessions/menus/show.html.erb @@ -13,7 +13,7 @@ <% @memberships.each do |membership| %> diff --git a/config/routes.rb b/config/routes.rb index 89699f4dd..ef99bad1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,8 @@ Rails.application.routes.draw do end end + resource :landing + resources :collections do scope module: :collections do resource :subscriptions diff --git a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb b/gems/fizzy-saas/app/controllers/signups/completions_controller.rb index e86731f22..58dd528d2 100644 --- a/gems/fizzy-saas/app/controllers/signups/completions_controller.rb +++ b/gems/fizzy-saas/app/controllers/signups/completions_controller.rb @@ -13,7 +13,7 @@ class Signups::CompletionsController < ApplicationController @signup = Signup.new(signup_params) if @signup.complete - redirect_to root_url(script_name: "/#{@signup.tenant}") + redirect_to landing_url(script_name: "/#{@signup.tenant}") else render :new, status: :unprocessable_entity end 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 e9f8a472f..35861722d 100644 --- a/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb +++ b/gems/fizzy-saas/test/controllers/signups/completions_controller_test.rb @@ -33,7 +33,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest }, headers: http_basic_auth_headers end - assert_redirected_to root_url(script_name: "/#{@signup.tenant}"), "Successful completion should redirect to root in new tenant" + assert_redirected_to landing_path(script_name: "/#{@signup.tenant}"), "Successful completion should redirect to root in new tenant" untenanted do post saas.signup_completion_path, params: { diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb index de8c5daa5..2387e30ce 100644 --- a/test/controllers/join_codes_controller_test.rb +++ b/test/controllers/join_codes_controller_test.rb @@ -42,7 +42,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest end assert_redirected_to session_magic_link_path - assert_equal root_url(script_name: "/#{@tenant}"), session[:return_to_after_authenticating] + assert_equal landing_url(script_name: "/#{@tenant}"), session[:return_to_after_authenticating] end end diff --git a/test/controllers/landings_controller_test.rb b/test/controllers/landings_controller_test.rb new file mode 100644 index 000000000..eb24a3058 --- /dev/null +++ b/test/controllers/landings_controller_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +class LandingsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "redirects to the timeline when many collections" do + get landing_path + assert_redirected_to events_path + end + + test "redirects to collections when only one collection" do + sole_collection, *collections_to_delete = users(:kevin).collections.to_a + collections_to_delete.each(&:destroy) + + get landing_path + assert_redirected_to collection_path(sole_collection) + end +end diff --git a/test/controllers/users/joins_controller_test.rb b/test/controllers/users/joins_controller_test.rb index 3d06b7b08..4fe3ec218 100644 --- a/test/controllers/users/joins_controller_test.rb +++ b/test/controllers/users/joins_controller_test.rb @@ -26,7 +26,7 @@ class Users::JoinsControllerTest < ActionDispatch::IntegrationTest assert_difference -> { User.count }, +1 do post users_joins_path, params: { user: { name: "Newart Userbaum" } } - assert_redirected_to root_path + assert_redirected_to landing_path end end end From 57757021592a78cda61cb6dd81dcf252c1c5d38d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 4 Nov 2025 17:20:04 +0100 Subject: [PATCH 136/173] Fix smoke tests --- test/system/smoke_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index cc84bfef2..de994d348 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -51,7 +51,7 @@ class SmokeTest < ApplicationSystemTestCase def sign_in_as(user) visit session_transfer_url(user.identity.transfer_id, script_name: nil) click_on ApplicationRecord.with_tenant(user.tenant) { Account.sole.name } - assert_selector "h1", text: "Activity" + assert_selector "h1", text: "Writebook" end def fill_in_lexxy(selector = "lexxy-editor", with:) From 875515b51d8a6234e01e2e4af17ee917e401d739 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Tue, 4 Nov 2025 17:52:31 +0100 Subject: [PATCH 137/173] Skip the menu screen if there is just one account --- app/controllers/sessions/menus_controller.rb | 15 +++-- .../sessions/menus_controller_test.rb | 66 +++++++++++++++++-- test/system/smoke_test.rb | 3 +- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/app/controllers/sessions/menus_controller.rb b/app/controllers/sessions/menus_controller.rb index 91f3d742b..7ad3b6a09 100644 --- a/app/controllers/sessions/menus_controller.rb +++ b/app/controllers/sessions/menus_controller.rb @@ -1,17 +1,24 @@ class Sessions::MenusController < ApplicationController require_untenanted_access + before_action(if: :render_as_menu_section?) { request.variant = :menu_section } + layout "public" def show - if params[:menu_section] - request.variant = :menu_section - end - @memberships = Current.identity.memberships if params[:without] @memberships = @memberships.where.not(tenant: params[:without]) end + + if @memberships.one? && !render_as_menu_section? + redirect_to root_url(script_name: "/#{@memberships.first.tenant}") + end end + + private + def render_as_menu_section? + params[:menu_section] + end end diff --git a/test/controllers/sessions/menus_controller_test.rb b/test/controllers/sessions/menus_controller_test.rb index d85d594fb..c6b64d316 100644 --- a/test/controllers/sessions/menus_controller_test.rb +++ b/test/controllers/sessions/menus_controller_test.rb @@ -1,13 +1,69 @@ require "test_helper" class Sessions::MenusControllerTest < ActionDispatch::IntegrationTest - test "show" do + setup do + @identity = identities(:kevin) + end + + test "show with no memberships" do + sign_in_as @identity + @identity.memberships.delete_all + untenanted do - sign_in_as :kevin - get session_menu_url - - assert_response :success end + + assert_response :success, "Renders an empty menu" + end + + test "show with exactly one membership" do + sign_in_as @identity + @identity.memberships.delete_all + @identity.memberships.create(tenant: "37signals") + + untenanted do + get session_menu_url + end + + assert_response :redirect + assert_redirected_to root_url(script_name: "/37signals") + end + + test "show with multiple memeberships" do + sign_in_as @identity + @identity.memberships.delete_all + @identity.memberships.create(tenant: "37signals") + @identity.memberships.create(tenant: "acme") + + untenanted do + get session_menu_url + end + + assert_response :success + end + + test "show renders as a menu section" do + sign_in_as @identity + @identity.memberships.delete_all + @identity.memberships.create(tenant: "37signals") + @identity.memberships.create(tenant: "acme") + + untenanted do + get session_menu_url menu_section: true + end + + assert_response :success + end + + test "show doesn't redirect when rendered as a menu section" do + sign_in_as @identity + @identity.memberships.delete_all + @identity.memberships.create(tenant: "37signals") + + untenanted do + get session_menu_url menu_section: true + end + + assert_response :success end end diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb index de994d348..e9c875825 100644 --- a/test/system/smoke_test.rb +++ b/test/system/smoke_test.rb @@ -50,8 +50,7 @@ class SmokeTest < ApplicationSystemTestCase private def sign_in_as(user) visit session_transfer_url(user.identity.transfer_id, script_name: nil) - click_on ApplicationRecord.with_tenant(user.tenant) { Account.sole.name } - assert_selector "h1", text: "Writebook" + assert_selector "h1", text: "Latest Activity" end def fill_in_lexxy(selector = "lexxy-editor", with:) From b554d8ce5a5fc3418394fdc4456a53d24f3185f5 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 11:35:46 -0600 Subject: [PATCH 138/173] Add renaming gif --- app/models/account/seeder.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 04ae290e5..df5e322c6 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -82,6 +82,8 @@ class Account::Seeder
  • Then, hit "Mark as Done" at the bottom of the card.
  • Finally, hit “←Playground” in the top left of the screen to go back to the board.
  • +


    + HTML end From 8c5006152cde400314421f681c4b97ea9754b560 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 12:51:38 -0600 Subject: [PATCH 139/173] Add the rest --- app/models/account/seeder.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index df5e322c6..624f4abd1 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -39,26 +39,38 @@ class Account::Seeder playground.cards.create! creator: creator, title: "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

    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.

    +


    + HTML playground.cards.create! creator: creator, title: "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

    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

    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 @@ -70,10 +82,14 @@ class Account::Seeder

    Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.


    After that, drag this card to “DONE” or select “DONE” in the sidebar.

    +


    + HTML playground.cards.create! creator: creator, title: "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 From aaa24074f3bb1af435c522f6de640f905a3fc8bb Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 13:18:14 -0600 Subject: [PATCH 140/173] Fix magic link code input Fix that paste action didn't work and hitting `enter` caused the form to post twice --- app/views/sessions/magic_links/show.html.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/sessions/magic_links/show.html.erb b/app/views/sessions/magic_links/show.html.erb index 57932a475..cf81f458d 100644 --- a/app/views/sessions/magic_links/show.html.erb +++ b/app/views/sessions/magic_links/show.html.erb @@ -9,10 +9,10 @@

    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, - autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code], - data: { form_target: "input", action: "paste->form#submit, keydown.enter->form#submit" } %> + <%= 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, + autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code], + data: { form_target: "input", action: "paste->form#debouncedSubmit" } %> <% end %>
    From 1c5abbae2f1d0934ca1f899b3cdc20714e6b9d6d Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 16:27:44 -0600 Subject: [PATCH 141/173] Add space between label and hotkey another way --- app/views/my/menus/_button.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index 52305cf60..dbeb2e9bf 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -1,8 +1,8 @@ -<%= tag.button class:"boxcar-menu-trigger input input--select center flex-inline gap-half align-center txt-normal", +<%= 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 + J <% end %> From 08df019ce66548a5ae0c5d883eb1f7bd78837d82 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 16:34:28 -0600 Subject: [PATCH 142/173] Hide keyboard shortcuts on touch devices --- app/assets/stylesheets/utilities.css | 14 ++++++++++++++ app/views/bar/_bar.html.erb | 2 +- app/views/columns/show/_add_card_button.html.erb | 3 ++- app/views/events/index/_add_card_button.html.erb | 3 ++- app/views/my/menus/_button.html.erb | 2 +- app/views/my/pins/_tray.html.erb | 2 +- app/views/notifications/_tray.html.erb | 2 +- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css index 9b7b00bb1..99bac2252 100644 --- a/app/assets/stylesheets/utilities.css +++ b/app/assets/stylesheets/utilities.css @@ -233,4 +233,18 @@ .hide-focus-ring { --focus-ring-size: 0; } + + .hide-on-touch { + @media (any-hover: none) { + display: none; + } + } + + .show-on-touch { + display: none; + + @media (any-hover: none) { + display: unset; + } + } } diff --git a/app/views/bar/_bar.html.erb b/app/views/bar/_bar.html.erb index 095e7c0b2..6d2014e12 100644 --- a/app/views/bar/_bar.html.erb +++ b/app/views/bar/_bar.html.erb @@ -3,7 +3,7 @@
    <%= tag.button class: "btn btn--plain", data: { controller: "hotkey", action: "bar#search keydown.k@document->hotkey#click" } do %> - Search <% if platform.desktop? -%>K<% end %> + Search K <% end %>
    diff --git a/app/views/columns/show/_add_card_button.html.erb b/app/views/columns/show/_add_card_button.html.erb index be4f7b420..a161465ef 100644 --- a/app/views/columns/show/_add_card_button.html.erb +++ b/app/views/columns/show/_add_card_button.html.erb @@ -1,7 +1,8 @@
    <%= button_to collection_cards_path(collection), method: :post, class: "btn btn--link", form: { data: { turbo_frame: "_top" } }, data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %> + <%= icon_tag "add", class: "show-on-touch" %> Add a card - C + C <% end %> diff --git a/app/views/events/index/_add_card_button.html.erb b/app/views/events/index/_add_card_button.html.erb index fd0033541..664152e3d 100644 --- a/app/views/events/index/_add_card_button.html.erb +++ b/app/views/events/index/_add_card_button.html.erb @@ -1,8 +1,9 @@
    <% if collection = user_filtering.single_collection_or_first %> <%= button_to collection_cards_path(collection), 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 - C + C <% end %> <% end %>
    diff --git a/app/views/my/menus/_button.html.erb b/app/views/my/menus/_button.html.erb index dbeb2e9bf..85b2c9a93 100644 --- a/app/views/my/menus/_button.html.erb +++ b/app/views/my/menus/_button.html.erb @@ -4,5 +4,5 @@ controller: "hotkey" } do %> <%= image_tag "logo.png" %> Fizzy - J + J <% end %> diff --git a/app/views/my/pins/_tray.html.erb b/app/views/my/pins/_tray.html.erb index aa3a5d4e2..571018728 100644 --- a/app/views/my/pins/_tray.html.erb +++ b/app/views/my/pins/_tray.html.erb @@ -14,7 +14,7 @@ diff --git a/app/views/notifications/_tray.html.erb b/app/views/notifications/_tray.html.erb index b81e72b44..3d01d594c 100644 --- a/app/views/notifications/_tray.html.erb +++ b/app/views/notifications/_tray.html.erb @@ -42,7 +42,7 @@ From 739d77e0c6d03c1d6e10df8b2ca936ddb5996886 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 4 Nov 2025 18:48:59 -0500 Subject: [PATCH 143/173] Only include active users in Lexxy prompts Specifically, exclude users that are inactive or with role "system". ref: https://app.fizzy.do/5986089/cards/2760 ref: https://app.fizzy.do/5986089/cards/2789 --- app/controllers/prompts/users_controller.rb | 2 +- app/models/collection/accessible.rb | 2 +- test/controllers/collections_controller_test.rb | 2 +- test/models/collection/accessible_test.rb | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/prompts/users_controller.rb b/app/controllers/prompts/users_controller.rb index 1069c3841..c926e2de4 100644 --- a/app/controllers/prompts/users_controller.rb +++ b/app/controllers/prompts/users_controller.rb @@ -1,6 +1,6 @@ class Prompts::UsersController < ApplicationController def index - @users = User.all.alphabetically + @users = User.active.alphabetically if stale? etag: @users render layout: false diff --git a/app/models/collection/accessible.rb b/app/models/collection/accessible.rb index 42fb45692..2d6aee476 100644 --- a/app/models/collection/accessible.rb +++ b/app/models/collection/accessible.rb @@ -57,7 +57,7 @@ module Collection::Accessible end def grant_access_to_everyone - accesses.grant_to(User.all) if all_access_previously_changed?(to: true) + accesses.grant_to(User.active) if all_access_previously_changed?(to: true) end def mentions_for_user(user) diff --git a/test/controllers/collections_controller_test.rb b/test/controllers/collections_controller_test.rb index 92bb2237c..4839ceaab 100644 --- a/test/controllers/collections_controller_test.rb +++ b/test/controllers/collections_controller_test.rb @@ -86,7 +86,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to edit_collection_path(collection) assert collection.reload.all_access? - assert_equal User.all, collection.users + assert_equal User.active.sort, collection.users.sort end test "destroy" do diff --git a/test/models/collection/accessible_test.rb b/test/models/collection/accessible_test.rb index a04327bd3..96e33f460 100644 --- a/test/models/collection/accessible_test.rb +++ b/test/models/collection/accessible_test.rb @@ -18,7 +18,7 @@ class Collection::AccessibleTest < ActiveSupport::TestCase collection = Current.set(session: sessions(:david)) do Collection.create! name: "New collection", all_access: true end - assert_equal User.all, collection.users + assert_equal User.active.sort, collection.users.sort end test "grants access to everyone after update" do @@ -28,7 +28,7 @@ class Collection::AccessibleTest < ActiveSupport::TestCase assert_equal [ users(:david) ], collection.users collection.update! all_access: true - assert_equal User.all, collection.users.reload + assert_equal User.active.sort, collection.users.reload.sort end test "collection watchers" do From df640d50e56c302de0706b921152ded4b53dc2b4 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:53:37 -0600 Subject: [PATCH 144/173] Don't add draft status to cards --- app/assets/stylesheets/card-columns.css | 8 -------- app/helpers/cards_helper.rb | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 2e38b9447..eebbb74f9 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -441,14 +441,6 @@ } } - .card--drafted { - --card-color: var(--color-ink-medium) !important; - - border-color: var(--card-color) !important; - border-style: dashed !important; - box-shadow: none !important; - } - .card:has(.card__background img:not([src=""])) { .card__content, .card__meta, diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 5a4a35d62..2b67f8750 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -4,8 +4,7 @@ module CardsHelper options.delete(:class), ("golden-effect" if card.golden?), ("card--postponed" if card.postponed?), - ("card--active" if card.active?), - ("card--drafted" if card.drafted?) + ("card--active" if card.active?) ].compact.join(" ") tag.article \ From 1fcf28abcf4aa20a15b764a54054056f765c55ff Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:53:50 -0600 Subject: [PATCH 145/173] Don't include drafted cards in filtered views --- app/models/filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/filter.rb b/app/models/filter.rb index ca14629a9..903007c9f 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -18,7 +18,7 @@ class Filter < ApplicationRecord def cards @cards ||= begin - result = creator.accessible_cards.published_or_drafted_by(creator) + result = creator.accessible_cards.published result = result.indexed_by(indexed_by) result = result.sorted_by(sorted_by) result = result.where(id: card_ids) if card_ids.present? From b826eb47fe6f549cfb1cc4a3f28467b34e104b16 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:54:14 -0600 Subject: [PATCH 146/173] Remove draft restoration notice We don't need to announce that there is autosaved content --- app/views/cards/_container.html.erb | 2 -- app/views/cards/container/_status.html.erb | 5 ----- 2 files changed, 7 deletions(-) delete mode 100644 app/views/cards/container/_status.html.erb diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 3914f93ff..935bd05ec 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,7 +1,5 @@ <% cache card do %>
    - <%= render "cards/container/status", card: card %> -
    <%= render "cards/container/gild", card: card if card.published? && !card.closed? %> <%= render "cards/container/image", card: card %> diff --git a/app/views/cards/container/_status.html.erb b/app/views/cards/container/_status.html.erb deleted file mode 100644 index 043e35dc0..000000000 --- a/app/views/cards/container/_status.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<% if card.drafted? && card.filled? %> -
    - Restored the draft you were working on -
    -<% end %> From 81ec0154eaa417fb27164b2d37fc8d002c9f6177 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:55:36 -0600 Subject: [PATCH 147/173] Don't indicate drafts in preview cards --- app/views/cards/display/common/_meta.html.erb | 3 --- app/views/cards/display/public_preview/_meta.html.erb | 3 --- 2 files changed, 6 deletions(-) diff --git a/app/views/cards/display/common/_meta.html.erb b/app/views/cards/display/common/_meta.html.erb index dd2d8efbd..27a8c9cf7 100644 --- a/app/views/cards/display/common/_meta.html.erb +++ b/app/views/cards/display/common/_meta.html.erb @@ -5,9 +5,6 @@ Added <%= local_datetime_tag(card.created_at, style: :daysago) %> - <% if card.drafted? %> - (Draft) - <% end %> diff --git a/app/views/cards/display/public_preview/_meta.html.erb b/app/views/cards/display/public_preview/_meta.html.erb index 479f11432..948a6e73c 100644 --- a/app/views/cards/display/public_preview/_meta.html.erb +++ b/app/views/cards/display/public_preview/_meta.html.erb @@ -5,9 +5,6 @@ Added <%= local_datetime_tag(card.created_at, style: :daysago) %> - <% if card.drafted? %> - (Draft) - <% end %> From aac63499990fd4b9cacd153037eba3e53ff428cc Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:56:25 -0600 Subject: [PATCH 148/173] Remove draft status filter --- app/models/filter/fields.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index c24ed74c8..c44dff996 100644 --- a/app/models/filter/fields.rb +++ b/app/models/filter/fields.rb @@ -1,7 +1,7 @@ module Filter::Fields extend ActiveSupport::Concern - INDEXES = %w[ all closed not_now stalled postponing_soon golden draft ] + INDEXES = %w[ all closed not_now stalled postponing_soon golden ] SORTED_BY = %w[ newest oldest latest ] delegate :default_value?, to: :class From 1f1303b7a6b988675e3dca1819defb8bcef44d45 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Tue, 4 Nov 2025 21:59:55 -0600 Subject: [PATCH 149/173] Don't test for the draft status filter, it's gone --- test/models/filter_test.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/models/filter_test.rb b/test/models/filter_test.rb index fab856b87..ceda6943c 100644 --- a/test/models/filter_test.rb +++ b/test/models/filter_test.rb @@ -31,10 +31,6 @@ class FilterTest < ActiveSupport::TestCase filter = users(:david).filters.new card_ids: [ cards(:logo, :layout).collect(&:id) ] assert_equal [ cards(:logo), cards(:layout) ], filter.cards - - filter = users(:david).filters.new card_ids: [ cards(:logo, :layout).collect(&:id) ] - cards(:logo).drafted! - assert_equal [ cards(:logo), cards(:layout) ], filter.cards end test "can't see cards in collections that aren't accessible" do From d0e45a08522537b252f2af89a3e7477d40f261e0 Mon Sep 17 00:00:00 2001 From: Aaron Santos Date: Wed, 5 Nov 2025 13:00:00 +0800 Subject: [PATCH 150/173] Use fizzy as app name on deploy notifications --- bin/notify_dash_of_deployment | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/notify_dash_of_deployment b/bin/notify_dash_of_deployment index b7c2b8418..f3c650d69 100755 --- a/bin/notify_dash_of_deployment +++ b/bin/notify_dash_of_deployment @@ -10,7 +10,7 @@ if [ -n "$DASH_BASIC_AUTH_SECRET" ]; then --arg env "${5}" \ --arg duration "${6}" \ '{ - "application":"boxcar", + "application":"fizzy", "rails_env": $env, "branch": $branch, "deployer": $author, From 10d14b5774391b612e42cc967736f77e48426d0d Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 08:31:42 +0100 Subject: [PATCH 151/173] Don't choke when no collections --- app/controllers/landings_controller.rb | 6 +++--- test/controllers/landings_controller_test.rb | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/controllers/landings_controller.rb b/app/controllers/landings_controller.rb index 5e8fce46d..5c2709a1f 100644 --- a/app/controllers/landings_controller.rb +++ b/app/controllers/landings_controller.rb @@ -1,9 +1,9 @@ class LandingsController < ApplicationController def show - if Current.user.collections.many? - redirect_to events_path - else + if Current.user.collections.one? redirect_to collection_path(Current.user.collections.first) + else + redirect_to events_path end end end diff --git a/test/controllers/landings_controller_test.rb b/test/controllers/landings_controller_test.rb index eb24a3058..4358355f6 100644 --- a/test/controllers/landings_controller_test.rb +++ b/test/controllers/landings_controller_test.rb @@ -10,6 +10,12 @@ class LandingsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to events_path end + test "redirects to the timeline when no collections" do + Collection.destroy_all + get landing_path + assert_redirected_to events_path + end + test "redirects to collections when only one collection" do sole_collection, *collections_to_delete = users(:kevin).collections.to_a collections_to_delete.each(&:destroy) From 33b69408598e24ad26c90f7716ad430324e26fd3 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 09:21:01 +0100 Subject: [PATCH 152/173] Disable card perma refreshes for now The problem described here is fixed since we removed the banner, but I want to be sure there are not other issues here. Want to avoid glitches in such a critical screen https://app.fizzy.do/5986089/cards/2765#comment_994784439 --- app/views/cards/show.html.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index c33a2c40b..9ce4cf342 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -11,7 +11,6 @@
    <% end %> -<%= turbo_stream_from @card %> <%= turbo_stream_from @card, :activity %>
    From b66bad082afe08b49aa8707e6260314af0dc869b Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Wed, 5 Nov 2025 08:32:10 +0000 Subject: [PATCH 153/173] Specify asset path in deployment We need this so that app containers can serve old asset versions, to cover the cases where a request for an old asset is a MISS on the CDN. --- config/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/deploy.yml b/config/deploy.yml index 19e9649ea..8fa2f5b4c 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,5 +1,6 @@ service: fizzy image: basecamp/fizzy +asset_path: /rails/public/assets servers: jobs: From 498e4644cc030e58a2afb4e19afb92753aa2eba7 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 11:07:51 +0100 Subject: [PATCH 154/173] Bump lexxy --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cc1518514..cfbb22c7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,7 +294,7 @@ GEM logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) - lexxy (0.1.17.beta) + lexxy (0.1.18.beta) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) From 8e76633bc772f3df26a605a16681d45328e54e53 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 11:41:41 +0100 Subject: [PATCH 155/173] Make sure attribution in events is cache-friendly --- app/helpers/events_helper.rb | 7 ++++++- app/views/layouts/shared/_head.html.erb | 2 ++ app/views/layouts/shared/_user_css.html.erb | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/views/layouts/shared/_user_css.html.erb diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c7aa72715..7c55f857e 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -68,7 +68,12 @@ module EventsHelper end def event_creator_name(event) - event.creator == Current.user ? "You" : h(event.creator.name) + 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) diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb index 8e96ff3bf..997bc48db 100644 --- a/app/views/layouts/shared/_head.html.erb +++ b/app/views/layouts/shared/_head.html.erb @@ -20,6 +20,8 @@ <%= javascript_importmap_tags %> <%= tenanted_action_cable_meta_tag %> + <%= render "layouts/shared/user_css" %> + <%= yield :head %> <% if ApplicationRecord.current_tenant %> diff --git a/app/views/layouts/shared/_user_css.html.erb b/app/views/layouts/shared/_user_css.html.erb new file mode 100644 index 000000000..3a9bfc3dc --- /dev/null +++ b/app/views/layouts/shared/_user_css.html.erb @@ -0,0 +1,10 @@ +<% if Current.user %> + +<% end %> From 999043eda02c0109a23550121a5592e50b1dd532 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 11:48:36 +0100 Subject: [PATCH 156/173] Fix tests --- test/controllers/events_controller_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/controllers/events_controller_test.rb b/test/controllers/events_controller_test.rb index c89c06540..38affbac9 100644 --- a/test/controllers/events_controller_test.rb +++ b/test/controllers/events_controller_test.rb @@ -12,7 +12,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest get events_path assert_select "div.events__time-block[style='grid-area: 17/2']" do - assert_select "strong", text: "David assigned JZ to Layout is broken" + assert_select "strong", text: /assigned JZ to Layout is broken/ end end @@ -22,7 +22,7 @@ class EventsControllerTest < ActionDispatch::IntegrationTest get events_path assert_select "div.events__time-block[style='grid-area: 22/2']" do - assert_select "strong", text: "David assigned JZ to Layout is broken" + assert_select "strong", text: /assigned JZ to Layout is broken/ end end From cfdd7ab1cb8c3ee8779c4c036b973570dbbbccda Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 5 Nov 2025 11:57:40 +0100 Subject: [PATCH 157/173] Prohibit duplicate memberships --- app/controllers/join_codes_controller.rb | 6 ++- .../confirmations_controller.rb | 2 +- .../memberships/email_addresses_controller.rb | 9 +++- config/routes.rb | 2 +- ...n_identity_id_and_tenant_to_memberships.rb | 15 +++++++ db/untenanted_schema.rb | 3 +- .../controllers/join_codes_controller_test.rb | 2 +- .../confirmations_controller_test.rb | 42 ++++++++++++++++++ .../email_addresses_controller_test.rb | 43 +++++++++++++++++++ .../memberships/unlink_controller_test.rb | 30 +++++++++++++ .../email_address_changeable_test.rb | 3 ++ 11 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 db/untenanted_migrate/20251105082803_add_unique_constraint_on_identity_id_and_tenant_to_memberships.rb create mode 100644 test/controllers/memberships/email_addresses/confirmations_controller_test.rb create mode 100644 test/controllers/memberships/email_addresses_controller_test.rb create mode 100644 test/controllers/memberships/unlink_controller_test.rb diff --git a/app/controllers/join_codes_controller.rb b/app/controllers/join_codes_controller.rb index 0ddbcdd7b..f9980a8db 100644 --- a/app/controllers/join_codes_controller.rb +++ b/app/controllers/join_codes_controller.rb @@ -10,8 +10,10 @@ class JoinCodesController < ApplicationController def create Identity.transaction do - identity = Identity.find_or_create_by(email_address: params.expect(:email_address)) - identity.memberships.create!(tenant: tenant, join_code: code) + identity = Identity.find_or_create_by!(email_address: params.expect(:email_address)) + identity.memberships.find_or_create_by!(tenant: tenant) do |membership| + membership.join_code = code + end identity.send_magic_link end diff --git a/app/controllers/memberships/email_addresses/confirmations_controller.rb b/app/controllers/memberships/email_addresses/confirmations_controller.rb index 8ad43971f..b53fdc135 100644 --- a/app/controllers/memberships/email_addresses/confirmations_controller.rb +++ b/app/controllers/memberships/email_addresses/confirmations_controller.rb @@ -11,7 +11,7 @@ class Memberships::EmailAddresses::ConfirmationsController < ApplicationControll def create membership = Membership.change_email_address_using_token(token) - terminate_session + terminate_session if Current.session start_new_session_for membership.reload.identity redirect_to edit_user_url(script_name: "/#{@membership.tenant}", id: @membership.user) diff --git a/app/controllers/memberships/email_addresses_controller.rb b/app/controllers/memberships/email_addresses_controller.rb index e918005e1..c75783ac1 100644 --- a/app/controllers/memberships/email_addresses_controller.rb +++ b/app/controllers/memberships/email_addresses_controller.rb @@ -8,7 +8,14 @@ class Memberships::EmailAddressesController < ApplicationController end def create - @membership.send_email_address_change_confirmation(new_email_address) + identity = Identity.find_by_email_address(new_email_address) + + if identity&.memberships&.exists?(tenant: @membership.tenant) + flash[:alert] = "You already have a user in this account with that email address" + redirect_to new_email_address_path + else + @membership.send_email_address_change_confirmation(new_email_address) + end end private diff --git a/config/routes.rb b/config/routes.rb index ef99bad1b..af7b9c383 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -151,7 +151,7 @@ Rails.application.routes.draw do end scope module: :memberships, path: "memberships/:membership_id" do - resource :unlink, controller: :unlink, as: :unlink_membership + resource :unlink, only: %i[ show create ], controller: :unlink, as: :unlink_membership resources :email_addresses, param: :token do resource :confirmation, module: :email_addresses diff --git a/db/untenanted_migrate/20251105082803_add_unique_constraint_on_identity_id_and_tenant_to_memberships.rb b/db/untenanted_migrate/20251105082803_add_unique_constraint_on_identity_id_and_tenant_to_memberships.rb new file mode 100644 index 000000000..5d42ac6ed --- /dev/null +++ b/db/untenanted_migrate/20251105082803_add_unique_constraint_on_identity_id_and_tenant_to_memberships.rb @@ -0,0 +1,15 @@ +class AddUniqueConstraintOnIdentityIdAndTenantToMemberships < ActiveRecord::Migration[8.2] + def change + # Remove duplicates, keeping the youngest membership for each identity_id + tenant combination + execute <<-SQL + DELETE FROM memberships + WHERE id NOT IN ( + SELECT MAX(id) + FROM memberships + GROUP BY identity_id, tenant + ) + SQL + + add_index :memberships, %i[ tenant identity_id ], unique: true + end +end diff --git a/db/untenanted_schema.rb b/db/untenanted_schema.rb index b4582291a..efe4c961b 100644 --- a/db/untenanted_schema.rb +++ b/db/untenanted_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_03_125952) do +ActiveRecord::Schema[8.2].define(version: 2025_11_05_082803) do create_table "identities", force: :cascade do |t| t.datetime "created_at", null: false t.string "email_address", null: false @@ -36,6 +36,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_03_125952) do t.string "tenant", null: false t.datetime "updated_at", null: false t.index ["identity_id"], name: "index_memberships_on_identity_id" + t.index ["tenant", "identity_id"], name: "index_memberships_on_tenant_and_identity_id", unique: true t.index ["tenant"], name: "index_memberships_on_user_tenant_and_user_id" end diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb index 2387e30ce..85a6fc15a 100644 --- a/test/controllers/join_codes_controller_test.rb +++ b/test/controllers/join_codes_controller_test.rb @@ -51,7 +51,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest untenanted do assert_no_difference -> { Identity.count } do - assert_difference -> { Membership.count }, 1 do + assert_no_difference -> { Membership.count } do post join_path(tenant: @tenant, code: @join_code.code), params: { email_address: identity.email_address } end end diff --git a/test/controllers/memberships/email_addresses/confirmations_controller_test.rb b/test/controllers/memberships/email_addresses/confirmations_controller_test.rb new file mode 100644 index 000000000..86c5ff171 --- /dev/null +++ b/test/controllers/memberships/email_addresses/confirmations_controller_test.rb @@ -0,0 +1,42 @@ +require "test_helper" + +class Memberships::EmailAddresses::ConfirmationsControllerTest < ActionDispatch::IntegrationTest + test "show" do + untenanted do + membership = memberships(:kevin_in_37signals) + + get email_address_confirmation_path( + membership_id: membership.id, + email_address_token: "dummy_token" + ) + + assert_response :success + end + end + + test "create" do + untenanted do + membership = memberships(:kevin_in_37signals) + old_identity = membership.identity + new_email = "updated@example.com" + + # Generate a real token + token = membership.send(:generate_email_address_change_token, to: new_email) + + assert_difference -> { Identity.count }, 1 do + post email_address_confirmation_path( + membership_id: membership.id, + email_address_token: token + ), + params: { email_address_token: token } + end + + membership.reload + assert_equal new_email, membership.identity.email_address + assert_not_equal old_identity.id, membership.identity_id + + assert cookies[:session_token].present?, "Should have started new session" + assert_redirected_to edit_user_url(script_name: "/#{membership.tenant}", id: membership.user) + end + end +end diff --git a/test/controllers/memberships/email_addresses_controller_test.rb b/test/controllers/memberships/email_addresses_controller_test.rb new file mode 100644 index 000000000..ef88bfaa1 --- /dev/null +++ b/test/controllers/memberships/email_addresses_controller_test.rb @@ -0,0 +1,43 @@ +require "test_helper" + +class Memberships::EmailAddressesControllerTest < ActionDispatch::IntegrationTest + test "new" do + untenanted do + sign_in_as :kevin + + membership = memberships(:kevin_in_37signals) + + get new_email_address_path(membership_id: membership.id) + assert_response :success + end + end + + test "create" do + untenanted do + sign_in_as :kevin + + membership = memberships(:kevin_in_37signals) + + assert_enqueued_emails 1 do + post email_addresses_path(membership_id: membership.id), + params: { email_address: "newemail@example.com" } + end + + assert_response :success + end + end + + test "create with an email for someone already in the account" do + untenanted do + sign_in_as :kevin + + membership = memberships(:kevin_in_37signals) + + post email_addresses_path(membership_id: membership.id), + params: { email_address: identities(:david).email_address } + + assert_redirected_to new_email_address_path + assert_equal "You already have a user in this account with that email address", flash[:alert] + end + end +end diff --git a/test/controllers/memberships/unlink_controller_test.rb b/test/controllers/memberships/unlink_controller_test.rb new file mode 100644 index 000000000..2020a2eb2 --- /dev/null +++ b/test/controllers/memberships/unlink_controller_test.rb @@ -0,0 +1,30 @@ +require "test_helper" + +class Memberships::UnlinkControllerTest < ActionDispatch::IntegrationTest + test "show" do + untenanted do + sign_in_as :kevin + + membership = memberships(:kevin_in_37signals) + signed_id = membership.signed_id(purpose: :unlinking) + + get unlink_membership_path(membership_id: signed_id) + assert_response :success + end + end + + test "create" do + untenanted do + sign_in_as :kevin + + membership = memberships(:kevin_in_37signals) + signed_id = membership.signed_id(purpose: :unlinking) + + assert_difference -> { Membership.count }, -1 do + post unlink_membership_path(membership_id: signed_id) + end + + assert_redirected_to session_menu_path + end + end +end diff --git a/test/models/membership/email_address_changeable_test.rb b/test/models/membership/email_address_changeable_test.rb index bad777c81..fbc61bbb1 100644 --- a/test/models/membership/email_address_changeable_test.rb +++ b/test/models/membership/email_address_changeable_test.rb @@ -27,6 +27,9 @@ class Membership::EmailAddressChangeableTest < ActiveSupport::TestCase assert_not old_identity.reload.memberships.exists?(id: @membership.id) assert_equal @new_email, @membership.reload.identity.email_address + # Make sure that a prior membership doesn't exist + identities(:david).memberships.where(tenant: @tenant).delete_all + assert_no_difference -> { Identity.count } do @membership.change_email_address(identities(:david).email_address) end From 3f054c79dd7af571eba5890d27216830dbf247a0 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 5 Nov 2025 12:09:47 +0100 Subject: [PATCH 158/173] Check that the join code was copied to the membership --- test/controllers/join_codes_controller_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/controllers/join_codes_controller_test.rb b/test/controllers/join_codes_controller_test.rb index 85a6fc15a..070662b5a 100644 --- a/test/controllers/join_codes_controller_test.rb +++ b/test/controllers/join_codes_controller_test.rb @@ -41,6 +41,8 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest end end + assert_equal @join_code.code, Membership.last.join_code + assert_redirected_to session_magic_link_path assert_equal landing_url(script_name: "/#{@tenant}"), session[:return_to_after_authenticating] end From 03a345609e92225514c8e4c7cbf859ad23ce2dc5 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 5 Nov 2025 13:31:54 +0100 Subject: [PATCH 159/173] Baseline replacing collection with board across code --- app/assets/stylesheets/bubble.css | 2 +- app/assets/stylesheets/card-columns.css | 10 +-- app/assets/stylesheets/card-perma.css | 2 +- app/assets/stylesheets/cards.css | 10 +-- app/assets/stylesheets/events.css | 4 +- app/assets/stylesheets/icons.css | 4 +- app/assets/stylesheets/print.css | 6 +- app/assets/stylesheets/trays.css | 2 +- .../cards/assignments_controller.rb | 4 +- .../cards/collections_controller.rb | 12 +-- app/controllers/cards/columns_controller.rb | 2 +- app/controllers/cards/publishes_controller.rb | 4 +- app/controllers/cards/readings_controller.rb | 6 +- app/controllers/cards/triages_controller.rb | 2 +- app/controllers/cards_controller.rb | 12 +-- .../collections/columns/closeds_controller.rb | 6 +- .../columns/not_nows_controller.rb | 6 +- .../collections/columns/streams_controller.rb | 6 +- .../collections/columns_controller.rb | 8 +- .../collections/entropies_controller.rb | 8 +- .../collections/involvements_controller.rb | 6 +- .../collections/publications_controller.rb | 10 +-- app/controllers/collections_controller.rb | 42 +++++----- .../columns/cards/drops/columns_controller.rb | 2 +- .../columns/cards/drops/streams_controller.rb | 2 +- app/controllers/concerns/card_scoped.rb | 6 +- app/controllers/concerns/collection_scoped.rb | 8 +- app/controllers/landings_controller.rb | 4 +- .../notifications/settings_controller.rb | 2 +- .../prompts/collections/users_controller.rb | 6 +- app/controllers/public/base_controller.rb | 8 +- .../collections/columns/closeds_controller.rb | 4 +- .../columns/not_nows_controller.rb | 4 +- .../collections/columns/streams_controller.rb | 4 +- .../public/collections/columns_controller.rb | 4 +- .../public/collections_controller.rb | 4 +- app/controllers/webhooks_controller.rb | 14 ++-- app/helpers/accesses_helper.rb | 32 +++---- app/helpers/cards_helper.rb | 4 +- app/helpers/collections_helper.rb | 14 ++-- app/helpers/events_helper.rb | 6 +- app/helpers/filters_helper.rb | 4 +- app/helpers/my/menu_helper.rb | 2 +- app/helpers/rich_text_helper.rb | 8 +- app/helpers/webhooks_helper.rb | 8 +- .../collapsible_columns_controller.js | 4 +- .../controllers/toggle_class_controller.js | 2 +- .../collection/clean_inaccessible_data_job.rb | 6 +- app/models/access.rb | 4 +- app/models/account/seeder.rb | 6 +- app/models/card.rb | 26 +++--- app/models/card/entropic.rb | 6 +- app/models/card/eventable/system_commenter.rb | 4 +- app/models/card/promptable.rb | 4 +- app/models/card/triageable.rb | 2 +- app/models/collection.rb | 2 +- app/models/collection/accessible.rb | 10 +-- app/models/collection/auto_closing.rb | 2 +- app/models/collection/broadcastable.rb | 4 +- app/models/collection/cards.rb | 2 +- app/models/collection/entropic.rb | 2 +- app/models/collection/publication.rb | 4 +- app/models/collection/publishable.rb | 6 +- app/models/collection/triageable.rb | 2 +- app/models/column.rb | 4 +- app/models/column/positioned.rb | 6 +- app/models/comment.rb | 2 +- app/models/comment/eventable.rb | 2 +- app/models/concerns/mentions.rb | 2 +- app/models/event.rb | 2 +- app/models/eventable.rb | 4 +- app/models/filter.rb | 10 +-- app/models/filter/params.rb | 8 +- app/models/filter/resources.rb | 18 ++-- app/models/notifier/card_event_notifier.rb | 6 +- app/models/search.rb | 4 +- app/models/user/accessor.rb | 10 +-- app/models/user/day_timeline.rb | 8 +- app/models/user/filtering.rb | 26 +++--- app/models/user/timelined.rb | 2 +- app/models/webhook.rb | 4 +- app/models/webhook/triggerable.rb | 2 +- app/views/account/settings/_users.html.erb | 2 +- app/views/cards/_broadcasts.html.erb | 8 +- app/views/cards/_card.json.jbuilder | 4 +- app/views/cards/_container.html.erb | 2 +- app/views/cards/_messages.html.erb | 2 +- app/views/cards/collections/edit.html.erb | 10 +-- app/views/cards/comments/_new.html.erb | 2 +- app/views/cards/comments/edit.html.erb | 2 +- .../comments/reactions/_reactions.html.erb | 2 +- app/views/cards/container/_title.html.erb | 2 +- app/views/cards/display/_preview.html.erb | 2 +- app/views/cards/display/_previews.html.erb | 2 +- .../cards/display/_public_preview.html.erb | 8 +- .../cards/display/_public_previews.html.erb | 2 +- .../cards/display/common/_collection.html.erb | 6 +- .../cards/display/perma/_collection.html.erb | 8 +- app/views/cards/display/perma/_steps.html.erb | 2 +- app/views/cards/display/perma/_tags.html.erb | 2 +- .../display/preview/_collection.html.erb | 2 +- .../cards/display/preview/_columns.html.erb | 2 +- .../display/public_preview/_columns.html.erb | 2 +- app/views/cards/edit.html.erb | 2 +- app/views/cards/index.html.erb | 8 +- .../cards/previews/index.turbo_stream.erb | 2 +- app/views/cards/show.html.erb | 2 +- app/views/collections/_access_toggle.erb | 2 +- .../collections/_collection.json.jbuilder | 8 +- .../collections/columns/closeds/show.html.erb | 6 +- .../columns/create.turbo_stream.erb | 2 +- .../columns/not_nows/show.html.erb | 6 +- app/views/collections/columns/show.html.erb | 6 +- .../collections/columns/streams/show.html.erb | 6 +- .../columns/update.turbo_stream.erb | 2 +- app/views/collections/edit.html.erb | 24 +++--- .../collections/edit/_auto_close.html.erb | 4 +- app/views/collections/edit/_delete.html.erb | 2 +- app/views/collections/edit/_name.html.erb | 2 +- .../collections/edit/_publication.html.erb | 16 ++-- app/views/collections/edit/_users.html.erb | 6 +- .../entropies/update.turbo_stream.erb | 2 +- .../collections/involvements/update.html.erb | 2 +- app/views/collections/new.html.erb | 2 +- .../publications/create.turbo_stream.erb | 2 +- .../publications/destroy.turbo_stream.erb | 2 +- app/views/collections/show.html.erb | 20 ++--- app/views/collections/show/_closed.html.erb | 6 +- app/views/collections/show/_column.html.erb | 8 +- app/views/collections/show/_columns.html.erb | 12 +-- app/views/collections/show/_not_now.html.erb | 6 +- app/views/collections/show/_stream.html.erb | 6 +- .../collections/show/menu/_column.html.erb | 4 +- .../show/menu/_column_form.html.erb | 2 +- .../collections/show/menu/_columns.html.erb | 2 +- .../drops/closures/create.turbo_stream.erb | 2 +- .../drops/columns/create.turbo_stream.erb | 2 +- .../drops/not_nows/create.turbo_stream.erb | 2 +- .../drops/streams/create.turbo_stream.erb | 2 +- .../left_positions/create.turbo_stream.erb | 2 +- .../right_positions/create.turbo_stream.erb | 2 +- .../columns/show/_add_card_button.html.erb | 6 +- app/views/events/_day_timeline.html.erb | 2 +- .../events/day_timeline/_column.html.erb | 2 +- app/views/events/event/_attachments.html.erb | 6 +- app/views/events/event/_layout.html.erb | 6 +- app/views/events/index.html.erb | 4 +- .../events/index/_add_card_button.html.erb | 4 +- app/views/events/index/_filter.html.erb | 16 ++-- .../filters/settings/_collections.html.erb | 14 ++-- app/views/filters/settings/_controls.html.erb | 2 +- .../bundle_mailer/_header.html.erb | 2 +- .../bundle_mailer/_header.text.erb | 2 +- .../bundle_mailer/notification.html.erb | 2 +- .../bundle_mailer/notification.text.erb | 2 +- app/views/my/menus/_collections.html.erb | 6 +- app/views/my/menus/_hotkeys.html.erb | 2 +- app/views/my/menus/_saved_filter.html.erb | 2 +- app/views/my/menus/_tags.html.erb | 2 +- app/views/my/menus/_users.html.erb | 2 +- .../my/menus/collections/_collection.html.erb | 8 +- .../my/menus/saved_filter/_filter.html.erb | 2 +- app/views/my/menus/show.html.erb | 2 +- app/views/my/pins/index.html.erb | 2 +- app/views/notifications/index.html.erb | 4 +- .../notifications/index.turbo_stream.erb | 2 +- .../notification/event/_header.html.erb | 6 +- .../notification/mention/_header.html.erb | 6 +- .../settings/_collection.html.erb | 6 +- .../notifications/settings/show.html.erb | 2 +- app/views/notifications/trays/show.html.erb | 2 +- app/views/prompts/cards/index.html.erb | 2 +- .../prompts/collections/users/index.html.erb | 2 +- app/views/prompts/commands/index.html.erb | 2 +- app/views/prompts/tags/index.html.erb | 2 +- app/views/prompts/users/index.html.erb | 2 +- app/views/public/cards/show.html.erb | 10 +-- .../card_previews/index.turbo_stream.erb | 4 +- .../collections/columns/closeds/show.html.erb | 2 +- .../columns/not_nows/show.html.erb | 2 +- .../public/collections/columns/show.html.erb | 2 +- .../collections/columns/streams/show.html.erb | 2 +- app/views/public/collections/show.html.erb | 20 ++--- .../public/collections/show/_closed.html.erb | 4 +- .../public/collections/show/_column.html.erb | 6 +- .../public/collections/show/_columns.html.erb | 10 +-- .../collections/show/_considering.html.erb | 2 +- .../public/collections/show/_doing.html.erb | 2 +- .../public/collections/show/_not_now.html.erb | 6 +- .../public/collections/show/_on_deck.html.erb | 6 +- .../public/collections/show/_stream.html.erb | 2 +- app/views/searches/_result.html.erb | 2 +- app/views/searches/_results.html.erb | 2 +- app/views/webhooks/edit.html.erb | 2 +- app/views/webhooks/event.json.jbuilder | 4 +- app/views/webhooks/form/_actions.html.erb | 2 +- app/views/webhooks/index.html.erb | 10 +-- app/views/webhooks/new.html.erb | 6 +- app/views/webhooks/show.html.erb | 8 +- config/routes.rb | 22 ++--- script/load-prod-db-in-dev.rb | 4 +- .../20251029-populate-column-positions.rb | 6 +- .../migrate-content-to-slugged-urls.rb | 8 +- .../migrations/migrate_to_flat_card_urls.rb | 2 +- .../migrate_to_new_cards_url_scheme.rb | 2 +- .../populate_columns_from_workflow_stages.rb | 16 ++-- script/migrations/renaming/content.rb | 2 +- script/migrations/renaming/files.rb | 2 +- script/migrations/reset_collections_ids.rb | 70 ++++++++-------- script/populate.rb | 10 +-- .../cards/collections_controller_test.rb | 12 +-- .../cards/publishes_controller_test.rb | 2 +- test/controllers/cards_controller_test.rb | 12 +-- .../columns/closeds_controller_test.rb | 4 +- .../columns/not_nows_controller_test.rb | 4 +- .../columns/streams_controller_test.rb | 4 +- .../collections/columns_controller_test.rb | 16 ++-- .../collections/entropies_controller_test.rb | 10 +-- .../involvements_controller_test.rb | 10 +-- .../publications_controller_test.rb | 26 +++--- .../collections_controller_test.rb | 84 +++++++++---------- .../columns/left_positions_controller_test.rb | 4 +- .../right_positions_controller_test.rb | 4 +- test/controllers/events_controller_test.rb | 6 +- test/controllers/filters_controller_test.rb | 4 +- test/controllers/landings_controller_test.rb | 14 ++-- .../collections/users_controller_test.rb | 6 +- .../public/cards_controller_test.rb | 12 +-- .../columns/closeds_controller_test.rb | 6 +- .../columns/not_nows_controller_test.rb | 6 +- .../columns/streams_controller_test.rb | 6 +- .../collections/columns_controller_test.rb | 6 +- .../public/collections_controller_test.rb | 16 ++-- .../webhooks/activations_controller_test.rb | 4 +- test/controllers/webhooks_controller_test.rb | 34 ++++---- test/fixtures/accesses.yml | 8 +- test/fixtures/cards.yml | 10 +-- test/fixtures/collections.yml | 2 +- test/fixtures/columns.yml | 12 +-- test/fixtures/entropies.yml | 8 +- test/fixtures/events.yml | 20 ++--- test/fixtures/webhooks.yml | 4 +- test/models/access_test.rb | 16 ++-- test/models/account/seedeable_test.rb | 4 +- test/models/card/commentable_test.rb | 2 +- test/models/card/entropic_test.rb | 18 ++-- test/models/card/eventable_test.rb | 2 +- test/models/card/messages_test.rb | 2 +- test/models/card/readable_test.rb | 2 +- test/models/card/stallable_test.rb | 4 +- test/models/card/statuses_test.rb | 12 +-- test/models/card/triageable_test.rb | 10 +-- test/models/card/watchable_test.rb | 6 +- test/models/card_test.rb | 54 ++++++------ test/models/collection/accessible_test.rb | 42 +++++----- test/models/collection/cards_test.rb | 12 +-- test/models/collection/publishable_test.rb | 46 +++++----- test/models/column/positioned_test.rb | 22 ++--- test/models/column_test.rb | 6 +- test/models/concerns/mentions_test.rb | 22 ++--- test/models/entropy_test.rb | 8 +- test/models/filter_test.rb | 46 +++++----- test/models/notifier/event_notifier_test.rb | 16 ++-- test/models/search_test.rb | 4 +- test/models/user/accessor_test.rb | 10 +-- test/models/user_test.rb | 6 +- test/models/webhook/delivery_test.rb | 8 +- test/models/webhook_test.rb | 16 ++-- test/system/smoke_test.rb | 2 +- 269 files changed, 990 insertions(+), 990 deletions(-) diff --git a/app/assets/stylesheets/bubble.css b/app/assets/stylesheets/bubble.css index 3eb595b65..33dd732c2 100644 --- a/app/assets/stylesheets/bubble.css +++ b/app/assets/stylesheets/bubble.css @@ -45,7 +45,7 @@ .bubble__number { display: grid; - font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.fizzy.do/5986089/collections/2/cards/1373 */ + font-size: clamp(10px, 50cqi, var(--bubble-number-max)); /* FF bug: https://app.fizzy.do/5986089/boards/2/cards/1373 */ font-weight: 900; inset: 0; place-content: center; diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index eebbb74f9..a74e8e3f8 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -492,7 +492,7 @@ } } - .card__collection { + .card__board { background-color: transparent; color: var(--card-content-color); } @@ -519,10 +519,10 @@ } } - /* Collection tools + /* Board tools /* -------------------------------------------------------------------------- */ - .collection-tools.card { + .board-tools.card { --border-color: var(--color-selected-dark); --border-size: 1px; --card-padding-block: var(--block-space); @@ -560,7 +560,7 @@ } } - .collection-tools__watching { + .board-tools__watching { --btn-size: 32px; --gap: 0.5ch; @@ -572,7 +572,7 @@ position: relative; } - .collection-tools__watching-dialog { + .board-tools__watching-dialog { --panel-padding: 2ch; --panel-size: 100%; diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index d12e3eddc..0160fdaaf 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -85,7 +85,7 @@ } } - .collection-picker__button { + .board-picker__button { --btn-border-size: 0; font-size: 0.5em; diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index c32dc74c5..2b61ac4ce 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -55,7 +55,7 @@ } } - .card__collection { + .card__board { background-color: var(--card-color); border-radius: var(--border-radius) 0 var(--border-radius) 0; color: var(--color-ink-inverted); @@ -66,7 +66,7 @@ padding-block: 0.25lh; padding-inline: var(--card-padding-inline) 1ch; - &:has(.collection-picker__button) { + &:has(.board-picker__button) { cursor: pointer; transition: background-color 100ms ease-out; @@ -92,7 +92,7 @@ } } - .card__collection-name { + .card__board-name { border-inline-start: 1px solid color-mix(in hsl, transparent 75%, currentColor); color: currentColor; display: flex; @@ -404,7 +404,7 @@ padding-block-end: 0; } - .card__collection { + .card__board { font-size: var(--text-xx-small); padding-block: 0.5ch; padding-inline-start: var(--inline-space-double); @@ -487,7 +487,7 @@ } } - .card__collection-public-description { + .card__board-public-description { max-inline-size: 66ch; > *:first-child { margin-block-start: 0; } diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 2ff88e5eb..06eaaa15c 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -270,7 +270,7 @@ z-index: 0; } - .card__collection { + .card__board { background-color: transparent; color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); font-size: 0.7em; @@ -278,7 +278,7 @@ translate: 0 -0.3em; } - .card__collection-name { + .card__board-name { border-inline-start: 1px solid color-mix(in srgb, var(--color-ink) 33%, var(--color-canvas)); color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); margin: 0 0 0 calc(var(--inline-space) * 0.75); diff --git a/app/assets/stylesheets/icons.css b/app/assets/stylesheets/icons.css index e1c4df1d7..8575b72f2 100644 --- a/app/assets/stylesheets/icons.css +++ b/app/assets/stylesheets/icons.css @@ -42,8 +42,8 @@ .icon--clipboard { --svg: url("clipboard.svg "); } .icon--close { --svg: url("close.svg "); } .icon--close-circle { --svg: url("close-circle.svg "); } - .icon--collection { --svg: url("collection.svg "); } - .icon--collection-add { --svg: url("collection-add.svg "); } + .icon--board { --svg: url("board.svg "); } + .icon--board-add { --svg: url("board-add.svg "); } .icon--column-left { --svg: url("column-left.svg "); } .icon--column-right { --svg: url("column-right.svg "); } .icon--comment { --svg: url("comment.svg "); } diff --git a/app/assets/stylesheets/print.css b/app/assets/stylesheets/print.css index d3f849805..df7f8d3a8 100644 --- a/app/assets/stylesheets/print.css +++ b/app/assets/stylesheets/print.css @@ -63,7 +63,7 @@ } .card { - .card__collection { + .card__board { background: var(--color-canvas); border-block-end: var(--border-light); border-inline-end: var(--border-light); @@ -118,7 +118,7 @@ } } - /* Collections + /* Boards /* ------------------------------------------------------------------------ */ .filters, @@ -181,7 +181,7 @@ .card__meta-avatars--assignees > div > div:last-child, .steps > .step:last-child, div:has(> .tag-picker__button), - div:has(> .collection-picker__button), + div:has(> .board-picker__button), .delete-card, .header--card .header__title { display: none; diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 519baca57..2352cb2b4 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -314,7 +314,7 @@ padding-block-end: 0; } - .card__collection { + .card__board { padding-block: 0.25ch; } diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index d32696f11..cac32efde 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -2,11 +2,11 @@ class Cards::AssignmentsController < ApplicationController include CardScoped def new - @users = @collection.users.active.alphabetically + @users = @board.users.active.alphabetically fresh_when @users end def create - @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) + @card.toggle_assignment @board.users.active.find(params[:assignee_id]) end end diff --git a/app/controllers/cards/collections_controller.rb b/app/controllers/cards/collections_controller.rb index 791fd945e..dfbc522a4 100644 --- a/app/controllers/cards/collections_controller.rb +++ b/app/controllers/cards/collections_controller.rb @@ -1,16 +1,16 @@ -class Cards::CollectionsController < ApplicationController - include CollectionScoped +class Cards::BoardsController < ApplicationController + include BoardScoped - skip_before_action :set_collection, only: %i[ edit ] + skip_before_action :set_board, only: %i[ edit ] before_action :set_card def edit - @collections = Current.user.collections.ordered_by_recently_accessed - fresh_when @collections + @boards = Current.user.boards.ordered_by_recently_accessed + fresh_when @boards end def update - @card.move_to(@collection) + @card.move_to(@board) redirect_to @card end diff --git a/app/controllers/cards/columns_controller.rb b/app/controllers/cards/columns_controller.rb index e41225050..f250f2dd6 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.collection.columns + @columns = @card.board.columns fresh_when etag: [ @card, @columns ] end diff --git a/app/controllers/cards/publishes_controller.rb b/app/controllers/cards/publishes_controller.rb index 9aeab76ce..641f728cd 100644 --- a/app/controllers/cards/publishes_controller.rb +++ b/app/controllers/cards/publishes_controller.rb @@ -5,9 +5,9 @@ class Cards::PublishesController < ApplicationController @card.publish if add_another_param? - redirect_to @collection.cards.create!, notice: "Card added" + redirect_to @board.cards.create!, notice: "Card added" else - redirect_to @card.collection + redirect_to @card.board end end diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index d2e1bb8d1..e6a728829 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -5,11 +5,11 @@ class Cards::ReadingsController < ApplicationController def create @notifications = @card.read_by(Current.user) - record_collection_access + record_board_access end private - def record_collection_access - @card.collection.accessed_by(Current.user) + def record_board_access + @card.board.accessed_by(Current.user) end end diff --git a/app/controllers/cards/triages_controller.rb b/app/controllers/cards/triages_controller.rb index 10fb3df34..0c1a7bd1e 100644 --- a/app/controllers/cards/triages_controller.rb +++ b/app/controllers/cards/triages_controller.rb @@ -2,7 +2,7 @@ class Cards::TriagesController < ApplicationController include CardScoped def create - column = @card.collection.columns.find(params[:column_id]) + column = @card.board.columns.find(params[:column_id]) @card.triage_into(column) redirect_to @card diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 7fcd7123e..56bd94da1 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -1,7 +1,7 @@ class CardsController < ApplicationController include FilterScoped - before_action :set_collection, only: %i[ create ] + before_action :set_board, only: %i[ create ] before_action :set_card, only: %i[ show edit update destroy ] def index @@ -9,7 +9,7 @@ class CardsController < ApplicationController end def create - card = @collection.cards.find_or_create_by!(creator: Current.user, status: "drafted") + card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted") redirect_to card end @@ -30,12 +30,12 @@ class CardsController < ApplicationController def destroy @card.destroy! - redirect_to @card.collection, notice: "Card deleted" + redirect_to @card.board, notice: "Card deleted" end private - def set_collection - @collection = Current.user.collections.find params[:collection_id] + def set_board + @board = Current.user.boards.find params[:board_id] end def set_card @@ -46,7 +46,7 @@ class CardsController < ApplicationController if card.published? yield else - Collection.suppressing_turbo_broadcasts(&block) + Board.suppressing_turbo_broadcasts(&block) end end diff --git a/app/controllers/collections/columns/closeds_controller.rb b/app/controllers/collections/columns/closeds_controller.rb index b5bf35953..b9aad40b3 100644 --- a/app/controllers/collections/columns/closeds_controller.rb +++ b/app/controllers/collections/columns/closeds_controller.rb @@ -1,8 +1,8 @@ -class Collections::Columns::ClosedsController < ApplicationController - include CollectionScoped +class Boards::Columns::ClosedsController < ApplicationController + include BoardScoped def show - set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first + set_page_and_extract_portion_from @board.cards.closed.recently_closed_first fresh_when etag: @page.records end end diff --git a/app/controllers/collections/columns/not_nows_controller.rb b/app/controllers/collections/columns/not_nows_controller.rb index 4c1a6796b..a65f538c4 100644 --- a/app/controllers/collections/columns/not_nows_controller.rb +++ b/app/controllers/collections/columns/not_nows_controller.rb @@ -1,8 +1,8 @@ -class Collections::Columns::NotNowsController < ApplicationController - include CollectionScoped +class Boards::Columns::NotNowsController < ApplicationController + include BoardScoped def show - set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @board.cards.postponed.reverse_chronologically.with_golden_first fresh_when etag: @page.records end end diff --git a/app/controllers/collections/columns/streams_controller.rb b/app/controllers/collections/columns/streams_controller.rb index dccc9aa15..03b48d900 100644 --- a/app/controllers/collections/columns/streams_controller.rb +++ b/app/controllers/collections/columns/streams_controller.rb @@ -1,8 +1,8 @@ -class Collections::Columns::StreamsController < ApplicationController - include CollectionScoped +class Boards::Columns::StreamsController < ApplicationController + include BoardScoped def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first fresh_when etag: @page.records end end diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index a6c9103c5..1648da401 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -1,5 +1,5 @@ -class Collections::ColumnsController < ApplicationController - include CollectionScoped +class Boards::ColumnsController < ApplicationController + include BoardScoped before_action :set_column, only: %i[ show update destroy ] @@ -9,7 +9,7 @@ class Collections::ColumnsController < ApplicationController end def create - @column = @collection.columns.create!(column_params) + @column = @board.columns.create!(column_params) end def update @@ -22,7 +22,7 @@ class Collections::ColumnsController < ApplicationController private def set_column - @column = @collection.columns.find(params[:id]) + @column = @board.columns.find(params[:id]) end def column_params diff --git a/app/controllers/collections/entropies_controller.rb b/app/controllers/collections/entropies_controller.rb index 4affaf5af..153dafe5f 100644 --- a/app/controllers/collections/entropies_controller.rb +++ b/app/controllers/collections/entropies_controller.rb @@ -1,12 +1,12 @@ -class Collections::EntropiesController < ApplicationController - include CollectionScoped +class Boards::EntropiesController < ApplicationController + include BoardScoped def update - @collection.entropy.update!(entropy_params) + @board.entropy.update!(entropy_params) end private def entropy_params - params.expect(collection: [ :auto_postpone_period ]) + params.expect(board: [ :auto_postpone_period ]) end end diff --git a/app/controllers/collections/involvements_controller.rb b/app/controllers/collections/involvements_controller.rb index 69f7f2fbc..a904f2989 100644 --- a/app/controllers/collections/involvements_controller.rb +++ b/app/controllers/collections/involvements_controller.rb @@ -1,7 +1,7 @@ -class Collections::InvolvementsController < ApplicationController - include CollectionScoped +class Boards::InvolvementsController < ApplicationController + include BoardScoped def update - @collection.access_for(Current.user).update!(involvement: params[:involvement]) + @board.access_for(Current.user).update!(involvement: params[:involvement]) end end diff --git a/app/controllers/collections/publications_controller.rb b/app/controllers/collections/publications_controller.rb index 78400dd3c..b7cda07ee 100644 --- a/app/controllers/collections/publications_controller.rb +++ b/app/controllers/collections/publications_controller.rb @@ -1,12 +1,12 @@ -class Collections::PublicationsController < ApplicationController - include CollectionScoped +class Boards::PublicationsController < ApplicationController + include BoardScoped def create - @collection.publish + @board.publish end def destroy - @collection.unpublish - @collection.reload + @board.unpublish + @board.reload end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index b7c722eca..12448d9dd 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -1,10 +1,10 @@ -class CollectionsController < ApplicationController +class BoardsController < ApplicationController include FilterScoped - before_action :set_collection, except: %i[ new create ] + before_action :set_board, except: %i[ new create ] def show - if @filter.used?(ignore_collections: true) + if @filter.used?(ignore_boards: true) show_filtered_cards else show_columns @@ -12,53 +12,53 @@ class CollectionsController < ApplicationController end def new - @collection = Collection.new + @board = Board.new end def create - @collection = Collection.create! collection_params.with_defaults(all_access: true) - redirect_to collection_path(@collection) + @board = Board.create! board_params.with_defaults(all_access: true) + redirect_to board_path(@board) end def edit - selected_user_ids = @collection.users.pluck :id + selected_user_ids = @board.users.pluck :id @selected_users, @unselected_users = \ User.active.alphabetically.partition { |user| selected_user_ids.include? user.id } end def update - @collection.update! collection_params - @collection.accesses.revise granted: grantees, revoked: revokees if grantees_changed? + @board.update! board_params + @board.accesses.revise granted: grantees, revoked: revokees if grantees_changed? - if @collection.accessible_to?(Current.user) - redirect_to edit_collection_path(@collection), notice: "Saved" + if @board.accessible_to?(Current.user) + redirect_to edit_board_path(@board), notice: "Saved" else - redirect_to root_path, notice: "Saved (you were removed from the collection)" + redirect_to root_path, notice: "Saved (you were removed from the board)" end end def destroy - @collection.destroy + @board.destroy redirect_to root_path end private - def set_collection - @collection = Current.user.collections.find params[:id] + def set_board + @board = Current.user.boards.find params[:id] end def show_filtered_cards - @filter.collection_ids = [ @collection.id ] + @filter.board_ids = [ @board.id ] set_page_and_extract_portion_from @filter.cards end def show_columns - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first - fresh_when etag: [ @collection, @page.records, @user_filtering ] + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first + fresh_when etag: [ @board, @page.records, @user_filtering ] end - def collection_params - params.expect(collection: [ :name, :all_access, :auto_postpone_period, :public_description ]) + def board_params + params.expect(board: [ :name, :all_access, :auto_postpone_period, :public_description ]) end def grantees @@ -66,7 +66,7 @@ class CollectionsController < ApplicationController end def revokees - @collection.users.where.not id: grantee_ids + @board.users.where.not id: grantee_ids end def grantee_ids diff --git a/app/controllers/columns/cards/drops/columns_controller.rb b/app/controllers/columns/cards/drops/columns_controller.rb index 3de47992e..1d2bd8ce5 100644 --- a/app/controllers/columns/cards/drops/columns_controller.rb +++ b/app/controllers/columns/cards/drops/columns_controller.rb @@ -2,7 +2,7 @@ class Columns::Cards::Drops::ColumnsController < ApplicationController include CardScoped def create - @column = @card.collection.columns.find(params[:column_id]) + @column = @card.board.columns.find(params[:column_id]) @card.triage_into(@column) end end diff --git a/app/controllers/columns/cards/drops/streams_controller.rb b/app/controllers/columns/cards/drops/streams_controller.rb index d3ce8d1fa..fefdbb23f 100644 --- a/app/controllers/columns/cards/drops/streams_controller.rb +++ b/app/controllers/columns/cards/drops/streams_controller.rb @@ -3,6 +3,6 @@ class Columns::Cards::Drops::StreamsController < ApplicationController def create @card.send_back_to_triage - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first end end diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index 5438871c1..77496e160 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -2,7 +2,7 @@ module CardScoped extend ActiveSupport::Concern included do - before_action :set_card, :set_collection + before_action :set_card, :set_board end private @@ -10,8 +10,8 @@ module CardScoped @card = Current.user.accessible_cards.find(params[:card_id]) end - def set_collection - @collection = @card.collection + def set_board + @board = @card.board end def render_card_replacement diff --git a/app/controllers/concerns/collection_scoped.rb b/app/controllers/concerns/collection_scoped.rb index 2ddde9531..2772c7d01 100644 --- a/app/controllers/concerns/collection_scoped.rb +++ b/app/controllers/concerns/collection_scoped.rb @@ -1,12 +1,12 @@ -module CollectionScoped +module BoardScoped extend ActiveSupport::Concern included do - before_action :set_collection + before_action :set_board end private - def set_collection - @collection = Current.user.collections.find(params[:collection_id]) + def set_board + @board = Current.user.boards.find(params[:board_id]) end end diff --git a/app/controllers/landings_controller.rb b/app/controllers/landings_controller.rb index 5c2709a1f..f7f7558c4 100644 --- a/app/controllers/landings_controller.rb +++ b/app/controllers/landings_controller.rb @@ -1,7 +1,7 @@ class LandingsController < ApplicationController def show - if Current.user.collections.one? - redirect_to collection_path(Current.user.collections.first) + if Current.user.boards.one? + redirect_to board_path(Current.user.boards.first) else redirect_to events_path end diff --git a/app/controllers/notifications/settings_controller.rb b/app/controllers/notifications/settings_controller.rb index 42608b558..c4aab7f69 100644 --- a/app/controllers/notifications/settings_controller.rb +++ b/app/controllers/notifications/settings_controller.rb @@ -2,7 +2,7 @@ class Notifications::SettingsController < ApplicationController before_action :set_settings def show - @collections = Current.user.collections.alphabetically + @boards = Current.user.boards.alphabetically end def update diff --git a/app/controllers/prompts/collections/users_controller.rb b/app/controllers/prompts/collections/users_controller.rb index 19edbbd5d..e5f8775a6 100644 --- a/app/controllers/prompts/collections/users_controller.rb +++ b/app/controllers/prompts/collections/users_controller.rb @@ -1,8 +1,8 @@ -class Prompts::Collections::UsersController < ApplicationController - include CollectionScoped +class Prompts::Boards::UsersController < ApplicationController + include BoardScoped def index - @users = @collection.users.alphabetically + @users = @board.users.alphabetically if stale? etag: @users render layout: false diff --git a/app/controllers/public/base_controller.rb b/app/controllers/public/base_controller.rb index 7fa2e69fb..28a1bf463 100644 --- a/app/controllers/public/base_controller.rb +++ b/app/controllers/public/base_controller.rb @@ -1,17 +1,17 @@ class Public::BaseController < ApplicationController allow_unauthenticated_access - before_action :set_collection, :set_card, :set_public_cache_expiration + before_action :set_board, :set_card, :set_public_cache_expiration layout "public" private - def set_collection - @collection = Collection.find_by_published_key(params[:collection_id] || params[:id]) + def set_board + @board = Board.find_by_published_key(params[:board_id] || params[:id]) end def set_card - @card = @collection.cards.find(params[:id]) if params[:collection_id] && params[:id] + @card = @board.cards.find(params[:id]) if params[:board_id] && params[:id] end def set_public_cache_expiration diff --git a/app/controllers/public/collections/columns/closeds_controller.rb b/app/controllers/public/collections/columns/closeds_controller.rb index f782fbc8f..63ca9de7b 100644 --- a/app/controllers/public/collections/columns/closeds_controller.rb +++ b/app/controllers/public/collections/columns/closeds_controller.rb @@ -1,5 +1,5 @@ -class Public::Collections::Columns::ClosedsController < Public::BaseController +class Public::Boards::Columns::ClosedsController < Public::BaseController def show - set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first + set_page_and_extract_portion_from @board.cards.closed.recently_closed_first end end diff --git a/app/controllers/public/collections/columns/not_nows_controller.rb b/app/controllers/public/collections/columns/not_nows_controller.rb index 6ee68e3e7..d518fb8e0 100644 --- a/app/controllers/public/collections/columns/not_nows_controller.rb +++ b/app/controllers/public/collections/columns/not_nows_controller.rb @@ -1,5 +1,5 @@ -class Public::Collections::Columns::NotNowsController < Public::BaseController +class Public::Boards::Columns::NotNowsController < Public::BaseController def show - set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @board.cards.postponed.reverse_chronologically.with_golden_first end end diff --git a/app/controllers/public/collections/columns/streams_controller.rb b/app/controllers/public/collections/columns/streams_controller.rb index 602b3d91d..778817b3e 100644 --- a/app/controllers/public/collections/columns/streams_controller.rb +++ b/app/controllers/public/collections/columns/streams_controller.rb @@ -1,5 +1,5 @@ -class Public::Collections::Columns::StreamsController < Public::BaseController +class Public::Boards::Columns::StreamsController < Public::BaseController def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first end end diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb index 44262c545..5a5c23d5e 100644 --- a/app/controllers/public/collections/columns_controller.rb +++ b/app/controllers/public/collections/columns_controller.rb @@ -1,4 +1,4 @@ -class Public::Collections::ColumnsController < Public::BaseController +class Public::Boards::ColumnsController < Public::BaseController before_action :set_column def show @@ -11,6 +11,6 @@ class Public::Collections::ColumnsController < Public::BaseController end def set_column - @column = @collection.columns.find(params[:id]) + @column = @board.columns.find(params[:id]) end end diff --git a/app/controllers/public/collections_controller.rb b/app/controllers/public/collections_controller.rb index d7e4977af..d56a1f684 100644 --- a/app/controllers/public/collections_controller.rb +++ b/app/controllers/public/collections_controller.rb @@ -1,5 +1,5 @@ -class Public::CollectionsController < Public::BaseController +class Public::BoardsController < Public::BaseController def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first + set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first end end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index b9ad06187..7d79c4587 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -1,22 +1,22 @@ class WebhooksController < ApplicationController - include CollectionScoped + include BoardScoped before_action :ensure_admin before_action :set_webhook, except: %i[ index new create ] def index - set_page_and_extract_portion_from @collection.webhooks.ordered + set_page_and_extract_portion_from @board.webhooks.ordered end def show end def new - @webhook = @collection.webhooks.new + @webhook = @board.webhooks.new end def create - webhook = @collection.webhooks.create!(webhook_params) + webhook = @board.webhooks.create!(webhook_params) redirect_to webhook end @@ -30,17 +30,17 @@ class WebhooksController < ApplicationController def destroy @webhook.destroy! - redirect_to collection_webhooks_path + redirect_to board_webhooks_path end private def set_webhook - @webhook = @collection.webhooks.find(params[:id]) + @webhook = @board.webhooks.find(params[:id]) end def webhook_params params .expect(webhook: [ :name, :url, subscribed_actions: [] ]) - .merge(collection_id: @collection.id) + .merge(board_id: @board.id) end end diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index ec98acc22..372ee7475 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -1,8 +1,8 @@ module AccessesHelper MAX_DISPLAYED_WATCHERS = 8 - def access_menu_tag(collection, **options, &) - tag.menu class: [ options[:class], { "toggler--toggled": collection.all_access? } ], data: { + def access_menu_tag(board, **options, &) + tag.menu class: [ options[:class], { "toggler--toggled": board.all_access? } ], data: { controller: "filter toggle-class navigable-list", action: "keydown->navigable-list#navigate filter:changed->navigable-list#reset", navigable_list_focus_on_selection_value: true, @@ -11,35 +11,35 @@ module AccessesHelper end def access_toggles_for(users, selected:) - render partial: "collections/access_toggle", - collection: users, as: :user, + render partial: "boards/access_toggle", + board: users, as: :user, locals: { selected: selected }, cached: ->(user) { [ user, selected ] } end - def access_involvement_advance_button(collection, user, show_watchers: true, icon_only: false) - access = collection.access_for(user) + def access_involvement_advance_button(board, user, show_watchers: true, icon_only: false) + access = board.access_for(user) - turbo_frame_tag dom_id(collection, :involvement_button) do - concat collection_watchers_list(collection) if show_watchers - concat involvement_button(collection, access, show_watchers, icon_only) + turbo_frame_tag dom_id(board, :involvement_button) do + concat board_watchers_list(board) if show_watchers + concat involvement_button(board, access, show_watchers, icon_only) end end - def collection_watchers_list(collection) - watchers = collection.watchers + def board_watchers_list(board) + watchers = board.watchers displayed_watchers = watchers.limit(MAX_DISPLAYED_WATCHERS) overflow_count = watchers.count - MAX_DISPLAYED_WATCHERS safe_join([ tag.strong(displayed_watchers.any? ? "Watching for new cards" : "No one is watching for new cards", class: "txt-uppercase"), - tag.div(class: "collection-tools__watching") do + tag.div(class: "board-tools__watching") do safe_join([ safe_join(displayed_watchers.map { |watcher| avatar_tag(watcher) }), (tag.div(data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" }) do concat tag.button("+#{overflow_count}", class: "overflow-count btn btn--circle borderless", data: { action: "dialog#open" }, aria: { label: "Show #{overflow_count} more watchers" }) - concat(tag.dialog(class: "collection-tools__watching-dialog dialog panel", data: { dialog_target: "dialog" }, aria: { hidden: "true" }) do + concat(tag.dialog(class: "board-tools__watching-dialog dialog panel", data: { dialog_target: "dialog" }, aria: { hidden: "true" }) do safe_join(watchers.map { |watcher| avatar_tag(watcher) }) end) end if overflow_count > 0) @@ -48,11 +48,11 @@ module AccessesHelper ]) end - def involvement_button(collection, access, show_watchers, icon_only) - involvement_label_id = dom_id(collection, :involvement_label) + def involvement_button(board, access, show_watchers, icon_only) + involvement_label_id = dom_id(board, :involvement_label) button_to( - collection_involvement_path(collection), + board_involvement_path(board), method: :put, aria: { labelledby: involvement_label_id }, class: class_names("btn", { "btn--reversed": access.watching? && icon_only }), diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 2b67f8750..56df2f54d 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -27,14 +27,14 @@ module CardsHelper title = [ card.title, "added by #{card.creator.name}", - "in #{card.collection.name}" + "in #{card.board.name}" ] title << "assigned to #{card.assignees.map(&:name).to_sentence}" if card.assignees.any? title.join(" ") end def card_social_tags(card) - tag.meta(property: "og:title", content: "#{card.title} | #{card.collection.name}") + + tag.meta(property: "og:title", content: "#{card.title} | #{card.board.name}") + tag.meta(property: "og:description", content: format_excerpt(@card&.description, length: 200)) + tag.meta(property: "og:image", content: @card.image.attached? ? "#{request.base_url}#{url_for(@card.image)}" : "#{request.base_url}/app-icon.png") + tag.meta(property: "og:url", content: card_url(@card)) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index d10b00a4c..ae8739d05 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,14 +1,14 @@ -module CollectionsHelper - def link_back_to_collection(collection) - link_to collection, class: "btn borderless txt-medium", +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(collection.name, class: "overflow-ellipsis")).html_safe + tag.span ("←" + tag.strong(board.name, class: "overflow-ellipsis")).html_safe end end - def link_to_edit_collection(collection) - link_to edit_collection_path(collection), class: "btn", data: { controller: "tooltip" } do - icon_tag("settings") + tag.span("Settings for #{collection.name}", class: "for-screen-reader") + def link_to_edit_board(board) + link_to edit_board_path(board), class: "btn", data: { controller: "tooltip" } do + icon_tag("settings") + tag.span("Settings for #{board.name}", class: "for-screen-reader") end end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 7c55f857e..3156dd25d 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -102,8 +102,8 @@ module EventsHelper "#{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_collection_changed" - "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'new_collection')}”" + when "card_board_changed" + "#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'new_board')}”" 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" @@ -129,7 +129,7 @@ module EventsHelper "comment" when "card_title_changed" "rename" - when "card_collection_changed" + when "card_board_changed" "move" else "person" diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index cfb634223..49f175067 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -11,8 +11,8 @@ module FiltersHelper hidden_field_tag name, value, id: nil end - def filter_selected_collections_title(user_filtering) - user_filtering.selected_collection_titles.collect { tag.strong it }.to_sentence.html_safe + def filter_selected_boards_title(user_filtering) + user_filtering.selected_board_titles.collect { tag.strong it }.to_sentence.html_safe end def filter_place_menu_item(path, label, icon, new_window: false, current: false) diff --git a/app/helpers/my/menu_helper.rb b/app/helpers/my/menu_helper.rb index a972a3408..2962a49ac 100644 --- a/app/helpers/my/menu_helper.rb +++ b/app/helpers/my/menu_helper.rb @@ -3,7 +3,7 @@ module My::MenuHelper text_field_tag :search, nil, type: "search", role: "combobox", - placeholder: "Type to jump to a collection, person, place, or tag…", + placeholder: "Type to jump to a board, person, place, or tag…", class: "input input--transparent txt-small", autofocus: true, autocorrect: "off", diff --git a/app/helpers/rich_text_helper.rb b/app/helpers/rich_text_helper.rb index 752af1075..c1bc2232a 100644 --- a/app/helpers/rich_text_helper.rb +++ b/app/helpers/rich_text_helper.rb @@ -1,6 +1,6 @@ module RichTextHelper - def mentions_prompt(collection) - content_tag "lexxy-prompt", "", trigger: "@", src: prompts_collection_users_path(collection), name: "mention" + def mentions_prompt(board) + content_tag "lexxy-prompt", "", trigger: "@", src: prompts_board_users_path(board), name: "mention" end def global_mentions_prompt @@ -19,7 +19,7 @@ module RichTextHelper content_tag "lexxy-code-language-picker" end - def general_prompts(collection) - safe_join([ mentions_prompt(collection), cards_prompt, code_language_picker ]) + def general_prompts(board) + safe_join([ mentions_prompt(board), cards_prompt, code_language_picker ]) end end diff --git a/app/helpers/webhooks_helper.rb b/app/helpers/webhooks_helper.rb index c809ac6ef..e0e0e3387 100644 --- a/app/helpers/webhooks_helper.rb +++ b/app/helpers/webhooks_helper.rb @@ -2,7 +2,7 @@ module WebhooksHelper ACTION_LABELS = { card_published: "Card added", card_title_changed: "Card title changed", - card_collection_changed: "Card collection changed", + card_board_changed: "Card board changed", comment_created: "Comment added", card_assigned: "Card assigned", card_unassigned: "Card unassigned", @@ -22,9 +22,9 @@ module WebhooksHelper ACTION_LABELS[action] || action.to_s.humanize end - def link_to_webhooks(collection, &) - link_to collection_webhooks_path(collection_id: collection), - class: [ "btn", { "btn--reversed": collection.webhooks.any? } ], + def link_to_webhooks(board, &) + link_to board_webhooks_path(board_id: board), + class: [ "btn", { "btn--reversed": board.webhooks.any? } ], data: { controller: "tooltip" } do icon_tag("world") + tag.span("Webhooks", class: "for-screen-reader") end diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 0a87e47d5..74dd1ff65 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { static classes = [ "collapsed", "noTransitions" ] static targets = [ "column", "button" ] static values = { - collection: String + board: String } initialize() { @@ -104,6 +104,6 @@ export default class extends Controller { } #localStorageKeyFor(column) { - return `expand-${this.collectionValue}-${column.getAttribute("id")}` + return `expand-${this.boardValue}-${column.getAttribute("id")}` } } diff --git a/app/javascript/controllers/toggle_class_controller.js b/app/javascript/controllers/toggle_class_controller.js index 357525f67..04969aee2 100644 --- a/app/javascript/controllers/toggle_class_controller.js +++ b/app/javascript/controllers/toggle_class_controller.js @@ -24,7 +24,7 @@ export default class extends Controller { checkNone() { this.checkboxTargets.forEach(checkbox => { - if (checkbox.dataset.collectionsFormTarget === "meCheckbox") return + if (checkbox.dataset.boardsFormTarget === "meCheckbox") return checkbox.checked = false }) } diff --git a/app/jobs/collection/clean_inaccessible_data_job.rb b/app/jobs/collection/clean_inaccessible_data_job.rb index 16ff1b90f..b2cdff085 100644 --- a/app/jobs/collection/clean_inaccessible_data_job.rb +++ b/app/jobs/collection/clean_inaccessible_data_job.rb @@ -1,5 +1,5 @@ -class Collection::CleanInaccessibleDataJob < ApplicationJob - def perform(user, collection) - collection.clean_inaccessible_data_for(user) +class Board::CleanInaccessibleDataJob < ApplicationJob + def perform(user, board) + board.clean_inaccessible_data_for(user) end end diff --git a/app/models/access.rb b/app/models/access.rb index fe4efe590..27026fb87 100644 --- a/app/models/access.rb +++ b/app/models/access.rb @@ -1,5 +1,5 @@ class Access < ApplicationRecord - belongs_to :collection, touch: true + belongs_to :board, touch: true belongs_to :user, touch: true enum :involvement, %i[ access_only watching ].index_by(&:itself), default: :access_only @@ -18,6 +18,6 @@ class Access < ApplicationRecord end def clean_inaccessible_data_later - Collection::CleanInaccessibleDataJob.perform_later(user, collection) + Board::CleanInaccessibleDataJob.perform_later(user, board) end end diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 624f4abd1..991877833 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -26,9 +26,9 @@ class Account::Seeder def populate # --------------- - # Playground Collection + # Playground Board # --------------- - playground = Collection.create! name: "Playground", creator: creator, all_access: true + 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 @@ -105,7 +105,7 @@ class Account::Seeder def delete_everything Current.set session: session do - Collection.destroy_all + Board.destroy_all end end end diff --git a/app/models/card.rb b/app/models/card.rb index 7ac40ca09..65c7cab18 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -3,7 +3,7 @@ class Card < ApplicationRecord Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable - belongs_to :collection, touch: true + belongs_to :board, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } has_many :comments, dependent: :destroy @@ -12,7 +12,7 @@ class Card < ApplicationRecord has_rich_text :description before_save :set_default_title, if: :published? - after_update :handle_collection_change, if: :saved_change_to_collection_id? + after_update :handle_board_change, if: :saved_change_to_board_id? scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } @@ -39,16 +39,16 @@ class Card < ApplicationRecord end end - delegate :accessible_to?, to: :collection + delegate :accessible_to?, to: :board def card self end - def move_to(new_collection) + def move_to(new_board) transaction do - card.update!(collection: new_collection) - card.events.update_all(collection_id: new_collection.id) + card.update!(board: new_board) + card.events.update_all(board_id: new_board.id) end end @@ -61,23 +61,23 @@ class Card < ApplicationRecord self.title = "Untitled" if title.blank? end - def handle_collection_change - old_collection = Collection.find_by(id: collection_id_before_last_save) + def handle_board_change + old_board = Board.find_by(id: board_id_before_last_save) transaction do update! column: nil - track_collection_change_event(old_collection.name) - grant_access_to_assignees unless collection.all_access? + track_board_change_event(old_board.name) + grant_access_to_assignees unless board.all_access? end remove_inaccessible_notifications_later end - def track_collection_change_event(old_collection_name) - track_event "collection_changed", particulars: { old_collection: old_collection_name, new_collection: collection.name } + def track_board_change_event(old_board_name) + track_event "board_changed", particulars: { old_board: old_board_name, new_board: board.name } end def grant_access_to_assignees - collection.accesses.grant_to(assignees) + board.accesses.grant_to(assignees) end end diff --git a/app/models/card/entropic.rb b/app/models/card/entropic.rb index 68c0c6aa1..478d4bc52 100644 --- a/app/models/card/entropic.rb +++ b/app/models/card/entropic.rb @@ -4,19 +4,19 @@ module Card::Entropic included do scope :due_to_be_postponed, -> do active - .left_outer_joins(collection: :entropy) + .left_outer_joins(board: :entropy) .where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period) end scope :postponing_soon, -> do active - .left_outer_joins(collection: :entropy) + .left_outer_joins(board: :entropy) .where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period) .where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Account.sole.entropy.auto_postpone_period) end - delegate :auto_postpone_period, to: :collection + delegate :auto_postpone_period, to: :board end class_methods do diff --git a/app/models/card/eventable/system_commenter.rb b/app/models/card/eventable/system_commenter.rb index e74475c79..ace5701bf 100644 --- a/app/models/card/eventable/system_commenter.rb +++ b/app/models/card/eventable/system_commenter.rb @@ -28,8 +28,8 @@ class Card::Eventable::SystemCommenter "Closed as “Not Now” due to inactivity" when "card_title_changed" "#{event.creator.name} changed the title from “#{event.particulars.dig('particulars', 'old_title')}” to “#{event.particulars.dig('particulars', 'new_title')}”." - when "card_collection_changed" - "#{event.creator.name} moved this from “#{event.particulars.dig('particulars', 'old_collection')}” to “#{event.particulars.dig('particulars', 'new_collection')}”." + when "card_board_changed" + "#{event.creator.name} moved this from “#{event.particulars.dig('particulars', 'old_board')}” to “#{event.particulars.dig('particulars', 'new_board')}”." when "card_triaged" "#{event.creator.name} moved this to “#{event.particulars.dig('particulars', 'column')}”" when "card_sent_back_to_triage" diff --git a/app/models/card/promptable.rb b/app/models/card/promptable.rb index efbcd64db..e30816023 100644 --- a/app/models/card/promptable.rb +++ b/app/models/card/promptable.rb @@ -21,8 +21,8 @@ module Card::Promptable * Assigned to: #{assignees.map(&:name).join(", ")} * Column: #{column_prompt_label} * Created at: #{created_at}} - * Collection id: #{collection_id} - * Collection name: #{collection.name} + * Board id: #{board_id} + * Board name: #{board.name} * Number of comments: #{comments.count} * Path: #{card_path(self, script_name: Account.sole.slug)} diff --git a/app/models/card/triageable.rb b/app/models/card/triageable.rb index c324c709b..d15663902 100644 --- a/app/models/card/triageable.rb +++ b/app/models/card/triageable.rb @@ -17,7 +17,7 @@ module Card::Triageable end def triage_into(column) - raise "The column must belong to the card collection" unless collection == column.collection + raise "The column must belong to the card board" unless board == column.board transaction do resume diff --git a/app/models/collection.rb b/app/models/collection.rb index ae09d9471..ca4398325 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -1,4 +1,4 @@ -class Collection < ApplicationRecord +class Board < ApplicationRecord include AutoClosing, Accessible, Broadcastable, Cards, Entropic, Filterable, Publishable, Triageable belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/collection/accessible.rb b/app/models/collection/accessible.rb index 2d6aee476..6d62c0b21 100644 --- a/app/models/collection/accessible.rb +++ b/app/models/collection/accessible.rb @@ -1,4 +1,4 @@ -module Collection::Accessible +module Board::Accessible extend ActiveSupport::Concern included do @@ -11,7 +11,7 @@ module Collection::Accessible end def grant_to(users) - Access.insert_all Array(users).collect { |user| { collection_id: proxy_association.owner.id, user_id: user.id } } + Access.insert_all Array(users).collect { |user| { board_id: proxy_association.owner.id, user_id: user.id } } end def revoke_from(users) @@ -69,7 +69,7 @@ module Collection::Accessible .joins("LEFT JOIN cards ON mentions.source_id = cards.id AND mentions.source_type = 'Card'") .joins("LEFT JOIN comments ON mentions.source_id = comments.id AND mentions.source_type = 'Comment'") .joins("LEFT JOIN cards AS comment_cards ON comments.card_id = comment_cards.id") - .where("(mentions.source_type = 'Card' AND cards.collection_id = ?) OR (mentions.source_type = 'Comment' AND comment_cards.collection_id = ?)", id, id) + .where("(mentions.source_type = 'Card' AND cards.board_id = ?) OR (mentions.source_type = 'Comment' AND comment_cards.board_id = ?)", id, id) end def notifications_for_user(user) @@ -85,8 +85,8 @@ module Collection::Accessible .joins("LEFT JOIN cards AS event_cards ON events.eventable_id = event_cards.id AND events.eventable_type = 'Card'") .joins("LEFT JOIN comments AS event_comments ON events.eventable_id = event_comments.id AND events.eventable_type = 'Comment'") .joins("LEFT JOIN cards AS event_comment_cards ON event_comments.card_id = event_comment_cards.id") - .where("(notifications.source_type = 'Event' AND events.eventable_type = 'Card' AND event_cards.collection_id = ?) OR - (notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.collection_id = ?)", + .where("(notifications.source_type = 'Event' AND events.eventable_type = 'Card' AND event_cards.board_id = ?) OR + (notifications.source_type = 'Event' AND events.eventable_type = 'Comment' AND event_comment_cards.board_id = ?)", id, id) end end diff --git a/app/models/collection/auto_closing.rb b/app/models/collection/auto_closing.rb index 98cf21385..27da3bf48 100644 --- a/app/models/collection/auto_closing.rb +++ b/app/models/collection/auto_closing.rb @@ -1,4 +1,4 @@ -module Collection::AutoClosing +module Board::AutoClosing extend ActiveSupport::Concern included do diff --git a/app/models/collection/broadcastable.rb b/app/models/collection/broadcastable.rb index e44adc0ac..177509800 100644 --- a/app/models/collection/broadcastable.rb +++ b/app/models/collection/broadcastable.rb @@ -1,8 +1,8 @@ -module Collection::Broadcastable +module Board::Broadcastable extend ActiveSupport::Concern included do broadcasts_refreshes - broadcasts_refreshes_to ->(_) { :all_collections } + broadcasts_refreshes_to ->(_) { :all_boards } end end diff --git a/app/models/collection/cards.rb b/app/models/collection/cards.rb index 105f4912b..096400957 100644 --- a/app/models/collection/cards.rb +++ b/app/models/collection/cards.rb @@ -1,4 +1,4 @@ -module Collection::Cards +module Board::Cards extend ActiveSupport::Concern included do diff --git a/app/models/collection/entropic.rb b/app/models/collection/entropic.rb index a84e7eb62..b29d5ea42 100644 --- a/app/models/collection/entropic.rb +++ b/app/models/collection/entropic.rb @@ -1,4 +1,4 @@ -module Collection::Entropic +module Board::Entropic extend ActiveSupport::Concern included do diff --git a/app/models/collection/publication.rb b/app/models/collection/publication.rb index fbbc68741..a21fb00b6 100644 --- a/app/models/collection/publication.rb +++ b/app/models/collection/publication.rb @@ -1,5 +1,5 @@ -class Collection::Publication < ApplicationRecord - belongs_to :collection +class Board::Publication < ApplicationRecord + belongs_to :board has_secure_token :key end diff --git a/app/models/collection/publishable.rb b/app/models/collection/publishable.rb index 25b7f3fc7..6c1ce47d4 100644 --- a/app/models/collection/publishable.rb +++ b/app/models/collection/publishable.rb @@ -1,14 +1,14 @@ -module Collection::Publishable +module Board::Publishable extend ActiveSupport::Concern included do - has_one :publication, class_name: "Collection::Publication", dependent: :destroy + has_one :publication, class_name: "Board::Publication", dependent: :destroy scope :published, -> { joins(:publication) } end class_methods do def find_by_published_key(key) - Collection::Publication.find_by!(key: key).collection + Board::Publication.find_by!(key: key).board end end diff --git a/app/models/collection/triageable.rb b/app/models/collection/triageable.rb index a314868c8..3e922b4a2 100644 --- a/app/models/collection/triageable.rb +++ b/app/models/collection/triageable.rb @@ -1,4 +1,4 @@ -module Collection::Triageable +module Board::Triageable extend ActiveSupport::Concern included do diff --git a/app/models/column.rb b/app/models/column.rb index 87772367f..ec5fd6217 100644 --- a/app/models/column.rb +++ b/app/models/column.rb @@ -1,10 +1,10 @@ class Column < ApplicationRecord include Positioned - belongs_to :collection, touch: true + belongs_to :board, touch: true has_many :cards, dependent: :nullify before_validation -> { self.color ||= Card::DEFAULT_COLOR } after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? } - after_destroy_commit -> { collection.cards.touch_all } + after_destroy_commit -> { board.cards.touch_all } end diff --git a/app/models/column/positioned.rb b/app/models/column/positioned.rb index 333882e36..8518172de 100644 --- a/app/models/column/positioned.rb +++ b/app/models/column/positioned.rb @@ -16,11 +16,11 @@ module Column::Positioned end def left_column - collection.columns.where("position < ?", position).sorted.last + board.columns.where("position < ?", position).sorted.last end def right_column - collection.columns.where("position > ?", position).sorted.first + board.columns.where("position > ?", position).sorted.first end def leftmost? @@ -33,7 +33,7 @@ module Column::Positioned private def set_position - max_position = collection.columns.maximum(:position) || 0 + max_position = board.columns.maximum(:position) || 0 self.position = max_position + 1 end diff --git a/app/models/comment.rb b/app/models/comment.rb index 703c3e6a9..4428c8e48 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -14,7 +14,7 @@ class Comment < ApplicationRecord after_create_commit :watch_card_by_creator - delegate :collection, :watch_by, to: :card + delegate :board, :watch_by, to: :card def to_partial_path "cards/#{super}" diff --git a/app/models/comment/eventable.rb b/app/models/comment/eventable.rb index 0b30c0504..7fe7abebb 100644 --- a/app/models/comment/eventable.rb +++ b/app/models/comment/eventable.rb @@ -17,6 +17,6 @@ module Comment::Eventable end def track_creation - track_event("created", collection: card.collection, creator: creator) + track_event("created", board: card.board, creator: creator) end end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb index fa50de543..055c114fc 100644 --- a/app/models/concerns/mentions.rb +++ b/app/models/concerns/mentions.rb @@ -41,7 +41,7 @@ module Mentions end def mentionable_users - collection.users + board.users end def rich_text_associations diff --git a/app/models/event.rb b/app/models/event.rb index c45702833..ad77d3492 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,7 +1,7 @@ class Event < ApplicationRecord include Notifiable, Particulars, Promptable - belongs_to :collection + belongs_to :board belongs_to :creator, class_name: "User" belongs_to :eventable, polymorphic: true diff --git a/app/models/eventable.rb b/app/models/eventable.rb index e14803f09..c5e58cc08 100644 --- a/app/models/eventable.rb +++ b/app/models/eventable.rb @@ -5,9 +5,9 @@ module Eventable has_many :events, as: :eventable, dependent: :destroy end - def track_event(action, creator: Current.user, collection: self.collection, **particulars) + def track_event(action, creator: Current.user, board: self.board, **particulars) if should_track_event? - collection.events.create!(action: "#{eventable_prefix}_#{action}", creator:, collection:, eventable: self, particulars:) + board.events.create!(action: "#{eventable_prefix}_#{action}", creator:, board:, eventable: self, particulars:) end end diff --git a/app/models/filter.rb b/app/models/filter.rb index 903007c9f..459d18621 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -27,7 +27,7 @@ class Filter < ApplicationRecord result = result.unassigned if assignment_status.unassigned? result = result.assigned_to(assignees.ids) if assignees.present? result = result.where(creator_id: creators.ids) if creators.present? - result = result.where(collection: collections.ids) if collections.present? + result = result.where(board: boards.ids) if boards.present? result = result.tagged_with(tags.ids) if tags.present? result = result.where("cards.created_at": creation_window) if creation_window result = result.closed_at_window(closure_window) if closure_window @@ -44,16 +44,16 @@ class Filter < ApplicationRecord self.class.normalize_params(as_params).blank? end - def single_collection - collections.first if collections.one? + def single_board + boards.first if boards.one? end def single_workflow - collections.first.workflow if collections.pluck(:workflow_id).uniq.one? + boards.first.workflow if boards.pluck(:workflow_id).uniq.one? end def cacheable? - collections.exists? + boards.exists? end def cache_key diff --git a/app/models/filter/params.rb b/app/models/filter/params.rb index 166436e4b..586a75885 100644 --- a/app/models/filter/params.rb +++ b/app/models/filter/params.rb @@ -11,7 +11,7 @@ module Filter::Params assignee_ids: [], creator_ids: [], closer_ids: [], - collection_ids: [], + board_ids: [], tag_ids: [], terms: [] ] @@ -40,9 +40,9 @@ module Filter::Params before_save { self.params_digest = self.class.digest_params(as_params) } end - def used?(ignore_collections: false) + def used?(ignore_boards: false) tags.any? || assignees.any? || creators.any? || closers.any? || - terms.any? || card_ids&.any? || (!ignore_collections && collections.present?) || + terms.any? || card_ids&.any? || (!ignore_boards && boards.present?) || assignment_status.unassigned? || !indexed_by.all? || !sorted_by.latest? end @@ -57,7 +57,7 @@ module Filter::Params params[:assignment_status] = assignment_status params[:terms] = terms params[:tag_ids] = tags.ids - params[:collection_ids] = collections.ids + params[:board_ids] = boards.ids params[:card_ids] = card_ids params[:assignee_ids] = assignees.ids params[:creator_ids] = creators.ids diff --git a/app/models/filter/resources.rb b/app/models/filter/resources.rb index 3dc777b2f..f7045f38e 100644 --- a/app/models/filter/resources.rb +++ b/app/models/filter/resources.rb @@ -3,7 +3,7 @@ module Filter::Resources included do has_and_belongs_to_many :tags - has_and_belongs_to_many :collections + has_and_belongs_to_many :boards has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id" has_and_belongs_to_many :creators, class_name: "User", join_table: "creators_filters", association_foreign_key: "creator_id" has_and_belongs_to_many :closers, class_name: "User", join_table: "closers_filters", association_foreign_key: "closer_id" @@ -17,19 +17,19 @@ module Filter::Resources destroy! end - def collections - creator.collections.where id: super.ids + def boards + creator.boards.where id: super.ids end - def collection_titles - if collections.none? - Collection.one? ? [ Collection.first.name ] : [ "all boards" ] + def board_titles + if boards.none? + Board.one? ? [ Board.first.name ] : [ "all boards" ] else - collections.map(&:name) + boards.map(&:name) end end - def collections_label - collection_titles.to_sentence + def boards_label + board_titles.to_sentence end end diff --git a/app/models/notifier/card_event_notifier.rb b/app/models/notifier/card_event_notifier.rb index f6801d7ec..570989566 100644 --- a/app/models/notifier/card_event_notifier.rb +++ b/app/models/notifier/card_event_notifier.rb @@ -1,6 +1,6 @@ class Notifier::CardEventNotifier < Notifier delegate :creator, to: :source - delegate :collection, to: :card + delegate :board, to: :card private def recipients @@ -8,11 +8,11 @@ class Notifier::CardEventNotifier < Notifier when "card_assigned" source.assignees.excluding(creator) when "card_published" - collection.watchers.without(creator, *card.mentionees) + board.watchers.without(creator, *card.mentionees) when "comment_created" card.watchers.without(creator, *source.eventable.mentionees) else - collection.watchers.without(creator) + board.watchers.without(creator) end end diff --git a/app/models/search.rb b/app/models/search.rb index a51e757f6..518462094 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -21,7 +21,7 @@ class Search "highlight(cards_search_index, 0, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}') AS card_title", "snippet(cards_search_index, 1, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}', '...', 20) AS card_description", "null as comment_body", - "collections.name as collection_name", + "boards.name as board_name", "cards.creator_id", "cards.created_at", "bm25(cards_search_index, 10.0, 2.0) AS score" @@ -34,7 +34,7 @@ class Search "cards.title AS card_title", "null AS card_description", "snippet(comments_search_index, 0, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}', '...', 20) AS comment_body", - "collections.name as collection_name", + "boards.name as board_name", "comments.creator_id", "comments.created_at", "bm25(comments_search_index, 1.0) AS score" diff --git a/app/models/user/accessor.rb b/app/models/user/accessor.rb index c538c87a4..a7826b0b4 100644 --- a/app/models/user/accessor.rb +++ b/app/models/user/accessor.rb @@ -3,15 +3,15 @@ module User::Accessor included do has_many :accesses, dependent: :destroy - has_many :collections, through: :accesses - has_many :accessible_cards, through: :collections, source: :cards + has_many :boards, through: :accesses + has_many :accessible_cards, through: :boards, source: :cards has_many :accessible_comments, through: :accessible_cards, source: :comments - after_create_commit :grant_access_to_collections, unless: :system? + after_create_commit :grant_access_to_boards, unless: :system? end private - def grant_access_to_collections - Access.insert_all Collection.all_access.pluck(:id).collect { |collection_id| { collection_id: collection_id, user_id: id } } + def grant_access_to_boards + Access.insert_all Board.all_access.pluck(:id).collect { |board_id| { board_id: board_id, user_id: id } } end end diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb index ec3d53760..98186f18c 100644 --- a/app/models/user/day_timeline.rb +++ b/app/models/user/day_timeline.rb @@ -40,7 +40,7 @@ class User::DayTimeline card_published card_closed card_reopened - card_collection_changed + card_board_changed card_postponed card_triaged card_sent_back_to_triage @@ -57,12 +57,12 @@ class User::DayTimeline def timelineable_events Event - .where(collection: collections) + .where(board: boards) .where(action: TIMELINEABLE_ACTIONS) end - def collections - filter.collections.presence || user.collections + def boards + filter.boards.presence || user.boards end def latest_event_before diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index fdc7f2bcb..4ac9cf86a 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -1,23 +1,23 @@ class User::Filtering attr_reader :user, :filter, :expanded - delegate :as_params, :single_collection, to: :filter + delegate :as_params, :single_board, to: :filter delegate :only_closed?, to: :filter def initialize(user, filter, expanded: false) @user, @filter, @expanded = user, filter, expanded end - def collections - @collections ||= user.collections.ordered_by_recently_accessed + def boards + @boards ||= user.boards.ordered_by_recently_accessed end - def selected_collection_titles - filter.collection_titles + def selected_board_titles + filter.board_titles end - def selected_collections_label - filter.collections_label + def selected_boards_label + filter.boards_label end def tags @@ -37,7 +37,7 @@ class User::Filtering end def any? - filter.used?(ignore_collections: true) + filter.used?(ignore_boards: true) end def show_indexed_by? @@ -65,16 +65,16 @@ class User::Filtering filter.closers.any? end - def show_collections? - filter.collections.any? + def show_boards? + filter.boards.any? end - def single_collection_or_first + def single_board_or_first # Default to the first selected or, when no selection, to the first one - filter.collections.first || collections.first + filter.boards.first || boards.first end def cache_key - ActiveSupport::Cache.expand_cache_key([ user, filter, expanded?, collections, tags, users, filters ], "user-filtering") + ActiveSupport::Cache.expand_cache_key([ user, filter, expanded?, boards, tags, users, filters ], "user-filtering") end end diff --git a/app/models/user/timelined.rb b/app/models/user/timelined.rb index 1f5f2c8fe..2cd808a44 100644 --- a/app/models/user/timelined.rb +++ b/app/models/user/timelined.rb @@ -2,7 +2,7 @@ module User::Timelined extend ActiveSupport::Concern included do - has_many :accessible_events, through: :collections, source: :events + has_many :accessible_events, through: :boards, source: :events end def timeline_for(day, filter:) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 1af19cd47..f66cc6942 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -11,7 +11,7 @@ class Webhook < ApplicationRecord card_closed card_postponed card_auto_postponed - card_collection_changed + card_board_changed card_published card_reopened card_sent_back_to_triage @@ -25,7 +25,7 @@ class Webhook < ApplicationRecord has_many :deliveries, dependent: :delete_all has_one :delinquency_tracker, dependent: :delete - belongs_to :collection + belongs_to :board serialize :subscribed_actions, type: Array, coder: JSON diff --git a/app/models/webhook/triggerable.rb b/app/models/webhook/triggerable.rb index a2f5ea186..443c801fd 100644 --- a/app/models/webhook/triggerable.rb +++ b/app/models/webhook/triggerable.rb @@ -2,7 +2,7 @@ module Webhook::Triggerable extend ActiveSupport::Concern included do - scope :triggered_by, ->(event) { where(collection: event.collection).triggered_by_action(event.action) } + scope :triggered_by, ->(event) { where(board: event.board).triggered_by_action(event.action) } scope :triggered_by_action, ->(action) { where("subscribed_actions LIKE ?", "%\"#{action}\"%") } end diff --git a/app/views/account/settings/_users.html.erb b/app/views/account/settings/_users.html.erb index 5e571eb67..e44955c5b 100644 --- a/app/views/account/settings/_users.html.erb +++ b/app/views/account/settings/_users.html.erb @@ -13,7 +13,7 @@
      - <%= render partial: "account/settings/user", collection: users %> + <%= render partial: "account/settings/user", board: users %>
    diff --git a/app/views/cards/_broadcasts.html.erb b/app/views/cards/_broadcasts.html.erb index e3a285f05..87c21e05d 100644 --- a/app/views/cards/_broadcasts.html.erb +++ b/app/views/cards/_broadcasts.html.erb @@ -1,7 +1,7 @@ -<% if filter.collections.any? %> - <% filter.collections.each do |collection| %> - <%= turbo_stream_from collection %> +<% if filter.boards.any? %> + <% filter.boards.each do |board| %> + <%= turbo_stream_from board %> <% end %> <% else %> - <%= turbo_stream_from :all_collections %> + <%= turbo_stream_from :all_boards %> <% end %> diff --git a/app/views/cards/_card.json.jbuilder b/app/views/cards/_card.json.jbuilder index 8bb61330c..1fa339328 100644 --- a/app/views/cards/_card.json.jbuilder +++ b/app/views/cards/_card.json.jbuilder @@ -8,8 +8,8 @@ json.cache! [ card, card.column&.color ] do json.url card_url(card) - json.collection do - json.partial! "collections/collection", locals: { collection: card.collection } + json.board do + json.partial! "boards/board", locals: { board: card.board } end json.column do diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 935bd05ec..821211be9 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -8,7 +8,7 @@
    <%= card_article_tag card, class: "card" do %>
    - <%= render "cards/display/perma/collection", card: card %> + <%= render "cards/display/perma/board", card: card %> <%= render "cards/display/perma/tags", card: card %>
    diff --git a/app/views/cards/_messages.html.erb b/app/views/cards/_messages.html.erb index 144be94cc..4356d64a2 100644 --- a/app/views/cards/_messages.html.erb +++ b/app/views/cards/_messages.html.erb @@ -1,6 +1,6 @@ <%= messages_tag(card) do %> <% if card.published? %> - <%= render partial: "cards/comments/comment", collection: card.comments.chronologically, cached: true %> + <%= render partial: "cards/comments/comment", board: card.comments.chronologically, cached: true %> <%= render "cards/comments/new", card: card %> <%= render "cards/comments/watchers", card: card %> diff --git a/app/views/cards/collections/edit.html.erb b/app/views/cards/collections/edit.html.erb index 9a9ef4130..265dc0a9e 100644 --- a/app/views/cards/collections/edit.html.erb +++ b/app/views/cards/collections/edit.html.erb @@ -1,4 +1,4 @@ -<%= turbo_frame_tag "collection_picker" do %> +<%= turbo_frame_tag "board_picker" do %> <%= tag.div class: "max-width full-width", data: { action: "turbo:before-cache@document->dialog#close dialog:show@document->navigable-list#reset keydown->navigable-list#navigate filter:changed->navigable-list#reset", controller: "filter navigable-list", @@ -11,12 +11,12 @@ type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
    diff --git a/app/views/collections/show/_column.html.erb b/app/views/collections/show/_column.html.erb index 3a6084b0c..7630ce3fe 100644 --- a/app/views/collections/show/_column.html.erb +++ b/app/views/collections/show/_column.html.erb @@ -7,14 +7,14 @@ 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 "collections/show/expander", title: column.name, count: column.cards.active.count, column_id: dom_id(column) do %> - <%= render "collections/show/menu/column", column: column %> + <%= 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 %> - <%= link_to collection_column_path(column.collection, column), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= 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: collection_column_path(column.collection, column) %> + <%= column_frame_tag dom_id(column, :cards), src: board_column_path(column.board, column) %> diff --git a/app/views/collections/show/_columns.html.erb b/app/views/collections/show/_columns.html.erb index 27912c09d..2475d94b1 100644 --- a/app/views/collections/show/_columns.html.erb +++ b/app/views/collections/show/_columns.html.erb @@ -8,18 +8,18 @@ dragenter->drag-and-strum#dragEnter drop->drag-and-drop#drop dragend->drag-and-drop#dragEnd" } do %> -
    +
    - <%= render "collections/show/not_now", collection: collection %> + <%= render "boards/show/not_now", board: board %>
    - <%= render "collections/show/stream", collection: collection, page: page %> + <%= render "boards/show/stream", board: board, page: page %>
    - <%= render partial: "collections/show/column", collection: collection.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %> - <%= render "collections/show/closed", collection: collection %> + <%= render partial: "boards/show/column", board: board.columns.sorted, cached: ->(column){ [ column, column.leftmost?, column.rightmost? ] } %> + <%= render "boards/show/closed", board: board %> - <%= render "collections/show/menu/columns", collection: collection %> + <%= render "boards/show/menu/columns", board: board %>
    <% end %> diff --git a/app/views/collections/show/_not_now.html.erb b/app/views/collections/show/_not_now.html.erb index c99b9b3af..cdb40fe79 100644 --- a/app/views/collections/show/_not_now.html.erb +++ b/app/views/collections/show/_not_now.html.erb @@ -6,9 +6,9 @@ data-drag-and-drop-url="<%= columns_card_drops_not_now_path("__id__") %>" > - <%= render "collections/show/expander", title: "Not Now", count: collection.cards.postponed.count, column_id: "not-now" do %> - <%= render "collections/show/menu/expand", column_path: collection_columns_not_now_path(collection) %> + <%= 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: collection_columns_not_now_path(collection) %> + <%= column_frame_tag :not_now_column, src: board_columns_not_now_path(board) %> diff --git a/app/views/collections/show/_stream.html.erb b/app/views/collections/show/_stream.html.erb index a1985a3f2..f8b9cd81f 100644 --- a/app/views/collections/show/_stream.html.erb +++ b/app/views/collections/show/_stream.html.erb @@ -4,17 +4,17 @@ data-drag-and-drop-url="<%= columns_card_drops_stream_path("__id__") %>">

    Maybe?

    - <%= link_to collection_columns_stream_path(collection), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %> + <%= 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 <% end %>
    - <%= render "columns/show/add_card_button", collection: collection %> + <%= render "columns/show/add_card_button", board: board %> <% if page.used? %> <%= with_automatic_pagination "the-stream", @page do %> - <%= render "collections/columns/list", cards: page.records, draggable: true %> + <%= render "boards/columns/list", cards: page.records, draggable: true %> <% end %> <% end %> diff --git a/app/views/collections/show/menu/_column.html.erb b/app/views/collections/show/menu/_column.html.erb index 25c6085a4..52637cce4 100644 --- a/app/views/collections/show/menu/_column.html.erb +++ b/app/views/collections/show/menu/_column.html.erb @@ -33,7 +33,7 @@
    diff --git a/app/views/collections/show/menu/_column_form.html.erb b/app/views/collections/show/menu/_column_form.html.erb index 7b7ee4c44..b42549d3c 100644 --- a/app/views/collections/show/menu/_column_form.html.erb +++ b/app/views/collections/show/menu/_column_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: [collection, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset" } do |form| %> +<%= form_with model: [board, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset" } do |form| %> <%= form.text_field :name, class: "input", placeholder: "Name this column", value: column.name, required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank", data: { action: "focus->form#select" } %> diff --git a/app/views/collections/show/menu/_columns.html.erb b/app/views/collections/show/menu/_columns.html.erb index a02b23742..6d0ab0c90 100644 --- a/app/views/collections/show/menu/_columns.html.erb +++ b/app/views/collections/show/menu/_columns.html.erb @@ -3,6 +3,6 @@ <%= icon_tag "add", class: "translucent" %> - <%= render "collections/show/menu/column_form", collection: collection, column: Column.new, label: "Add column" %> + <%= render "boards/show/menu/column_form", board: board, column: Column.new, label: "Add column" %>
    diff --git a/app/views/columns/cards/drops/closures/create.turbo_stream.erb b/app/views/columns/cards/drops/closures/create.turbo_stream.erb index 8c4630d20..b3bee9f84 100644 --- a/app/views/columns/cards/drops/closures/create.turbo_stream.erb +++ b/app/views/columns/cards/drops/closures/create.turbo_stream.erb @@ -1 +1 @@ -<%= turbo_stream.replace("closed-cards", partial: "collections/show/closed", method: :morph, locals: { collection: @card.collection }) %> +<%= turbo_stream.replace("closed-cards", partial: "boards/show/closed", method: :morph, locals: { board: @card.board }) %> diff --git a/app/views/columns/cards/drops/columns/create.turbo_stream.erb b/app/views/columns/cards/drops/columns/create.turbo_stream.erb index 5ab9915a0..88cd716d1 100644 --- a/app/views/columns/cards/drops/columns/create.turbo_stream.erb +++ b/app/views/columns/cards/drops/columns/create.turbo_stream.erb @@ -1 +1 @@ -<%= turbo_stream.replace(dom_id(@column), partial: "collections/show/column", method: :morph, locals: { column: @column }) %> +<%= turbo_stream.replace(dom_id(@column), partial: "boards/show/column", method: :morph, locals: { column: @column }) %> diff --git a/app/views/columns/cards/drops/not_nows/create.turbo_stream.erb b/app/views/columns/cards/drops/not_nows/create.turbo_stream.erb index faf65ba56..f78a91093 100644 --- a/app/views/columns/cards/drops/not_nows/create.turbo_stream.erb +++ b/app/views/columns/cards/drops/not_nows/create.turbo_stream.erb @@ -1 +1 @@ -<%= turbo_stream.replace("not-now", partial: "collections/show/not_now", method: :morph, locals: { collection: @card.collection }) %> +<%= turbo_stream.replace("not-now", partial: "boards/show/not_now", method: :morph, locals: { board: @card.board }) %> diff --git a/app/views/columns/cards/drops/streams/create.turbo_stream.erb b/app/views/columns/cards/drops/streams/create.turbo_stream.erb index 1e957f827..f9c63881d 100644 --- a/app/views/columns/cards/drops/streams/create.turbo_stream.erb +++ b/app/views/columns/cards/drops/streams/create.turbo_stream.erb @@ -1 +1 @@ -<%= turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page }) %> +<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %> diff --git a/app/views/columns/left_positions/create.turbo_stream.erb b/app/views/columns/left_positions/create.turbo_stream.erb index 66eadf968..70729cf1b 100644 --- a/app/views/columns/left_positions/create.turbo_stream.erb +++ b/app/views/columns/left_positions/create.turbo_stream.erb @@ -1,4 +1,4 @@ <% if @left_column %> <%= turbo_stream.remove(dom_id(@column)) %> - <%= turbo_stream.before(@left_column, partial: "collections/show/column", locals: { column: @column }) %> + <%= turbo_stream.before(@left_column, partial: "boards/show/column", locals: { column: @column }) %> <% end %> diff --git a/app/views/columns/right_positions/create.turbo_stream.erb b/app/views/columns/right_positions/create.turbo_stream.erb index ed04429fd..a01ac7ce2 100644 --- a/app/views/columns/right_positions/create.turbo_stream.erb +++ b/app/views/columns/right_positions/create.turbo_stream.erb @@ -1,4 +1,4 @@ <% if @right_column %> <%= turbo_stream.remove(dom_id(@column)) %> - <%= turbo_stream.after(@right_column, partial: "collections/show/column", locals: { column: @column }) %> + <%= turbo_stream.after(@right_column, partial: "boards/show/column", locals: { column: @column }) %> <% end %> diff --git a/app/views/columns/show/_add_card_button.html.erb b/app/views/columns/show/_add_card_button.html.erb index a161465ef..7be6297a7 100644 --- a/app/views/columns/show/_add_card_button.html.erb +++ b/app/views/columns/show/_add_card_button.html.erb @@ -1,5 +1,5 @@ -
    - <%= button_to collection_cards_path(collection), method: :post, class: "btn btn--link", form: { data: { turbo_frame: "_top" } }, data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %> +
    + <%= button_to board_cards_path(board), method: :post, class: "btn btn--link", form: { data: { turbo_frame: "_top" } }, data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %> <%= icon_tag "add", class: "show-on-touch" %> Add a card C @@ -8,6 +8,6 @@
    - <%= access_involvement_advance_button(collection, Current.user, show_watchers: true) %> + <%= access_involvement_advance_button(board, Current.user, show_watchers: true) %>
    diff --git a/app/views/events/_day_timeline.html.erb b/app/views/events/_day_timeline.html.erb index 244414d2a..48d76c18e 100644 --- a/app/views/events/_day_timeline.html.erb +++ b/app/views/events/_day_timeline.html.erb @@ -1,7 +1,7 @@ <% 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 %> + <%= render partial: "events/event", board: events %> <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> <% end %> <% end %> diff --git a/app/views/events/day_timeline/_column.html.erb b/app/views/events/day_timeline/_column.html.erb index aefe9258e..463da27e1 100644 --- a/app/views/events/day_timeline/_column.html.erb +++ b/app/views/events/day_timeline/_column.html.erb @@ -4,7 +4,7 @@

    <%= column[:title] %>

    <% column[:events].group_by { it.created_at.hour }.each do |hour, events| %> <%= event_cluster_tag(hour, column[:index]) do %> - <%= render partial: "events/event", collection: events, cached: true %> + <%= render partial: "events/event", board: events, cached: true %> <%= local_datetime_tag events.first.created_at, class: "event__timestamp txt-small translucent" %> <% end %> <% end %> diff --git a/app/views/events/event/_attachments.html.erb b/app/views/events/event/_attachments.html.erb index faaf73f5a..3c62c8108 100644 --- a/app/views/events/event/_attachments.html.erb +++ b/app/views/events/event/_attachments.html.erb @@ -1,7 +1,7 @@ <% if eventable&.has_attachments? || eventable&.has_remote_images? || eventable&.has_remote_videos? %> - <%= render partial: "events/event/attachments/attachment", collection: eventable.attachments %> - <%= render partial: "events/event/attachments/remote_image", collection: eventable.remote_images %> - <%= render partial: "events/event/attachments/remote_video", collection: eventable.remote_videos %> + <%= render partial: "events/event/attachments/attachment", board: eventable.attachments %> + <%= render partial: "events/event/attachments/remote_image", board: eventable.remote_images %> + <%= render partial: "events/event/attachments/remote_video", board: eventable.remote_videos %> <% end %> diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb index 60700bb53..0fc767862 100644 --- a/app/views/events/event/_layout.html.erb +++ b/app/views/events/event/_layout.html.erb @@ -8,13 +8,13 @@ related_element_group_value: card.id, action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
    -

    +

    Card number <%= card.id %> - - <%= event.collection.name %> + + <%= event.board.name %>

    diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index ca071f189..8f0e659e7 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -7,10 +7,10 @@ <%= render "events/index/add_card_button", user_filtering: @user_filtering %>

    - <% if @user_filtering.collections.one? %> + <% if @user_filtering.boards.one? %> Latest Activity <% else %> - Activity <%= @user_filtering.filter.collections.any? ? "in" : "across" %> + Activity <%= @user_filtering.filter.boards.any? ? "in" : "across" %> <%= render "events/index/filter", user_filtering: @user_filtering %> <% end %>

    diff --git a/app/views/events/index/_add_card_button.html.erb b/app/views/events/index/_add_card_button.html.erb index 664152e3d..c22841cf5 100644 --- a/app/views/events/index/_add_card_button.html.erb +++ b/app/views/events/index/_add_card_button.html.erb @@ -1,6 +1,6 @@
    - <% if collection = user_filtering.single_collection_or_first %> - <%= button_to collection_cards_path(collection), method: :post, class: "btn btn--link btn--circle-mobile", data: { controller: "hotkey", action: "keydown.c@document->hotkey#click" } do %> + <% 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 C diff --git a/app/views/events/index/_filter.html.erb b/app/views/events/index/_filter.html.erb index 12b766977..37dab9e2b 100644 --- a/app/views/events/index/_filter.html.erb +++ b/app/views/events/index/_filter.html.erb @@ -7,32 +7,32 @@ data: { controller: "dialog multi-selection-combobox", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside turbo:morph@window->multi-selection-combobox#refresh", - filter_show: user_filtering.show_collections?, - multi_selection_combobox_no_selection_label_value: user_filtering.selected_collections_label } do %> + filter_show: user_filtering.show_boards?, + multi_selection_combobox_no_selection_label_value: user_filtering.selected_boards_label } do %> <%= filter_dialog "Board…" do %> Board… - <% if user_filtering.collections.many? %> + <% if user_filtering.boards.many? %> <%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true, type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> <% end %>