plutonium 0.60.5 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +19 -1
- data/.claude/skills/plutonium-app/SKILL.md +41 -0
- data/.claude/skills/plutonium-auth/SKILL.md +40 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +47 -1
- data/.claude/skills/plutonium-kanban/SKILL.md +378 -0
- data/.claude/skills/plutonium-resource/SKILL.md +40 -0
- data/.claude/skills/plutonium-tenancy/SKILL.md +43 -0
- data/.claude/skills/plutonium-testing/SKILL.md +38 -0
- data/.claude/skills/plutonium-ui/SKILL.md +51 -0
- data/.claude/skills/plutonium-wizard/SKILL.md +469 -0
- data/.cliff.toml +6 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +576 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +1173 -194
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +53 -53
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/layouts/basic.html.erb +7 -0
- data/app/views/plutonium/_flash_toasts.html.erb +2 -46
- data/app/views/plutonium/_toast.html.erb +52 -0
- data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
- data/app/views/resource/_resource_kanban.html.erb +1 -0
- data/app/views/resource/kanban_move_form.html.erb +1 -0
- data/config/brakeman.ignore +2 -2
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +45 -1
- data/docs/.vitepress/sync-skills.mjs +45 -0
- data/docs/ai.md +99 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +557 -0
- data/docs/guides/wizards.md +447 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +14 -0
- data/docs/reference/kanban/authorization.md +82 -0
- data/docs/reference/kanban/dsl.md +334 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +26 -1
- data/docs/reference/resource/model.md +26 -0
- data/docs/reference/ui/forms.md +77 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +337 -0
- data/docs/reference/wizard/index.md +33 -0
- data/docs/reference/wizard/one-time.md +129 -0
- data/docs/reference/wizard/registration-launch.md +177 -0
- data/docs/reference/wizard/storage-config.md +151 -0
- data/docs/superpowers/plans/2026-06-14-form-sectioning.md +2 -2
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md +1619 -0
- data/docs/superpowers/plans/2026-06-15-wizard-dsl.md.tasks.json +68 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md +1128 -0
- data/docs/superpowers/plans/2026-06-26-kanban-dsl.md.tasks.json +24 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md +714 -0
- data/docs/superpowers/plans/2026-07-02-kanban-drop-interactions.md.tasks.json +68 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-design.md +836 -0
- data/docs/superpowers/specs/2026-06-15-wizard-dsl-examples.rb +245 -0
- data/docs/superpowers/specs/2026-06-17-wizard-relaunch-prompt-design.md +86 -0
- data/docs/superpowers/specs/2026-06-18-wizard-attachments-design.md +101 -0
- data/docs/superpowers/specs/2026-06-18-wizard-hosting-design.md +220 -0
- data/docs/superpowers/specs/2026-06-26-kanban-dsl-design.md +388 -0
- data/docs/superpowers/specs/2026-07-03-kanban-auth-simplification.md +159 -0
- data/gemfiles/postgres.gemfile +8 -0
- data/gemfiles/postgres.gemfile.lock +321 -0
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +14 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +17 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +16 -0
- data/lib/plutonium/core/controller.rb +20 -1
- data/lib/plutonium/definition/base.rb +25 -0
- data/lib/plutonium/definition/form_layout.rb +54 -35
- data/lib/plutonium/definition/index_views.rb +70 -1
- data/lib/plutonium/definition/wizards.rb +209 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +9 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +9 -0
- data/lib/plutonium/invites/controller.rb +4 -1
- data/lib/plutonium/kanban/action.rb +7 -0
- data/lib/plutonium/kanban/board.rb +40 -0
- data/lib/plutonium/kanban/broadcaster.rb +54 -0
- data/lib/plutonium/kanban/column.rb +122 -0
- data/lib/plutonium/kanban/context.rb +15 -0
- data/lib/plutonium/kanban/dsl.rb +71 -0
- data/lib/plutonium/kanban/grouping.rb +51 -0
- data/lib/plutonium/kanban/positioning.rb +75 -0
- data/lib/plutonium/kanban.rb +11 -0
- data/lib/plutonium/migrations.rb +40 -0
- data/lib/plutonium/models/has_cents.rb +30 -2
- data/lib/plutonium/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +24 -1
- data/lib/plutonium/resource/controllers/crud_actions.rb +9 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +851 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +14 -0
- data/lib/plutonium/routing/mapper_extensions.rb +45 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/components/currency.rb +41 -9
- data/lib/plutonium/ui/display/options/inferred_types.rb +2 -5
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +25 -5
- data/lib/plutonium/ui/form/components/currency.rb +64 -0
- data/lib/plutonium/ui/form/components/intl_tel_input.rb +27 -1
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +26 -5
- data/lib/plutonium/ui/form/kanban_move.rb +46 -0
- data/lib/plutonium/ui/form/options/inferred_types.rb +26 -0
- data/lib/plutonium/ui/form/resource.rb +13 -1
- data/lib/plutonium/ui/form/theme.rb +7 -0
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +56 -18
- data/lib/plutonium/ui/kanban/card.rb +67 -0
- data/lib/plutonium/ui/kanban/color_dot.rb +36 -0
- data/lib/plutonium/ui/kanban/column.rb +411 -0
- data/lib/plutonium/ui/kanban/resource.rb +319 -0
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/resource_layout.rb +7 -1
- data/lib/plutonium/ui/modal/base.rb +30 -3
- data/lib/plutonium/ui/modal/centered.rb +5 -2
- data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
- data/lib/plutonium/ui/page/index.rb +2 -1
- data/lib/plutonium/ui/page/interactive_action.rb +12 -2
- data/lib/plutonium/ui/page/kanban_move.rb +20 -0
- data/lib/plutonium/ui/page/show.rb +29 -1
- data/lib/plutonium/ui/page/wizard.rb +371 -0
- data/lib/plutonium/ui/page/wizard_chooser.rb +97 -0
- data/lib/plutonium/ui/page/wizard_completed.rb +86 -0
- data/lib/plutonium/ui/table/base.rb +1 -1
- data/lib/plutonium/ui/table/components/view_switcher.rb +2 -1
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/ui/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +92 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachment_data.rb +42 -0
- data/lib/plutonium/wizard/attachments.rb +226 -0
- data/lib/plutonium/wizard/base.rb +216 -0
- data/lib/plutonium/wizard/base_controller.rb +31 -0
- data/lib/plutonium/wizard/configuration.rb +42 -0
- data/lib/plutonium/wizard/controller.rb +162 -0
- data/lib/plutonium/wizard/data.rb +134 -0
- data/lib/plutonium/wizard/driving.rb +639 -0
- data/lib/plutonium/wizard/dsl.rb +336 -0
- data/lib/plutonium/wizard/errors.rb +27 -0
- data/lib/plutonium/wizard/field_capture.rb +157 -0
- data/lib/plutonium/wizard/field_importer.rb +208 -0
- data/lib/plutonium/wizard/gate.rb +171 -0
- data/lib/plutonium/wizard/instance_key.rb +97 -0
- data/lib/plutonium/wizard/lazy_persisted.rb +77 -0
- data/lib/plutonium/wizard/resume.rb +250 -0
- data/lib/plutonium/wizard/review_step.rb +48 -0
- data/lib/plutonium/wizard/route_resolution.rb +40 -0
- data/lib/plutonium/wizard/runner.rb +684 -0
- data/lib/plutonium/wizard/session.rb +53 -0
- data/lib/plutonium/wizard/state.rb +35 -0
- data/lib/plutonium/wizard/step.rb +61 -0
- data/lib/plutonium/wizard/step_adapter.rb +103 -0
- data/lib/plutonium/wizard/store/active_record.rb +174 -0
- data/lib/plutonium/wizard/store/base.rb +42 -0
- data/lib/plutonium/wizard/store/memory.rb +44 -0
- data/lib/plutonium/wizard/sweep_job.rb +76 -0
- data/lib/plutonium/wizard.rb +86 -0
- data/lib/plutonium.rb +5 -0
- data/lib/rodauth/features/case_insensitive_login.rb +1 -1
- data/lib/tasks/release.rake +144 -191
- data/package.json +7 -5
- data/src/css/components.css +137 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/currency_input_controller.js +39 -0
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/intl_tel_input_controller.js +4 -0
- data/src/js/controllers/kanban_controller.js +717 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +8 -0
- data/src/js/controllers/remote_modal_controller.js +10 -0
- data/src/js/controllers/row_click_controller.js +14 -1
- data/src/js/controllers/wizard_controller.js +54 -0
- data/src/js/turbo/turbo_confirm.js +1 -1
- data/yarn.lock +945 -286
- metadata +113 -2
|
@@ -8,18 +8,24 @@ module Plutonium
|
|
|
8
8
|
# `grid_fields` on the resource definition. Each slot is optional;
|
|
9
9
|
# `header` falls back to `record.to_label` when undeclared.
|
|
10
10
|
class Card < Plutonium::UI::Component::Base
|
|
11
|
-
attr_reader :record, :resource_definition, :resource_fields
|
|
11
|
+
attr_reader :record, :resource_definition, :resource_fields, :card_fields
|
|
12
12
|
|
|
13
|
-
def initialize(record, resource_definition:, resource_fields: nil)
|
|
13
|
+
def initialize(record, resource_definition:, resource_fields: nil, card_fields: nil, show_turbo_frame: nil)
|
|
14
14
|
@record = record
|
|
15
15
|
@resource_definition = resource_definition
|
|
16
16
|
@resource_fields = resource_fields
|
|
17
|
+
@card_fields = card_fields
|
|
18
|
+
# Overrides the show link's turbo-frame target. Defaults to the show
|
|
19
|
+
# action's own frame (nil → normal navigation). The kanban board sets
|
|
20
|
+
# "_top" so a card click escapes its column's lazy turbo-frame instead
|
|
21
|
+
# of loading the show page inside the column.
|
|
22
|
+
@show_turbo_frame = show_turbo_frame
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
def view_template
|
|
20
26
|
article(
|
|
21
27
|
class: card_class,
|
|
22
|
-
data: {controller: "row-click", action: "click->row-click#click"}
|
|
28
|
+
data: {controller: "row-click", action: "click->row-click#click auxclick->row-click#click"}
|
|
23
29
|
) do
|
|
24
30
|
render_show_link if can_show?
|
|
25
31
|
render_actions_dropdown
|
|
@@ -32,7 +38,12 @@ module Plutonium
|
|
|
32
38
|
|
|
33
39
|
private
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
# Returns the slot hash used for rendering.
|
|
42
|
+
# When the kanban board declares `card_fields`, it is passed in
|
|
43
|
+
# explicitly and takes precedence over the resource definition's
|
|
44
|
+
# `defined_grid_fields`. A nil card_fields falls back to the
|
|
45
|
+
# definition, which is the default for the grid view.
|
|
46
|
+
def slots = @card_fields || resource_definition.defined_grid_fields
|
|
36
47
|
|
|
37
48
|
# ---------------------------------------------------------------
|
|
38
49
|
# Layout shells
|
|
@@ -116,14 +127,13 @@ module Plutonium
|
|
|
116
127
|
end
|
|
117
128
|
|
|
118
129
|
def render_meta_slot
|
|
119
|
-
|
|
120
|
-
values = fields.map { |f| field_value(f) }.reject(&:blank?)
|
|
130
|
+
pairs = Array(slots[:meta]).map { |name| [name, field_value(name)] }.reject { |_, value| value.blank? }
|
|
121
131
|
|
|
122
132
|
div(class: "flex flex-wrap items-center gap-1.5 mt-1") do
|
|
123
|
-
if
|
|
133
|
+
if pairs.empty?
|
|
124
134
|
render_blank_placeholder
|
|
125
135
|
else
|
|
126
|
-
|
|
136
|
+
pairs.each { |name, value| render_meta_badge(name, value) }
|
|
127
137
|
end
|
|
128
138
|
end
|
|
129
139
|
end
|
|
@@ -147,21 +157,38 @@ module Plutonium
|
|
|
147
157
|
# rendered by the timeago Stimulus controller.
|
|
148
158
|
raw safe(helpers.display_datetime_value(value))
|
|
149
159
|
elsif currency_field?(name)
|
|
150
|
-
plain helpers.number_to_currency(value, unit:
|
|
160
|
+
plain helpers.number_to_currency(value, unit: currency_unit_for(name))
|
|
151
161
|
else
|
|
152
162
|
plain helpers.display_name_of(value)
|
|
153
163
|
end
|
|
154
164
|
end
|
|
155
165
|
|
|
156
|
-
# Renders a meta value as a colored pill, borrowing the Badge
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
166
|
+
# Renders a meta value as a colored pill, borrowing the Badge display
|
|
167
|
+
# component's semantic color + humanize logic. Non-string types are
|
|
168
|
+
# formatted by type first so they don't badge their raw value:
|
|
169
|
+
# - has_cents columns → currency (matches render_formatted_value)
|
|
170
|
+
# - associations → display_name_of (label, not an object inspect)
|
|
171
|
+
# - everything else → humanized, with the RAW value driving the
|
|
172
|
+
# variant so status-like enums (in_progress, published…) still
|
|
173
|
+
# resolve to a semantic color.
|
|
174
|
+
# The variant hashes the stable formatted label for currency/associations,
|
|
175
|
+
# so the decorative color no longer churns on an object's memory address.
|
|
176
|
+
def render_meta_badge(name, value)
|
|
161
177
|
badge = Plutonium::UI::Display::Components::Badge
|
|
162
|
-
|
|
178
|
+
|
|
179
|
+
if currency_field?(name)
|
|
180
|
+
label = helpers.number_to_currency(value, unit: currency_unit_for(name))
|
|
181
|
+
variant = badge.variant_for(label)
|
|
182
|
+
elsif association_field?(name)
|
|
183
|
+
label = helpers.display_name_of(value)
|
|
184
|
+
variant = badge.variant_for(label)
|
|
185
|
+
else
|
|
186
|
+
label = badge.humanize(value)
|
|
187
|
+
variant = badge.variant_for(value)
|
|
188
|
+
end
|
|
189
|
+
|
|
163
190
|
span(class: tokens("pu-badge", "pu-badge-#{variant}")) do
|
|
164
|
-
plain
|
|
191
|
+
plain label
|
|
165
192
|
end
|
|
166
193
|
end
|
|
167
194
|
|
|
@@ -170,6 +197,17 @@ module Plutonium
|
|
|
170
197
|
klass.respond_to?(:has_cents_decimal_attribute?) && klass.has_cents_decimal_attribute?(name.to_sym)
|
|
171
198
|
end
|
|
172
199
|
|
|
200
|
+
# Delegates to the shared resolver so cards format currency identically to
|
|
201
|
+
# the Currency display component — has_cents unit → configured/i18n default,
|
|
202
|
+
# with `false` meaning no symbol. Cards have no per-display unit (nil).
|
|
203
|
+
def currency_unit_for(name)
|
|
204
|
+
Plutonium::UI::Display::Components::Currency.resolve_unit(nil, record, name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def association_field?(name)
|
|
208
|
+
record.class.reflect_on_association(name.to_sym).present?
|
|
209
|
+
end
|
|
210
|
+
|
|
173
211
|
# A declared slot with no value renders a muted em-dash rather than
|
|
174
212
|
# collapsing, so cards in a grid keep an even height instead of
|
|
175
213
|
# ragged rows when some records lack the field.
|
|
@@ -201,7 +239,7 @@ module Plutonium
|
|
|
201
239
|
url = route_options_to_url(show.route_options, record)
|
|
202
240
|
a(
|
|
203
241
|
href: url,
|
|
204
|
-
data: {row_click_target: "show", turbo_frame: show.turbo_frame(resource_definition)},
|
|
242
|
+
data: {row_click_target: "show", turbo_frame: @show_turbo_frame || show.turbo_frame(resource_definition)},
|
|
205
243
|
class: "sr-only",
|
|
206
244
|
tabindex: "-1",
|
|
207
245
|
"aria-label": "Open #{header_text}"
|
|
@@ -232,7 +270,7 @@ module Plutonium
|
|
|
232
270
|
|
|
233
271
|
def row_actions
|
|
234
272
|
@row_actions ||= resource_definition.defined_actions.values.select { |a|
|
|
235
|
-
a.collection_record_action? && a.permitted_by?(record_policy) && a.condition_met?(view_context, record:)
|
|
273
|
+
a.collection_record_action? && !a.kanban_drop? && a.permitted_by?(record_policy) && a.condition_met?(view_context, record:)
|
|
236
274
|
}
|
|
237
275
|
end
|
|
238
276
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Kanban
|
|
6
|
+
# Thin wrapper around Grid::Card that makes the card draggable.
|
|
7
|
+
#
|
|
8
|
+
# Emits a draggable container div carrying:
|
|
9
|
+
# data-kanban-record-id — the record's id, consumed by the Stimulus
|
|
10
|
+
# drag controller (Task 11) to identify the card
|
|
11
|
+
# data-kanban-column-key — the source column key, used by the move
|
|
12
|
+
# handler (Task 7) to determine which column
|
|
13
|
+
# the card came from
|
|
14
|
+
#
|
|
15
|
+
# All actual card rendering (image, header, meta, actions dropdown) is
|
|
16
|
+
# delegated to Plutonium::UI::Grid::Card which already handles slots,
|
|
17
|
+
# policy-gated actions, and the row-click controller.
|
|
18
|
+
class Card < Plutonium::UI::Component::Base
|
|
19
|
+
attr_reader :record, :column_key, :resource_definition, :resource_fields, :card_fields
|
|
20
|
+
|
|
21
|
+
def initialize(record, column_key:, resource_definition:, resource_fields:, card_fields: nil, show_turbo_frame: "_top")
|
|
22
|
+
@record = record
|
|
23
|
+
@column_key = column_key
|
|
24
|
+
@resource_definition = resource_definition
|
|
25
|
+
@resource_fields = resource_fields
|
|
26
|
+
# Optional slot-layout override from the board's card_fields declaration.
|
|
27
|
+
# Threaded through to Grid::Card so it takes precedence over the
|
|
28
|
+
# resource definition's grid_fields. nil means use the definition.
|
|
29
|
+
@card_fields = card_fields
|
|
30
|
+
# The turbo-frame the card's show link targets — the remote-modal frame
|
|
31
|
+
# (board show_in :modal) or "_top" (show_in :page). Either escapes the
|
|
32
|
+
# column's lazy frame. Defaults to "_top".
|
|
33
|
+
@show_turbo_frame = show_turbo_frame
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def view_template
|
|
37
|
+
div(
|
|
38
|
+
draggable: "true",
|
|
39
|
+
data: {
|
|
40
|
+
kanban_record_id: record.id,
|
|
41
|
+
kanban_column_key: column_key.to_s
|
|
42
|
+
}
|
|
43
|
+
) do
|
|
44
|
+
render_grid_card
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Extracted so tests can stub render_grid_card without needing a full
|
|
51
|
+
# view_context (Grid::Card calls policy_for, route_options_to_url, etc.).
|
|
52
|
+
def render_grid_card
|
|
53
|
+
render Plutonium::UI::Grid::Card.new(
|
|
54
|
+
record,
|
|
55
|
+
resource_definition: resource_definition,
|
|
56
|
+
resource_fields: resource_fields,
|
|
57
|
+
card_fields: @card_fields,
|
|
58
|
+
# Escape the column's lazy turbo-frame: either "_top" (full page) or
|
|
59
|
+
# the document-wide remote-modal frame, both of which resolve outside
|
|
60
|
+
# the kanban-col-<key> frame.
|
|
61
|
+
show_turbo_frame: @show_turbo_frame
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Kanban
|
|
6
|
+
# Shared color-dot rendering for the board shell placeholder header
|
|
7
|
+
# (Resource) and the loaded column header (Column), so a column's
|
|
8
|
+
# `color:` shows consistently in both states.
|
|
9
|
+
module ColorDot
|
|
10
|
+
def render_color_dot(color)
|
|
11
|
+
span(
|
|
12
|
+
class: "shrink-0 w-2.5 h-2.5 rounded-full",
|
|
13
|
+
style: "background-color: #{color_css_value(color)}"
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Maps a column color symbol to a CSS value via Tailwind design tokens.
|
|
18
|
+
# Raw CSS strings (e.g. "#ff0000") are passed through unchanged.
|
|
19
|
+
def color_css_value(color)
|
|
20
|
+
case color.to_sym
|
|
21
|
+
when :red then "var(--color-red-500)"
|
|
22
|
+
when :orange then "var(--color-orange-500)"
|
|
23
|
+
when :amber then "var(--color-amber-500)"
|
|
24
|
+
when :yellow then "var(--color-yellow-500)"
|
|
25
|
+
when :green then "var(--color-green-500)"
|
|
26
|
+
when :blue then "var(--color-blue-500)"
|
|
27
|
+
when :purple then "var(--color-purple-500)"
|
|
28
|
+
when :pink then "var(--color-pink-500)"
|
|
29
|
+
when :gray then "var(--pu-text-muted)"
|
|
30
|
+
else color.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Kanban
|
|
6
|
+
# Renders a single column body: header with wip badge, card list, and a
|
|
7
|
+
# "+N more" footer when the total exceeds per_column.
|
|
8
|
+
#
|
|
9
|
+
# This is the component that Task 6's lazy frame endpoint serves. The
|
|
10
|
+
# board shell (Resource) wraps each column in a <turbo-frame>; the frame
|
|
11
|
+
# src hits the column endpoint which renders this component as the body.
|
|
12
|
+
#
|
|
13
|
+
# Collapsed variant: when column.collapsed? the component renders a thin
|
|
14
|
+
# rotated-label strip instead of the full card list. Both the strip and
|
|
15
|
+
# the expanded body are always emitted in the HTML; CSS (controlled by
|
|
16
|
+
# the `pu-kanban-column-collapsed` class on the wrapper) shows one and
|
|
17
|
+
# hides the other. The Stimulus controller's toggleColumn action flips the
|
|
18
|
+
# class and persists the choice to localStorage.
|
|
19
|
+
#
|
|
20
|
+
# Action slot: column.actions are rendered as minimal placeholder buttons
|
|
21
|
+
# so the DOM seam is in place for Task 8 which wires the actual action
|
|
22
|
+
# handlers (interactive bulk actions). The buttons carry data-kanban-action
|
|
23
|
+
# and data-kanban-column attributes for Stimulus targeting.
|
|
24
|
+
#
|
|
25
|
+
# Drop policy: the wrapper emits data-kanban-accepts and data-kanban-locked
|
|
26
|
+
# so the Stimulus drag controller can provide client-side drop hints without
|
|
27
|
+
# re-implementing server-side logic. The server remains the authority.
|
|
28
|
+
class Column < Plutonium::UI::Component::Base
|
|
29
|
+
include ColorDot
|
|
30
|
+
include Phlex::Rails::Helpers::LinkTo
|
|
31
|
+
|
|
32
|
+
attr_reader :column, :cards, :total, :per_column, :resource_definition, :resource_fields
|
|
33
|
+
|
|
34
|
+
# column_action_data: array of {action: Plutonium::Kanban::Action, ids: [Integer, ...]}
|
|
35
|
+
# Resolved by the controller (KanbanActions#render_kanban_column_html) and
|
|
36
|
+
# threaded here so the component can render real bulk-action links without
|
|
37
|
+
# needing to re-query the DB. Defaults to [] when the component is
|
|
38
|
+
# constructed outside of a controller context (e.g., tests or the board
|
|
39
|
+
# shell which renders column frames without card data).
|
|
40
|
+
#
|
|
41
|
+
# column_add_url: URL for the "+ Add" quick-add button (or nil).
|
|
42
|
+
# Set by the controller when column.add? is true and the policy permits
|
|
43
|
+
# create. Carries kanban_column=<key> so the new form pre-fills the
|
|
44
|
+
# grouping attribute via apply_kanban_column_defaults!.
|
|
45
|
+
#
|
|
46
|
+
# card_fields: optional slot-layout hash from the board's card_fields
|
|
47
|
+
# declaration (e.g. { header: :title, meta: [:status] }). Threaded
|
|
48
|
+
# through to each Kanban::Card (and ultimately Grid::Card) so it
|
|
49
|
+
# overrides the resource definition's grid_fields for every card in the
|
|
50
|
+
# column. nil means "use the definition's grid_fields" (default).
|
|
51
|
+
# card_show_frame: the turbo-frame each card's show link targets — the
|
|
52
|
+
# remote-modal frame (board show_in :modal) or "_top" (show_in :page).
|
|
53
|
+
# Resolved by the controller and threaded through to Kanban::Card.
|
|
54
|
+
# Defaults to "_top" so a card always escapes the column's lazy frame when
|
|
55
|
+
# the component is built outside a controller (tests, board shell).
|
|
56
|
+
# collapsed: the EFFECTIVE collapse state for this render (the user's
|
|
57
|
+
# cookie-persisted choice resolved against the column default by the
|
|
58
|
+
# controller). nil → fall back to the column's declared default. The
|
|
59
|
+
# component still emits the DEFAULT separately (data-kanban-default-
|
|
60
|
+
# collapsed) so the controller can store only deltas from it.
|
|
61
|
+
# drop_form_url_template: the kanban_move_form member URL with an __ID__
|
|
62
|
+
# placeholder for the dragged card's record id (mirrors the board's
|
|
63
|
+
# move-url template). Set by the controller ONLY for columns that declare
|
|
64
|
+
# an enter_interaction:; nil for plain columns. When present, the wrapper
|
|
65
|
+
# advertises data-kanban-drop-interaction + data-kanban-drop-form-url-
|
|
66
|
+
# template so Task 6's Stimulus controller opens the interaction modal on
|
|
67
|
+
# drop instead of committing the move immediately.
|
|
68
|
+
def initialize(column:, cards:, total:, per_column:, resource_definition:, resource_fields:, column_action_data: [], column_add_url: nil, board_url: nil, card_fields: nil, card_show_frame: "_top", collapsed: nil, drop_form_url_template: nil, drop_immediate: false, drop_confirm: nil)
|
|
69
|
+
@column = column
|
|
70
|
+
@cards = cards
|
|
71
|
+
@total = total
|
|
72
|
+
@per_column = per_column
|
|
73
|
+
@resource_definition = resource_definition
|
|
74
|
+
@resource_fields = resource_fields
|
|
75
|
+
@column_action_data = column_action_data
|
|
76
|
+
@column_add_url = column_add_url
|
|
77
|
+
@board_url = board_url
|
|
78
|
+
@card_fields = card_fields
|
|
79
|
+
@card_show_frame = card_show_frame
|
|
80
|
+
@collapsed = collapsed
|
|
81
|
+
@drop_form_url_template = drop_form_url_template
|
|
82
|
+
@drop_immediate = drop_immediate
|
|
83
|
+
@drop_confirm = drop_confirm
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Effective collapse state: the caller's resolved value, or the column
|
|
87
|
+
# default when none was passed (tests / board shell / non-cookie paths).
|
|
88
|
+
def effective_collapsed?
|
|
89
|
+
@collapsed.nil? ? column.collapsed? : @collapsed
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def view_template
|
|
93
|
+
# Wrapper carries the drop-policy data attributes and the initial
|
|
94
|
+
# collapsed CSS class. The Stimulus controller reads data-kanban-col
|
|
95
|
+
# to find wrappers unambiguously (distinct from toggle button attrs).
|
|
96
|
+
div(
|
|
97
|
+
class: tokens(
|
|
98
|
+
"pu-kanban-column-wrapper",
|
|
99
|
+
effective_collapsed? && "pu-kanban-column-collapsed"
|
|
100
|
+
),
|
|
101
|
+
data: wrapper_data
|
|
102
|
+
) do
|
|
103
|
+
render_collapsed_strip
|
|
104
|
+
render_expanded
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Data attributes for the [data-kanban-col] wrapper. Drop-policy hints
|
|
111
|
+
# (accepts/locked) and the collapse default are always present; the
|
|
112
|
+
# drop-interaction attributes are added only when the column declares an
|
|
113
|
+
# enter_interaction: (advertised via the controller-computed template).
|
|
114
|
+
def wrapper_data
|
|
115
|
+
data = {
|
|
116
|
+
kanban_col: column.key.to_s,
|
|
117
|
+
# The DEFAULT (not the effective state) so the controller can store
|
|
118
|
+
# only a delta from it in the cookie.
|
|
119
|
+
kanban_default_collapsed: column.collapsed?.to_s,
|
|
120
|
+
kanban_accepts: accepts_value,
|
|
121
|
+
kanban_locked: column.locked?.to_s
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if column.enter_interaction?
|
|
125
|
+
data[:kanban_drop_interaction] = "true"
|
|
126
|
+
data[:kanban_drop_form_url_template] = @drop_form_url_template
|
|
127
|
+
# An input-less interaction commits directly on drop (no form modal);
|
|
128
|
+
# the client reads this to skip opening an empty modal. The optional
|
|
129
|
+
# confirm message (auto "<label>?" for immediate actions) gates it.
|
|
130
|
+
data[:kanban_drop_immediate] = "true" if @drop_immediate
|
|
131
|
+
data[:kanban_drop_confirm] = @drop_confirm if @drop_confirm.present?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
data
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------
|
|
138
|
+
# Collapsed strip
|
|
139
|
+
# ---------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def render_collapsed_strip
|
|
142
|
+
# CSS hides this strip when the wrapper lacks pu-kanban-column-collapsed.
|
|
143
|
+
# Always emitted so the JS toggle can switch between strip and body
|
|
144
|
+
# without a server round-trip.
|
|
145
|
+
div(
|
|
146
|
+
class: "pu-kanban-strip w-10 flex flex-col items-center justify-between " \
|
|
147
|
+
"py-3 gap-2 bg-[var(--pu-surface)] border border-[var(--pu-border)] " \
|
|
148
|
+
"rounded-[var(--pu-radius-md)] select-none",
|
|
149
|
+
data: {kanban_role: "strip"}
|
|
150
|
+
) do
|
|
151
|
+
# The color dot is the outcome signal for terminal columns (:done
|
|
152
|
+
# green, :lost red) — keep it visible when collapsed too.
|
|
153
|
+
render_color_dot(column.color) if column.color
|
|
154
|
+
span(
|
|
155
|
+
class: "text-xs font-semibold text-[var(--pu-text-muted)] " \
|
|
156
|
+
"[writing-mode:vertical-lr] rotate-180"
|
|
157
|
+
) { plain column.label }
|
|
158
|
+
span(class: "pu-badge pu-badge-neutral text-xs font-mono") { plain cards.size.to_s }
|
|
159
|
+
# Expand toggle button — the primary interactive seam for
|
|
160
|
+
# collapsing/expanding. data-kanban-column-key is on this button so
|
|
161
|
+
# the integration test can assert the contract without JS execution.
|
|
162
|
+
button(
|
|
163
|
+
class: "p-0.5 rounded text-[var(--pu-text-muted)] " \
|
|
164
|
+
"hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]",
|
|
165
|
+
title: "Expand #{column.label}",
|
|
166
|
+
type: "button",
|
|
167
|
+
data: {
|
|
168
|
+
action: "click->kanban#toggleColumn",
|
|
169
|
+
kanban_column_key: column.key.to_s
|
|
170
|
+
}
|
|
171
|
+
) { plain "▶" }
|
|
172
|
+
# No column actions here: a collapsed column is a thin strip, so its
|
|
173
|
+
# bulk actions (and "+ Add") stay hidden until it's expanded. They
|
|
174
|
+
# live only in the expanded header (render_header).
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------
|
|
179
|
+
# Expanded column
|
|
180
|
+
# ---------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def render_expanded
|
|
183
|
+
# CSS hides this body when the wrapper has pu-kanban-column-collapsed.
|
|
184
|
+
div(
|
|
185
|
+
class: "pu-kanban-body pu-kanban-column w-72 shrink-0 flex flex-col " \
|
|
186
|
+
"bg-[var(--pu-surface-alt)] border border-[var(--pu-border)] " \
|
|
187
|
+
"rounded-[var(--pu-radius-md)] overflow-hidden",
|
|
188
|
+
data: {kanban_role: "body"}
|
|
189
|
+
) do
|
|
190
|
+
render_header
|
|
191
|
+
render_card_list
|
|
192
|
+
render_more_footer if more_count > 0
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def render_header
|
|
197
|
+
div(class: "px-3 py-2 flex items-center justify-between gap-2 border-b border-[var(--pu-border)] bg-[var(--pu-surface)]") do
|
|
198
|
+
div(class: "flex items-center gap-2 min-w-0 flex-1") do
|
|
199
|
+
render_color_dot(column.color) if column.color
|
|
200
|
+
span(class: "font-semibold text-sm text-[var(--pu-text)] truncate") { plain column.label }
|
|
201
|
+
# Always show a card count (matching the collapsed strip); when a
|
|
202
|
+
# WIP limit is set it becomes the "count/limit" badge instead.
|
|
203
|
+
if column.wip
|
|
204
|
+
render_wip_badge
|
|
205
|
+
else
|
|
206
|
+
render_count_badge
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
render_column_actions if @column_add_url || column.actions.any?
|
|
210
|
+
# Collapse toggle — always present in the expanded header so the
|
|
211
|
+
# user can collapse a column even when no other actions are visible.
|
|
212
|
+
# Kept outside render_column_actions so the action-slot tests are
|
|
213
|
+
# unaffected (they check the flex-gap container which is conditional).
|
|
214
|
+
button(
|
|
215
|
+
class: "shrink-0 p-0.5 rounded text-[var(--pu-text-muted)] " \
|
|
216
|
+
"hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]",
|
|
217
|
+
title: "Collapse #{column.label}",
|
|
218
|
+
type: "button",
|
|
219
|
+
data: {
|
|
220
|
+
action: "click->kanban#toggleColumn",
|
|
221
|
+
kanban_column_key: column.key.to_s
|
|
222
|
+
}
|
|
223
|
+
) { plain "◀" }
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Plain card-count badge for columns without a WIP limit — mirrors the
|
|
228
|
+
# count shown on the collapsed strip so the number is visible either way.
|
|
229
|
+
def render_count_badge
|
|
230
|
+
span(class: "pu-badge pu-badge-neutral text-xs font-mono") { plain cards.size.to_s }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def render_wip_badge
|
|
234
|
+
over = wip_over_limit?
|
|
235
|
+
span(
|
|
236
|
+
class: tokens(
|
|
237
|
+
"pu-badge text-xs font-mono",
|
|
238
|
+
over ? "pu-badge-danger" : "pu-badge-neutral"
|
|
239
|
+
),
|
|
240
|
+
title: over ? "WIP limit exceeded" : "WIP limit: #{column.wip}"
|
|
241
|
+
) do
|
|
242
|
+
plain "#{cards.size}/#{column.wip}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------
|
|
247
|
+
# Card list
|
|
248
|
+
# ---------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def render_card_list
|
|
251
|
+
div(
|
|
252
|
+
class: "flex flex-col gap-2 p-2 min-h-[3rem] flex-1",
|
|
253
|
+
data: {
|
|
254
|
+
kanban_target: "column",
|
|
255
|
+
kanban_column_key_value: column.key.to_s
|
|
256
|
+
}
|
|
257
|
+
) do
|
|
258
|
+
render_cards
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Extracted so tests can stub render_cards without needing a real
|
|
263
|
+
# view_context (Grid::Card requires policy_for etc.).
|
|
264
|
+
def render_cards
|
|
265
|
+
cards.each do |record|
|
|
266
|
+
render Plutonium::UI::Kanban::Card.new(
|
|
267
|
+
record,
|
|
268
|
+
column_key: column.key,
|
|
269
|
+
resource_definition: resource_definition,
|
|
270
|
+
resource_fields: resource_fields,
|
|
271
|
+
card_fields: @card_fields,
|
|
272
|
+
show_turbo_frame: @card_show_frame
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------
|
|
278
|
+
# "+N more" footer
|
|
279
|
+
# ---------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def render_more_footer
|
|
282
|
+
div(class: "px-3 py-2 border-t border-[var(--pu-border)] bg-[var(--pu-surface)]") do
|
|
283
|
+
span(class: "text-xs text-[var(--pu-text-muted)]") do
|
|
284
|
+
plain "+#{more_count} more"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------
|
|
290
|
+
# Column action slot
|
|
291
|
+
# ---------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
# Renders the "+ Add" quick-add button that opens the resource's new form
|
|
294
|
+
# in the remote modal frame, pre-seeded for this column.
|
|
295
|
+
def render_add_button
|
|
296
|
+
link_to(
|
|
297
|
+
@column_add_url,
|
|
298
|
+
class: "pu-btn pu-btn-ghost pu-btn-xs text-[var(--pu-text-muted)]",
|
|
299
|
+
title: "Add to #{column.label}",
|
|
300
|
+
data: {turbo_frame: Plutonium::REMOTE_MODAL_FRAME}
|
|
301
|
+
) do
|
|
302
|
+
plain "+ Add"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Renders bulk-action links for each column action, and the "+ Add"
|
|
307
|
+
# quick-add button when column_add_url is present.
|
|
308
|
+
#
|
|
309
|
+
# Each link targets /resources/bulk_actions/:key?ids[]=… — the same path
|
|
310
|
+
# for the GET form and the POST commit. The render mirrors ActionButton so
|
|
311
|
+
# the action's shape is honoured (see #column_action_link_data):
|
|
312
|
+
# • an interaction with NO user inputs is `immediate` → POST + confirm,
|
|
313
|
+
# executed directly instead of opening an empty form modal;
|
|
314
|
+
# • one with inputs opens the interaction form in the remote modal.
|
|
315
|
+
#
|
|
316
|
+
# The action is only rendered when it's registered in defined_actions
|
|
317
|
+
# (auto-registered by Definition::IndexViews.kanban) and permitted by the
|
|
318
|
+
# policy. The bulk endpoint re-authorizes each record, so this is a
|
|
319
|
+
# display-only gate, not the security boundary.
|
|
320
|
+
def render_column_actions
|
|
321
|
+
div(class: "flex items-center gap-1 shrink-0") do
|
|
322
|
+
render_add_button if @column_add_url
|
|
323
|
+
@column_action_data.each do |entry|
|
|
324
|
+
col_action = entry[:action]
|
|
325
|
+
ids = entry[:ids]
|
|
326
|
+
|
|
327
|
+
registered = current_definition.defined_actions[col_action.key]
|
|
328
|
+
next unless registered&.permitted_by?(current_policy)
|
|
329
|
+
|
|
330
|
+
# Skip when the resolved id set is empty: resource_url_for with
|
|
331
|
+
# ids: [] would resolve to the RESOURCE action route
|
|
332
|
+
# (/resource_actions/:key) rather than the bulk route, misfiring
|
|
333
|
+
# if clicked. An empty column simply renders no action link.
|
|
334
|
+
next if ids.empty?
|
|
335
|
+
|
|
336
|
+
# return_to the board so the action redirects back to it (not the
|
|
337
|
+
# table index) AND the board refreshes: the board-bound redirect is
|
|
338
|
+
# tagged with kanban_reload (KanbanActions#kanban_reload_url), so the
|
|
339
|
+
# column re-fetches and the mutated cards (e.g. archived) update.
|
|
340
|
+
url = resource_url_for(resource_class, interaction: col_action.key, ids: ids, return_to: @board_url)
|
|
341
|
+
label = col_action.label || col_action.key.to_s.humanize
|
|
342
|
+
|
|
343
|
+
link_to(
|
|
344
|
+
url,
|
|
345
|
+
class: "pu-btn pu-btn-ghost pu-btn-xs text-[var(--pu-text-muted)]",
|
|
346
|
+
title: label,
|
|
347
|
+
data: column_action_link_data(col_action, registered)
|
|
348
|
+
) do
|
|
349
|
+
render col_action.icon.new(class: "h-4 w-4") if col_action.icon
|
|
350
|
+
plain label
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Data attributes for a column-action link, honouring the interaction's
|
|
357
|
+
# shape. `immediate` actions (no inputs) POST straight to the commit route
|
|
358
|
+
# with a confirmation, executed directly; the rest open the form in the
|
|
359
|
+
# remote modal. The confirmation prefers the DSL-supplied one, falling
|
|
360
|
+
# back to the action's own default ("<label>?" for immediate actions).
|
|
361
|
+
def column_action_link_data(col_action, registered)
|
|
362
|
+
data = {
|
|
363
|
+
kanban_action: col_action.key.to_s,
|
|
364
|
+
kanban_column: column.key.to_s
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if registered.immediate
|
|
368
|
+
data[:turbo_method] = :post
|
|
369
|
+
confirmation = col_action.confirmation || registered.confirmation
|
|
370
|
+
data[:turbo_confirm] = confirmation if confirmation.present?
|
|
371
|
+
else
|
|
372
|
+
data[:turbo_frame] = Plutonium::REMOTE_MODAL_FRAME
|
|
373
|
+
data[:turbo_confirm] = col_action.confirmation if col_action.confirmation
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
data
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------
|
|
380
|
+
# Pure helpers
|
|
381
|
+
# ---------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
def wip_over_limit?
|
|
384
|
+
column.wip && cards.size > column.wip
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def more_count
|
|
388
|
+
[total - cards.size, 0].max
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Serialises the column's accepts policy for the data-kanban-accepts
|
|
392
|
+
# attribute consumed by the Stimulus drag controller for drop hints.
|
|
393
|
+
# The server remains the authority; this is display only.
|
|
394
|
+
#
|
|
395
|
+
# true → "all" (any source is accepted)
|
|
396
|
+
# false → "none" (no source is accepted)
|
|
397
|
+
# Array → comma-joined list of accepted source column keys
|
|
398
|
+
# Proc → "all" (per-card predicate; treated as permissive at the
|
|
399
|
+
# column level — server evaluates per record)
|
|
400
|
+
def accepts_value
|
|
401
|
+
case column.accepts
|
|
402
|
+
when true then "all"
|
|
403
|
+
when false then "none"
|
|
404
|
+
when Array then column.accepts.join(",")
|
|
405
|
+
else "all"
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|