From 7b3b5c471b7e994c6d85edf42c6c4f820659959d Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 16 Apr 2025 18:53:26 -0500 Subject: [PATCH 001/284] Add a description filed for cards --- app/assets/stylesheets/cards.css | 24 ++++++++++---- app/assets/stylesheets/house.css | 3 +- app/controllers/cards_controller.rb | 2 +- app/models/card.rb | 2 ++ app/views/cards/display/_perma.html.erb | 34 ++++++++++++++------ app/views/cards/display/_preview.html.erb | 4 +-- app/views/cards/edit.html.erb | 27 ++++++++++------ app/views/cards/show.html.erb | 38 +++++------------------ 8 files changed, 74 insertions(+), 60 deletions(-) diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 6f869df5c..d8a484ab0 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -99,18 +99,32 @@ padding-block: var(--card-padding-block); } + .card__description { + house-md-toolbar, + .house-md-content { + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + } + } + .card__title { --hover-size: 0; --input-border-radius: 0; + --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --lines: 3; + color: inherit; flex: 2 1 75%; font-size: var(--text-xx-large); font-weight: 900; - line-height: 1.1; + line-height: 1.2; min-block-size: 3lh; text-wrap: balance; - .card__title-field { + .card-perma__bg & { + min-block-size: unset; + } + + .card-field__title { &:is(textarea)::placeholder { color: inherit; opacity: 0.5; @@ -118,8 +132,6 @@ } .card__title-link { - --lines: 3; - color: inherit; } } @@ -130,11 +142,11 @@ flex-direction: column; max-width: 100%; min-width: 0; - padding-block: var(--block-space); + padding-block: var(--block-space-half); } .card__move-button { - --btn-background: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --btn-background: color-mix(in srgb, inherit 40%, var(--color-ink)); margin-block-start: calc(var(--block-space) * 1.2); margin-inline-end: calc(var(--inline-space) * -4); diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css index 30982ad76..e1ce84325 100644 --- a/app/assets/stylesheets/house.css +++ b/app/assets/stylesheets/house.css @@ -58,7 +58,6 @@ /* Markdown Content */ .house-md-content { - caret-color: var(--color-link); flex-grow: 1; font-family: var(--font-sans); text-align: left; @@ -67,7 +66,7 @@ &.house-md-content-empty::before { content: attr(placeholder); - color: var(--color-ink); + color: currentColor; opacity: 0.5; pointer-events: none; display: block; diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e62146d4e..1eed61b62 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -49,6 +49,6 @@ class CardsController < ApplicationController end def card_params - params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) + params.expect(card: [ :status, :title, :description, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) end end diff --git a/app/models/card.rb b/app/models/card.rb index e2191aa41..f0923af91 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -3,6 +3,8 @@ class Card < ApplicationRecord Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable + has_markdown :description + belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/views/cards/display/_perma.html.erb b/app/views/cards/display/_perma.html.erb index f70b9d2c8..9b9cc7cbd 100644 --- a/app/views/cards/display/_perma.html.erb +++ b/app/views/cards/display/_perma.html.erb @@ -8,20 +8,36 @@
-

- <% if card.published? %> - <%= turbo_frame_tag card, :edit do %> - <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> - <% end %> - <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> + <% if card.published? %> + <%= turbo_frame_tag card, :edit do %> +
+

+ <%= card.title %> + <%= link_to edit_collection_card_path(card.collection, card), class: "btn txt-small margin-block-start-half" do %> + <%= icon_tag "pencil" %> + Edit + <% end %> +

+

+ <%= sanitize card.description_html %> +

+
+ <% end %> + <% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "full-width", data: { controller: "auto-save" } do |form| %> +

<%= form.text_area :title, placeholder: "Name it…", class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> - <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width fill-white borderless txt-align-start card-field__description #{ "fill-highlight" if card.creating? }", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "auto-save#change blur->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
<% end %> - + <% end %> <% if card.doing? %> <%= turbo_frame_tag dom_id(@card, :stages) do %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 7d673308c..9bd44aa81 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -6,8 +6,8 @@
-

- <%= card.title %> +

+ <%= card.title %>

<%= link_to collection_card_path(card.collection, card), class: "card__link" do %> diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 5e5e0d1d9..9ebf63e8e 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,13 +1,22 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %> - <%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card__title-field", - required: true, autofocus: true, placeholder: "Name it…", - rows: 3, - data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" }, - style: "color: var(--spat-color); --input-border-radius: 0" %> - <% end %> - <%= form.button "Save", type: :submit, hidden: true %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "full-width", data: { controller: "form paste" } do |form| %> +

+ <%= form.label :title, class: "flex flex-column align-center" do %> + <%= form.text_area :title, class: "input input--textarea full-width borderless fill-white txt-align-start card-field__title", + required: true, autofocus: true, placeholder: "Name it…", + rows: 1, + data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> + <% end %> + <%= form.button type: :submit, class: "btn txt-small margin-block-start-half" do %> + <%= icon_tag "check" %> + Save + <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width fill-white borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
<%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index f1f2ea95a..af59aad87 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -30,7 +30,7 @@ <% end %>
-
" style="--card-color: <%= @card.color %>;"> +
<%= render "cards/doing_toggle", card: @card %> @@ -57,7 +57,12 @@ <% end %> - <% unless @card.drafted? || @card.creating? %> + <% if @card.creating? %> +
+ <%= button_to "Create card", card_publish_path(@card), class: "btn btn--reversed" %> + <%= button_to "Save as draft", collection_card_path(@card.collection, @card), name: "card[status]", value: "drafted", method: :put, class: "btn" %> +
+ <% elsif @card.published? %> <%= render "cards/closure_toggle", card: @card %> <% end %>
@@ -65,34 +70,5 @@ <% if @card.published? %> <%= render "cards/messages", card: @card %> - <% else %> -
-
- - -
-
-
- <%= tag.house_md @card.draft_comment, name: "card[draft_comment]", class: "input comment__input", - form: "card_form", - placeholder: new_comment_placeholder(@card), - data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %> -
- - <% if @card.creating? %> -
- <%= button_to "Create card", card_publish_path(@card), class: "btn btn--reversed" %> - <%= button_to "Save as draft", collection_card_path(@card.collection, @card), name: "card[status]", value: "drafted", method: :put, class: "btn" %> -
- <% end %> -
-
-
-
<% end %>
From d257cb5fb6fe29d351f4b8d7846eb211ca993650 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 16 Apr 2025 18:57:47 -0500 Subject: [PATCH 002/284] No longer used --- app/controllers/cards_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 1eed61b62..c2c932cb5 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -49,6 +49,6 @@ class CardsController < ApplicationController end def card_params - params.expect(card: [ :status, :title, :description, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) + params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ]) end end From 47d0ae98a1f352539ea2b0fcf671cde3952b3102 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 16 Apr 2025 19:01:21 -0500 Subject: [PATCH 003/284] Remove DraftCommenting --- app/models/card.rb | 2 +- app/models/card/draft_commenting.rb | 33 -------------------- test/controllers/cards_controller_test.rb | 6 ++-- test/models/card/messages_test.rb | 37 ----------------------- 4 files changed, 3 insertions(+), 75 deletions(-) delete mode 100644 app/models/card/draft_commenting.rb diff --git a/app/models/card.rb b/app/models/card.rb index f0923af91..321a4f90e 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,5 +1,5 @@ class Card < ApplicationRecord - include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, + include Assignable, Colored, Engageable, Eventable, Golden, Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable diff --git a/app/models/card/draft_commenting.rb b/app/models/card/draft_commenting.rb deleted file mode 100644 index 615d23916..000000000 --- a/app/models/card/draft_commenting.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Card::DraftCommenting - extend ActiveSupport::Concern - - included do - after_save :capture_draft_comment - end - - def draft_comment - find_or_build_initial_comment.body.content - end - - def draft_comment=(body) - if body.present? - @draft_comment = body - else - messages.comments.destroy_all - end - end - - private - def find_or_build_initial_comment - message = messages.comments.first || messages.new(messageable: Comment.new) - message.comment - end - - def capture_draft_comment - if @draft_comment.present? - find_or_build_initial_comment.update! body: @draft_comment, creator: creator - end - - @draft_comment = nil - end -end diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index e794e7898..5651ebcb9 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -36,19 +36,17 @@ class CardsControllerTest < ActionDispatch::IntegrationTest patch collection_card_path(collections(:writebook), cards(:logo)), params: { card: { title: "Logo needs to change", - due_on: 1.week.from_now, image: fixture_file_upload("moon.jpg", "image/jpeg"), - draft_comment: "Something more in-depth", + description: "Something more in-depth", tag_ids: [ tags(:mobile).id ] } } assert_redirected_to collection_card_path(collections(:writebook), cards(:logo)) card = cards(:logo).reload assert_equal "Logo needs to change", card.title - assert_equal 1.week.from_now.to_date, card.due_on assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.messages.comments.first.comment.body_plain_text.strip + assert_equal "Something more in-depth", card.description_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/models/card/messages_test.rb b/test/models/card/messages_test.rb index 08ade7f0f..31d730306 100644 --- a/test/models/card/messages_test.rb +++ b/test/models/card/messages_test.rb @@ -6,41 +6,4 @@ class Card::MessagesTest < ActiveSupport::TestCase assert_empty card.messages end - - test "creating a card with an initial draft comment" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - assert_equal 1, card.messages.count - assert_equal "This is a comment", card.draft_comment.strip - end - - test "updating the draft comment" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! draft_comment: "This is an updated comment" - - assert_equal 1, card.messages.count - assert_equal "This is an updated comment", card.draft_comment.strip - end - - test "setting the draft comment to be blank removes it" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! draft_comment: " " - - assert card.messages.first.nil? - end - - test "omitting the draft comment does not remove it" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! title: "Newer" - - assert_equal 1, card.messages.count - assert_equal "This is a comment", card.draft_comment.strip - end end From 94ccbdb6013ebed36bbc3962fd48e760e5544024 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Wed, 16 Apr 2025 19:06:18 -0500 Subject: [PATCH 004/284] Fix close card notch state --- app/views/cards/_closure_toggle.html.erb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/cards/_closure_toggle.html.erb b/app/views/cards/_closure_toggle.html.erb index 7958b0887..661b8edea 100644 --- a/app/views/cards/_closure_toggle.html.erb +++ b/app/views/cards/_closure_toggle.html.erb @@ -1,9 +1,11 @@ -
"> +
<% if card.closed? %> - Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. - <%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %> - Un-do - <% end %> +
+ Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. + <%= button_to card_closure_path(card), method: :delete, class: "btn btn--plain borderless fill-transparent" do %> + Un-do + <% end %> +
<% else %>
From 6c8fe164b3369c992bc3f0c973728ed3fe18e146 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Thu, 17 Apr 2025 09:37:34 +0100 Subject: [PATCH 005/284] Read a card should dismiss all notifications Previous code assumed that there was a single notification to dismiss for that card, but in practice there can be zero or many. If it's zero, we need to avoid the crash. If it's many, we should dismiss them all. Also re-rendering the notification stack after dismissing enables us to remove all the removed cards, as well as reveal any older ones that were previously outside the visible page size. This reverts commit 6135909b7e7b34ffdc7430a8b609c1b561085ba4. --- app/controllers/cards/readings_controller.rb | 4 ++-- app/views/cards/readings/create.turbo_stream.erb | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index bbc4d53c0..dbb379b15 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -2,7 +2,7 @@ class Cards::ReadingsController < ApplicationController include CardScoped def create - @notification = Current.user.notifications.find_by(card: @card) - @notification.read + Current.user.notifications.unread.where(card: @card).read_all + @notifications = Current.user.notifications.unread.ordered.limit(20) end end diff --git a/app/views/cards/readings/create.turbo_stream.erb b/app/views/cards/readings/create.turbo_stream.erb index 1f3b07b47..8f758752e 100644 --- a/app/views/cards/readings/create.turbo_stream.erb +++ b/app/views/cards/readings/create.turbo_stream.erb @@ -1 +1,3 @@ -<%= turbo_stream.remove @notification %> +<%= turbo_stream.update "notifications" do %> + <%= render collection: @notifications, partial: "notifications/notification" %> +<% end %> From a5de4863fa7bd22a1ac0a35fc8a9b77982e784fc Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 11:39:05 +0200 Subject: [PATCH 006/284] Allow for multiple notifications to be read cc @kevinmcconnell --- app/controllers/cards/readings_controller.rb | 4 ++-- app/views/cards/readings/create.turbo_stream.erb | 4 ++-- test/controllers/cards/readings_controller_test.rb | 12 +++++++++++- test/fixtures/notifications.yml | 7 +++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index dbb379b15..2d8b51ac6 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -2,7 +2,7 @@ class Cards::ReadingsController < ApplicationController include CardScoped def create - Current.user.notifications.unread.where(card: @card).read_all - @notifications = Current.user.notifications.unread.ordered.limit(20) + @notifications = Current.user.notifications.where(card: @card) + @notifications.each(&:read) end end diff --git a/app/views/cards/readings/create.turbo_stream.erb b/app/views/cards/readings/create.turbo_stream.erb index 8f758752e..5d7fe7801 100644 --- a/app/views/cards/readings/create.turbo_stream.erb +++ b/app/views/cards/readings/create.turbo_stream.erb @@ -1,3 +1,3 @@ -<%= turbo_stream.update "notifications" do %> - <%= render collection: @notifications, partial: "notifications/notification" %> +<% @notifications.each do |notification| %> + <%= turbo_stream.remove notification %> <% end %> diff --git a/test/controllers/cards/readings_controller_test.rb b/test/controllers/cards/readings_controller_test.rb index c169517e2..a880e46c7 100644 --- a/test/controllers/cards/readings_controller_test.rb +++ b/test/controllers/cards/readings_controller_test.rb @@ -5,11 +5,21 @@ class Cards::ReadingsControllerTest < ActionDispatch::IntegrationTest sign_in_as :kevin end - test "index" do + test "read one notification on card visit" do assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do post card_reading_path(cards(:logo)), as: :turbo_stream end assert_response :success end + + test "read multiple notifications on card visit" do + assert_changes -> { notifications(:logo_published_kevin).reload.read? }, from: false, to: true do + assert_changes -> { notifications(:logo_assignment_kevin).reload.read? }, from: false, to: true do + post card_reading_path(cards(:logo)), as: :turbo_stream + end + end + + assert_response :success + end end diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml index d6db9d360..7579f8eb6 100644 --- a/test/fixtures/notifications.yml +++ b/test/fixtures/notifications.yml @@ -5,6 +5,13 @@ logo_published_kevin: resource: logo (Card) created_at: <%= 1.week.ago %> +logo_assignment_kevin: + user: kevin + event: logo_assignment_km + card: logo + resource: logo (Card) + created_at: <%= 1.week.ago %> + layout_commented_kevin: user: kevin event: layout_commented From bf99da8914242f1ac70daf47adb5719fb6b55a07 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 13:33:20 +0200 Subject: [PATCH 007/284] Style --- app/views/cards/show.html.erb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index f1f2ea95a..b25af4f3c 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -31,18 +31,14 @@
" style="--card-color: <%= @card.color %>;"> - <%= render "cards/doing_toggle", card: @card %> From b5d6d40462a991824071a806759e352d6730e10a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 13:42:06 +0200 Subject: [PATCH 008/284] Use domain language termn --- .../cards/{_doing_toggle.html.erb => _engagement.html.erb} | 0 app/views/cards/show.html.erb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/cards/{_doing_toggle.html.erb => _engagement.html.erb} (100%) diff --git a/app/views/cards/_doing_toggle.html.erb b/app/views/cards/_engagement.html.erb similarity index 100% rename from app/views/cards/_doing_toggle.html.erb rename to app/views/cards/_engagement.html.erb diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index b25af4f3c..813fff2bc 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -31,7 +31,7 @@
" style="--card-color: <%= @card.color %>;"> - <%= render "cards/doing_toggle", card: @card %> + <%= render "cards/engagement", card: @card %> From 547e5ea24fb7217eed627286d0d2742f551ebc34 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 14:54:29 +0200 Subject: [PATCH 020/284] Rerender instead of redirect --- app/controllers/cards/goldnesses_controller.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/cards/goldnesses_controller.rb b/app/controllers/cards/goldnesses_controller.rb index 4be611d14..2c37ad80a 100644 --- a/app/controllers/cards/goldnesses_controller.rb +++ b/app/controllers/cards/goldnesses_controller.rb @@ -3,11 +3,16 @@ class Cards::GoldnessesController < ApplicationController def create @card.gild - redirect_to @card + rerender_card_container end def destroy @card.ungild - redirect_to @card + rerender_card_container end + + private + def rerender_card_container + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) + end end From 06da295f44d30df7883a34f3e4ddc17389f3ece7 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 14:56:53 +0200 Subject: [PATCH 021/284] Fix tests --- .../cards/goldnesses_controller_test.rb | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/controllers/cards/goldnesses_controller_test.rb b/test/controllers/cards/goldnesses_controller_test.rb index b8cfb9469..50fd411b0 100644 --- a/test/controllers/cards/goldnesses_controller_test.rb +++ b/test/controllers/cards/goldnesses_controller_test.rb @@ -6,22 +6,16 @@ class Cards::GoldnessesControllerTest < ActionDispatch::IntegrationTest end test "create" do - card = cards(:text) - - assert_changes -> { card.reload.golden? }, from: false, to: true do - post card_goldness_path(card) + assert_changes -> { cards(:text).reload.golden? }, from: false, to: true do + post card_goldness_path(cards(:text)), as: :turbo_stream + assert_response :success end - - assert_redirected_to collection_card_path(card.collection, card) end test "destroy" do - card = cards(:logo) - - assert_changes -> { card.reload.golden? }, from: true, to: false do - delete card_goldness_path(card) + assert_changes -> { cards(:logo).reload.golden? }, from: true, to: false do + delete card_goldness_path(cards(:logo)), as: :turbo_stream + assert_response :success end - - assert_redirected_to collection_card_path(card.collection, card) end end From 770029c26d09beae97a9c288ae22ada08a7fa5a8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 14:59:15 +0200 Subject: [PATCH 022/284] Stick with domain language --- app/views/cards/_container.html.erb | 2 +- app/views/cards/{_golden_toggle.html.erb => _gild.html.erb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/cards/{_golden_toggle.html.erb => _gild.html.erb} (100%) diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 183315dcb..4807d0028 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -2,7 +2,7 @@ <%= render "cards/engagement", card: card %>
From 7adbd8d6348e1885bb12a9db2eada25c98df1c9a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:20:27 +0200 Subject: [PATCH 024/284] Use card rerendering instead of redirects --- app/controllers/cards/assignments_controller.rb | 1 + app/controllers/cards/closures_controller.rb | 4 ++-- app/controllers/cards/engagements_controller.rb | 4 ++-- app/controllers/cards/goldnesses_controller.rb | 5 ----- app/controllers/cards/stagings_controller.rb | 2 ++ app/controllers/cards/taggings_controller.rb | 1 + app/controllers/concerns/card_scoped.rb | 4 ++++ app/views/cards/assignments/create.turbo_stream.erb | 1 - app/views/cards/stagings/create.turbo_stream.erb | 8 -------- app/views/cards/taggings/create.turbo_stream.erb | 1 - test/controllers/cards/assignments_controller_test.rb | 4 ++-- test/controllers/cards/closures_controller_test.rb | 6 ++---- test/controllers/cards/engagements_controller_test.rb | 11 +++++++---- test/controllers/cards/goldnesses_controller_test.rb | 4 ++-- test/controllers/cards/taggings_controller_test.rb | 4 ++-- test/test_helper.rb | 2 +- test/test_helpers/card_test_helper.rb | 5 +++++ 17 files changed, 33 insertions(+), 34 deletions(-) delete mode 100644 app/views/cards/assignments/create.turbo_stream.erb delete mode 100644 app/views/cards/stagings/create.turbo_stream.erb delete mode 100644 app/views/cards/taggings/create.turbo_stream.erb create mode 100644 test/test_helpers/card_test_helper.rb diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index 63dce613c..bba888bf4 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -6,5 +6,6 @@ class Cards::AssignmentsController < ApplicationController def create @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) + rerender_card_container end end diff --git a/app/controllers/cards/closures_controller.rb b/app/controllers/cards/closures_controller.rb index 3682552ee..f79608036 100644 --- a/app/controllers/cards/closures_controller.rb +++ b/app/controllers/cards/closures_controller.rb @@ -3,11 +3,11 @@ class Cards::ClosuresController < ApplicationController def create @card.close(user: Current.user, reason: params[:reason]) - redirect_to @card + rerender_card_container end def destroy @card.reopen - redirect_to @card + rerender_card_container end end diff --git a/app/controllers/cards/engagements_controller.rb b/app/controllers/cards/engagements_controller.rb index 72025d16a..e1178f1b3 100644 --- a/app/controllers/cards/engagements_controller.rb +++ b/app/controllers/cards/engagements_controller.rb @@ -3,11 +3,11 @@ class Cards::EngagementsController < ApplicationController def create @card.engage - redirect_to @card + rerender_card_container end def destroy @card.reconsider - redirect_to @card + rerender_card_container end end diff --git a/app/controllers/cards/goldnesses_controller.rb b/app/controllers/cards/goldnesses_controller.rb index 2c37ad80a..fb6076eec 100644 --- a/app/controllers/cards/goldnesses_controller.rb +++ b/app/controllers/cards/goldnesses_controller.rb @@ -10,9 +10,4 @@ class Cards::GoldnessesController < ApplicationController @card.ungild rerender_card_container end - - private - def rerender_card_container - render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) - end end diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb index 071d19f5a..728cad8a5 100644 --- a/app/controllers/cards/stagings_controller.rb +++ b/app/controllers/cards/stagings_controller.rb @@ -9,6 +9,8 @@ class Cards::StagingsController < ApplicationController else @card.update!(stage: nil) end + + rerender_card_container end private diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index 710b59fdd..a44b8a4df 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -7,6 +7,7 @@ class Cards::TaggingsController < ApplicationController def create @card.toggle_tag_with sanitized_tag_title_param + rerender_card_container end private diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index ef40d56e3..e89bf7f4d 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -13,4 +13,8 @@ module CardScoped def set_collection @collection = @card.collection end + + def rerender_card_container + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) + end end diff --git a/app/views/cards/assignments/create.turbo_stream.erb b/app/views/cards/assignments/create.turbo_stream.erb deleted file mode 100644 index f05f15d8d..000000000 --- a/app/views/cards/assignments/create.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= turbo_stream.replace([ @card, :meta ], partial: "cards/display/perma/meta", locals: { card: @card }) %> diff --git a/app/views/cards/stagings/create.turbo_stream.erb b/app/views/cards/stagings/create.turbo_stream.erb deleted file mode 100644 index ac3942d6a..000000000 --- a/app/views/cards/stagings/create.turbo_stream.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= turbo_stream.replace dom_id(@card, "stages") do %> - <%= render partial: "cards/stagings/stages", locals: { card: @card } %> -<% end %> - -<%= turbo_stream.set_css_variable dom_id(@card, :card_closure_toggle), name: "--card-color", value: @card.color %> -<%= turbo_stream.set_css_variable dom_id(@card, :card_container), name: "--card-color", value: @card.color %> -<%= turbo_stream.set_css_variable dom_id(@card, :card_engagement_toggle), name: "--card-color", value: @card.color %> -<%= turbo_stream.set_css_variable dom_id(@card, :ticket), name: "--card-color", value: @card.color %> diff --git a/app/views/cards/taggings/create.turbo_stream.erb b/app/views/cards/taggings/create.turbo_stream.erb deleted file mode 100644 index f1775e2f8..000000000 --- a/app/views/cards/taggings/create.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= turbo_stream.replace([ @card, :tags ], partial: "cards/display/perma/tags", locals: { card: @card }) %> diff --git a/test/controllers/cards/assignments_controller_test.rb b/test/controllers/cards/assignments_controller_test.rb index c92f57627..e0629aee4 100644 --- a/test/controllers/cards/assignments_controller_test.rb +++ b/test/controllers/cards/assignments_controller_test.rb @@ -13,12 +13,12 @@ class Cards::AssignmentsControllerTest < ActionDispatch::IntegrationTest test "create" do assert_changes "cards(:logo).assigned_to?(users(:david))", from: false, to: true do post card_assignments_path(cards(:logo)), params: { assignee_id: users(:david).id }, as: :turbo_stream + assert_card_container_rerendered(cards(:logo)) end - assert_response :success assert_changes "cards(:logo).assigned_to?(users(:david))", from: true, to: false do post card_assignments_path(cards(:logo)), params: { assignee_id: users(:david).id }, as: :turbo_stream + assert_card_container_rerendered(cards(:logo)) end - assert_response :success end end diff --git a/test/controllers/cards/closures_controller_test.rb b/test/controllers/cards/closures_controller_test.rb index 1729010a9..1e8f85871 100644 --- a/test/controllers/cards/closures_controller_test.rb +++ b/test/controllers/cards/closures_controller_test.rb @@ -10,11 +10,10 @@ class Cards::ClosuresControllerTest < ActionDispatch::IntegrationTest assert_changes -> { card.reload.closed? }, from: false, to: true do post card_closure_path(card, reason: "Scope too big") + assert_card_container_rerendered(card) end assert_equal "Scope too big", card.closure.reason - - assert_redirected_to collection_card_path(card.collection, card) end test "destroy" do @@ -22,8 +21,7 @@ class Cards::ClosuresControllerTest < ActionDispatch::IntegrationTest assert_changes -> { card.reload.closed? }, from: true, to: false do delete card_closure_path(card) + assert_card_container_rerendered(card) end - - assert_redirected_to collection_card_path(card.collection, card) end end diff --git a/test/controllers/cards/engagements_controller_test.rb b/test/controllers/cards/engagements_controller_test.rb index 1e966108c..6b4265b86 100644 --- a/test/controllers/cards/engagements_controller_test.rb +++ b/test/controllers/cards/engagements_controller_test.rb @@ -10,9 +10,8 @@ class Cards::EngagementsControllerTest < ActionDispatch::IntegrationTest assert_changes -> { card.reload.doing? }, from: false, to: true do post card_engagement_path(card) + assert_card_container_rerendered(card) end - - assert_redirected_to collection_card_path(card.collection, card) end test "destroy" do @@ -20,8 +19,12 @@ class Cards::EngagementsControllerTest < ActionDispatch::IntegrationTest assert_changes -> { card.reload.doing? }, from: true, to: false do delete card_engagement_path(card) + assert_card_container_rerendered(card) end - - assert_redirected_to collection_card_path(card.collection, card) end + + private + def assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(card, :card_container) + end end diff --git a/test/controllers/cards/goldnesses_controller_test.rb b/test/controllers/cards/goldnesses_controller_test.rb index 50fd411b0..447aa2b71 100644 --- a/test/controllers/cards/goldnesses_controller_test.rb +++ b/test/controllers/cards/goldnesses_controller_test.rb @@ -8,14 +8,14 @@ class Cards::GoldnessesControllerTest < ActionDispatch::IntegrationTest test "create" do assert_changes -> { cards(:text).reload.golden? }, from: false, to: true do post card_goldness_path(cards(:text)), as: :turbo_stream - assert_response :success + assert_card_container_rerendered(cards(:text)) end end test "destroy" do assert_changes -> { cards(:logo).reload.golden? }, from: true, to: false do delete card_goldness_path(cards(:logo)), as: :turbo_stream - assert_response :success + assert_card_container_rerendered(cards(:logo)) end end end diff --git a/test/controllers/cards/taggings_controller_test.rb b/test/controllers/cards/taggings_controller_test.rb index ad5a8fa91..60f4cf743 100644 --- a/test/controllers/cards/taggings_controller_test.rb +++ b/test/controllers/cards/taggings_controller_test.rb @@ -13,14 +13,14 @@ class Cards::TaggingsControllerTest < ActionDispatch::IntegrationTest test "toggle tag on" do assert_changes "cards(:logo).tagged_with?(tags(:mobile))", from: false, to: true do post card_taggings_path(cards(:logo)), params: { tag_title: tags(:mobile).title }, as: :turbo_stream + assert_card_container_rerendered(cards(:logo)) end - assert_response :success end test "toggle tag off" do assert_changes "cards(:logo).tagged_with?(tags(:web))", from: true, to: false do post card_taggings_path(cards(:logo)), params: { tag_title: tags(:web).title }, as: :turbo_stream + assert_card_container_rerendered(cards(:logo)) end - assert_response :success end end diff --git a/test/test_helper.rb b/test/test_helper.rb index f7cc1e088..1447430b1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,6 +10,6 @@ module ActiveSupport # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all - include ChangeTestHelper, SessionTestHelper + include CardTestHelper, ChangeTestHelper, SessionTestHelper end end diff --git a/test/test_helpers/card_test_helper.rb b/test/test_helpers/card_test_helper.rb new file mode 100644 index 000000000..6de139c86 --- /dev/null +++ b/test/test_helpers/card_test_helper.rb @@ -0,0 +1,5 @@ +module CardTestHelper + def assert_card_container_rerendered(card) + assert_turbo_stream action: :replace, target: dom_id(card, :card_container) + end +end From 4dbd8205cebed7d1bc73475ba6bcd59b558718cc Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:21:14 +0200 Subject: [PATCH 025/284] Fix trailing space --- app/controllers/cards/stagings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb index 728cad8a5..37d4f91d7 100644 --- a/app/controllers/cards/stagings_controller.rb +++ b/app/controllers/cards/stagings_controller.rb @@ -9,7 +9,7 @@ class Cards::StagingsController < ApplicationController else @card.update!(stage: nil) end - + rerender_card_container end From b788ef1638c296d36536f696a99a700d68ff8b14 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:23:40 +0200 Subject: [PATCH 026/284] Match domain language --- app/views/cards/{_closure_toggle.html.erb => _closure.html.erb} | 0 app/views/cards/_container.html.erb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/views/cards/{_closure_toggle.html.erb => _closure.html.erb} (100%) diff --git a/app/views/cards/_closure_toggle.html.erb b/app/views/cards/_closure.html.erb similarity index 100% rename from app/views/cards/_closure_toggle.html.erb rename to app/views/cards/_closure.html.erb diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 4807d0028..69f486b97 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -22,6 +22,6 @@ <% end %> <% unless card.drafted? || card.creating? %> - <%= render "cards/closure_toggle", card: card %> + <%= render "cards/closure", card: card %> <% end %>
From df1f6234356ea1dadefc0fc2ff9c02f373d30042 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:25:11 +0200 Subject: [PATCH 027/284] Group partials for container --- app/views/cards/_container.html.erb | 8 ++++---- app/views/cards/{ => container}/_closure.html.erb | 0 app/views/cards/{ => container}/_engagement.html.erb | 0 app/views/cards/{ => container}/_gild.html.erb | 0 app/views/cards/{ => container}/_image.html.erb | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename app/views/cards/{ => container}/_closure.html.erb (100%) rename app/views/cards/{ => container}/_engagement.html.erb (100%) rename app/views/cards/{ => container}/_gild.html.erb (100%) rename app/views/cards/{ => container}/_image.html.erb (100%) diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 69f486b97..86844aaa6 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,9 +1,9 @@
" style="--card-color: <%= card.color %>;"> - <%= render "cards/engagement", card: card %> + <%= render "cards/container/engagement", card: card %>
diff --git a/app/views/cards/_closure.html.erb b/app/views/cards/container/_closure.html.erb similarity index 100% rename from app/views/cards/_closure.html.erb rename to app/views/cards/container/_closure.html.erb diff --git a/app/views/cards/_engagement.html.erb b/app/views/cards/container/_engagement.html.erb similarity index 100% rename from app/views/cards/_engagement.html.erb rename to app/views/cards/container/_engagement.html.erb diff --git a/app/views/cards/_gild.html.erb b/app/views/cards/container/_gild.html.erb similarity index 100% rename from app/views/cards/_gild.html.erb rename to app/views/cards/container/_gild.html.erb diff --git a/app/views/cards/_image.html.erb b/app/views/cards/container/_image.html.erb similarity index 100% rename from app/views/cards/_image.html.erb rename to app/views/cards/container/_image.html.erb From 62088b66d156e9902f822d10ecb99136498cee4b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:30:55 +0200 Subject: [PATCH 028/284] Suffix qualifier is unnnecessary --- app/controllers/cards/assignments_controller.rb | 2 +- app/controllers/cards/closures_controller.rb | 4 ++-- app/controllers/cards/engagements_controller.rb | 4 ++-- app/controllers/cards/goldnesses_controller.rb | 4 ++-- app/controllers/cards/stagings_controller.rb | 2 +- app/controllers/cards/taggings_controller.rb | 2 +- app/controllers/concerns/card_scoped.rb | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index bba888bf4..d2d8b8aa6 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -6,6 +6,6 @@ class Cards::AssignmentsController < ApplicationController def create @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) - rerender_card_container + rerender_card end end diff --git a/app/controllers/cards/closures_controller.rb b/app/controllers/cards/closures_controller.rb index f79608036..ac2e0ec46 100644 --- a/app/controllers/cards/closures_controller.rb +++ b/app/controllers/cards/closures_controller.rb @@ -3,11 +3,11 @@ class Cards::ClosuresController < ApplicationController def create @card.close(user: Current.user, reason: params[:reason]) - rerender_card_container + rerender_card end def destroy @card.reopen - rerender_card_container + rerender_card end end diff --git a/app/controllers/cards/engagements_controller.rb b/app/controllers/cards/engagements_controller.rb index e1178f1b3..9baaa7609 100644 --- a/app/controllers/cards/engagements_controller.rb +++ b/app/controllers/cards/engagements_controller.rb @@ -3,11 +3,11 @@ class Cards::EngagementsController < ApplicationController def create @card.engage - rerender_card_container + rerender_card end def destroy @card.reconsider - rerender_card_container + rerender_card end end diff --git a/app/controllers/cards/goldnesses_controller.rb b/app/controllers/cards/goldnesses_controller.rb index fb6076eec..95a821fc3 100644 --- a/app/controllers/cards/goldnesses_controller.rb +++ b/app/controllers/cards/goldnesses_controller.rb @@ -3,11 +3,11 @@ class Cards::GoldnessesController < ApplicationController def create @card.gild - rerender_card_container + rerender_card end def destroy @card.ungild - rerender_card_container + rerender_card end end diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb index 37d4f91d7..a3fa96d64 100644 --- a/app/controllers/cards/stagings_controller.rb +++ b/app/controllers/cards/stagings_controller.rb @@ -10,7 +10,7 @@ class Cards::StagingsController < ApplicationController @card.update!(stage: nil) end - rerender_card_container + rerender_card end private diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index a44b8a4df..1d46d6a78 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -7,7 +7,7 @@ class Cards::TaggingsController < ApplicationController def create @card.toggle_tag_with sanitized_tag_title_param - rerender_card_container + rerender_card end private diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index e89bf7f4d..65cd75920 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -14,7 +14,7 @@ module CardScoped @collection = @card.collection end - def rerender_card_container + def rerender_card render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) end end From ac9cbad4f92afe4d614ec0189050af065c4dc4cd Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:32:09 +0200 Subject: [PATCH 029/284] I like this even better --- app/controllers/cards/assignments_controller.rb | 2 +- app/controllers/cards/closures_controller.rb | 4 ++-- app/controllers/cards/engagements_controller.rb | 4 ++-- app/controllers/cards/goldnesses_controller.rb | 4 ++-- app/controllers/cards/stagings_controller.rb | 2 +- app/controllers/cards/taggings_controller.rb | 2 +- app/controllers/concerns/card_scoped.rb | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/controllers/cards/assignments_controller.rb b/app/controllers/cards/assignments_controller.rb index d2d8b8aa6..0dd3d460e 100644 --- a/app/controllers/cards/assignments_controller.rb +++ b/app/controllers/cards/assignments_controller.rb @@ -6,6 +6,6 @@ class Cards::AssignmentsController < ApplicationController def create @card.toggle_assignment @collection.users.active.find(params[:assignee_id]) - rerender_card + render_card_replacement end end diff --git a/app/controllers/cards/closures_controller.rb b/app/controllers/cards/closures_controller.rb index ac2e0ec46..29dcfd30c 100644 --- a/app/controllers/cards/closures_controller.rb +++ b/app/controllers/cards/closures_controller.rb @@ -3,11 +3,11 @@ class Cards::ClosuresController < ApplicationController def create @card.close(user: Current.user, reason: params[:reason]) - rerender_card + render_card_replacement end def destroy @card.reopen - rerender_card + render_card_replacement end end diff --git a/app/controllers/cards/engagements_controller.rb b/app/controllers/cards/engagements_controller.rb index 9baaa7609..50a6daa83 100644 --- a/app/controllers/cards/engagements_controller.rb +++ b/app/controllers/cards/engagements_controller.rb @@ -3,11 +3,11 @@ class Cards::EngagementsController < ApplicationController def create @card.engage - rerender_card + render_card_replacement end def destroy @card.reconsider - rerender_card + render_card_replacement end end diff --git a/app/controllers/cards/goldnesses_controller.rb b/app/controllers/cards/goldnesses_controller.rb index 95a821fc3..1a0912a24 100644 --- a/app/controllers/cards/goldnesses_controller.rb +++ b/app/controllers/cards/goldnesses_controller.rb @@ -3,11 +3,11 @@ class Cards::GoldnessesController < ApplicationController def create @card.gild - rerender_card + render_card_replacement end def destroy @card.ungild - rerender_card + render_card_replacement end end diff --git a/app/controllers/cards/stagings_controller.rb b/app/controllers/cards/stagings_controller.rb index a3fa96d64..102c17599 100644 --- a/app/controllers/cards/stagings_controller.rb +++ b/app/controllers/cards/stagings_controller.rb @@ -10,7 +10,7 @@ class Cards::StagingsController < ApplicationController @card.update!(stage: nil) end - rerender_card + render_card_replacement end private diff --git a/app/controllers/cards/taggings_controller.rb b/app/controllers/cards/taggings_controller.rb index 1d46d6a78..a8fe7ecf9 100644 --- a/app/controllers/cards/taggings_controller.rb +++ b/app/controllers/cards/taggings_controller.rb @@ -7,7 +7,7 @@ class Cards::TaggingsController < ApplicationController def create @card.toggle_tag_with sanitized_tag_title_param - rerender_card + render_card_replacement end private diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index 65cd75920..2ea4752c4 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -14,7 +14,7 @@ module CardScoped @collection = @card.collection end - def rerender_card + def render_card_replacement render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) end end From 182d1c934b24d4d84c029f8fc0a88c9b0b16ec1a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:35:56 +0200 Subject: [PATCH 030/284] Note that we dont want to keep reloading these frames --- app/views/cards/_container.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 86844aaa6..76bc48262 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -15,6 +15,7 @@
<% if card.published? %> + <%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %>
From 0e442ac0d04840cbeea1758571d85690541bad63 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 15:56:54 +0200 Subject: [PATCH 034/284] Use card replacement for card updates too --- app/controllers/cards_controller.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index e62146d4e..25428545f 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -25,16 +25,16 @@ class CardsController < ApplicationController def edit end + def update + @card.update! card_params + render_card_replacement + end + def destroy @card.destroy! redirect_to cards_path(collection_ids: [ @card.collection ]), notice: ("Card deleted" unless @card.creating?) end - def update - @card.update! card_params - redirect_to @card - end - private def set_card @card = @collection.cards.find params[:id] @@ -51,4 +51,8 @@ class CardsController < ApplicationController def card_params params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) end + + def render_card_replacement + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) + end end From b8775d41f51193aa900b81b725c895dce8e79339 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 16:06:28 +0200 Subject: [PATCH 035/284] Inline perma to container partial Reveals all the container elements and perma wasnt used elsewhere. --- app/views/cards/_container.html.erb | 20 +++++++++++- app/views/cards/container/_title.html.erb | 14 ++++++++ app/views/cards/display/_perma.html.erb | 39 ----------------------- 3 files changed, 33 insertions(+), 40 deletions(-) create mode 100644 app/views/cards/container/_title.html.erb delete mode 100644 app/views/cards/display/_perma.html.erb diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index d3a2f1423..a817a21af 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -10,7 +10,25 @@
- <%= render "cards/display/perma", card: card %> + <%= card_article_tag card, class: "card" do %> +
+ <%= link_to card.collection.name, cards_path(collection_ids: [ card.collection ]), + class: "card__collection txt-uppercase overflow-ellipsis txt-reversed" %> + + <%= render "cards/display/perma/tags", card: card %> +
+ +
+ <%= render "cards/container/title", card: card %> + <%= render "cards/stagings/stages", card: card if card.doing? %> +
+ +
+ <%= render "cards/display/perma/meta", card: card %> +
+ + <%= render "cards/display/common/background", card: card %> + <% end %>
<% if card.published? %> diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb new file mode 100644 index 000000000..b62eb2fb4 --- /dev/null +++ b/app/views/cards/container/_title.html.erb @@ -0,0 +1,14 @@ +

