From cc6249340d625cf818892e4a5cf0188017681183 Mon Sep 17 00:00:00 2001 From: Zoltan Hosszu Date: Fri, 12 Dec 2025 11:14:43 +0100 Subject: [PATCH 1/5] Adjusting text content spacing between paragraphs This PR removes spacing between paragraphs. It keeps spacing for elements following paragraphs (blockquotes, lists, attachments, etc). --- app/assets/stylesheets/rich-text-content.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 5a8ba3fd7..88e4d2ae2 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -28,6 +28,10 @@ } } + p:has(+ p) { + margin: 0; + } + ol, ul { padding-inline-start: 3ch; } From b2017a75cf6ce159801787025d61af208f2afc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20P=C3=A9ch=C3=A8r?= Date: Mon, 15 Dec 2025 14:20:17 +0000 Subject: [PATCH 2/5] Add script to separate non-blank sibling

--- .../split-sibling-paragraphs-with-p-br.rb | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 script/migrations/split-sibling-paragraphs-with-p-br.rb diff --git a/script/migrations/split-sibling-paragraphs-with-p-br.rb b/script/migrations/split-sibling-paragraphs-with-p-br.rb new file mode 100644 index 000000000..f3e8954d6 --- /dev/null +++ b/script/migrations/split-sibling-paragraphs-with-p-br.rb @@ -0,0 +1,124 @@ +#!/usr/bin/env ruby + +BACKFILL_TIMESTAMP = Time.parse("2025-12-18 00:00:00 UTC") +ACCOUNT_ID = nil # restrict to an account_id + +# Split sibling

tags with content by inserting

to replicaate previous view. +# Run for the time range before paragraphs were not spaced +# See https://app.fizzy.do/5986089/cards/3472 +# and https://github.com/basecamp/fizzy/pull/2107 +# +# MUST BE RUN AFTER `decrypt!` when using ActiveRecord Encryption +# +# Run locally: +# bin/rails runner script/migrations/split-sibling-paragraphs-with-p-br.rb +# +# Run via Kamal: +# kamal app exec -d -p --reuse "bin/rails runner script/migrations/split-sibling-paragraphs-with-p-br.rb" +# +# Safe to re-run for a time range: won't re-detect unsplit paragraphs and updated_at will be outside time window +class SeparateSiblingParagraphs + attr_reader :updated_at, :account_id + + def initialize(updated_at, account_id: nil) + @updated_at = updated_at + @account_id = account_id + end + + def run + puts "Separating non-blank sibling paragraphs" + + puts "Updated at: #{updated_at}" + puts account_id ? "Only account id: #{account_id}" : "For **ALL ACCOUNTS**" + + puts "\nPress ENTER to continue running or CTRL-C to bail..." + gets + + puts "\nRunning..." + + # Suppress SQL logs + Rails.event.debug_mode = false + + seconds = Benchmark.realtime do + suppressing_turbo_broadcasts do + separate_nonblank_paragraphs + end + end + + puts "\n\n" + puts "Finished splitting non-blank

s in %.2f seconds." % seconds + end + + private + def suppressing_turbo_broadcasts + Board.suppressing_turbo_broadcasts do + Card.suppressing_turbo_broadcasts do + yield + end + end + end + + def separate_nonblank_paragraphs + scanned = 0 + fixed = 0 + insertions = 0 + + action_texts_scope.find_each(**batch_options) do |rich_text| + next if account_id && rich_text.record.account.external_account_id != account_id + + scanned += 1 + edited = false + + rich_text.body&.fragment.tap do |fragment| + next unless fragment + + fragment.find_all("p + p").each do |node| + unless ineligible?(node) || ineligible?(node.previous_sibling) + node.add_previous_sibling empty_node_markup + edited = true + insertions += 1 + end + end + + if edited + puts " - modifying #{rich_text.record.class.name} #{rich_text.record.to_param} (account: #{rich_text.record.account.external_account_id})" unless demo_card?(rich_text.record) + # allow implicit touching to invalidate caches + rich_text.update! body: fragment.to_html + fixed +=1 + end + end + end + + puts "\n\Separation complete!" + puts " Rich texts examined: #{scanned}" + puts " Rich texts modified: #{fixed}" + puts " Paragraphs inserted: #{insertions}" + fixed + end + + def action_texts_scope + ActionText::RichText.where(updated_at: updated_at) + end + + def batch_options + { batch_size: 20, order: :desc } + end + + def ineligible?(node) + empty_node?(node) || wrapping_attachment?(node) + end + + def empty_node?(node) + node.to_html == empty_node_markup + end + + def empty_node_markup + "


