Add steps to cards
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 "); }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,6 +315,7 @@
|
||||
.card__meta .btn,
|
||||
.card__meta-item:not(.card__meta-item--updated),
|
||||
.card__stages,
|
||||
.card__steps,
|
||||
.card__closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
class Step < ApplicationRecord
|
||||
belongs_to :card, touch: true
|
||||
|
||||
def completed?
|
||||
completed
|
||||
end
|
||||
end
|
||||
@@ -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 %>
|
||||
@@ -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 %>
|
||||
@@ -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 %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%= turbo_frame_tag @step do %>
|
||||
<%= render "cards/steps/step", step: @step %>
|
||||
<% end %>
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user