+ <% if card.published? %> + <%= turbo_frame_tag card, :edit do %> + <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> + <% end %> + <% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> + <%= form.text_area :title, placeholder: "Name it…", + class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", + autofocus: card.title.blank?, + data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> + <% end %> + <% end %> +

diff --git a/app/views/cards/display/_perma.html.erb b/app/views/cards/display/_perma.html.erb deleted file mode 100644 index f70b9d2c8..000000000 --- a/app/views/cards/display/_perma.html.erb +++ /dev/null @@ -1,39 +0,0 @@ -<% cache card do %> - <%= card_article_tag card, class: "card" do %> -
- <%= link_to card.collection.name, cards_path(collection_ids: [card.collection]), - class: "card__collection txt-uppercase overflow-ellipsis txt-reversed" %> - - <%= render "cards/display/perma/tags", card: card %> -
- -
-

- <% if card.published? %> - <%= turbo_frame_tag card, :edit do %> - <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> - <% end %> - <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> - <%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", - autofocus: card.title.blank?, - data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> - <% end %> - <% end %> -

- - <% if card.doing? %> - <%= turbo_frame_tag dom_id(@card, :stages) do %> - <%= render "cards/stagings/stages", card: card %> - <% end %> - <% end %> -
- -
- <%= render "cards/display/perma/meta", card: card %> -
- - <%= render "cards/display/common/background", card: card %> - <% end %> -<% end %> From 36e9ff47fb9b7da2b25394f9d785b868b44007fe Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Thu, 17 Apr 2025 16:27:01 +0100 Subject: [PATCH 036/284] Less noisy logging in production --- config/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index 29eb15ba4..3ee355012 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -25,7 +25,7 @@ accessories: beamer-primary: <<: *beamer-accessory service: beamer-primary - cmd: beamer primary --debug --remove-after=1h --dir=/storage + cmd: beamer primary --remove-after=1h --dir=/storage port: 5000:80 roles: - web @@ -33,7 +33,7 @@ accessories: beamer-replica: <<: *beamer-accessory service: beamer-replica - cmd: beamer replica --debug --primary=http://fizzy-app-101:5000/ --dir=/storage + cmd: beamer replica --primary=http://fizzy-app-101:5000/ --dir=/storage roles: - web-replica From 09e544a881de6c62de6b898860fe456ba2f62d08 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Thu, 17 Apr 2025 16:28:20 +0100 Subject: [PATCH 037/284] Run beamer accessories in production only --- config/deploy.production.yml | 29 +++++++++++++++++++++++++++++ config/deploy.yml | 36 +++--------------------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/config/deploy.production.yml b/config/deploy.production.yml index 00a4b176e..cc05d75b5 100644 --- a/config/deploy.production.yml +++ b/config/deploy.production.yml @@ -18,3 +18,32 @@ proxy: ssh: user: app + + +x-beamer-accessory: &beamer-accessory + image: basecamp/beamer + registry: + username: bcbot + password: + - BASECAMP_REGISTRY_PASSWORD + options: + user: 1000 # Match the UID of the Rails app, for volume permissions + volumes: + - fizzy:/storage + + +accessories: + beamer-primary: + <<: *beamer-accessory + service: beamer-primary + cmd: beamer primary --remove-after=1h --dir=/storage + port: 5000:80 + roles: + - web + + beamer-replica: + <<: *beamer-accessory + service: beamer-replica + cmd: beamer replica --primary=http://fizzy-app-101:5000/ --dir=/storage + roles: + - web-replica diff --git a/config/deploy.yml b/config/deploy.yml index 3ee355012..9617a8b87 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,42 +1,10 @@ service: fizzy image: basecamp/fizzy - -x-basecamp-registry: &basecamp-registry - username: bcbot - password: - - BASECAMP_REGISTRY_PASSWORD - -x-beamer-accessory: &beamer-accessory - image: basecamp/beamer - registry: - <<: *basecamp-registry - options: - user: 1000 # Match the UID of the Rails app, for volume permissions - volumes: - - fizzy:/storage - - servers: jobs: cmd: bin/jobs -accessories: - beamer-primary: - <<: *beamer-accessory - service: beamer-primary - cmd: beamer primary --remove-after=1h --dir=/storage - port: 5000:80 - roles: - - web - - beamer-replica: - <<: *beamer-accessory - service: beamer-replica - cmd: beamer replica --primary=http://fizzy-app-101:5000/ --dir=/storage - roles: - - web-replica - volumes: - fizzy:/rails/storage @@ -44,7 +12,9 @@ proxy: ssl: true registry: - <<: *basecamp-registry + username: bcbot + password: + - BASECAMP_REGISTRY_PASSWORD builder: arch: amd64 From b37a8e3f33e7482d47e32f78ae273c0828672404 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 16:54:11 -0500 Subject: [PATCH 038/284] Hide Previously Seen section when empty with CSS --- app/assets/stylesheets/notifications.css | 8 ++++++++ app/views/notifications/index.html.erb | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index d8779c2e6..989d04773 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -94,3 +94,11 @@ } } } + +.notificiations-list--read { + display: none; + + &:has(.notification) { + display: flex; + } +} diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index bf83657ee..917d3b0b5 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -23,14 +23,13 @@ <%= render partial: "notifications/notification", collection: @unread, cached: true %> <% else %> <%# FIXME: Need to reveal this section if there are unread notification records in it %> -
+
Nothing new for you.
<% end %> -<%# FIXME: Need to hide this section if there are no notification records in it, but then it needs to appear when any are added %> -
+

Previously seen

