Add steps to cards

This commit is contained in:
Jason Zimdars
2025-07-01 19:56:29 -05:00
parent 5f31e6612b
commit f02215c5b7
21 changed files with 363 additions and 16 deletions
+5 -1
View File
@@ -108,6 +108,10 @@
}
}
}
.card__steps {
display: flex;
}
}
/* Column elements
@@ -197,7 +201,7 @@
.card__header {
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
margin-inline-start: calc(-0.5 * var(--card-padding-inline));
margin-inline: calc(-0.5 * var(--card-padding-inline)) calc(-0.25 * var(--card-padding-inline));
}
.card__people-label {
+2 -2
View File
@@ -119,8 +119,8 @@
flex-wrap: nowrap;
gap: var(--inline-space);
margin-block-start: calc(-1 * var(--card-padding-block));
margin-inline-start: calc(-1 * var(--card-padding-inline));
max-inline-size: 100%;
margin-inline: calc(-1 * var(--card-padding-inline)) calc(-0.5 * var(--card-padding-inline));
max-inline-size: unset;
min-inline-size: 0;
.workflow-stage {
+1
View File
@@ -30,6 +30,7 @@
.icon--camera { --svg: url("camera.svg "); }
.icon--caret-down { --svg: url("caret-down.svg "); }
.icon--check { --svg: url("check.svg "); }
.icon--check-all { --svg: url("check-all.svg "); }
.icon--clipboard { --svg: url("clipboard.svg "); }
.icon--close { --svg: url("close.svg "); }
.icon--collection { --svg: url("collection.svg "); }
+98
View File
@@ -0,0 +1,98 @@
@layer components {
.step {
display: grid;
grid-template-columns: 1em auto auto;
gap: calc(var(--inline-space) * 2/3);
inline-size: auto;
}
.step__checkbox {
--hover-color: var(--card-color);
appearance: none;
background-color: var(--color-canvas);
block-size: 1.1em;
border: 1px solid currentColor;
border-radius: 0.15em;
color: currentColor;
display: grid;
font: inherit;
inline-size: 1.1em;
margin: 0;
place-content: center;
transform: translateY(0.1em);
&::before {
background-color: CanvasText;
block-size: 0.65em;
box-shadow: inset 1em 1em currentColor;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
content: "";
inline-size: 0.65em;
transform: scale(0);
transform-origin: center;
transition: 150ms transform ease-in-out;
}
&:checked::before {
transform: scale(1) rotate(10deg);
}
&:where([disabled]):not(:hover):not(:active) {
filter: none;
opacity: 0.5;
}
}
.step__content {
--input-border-radius: 0;
--input-border-size: 0;
--input-padding: 0;
border-bottom: 1px solid transparent;
color: currentColor;
font-weight: 500;
margin-block-end: calc(var(--block-space) * 1/3);
&:is(a, input[type=text]) {
--hover-size: 0;
}
.step:has(:checked) & {
opacity: 0.7;
text-decoration: line-through;
}
&::placeholder {
color: var(--card-color);
}
}
.step__content--edit {
border-bottom-color: currentColor;
}
.steps {
contain: inline-size;
display: grid;
list-style: none;
margin: 0;
max-inline-size: 100%;
padding: 0;
}
.steps__icon {
background-color: var(--card-color);
block-size: 1.3em;
border-radius: 50%;
display: grid;
inline-size: 1.3em;
place-content: center;
.icon {
background-color: var(--color-ink-inverted);
block-size: 0.8em;
inline-size: 0.8em;
}
}
}
+1
View File
@@ -315,6 +315,7 @@
.card__meta .btn,
.card__meta-item:not(.card__meta-item--updated),
.card__stages,
.card__steps,
.card__closed {
display: none;
}
+35
View File
@@ -0,0 +1,35 @@
class Cards::StepsController < ApplicationController
include CardScoped
before_action :set_step, only: %i[ show edit update destroy ]
def create
@step = @card.steps.create!(step_params)
render_card_replacement
end
def show
end
def edit
end
def update
@step.update!(step_params)
render_card_replacement
end
def destroy
@step.destroy!
render_card_replacement
end
private
def set_step
@step = @card.steps.find(params[:id])
end
def step_params
params.require(:step).permit(:content, :completed)
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
class Card < ApplicationRecord
include Assignable, Colored, Engageable, Entropic, Eventable,
Golden, Mentions, Pinnable, Closeable, Readable, Searchable,
Golden, Mentions, Multistep, Pinnable, Closeable, Readable, Searchable,
Staged, Stallable, Statuses, Taggable, Watchable
belongs_to :collection, touch: true
+11
View File
@@ -0,0 +1,11 @@
module Card::Multistep
extend ActiveSupport::Concern
included do
has_many :steps, dependent: :destroy
end
def completed_steps_count
steps.where(completed: true).count
end
end
+7
View File
@@ -0,0 +1,7 @@
class Step < ApplicationRecord
belongs_to :card, touch: true
def completed?
completed
end
end
+2
View File
@@ -19,6 +19,8 @@
<%= render "cards/stagings/stages", card: card if card.doing? %>
</div>
<%= render "cards/display/perma/steps", card: card %>
<footer class="card__footer full-width flex align-start gap">
<%= render "cards/display/perma/meta", card: card %>
<%= render "cards/display/perma/background", card: card %>
@@ -6,6 +6,7 @@
<header class="card__header">
<%= render "cards/display/preview/collection", card: card %>
<%= render "cards/display/preview/tags", card: card %>
<%= render "cards/display/preview/steps", card: card %>
<% if card.staged? && card.doing? %>
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
@@ -0,0 +1,10 @@
<ol class="steps txt-small margin-block">
<%= render partial: "cards/steps/step", collection: card.steps, as: :step %>
<div class="step">
<input type="checkbox" class="step__checkbox" disabled>
<%= form_with model: [card, Step.new], url: card_steps_path(card) do |form| %>
<%= form.text_field :content, class: "input step__content", placeholder: "Add a step…", required: true, autocomplete: "off" %>
<% end %>
</div>
</ol>
@@ -0,0 +1,8 @@
<% if card.steps.any? %>
<div class="card__steps align-center gap-half flex-item-justify-end flex-item-no-shrink">
<span class="steps__icon">
<%= icon_tag "check" %>
</span>
<strong><%= "#{card.completed_steps_count}/#{card.steps.count}" %></strong>
</div>
<% end %>
+8
View File
@@ -0,0 +1,8 @@
<%= turbo_frame_tag step do %>
<li class="step">
<%= form_with model: [step.card, step], data: { controller: "form" } do |form| %>
<%= form.check_box :completed, { class: "step__checkbox", data: { action: "change->form#submit" } } %>
<% end %>
<%= link_to step.content, edit_card_step_path(step.card, step), class: "step__content" %>
</li>
<% end %>
+18
View File
@@ -0,0 +1,18 @@
<%= turbo_frame_tag @step do %>
<div class="flex align-center gap-half">
<%= form_with model: [@card, @step], class: "step", data: { controller: "form" } do |form| %>
<%= form.check_box :completed, { class: "step__checkbox", checked: @step.completed?, disabled: true } %>
<%= form.text_field :content, class: "input step__content step__content--edit", placeholder: "Name this step…", required: true, autofocus: true, autocomplete: "off",
data: { action: "keydown.esc->form#cancel focus->form#select" } %>
<%= form.button type: "submit", class: "btn btn--positive txt-xx-small" do %>
<%= icon_tag "check" %>
<span class="for-screen-reader">Save changes</span>
<% end %>
<%= link_to "Cancel changes", card_step_path(@card, @step), data: { form_target: "cancel" }, hidden: true %>
<% end %>
<%= button_to card_step_path(@card, @step), method: :delete, class: "btn btn--negative txt-xx-small" do %>
<%= icon_tag "trash" %>
<span class="for-screen-reader">Delete this step</span>
<% end %>
</div>
<% end %>
+3
View File
@@ -0,0 +1,3 @@
<%= turbo_frame_tag @step do %>
<%= render "cards/steps/step", step: @step %>
<% end %>
+1
View File
@@ -44,6 +44,7 @@ Rails.application.routes.draw do
resources :assignments
resources :taggings
resources :steps
resources :comments do
resources :reactions, module: :comments
@@ -0,0 +1,13 @@
class AddStepsToCards < ActiveRecord::Migration[8.1]
def change
create_table :steps do |t|
t.references :card, null: false, foreign_key: true
t.text :content, null: false
t.boolean :completed, default: false, null: false
t.timestamps
end
add_index :steps, :completed
end
end
Generated
+12 -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_06_25_111932) do
ActiveRecord::Schema[8.1].define(version: 2025_06_25_120000) do
create_table "accesses", force: :cascade do |t|
t.datetime "accessed_at"
t.integer "collection_id", null: false
@@ -325,6 +325,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_25_111932) do
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "steps", force: :cascade do |t|
t.integer "card_id", null: false
t.boolean "completed", default: false, null: false
t.text "content", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["card_id"], name: "index_steps_on_card_id"
t.index ["completed"], name: "index_steps_on_completed"
end
create_table "taggings", force: :cascade do |t|
t.integer "card_id", null: false
t.datetime "created_at", null: false
@@ -400,6 +410,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_25_111932) do
add_foreign_key "pins", "users"
add_foreign_key "search_queries", "users"
add_foreign_key "sessions", "users"
add_foreign_key "steps", "cards"
add_foreign_key "taggings", "cards"
add_foreign_key "taggings", "tags"
add_foreign_key "watches", "cards"
+71 -11
View File
@@ -451,7 +451,7 @@ columns:
default_function:
collation:
comment:
- &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- &33 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: title
cast_type: *7
@@ -520,11 +520,11 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: all_access
cast_type: &32 !ruby/object:ActiveModel::Type::Boolean
cast_type: &30 !ruby/object:ActiveModel::Type::Boolean
precision:
scale:
limit:
sql_type_metadata: &33 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type_metadata: &31 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
sql_type: boolean
type: :boolean
limit:
@@ -744,7 +744,7 @@ columns:
comment:
filters_tags:
- *19
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: tag_id
cast_type: *3
@@ -940,23 +940,48 @@ columns:
collation:
comment:
- *25
steps:
- *22
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: completed
cast_type: *30
sql_type_metadata: *31
'null': false
default: false
default_function:
collation:
comment:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: content
cast_type: *15
sql_type_metadata: *16
'null': false
default:
default_function:
collation:
comment:
- *5
- *6
- *9
taggings:
- *22
- *5
- *6
- *30
- *32
- *9
tags:
- *5
- *6
- *31
- *33
- *9
users:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: active
cast_type: *32
sql_type_metadata: *33
cast_type: *30
sql_type_metadata: *31
'null': false
default: true
default_function:
@@ -1015,8 +1040,8 @@ columns:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
name: watching
cast_type: *32
sql_type_metadata: *33
cast_type: *30
sql_type_metadata: *31
'null': false
default: true
default_function:
@@ -1088,6 +1113,7 @@ primary_keys:
search_queries: id
search_results: id
sessions: id
steps: id
taggings: id
tags: id
users: id
@@ -1130,6 +1156,7 @@ data_sources:
search_queries: true
search_results: true
sessions: true
steps: true
taggings: true
tags: true
users: true
@@ -2256,6 +2283,39 @@ indexes:
nulls_not_distinct:
comment:
valid: true
steps:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: steps
name: index_steps_on_card_id
unique: false
columns:
- card_id
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: steps
name: index_steps_on_completed
unique: false
columns:
- completed
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
taggings:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: taggings
@@ -2391,4 +2451,4 @@ indexes:
comment:
valid: true
workflows: []
version: 20250625111932
version: 20250625120000
@@ -0,0 +1,55 @@
require "test_helper"
class Cards::StepsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "create" do
card = cards(:logo)
assert_difference -> { card.steps.count }, +1 do
post card_steps_path(card), params: { step: { content: "Research alternatives" } }, as: :turbo_stream
assert_card_container_rerendered(card)
end
assert_equal "Research alternatives", card.steps.last.content
end
test "update" do
card = cards(:logo)
step = card.steps.create!(content: "Original content")
assert_changes -> { step.reload.content }, from: "Original content", to: "Updated content" do
put card_step_path(card, step), params: { step: { content: "Updated content" } }, as: :turbo_stream
assert_card_container_rerendered(card)
end
end
test "destroy" do
card = cards(:logo)
step = card.steps.create!(content: "Step to delete")
assert_difference -> { card.steps.count }, -1 do
delete card_step_path(card, step), as: :turbo_stream
assert_card_container_rerendered(card)
end
end
test "toggle completion" do
card = cards(:logo)
step = card.steps.create!(content: "Test step", completed: false)
# Toggle to completed
assert_changes -> { step.reload.completed? }, from: false, to: true do
put card_step_path(card, step), params: { step: { completed: "1" } }, as: :turbo_stream
assert_card_container_rerendered(card)
end
# Toggle back to incomplete
assert_changes -> { step.reload.completed? }, from: true, to: false do
put card_step_path(card, step), params: { step: { completed: "0" } }, as: :turbo_stream
assert_card_container_rerendered(card)
end
end
end