" + end + + def wrapping_attachment?(node) + node.children.first&.name == "action-text-attachment" + end +end + +SeparateSiblingParagraphs.new(..BACKFILL_TIMESTAMP, account_id: ACCOUNT_ID).run From d250c256c65b44102fbecf67917995eb8650e596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20P=C3=A9ch=C3=A8r?= Date: Thu, 18 Dec 2025 00:24:46 +0000 Subject: [PATCH 3/5] Add paragraph space to seed Card #8 --- app/models/account/seeder.rb | 1 + script/migrations/split-sibling-paragraphs-with-p-br.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/models/account/seeder.rb b/app/models/account/seeder.rb index 4a349b939..ff97538be 100644 --- a/app/models/account/seeder.rb +++ b/app/models/account/seeder.rb @@ -69,6 +69,7 @@ class Account::Seeder
  • Make another called "Working on"
  • Go back to the Board view, click the little “+” to the right of the DONE column, name the column, pick a color, then do it again.

    +


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

    HTML diff --git a/script/migrations/split-sibling-paragraphs-with-p-br.rb b/script/migrations/split-sibling-paragraphs-with-p-br.rb index f3e8954d6..42778292f 100644 --- a/script/migrations/split-sibling-paragraphs-with-p-br.rb +++ b/script/migrations/split-sibling-paragraphs-with-p-br.rb @@ -119,6 +119,10 @@ class SeparateSiblingParagraphs def wrapping_attachment?(node) node.children.first&.name == "action-text-attachment" end + + def demo_card?(record) + record.is_a?(Card) && record.number <= 8 + end end SeparateSiblingParagraphs.new(..BACKFILL_TIMESTAMP, account_id: ACCOUNT_ID).run From 60dc6927fe26ec6eed8c572733ae092736d4cc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20P=C3=A9ch=C3=A8r?= Date: Thu, 18 Dec 2025 17:34:25 +0000 Subject: [PATCH 4/5] Remove attachment detection --- .../migrations/split-sibling-paragraphs-with-p-br.rb | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/script/migrations/split-sibling-paragraphs-with-p-br.rb b/script/migrations/split-sibling-paragraphs-with-p-br.rb index 42778292f..4b852007b 100644 --- a/script/migrations/split-sibling-paragraphs-with-p-br.rb +++ b/script/migrations/split-sibling-paragraphs-with-p-br.rb @@ -73,7 +73,7 @@ class SeparateSiblingParagraphs next unless fragment fragment.find_all("p + p").each do |node| - unless ineligible?(node) || ineligible?(node.previous_sibling) + unless empty_node?(node) || empty_node?(node.previous_sibling) node.add_previous_sibling empty_node_markup edited = true insertions += 1 @@ -104,10 +104,6 @@ class SeparateSiblingParagraphs { batch_size: 20, order: :desc } end - def ineligible?(node) - empty_node?(node) || wrapping_attachment?(node) - end - def empty_node?(node) node.to_html == empty_node_markup end @@ -116,10 +112,6 @@ class SeparateSiblingParagraphs "


    " end - def wrapping_attachment?(node) - node.children.first&.name == "action-text-attachment" - end - def demo_card?(record) record.is_a?(Card) && record.number <= 8 end From 6b427ad9b9d68fb161a758d780dedf4fdfd9fa6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20P=C3=A9ch=C3=A8r?= Date: Thu, 18 Dec 2025 17:34:47 +0000 Subject: [PATCH 5/5] Update timestamp --- script/migrations/split-sibling-paragraphs-with-p-br.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/migrations/split-sibling-paragraphs-with-p-br.rb b/script/migrations/split-sibling-paragraphs-with-p-br.rb index 4b852007b..d0844a540 100644 --- a/script/migrations/split-sibling-paragraphs-with-p-br.rb +++ b/script/migrations/split-sibling-paragraphs-with-p-br.rb @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -BACKFILL_TIMESTAMP = Time.parse("2025-12-18 00:00:00 UTC") +BACKFILL_TIMESTAMP = Time.parse("2025-12-19 00:07:00 UTC") ACCOUNT_ID = nil # restrict to an account_id # Split sibling

    tags with content by inserting

    to replicaate previous view.