From e16dc859e305e5a5f9df3f90968d300c72c51cb4 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 16:57:24 -0500 Subject: [PATCH 039/284] Display the empty message via CSS when there are no unread notifications --- app/assets/stylesheets/notifications.css | 6 ++++++ app/views/notifications/index.html.erb | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/notifications.css b/app/assets/stylesheets/notifications.css index 989d04773..bc95412d7 100644 --- a/app/assets/stylesheets/notifications.css +++ b/app/assets/stylesheets/notifications.css @@ -95,6 +95,12 @@ } } +.notifications__empty-message { + .notificiations-list:has(.notification) & { + display: none; + } +} + .notificiations-list--read { display: none; diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 917d3b0b5..4b76c0779 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -13,7 +13,7 @@ <% end %> -
+
<% if @unread.any? %>

New for you

@@ -22,8 +22,7 @@ <%= render partial: "notifications/notification", collection: @unread, cached: true %> <% else %> - <%# FIXME: Need to reveal this section if there are unread notification records in it %> -
+
Nothing new for you.
<% end %> From c61fa77d6bdf8839bc4070ea309e1c0d355ccd10 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 17:03:18 -0500 Subject: [PATCH 040/284] Fix failing test (we're no longer redirecting here See: #411 --- test/controllers/cards_controller_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index e794e7898..5272dc182 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -40,7 +40,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest image: fixture_file_upload("moon.jpg", "image/jpeg"), draft_comment: "Something more in-depth", tag_ids: [ tags(:mobile).id ] } } - assert_redirected_to collection_card_path(collections(:writebook), cards(:logo)) + assert_response :success card = cards(:logo).reload assert_equal "Logo needs to change", card.title From 026020c06e157eb5920c67bcea67c87c58c0c154 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 18:07:39 -0500 Subject: [PATCH 041/284] Restore drafted card state --- app/helpers/cards_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 028a4e715..31c370479 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -19,7 +19,8 @@ module CardsHelper classes = [ options.delete(:class), ("card--golden" if card.golden?), - ("card--doing" if card.doing?) + ("card--doing" if card.doing?), + ("card--drafted" if card.drafted?) ].compact.join(" ") tag.article \ From e51dc10dc5caa885db51706412b4058af4d9fc56 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 18:07:56 -0500 Subject: [PATCH 042/284] Remove draft comments --- .../_description_as_draft_comment.html.erb | 29 ------------------- app/views/cards/show.html.erb | 2 -- 2 files changed, 31 deletions(-) delete mode 100644 app/views/cards/_description_as_draft_comment.html.erb diff --git a/app/views/cards/_description_as_draft_comment.html.erb b/app/views/cards/_description_as_draft_comment.html.erb deleted file mode 100644 index 5e7a1cd71..000000000 --- a/app/views/cards/_description_as_draft_comment.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -
-
- - -
-
-
- <%= tag.house_md card.draft_comment, name: "card[draft_comment]", class: "input comment__input", - form: "card_form", - placeholder: new_comment_placeholder(card), - data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %> -
- - <% if card.creating? %> -
- <%= button_to "Create card", card_publish_path(card), class: "btn btn--reversed" %> - <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn" %> -
- <% end %> -
-
-
-
- diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index df993c968..56ca920bb 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -34,7 +34,5 @@ <% if @card.published? %> <%= render "cards/messages", card: @card %> - <% else %> - <%= render "cards/description_as_draft_comment", card: @card %> <% end %>
From 3a823cae7dd323e388c3fed85d9d0b6210f16c20 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 18:08:12 -0500 Subject: [PATCH 043/284] No more yellow highlight --- app/views/cards/display/common/_assignees.html.erb | 2 +- app/views/cards/display/perma/_tags.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index 7cfa48447..c89cdcc57 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -4,7 +4,7 @@ <% end %>
> - diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index cecde410e..46e66c56e 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -12,7 +12,7 @@
> - From bc5035c8f33606c57d74c4fc28f1db75269d0394 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 18:08:28 -0500 Subject: [PATCH 044/284] Remove shadow on drafted cards --- app/assets/stylesheets/card-columns.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 022201a26..0b193b0d5 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -96,6 +96,7 @@ border-color: var(--card-color) !important; border-style: dashed !important; + box-shadow: none !important; } .card__background { From 0f3cb03bc48c68bdd8abc20b60c2c11fa704a003 Mon Sep 17 00:00:00 2001 From: Jason Zimdars Date: Thu, 17 Apr 2025 18:09:03 -0500 Subject: [PATCH 045/284] Restore description field design clobbered in merge --- app/assets/stylesheets/card-perma.css | 32 ++++---------------- app/assets/stylesheets/cards.css | 10 +++++++ app/assets/stylesheets/comments.css | 1 + app/views/cards/_container.html.erb | 13 +++++++- app/views/cards/container/_closure.html.erb | 2 +- app/views/cards/container/_title.html.erb | 33 ++++++++++++++------- app/views/cards/edit.html.erb | 11 +++---- 7 files changed, 57 insertions(+), 45 deletions(-) diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index af825bb32..4f25c7810 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -100,7 +100,7 @@ z-index: 1; } - .btn:not(.popup__item, .btn--plain) { + .btn:not(.popup__item, .btn--plain, .btn--reversed) { --btn-background: var(--card-color); --btn-color: var(--color-ink-reversed); } @@ -110,6 +110,11 @@ text-decoration: underline; } + + .btn--reversed { + --btn-background: var(--color-bg); + --btn-color: var(--card-color); + } } .card-perma__closure-message { @@ -119,29 +124,4 @@ padding-left: 1ch; } } - - /* Variants - /* ------------------------------------------------------------------------ */ - - .card-perma--pointing { - --arrow-size: 20em; - --aspect-ratio: 7; - - position: relative; - margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio)); - - &::after { - aspect-ratio: var(--aspect-ratio); - background-color: var(--color-container); - clip-path: polygon(50% 100%, 100% 0, 0 0); - inline-size: var(--arrow-size); - content: ""; - display: block; - inset-block-start: 99%; - inset-inline-start: 50%; - position: absolute; - transform: translateX(-50%); - z-index: -1; - } - } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index d8a484ab0..4be6b06ed 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -104,6 +104,16 @@ .house-md-content { color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); } + + .house-md-content { + min-block-size: 3lh; + padding: var(--block-space-half) var(--inline-space) 0 0; + } + + & ~ .btn { + --btn-background: var(--card-color); + --btn-color: var(--color-ink-reversed); + } } .card__title { diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 2db3016db..5ac91e94e 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -68,6 +68,7 @@ padding: var(--comment-padding-block) var(--comment-padding-inline); + word-wrap: break-word; } .comment__edit { diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index a817a21af..3a1620036 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,8 +1,14 @@
" style="--card-color: <%= card.color %>;"> + class="card-perma" style="--card-color: <%= card.color %>;"> <%= render "cards/container/engagement", card: card %> <%= render "cards/container/closure", card: card %> + <% elsif card.creating? %> +
+ <%= button_to "Create card", card_publish_path(card), class: "btn" %> + <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> +
<% end %>
diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index 661b8edea..e9006f3aa 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -1,4 +1,4 @@ -
+
<% if card.closed? %>
Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index b62eb2fb4..a959e888f 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -1,14 +1,27 @@ -

- <% if card.published? %> - <%= turbo_frame_tag card, :edit do %> - <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> - <% end %> - <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> +<% if card.published? %> + <%= turbo_frame_tag card, :edit do %> +
+

+ <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %> +

+

+ <%= sanitize card.description_html %> +

+
+ <% end %> +<% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "full-width", data: { controller: "auto-save" } do |form| %> +

<%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", + class: "input input--textarea full-width borderless txt-align-start", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> - <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
+ <%= form.button "Save changes", type: :submit, class: "btn"%> <% end %> - +<% end %> \ No newline at end of file diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 9ebf63e8e..ad509b0e0 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,22 +1,19 @@ <%= turbo_frame_tag @card, :edit do %> <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "full-width", data: { controller: "form paste" } do |form| %> -

+

<%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless fill-white txt-align-start card-field__title", + <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", required: true, autofocus: true, placeholder: "Name it…", rows: 1, data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> <% end %> - <%= form.button type: :submit, class: "btn txt-small margin-block-start-half" do %> - <%= icon_tag "check" %> - Save - <% end %>

- <%= form.markdown_area :description, class: "input input--textarea full-width fill-white borderless txt-align-start card-field__description", + <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
+ <%= form.button "Save changes", type: :submit, class: "btn"%> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> From de23b4a866e1097f67092367d30b0ecf2113e7c2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 17 Apr 2025 16:07:33 +0200 Subject: [PATCH 046/284] Not needed for a single dependency --- app/views/cards/display/_preview.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 7d673308c..e1ed5b946 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -1,4 +1,4 @@ -<% cache [ card ] do %> +<% cache card do %> <%= card_article_tag card, class: "card" do %>
<%= card.collection.name %> From 7395d4e034f5729e708f6b6842a21840473b5da0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 15:14:36 +0200 Subject: [PATCH 047/284] Removing the image can also just be done as a card replacement --- app/controllers/cards/images_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/cards/images_controller.rb b/app/controllers/cards/images_controller.rb index fad419c52..f5a3f02b6 100644 --- a/app/controllers/cards/images_controller.rb +++ b/app/controllers/cards/images_controller.rb @@ -3,6 +3,6 @@ class Cards::ImagesController < ApplicationController def destroy @card.image.purge_later - redirect_to @card + render_card_replacement end end From f3000a02c5411bf2c9225a534ea8a0d4ac5ead46 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 15:20:38 +0200 Subject: [PATCH 048/284] Follow render_card_replacement pattern And clean up the rest of the implementation a tad. --- app/controllers/cards/pins_controller.rb | 24 ++++++++++++------- app/views/cards/pins/create.turbo_stream.erb | 1 - app/views/cards/pins/destroy.turbo_stream.erb | 1 - 3 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 app/views/cards/pins/create.turbo_stream.erb delete mode 100644 app/views/cards/pins/destroy.turbo_stream.erb diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index 235393178..a4b49433f 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -5,21 +5,29 @@ class Cards::PinsController < ApplicationController end def create - pin = @card.pin_by Current.user - broadcast_add_to_tray pin + @pin = @card.pin_by Current.user + + broadcast_add_pin_to_tray + render_pin_button_replacement end def destroy - pin = @card.unpin_by Current.user - broadcast_remove_from_tray pin + @pin = @card.unpin_by Current.user + + broadcast_remove_pin_from_tray + render_pin_button_replacement end private - def broadcast_add_to_tray(pin) - pin.broadcast_prepend_to [ Current.user, :pins_tray ], target: "pins", partial: "my/pins/pin" + def broadcast_add_pin_to_tray + @pin.broadcast_prepend_to [ Current.user, :pins_tray ], target: "pins", partial: "my/pins/pin" end - def broadcast_remove_from_tray(pin) - pin.broadcast_remove_to [ Current.user, :pins_tray ] + def broadcast_remove_pin_from_tray + @pin.broadcast_remove_to [ Current.user, :pins_tray ] + end + + def render_pin_button_replacement + turbo_stream.replace [ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card } end end diff --git a/app/views/cards/pins/create.turbo_stream.erb b/app/views/cards/pins/create.turbo_stream.erb deleted file mode 100644 index b1dd787e9..000000000 --- a/app/views/cards/pins/create.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= turbo_stream.replace [ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card } %> diff --git a/app/views/cards/pins/destroy.turbo_stream.erb b/app/views/cards/pins/destroy.turbo_stream.erb deleted file mode 100644 index d54dea5f1..000000000 --- a/app/views/cards/pins/destroy.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render template: "cards/pins/create" %> From 1026408736dc14d701208e5ef17b13b96528475e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 15:22:59 +0200 Subject: [PATCH 049/284] Need this for the draft header to be visible --- app/controllers/cards_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 25428545f..eacc517b6 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -27,7 +27,12 @@ class CardsController < ApplicationController def update @card.update! card_params - render_card_replacement + + if @card.published? + render_card_replacement + else + redirect_to @card + end end def destroy From fd21838f887883d85b1a29084883014182472a6f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:06:30 +0200 Subject: [PATCH 050/284] Add assert_in_body helper Coming upstream --- test/controllers/users_controller_test.rb | 6 +++--- test/test_helper.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index ff7ec0f2c..fad78128f 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -5,8 +5,8 @@ class UsersControllerTest < ActionDispatch::IntegrationTest sign_in_as :kevin get users_path - assert_match /#{users(:david).name}/, @response.body - assert_match /#{users(:kevin).name}/, @response.body + assert_in_body users(:david).name + assert_in_body users(:kevin).name end test "new" do @@ -32,7 +32,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest sign_in_as :kevin get user_path(users(:david)) - assert_match /#{users(:david).name}/, @response.body + assert_in_body users(:david).name end test "update oneself" do diff --git a/test/test_helper.rb b/test/test_helper.rb index 1447430b1..dd27bd4ca 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,5 +11,15 @@ module ActiveSupport fixtures :all include CardTestHelper, ChangeTestHelper, SessionTestHelper + + # FIXME: Remove when upstream in Rails has landed (https://github.com/rails/rails/pull/54938) + def assert_in_body(text) + assert_match /#{text}/, @response.body + end + + # FIXME: Remove when upstream in Rails has landed (https://github.com/rails/rails/pull/54938) + def assert_not_in_body(text) + assert_no_match /#{text}/, @response.body + end end end From f6cd21f7cb9dd506127ccbd28d49640853fc1c67 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:07:01 +0200 Subject: [PATCH 051/284] Extract default stages into something that will be created together with a new Workflow in the model --- app/controllers/workflows_controller.rb | 2 -- app/models/workflow.rb | 10 ++++++++++ test/controllers/workflows_controller_test.rb | 19 +++++++++++++++++++ test/models/workflow_test.rb | 8 ++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 test/controllers/workflows_controller_test.rb create mode 100644 test/models/workflow_test.rb diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 49abcd26f..f8b54254d 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -11,8 +11,6 @@ class WorkflowsController < ApplicationController def create @workflow = Workflow.create! workflow_params - # FIXME: this should definitely change. - [ "Triage", "In progress", "On Hold", "Review" ].each { |name| @workflow.stages.create! name: name } redirect_to workflows_path end diff --git a/app/models/workflow.rb b/app/models/workflow.rb index df896cf09..1812d7225 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -1,3 +1,13 @@ class Workflow < ApplicationRecord + DEFAULT_STAGES = [ "Triage", "In progress", "On Hold", "Review" ] + has_many :stages, dependent: :delete_all + + after_create_commit :create_default_stages + + private + def create_default_stages + Workflow::Stage.insert_all \ + DEFAULT_STAGES.collect { |default_stage_name| { workflow_id: id , name: default_stage_name } } + end end diff --git a/test/controllers/workflows_controller_test.rb b/test/controllers/workflows_controller_test.rb new file mode 100644 index 000000000..392f6514b --- /dev/null +++ b/test/controllers/workflows_controller_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class WorkflowsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in_as :kevin + end + + test "index" do + get workflows_path + assert_in_body workflows(:on_call).name + end + + test "create" do + assert_difference -> { Workflow.count }, +1 do + post workflows_path, params: { workflow: { name: "My new workflow!" } } + assert_redirected_to workflows_path + end + end +end diff --git a/test/models/workflow_test.rb b/test/models/workflow_test.rb new file mode 100644 index 000000000..23239b3b4 --- /dev/null +++ b/test/models/workflow_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class WorkflowTest < ActiveSupport::TestCase + test "create with default stages" do + workflow = Workflow.create name: "My New Workflow" + assert_equal Workflow::DEFAULT_STAGES.sort, workflow.stages.collect(&:name).sort + end +end From 7b0d3d033f864bba001e9e579d7a84ca3866ca02 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:08:53 +0200 Subject: [PATCH 052/284] Fix extra space --- app/models/workflow.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/workflow.rb b/app/models/workflow.rb index 1812d7225..b1e1fdb22 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -8,6 +8,6 @@ class Workflow < ApplicationRecord private def create_default_stages Workflow::Stage.insert_all \ - DEFAULT_STAGES.collect { |default_stage_name| { workflow_id: id , name: default_stage_name } } + DEFAULT_STAGES.collect { |default_stage_name| { workflow_id: id, name: default_stage_name } } end end From 3003105ac9b78bce8c74a30c5613a513c7da54a0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:16:04 +0200 Subject: [PATCH 053/284] Flesh out the rest of the controller tests --- test/controllers/workflows_controller_test.rb | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/controllers/workflows_controller_test.rb b/test/controllers/workflows_controller_test.rb index 392f6514b..8d3d64c69 100644 --- a/test/controllers/workflows_controller_test.rb +++ b/test/controllers/workflows_controller_test.rb @@ -11,9 +11,33 @@ class WorkflowsControllerTest < ActionDispatch::IntegrationTest end test "create" do + get new_workflow_path + assert_response :success + assert_difference -> { Workflow.count }, +1 do post workflows_path, params: { workflow: { name: "My new workflow!" } } assert_redirected_to workflows_path end end + + test "show" do + get workflow_path(workflows(:on_call)) + assert_in_body workflows(:on_call).name + end + + test "update" do + get edit_workflow_path(workflows(:on_call)) + assert_response :success + + put workflow_path(workflows(:on_call)), params: { workflow: { name: "Monkeyflow!" } } + follow_redirect! + assert_in_body "Monkeyflow!" + end + + test "destroy" do + assert_difference -> { Workflow.count }, -1 do + delete workflow_path(workflows(:on_call)) + assert_redirected_to workflows_path + end + end end From f594bc09a0b8cc1397835deb927b4d8e9bd6188a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:24:54 +0200 Subject: [PATCH 054/284] Simplify This is coming straight from the form, unlikely to blow up --- app/controllers/collections/workflows_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/collections/workflows_controller.rb b/app/controllers/collections/workflows_controller.rb index ae50b90f9..28b691f11 100644 --- a/app/controllers/collections/workflows_controller.rb +++ b/app/controllers/collections/workflows_controller.rb @@ -10,6 +10,6 @@ class Collections::WorkflowsController < ApplicationController private def set_workflow - @workflow = Workflow.find(params.expect(collection: [ :workflow_id ]).require(:workflow_id)) + @workflow = Workflow.find(params[:collection][:workflow_id]) end end From 70275c9f04a8bb1d8cc3fd4e47646dd0bb0eb048 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:30:30 +0200 Subject: [PATCH 055/284] Yank Solid Queue from development It clogs up the test log and occasionally causes the puma to hang when you want to quit. --- bin/dev | 2 +- config/environments/development.rb | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bin/dev b/bin/dev index 1b7f220b2..f8d3f3df9 100755 --- a/bin/dev +++ b/bin/dev @@ -2,4 +2,4 @@ echo "Starting Fizzy on http://development-tenant.fizzy.localhost:3006" -SOLID_QUEUE_IN_PUMA=true exec ./bin/rails server -p 3006 +exec ./bin/rails server -p 3006 diff --git a/config/environments/development.rb b/config/environments/development.rb index ac555fd16..7eba115d0 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,10 +62,6 @@ Rails.application.configure do # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true - # Use Solid Queue in Development. - config.active_job.queue_adapter = :solid_queue - config.solid_queue.connects_to = { database: { writing: :queue } } - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true From 9cea403acd30526f5dda98afeb4c5381c118e8f4 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:32:27 +0200 Subject: [PATCH 056/284] Make sure that any card getting read does not empty out the notifications tray Since we wont rehydrate it --- app/controllers/notifications/trays_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/notifications/trays_controller.rb b/app/controllers/notifications/trays_controller.rb index 4a01a1ee9..7c7a52bb3 100644 --- a/app/controllers/notifications/trays_controller.rb +++ b/app/controllers/notifications/trays_controller.rb @@ -1,5 +1,5 @@ class Notifications::TraysController < ApplicationController def show - @notifications = Current.user.notifications.unread.ordered.limit(20) + @notifications = Current.user.notifications.unread.ordered.limit(100) end end From c6f363c2813376aaac4d43b881d03b166e09b7df Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:42:35 +0200 Subject: [PATCH 057/284] Style --- app/helpers/workflows_helper.rb | 9 +++------ app/views/cards/stagings/_stages.html.erb | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index b78482c9f..90f839cc6 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -1,13 +1,10 @@ module WorkflowsHelper def button_to_set_stage(card, stage) - button_to card_stagings_path(card, stage_id: stage), + button_to \ + tag.span(stage.name, class: "overflow-ellipsis"), card_stagings_path(card, stage_id: stage), method: :post, class: [ "btn justify-start workflow-stage txt-uppercase workflow-stage", { "workflow-stage--current": stage == card.stage } ], form_class: "flex align-center gap-half", - data: { turbo_frame: "_top" } do - tag.span class: "overflow-ellipsis" do - stage.name - end - end + data: { turbo_frame: "_top" } end def stage_color(stage) diff --git a/app/views/cards/stagings/_stages.html.erb b/app/views/cards/stagings/_stages.html.erb index 71885bce9..4cecdf637 100644 --- a/app/views/cards/stagings/_stages.html.erb +++ b/app/views/cards/stagings/_stages.html.erb @@ -1,6 +1,4 @@ -<% workflow = card.collection.workflow %> - -<% if workflow %> +<% if workflow = card.collection.workflow %>
" class="card__stages"> <% workflow.stages.each do |stage| %> <%= button_to_set_stage card, stage %> From 406c764253f01133f5b81268e3fb87c201c46d81 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:46:20 +0200 Subject: [PATCH 058/284] Style --- app/views/cards/stagings/_stages.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cards/stagings/_stages.html.erb b/app/views/cards/stagings/_stages.html.erb index 4cecdf637..ce0bbda4a 100644 --- a/app/views/cards/stagings/_stages.html.erb +++ b/app/views/cards/stagings/_stages.html.erb @@ -1,5 +1,5 @@ <% if workflow = card.collection.workflow %> -
" class="card__stages"> +
<% workflow.stages.each do |stage| %> <%= button_to_set_stage card, stage %> <% end %> From 247524da2960a9d1797134f34b3df2ff577f8c64 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:47:53 +0200 Subject: [PATCH 059/284] Stlye --- app/helpers/workflows_helper.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index 90f839cc6..1bbed5c32 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -1,10 +1,12 @@ module WorkflowsHelper def button_to_set_stage(card, stage) button_to \ - tag.span(stage.name, class: "overflow-ellipsis"), card_stagings_path(card, stage_id: stage), - method: :post, class: [ "btn justify-start workflow-stage txt-uppercase workflow-stage", { "workflow-stage--current": stage == card.stage } ], - form_class: "flex align-center gap-half", - data: { turbo_frame: "_top" } + tag.span(stage.name, class: "overflow-ellipsis"), + card_stagings_path(card, stage_id: stage), + method: :post, + class: [ "btn justify-start workflow-stage txt-uppercase workflow-stage", { "workflow-stage--current": stage == card.stage } ], + form_class: "flex align-center gap-half", + data: { turbo_frame: "_top" } end def stage_color(stage) From 9aef965db8ea4dbf9ba717e5cf89bb70360974b0 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:49:36 +0200 Subject: [PATCH 060/284] No longer used I assume it was from the old bubble design --- .../controllers/animation_controller.js | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 app/javascript/controllers/animation_controller.js diff --git a/app/javascript/controllers/animation_controller.js b/app/javascript/controllers/animation_controller.js deleted file mode 100644 index f63d36e97..000000000 --- a/app/javascript/controllers/animation_controller.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Controller } from "@hotwired/stimulus" -import { nextFrame } from "helpers/timing_helpers" - -export default class extends Controller { - static classes = [ "play" ] - static values = { playOnLoad: { type: Boolean, default: false } } - - connect() { - if (this.playOnLoadValue) { - this.play() - } - } - - async play() { - await nextFrame() - this.element.classList.remove(this.playClass) - this.#forceReflow() - this.element.classList.add(this.playClass) - } - - #forceReflow() { - this.element.offsetWidth - } -} From c65639a3ce7c5d0a63f2853bdd5f487dcc5124b2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 16:52:55 +0200 Subject: [PATCH 061/284] No longer used Seemed to have been used for an old combobox design --- .../controllers/autofocus_controller.js | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/javascript/controllers/autofocus_controller.js diff --git a/app/javascript/controllers/autofocus_controller.js b/app/javascript/controllers/autofocus_controller.js deleted file mode 100644 index 75e5c4de9..000000000 --- a/app/javascript/controllers/autofocus_controller.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static targets = [ "element" ] - - connect() { - this.observer = new IntersectionObserver(([entry]) => { - if (entry.isIntersecting) this.elementTarget.focus() - }) - this.observer.observe(this.elementTarget) - } - - disconnect() { - this.observer.disconnect() - } -} From 3eeb121ea1cd10e3ee13e90a23270bb8747b615f Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:00:13 +0200 Subject: [PATCH 062/284] Cards are no longer resized on the basis of activity scores --- .../controllers/card_size_controller.js | 43 ------------------- .../cards/index/engagement/_doing.html.erb | 2 +- 2 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 app/javascript/controllers/card_size_controller.js diff --git a/app/javascript/controllers/card_size_controller.js b/app/javascript/controllers/card_size_controller.js deleted file mode 100644 index e688358da..000000000 --- a/app/javascript/controllers/card_size_controller.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -const SIZES = [ "one", "two", "three", "four", "five" ] - -export default class extends Controller { - static targets = [ "card" ] - - connect() { - this.resize() - } - - resize() { - const [ min, max ] = this.#getScoreRange() - - this.cardTargets.forEach(card => { - const score = this.#currentCardScore(card) - const idx = Math.round((score - min) / (max - min) * (SIZES.length - 1)) - - card.style.setProperty("--card-size", `var(--card-size-${SIZES[idx]})`) - }) - } - - #getScoreRange() { - var min = 0, max = 1; - - this.cardTargets.forEach(card => { - const score = this.#currentCardScore(card) - - min = Math.min(min, score) - max = Math.max(max, score) - }) - - return [ min, max ] - } - - #currentCardScore(el) { - const score = el.dataset.activityScore - const scoreAt = el.dataset.activityScoreAt - const daysAgo = (Date.now() / 1000 - scoreAt) / (60 * 60 * 24) - - return score / (2**daysAgo) - } -} diff --git a/app/views/cards/index/engagement/_doing.html.erb b/app/views/cards/index/engagement/_doing.html.erb index e59c70700..216866e85 100644 --- a/app/views/cards/index/engagement/_doing.html.erb +++ b/app/views/cards/index/engagement/_doing.html.erb @@ -1,4 +1,4 @@ -
+
<% if workflow = filter.single_workflow %> <%= render "cards/index/workflow_filter", workflow: workflow, filter: filter %> <% else %> From 1114679c2019f1d2908ab0b20b2fee8999b986ff Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:05:17 +0200 Subject: [PATCH 063/284] Remove activity score columns --- ...250418150259_remove_activity_from_cards.rb | 8 ++ db/schema.rb | 6 +- db/schema_cache.yml | 120 +++++------------- test/fixtures/cards.yml | 2 - 4 files changed, 42 insertions(+), 94 deletions(-) create mode 100644 db/migrate/20250418150259_remove_activity_from_cards.rb diff --git a/db/migrate/20250418150259_remove_activity_from_cards.rb b/db/migrate/20250418150259_remove_activity_from_cards.rb new file mode 100644 index 000000000..f8910ea9b --- /dev/null +++ b/db/migrate/20250418150259_remove_activity_from_cards.rb @@ -0,0 +1,8 @@ +class RemoveActivityFromCards < ActiveRecord::Migration[8.1] + def change + remove_index :cards, :activity_score_order + remove_column :cards, :activity_score_order + remove_column :cards, :activity_score_at + remove_column :cards, :activity_score + end +end diff --git a/db/schema.rb b/db/schema.rb index 06a8eb245..3a56a34ae 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.1].define(version: 2025_04_15_131804) do +ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -108,9 +108,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_15_131804) do end create_table "cards", force: :cascade do |t| - t.float "activity_score", default: 0.0, null: false - t.datetime "activity_score_at" - t.float "activity_score_order", default: 0.0, null: false t.integer "collection_id", null: false t.datetime "created_at", null: false t.integer "creator_id", null: false @@ -120,7 +117,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_15_131804) do t.text "status", default: "creating", null: false t.string "title" t.datetime "updated_at", null: false - t.index ["activity_score_order"], name: "index_cards_on_activity_score_order" t.index ["collection_id"], name: "index_cards_on_collection_id" t.index ["last_active_at", "status"], name: "index_cards_on_last_active_at_and_status" t.index ["stage_id"], name: "index_cards_on_stage_id" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index f662880fe..8152c393d 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -21,7 +21,7 @@ columns: default_function: collation: comment: - - &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: collection_id cast_type: *1 @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -374,47 +374,9 @@ columns: - *9 cards: - *5 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: activity_score - cast_type: &22 !ruby/object:ActiveModel::Type::Float - precision: - scale: - limit: - sql_type_metadata: &23 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata - sql_type: float - type: :float - limit: - precision: - scale: - 'null': false - default: 0.0 - default_function: - collation: - comment: - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: activity_score_at - cast_type: *3 - sql_type_metadata: *4 - 'null': true - default: - default_function: - collation: - comment: - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: activity_score_order - cast_type: *22 - sql_type_metadata: *23 - 'null': false - default: 0.0 - default_function: - collation: - comment: - - *24 + - *22 - *6 - - &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: creator_id cast_type: *1 @@ -427,12 +389,12 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: due_on - cast_type: &26 !ruby/object:ActiveRecord::Type::Date + cast_type: &24 !ruby/object:ActiveRecord::Type::Date precision: scale: limit: timezone: - sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &25 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: date type: :date limit: @@ -473,7 +435,7 @@ columns: default_function: collation: comment: - - &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -528,11 +490,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &33 !ruby/object:ActiveModel::Type::Boolean + cast_type: &31 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &34 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -544,7 +506,7 @@ columns: collation: comment: - *6 - - *25 + - *23 - *10 - *9 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -558,15 +520,15 @@ columns: collation: comment: collections_filters: - - *24 + - *22 - *18 comments: - *5 - *6 - - *25 + - *23 - *9 creators_filters: - - *25 + - *23 - *18 event_summaries: - *5 @@ -586,12 +548,12 @@ columns: comment: - *21 - *6 - - *25 + - *23 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: due_date - cast_type: *26 - sql_type_metadata: *27 + cast_type: *24 + sql_type_metadata: *25 'null': true default: default_function: @@ -600,11 +562,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: &28 !ruby/object:ActiveRecord::Type::Json + cast_type: &26 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &29 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -629,12 +591,12 @@ columns: filters: - *5 - *6 - - *25 + - *23 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *28 - sql_type_metadata: *29 + cast_type: *26 + sql_type_metadata: *27 'null': false default: "{}" default_function: @@ -665,7 +627,7 @@ columns: comment: filters_tags: - *18 - - &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -745,13 +707,13 @@ columns: collation: comment: - *9 - - *30 + - *28 pins: - *5 - *21 - *6 - *9 - - *30 + - *28 reactions: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -831,25 +793,25 @@ columns: default_function: collation: comment: - - *30 + - *28 taggings: - *5 - *21 - *6 - - *31 + - *29 - *9 tags: - *5 - *6 - - *32 + - *30 - *9 users: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *33 - sql_type_metadata: *34 + cast_type: *31 + sql_type_metadata: *32 'null': false default: true default_function: @@ -893,12 +855,12 @@ columns: - *21 - *6 - *9 - - *30 + - *28 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *33 - sql_type_metadata: *34 + cast_type: *31 + sql_type_metadata: *32 'null': false default: true default_function: @@ -1350,22 +1312,6 @@ indexes: nulls_not_distinct: comment: valid: true - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: cards - name: index_cards_on_activity_score_order - unique: false - columns: - - activity_score_order - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true closure_reasons: [] closures: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition @@ -1992,4 +1938,4 @@ indexes: comment: valid: true workflows: [] -version: 20250415131804 +version: 20250418150259 diff --git a/test/fixtures/cards.yml b/test/fixtures/cards.yml index f49179eb3..c8dc163a3 100644 --- a/test/fixtures/cards.yml +++ b/test/fixtures/cards.yml @@ -4,7 +4,6 @@ logo: title: The logo isn't big enough due_on: <%= 3.days.from_now %> created_at: <%= 1.week.ago %> - activity_score: 7 status: published last_active_at: <%= Time.current %> @@ -13,7 +12,6 @@ layout: creator: david title: Layout is broken created_at: <%= 1.week.ago %> - activity_score: 1 status: published last_active_at: <%= Time.current %> From 7c76bdd96d425646acea57e8d1ec1c984a78356d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:07:36 +0200 Subject: [PATCH 064/284] Drop Scorable concern --- app/models/card.rb | 13 +++---- app/models/card/scorable.rb | 64 ------------------------------- test/models/card/scorable_test.rb | 12 ------ 3 files changed, 6 insertions(+), 83 deletions(-) delete mode 100644 app/models/card/scorable.rb delete mode 100644 test/models/card/scorable_test.rb diff --git a/app/models/card.rb b/app/models/card.rb index e2191aa41..4c17269c9 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,6 +1,6 @@ class Card < ApplicationRecord include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, - Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged, + Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable belongs_to :collection, touch: true @@ -21,12 +21,11 @@ class Card < ApplicationRecord scope :indexed_by, ->(index) do case index - when "most_active" then ordered_by_activity - when "newest" then reverse_chronologically - when "oldest" then chronologically - when "latest" then latest - when "stalled" then ordered_by_staleness - when "closed" then closed + when "newest" then reverse_chronologically + when "oldest" then chronologically + when "latest" then latest + when "stalled" then chronologically + when "closed" then closed end end diff --git a/app/models/card/scorable.rb b/app/models/card/scorable.rb deleted file mode 100644 index 91b0856b5..000000000 --- a/app/models/card/scorable.rb +++ /dev/null @@ -1,64 +0,0 @@ -module Card::Scorable - extend ActiveSupport::Concern - - REFERENCE_DATE = Time.utc(2025, 1, 1) - - included do - scope :ordered_by_activity, -> { order activity_score_order: :desc } - - # Staleness is measured by the amount of activity_score lost since it was last updated. - # The factor 0.9 is chosen to make the decay curve closer to linear; an exponential - # curve would make recent items appear stale very quickly, due to the sharp dropoff. - scope :ordered_by_staleness, -> { order Arel.sql( - "coalesce(activity_score * (1 - power(0.9, julianday('now') - julianday(activity_score_at))), 0) desc" - ) } - end - - def rescore - score = calculate_activity_score - score_at = last_scorable_activity_at - - update! \ - activity_score: score, - activity_score_at: score_at, - activity_score_order: event_score_reference(score, score_at) - end - - private - def calculate_activity_score - scorable_events.sum { |event| event_score(event) } - end - - def event_score(event) - days_ago = (last_scorable_activity_at - event.created_at) / 1.day - event_weight(event) / (2**days_ago) - end - - def event_weight(event) - case - when event.comment&.first_by_author_on_card? then 20 - when event.comment&.follows_comment_by_another_author? then 15 - when event.commented? then 10 - else 0 - end - end - - def event_score_reference(score, activity_at) - # The reference score is used to make the activity score comparable - # across different cards, since it represents the card's activity - # level at a consistent point in time. - # - # We store this as log2 to tame the huge/tiny numbers we'd otherwise get - # when activity is far from the reference date. - days_diff = (activity_at - REFERENCE_DATE) / 1.day - Math.log2(1.0 + score) + days_diff - end - - def last_scorable_activity_at - scorable_events.maximum(:created_at) || created_at - end - - def scorable_events - events.where(action: :commented) - end -end diff --git a/test/models/card/scorable_test.rb b/test/models/card/scorable_test.rb deleted file mode 100644 index f24785e63..000000000 --- a/test/models/card/scorable_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require "test_helper" - -class Card::ScorableTest < ActiveSupport::TestCase - test "cards with no activity have a valid activity_score_order" do - card = Card.create! collection: collections(:writebook), creator: users(:kevin) - - card.rescore - - assert card.activity_score.zero? - assert_not card.activity_score_order.infinite? - end -end From aae6c7b1a2735da0e73802b854d7d11fb3a2fc7c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:09:55 +0200 Subject: [PATCH 065/284] Indention --- app/models/card.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index 4c17269c9..7e8a72da8 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -31,8 +31,8 @@ class Card < ApplicationRecord scope :by_engagement_status, ->(status) do case status.to_s - when "considering" then considering - when "doing" then doing.with_golden_first + when "considering" then considering + when "doing" then doing.with_golden_first end end From f368b9f9c9de6d84c519d2da0259953a19becf32 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:13:11 +0200 Subject: [PATCH 066/284] Inline anemic method --- app/models/card/eventable.rb | 4 ---- app/models/event.rb | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index c8098708b..7343258f9 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -6,10 +6,6 @@ module Card::Eventable before_create { self.last_active_at = Time.current } end - def touch_last_active_at - touch :last_active_at - end - def track_event(action, creator: Current.user, **particulars) if published? event = find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars diff --git a/app/models/event.rb b/app/models/event.rb index ad17a5e8c..fe2a60c6d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -10,7 +10,7 @@ class Event < ApplicationRecord scope :chronologically, -> { order created_at: :asc, id: :desc } - after_create -> { card.touch_last_active_at } + after_create -> { card.touch(:last_active_at) } def commented? action == "commented" From cacee0febbbdc7aba513bded3020fbbec556391b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:18:13 +0200 Subject: [PATCH 067/284] Remove anemic scope --- app/models/card.rb | 1 - app/models/filter.rb | 2 +- test/models/card_test.rb | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index 7e8a72da8..d2f3a4599 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -17,7 +17,6 @@ class Card < ApplicationRecord scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } - scope :in_collection, ->(collection) { where collection: collection } scope :indexed_by, ->(index) do case index diff --git a/app/models/filter.rb b/app/models/filter.rb index 1ec1a5e6c..b2957bfd5 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -24,7 +24,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.in_collection(collections.ids) if collections.present? + result = result.where(collection: collections.ids) if collections.present? result = result.in_stage(stages.ids) if stages.present? result = result.tagged_with(tags.ids) if tags.present? result = terms.reduce(result) do |result, term| diff --git a/test/models/card_test.rb b/test/models/card_test.rb index fd0f26bfa..49678bd0c 100644 --- a/test/models/card_test.rb +++ b/test/models/card_test.rb @@ -88,8 +88,8 @@ class CardTest < ActiveSupport::TestCase test "in collection" do new_collection = Collection.create! name: "New Collection", creator: users(:david) - assert_equal cards(:logo, :shipping, :layout, :text), Card.in_collection(collections(:writebook)) - assert_empty Card.in_collection(new_collection) + assert_equal cards(:logo, :shipping, :layout, :text), Card.where(collection: collections(:writebook)) + assert_empty Card.where(collection: new_collection) end test "tagged with" do From 78369725c42c1ec01630a17dd074ce442f3f0fed Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 18 Apr 2025 17:18:28 +0200 Subject: [PATCH 068/284] Move card concern logic into those concerns --- app/models/card.rb | 41 ----------------------------------- app/models/card/engageable.rb | 7 ++++++ app/models/card/eventable.rb | 22 +++++++++++++++++++ app/models/card/staged.rb | 11 ++++++++++ 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index d2f3a4599..9c8ce4908 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -10,10 +10,6 @@ class Card < ApplicationRecord has_one_attached :image, dependent: :purge_later - after_save :track_due_date_change, if: :saved_change_to_due_on? - after_save :track_title_change, if: :saved_change_to_title? - after_create :assign_initial_stage - scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } @@ -28,13 +24,6 @@ class Card < ApplicationRecord end end - scope :by_engagement_status, ->(status) do - case status.to_s - when "considering" then considering - when "doing" then doing.with_golden_first - end - end - def title=(new_title) self[:title] = new_title.presence || "Untitled" end @@ -42,34 +31,4 @@ class Card < ApplicationRecord def cache_key [ super, collection&.name ].compact.join("/") end - - private - def track_due_date_change - if due_on.present? - if due_on_before_last_save.nil? - track_event("due_date_added", particulars: { due_date: due_on }) - else - track_event("due_date_changed", particulars: { due_date: due_on }) - end - elsif due_on_before_last_save.present? - track_event("due_date_removed") - end - end - - def track_title_change - if title_before_last_save.present? - track_event("title_changed", particulars: { - old_title: title_before_last_save, - new_title: title - }) - end - end - - def assign_initial_stage - if workflow_stage = collection.workflow&.stages&.first - self.stage = workflow_stage - save! touch: false - track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name - end - end end diff --git a/app/models/card/engageable.rb b/app/models/card/engageable.rb index 800088f72..78dbe0ebe 100644 --- a/app/models/card/engageable.rb +++ b/app/models/card/engageable.rb @@ -9,6 +9,13 @@ module Card::Engageable scope :considering, -> { published_or_drafted_by(Current.user).active.where.missing(:engagement) } scope :doing, -> { published.active.joins(:engagement) } scope :stagnated, -> { doing.where(last_active_at: ..STAGNATED_AFTER.ago) } + + scope :by_engagement_status, ->(status) do + case status.to_s + when "considering" then considering + when "doing" then doing.with_golden_first + end + end end class_methods do diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index 7343258f9..1d3c4e13b 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -3,7 +3,11 @@ module Card::Eventable included do has_many :events, dependent: :destroy + before_create { self.last_active_at = Time.current } + + after_save :track_due_date_change, if: :saved_change_to_due_on? + after_save :track_title_change, if: :saved_change_to_title? end def track_event(action, creator: Current.user, **particulars) @@ -14,6 +18,24 @@ module Card::Eventable end private + def track_due_date_change + if due_on.present? + if due_on_before_last_save.nil? + track_event("due_date_added", particulars: { due_date: due_on }) + else + track_event("due_date_changed", particulars: { due_date: due_on }) + end + elsif due_on_before_last_save.present? + track_event("due_date_removed") + end + end + + def track_title_change + if title_before_last_save.present? + track_event "title_changed", particulars: { old_title: title_before_last_save, new_title: title } + end + end + def find_or_capture_event_summary transaction do messages.last&.event_summary || capture(EventSummary.new).event_summary diff --git a/app/models/card/staged.rb b/app/models/card/staged.rb index b669aeddd..de853b818 100644 --- a/app/models/card/staged.rb +++ b/app/models/card/staged.rb @@ -4,6 +4,8 @@ module Card::Staged included do belongs_to :stage, class_name: "Workflow::Stage", optional: true + after_create :assign_initial_stage + scope :in_stage, ->(stage) { where stage: stage } end @@ -19,4 +21,13 @@ module Card::Staged track_event event, stage_id: stage.id, stage_name: stage.name end end + + private + def assign_initial_stage + if workflow_stage = collection.workflow&.stages&.first + self.stage = workflow_stage + save! touch: false + track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name + end + end end From 883a2259463e66e1377d1b5905cda5b1d1604b34 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 19 Apr 2025 10:08:08 +0200 Subject: [PATCH 069/284] Use upstreamed assert_in_body --- Gemfile.lock | 6 +++--- test/test_helper.rb | 10 ---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8fbd0f756..533616d7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,7 +18,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: 32d96235810e268eb5335a7837b13cda3f8b653d + revision: 8b00d0985f88a3f151dd21e8dfdaf6d1c2f497b0 branch: main specs: actioncable (8.1.0.alpha) @@ -145,7 +145,7 @@ GEM xpath (~> 3.2) chunky_png (1.4.0) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.1) crass (1.0.6) date (3.4.1) debug (1.10.0) @@ -264,7 +264,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.12) + rack (3.1.13) rack-contrib (2.5.0) rack (< 4) rack-session (2.1.0) diff --git a/test/test_helper.rb b/test/test_helper.rb index dd27bd4ca..1447430b1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -11,15 +11,5 @@ module ActiveSupport fixtures :all include CardTestHelper, ChangeTestHelper, SessionTestHelper - - # FIXME: Remove when upstream in Rails has landed (https://github.com/rails/rails/pull/54938) - def assert_in_body(text) - assert_match /#{text}/, @response.body - end - - # FIXME: Remove when upstream in Rails has landed (https://github.com/rails/rails/pull/54938) - def assert_not_in_body(text) - assert_no_match /#{text}/, @response.body - end end end From ccd98f6d98609d882e692c6106e1b014d81c8077 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 19 Apr 2025 20:01:41 +0200 Subject: [PATCH 070/284] Update card.rb --- app/models/card.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/card.rb b/app/models/card.rb index 5b2e17d42..20a957a50 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,5 +1,5 @@ class Card < ApplicationRecord - include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, + include Assignable, Colored, Engageable, Eventable, Golden, Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable From d3fa62d4c2cc7e82b46bca8c349c42a20745fc87 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 20 Apr 2025 16:21:08 +0200 Subject: [PATCH 071/284] Inline touch check thats only used once Not worth loading an entire file for this. --- app/javascript/controllers/soft_keyboard_controller.js | 4 ++-- app/javascript/helpers/navigator_helpers.js | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 app/javascript/helpers/navigator_helpers.js diff --git a/app/javascript/controllers/soft_keyboard_controller.js b/app/javascript/controllers/soft_keyboard_controller.js index becc36c52..75fb4f2ee 100644 --- a/app/javascript/controllers/soft_keyboard_controller.js +++ b/app/javascript/controllers/soft_keyboard_controller.js @@ -1,10 +1,10 @@ import { Controller } from "@hotwired/stimulus" import { nextEventNamed } from "helpers/timing_helpers" -import { isTouchDevice } from "helpers/navigator_helpers" export default class extends Controller { + // Only load for touch devices static get shouldLoad() { - return isTouchDevice() + return "ontouchstart" in window && navigator.maxTouchPoints > 0 } // Use a fake input to trigger the soft keyboard on actions that load async content diff --git a/app/javascript/helpers/navigator_helpers.js b/app/javascript/helpers/navigator_helpers.js deleted file mode 100644 index b75f62875..000000000 --- a/app/javascript/helpers/navigator_helpers.js +++ /dev/null @@ -1,3 +0,0 @@ -export function isTouchDevice() { - return "ontouchstart" in window && navigator.maxTouchPoints > 0 -} From 48a6c052505ae4ab8d54357be0aa87e6d98f36e1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 20 Apr 2025 16:23:15 +0200 Subject: [PATCH 072/284] No longer used --- app/javascript/initializers/custom_stream_actions.js | 8 -------- app/javascript/initializers/index.js | 1 - lib/rails_ext/turbo_streams_action_helper.rb | 7 ------- 3 files changed, 16 deletions(-) delete mode 100644 app/javascript/initializers/custom_stream_actions.js delete mode 100644 lib/rails_ext/turbo_streams_action_helper.rb diff --git a/app/javascript/initializers/custom_stream_actions.js b/app/javascript/initializers/custom_stream_actions.js deleted file mode 100644 index fecb25ff5..000000000 --- a/app/javascript/initializers/custom_stream_actions.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Turbo } from "@hotwired/turbo-rails" - -Turbo.StreamActions.set_css_variable = function() { - const name = this.getAttribute("name") - const value = this.getAttribute("value") - - this.targetElements.forEach(element => element.style.setProperty(name, value)) -} diff --git a/app/javascript/initializers/index.js b/app/javascript/initializers/index.js index 01fdd9220..10fb36975 100644 --- a/app/javascript/initializers/index.js +++ b/app/javascript/initializers/index.js @@ -1,2 +1 @@ import "initializers/current" -import "initializers/custom_stream_actions" diff --git a/lib/rails_ext/turbo_streams_action_helper.rb b/lib/rails_ext/turbo_streams_action_helper.rb deleted file mode 100644 index 001c9706e..000000000 --- a/lib/rails_ext/turbo_streams_action_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module TurboStreamsActionsHelper - def set_css_variable(target, name:, value:) - tag.turbo_stream target: target, action: "set_css_variable", name:, value: - end -end - -Turbo::Streams::TagBuilder.prepend(TurboStreamsActionsHelper) From dc2205ee79d621b0820cf67c23cb229727c178a5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 09:18:42 +0200 Subject: [PATCH 073/284] Cards have a not-null schema and collections destroy all cards on deletion This shouldnt be necessary. Maybe remnant of some previous design? --- app/models/card.rb | 2 +- test/models/card_test.rb | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index 9c8ce4908..7237e1f58 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -29,6 +29,6 @@ class Card < ApplicationRecord end def cache_key - [ super, collection&.name ].compact.join("/") + [ super, collection.name ].compact.join("/") end end diff --git a/test/models/card_test.rb b/test/models/card_test.rb index 49678bd0c..93f13e565 100644 --- a/test/models/card_test.rb +++ b/test/models/card_test.rb @@ -120,11 +120,4 @@ class CardTest < ActiveSupport::TestCase assert_includes card.cache_key, ApplicationRecord.current_tenant, "cache key must always include the tenant" end - - test "cache key gracefully handles a nil collection" do - card = cards(:logo) - card.update_column :collection_id, Collection.last.id + 1 - - assert_nothing_raised { card.reload.cache_key } - end end From 89d1b316f083fb4da9a877df263567ea356c0574 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 09:21:55 +0200 Subject: [PATCH 074/284] Name is already schema and UI required And we are not using these validations for display anywhere. --- app/models/collection.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/collection.rb b/app/models/collection.rb index 319b2bb7d..5f4a75051 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -7,8 +7,6 @@ class Collection < ApplicationRecord has_many :cards, dependent: :destroy has_many :tags, -> { distinct }, through: :cards - validates_presence_of :name - after_save :update_cards_workflow, if: :saved_change_to_workflow_id? scope :alphabetically, -> { order("lower(name)") } From 3e1a2bcc7e468b6e731c6f7e8fb3fdd3e8794f76 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 09:24:57 +0200 Subject: [PATCH 075/284] Use morphing to replace the card and exclude turbo frames from being replaced Reconnecting the frames result in a visible flickering. This also prevents flickering in the avatars when replacing the card container. --- app/controllers/concerns/card_scoped.rb | 2 +- app/views/cards/_container.html.erb | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/card_scoped.rb b/app/controllers/concerns/card_scoped.rb index 2ea4752c4..5438871c1 100644 --- a/app/controllers/concerns/card_scoped.rb +++ b/app/controllers/concerns/card_scoped.rb @@ -15,6 +15,6 @@ module CardScoped end def render_card_replacement - render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload }) + render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) end end diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index a817a21af..9c2d11aaf 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -32,10 +32,9 @@
<% if card.published? %> - <%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> <%= render "cards/container/closure", card: card %> From 4b294ccaa6cc3ac07d2a7cad84cb09c0bdbfb3b8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 13:55:09 +0200 Subject: [PATCH 076/284] No need to run Solid Cache in development --- config/environments/development.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 7eba115d0..6a7ef9df3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -23,7 +23,7 @@ Rails.application.configure do config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true - config.cache_store = :solid_cache_store + config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } From 167563d9f82720a6a70f3d7d6cddb29035afd370 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 14:01:53 +0200 Subject: [PATCH 077/284] Stlye --- config/environments/development.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 6a7ef9df3..d2aae6589 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,9 +24,7 @@ Rails.application.configure do config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store - config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{2.days.to_i}" - } + config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = true From 491e743c27e37ee9f99d64e9b75d29b730eec490 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 14:48:07 +0200 Subject: [PATCH 078/284] Place below associations --- app/models/card.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/card.rb b/app/models/card.rb index 20a957a50..fab514df6 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -3,8 +3,6 @@ class Card < ApplicationRecord Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable - has_markdown :description - belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } @@ -12,6 +10,8 @@ class Card < ApplicationRecord has_one_attached :image, dependent: :purge_later + has_markdown :description + scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } From 7925cd7bf969059d6cf5e41baf77ffa44978fb14 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 14:52:21 +0200 Subject: [PATCH 079/284] Extract partials --- app/views/cards/_container.html.erb | 20 +++---------------- app/views/cards/container/_edit.html.erb | 4 ++++ .../cards/container/footer/_draft.html.erb | 4 ++++ .../container/footer/_published.html.erb | 7 +++++++ 4 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 app/views/cards/container/_edit.html.erb create mode 100644 app/views/cards/container/footer/_draft.html.erb create mode 100644 app/views/cards/container/footer/_published.html.erb diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 3a1620036..034106e59 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -3,12 +3,7 @@ <%= render "cards/container/engagement", card: card %>
<% if card.published? %> - <%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> - - - <%= render "cards/container/closure", card: card %> + <%= render "cards/container/footer/published", card: card %> <% elsif card.creating? %> -
- <%= button_to "Create card", card_publish_path(card), class: "btn" %> - <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> -
+ <%= render "cards/container/footer/draft", card: card %> <% end %>
diff --git a/app/views/cards/container/_edit.html.erb b/app/views/cards/container/_edit.html.erb new file mode 100644 index 000000000..23ea9f48d --- /dev/null +++ b/app/views/cards/container/_edit.html.erb @@ -0,0 +1,4 @@ +<%= link_to edit_collection_card_path(card.collection, card), class: "btn", data: { turbo_frame: dom_id(card, :edit) } do %> + <%= icon_tag "pencil" %> + Edit +<% end %> diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb new file mode 100644 index 000000000..f46e08b41 --- /dev/null +++ b/app/views/cards/container/footer/_draft.html.erb @@ -0,0 +1,4 @@ +
+ <%= button_to "Create card", card_publish_path(card), class: "btn" %> + <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> +
diff --git a/app/views/cards/container/footer/_published.html.erb b/app/views/cards/container/footer/_published.html.erb new file mode 100644 index 000000000..c4e0a0cdf --- /dev/null +++ b/app/views/cards/container/footer/_published.html.erb @@ -0,0 +1,7 @@ +<%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> + + +<%= render "cards/container/closure", card: card %> From b6584ed65540b01d61750a14c71caa40b94c800a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:12:57 +0200 Subject: [PATCH 080/284] Seed multiple seed accounts with basic DSL --- db/seeds.rb | 6 ++++++ db/seeds/37signals.rb | 21 +++++++++++++++++++++ db/seeds/helpers.rb | 33 +++++++++++++++++++++++++++++++++ db/seeds/honcho.rb | 22 ++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 db/seeds.rb create mode 100644 db/seeds/37signals.rb create mode 100644 db/seeds/helpers.rb create mode 100644 db/seeds/honcho.rb diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 000000000..065346716 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,6 @@ +require_relative "seeds/helpers" +raise "Seeding is just for development" unless Rails.env.development? + +# Seed accounts +seed_account "37signals" +seed_account "honcho" diff --git a/db/seeds/37signals.rb b/db/seeds/37signals.rb new file mode 100644 index 000000000..432030bbc --- /dev/null +++ b/db/seeds/37signals.rb @@ -0,0 +1,21 @@ +create_tenant "37signals" + +david = create_first_run "David Heinemeier Hansson", "david@37signals.com" +login_as david + +jz = create_user "Jason Zimdars", "jz@37signals.com" +kevin = create_user "Kevin Mcconnell", "kevin@37signals.com" + +create_collection("Fizzy", access_to: [ jz, kevin ]).tap do |fizzy| + create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy) + + create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy).tap do |card| + card.toggle_assignment(kevin) + card.engage + end + + create_card("Plain text mentions", description: "We'll support plain text mentions first.", collection: fizzy).tap do |card| + card.toggle_assignment(david) + card.close + end +end diff --git a/db/seeds/helpers.rb b/db/seeds/helpers.rb new file mode 100644 index 000000000..ecd1184b3 --- /dev/null +++ b/db/seeds/helpers.rb @@ -0,0 +1,33 @@ +def seed_account(name) + print " #{name}…" + elapsed = Benchmark.realtime { require_relative name } + puts " #{elapsed.round(2)} sec" +end + +def create_tenant(name) + ApplicationRecord.destroy_tenant name + ApplicationRecord.create_tenant name + ApplicationRecord.current_tenant = name +end + +def create_first_run(name, email_address, password: "secret123456") + FirstRun.create!(name:, email_address:, password:) +end + +def login_as(user) + Current.session = user.sessions.create +end + +def create_user(name, email_address, password: "secret123456") + User.create!(name:, email_address:, password:) +end + +def create_collection(name, creator: Current.user, all_access: true, access_to: []) + Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } +end + +def create_card(title, collection:, description: nil, status: :published, creator: Current.user) + collection.cards.create!(title:, creator:, status:).tap do |card| + card.capture(Comment.new(body: description)) if description + end +end diff --git a/db/seeds/honcho.rb b/db/seeds/honcho.rb new file mode 100644 index 000000000..5a4a3a2e3 --- /dev/null +++ b/db/seeds/honcho.rb @@ -0,0 +1,22 @@ +create_tenant "honcho" + +david = create_first_run "David Heinemeier Hansson", "david@37signals.com" +login_as david + +jz = create_user "Jason Zimdars", "jz@37signals.com" +kevin = create_user "Kevin Mcconnell", "kevin@37signals.com" + +create_collection("Mucho", access_to: [ jz, kevin ]).tap do |fizzy| + create_card("Prepare MUCHO sign-up page", description: "We need to do this before the launch.", collection: fizzy) + + create_card("Prepare MUCHO sign-up page", description: "We need to do this before the launch.", collection: fizzy).tap do |card| + card.toggle_assignment(kevin) + card.engage + end + + create_card("Plain MUCHO text mentions", description: "We'll support plain text mentions first.", collection: fizzy).tap do |card| + card.toggle_assignment(david) + card.close + end +end + From ffdec1cc3b015f0f40225c6f1f877b64bd023abb Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 15:18:11 +0200 Subject: [PATCH 081/284] Index description too --- app/models/card/searchable.rb | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index 3627a555b..c61ee1017 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -4,7 +4,7 @@ module Card::Searchable included do include ::Searchable - searchable_by :title, using: :cards_search_index + searchable_by :title_and_description, using: :cards_search_index, as: :title scope :mentioning, ->(query) do if query = sanitize_query_syntax(query) @@ -18,25 +18,31 @@ module Card::Searchable end end - class_methods do - def sanitize_query_syntax(terms) - terms = terms.to_s - terms = remove_invalid_search_characters(terms) - terms = remove_unbalanced_quotes(terms) - terms.presence + class_methods do + def sanitize_query_syntax(terms) + terms = terms.to_s + terms = remove_invalid_search_characters(terms) + terms = remove_unbalanced_quotes(terms) + terms.presence + end + + private + def remove_invalid_search_characters(terms) + terms.gsub(/[^\w"]/, " ") end - private - def remove_invalid_search_characters(terms) - terms.gsub(/[^\w"]/, " ") + def remove_unbalanced_quotes(terms) + if terms.count("\"").even? + terms + else + terms.gsub("\"", " ") end + end + end - def remove_unbalanced_quotes(terms) - if terms.count("\"").even? - terms - else - terms.gsub("\"", " ") - end - end + private + # TODO: Temporary until we stabilize the search API + def title_and_description + [title, description.to_plain_text].join(" ") end end From 99dbc04e2c9d16052d84e73351005b7a489f9a4c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 15:20:03 +0200 Subject: [PATCH 082/284] Format --- app/models/card/searchable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index c61ee1017..2e01fef9b 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -43,6 +43,6 @@ module Card::Searchable private # TODO: Temporary until we stabilize the search API def title_and_description - [title, description.to_plain_text].join(" ") + [ title, description.to_plain_text ].join(" ") end end From 2bfead03810d97d486847291507dd90df7167dc6 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:23:08 +0200 Subject: [PATCH 083/284] Convert to conventional Rails style --- bin/setup | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/bin/setup b/bin/setup index e50c91ec6..ad27b6637 100755 --- a/bin/setup +++ b/bin/setup @@ -1,26 +1,31 @@ -#!/usr/bin/env bash -set -eo pipefail +#!/usr/bin/env ruby +require "fileutils" -# Use application binstubs -export PATH="./bin:$PATH" +APP_ROOT = File.expand_path("..", __dir__) -announce() { - printf "\n--- $@\n" -} +def system!(*args) + system(*args, exception: true) +end -announce "Installing dependencies" -gem install bundler --conservative -bundle check || bundle install +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. -announce "Preparing database" -if [[ $* == *--reset* ]]; then - rails db:reset -else - rails db:prepare -fi + puts "== Installing dependencies ==" + system("gem install bundler --conservative") + system("bundle check") || system!("bundle install") -announce "Removing old logs and tempfiles" -rails log:clear tmp:clear + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") -echo -echo "Start developing with bin/dev" + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end From dc127511925ad2db485694dfdef20cef91b6b793 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:23:18 +0200 Subject: [PATCH 084/284] Have an empty first-run tenant too --- db/seeds.rb | 1 + db/seeds/first-run.rb | 1 + 2 files changed, 2 insertions(+) create mode 100644 db/seeds/first-run.rb diff --git a/db/seeds.rb b/db/seeds.rb index 065346716..1f1b2322d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -4,3 +4,4 @@ raise "Seeding is just for development" unless Rails.env.development? # Seed accounts seed_account "37signals" seed_account "honcho" +seed_account "first-run" diff --git a/db/seeds/first-run.rb b/db/seeds/first-run.rb new file mode 100644 index 000000000..e6df219d9 --- /dev/null +++ b/db/seeds/first-run.rb @@ -0,0 +1 @@ +create_tenant "first-run" From 29948d8881184867eadb6ae2cd92ebd6ce753810 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:23:31 +0200 Subject: [PATCH 085/284] Enumerate all the options for logging in --- bin/dev | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/dev b/bin/dev index f8d3f3df9..e58bc97c3 100755 --- a/bin/dev +++ b/bin/dev @@ -1,5 +1,7 @@ #!/usr/bin/env sh -echo "Starting Fizzy on http://development-tenant.fizzy.localhost:3006" +echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.localhost:3006" +echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006" +echo "Access first run on http://first-run.fizzy.localhost:3006" exec ./bin/rails server -p 3006 From e1b2b623e8869a307b4c4fea5d6c28cad11e7b3d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:26:19 +0200 Subject: [PATCH 086/284] --skip-server in ci setup --- config/ci.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/ci.rb b/config/ci.rb index 1a22e3e33..c94990581 100644 --- a/config/ci.rb +++ b/config/ci.rb @@ -1,7 +1,7 @@ # Run using bin/ci CI.run do - step "Setup", "bin/setup" + step "Setup", "bin/setup --skip-server" step "Style: Ruby", "bin/rubocop" From 581070b22cb205c4b3f8c318456a4e760866740c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 15:26:47 +0200 Subject: [PATCH 087/284] Fix trailing empty line --- db/seeds/honcho.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/db/seeds/honcho.rb b/db/seeds/honcho.rb index 5a4a3a2e3..918b3a4d5 100644 --- a/db/seeds/honcho.rb +++ b/db/seeds/honcho.rb @@ -19,4 +19,3 @@ create_collection("Mucho", access_to: [ jz, kevin ]).tap do |fizzy| card.close end end - From 539b26c7df50c932d63870d036512891884bc474 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 16:01:01 +0200 Subject: [PATCH 088/284] Small enough DSL to inline it --- db/seeds.rb | 36 +++++++++++++++++++++++++++++++++++- db/seeds/helpers.rb | 33 --------------------------------- 2 files changed, 35 insertions(+), 34 deletions(-) delete mode 100644 db/seeds/helpers.rb diff --git a/db/seeds.rb b/db/seeds.rb index 1f1b2322d..65a417e5a 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,40 @@ -require_relative "seeds/helpers" raise "Seeding is just for development" unless Rails.env.development? +# Seed DSL +def seed_account(name) + print " #{name}…" + elapsed = Benchmark.realtime { require_relative name } + puts " #{elapsed.round(2)} sec" +end + +def create_tenant(name) + ApplicationRecord.destroy_tenant name + ApplicationRecord.create_tenant name + ApplicationRecord.current_tenant = name +end + +def create_first_run(name, email_address, password: "secret123456") + FirstRun.create!(name:, email_address:, password:) +end + +def login_as(user) + Current.session = user.sessions.create +end + +def create_user(name, email_address, password: "secret123456") + User.create!(name:, email_address:, password:) +end + +def create_collection(name, creator: Current.user, all_access: true, access_to: []) + Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } +end + +def create_card(title, collection:, description: nil, status: :published, creator: Current.user) + collection.cards.create!(title:, creator:, status:).tap do |card| + card.capture(Comment.new(body: description)) if description + end +end + # Seed accounts seed_account "37signals" seed_account "honcho" diff --git a/db/seeds/helpers.rb b/db/seeds/helpers.rb deleted file mode 100644 index ecd1184b3..000000000 --- a/db/seeds/helpers.rb +++ /dev/null @@ -1,33 +0,0 @@ -def seed_account(name) - print " #{name}…" - elapsed = Benchmark.realtime { require_relative name } - puts " #{elapsed.round(2)} sec" -end - -def create_tenant(name) - ApplicationRecord.destroy_tenant name - ApplicationRecord.create_tenant name - ApplicationRecord.current_tenant = name -end - -def create_first_run(name, email_address, password: "secret123456") - FirstRun.create!(name:, email_address:, password:) -end - -def login_as(user) - Current.session = user.sessions.create -end - -def create_user(name, email_address, password: "secret123456") - User.create!(name:, email_address:, password:) -end - -def create_collection(name, creator: Current.user, all_access: true, access_to: []) - Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) } -end - -def create_card(title, collection:, description: nil, status: :published, creator: Current.user) - collection.cards.create!(title:, creator:, status:).tap do |card| - card.capture(Comment.new(body: description)) if description - end -end From 4d1a1382e1a427022dd49ca9a101b08547483782 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 21 Apr 2025 16:21:20 +0200 Subject: [PATCH 089/284] Fix seeds for new descriptions --- db/seeds.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 65a417e5a..9af00fe66 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -3,7 +3,7 @@ raise "Seeding is just for development" unless Rails.env.development? # Seed DSL def seed_account(name) print " #{name}…" - elapsed = Benchmark.realtime { require_relative name } + elapsed = Benchmark.realtime { require_relative "seeds/#{name}" } puts " #{elapsed.round(2)} sec" end @@ -30,9 +30,7 @@ def create_collection(name, creator: Current.user, all_access: true, access_to: end def create_card(title, collection:, description: nil, status: :published, creator: Current.user) - collection.cards.create!(title:, creator:, status:).tap do |card| - card.capture(Comment.new(body: description)) if description - end + collection.cards.create!(title:, description:, creator:, status:) end # Seed accounts From a4cc143b09a2c8426d500d051027e9162b6d6fd8 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 16:41:33 +0200 Subject: [PATCH 090/284] Revert "Add a description field for cards" --- app/assets/stylesheets/card-columns.css | 1 - app/assets/stylesheets/card-perma.css | 32 ++++++++++++--- app/assets/stylesheets/cards.css | 34 +++------------- app/assets/stylesheets/comments.css | 1 - app/assets/stylesheets/house.css | 3 +- app/controllers/cards_controller.rb | 2 +- app/helpers/cards_helper.rb | 3 +- app/models/card.rb | 4 +- app/models/card/draft_commenting.rb | 33 +++++++++++++++ app/models/card/searchable.rb | 40 ++++++++----------- app/views/cards/_container.html.erb | 12 +++--- .../_description_as_draft_comment.html.erb | 29 ++++++++++++++ app/views/cards/container/_closure.html.erb | 2 +- app/views/cards/container/_edit.html.erb | 4 -- app/views/cards/container/_title.html.erb | 33 +++++---------- .../cards/container/footer/_draft.html.erb | 4 -- .../container/footer/_published.html.erb | 7 ---- app/views/cards/display/_preview.html.erb | 4 +- .../cards/display/common/_assignees.html.erb | 2 +- app/views/cards/display/perma/_tags.html.erb | 2 +- app/views/cards/edit.html.erb | 24 +++++------ app/views/cards/show.html.erb | 2 + test/controllers/cards_controller_test.rb | 6 ++- test/models/card/messages_test.rb | 37 +++++++++++++++++ 24 files changed, 190 insertions(+), 131 deletions(-) create mode 100644 app/models/card/draft_commenting.rb create mode 100644 app/views/cards/_description_as_draft_comment.html.erb delete mode 100644 app/views/cards/container/_edit.html.erb delete mode 100644 app/views/cards/container/footer/_draft.html.erb delete mode 100644 app/views/cards/container/footer/_published.html.erb diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 0b193b0d5..022201a26 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -96,7 +96,6 @@ border-color: var(--card-color) !important; border-style: dashed !important; - box-shadow: none !important; } .card__background { diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 4f25c7810..af825bb32 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -100,7 +100,7 @@ z-index: 1; } - .btn:not(.popup__item, .btn--plain, .btn--reversed) { + .btn:not(.popup__item, .btn--plain) { --btn-background: var(--card-color); --btn-color: var(--color-ink-reversed); } @@ -110,11 +110,6 @@ text-decoration: underline; } - - .btn--reversed { - --btn-background: var(--color-bg); - --btn-color: var(--card-color); - } } .card-perma__closure-message { @@ -124,4 +119,29 @@ padding-left: 1ch; } } + + /* Variants + /* ------------------------------------------------------------------------ */ + + .card-perma--pointing { + --arrow-size: 20em; + --aspect-ratio: 7; + + position: relative; + margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio)); + + &::after { + aspect-ratio: var(--aspect-ratio); + background-color: var(--color-container); + clip-path: polygon(50% 100%, 100% 0, 0 0); + inline-size: var(--arrow-size); + content: ""; + display: block; + inset-block-start: 99%; + inset-inline-start: 50%; + position: absolute; + transform: translateX(-50%); + z-index: -1; + } + } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 4be6b06ed..6f869df5c 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -99,42 +99,18 @@ padding-block: var(--card-padding-block); } - .card__description { - house-md-toolbar, - .house-md-content { - color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - } - - .house-md-content { - min-block-size: 3lh; - padding: var(--block-space-half) var(--inline-space) 0 0; - } - - & ~ .btn { - --btn-background: var(--card-color); - --btn-color: var(--color-ink-reversed); - } - } - .card__title { --hover-size: 0; --input-border-radius: 0; - --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); - --lines: 3; - color: inherit; flex: 2 1 75%; font-size: var(--text-xx-large); font-weight: 900; - line-height: 1.2; + line-height: 1.1; min-block-size: 3lh; text-wrap: balance; - .card-perma__bg & { - min-block-size: unset; - } - - .card-field__title { + .card__title-field { &:is(textarea)::placeholder { color: inherit; opacity: 0.5; @@ -142,6 +118,8 @@ } .card__title-link { + --lines: 3; + color: inherit; } } @@ -152,11 +130,11 @@ flex-direction: column; max-width: 100%; min-width: 0; - padding-block: var(--block-space-half); + padding-block: var(--block-space); } .card__move-button { - --btn-background: color-mix(in srgb, inherit 40%, var(--color-ink)); + --btn-background: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); margin-block-start: calc(var(--block-space) * 1.2); margin-inline-end: calc(var(--inline-space) * -4); diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 5ac91e94e..2db3016db 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -68,7 +68,6 @@ padding: var(--comment-padding-block) var(--comment-padding-inline); - word-wrap: break-word; } .comment__edit { diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css index e1ce84325..30982ad76 100644 --- a/app/assets/stylesheets/house.css +++ b/app/assets/stylesheets/house.css @@ -58,6 +58,7 @@ /* Markdown Content */ .house-md-content { + caret-color: var(--color-link); flex-grow: 1; font-family: var(--font-sans); text-align: left; @@ -66,7 +67,7 @@ &.house-md-content-empty::before { content: attr(placeholder); - color: currentColor; + color: var(--color-ink); opacity: 0.5; pointer-events: none; display: block; diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index d535f26d9..eacc517b6 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -54,7 +54,7 @@ class CardsController < ApplicationController end def card_params - params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ]) + params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) end def render_card_replacement diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 31c370479..028a4e715 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -19,8 +19,7 @@ module CardsHelper classes = [ options.delete(:class), ("card--golden" if card.golden?), - ("card--doing" if card.doing?), - ("card--drafted" if card.drafted?) + ("card--doing" if card.doing?) ].compact.join(" ") tag.article \ diff --git a/app/models/card.rb b/app/models/card.rb index 541d761a9..7237e1f58 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,5 +1,5 @@ class Card < ApplicationRecord - include Assignable, Colored, Engageable, Eventable, Golden, + include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable @@ -10,8 +10,6 @@ class Card < ApplicationRecord has_one_attached :image, dependent: :purge_later - has_markdown :description - scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } diff --git a/app/models/card/draft_commenting.rb b/app/models/card/draft_commenting.rb new file mode 100644 index 000000000..615d23916 --- /dev/null +++ b/app/models/card/draft_commenting.rb @@ -0,0 +1,33 @@ +module Card::DraftCommenting + extend ActiveSupport::Concern + + included do + after_save :capture_draft_comment + end + + def draft_comment + find_or_build_initial_comment.body.content + end + + def draft_comment=(body) + if body.present? + @draft_comment = body + else + messages.comments.destroy_all + end + end + + private + def find_or_build_initial_comment + message = messages.comments.first || messages.new(messageable: Comment.new) + message.comment + end + + def capture_draft_comment + if @draft_comment.present? + find_or_build_initial_comment.update! body: @draft_comment, creator: creator + end + + @draft_comment = nil + end +end diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index 2e01fef9b..3627a555b 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -4,7 +4,7 @@ module Card::Searchable included do include ::Searchable - searchable_by :title_and_description, using: :cards_search_index, as: :title + searchable_by :title, using: :cards_search_index scope :mentioning, ->(query) do if query = sanitize_query_syntax(query) @@ -18,31 +18,25 @@ module Card::Searchable end end - class_methods do - def sanitize_query_syntax(terms) - terms = terms.to_s - terms = remove_invalid_search_characters(terms) - terms = remove_unbalanced_quotes(terms) - terms.presence - end - - private - def remove_invalid_search_characters(terms) - terms.gsub(/[^\w"]/, " ") + class_methods do + def sanitize_query_syntax(terms) + terms = terms.to_s + terms = remove_invalid_search_characters(terms) + terms = remove_unbalanced_quotes(terms) + terms.presence end - def remove_unbalanced_quotes(terms) - if terms.count("\"").even? - terms - else - terms.gsub("\"", " ") + private + def remove_invalid_search_characters(terms) + terms.gsub(/[^\w"]/, " ") end - end - end - private - # TODO: Temporary until we stabilize the search API - def title_and_description - [ title, description.to_plain_text ].join(" ") + def remove_unbalanced_quotes(terms) + if terms.count("\"").even? + terms + else + terms.gsub("\"", " ") + end + end end end diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 034106e59..9c2d11aaf 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,9 +1,8 @@
+ class="card-perma <%= "card-perma--pointing" unless card.published? %>" style="--card-color: <%= card.color %>;"> <%= render "cards/container/engagement", card: card %>
<% if card.published? %> - <%= render "cards/container/footer/published", card: card %> - <% elsif card.creating? %> - <%= render "cards/container/footer/draft", card: card %> + + + <%= render "cards/container/closure", card: card %> <% end %>
diff --git a/app/views/cards/_description_as_draft_comment.html.erb b/app/views/cards/_description_as_draft_comment.html.erb new file mode 100644 index 000000000..5e7a1cd71 --- /dev/null +++ b/app/views/cards/_description_as_draft_comment.html.erb @@ -0,0 +1,29 @@ +
+
+ + +
+
+
+ <%= tag.house_md card.draft_comment, name: "card[draft_comment]", class: "input comment__input", + form: "card_form", + placeholder: new_comment_placeholder(card), + data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %> +
+ + <% if card.creating? %> +
+ <%= button_to "Create card", card_publish_path(card), class: "btn btn--reversed" %> + <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn" %> +
+ <% end %> +
+
+
+
+ diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index e9006f3aa..661b8edea 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -1,4 +1,4 @@ -
+
<% if card.closed? %>
Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. diff --git a/app/views/cards/container/_edit.html.erb b/app/views/cards/container/_edit.html.erb deleted file mode 100644 index 23ea9f48d..000000000 --- a/app/views/cards/container/_edit.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<%= link_to edit_collection_card_path(card.collection, card), class: "btn", data: { turbo_frame: dom_id(card, :edit) } do %> - <%= icon_tag "pencil" %> - Edit -<% end %> diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index a959e888f..b62eb2fb4 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -1,27 +1,14 @@ -<% if card.published? %> - <%= turbo_frame_tag card, :edit do %> -
-

