Merge branch 'main' into add-card-description

* main: (33 commits)
  Run beamer accessories in production only
  Less noisy logging in production
  Inline perma to container partial
  Use card replacement for card updates too
  Extract helper method to keep partial at a uniform level of abstraction
  Remove duplicated conditional
  Style
  Note that we dont want to keep reloading these frames
  I like this even better
  Suffix qualifier is unnnecessary
  Group partials for container
  Match domain language
  Fix trailing space
  Use card rerendering instead of redirects
  Better name
  Stick with domain language
  Fix tests
  Rerender instead of redirect
  Style
  Extract partials that can be rerendered
  ...
This commit is contained in:
Jason Zimdars
2025-04-17 16:40:23 -05:00
43 changed files with 231 additions and 208 deletions
@@ -6,5 +6,6 @@ class Cards::AssignmentsController < ApplicationController
def create
@card.toggle_assignment @collection.users.active.find(params[:assignee_id])
render_card_replacement
end
end
+2 -2
View File
@@ -3,11 +3,11 @@ class Cards::ClosuresController < ApplicationController
def create
@card.close(user: Current.user, reason: params[:reason])
redirect_to @card
render_card_replacement
end
def destroy
@card.reopen
redirect_to @card
render_card_replacement
end
end
@@ -3,11 +3,11 @@ class Cards::EngagementsController < ApplicationController
def create
@card.engage
redirect_to @card
render_card_replacement
end
def destroy
@card.reconsider
redirect_to @card
render_card_replacement
end
end
@@ -3,11 +3,11 @@ class Cards::GoldnessesController < ApplicationController
def create
@card.gild
redirect_to @card
render_card_replacement
end
def destroy
@card.ungild
redirect_to @card
render_card_replacement
end
end
+6 -10
View File
@@ -6,24 +6,20 @@ class Cards::PinsController < ApplicationController
def create
pin = @card.pin_by Current.user
broadcast_my_new pin
redirect_to card_pin_path(@card)
broadcast_add_to_tray pin
end
def destroy
pin = @card.unpin_by Current.user
broadcast_my_removed pin
redirect_to card_pin_path(@card)
broadcast_remove_from_tray pin
end
private
def broadcast_my_new(pin)
pin.card.broadcast_prepend_later_to [ Current.user, :pins ], target: "pins", partial: "cards/display/preview"
def broadcast_add_to_tray(pin)
pin.broadcast_prepend_to [ Current.user, :pins_tray ], target: "pins", partial: "my/pins/pin"
end
def broadcast_my_removed(pin)
pin.broadcast_remove_to [ Current.user, :pins ]
def broadcast_remove_from_tray(pin)
pin.broadcast_remove_to [ Current.user, :pins_tray ]
end
end
+2 -2
View File
@@ -2,7 +2,7 @@ class Cards::ReadingsController < ApplicationController
include CardScoped
def create
@notification = Current.user.notifications.find_by(card: @card)
@notification.read
@notifications = Current.user.notifications.where(card: @card)
@notifications.each(&:read)
end
end
@@ -9,6 +9,8 @@ class Cards::StagingsController < ApplicationController
else
@card.update!(stage: nil)
end
render_card_replacement
end
private
@@ -7,6 +7,7 @@ class Cards::TaggingsController < ApplicationController
def create
@card.toggle_tag_with sanitized_tag_title_param
render_card_replacement
end
private
+9 -5
View File
@@ -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, :description, :image, 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
+4
View File
@@ -13,4 +13,8 @@ module CardScoped
def set_collection
@collection = @card.collection
end
def render_card_replacement
render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", locals: { card: @card.reload })
end
end
+8
View File
@@ -29,4 +29,12 @@ module CardsHelper
**options,
&block
end
def button_to_delete_card(card)
button_to collection_card_path(card.collection, card),
method: :delete, class: "btn", data: { turbo_confirm: "Are you sure you want to delete this?" } do
concat(icon_tag("trash"))
concat(tag.span("Delete", class: "for-screen-reader"))
end
end
end
+43
View File
@@ -0,0 +1,43 @@
<section id="<%= dom_id(card, :card_container) %>"
class="card-perma <%= "card-perma--pointing" unless card.published? %>" style="--card-color: <%= card.color %>;">
<%= render "cards/container/engagement", card: card %>
<aside class="card-perma__actions card-perma__actions--left" role="toolbar">
<%= render "cards/container/gild", card: card if card.doing? %>
<%= render "cards/container/image", card: card %>
<%= button_to_delete_card(card) %>
</aside>
<div class="card-perma__bg">
<%= card_article_tag card, class: "card" do %>
<header class="card__header flex align-center gap min-width">
<%= 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 %>
</header>
<div class="card__body flex gap full-width flex-item-grow">
<%= render "cards/container/title", card: card %>
<%= render "cards/stagings/stages", card: card if card.doing? %>
</div>
<footer class="card__footer full-width flex align-start gap">
<%= render "cards/display/perma/meta", card: card %>
</footer>
<%= render "cards/display/common/background", card: card %>
<% end %>
</div>
<% 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 %>
<aside class="card-perma__actions card-perma__actions--right" role="toolbar">
<%= turbo_frame_tag card, :watch, src: card_watch_path(card) %>
<%= turbo_frame_tag card, :pin, src: card_pin_path(card) %>
</aside>
<%= render "cards/container/closure", card: card %>
<% end %>
</section>
@@ -0,0 +1,29 @@
<div class="comments pad-block">
<div class="comment comment--new flex-inline align-start full-width">
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag Current.user, hidden_for_screen_reader: true %>
</figure>
<div class="comment__content flex-inline flex-column full-width">
<div class="comment__body txt-align-start">
<form data-controller="outlet-auto-save paste"
class="flex flex-column gap full-width"
data-outlet-auto-save-auto-save-outlet="#card_form"
data-action="paste->paste#pasteFiles">
<%= 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") } %>
</form>
<% if card.creating? %>
<div class="margin-block-start flex align-center gap">
<%= 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" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
@@ -1 +0,0 @@
<%= turbo_stream.replace([ @card, :meta ], partial: "cards/display/perma/meta", locals: { card: @card }) %>
+14
View File
@@ -0,0 +1,14 @@
<h1 class="card__title">
<% 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 %>
</h1>
-55
View File
@@ -1,55 +0,0 @@
<% cache card do %>
<%= card_article_tag card, class: "card" do %>
<header class="card__header flex align-center gap min-width">
<%= 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 %>
</header>
<div class="card__body flex gap full-width flex-item-grow">
<% if card.published? %>
<%= turbo_frame_tag card, :edit do %>
<div class="full-width">
<h1 class="card__title flex align-start gap-half">
<span><%= card.title %></span>
<%= link_to edit_collection_card_path(card.collection, card), class: "btn txt-small margin-block-start-half" do %>
<%= icon_tag "pencil" %>
<span class="for-screen-reader">Edit</span>
<% end %>
</h1>
<p class="card__description margin-block-half">
<%= sanitize card.description_html %>
</p>
</div>
<% 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| %>
<h1 class="card__title">
<%= 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" } %>
</h1>
<div class="card__description margin-block-half">
<%= 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" } %>
</div>
<% end %>
<% end %>
<% if card.doing? %>
<%= turbo_frame_tag dom_id(@card, :stages) do %>
<%= render "cards/stagings/stages", card: card %>
<% end %>
<% end %>
</div>
<footer class="card__footer full-width flex align-start gap">
<%= render "cards/display/perma/meta", card: card %>
</footer>
<%= render "cards/display/common/background", card: card %>
<% end %>
<% end %>
+11
View File
@@ -0,0 +1,11 @@
<div id="<%= dom_id(card, :pin_button) %>">
<% if card.pinned_by? Current.user %>
<%= button_to card_pin_path(card), method: :delete, class: "btn btn--reversed" do %>
<%= icon_tag "pinned" %> <span class="for-screen-reader">Un-pin this card</span>
<% end %>
<% else %>
<%= button_to card_pin_path(card), class: "btn" do %>
<%= icon_tag "unpinned" %> <span class="for-screen-reader">Pin this card</span>
<% end %>
<% end %>
</div>
@@ -0,0 +1 @@
<%= turbo_stream.replace [ @card, :pin_button ], partial: "cards/pins/pin_button", locals: { card: @card } %>
@@ -0,0 +1 @@
<%= render template: "cards/pins/create" %>
+1 -11
View File
@@ -1,13 +1,3 @@
<%= turbo_frame_tag dom_id(@card, :pin) do %>
<% if @card.pinned_by? Current.user %>
<%= button_to card_pin_path(@card), method: :delete, class: "btn btn--reversed" do %>
<%= icon_tag "pinned" %>
<span class="for-screen-reader">Un-pin this card</span>
<% end %>
<% else %>
<%= button_to card_pin_path(@card), class: "btn" do %>
<%= icon_tag "unpinned" %>
<span class="for-screen-reader">Pin this card</span>
<% end %>
<% end %>
<%= render "cards/pins/pin_button", card: @card %>
<% end %>
@@ -1 +1,3 @@
<%= turbo_stream.remove @notification %>
<% @notifications.each do |notification| %>
<%= turbo_stream.remove notification %>
<% end %>
+3 -37
View File
@@ -30,45 +30,11 @@
<% end %>
<div data-controller="beacon" data-beacon-url-value="<%= card_reading_url(@card) %>">
<section id="<%= dom_id(@card, :card_container) %>" class="card-perma" style="--card-color: <%= @card.color %>;">
<%= render "cards/doing_toggle", card: @card %>
<aside class="card-perma__actions card-perma__actions--left" role="toolbar">
<%= render "cards/golden_toggle", card: @card if @card.doing? %>
<%= render "cards/image", card: @card %>
<%= button_to collection_card_path(@card.collection, @card),
method: :delete,
class: "btn",
data: { turbo_confirm: "Are you sure you want to delete this?" } do %>
<%= icon_tag "trash" %>
<span class="for-screen-reader">Delete</span>
<% end %>
</aside>
<div class="card-perma__bg">
<%= render "cards/display/perma", card: @card %>
</div>
<% if @card.published? %>
<aside class="card-perma__actions card-perma__actions--right" role="toolbar">
<%= turbo_frame_tag dom_id(@card, :watch), src: card_watch_path(@card), refresh: :morph %>
<%= turbo_frame_tag dom_id(@card, :pin), src: card_pin_path(@card), refresh: :morph %>
</aside>
<% end %>
<% if @card.creating? %>
<div class="card-perma__notch card-perma__notch--bottom">
<%= 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" %>
</div>
<% elsif @card.published? %>
<%= render "cards/closure_toggle", card: @card %>
<% end %>
</section>
<%= render "cards/container", card: @card %>
<% if @card.published? %>
<%= render "cards/messages", card: @card %>
<% else %>
<%= render "cards/description_as_draft_comment", card: @card %>
<% end %>
</div>
@@ -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 %>
@@ -1 +0,0 @@
<%= turbo_stream.replace([ @card, :tags ], partial: "cards/display/perma/tags", locals: { card: @card }) %>
+3
View File
@@ -0,0 +1,3 @@
<div class="pin tray__item" id="<%= dom_id pin %>">
<%= render "cards/display/preview", card: pin.card %>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= turbo_stream_from Current.user, :pins %>
<%= turbo_stream_from Current.user, :pins_tray %>
<%= tag.dialog id: "pin-tray", class: "tray pin-tray",
data: { controller: "dialog", turbo_permanent: true, dialog_modal_value: false, dialog_target: "dialog", action: "keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" } do %>
+1 -5
View File
@@ -1,7 +1,3 @@
<%= turbo_frame_tag "pins" do %>
<% @pins.map(&:card).each do |card| %>
<div class="pin tray__item">
<%= render partial: "cards/display/preview", locals: { card: card } %>
</div>
<% end %>
<%= render partial: "my/pins/pin", collection: @pins %>
<% end %>
+29
View File
@@ -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
+3 -33
View File
@@ -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 --debug --remove-after=1h --dir=/storage
port: 5000:80
roles:
- web
beamer-replica:
<<: *beamer-accessory
service: beamer-replica
cmd: beamer replica --debug --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
@@ -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
@@ -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
@@ -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
@@ -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_card_container_rerendered(cards(:text))
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_card_container_rerendered(cards(:logo))
end
assert_redirected_to collection_card_path(card.collection, card)
end
end
@@ -8,24 +8,24 @@ class Cards::PinsControllerTest < ActionDispatch::IntegrationTest
test "create" do
assert_changes -> { cards(:layout).pinned_by?(users(:kevin)) }, from: false, to: true do
perform_enqueued_jobs do
assert_turbo_stream_broadcasts([ users(:kevin), :pins ], count: 1) do
post card_pin_path(cards(:layout))
assert_turbo_stream_broadcasts([ users(:kevin), :pins_tray ], count: 1) do
post card_pin_path(cards(:layout)), as: :turbo_stream
end
end
end
assert_redirected_to card_pin_path(cards(:layout))
assert_response :success
end
test "destroy" do
assert_changes -> { cards(:shipping).pinned_by?(users(:kevin)) }, from: true, to: false do
perform_enqueued_jobs do
assert_turbo_stream_broadcasts([ users(:kevin), :pins ], count: 1) do
delete card_pin_path(cards(:shipping))
assert_turbo_stream_broadcasts([ users(:kevin), :pins_tray ], count: 1) do
delete card_pin_path(cards(:shipping)), as: :turbo_stream
end
end
end
assert_redirected_to card_pin_path(cards(:shipping))
assert_response :success
end
end
@@ -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
@@ -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
+7
View File
@@ -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
+1 -1
View File
@@ -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
+5
View File
@@ -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