Merge pull request #899 from basecamp/three-col-collections

Add a third, "On deck" column in Collections
This commit is contained in:
Jason Zimdars
2025-08-12 18:04:53 -05:00
committed by GitHub
16 changed files with 157 additions and 20 deletions
+2 -2
View File
@@ -185,8 +185,10 @@
--btn-border-radius: 0;
--radius: 0.3em;
flex: 1 0 33%;
inline-size: 100%;
justify-content: center;
white-space: nowrap;
}
form {
@@ -197,7 +199,6 @@
border-end-start-radius: var(--radius);
border-inline-end: 0;
border-start-start-radius: var(--radius);
flex: 1 0 50%;
padding-inline-end: 0.8em;
}
@@ -205,7 +206,6 @@
border-end-end-radius: var(--radius);
border-inline-start: 0;
border-start-end-radius: var(--radius);
flex: 1 0 50%;
padding-inline-start: 0.8em;
}
+36 -6
View File
@@ -7,7 +7,7 @@
#main:has(.card-columns) {
--cards-gap: min(2cqi, 1rem);
--gradient-border: linear-gradient(to right,var(--color-canvas), var(--color-ink-lighter) 3%, var(--color-ink-lighter) 97%, var(--color-canvas));
--reserved-bubble-space: calc(var(--bubble-size) + var(--bubble-gap));
--reserved-bubble-space: calc(var(--bubble-size) / 2 + var(--bubble-gap));
--bubble-gap: 0.5rem;
--bubble-size: 4rem;
@@ -17,13 +17,13 @@
@media (max-width: 639px) {
--bubble-gap: -0.5rem;
--bubble-size: 3rem;
--reserved-bubble-space: calc(var(--bubble-size) / 2 + var(--bubble-gap));
}
}
.card-columns {
margin: auto;
max-inline-size: 100%;
padding-inline: var(--reserved-bubble-space);
position: relative;
&:before {
@@ -36,7 +36,7 @@
@media (min-width: 640px) {
display: grid;
grid-template-columns: repeat(2, 50%);
grid-template-columns: repeat(3, 33%);
}
}
@@ -59,7 +59,7 @@
@media (max-width: 639px) {
padding-block-end: calc(var(--cards-gap) * 2);
padding-inline-start: var(--reserved-bubble-space);
padding-inline-start: var(--cards-gap);
}
.card {
@@ -155,7 +155,7 @@
.cards--considering {
@media (min-width: 640px) {
padding-inline: var(--reserved-bubble-space) var(--cards-gap);
padding-inline: var(--cards-gap);
&:after {
background: linear-gradient(var(--color-canvas), var(--color-ink-lighter) 1%, var(--color-ink-lighter) 99%, var(--color-canvas));
@@ -222,12 +222,42 @@
}
}
/* On Deck
/* ------------------------------------------------------------------------ */
.cards--on-deck {
@media (min-width: 640px) {
padding-inline: var(--cards-gap);
&:after {
background: linear-gradient(var(--color-canvas), var(--color-ink-lighter) 1%, var(--color-ink-lighter) 99%, var(--color-canvas));
block-size: calc(100% + 1.2em);
content: "";
display: block;
inline-size: 1px;
inset: -0.6em 0 -0.6em 100%;
position: absolute;
z-index: 3;
}
}
@media (max-width: 639px) {
&::before {
background: var(--gradient-border);
block-size: 1px;
content: "";
inset: 0 0 auto;
position: absolute;
}
}
}
/* Doing
/* ------------------------------------------------------------------------ */
.cards--doing {
@media (min-width: 640px) {
padding-inline: var(--cards-gap) var(--reserved-bubble-space);
padding-inline: var(--cards-gap);
}
@media (max-width: 639px) {
+4 -2
View File
@@ -8,7 +8,7 @@ class Cards::DropsController < ApplicationController
end
private
VALID_DROP_TARGETS = %w[ considering doing ]
VALID_DROP_TARGETS = %w[ considering on_deck doing ]
def set_card
@card = Current.user.accessible_cards.find(params[:dropped_item_id])
@@ -26,6 +26,8 @@ class Cards::DropsController < ApplicationController
case @drop_target
when :considering
@card.reconsider
when :on_deck
@card.move_to_on_deck
when :doing
@card.engage
end
@@ -33,6 +35,6 @@ class Cards::DropsController < ApplicationController
def render_column_replacement
page_and_filter = page_and_filter_for @filter.with(engagement_status: @drop_target.to_s), per_page: CardsController::PAGE_SIZE
render turbo_stream: turbo_stream.replace("#{@drop_target}-cards", method: :morph, partial: "cards/index/engagement/#{@drop_target}", locals: page_and_filter.to_h)
render turbo_stream: turbo_stream.replace("#{@drop_target.to_s.gsub('_', '-')}-cards", method: :morph, partial: "cards/index/engagement/#{@drop_target}", locals: page_and_filter.to_h)
end
end
@@ -2,7 +2,12 @@ class Cards::EngagementsController < ApplicationController
include CardScoped
def create
@card.engage
case params[:engagement]
when "doing"
@card.engage
when "on_deck"
@card.move_to_on_deck
end
render_card_replacement
end
+1
View File
@@ -10,6 +10,7 @@ class CardsController < ApplicationController
def index
@considering = page_and_filter_for @filter.with(engagement_status: "considering"), per_page: PAGE_SIZE
@on_deck = page_and_filter_for @filter.with(engagement_status: "on_deck"), per_page: PAGE_SIZE
@doing = page_and_filter_for @filter.with(engagement_status: "doing"), per_page: PAGE_SIZE
@closed = page_and_filter_for_closed_cards
end
+24 -3
View File
@@ -5,18 +5,24 @@ module Card::Engageable
has_one :engagement, dependent: :destroy, class_name: "Card::Engagement"
scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) }
scope :doing, -> { published.open.joins(:engagement) }
scope :on_deck, -> { published.open.joins(:engagement).where(card_engagements: { status: "on_deck" }) }
scope :doing, -> { published.open.joins(:engagement).where(card_engagements: { status: "doing" }) }
scope :by_engagement_status, ->(status) do
case status.to_s
when "considering" then considering.with_golden_first
when "on_deck" then on_deck.with_golden_first
when "doing" then doing.with_golden_first
end
end
end
def doing?
open? && published? && engagement.present?
open? && published? && engagement&.status == "doing"
end
def on_deck?
open? && published? && engagement&.status == "on_deck"
end
def considering?
@@ -26,6 +32,8 @@ module Card::Engageable
def engagement_status
if doing?
"doing"
elsif on_deck?
"on_deck"
elsif considering?
"considering"
end
@@ -35,7 +43,20 @@ module Card::Engageable
unless doing?
transaction do
reopen
create_engagement!
create_engagement!(status: "doing")
end
end
end
def move_to_on_deck
unless on_deck?
transaction do
reopen
if engagement.present?
engagement.update!(status: "on_deck")
else
create_engagement!(status: "on_deck")
end
end
end
end
+2
View File
@@ -1,3 +1,5 @@
class Card::Engagement < ApplicationRecord
belongs_to :card, class_name: "::Card", touch: true
validates :status, presence: true, inclusion: { in: %w[doing on_deck] }
end
+11 -2
View File
@@ -2,14 +2,23 @@
<div class="card-perma__notch card-perma__notch--top" id="<%= dom_id(@card, :card_engagement_toggle) %>" style="--card-color: <%= @card.color %>;">
<div class="btn__group flex full-width txt-uppercase">
<%= form_with url: card_engagement_path(@card), method: :delete, data: { controller: "form" } do |form| %>
<%= form.label :engagement, value: "considering", class: "btn", aria: { label: "Move to Doing"} do %>
<%= form.label :engagement, value: "considering", class: "btn", aria: { label: "Move to Considering"} do %>
<%= form.radio_button :engagement, "considering", checked: @card.considering?, class: "for-screen-reader", data: { action: "change->form#submit" } %>
<span>Considering</span>
<% end %>
<% end %>
<%= form_with url: card_engagement_path(@card), method: :post, data: { controller: "form" } do |form| %>
<%= form.label :engagement, value: "doing", class: "btn", aria: { label: "Move back to Considering"} do %>
<%= form.hidden_field :engagement, value: "on_deck" %>
<%= form.label :engagement, value: "on_deck", class: "btn", aria: { label: "Move to On Deck"} do %>
<%= form.radio_button :engagement, "on_deck", checked: @card.on_deck?, class: "for-screen-reader", data: { action: "change->form#submit" } %>
<span>On Deck</span>
<% end %>
<% end %>
<%= form_with url: card_engagement_path(@card), method: :post, data: { controller: "form" } do |form| %>
<%= form.hidden_field :engagement, value: "doing" %>
<%= form.label :engagement, value: "doing", class: "btn", aria: { label: "Move to Doing"} do %>
<%= form.radio_button :engagement, "doing", checked: @card.doing?, class: "for-screen-reader", data: { action: "change->form#submit" } %>
<span>Doing</span>
<% end %>
+1
View File
@@ -50,6 +50,7 @@
dragend->drag-and-drop#dragEnd" } do %>
<div class="card-columns">
<%= render "cards/index/engagement/considering", **@considering.to_h %>
<%= render "cards/index/engagement/on_deck", **@on_deck.to_h %>
<%= render "cards/index/engagement/doing", **@doing.to_h %>
</div>
@@ -1,7 +1,7 @@
<div class="cards__heading" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="cards__filter btn input input--select borderless overflow-ellipsis" data-action="click->dialog#open:stop">
<% if filter.stages.any? %>
<%= filter.stages.map(&:name).to_sentence %>
Doing: <%= filter.stages.map(&:name).to_sentence %>
<% else %>
Doing
<% end %>
@@ -0,0 +1,15 @@
<section id="on-deck-cards" class="cards cards--on-deck" data-drag-and-drop-target="container" data-drop-target="on_deck">
<h2 class="cards__heading">
Decided to do
</h2>
<% if page.used? %>
<%= render partial: "cards/display/preview", collection: page.records, as: :card, locals: { draggable: true }, cached: true %>
<% unless page.last? %>
<%= cards_next_page_link "on-deck-cards", page: page, filter: filter %>
<% end %>
<% else %>
<p class="txt-medium translucent">Nothing here</p>
<% end %>
</section>
@@ -0,0 +1,6 @@
class AddStatusToCardEngagements < ActiveRecord::Migration[8.1]
def change
add_column :card_engagements, :status, :string, default: "doing", null: false
add_index :card_engagements, :status
end
end
Generated
+3 -1
View File
@@ -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_08_06_174718) do
ActiveRecord::Schema[8.1].define(version: 2025_08_12_195130) do
create_table "accesses", force: :cascade do |t|
t.datetime "accessed_at"
t.integer "collection_id", null: false
@@ -107,8 +107,10 @@ ActiveRecord::Schema[8.1].define(version: 2025_08_06_174718) do
create_table "card_engagements", force: :cascade do |t|
t.integer "card_id"
t.datetime "created_at", null: false
t.string "status", default: "doing", null: false
t.datetime "updated_at", null: false
t.index ["card_id"], name: "index_card_engagements_on_card_id"
t.index ["status"], name: "index_card_engagements_on_status"
end
create_table "card_goldnesses", force: :cascade do |t|
+27 -1
View File
@@ -382,6 +382,16 @@ columns:
comment:
- *5
- *6
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: status
cast_type: *7
sql_type_metadata: *8
'null': false
default: doing
default_function:
collation:
comment:
- *9
card_goldnesses:
- *22
@@ -1587,6 +1597,22 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: card_engagements
name: index_card_engagements_on_status
unique: false
columns:
- status
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
card_goldnesses:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: card_goldnesses
@@ -2703,4 +2729,4 @@ indexes:
comment:
valid: true
workflows: []
version: 20250806174718
version: 20250812195130
@@ -9,7 +9,16 @@ class Cards::EngagementsControllerTest < ActionDispatch::IntegrationTest
card = cards(:text)
assert_changes -> { card.reload.doing? }, from: false, to: true do
post card_engagement_path(card)
post card_engagement_path(card), params: { engagement: "doing" }
assert_card_container_rerendered(card)
end
end
test "create on_deck" do
card = cards(:text)
assert_changes -> { card.reload.on_deck? }, from: false, to: true do
post card_engagement_path(card), params: { engagement: "on_deck" }
assert_card_container_rerendered(card)
end
end
+8
View File
@@ -12,6 +12,9 @@ class Card::EngageableTest < ActiveSupport::TestCase
assert_not cards(:logo).considering?
assert cards(:text).considering?
assert_not cards(:logo).on_deck?
assert_not cards(:text).on_deck?
assert_equal "doing", cards(:logo).engagement_status
assert_equal "considering", cards(:text).engagement_status
end
@@ -31,6 +34,7 @@ class Card::EngageableTest < ActiveSupport::TestCase
assert_not cards(:text).considering?
assert_not cards(:text).doing?
assert_not cards(:text).on_deck?
cards(:text).engage
assert_not cards(:text).reload.closed?
@@ -48,5 +52,9 @@ class Card::EngageableTest < ActiveSupport::TestCase
assert_includes Card.considering, cards(:text)
assert_not_includes Card.considering, cards(:logo)
cards(:text).move_to_on_deck
assert_includes Card.on_deck, cards(:text)
assert_not_includes Card.on_deck, cards(:logo)
end
end