- <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %> -

-

- <%= sanitize card.description_html %> -

-
- <% end %> -<% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "full-width", data: { controller: "auto-save" } do |form| %> -

+

+ <% if card.published? %> + <%= turbo_frame_tag card, :edit do %> + <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> + <% end %> + <% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> <%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start", + class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> -

-
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> -
- <%= form.button "Save changes", type: :submit, class: "btn"%> + <% end %> <% end %> -<% end %> \ No newline at end of file + diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb deleted file mode 100644 index f46e08b41..000000000 --- a/app/views/cards/container/footer/_draft.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -
- <%= button_to "Create card", card_publish_path(card), class: "btn" %> - <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> -
diff --git a/app/views/cards/container/footer/_published.html.erb b/app/views/cards/container/footer/_published.html.erb deleted file mode 100644 index c4e0a0cdf..000000000 --- a/app/views/cards/container/footer/_published.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> - - -<%= render "cards/container/closure", card: card %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 43284b534..e1ed5b946 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -6,8 +6,8 @@
-

- <%= card.title %> +

+ <%= card.title %>

<%= link_to collection_card_path(card.collection, card), class: "card__link" do %> diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index c89cdcc57..7cfa48447 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -4,7 +4,7 @@ <% end %>
> - diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index 46e66c56e..cecde410e 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -12,7 +12,7 @@
> - diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index ad509b0e0..5e5e0d1d9 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,19 +1,13 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "full-width", data: { controller: "form paste" } do |form| %> -

- <%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", - required: true, autofocus: true, placeholder: "Name it…", - rows: 1, - data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> - <% end %> -

-
- <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", - placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> -
- <%= form.button "Save changes", type: :submit, class: "btn"%> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %> + <%= form.label :title, class: "flex flex-column align-center" do %> + <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card__title-field", + required: true, autofocus: true, placeholder: "Name it…", + rows: 3, + data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" }, + style: "color: var(--spat-color); --input-border-radius: 0" %> + <% end %> + <%= form.button "Save", type: :submit, hidden: true %> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index 56ca920bb..df993c968 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -34,5 +34,7 @@ <% if @card.published? %> <%= render "cards/messages", card: @card %> + <% else %> + <%= render "cards/description_as_draft_comment", card: @card %> <% end %>
diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 5cb3a6582..5272dc182 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -36,17 +36,19 @@ class CardsControllerTest < ActionDispatch::IntegrationTest patch collection_card_path(collections(:writebook), cards(:logo)), params: { card: { title: "Logo needs to change", + due_on: 1.week.from_now, image: fixture_file_upload("moon.jpg", "image/jpeg"), - description: "Something more in-depth", + draft_comment: "Something more in-depth", tag_ids: [ tags(:mobile).id ] } } assert_response :success card = cards(:logo).reload assert_equal "Logo needs to change", card.title + assert_equal 1.week.from_now.to_date, card.due_on assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.description_plain_text.strip + assert_equal "Something more in-depth", card.messages.comments.first.comment.body_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/models/card/messages_test.rb b/test/models/card/messages_test.rb index 31d730306..08ade7f0f 100644 --- a/test/models/card/messages_test.rb +++ b/test/models/card/messages_test.rb @@ -6,4 +6,41 @@ class Card::MessagesTest < ActiveSupport::TestCase assert_empty card.messages end + + test "creating a card with an initial draft comment" do + card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", + draft_comment: "This is a comment" + + assert_equal 1, card.messages.count + assert_equal "This is a comment", card.draft_comment.strip + end + + test "updating the draft comment" do + card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", + draft_comment: "This is a comment" + + card.update! draft_comment: "This is an updated comment" + + assert_equal 1, card.messages.count + assert_equal "This is an updated comment", card.draft_comment.strip + end + + test "setting the draft comment to be blank removes it" do + card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", + draft_comment: "This is a comment" + + card.update! draft_comment: " " + + assert card.messages.first.nil? + end + + test "omitting the draft comment does not remove it" do + card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", + draft_comment: "This is a comment" + + card.update! title: "Newer" + + assert_equal 1, card.messages.count + assert_equal "This is a comment", card.draft_comment.strip + end end From b0aae3ad9e182717b3aef33aaf89bfd583ad3159 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 16:45:24 +0200 Subject: [PATCH 091/284] Reapply "Add a description field for cards" This reverts commit a4cc143b09a2c8426d500d051027e9162b6d6fd8. --- app/assets/stylesheets/card-columns.css | 1 + app/assets/stylesheets/card-perma.css | 32 +++------------ app/assets/stylesheets/cards.css | 34 +++++++++++++--- app/assets/stylesheets/comments.css | 1 + app/assets/stylesheets/house.css | 3 +- app/controllers/cards_controller.rb | 2 +- app/helpers/cards_helper.rb | 3 +- app/models/card.rb | 4 +- app/models/card/draft_commenting.rb | 33 --------------- app/models/card/searchable.rb | 40 +++++++++++-------- app/views/cards/_container.html.erb | 12 +++--- .../_description_as_draft_comment.html.erb | 29 -------------- app/views/cards/container/_closure.html.erb | 2 +- app/views/cards/container/_edit.html.erb | 4 ++ app/views/cards/container/_title.html.erb | 33 ++++++++++----- .../cards/container/footer/_draft.html.erb | 4 ++ .../container/footer/_published.html.erb | 7 ++++ app/views/cards/display/_preview.html.erb | 4 +- .../cards/display/common/_assignees.html.erb | 2 +- app/views/cards/display/perma/_tags.html.erb | 2 +- app/views/cards/edit.html.erb | 24 ++++++----- app/views/cards/show.html.erb | 2 - test/controllers/cards_controller_test.rb | 6 +-- test/models/card/messages_test.rb | 37 ----------------- 24 files changed, 131 insertions(+), 190 deletions(-) delete mode 100644 app/models/card/draft_commenting.rb delete mode 100644 app/views/cards/_description_as_draft_comment.html.erb create mode 100644 app/views/cards/container/_edit.html.erb create mode 100644 app/views/cards/container/footer/_draft.html.erb create mode 100644 app/views/cards/container/footer/_published.html.erb diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 022201a26..0b193b0d5 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -96,6 +96,7 @@ border-color: var(--card-color) !important; border-style: dashed !important; + box-shadow: none !important; } .card__background { diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index af825bb32..4f25c7810 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -100,7 +100,7 @@ z-index: 1; } - .btn:not(.popup__item, .btn--plain) { + .btn:not(.popup__item, .btn--plain, .btn--reversed) { --btn-background: var(--card-color); --btn-color: var(--color-ink-reversed); } @@ -110,6 +110,11 @@ text-decoration: underline; } + + .btn--reversed { + --btn-background: var(--color-bg); + --btn-color: var(--card-color); + } } .card-perma__closure-message { @@ -119,29 +124,4 @@ padding-left: 1ch; } } - - /* Variants - /* ------------------------------------------------------------------------ */ - - .card-perma--pointing { - --arrow-size: 20em; - --aspect-ratio: 7; - - position: relative; - margin-block-end: calc(var(--arrow-size) / var(--aspect-ratio)); - - &::after { - aspect-ratio: var(--aspect-ratio); - background-color: var(--color-container); - clip-path: polygon(50% 100%, 100% 0, 0 0); - inline-size: var(--arrow-size); - content: ""; - display: block; - inset-block-start: 99%; - inset-inline-start: 50%; - position: absolute; - transform: translateX(-50%); - z-index: -1; - } - } } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index 6f869df5c..4be6b06ed 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -99,18 +99,42 @@ padding-block: var(--card-padding-block); } + .card__description { + house-md-toolbar, + .house-md-content { + color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + } + + .house-md-content { + min-block-size: 3lh; + padding: var(--block-space-half) var(--inline-space) 0 0; + } + + & ~ .btn { + --btn-background: var(--card-color); + --btn-color: var(--color-ink-reversed); + } + } + .card__title { --hover-size: 0; --input-border-radius: 0; + --input-color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --lines: 3; + color: inherit; flex: 2 1 75%; font-size: var(--text-xx-large); font-weight: 900; - line-height: 1.1; + line-height: 1.2; min-block-size: 3lh; text-wrap: balance; - .card__title-field { + .card-perma__bg & { + min-block-size: unset; + } + + .card-field__title { &:is(textarea)::placeholder { color: inherit; opacity: 0.5; @@ -118,8 +142,6 @@ } .card__title-link { - --lines: 3; - color: inherit; } } @@ -130,11 +152,11 @@ flex-direction: column; max-width: 100%; min-width: 0; - padding-block: var(--block-space); + padding-block: var(--block-space-half); } .card__move-button { - --btn-background: color-mix(in srgb, var(--card-color) 40%, var(--color-ink)); + --btn-background: color-mix(in srgb, inherit 40%, var(--color-ink)); margin-block-start: calc(var(--block-space) * 1.2); margin-inline-end: calc(var(--inline-space) * -4); diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index 2db3016db..5ac91e94e 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -68,6 +68,7 @@ padding: var(--comment-padding-block) var(--comment-padding-inline); + word-wrap: break-word; } .comment__edit { diff --git a/app/assets/stylesheets/house.css b/app/assets/stylesheets/house.css index 30982ad76..e1ce84325 100644 --- a/app/assets/stylesheets/house.css +++ b/app/assets/stylesheets/house.css @@ -58,7 +58,6 @@ /* Markdown Content */ .house-md-content { - caret-color: var(--color-link); flex-grow: 1; font-family: var(--font-sans); text-align: left; @@ -67,7 +66,7 @@ &.house-md-content-empty::before { content: attr(placeholder); - color: var(--color-ink); + color: currentColor; opacity: 0.5; pointer-events: none; display: block; diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index eacc517b6..d535f26d9 100644 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -54,7 +54,7 @@ class CardsController < ApplicationController end def card_params - params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ]) + params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ]) end def render_card_replacement diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 028a4e715..31c370479 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -19,7 +19,8 @@ module CardsHelper classes = [ options.delete(:class), ("card--golden" if card.golden?), - ("card--doing" if card.doing?) + ("card--doing" if card.doing?), + ("card--drafted" if card.drafted?) ].compact.join(" ") tag.article \ diff --git a/app/models/card.rb b/app/models/card.rb index 7237e1f58..541d761a9 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,5 +1,5 @@ class Card < ApplicationRecord - include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden, + include Assignable, Colored, Engageable, Eventable, Golden, Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable @@ -10,6 +10,8 @@ class Card < ApplicationRecord has_one_attached :image, dependent: :purge_later + has_markdown :description + scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } diff --git a/app/models/card/draft_commenting.rb b/app/models/card/draft_commenting.rb deleted file mode 100644 index 615d23916..000000000 --- a/app/models/card/draft_commenting.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Card::DraftCommenting - extend ActiveSupport::Concern - - included do - after_save :capture_draft_comment - end - - def draft_comment - find_or_build_initial_comment.body.content - end - - def draft_comment=(body) - if body.present? - @draft_comment = body - else - messages.comments.destroy_all - end - end - - private - def find_or_build_initial_comment - message = messages.comments.first || messages.new(messageable: Comment.new) - message.comment - end - - def capture_draft_comment - if @draft_comment.present? - find_or_build_initial_comment.update! body: @draft_comment, creator: creator - end - - @draft_comment = nil - end -end diff --git a/app/models/card/searchable.rb b/app/models/card/searchable.rb index 3627a555b..2e01fef9b 100644 --- a/app/models/card/searchable.rb +++ b/app/models/card/searchable.rb @@ -4,7 +4,7 @@ module Card::Searchable included do include ::Searchable - searchable_by :title, using: :cards_search_index + searchable_by :title_and_description, using: :cards_search_index, as: :title scope :mentioning, ->(query) do if query = sanitize_query_syntax(query) @@ -18,25 +18,31 @@ module Card::Searchable end end - class_methods do - def sanitize_query_syntax(terms) - terms = terms.to_s - terms = remove_invalid_search_characters(terms) - terms = remove_unbalanced_quotes(terms) - terms.presence + class_methods do + def sanitize_query_syntax(terms) + terms = terms.to_s + terms = remove_invalid_search_characters(terms) + terms = remove_unbalanced_quotes(terms) + terms.presence + end + + private + def remove_invalid_search_characters(terms) + terms.gsub(/[^\w"]/, " ") end - private - def remove_invalid_search_characters(terms) - terms.gsub(/[^\w"]/, " ") + def remove_unbalanced_quotes(terms) + if terms.count("\"").even? + terms + else + terms.gsub("\"", " ") end + end + end - def remove_unbalanced_quotes(terms) - if terms.count("\"").even? - terms - else - terms.gsub("\"", " ") - end - end + private + # TODO: Temporary until we stabilize the search API + def title_and_description + [ title, description.to_plain_text ].join(" ") end end diff --git a/app/views/cards/_container.html.erb b/app/views/cards/_container.html.erb index 9c2d11aaf..034106e59 100644 --- a/app/views/cards/_container.html.erb +++ b/app/views/cards/_container.html.erb @@ -1,8 +1,9 @@
" style="--card-color: <%= card.color %>;"> + class="card-perma" style="--card-color: <%= card.color %>;"> <%= render "cards/container/engagement", card: card %>
<% if card.published? %> - - - <%= render "cards/container/closure", card: card %> + <%= render "cards/container/footer/published", card: card %> + <% elsif card.creating? %> + <%= render "cards/container/footer/draft", card: card %> <% end %>
diff --git a/app/views/cards/_description_as_draft_comment.html.erb b/app/views/cards/_description_as_draft_comment.html.erb deleted file mode 100644 index 5e7a1cd71..000000000 --- a/app/views/cards/_description_as_draft_comment.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -
-
- - -
-
-
- <%= tag.house_md card.draft_comment, name: "card[draft_comment]", class: "input comment__input", - form: "card_form", - placeholder: new_comment_placeholder(card), - data: { action: "house-md:change->outlet-auto-save#change focusout->outlet-auto-save#submit", uploads_url: uploads_url(format: "json") } %> -
- - <% if card.creating? %> -
- <%= button_to "Create card", card_publish_path(card), class: "btn btn--reversed" %> - <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn" %> -
- <% end %> -
-
-
-
- diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index 661b8edea..e9006f3aa 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -1,4 +1,4 @@ -
+
<% if card.closed? %>
Completed by <%= card.closed_by.name %> on <%= local_datetime_tag(card.closed_at, style: :shortdate) %>. diff --git a/app/views/cards/container/_edit.html.erb b/app/views/cards/container/_edit.html.erb new file mode 100644 index 000000000..23ea9f48d --- /dev/null +++ b/app/views/cards/container/_edit.html.erb @@ -0,0 +1,4 @@ +<%= link_to edit_collection_card_path(card.collection, card), class: "btn", data: { turbo_frame: dom_id(card, :edit) } do %> + <%= icon_tag "pencil" %> + Edit +<% end %> diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index b62eb2fb4..a959e888f 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -1,14 +1,27 @@ -

- <% if card.published? %> - <%= turbo_frame_tag card, :edit do %> - <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link overflow-line-clamp" %> - <% end %> - <% else %> - <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", data: { controller: "auto-save" } do |form| %> +<% if card.published? %> + <%= turbo_frame_tag card, :edit do %> +
+

+ <%= link_to card.title, edit_collection_card_path(card.collection, card), class: "card__title-link" %> +

+

+ <%= sanitize card.description_html %> +

+
+ <% end %> +<% else %> + <%= form_with model: card, url: collection_card_path(card.collection, card), id: "card_form", class: "full-width", data: { controller: "auto-save" } do |form| %> +

<%= form.text_area :title, placeholder: "Name it…", - class: "input input--textarea full-width borderless txt-align-start #{ "fill-highlight" if card.creating? }", + class: "input input--textarea full-width borderless txt-align-start", autofocus: card.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %> - <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
+ <%= form.button "Save changes", type: :submit, class: "btn"%> <% end %> - +<% end %> \ No newline at end of file diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb new file mode 100644 index 000000000..f46e08b41 --- /dev/null +++ b/app/views/cards/container/footer/_draft.html.erb @@ -0,0 +1,4 @@ +
+ <%= button_to "Create card", card_publish_path(card), class: "btn" %> + <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %> +
diff --git a/app/views/cards/container/footer/_published.html.erb b/app/views/cards/container/footer/_published.html.erb new file mode 100644 index 000000000..c4e0a0cdf --- /dev/null +++ b/app/views/cards/container/footer/_published.html.erb @@ -0,0 +1,7 @@ +<%# FIXME: Let's move this aside outside of the card container section so these frames don't reload/flicker when card is replaced %> + + +<%= render "cards/container/closure", card: card %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index e1ed5b946..43284b534 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -6,8 +6,8 @@
-

- <%= card.title %> +

+ <%= card.title %>

<%= link_to collection_card_path(card.collection, card), class: "card__link" do %> diff --git a/app/views/cards/display/common/_assignees.html.erb b/app/views/cards/display/common/_assignees.html.erb index 7cfa48447..c89cdcc57 100644 --- a/app/views/cards/display/common/_assignees.html.erb +++ b/app/views/cards/display/common/_assignees.html.erb @@ -4,7 +4,7 @@ <% end %>
> - diff --git a/app/views/cards/display/perma/_tags.html.erb b/app/views/cards/display/perma/_tags.html.erb index cecde410e..46e66c56e 100644 --- a/app/views/cards/display/perma/_tags.html.erb +++ b/app/views/cards/display/perma/_tags.html.erb @@ -12,7 +12,7 @@
> - diff --git a/app/views/cards/edit.html.erb b/app/views/cards/edit.html.erb index 5e5e0d1d9..ad509b0e0 100644 --- a/app/views/cards/edit.html.erb +++ b/app/views/cards/edit.html.erb @@ -1,13 +1,19 @@ <%= turbo_frame_tag @card, :edit do %> - <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %> - <%= form.label :title, class: "flex flex-column align-center" do %> - <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card__title-field", - required: true, autofocus: true, placeholder: "Name it…", - rows: 3, - data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" }, - style: "color: var(--spat-color); --input-border-radius: 0" %> - <% end %> - <%= form.button "Save", type: :submit, hidden: true %> + <%= form_with model: @card, url: collection_card_path(@card.collection, @card), class: "full-width", data: { controller: "form paste" } do |form| %> +

+ <%= form.label :title, class: "flex flex-column align-center" do %> + <%= form.text_area :title, class: "input input--textarea full-width borderless txt-align-start card-field__title", + required: true, autofocus: true, placeholder: "Name it…", + rows: 1, + data: { action: "keydown.enter->form#submit:prevent keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" } %> + <% end %> +

+
+ <%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", + placeholder: "Add some notes, context, pictures, or video about this…", + data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> +
+ <%= form.button "Save changes", type: :submit, class: "btn"%> <%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %> <% end %> <% end %> diff --git a/app/views/cards/show.html.erb b/app/views/cards/show.html.erb index df993c968..56ca920bb 100644 --- a/app/views/cards/show.html.erb +++ b/app/views/cards/show.html.erb @@ -34,7 +34,5 @@ <% if @card.published? %> <%= render "cards/messages", card: @card %> - <% else %> - <%= render "cards/description_as_draft_comment", card: @card %> <% end %>
diff --git a/test/controllers/cards_controller_test.rb b/test/controllers/cards_controller_test.rb index 5272dc182..5cb3a6582 100644 --- a/test/controllers/cards_controller_test.rb +++ b/test/controllers/cards_controller_test.rb @@ -36,19 +36,17 @@ class CardsControllerTest < ActionDispatch::IntegrationTest patch collection_card_path(collections(:writebook), cards(:logo)), params: { card: { title: "Logo needs to change", - due_on: 1.week.from_now, image: fixture_file_upload("moon.jpg", "image/jpeg"), - draft_comment: "Something more in-depth", + description: "Something more in-depth", tag_ids: [ tags(:mobile).id ] } } assert_response :success card = cards(:logo).reload assert_equal "Logo needs to change", card.title - assert_equal 1.week.from_now.to_date, card.due_on assert_equal "moon.jpg", card.image.filename.to_s assert_equal [ tags(:mobile) ], card.tags - assert_equal "Something more in-depth", card.messages.comments.first.comment.body_plain_text.strip + assert_equal "Something more in-depth", card.description_plain_text.strip end test "users can only see cards in collections they have access to" do diff --git a/test/models/card/messages_test.rb b/test/models/card/messages_test.rb index 08ade7f0f..31d730306 100644 --- a/test/models/card/messages_test.rb +++ b/test/models/card/messages_test.rb @@ -6,41 +6,4 @@ class Card::MessagesTest < ActiveSupport::TestCase assert_empty card.messages end - - test "creating a card with an initial draft comment" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - assert_equal 1, card.messages.count - assert_equal "This is a comment", card.draft_comment.strip - end - - test "updating the draft comment" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! draft_comment: "This is an updated comment" - - assert_equal 1, card.messages.count - assert_equal "This is an updated comment", card.draft_comment.strip - end - - test "setting the draft comment to be blank removes it" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! draft_comment: " " - - assert card.messages.first.nil? - end - - test "omitting the draft comment does not remove it" do - card = collections(:writebook).cards.create! creator: users(:kevin), title: "New", - draft_comment: "This is a comment" - - card.update! title: "Newer" - - assert_equal 1, card.messages.count - assert_equal "This is a comment", card.draft_comment.strip - end end From 16800952662c6d7526afbc74b35278ffff4833bd Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 16:45:35 +0200 Subject: [PATCH 092/284] Remove invalid " --- app/views/cards/comments/edit.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cards/comments/edit.html.erb b/app/views/cards/comments/edit.html.erb index e3a2e783d..cfe22b4c0 100644 --- a/app/views/cards/comments/edit.html.erb +++ b/app/views/cards/comments/edit.html.erb @@ -9,7 +9,7 @@ Save changes <% end %> <%= link_to card_comment_path(@card, @comment), class: "btn", data: { form_target: "cancel" } do %> - Cancel + Cancel <% end %> <%= tag.button type: :submit, class: "btn btn--negative flex-item-justify-end", form: dom_id(@comment, :delete_form), From 2c3ec3e8062f7d485c835e55a2257f31cb643743 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 17:00:25 +0200 Subject: [PATCH 093/284] Fix: Create new cards wasn't working --- app/views/cards/container/_title.html.erb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index a959e888f..e180c4d56 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -20,8 +20,7 @@
<%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + data: { action: "house-md:change->auto-save#change blur->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
- <%= form.button "Save changes", type: :submit, class: "btn"%> <% end %> -<% end %> \ No newline at end of file +<% end %> From 4f0cf0f5cb4293cb60e26d8ccdc6ccf1086f442c Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 21 Apr 2025 17:05:35 +0200 Subject: [PATCH 094/284] Fix autosave working with the new description field --- app/views/cards/container/_title.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/cards/container/_title.html.erb b/app/views/cards/container/_title.html.erb index e180c4d56..f3ab76788 100644 --- a/app/views/cards/container/_title.html.erb +++ b/app/views/cards/container/_title.html.erb @@ -20,7 +20,7 @@
<%= form.markdown_area :description, class: "input input--textarea full-width borderless txt-align-start card-field__description", placeholder: "Add some notes, context, pictures, or video about this…", - data: { action: "house-md:change->auto-save#change blur->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %> + data: { action: "house-md:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop paste->paste#pasteFiles" } %>
<% end %> <% end %> From 01d741b3f493ecec4e03a8fa03a32e868bf9bfab Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Mon, 21 Apr 2025 13:03:49 -0400 Subject: [PATCH 095/284] dep: update rails --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 533616d7c..37b14083c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,7 +18,7 @@ GIT GIT remote: https://github.com/rails/rails.git - revision: 8b00d0985f88a3f151dd21e8dfdaf6d1c2f497b0 + revision: e9ee790d694298b8a48d44cd089934e6c8bc7597 branch: main specs: actioncable (8.1.0.alpha) @@ -373,7 +373,7 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.6) + stringio (3.1.7) thor (1.3.2) thruster (0.1.12) thruster (0.1.12-aarch64-linux) From 682da733da03489410f4cef5759184111f26bf77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 04:58:30 +0000 Subject: [PATCH 096/284] Bump nokogiri from 1.18.7 to 1.18.8 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.18.7 to 1.18.8. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.7...v1.18.8) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.18.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 37b14083c..a9c459db0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,24 +223,24 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.4) - nokogiri (1.18.7) + nokogiri (1.18.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.7-aarch64-linux-gnu) + nokogiri (1.18.8-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-aarch64-linux-musl) + nokogiri (1.18.8-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-gnu) + nokogiri (1.18.8-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-arm-linux-musl) + nokogiri (1.18.8-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.7-arm64-darwin) + nokogiri (1.18.8-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-darwin) + nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-gnu) + nokogiri (1.18.8-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.7-x86_64-linux-musl) + nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) ostruct (0.6.1) parallel (1.26.3) From 58c7eb41bb47bb1ad4cc130fe6fb53f2cc146fda Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 22 Apr 2025 09:55:59 +0200 Subject: [PATCH 097/284] AI slop for the honcho seed data --- db/seeds/honcho.rb | 74 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/db/seeds/honcho.rb b/db/seeds/honcho.rb index 918b3a4d5..9132177ab 100644 --- a/db/seeds/honcho.rb +++ b/db/seeds/honcho.rb @@ -3,19 +3,73 @@ create_tenant "honcho" david = create_first_run "David Heinemeier Hansson", "david@37signals.com" login_as david -jz = create_user "Jason Zimdars", "jz@37signals.com" +jason = create_user "Jason Zimdars", "jz@37signals.com" kevin = create_user "Kevin Mcconnell", "kevin@37signals.com" +sarah = create_user "Sarah Johnson", "sarah@37signals.com" +mike = create_user "Mike Peterson", "mike@37signals.com" -create_collection("Mucho", access_to: [ jz, kevin ]).tap do |fizzy| - create_card("Prepare MUCHO sign-up page", description: "We need to do this before the launch.", collection: fizzy) +# Array of authors for random assignment +authors = [david, jason, kevin, sarah, mike] - create_card("Prepare MUCHO sign-up page", description: "We need to do this before the launch.", collection: fizzy).tap do |card| - card.toggle_assignment(kevin) - card.engage - end +# Card titles for reuse across collections +card_titles = [ + "Implement authentication", + "Design landing page", + "Set up database", + "Create API endpoints", + "Write unit tests", + "Optimize performance", + "Add user profiles", + "Implement search", + "Create admin panel", + "Set up CI/CD", + "Design logo", + "Create documentation", + "Add payment system", + "Implement notifications", + "Set up analytics", + "Create mobile layout", + "Add social sharing", + "Implement caching", + "Set up monitoring", + "Create error handling" +] - create_card("Plain MUCHO text mentions", description: "We'll support plain text mentions first.", collection: fizzy).tap do |card| - card.toggle_assignment(david) - card.close +# Create 10 collections +collections = [ + "Project Launch", + "Frontend Dev", + "Backend Dev", + "Design System", + "Testing Suite", + "Performance", + "User Experience", + "API Development", + "DevOps", + "Documentation" +] + +collections.each_with_index do |collection_name, index| + create_collection(collection_name, access_to: authors.sample(3)).tap do |collection| + # Create 20 unique cards for each collection + card_titles.each do |title| + create_card(title, + description: "#{title} for #{collection_name} phase #{index + 1}.", + collection: collection + ).tap do |card| + # Randomly assign to 1-2 authors + card.toggle_assignment(authors.sample) + card.toggle_assignment(authors.sample) if rand > 0.5 + + # Randomly set card state + case rand(3) + when 0 + card.engage + when 1 + card.close + # 2 remains open + end + end + end end end From 9b9acf36ae22b357f0977ddeabb4f6e946640905 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 22 Apr 2025 11:21:09 +0200 Subject: [PATCH 098/284] Events track due_date changes in particulars, not as a direct attribute --- ...50422091602_remove_due_date_from_events.rb | 5 ++ db/schema.rb | 3 +- db/schema_cache.yml | 54 ++++++++----------- 3 files changed, 28 insertions(+), 34 deletions(-) create mode 100644 db/migrate/20250422091602_remove_due_date_from_events.rb diff --git a/db/migrate/20250422091602_remove_due_date_from_events.rb b/db/migrate/20250422091602_remove_due_date_from_events.rb new file mode 100644 index 000000000..5a636e3dd --- /dev/null +++ b/db/migrate/20250422091602_remove_due_date_from_events.rb @@ -0,0 +1,5 @@ +class RemoveDueDateFromEvents < ActiveRecord::Migration[8.1] + def change + remove_column :events, :due_date + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a56a34ae..e1bea16a2 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.1].define(version: 2025_04_18_150259) do +ActiveRecord::Schema[8.1].define(version: 2025_04_22_091602) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -180,7 +180,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do t.integer "card_id", null: false t.datetime "created_at", null: false t.integer "creator_id", null: false - t.date "due_date" t.json "particulars", default: {} t.integer "summary_id", null: false t.datetime "updated_at", null: false diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 8152c393d..5d78e6883 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -80,7 +80,7 @@ columns: default_function: collation: comment: - - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &26 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: user_id cast_type: *1 @@ -389,12 +389,12 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: due_on - cast_type: &24 !ruby/object:ActiveRecord::Type::Date + cast_type: !ruby/object:ActiveRecord::Type::Date precision: scale: limit: timezone: - sql_type_metadata: &25 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: date type: :date limit: @@ -435,7 +435,7 @@ columns: default_function: collation: comment: - - &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: title cast_type: *7 @@ -490,11 +490,11 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: all_access - cast_type: &31 !ruby/object:ActiveModel::Type::Boolean + cast_type: &29 !ruby/object:ActiveModel::Type::Boolean precision: scale: limit: - sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &30 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: boolean type: :boolean limit: @@ -549,24 +549,14 @@ columns: - *21 - *6 - *23 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: due_date - cast_type: *24 - sql_type_metadata: *25 - 'null': true - default: - default_function: - collation: - comment: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: particulars - cast_type: &26 !ruby/object:ActiveRecord::Type::Json + cast_type: &24 !ruby/object:ActiveRecord::Type::Json precision: scale: limit: - sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type_metadata: &25 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata sql_type: json type: :json limit: @@ -595,8 +585,8 @@ columns: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: fields - cast_type: *26 - sql_type_metadata: *27 + cast_type: *24 + sql_type_metadata: *25 'null': false default: "{}" default_function: @@ -627,7 +617,7 @@ columns: comment: filters_tags: - *18 - - &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + - &27 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: tag_id cast_type: *1 @@ -707,13 +697,13 @@ columns: collation: comment: - *9 - - *28 + - *26 pins: - *5 - *21 - *6 - *9 - - *28 + - *26 reactions: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column @@ -793,25 +783,25 @@ columns: default_function: collation: comment: - - *28 + - *26 taggings: - *5 - *21 - *6 - - *29 + - *27 - *9 tags: - *5 - *6 - - *30 + - *28 - *9 users: - *5 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: active - cast_type: *31 - sql_type_metadata: *32 + cast_type: *29 + sql_type_metadata: *30 'null': false default: true default_function: @@ -855,12 +845,12 @@ columns: - *21 - *6 - *9 - - *28 + - *26 - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: watching - cast_type: *31 - sql_type_metadata: *32 + cast_type: *29 + sql_type_metadata: *30 'null': false default: true default_function: @@ -1938,4 +1928,4 @@ indexes: comment: valid: true workflows: [] -version: 20250418150259 +version: 20250422091602 From aa3acfeaa84158f192daf5e4969171e48055a2cf Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Tue, 22 Apr 2025 11:21:30 +0200 Subject: [PATCH 099/284] Add plain text mentions --- app/controllers/cards/readings_controller.rb | 2 +- app/helpers/notifications_helper.rb | 45 ++--- app/jobs/generate_notifications_job.rb | 7 - app/jobs/mention/collect_job.rb | 7 + app/jobs/notify_recipients_job.rb | 7 + app/models/card.rb | 7 +- app/models/card/eventable.rb | 3 +- app/models/card/mentions.rb | 12 ++ app/models/concerns/mentions.rb | 33 ++++ app/models/concerns/notifiable.rb | 13 +- app/models/event.rb | 22 ++- app/models/mention.rb | 7 + app/models/message.rb | 4 + app/models/message/mentions.rb | 12 ++ app/models/notification.rb | 5 +- app/models/notifier.rb | 33 ++-- app/models/notifier/assigned.rb | 6 - app/models/notifier/closed.rb | 2 - app/models/notifier/commented.rb | 6 - app/models/notifier/events/assigned.rb | 6 + app/models/notifier/events/base.rb | 12 ++ app/models/notifier/events/closed.rb | 2 + app/models/notifier/events/commented.rb | 6 + app/models/notifier/{ => events}/published.rb | 2 +- app/models/user.rb | 8 +- app/models/user/mentionable.rb | 20 ++ app/models/user/named.rb | 19 ++ .../notifications/_notification.html.erb | 10 +- .../notification/_event.html.erb | 7 + .../notification/_mention.html.erb | 11 ++ db/migrate/20250421120008_create_mentions.rb | 11 ++ ...50422054950_add_source_to_notifications.rb | 11 ++ ...emove_unused_columns_from_notifications.rb | 6 + db/schema.rb | 25 ++- db/schema_cache.yml | 178 +++++++++++++----- test/fixtures/notifications.yml | 9 +- test/models/concerns/mentions_test.rb | 23 +++ test/models/notifier_test.rb | 16 +- test/models/user/mentionable_test.rb | 7 + test/models/user/named_test.rb | 38 ++++ test/test_helper.rb | 1 + 41 files changed, 493 insertions(+), 168 deletions(-) delete mode 100644 app/jobs/generate_notifications_job.rb create mode 100644 app/jobs/mention/collect_job.rb create mode 100644 app/jobs/notify_recipients_job.rb create mode 100644 app/models/card/mentions.rb create mode 100644 app/models/concerns/mentions.rb create mode 100644 app/models/mention.rb create mode 100644 app/models/message/mentions.rb delete mode 100644 app/models/notifier/assigned.rb delete mode 100644 app/models/notifier/closed.rb delete mode 100644 app/models/notifier/commented.rb create mode 100644 app/models/notifier/events/assigned.rb create mode 100644 app/models/notifier/events/base.rb create mode 100644 app/models/notifier/events/closed.rb create mode 100644 app/models/notifier/events/commented.rb rename app/models/notifier/{ => events}/published.rb (67%) create mode 100644 app/models/user/mentionable.rb create mode 100644 app/models/user/named.rb create mode 100644 app/views/notifications/notification/_event.html.erb create mode 100644 app/views/notifications/notification/_mention.html.erb create mode 100644 db/migrate/20250421120008_create_mentions.rb create mode 100644 db/migrate/20250422054950_add_source_to_notifications.rb create mode 100644 db/migrate/20250422062930_remove_unused_columns_from_notifications.rb create mode 100644 test/models/concerns/mentions_test.rb create mode 100644 test/models/user/mentionable_test.rb create mode 100644 test/models/user/named_test.rb diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb index 2d8b51ac6..efd06e601 100644 --- a/app/controllers/cards/readings_controller.rb +++ b/app/controllers/cards/readings_controller.rb @@ -2,7 +2,7 @@ class Cards::ReadingsController < ApplicationController include CardScoped def create - @notifications = Current.user.notifications.where(card: @card) + @notifications = Current.user.notifications.where(source: @card.events) @notifications.each(&:read) end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 656edad54..f1cd88a6c 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,21 +1,25 @@ module NotificationsHelper - def notification_title(notification) - if notification.resource.is_a? Comment - "RE: " + notification.card.title - elsif notification_event_action(notification) == "assigned" - "Assigned to you: " + notification.card.title + def event_notification_title(event) + if event.commented? + "RE: " + event.card.title + elsif event.assignment? + "Assigned to you: " + event.card.title else - notification.card.title + event.card.title end end - def notification_body(notification) - name = notification.creator.name + def event_notification_body(event) + name = event.creator.name - case notification_event_action(notification) - when "closed" then "Closed by #{name}" - when "published" then "Added by #{name}" - else name + if event.closed? + "Closed by #{name}" + elsif event.published? + "Added by #{name}" + elsif event.commented? + "#{strip_tags(event.comment.body_html).blank? ? "#{name} replied" : "#{event.creator.name}:" } #{strip_tags(event.comment.body_html).truncate(200)}" + else + name end end @@ -36,8 +40,8 @@ module NotificationsHelper class: "notification__unread_indicator btn borderless", title: "Mark as read", data: { turbo_frame: "_top" } do - concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true })) - concat(tag.span("Mark as read", class: "for-screen-reader")) + concat(image_tag("remove-med.svg", class: "unread_icon", size: 12, aria: { hidden: true })) + concat(tag.span("Mark as read", class: "for-screen-reader")) end end @@ -46,17 +50,4 @@ module NotificationsHelper tag.div id: "next_page", data: { controller: "fetch-on-visible", fetch_on_visible_url_value: notifications_path(page: @page.next_param) } end end - - private - def notification_event_action(notification) - if notification_is_for_initial_assignement?(notification) - "assigned" - else - notification.event.action - end - end - - def notification_is_for_initial_assignement?(notification) - notification.event.action == "published" && notification.card.assigned_to?(notification.user) - end end diff --git a/app/jobs/generate_notifications_job.rb b/app/jobs/generate_notifications_job.rb deleted file mode 100644 index e386b7b37..000000000 --- a/app/jobs/generate_notifications_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -class GenerateNotificationsJob < ApplicationJob - queue_as :default - - def perform(event) - event.generate_notifications - end -end diff --git a/app/jobs/mention/collect_job.rb b/app/jobs/mention/collect_job.rb new file mode 100644 index 000000000..088e240dd --- /dev/null +++ b/app/jobs/mention/collect_job.rb @@ -0,0 +1,7 @@ +class Mention::CollectJob < ApplicationJob + queue_as :default + + def perform(record, mentioner:) + record.collect_mentions(mentioner:) + end +end diff --git a/app/jobs/notify_recipients_job.rb b/app/jobs/notify_recipients_job.rb new file mode 100644 index 000000000..2f14c2e96 --- /dev/null +++ b/app/jobs/notify_recipients_job.rb @@ -0,0 +1,7 @@ +class NotifyRecipientsJob < ApplicationJob + queue_as :default + + def perform(notifiable) + notifiable.notify_recipients + end +end diff --git a/app/models/card.rb b/app/models/card.rb index 541d761a9..57ff542da 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,13 +1,10 @@ class Card < ApplicationRecord - include Assignable, Colored, Engageable, Eventable, Golden, - Messages, Notifiable, Pinnable, Closeable, Searchable, Staged, - Statuses, Taggable, Watchable + include Assignable, Colored, Engageable, Eventable, Golden, Mentions, + Messages, Pinnable, Closeable, Searchable, Staged, Statuses, Taggable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :notifications, dependent: :destroy - has_one_attached :image, dependent: :purge_later has_markdown :description diff --git a/app/models/card/eventable.rb b/app/models/card/eventable.rb index 1d3c4e13b..55a0245ea 100644 --- a/app/models/card/eventable.rb +++ b/app/models/card/eventable.rb @@ -12,8 +12,7 @@ module Card::Eventable def track_event(action, creator: Current.user, **particulars) if published? - event = find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars - event.generate_notifications_later + find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars end end diff --git a/app/models/card/mentions.rb b/app/models/card/mentions.rb new file mode 100644 index 000000000..dd135f24c --- /dev/null +++ b/app/models/card/mentions.rb @@ -0,0 +1,12 @@ +module Card::Mentions + extend ActiveSupport::Concern + + included do + include ::Mentions + end + + private + def mentionable_content + description.to_plain_text + end +end diff --git a/app/models/concerns/mentions.rb b/app/models/concerns/mentions.rb new file mode 100644 index 000000000..8de010a98 --- /dev/null +++ b/app/models/concerns/mentions.rb @@ -0,0 +1,33 @@ +module Mentions + extend ActiveSupport::Concern + + included do + has_many :mentions, as: :container, dependent: :destroy + after_commit :collect_mentions_later, on: %i[ create update ] + end + + def collect_mentions(mentioner: Current.user) + scan_mentionees.each do |mentionee| + mentionee.mentioned_by mentioner, at: self + end + end + + private + def collect_mentions_later + Mention::CollectJob.perform_later(self, mentioner: Current.user) + end + + def scan_mentionees + scan_mentioned_handles.filter_map do |mention| + mentionable_users.find { |user| user.mentionable_handles.include?(mention) } + end + end + + def mentionable_users + collection.users + end + + def scan_mentioned_handles + (mentionable_content || "").scan(/(? { card.touch(:last_active_at) } - def commented? - action == "commented" + def assignment? + action == "assigned" || initial_assignment? end - def generate_notifications - Notifier.for(self)&.generate + # E.g: completed? is true if action == "completed" + def method_missing(method_name, *args, &block) + if method_name.to_s.end_with?("?") + action == method_name.to_s.chomp("?") + else + super + end end - def generate_notifications_later - GenerateNotificationsJob.perform_later self - end + private + def initial_assignment? + action == "published" && card.assigned_to?(creator) + end end diff --git a/app/models/mention.rb b/app/models/mention.rb new file mode 100644 index 000000000..5f263d9be --- /dev/null +++ b/app/models/mention.rb @@ -0,0 +1,7 @@ +class Mention < ApplicationRecord + include Notifiable + + belongs_to :container, polymorphic: true + belongs_to :mentionee, class_name: "User", inverse_of: :mentions + belongs_to :mentioner, class_name: "User" +end diff --git a/app/models/message.rb b/app/models/message.rb index 7d950b3b7..2947c99b5 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,7 +1,11 @@ class Message < ApplicationRecord + include Mentions + belongs_to :card, touch: true delegated_type :messageable, types: Messageable::TYPES, inverse_of: :message, dependent: :destroy scope :chronologically, -> { order created_at: :asc, id: :desc } + + delegate :collection, to: :card end diff --git a/app/models/message/mentions.rb b/app/models/message/mentions.rb new file mode 100644 index 000000000..fc8282413 --- /dev/null +++ b/app/models/message/mentions.rb @@ -0,0 +1,12 @@ +module Message::Mentions + extend ActiveSupport::Concern + + included do + include ::Mentions + end + + private + def mentionable_content + messageable.try(:body_plain_text) + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 9fbb94a5b..b4b323a3d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,14 +1,13 @@ class Notification < ApplicationRecord belongs_to :user - belongs_to :event - belongs_to :card + belongs_to :source, polymorphic: true belongs_to :resource, polymorphic: true scope :unread, -> { where(read_at: nil) } scope :read, -> { where.not(read_at: nil) } scope :ordered, -> { order(read_at: :desc, created_at: :desc) } - delegate :creator, to: :event + delegate :creator, to: :source after_create_commit :broadcast_unread def self.read_all diff --git a/app/models/notifier.rb b/app/models/notifier.rb index 19626be5f..455a900e3 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -1,40 +1,37 @@ class Notifier - attr_reader :event + attr_reader :source - delegate :creator, to: :event + delegate :creator, to: :source class << self - def for(event) - "Notifier::#{event.action.classify}".safe_constantize&.new(event) + def for(source) + case source + when Event + "Notifier::Events::#{source.action.classify}".safe_constantize&.new(source) + when Mention + Notifier::Mentions + end end end - def generate + def notify if should_notify? recipients.map do |recipient| - Notification.create! user: recipient, event: event, card: card, resource: resource + Notification.create! user: recipient, source: source, resource: resource end end end private - def initialize(event) - @event = event + def initialize(source) + @source = source end def should_notify? - !event.creator.system? - end - - def recipients - card.watchers_and_subscribers.without(creator) + !creator.system? end def resource - card - end - - def card - event.summary.message.card + source end end diff --git a/app/models/notifier/assigned.rb b/app/models/notifier/assigned.rb deleted file mode 100644 index 843376435..000000000 --- a/app/models/notifier/assigned.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Notifier::Assigned < Notifier - private - def recipients - event.assignees.excluding(card.collection.access_only_users) - end -end diff --git a/app/models/notifier/closed.rb b/app/models/notifier/closed.rb deleted file mode 100644 index f090b0308..000000000 --- a/app/models/notifier/closed.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Notifier::Closed < Notifier -end diff --git a/app/models/notifier/commented.rb b/app/models/notifier/commented.rb deleted file mode 100644 index 42ad4e5c8..000000000 --- a/app/models/notifier/commented.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Notifier::Commented < Notifier - private - def resource - event.comment - end -end diff --git a/app/models/notifier/events/assigned.rb b/app/models/notifier/events/assigned.rb new file mode 100644 index 000000000..fb106c677 --- /dev/null +++ b/app/models/notifier/events/assigned.rb @@ -0,0 +1,6 @@ +class Notifier::Events::Assigned < Notifier::Events::Base + private + def recipients + source.assignees.excluding(card.collection.access_only_users) + end +end diff --git a/app/models/notifier/events/base.rb b/app/models/notifier/events/base.rb new file mode 100644 index 000000000..b0e9d0489 --- /dev/null +++ b/app/models/notifier/events/base.rb @@ -0,0 +1,12 @@ +class Notifier::Events::Base < Notifier + delegate :card, to: :source + + private + def resource + card + end + + def recipients + card.watchers_and_subscribers.without(creator) + end +end diff --git a/app/models/notifier/events/closed.rb b/app/models/notifier/events/closed.rb new file mode 100644 index 000000000..133ca75c8 --- /dev/null +++ b/app/models/notifier/events/closed.rb @@ -0,0 +1,2 @@ +class Notifier::Events::Closed < Notifier::Events::Base +end diff --git a/app/models/notifier/events/commented.rb b/app/models/notifier/events/commented.rb new file mode 100644 index 000000000..55497a117 --- /dev/null +++ b/app/models/notifier/events/commented.rb @@ -0,0 +1,6 @@ +class Notifier::Events::Commented < Notifier::Events::Base + private + def resource + source.comment + end +end diff --git a/app/models/notifier/published.rb b/app/models/notifier/events/published.rb similarity index 67% rename from app/models/notifier/published.rb rename to app/models/notifier/events/published.rb index ce3917e8f..ff30d0219 100644 --- a/app/models/notifier/published.rb +++ b/app/models/notifier/events/published.rb @@ -1,4 +1,4 @@ -class Notifier::Published < Notifier +class Notifier::Events::Published < Notifier::Events::Base private def recipients card.watchers_and_subscribers(include_only_watching: true).without(creator) diff --git a/app/models/user.rb b/app/models/user.rb index a22841c42..c09455bfb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - include Accessor, Assignee, Avatar, Role, Transferable + include Accessor, Assignee, Avatar, Mentionable, Named, Role, Transferable has_many :sessions, dependent: :destroy has_secure_password validations: false @@ -15,12 +15,6 @@ class User < ApplicationRecord normalizes :email_address, with: ->(value) { value.strip.downcase } - scope :alphabetically, -> { order("lower(name)") } - - def initials - name.to_s.scan(/\b\p{L}/).join.upcase - end - def deactivate transaction do sessions.delete_all diff --git a/app/models/user/mentionable.rb b/app/models/user/mentionable.rb new file mode 100644 index 000000000..0d1c9fe36 --- /dev/null +++ b/app/models/user/mentionable.rb @@ -0,0 +1,20 @@ +module User::Mentionable + extend ActiveSupport::Concern + + included do + has_many :mentions, dependent: :destroy, inverse_of: :mentionee + end + + def mentioned_by(mentioner, at:) + mentions.create! container: at, mentioner: + end + + def mentionable_handles + [ initials, first_name, first_name_with_last_name_initial ].collect(&:downcase) + end + + private + def first_name_with_last_name_initial + "#{first_name}#{last_name&.first}" + end +end diff --git a/app/models/user/named.rb b/app/models/user/named.rb new file mode 100644 index 000000000..67420ec72 --- /dev/null +++ b/app/models/user/named.rb @@ -0,0 +1,19 @@ +module User::Named + extend ActiveSupport::Concern + + included do + scope :alphabetically, -> { order("lower(name)") } + end + + def first_name + name.split(/\s/).first + end + + def last_name + name.split(/\s/, 2).last + end + + def initials + name.scan(/\b\p{L}/).join.upcase + end +end diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb index 8cb22eeb1..36ec74370 100644 --- a/app/views/notifications/_notification.html.erb +++ b/app/views/notifications/_notification.html.erb @@ -5,15 +5,7 @@
- <%= notification_title(notification) %> -
- <% if notification.event.action == "commented" %> - <%= "#{strip_tags(notification.event.comment.body_html).blank? ? "#{notification.event.creator.name} replied" : "#{notification.event.creator.name}:" } #{strip_tags(notification.event.comment.body_html).truncate(200)}" %> - <% else %> - <%= notification_body(notification) %> - <% end %> -
-
<%= notification.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
+ <%= render "notifications/notification/#{notification.source_type.underscore}", notification: notification %>
<% end %> <% end %> diff --git a/app/views/notifications/notification/_event.html.erb b/app/views/notifications/notification/_event.html.erb new file mode 100644 index 000000000..b7b1231db --- /dev/null +++ b/app/views/notifications/notification/_event.html.erb @@ -0,0 +1,7 @@ +<%= event = notification.source %> + +<%= event_notification_title(event) %> +
+ <%= event_notification_body(event) %> +
+
<%= notification.source.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
diff --git a/app/views/notifications/notification/_mention.html.erb b/app/views/notifications/notification/_mention.html.erb new file mode 100644 index 000000000..996095e6d --- /dev/null +++ b/app/views/notifications/notification/_mention.html.erb @@ -0,0 +1,11 @@ +<% event = notification.event %> + +<%= event_notification_title(notification) %> +
+ <% if event.action == "commented" %> + <%= "#{strip_tags(event.comment.body_html).blank? ? "#{event.creator.name} replied" : "#{event.creator.name}:" } #{strip_tags(event.comment.body_html).truncate(200)}" %> + <% else %> + <%= event_notification_body(notification) %> + <% end %> +
+
<%= event.card.collection.name %> · <%= local_datetime_tag(notification.created_at, style: :ago) %>
diff --git a/db/migrate/20250421120008_create_mentions.rb b/db/migrate/20250421120008_create_mentions.rb new file mode 100644 index 000000000..da4923950 --- /dev/null +++ b/db/migrate/20250421120008_create_mentions.rb @@ -0,0 +1,11 @@ +class CreateMentions < ActiveRecord::Migration[8.1] + def change + create_table :mentions do |t| + t.references :container, polymorphic: true, null: false, index: true + t.references :mentionee, foreign_key: { to_table: :users }, null: false + t.references :mentioner, foreign_key: { to_table: :users }, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20250422054950_add_source_to_notifications.rb b/db/migrate/20250422054950_add_source_to_notifications.rb new file mode 100644 index 000000000..7108d680b --- /dev/null +++ b/db/migrate/20250422054950_add_source_to_notifications.rb @@ -0,0 +1,11 @@ +class AddSourceToNotifications < ActiveRecord::Migration[8.1] + def change + add_reference :notifications, :source, polymorphic: true, index: true + + execute <<~SQL + update notifications set source_type = 'Event' + SQL + + change_column_null :notifications, :source_type, false + end +end diff --git a/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb b/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb new file mode 100644 index 000000000..30f689c88 --- /dev/null +++ b/db/migrate/20250422062930_remove_unused_columns_from_notifications.rb @@ -0,0 +1,6 @@ +class RemoveUnusedColumnsFromNotifications < ActiveRecord::Migration[8.1] + def change + remove_column :notifications, :event_id + remove_column :notifications, :card_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 3a56a34ae..12c2edba1 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.1].define(version: 2025_04_18_150259) do +ActiveRecord::Schema[8.1].define(version: 2025_04_22_062930) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -212,6 +212,18 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do t.index ["tag_id"], name: "index_filters_tags_on_tag_id" end + create_table "mentions", force: :cascade do |t| + t.integer "container_id", null: false + t.string "container_type", null: false + t.datetime "created_at", null: false + t.integer "mentionee_id", null: false + t.integer "mentioner_id", null: false + t.datetime "updated_at", null: false + t.index ["container_type", "container_id"], name: "index_mentions_on_container" + t.index ["mentionee_id"], name: "index_mentions_on_mentionee_id" + t.index ["mentioner_id"], name: "index_mentions_on_mentioner_id" + end + create_table "messages", force: :cascade do |t| t.integer "card_id", null: false t.datetime "created_at", null: false @@ -223,17 +235,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do end create_table "notifications", force: :cascade do |t| - t.integer "card_id", null: false t.datetime "created_at", null: false - t.integer "event_id", null: false t.datetime "read_at" t.integer "resource_id", null: false t.string "resource_type", null: false + t.integer "source_id" + t.string "source_type", null: false t.datetime "updated_at", null: false t.integer "user_id", null: false - t.index ["card_id"], name: "index_notifications_on_card_id" - t.index ["event_id"], name: "index_notifications_on_event_id" t.index ["resource_type", "resource_id"], name: "index_notifications_on_resource" + t.index ["source_type", "source_id"], name: "index_notifications_on_source" t.index ["user_id", "read_at", "created_at"], name: "index_notifications_on_user_id_and_read_at_and_created_at", order: { read_at: :desc, created_at: :desc } t.index ["user_id"], name: "index_notifications_on_user_id" end @@ -328,9 +339,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do add_foreign_key "collections", "workflows" add_foreign_key "events", "cards" add_foreign_key "events", "event_summaries", column: "summary_id" + add_foreign_key "mentions", "users", column: "mentionee_id" + add_foreign_key "mentions", "users", column: "mentioner_id" add_foreign_key "messages", "cards" - add_foreign_key "notifications", "cards" - add_foreign_key "notifications", "events" add_foreign_key "notifications", "users" add_foreign_key "pins", "cards" add_foreign_key "pins", "users" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index 8152c393d..bab7bb58f 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -637,6 +637,50 @@ columns: default_function: collation: comment: + mentions: + - *5 + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: container_type + cast_type: *7 + sql_type_metadata: *8 + 'null': false + default: + default_function: + collation: + comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: container_id + cast_type: *1 + sql_type_metadata: *2 + 'null': false + default: + default_function: + collation: + comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: mentionee_id + cast_type: *1 + sql_type_metadata: *2 + 'null': false + default: + default_function: + collation: + comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: mentioner_id + cast_type: *1 + sql_type_metadata: *2 + 'null': false + default: + default_function: + collation: + comment: + - *6 + - *9 messages: - *5 - *21 @@ -664,18 +708,7 @@ columns: - *9 notifications: - *5 - - *21 - *6 - - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column - auto_increment: - name: event_id - cast_type: *1 - sql_type_metadata: *2 - 'null': false - default: - default_function: - collation: - comment: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: name: read_at @@ -708,6 +741,26 @@ columns: comment: - *9 - *28 + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: source_type + cast_type: *7 + sql_type_metadata: *8 + 'null': false + default: + default_function: + collation: + comment: + - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column + auto_increment: + name: source_id + cast_type: *1 + sql_type_metadata: *2 + 'null': true + default: + default_function: + collation: + comment: pins: - *5 - *21 @@ -921,6 +974,7 @@ primary_keys: filters: id filters_stages: filters_tags: + mentions: id messages: id notifications: id pins: id @@ -958,6 +1012,7 @@ data_sources: filters: true filters_stages: true filters_tags: true + mentions: true messages: true notifications: true pins: true @@ -1598,6 +1653,56 @@ indexes: nulls_not_distinct: comment: valid: true + mentions: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: mentions + name: index_mentions_on_mentioner_id + unique: false + columns: + - mentioner_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: mentions + name: index_mentions_on_mentionee_id + unique: false + columns: + - mentionee_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: mentions + name: index_mentions_on_container + unique: false + columns: + - container_type + - container_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true messages: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: messages @@ -1633,6 +1738,23 @@ indexes: comment: valid: true notifications: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: notifications + name: index_notifications_on_source + unique: false + columns: + - source_type + - source_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: notifications name: index_notifications_on_user_id @@ -1686,38 +1808,6 @@ indexes: nulls_not_distinct: comment: valid: true - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: notifications - name: index_notifications_on_event_id - unique: false - columns: - - event_id - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true - - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition - table: notifications - name: index_notifications_on_card_id - unique: false - columns: - - card_id - lengths: {} - orders: {} - opclasses: {} - where: - type: - using: - include: - nulls_not_distinct: - comment: - valid: true pins: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: pins @@ -1938,4 +2028,4 @@ indexes: comment: valid: true workflows: [] -version: 20250418150259 +version: 20250422062930 diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml index 7579f8eb6..ef44452f7 100644 --- a/test/fixtures/notifications.yml +++ b/test/fixtures/notifications.yml @@ -1,20 +1,17 @@ logo_published_kevin: user: kevin - event: logo_published - card: logo + source: logo_published (Event) resource: logo (Card) created_at: <%= 1.week.ago %> logo_assignment_kevin: user: kevin - event: logo_assignment_km - card: logo + source: logo_assignment_km (Event) resource: logo (Card) created_at: <%= 1.week.ago %> layout_commented_kevin: user: kevin - event: layout_commented - card: layout + source: layout_commented (Event) resource: layout_overflowing_david (Comment) created_at: <%= 1.week.ago %> diff --git a/test/models/concerns/mentions_test.rb b/test/models/concerns/mentions_test.rb new file mode 100644 index 000000000..3ed1c4d49 --- /dev/null +++ b/test/models/concerns/mentions_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class MentionsTest < ActiveSupport::TestCase + setup do + Current.session = sessions(:david) + end + + test "collect mentions on create" do + assert_difference -> { Mention.count }, +1 do + perform_enqueued_jobs only: Mention::CollectJob do + collections(:writebook).cards.create title: "Cleanup", description: "Did you finish up with the cleanup, @david?" + end + end + end + + test "collect mentions on update" do + assert_difference -> { Mention.count }, +1 do + perform_enqueued_jobs only: Mention::CollectJob do + cards(:logo).update! description: "Did you finish up with the cleanup, @david?" + end + end + end +end diff --git a/test/models/notifier_test.rb b/test/models/notifier_test.rb index 1e01e18e8..2e915bded 100644 --- a/test/models/notifier_test.rb +++ b/test/models/notifier_test.rb @@ -2,7 +2,7 @@ require "test_helper" class NotifierTest < ActiveSupport::TestCase test "for returns the matching notifier class for the event" do - assert_kind_of Notifier::Published, Notifier.for(events(:logo_published)) + assert_kind_of Notifier::Events::Published, Notifier.for(events(:logo_published)) end test "generate does not create notifications if the event was system-generated" do @@ -10,12 +10,12 @@ class NotifierTest < ActiveSupport::TestCase events(:logo_published).update!(creator: User.system) assert_no_difference -> { Notification.count } do - Notifier.for(events(:logo_published)).generate + Notifier.for(events(:logo_published)).notify end end test "creates a notification for each watcher, other than the event creator" do - notifications = Notifier.for(events(:layout_commented)).generate + notifications = Notifier.for(events(:layout_commented)).notify assert_equal [ users(:kevin) ], notifications.map(&:user) end @@ -23,19 +23,19 @@ class NotifierTest < ActiveSupport::TestCase test "does not create a notification for access-only users" do collections(:writebook).access_for(users(:kevin)).access_only! - notifications = Notifier.for(events(:layout_commented)).generate + notifications = Notifier.for(events(:layout_commented)).notify assert_equal [ users(:kevin) ], notifications.map(&:user) end test "the published event creates notifications for subscribers as well as watchers" do - notifications = Notifier.for(events(:logo_published)).generate + notifications = Notifier.for(events(:logo_published)).notify assert_equal users(:kevin, :jz).sort, notifications.map(&:user).sort end test "links to the card" do - Notifier.for(events(:logo_published)).generate + Notifier.for(events(:logo_published)).notify assert_equal cards(:logo), Notification.last.resource end @@ -44,7 +44,7 @@ class NotifierTest < ActiveSupport::TestCase collections(:writebook).access_for(users(:jz)).watching! collections(:writebook).access_for(users(:kevin)).everything! - notifications = Notifier.for(events(:logo_assignment_jz)).generate + notifications = Notifier.for(events(:logo_assignment_jz)).notify assert_equal [ users(:jz) ], notifications.map(&:user) end @@ -52,7 +52,7 @@ class NotifierTest < ActiveSupport::TestCase test "assignment events do not notify users who are access-only for the collection" do collections(:writebook).access_for(users(:jz)).access_only! - notifications = Notifier.for(events(:logo_assignment_jz)).generate + notifications = Notifier.for(events(:logo_assignment_jz)).notify assert_empty notifications end diff --git a/test/models/user/mentionable_test.rb b/test/models/user/mentionable_test.rb new file mode 100644 index 000000000..43e70792c --- /dev/null +++ b/test/models/user/mentionable_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class User::MentionableTest < ActiveSupport::TestCase + test "mentionable handles" do + assert_equal [ "dhh", "david", "davidh" ], User.new(name: "David Heinemeier-Hansson").mentionable_handles + end +end diff --git a/test/models/user/named_test.rb b/test/models/user/named_test.rb new file mode 100644 index 000000000..8993dbbf8 --- /dev/null +++ b/test/models/user/named_test.rb @@ -0,0 +1,38 @@ +require "test_helper" + +class User::NamedTest < ActiveSupport::TestCase + test "initials" do + assert_initials "M", name: "Michael" + assert_initials "SD", name: "Salvador Dali" + assert_initials "LMM", name: "Lin-Manuel Miranda" + assert_initials "OCD", name: "O'Conor Díez" + assert_initials "ACG", name: "Anne Christine García" + assert_initials "ÁL", name: "Ángela López" + end + + test "first name" do + assert_first_name "Michael", "Michael" + assert_first_name "Salvador", "Salvador Dali" + assert_first_name "Lin-Manuel", "Lin-Manuel Miranda" + assert_first_name "Anne", "Anne Christine García" + end + + test "last name" do + assert_last_name "Dali", "Salvador Dali" + assert_last_name "Miranda", "Lin_Manuel Miranda" + assert_last_name "Christine García", "Anne Christine García" + end + + private + def assert_initials(expected, **attributes) + assert_equal expected, User.new(attributes).initials + end + + def assert_first_name(expected, name) + assert_equal expected, User.new(name: name).first_name + end + + def assert_last_name(expected, name) + assert_equal expected, User.new(name: name).last_name + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1447430b1..cb34fd812 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,6 +10,7 @@ module ActiveSupport # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all + include ActiveJob::TestHelper include CardTestHelper, ChangeTestHelper, SessionTestHelper end end From 2d5901fe1e44fdd4ad7f992490d23da927620b8b Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 22 Apr 2025 11:48:19 +0200 Subject: [PATCH 100/284] Fix pin turbo stream response rendering --- app/controllers/cards/pins_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index a4b49433f..6ac118543 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -28,6 +28,6 @@ class Cards::PinsController < ApplicationController end def render_pin_button_replacement - turbo_stream.replace [ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card } + render turbo_stream: turbo_stream.replace([ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card }) end end From 268e367c6f2ecf146b87f310e59ce0ba0acec367 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 22 Apr 2025 11:53:16 +0200 Subject: [PATCH 101/284] Unused --- app/models/colorable.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/models/colorable.rb b/app/models/colorable.rb index 4fba4556d..e774fd946 100644 --- a/app/models/colorable.rb +++ b/app/models/colorable.rb @@ -3,8 +3,4 @@ module Colorable COLORS = %w[ #3b3633 #67695e #eb7a32 #bf7c2b #c09c6f #746b1e #2c6da8 #5d618f #663251 #ff63a8 ] DEFAULT_COLOR = "#2c6da8" - - included do - attribute :color, default: DEFAULT_COLOR - end end From 22abf0ba46c6b294258c3dacc6318a0651e0031d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 22 Apr 2025 11:56:41 +0200 Subject: [PATCH 102/284] Remove anemic concern Colors are a card concern. --- app/helpers/workflows_helper.rb | 2 +- app/models/card/colored.rb | 5 ++++- app/models/colorable.rb | 6 ------ app/views/workflows/stages/_color.html.erb | 2 +- test/models/card/colored_test.rb | 4 ++-- 5 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 app/models/colorable.rb diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb index 1bbed5c32..43d0d5973 100644 --- a/app/helpers/workflows_helper.rb +++ b/app/helpers/workflows_helper.rb @@ -10,6 +10,6 @@ module WorkflowsHelper end def stage_color(stage) - stage.color.presence || Colorable::DEFAULT_COLOR + stage.color.presence || Card::DEFAULT_COLOR end end diff --git a/app/models/card/colored.rb b/app/models/card/colored.rb index 99e934a94..9a16ee4a4 100644 --- a/app/models/card/colored.rb +++ b/app/models/card/colored.rb @@ -1,8 +1,11 @@ module Card::Colored extend ActiveSupport::Concern + COLORS = %w[ #3b3633 #67695e #eb7a32 #bf7c2b #c09c6f #746b1e #2c6da8 #5d618f #663251 #ff63a8 ] + DEFAULT_COLOR = "#2c6da8" + def color - color_from_stage || Colorable::DEFAULT_COLOR + color_from_stage || DEFAULT_COLOR end private diff --git a/app/models/colorable.rb b/app/models/colorable.rb deleted file mode 100644 index e774fd946..000000000 --- a/app/models/colorable.rb +++ /dev/null @@ -1,6 +0,0 @@ -module Colorable - extend ActiveSupport::Concern - - COLORS = %w[ #3b3633 #67695e #eb7a32 #bf7c2b #c09c6f #746b1e #2c6da8 #5d618f #663251 #ff63a8 ] - DEFAULT_COLOR = "#2c6da8" -end diff --git a/app/views/workflows/stages/_color.html.erb b/app/views/workflows/stages/_color.html.erb index 04a9e12a3..f2b6e3160 100644 --- a/app/views/workflows/stages/_color.html.erb +++ b/app/views/workflows/stages/_color.html.erb @@ -6,7 +6,7 @@ <%= form_with model: stage, url: workflow_stage_path(stage.workflow, stage), class: "flex gap-half", data: { controller: "form", action: "change->form#submit" } do |form| %> - <% Colorable::COLORS.each do |color| %> + <% Card::COLORS.each do |color| %>