plutonium 0.61.0 → 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-kanban/SKILL.md +89 -24
- data/CHANGELOG.md +27 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +315 -38
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +31 -31
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/_kanban_move_action_form.html.erb +1 -0
- data/app/views/resource/kanban_move_form.html.erb +1 -0
- data/config/brakeman.ignore +2 -2
- data/docs/.vitepress/config.ts +21 -1
- data/docs/.vitepress/sync-skills.mjs +45 -0
- data/docs/ai.md +99 -0
- data/docs/guides/kanban.md +128 -18
- data/docs/reference/kanban/authorization.md +25 -5
- data/docs/reference/kanban/dsl.md +49 -8
- data/docs/reference/kanban/index.md +3 -3
- data/docs/reference/kanban/positioning.md +1 -1
- data/docs/reference/resource/definition.md +10 -1
- data/docs/reference/resource/model.md +26 -0
- data/docs/reference/ui/forms.md +41 -0
- data/docs/reference/wizard/dsl.md +5 -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-07-03-kanban-auth-simplification.md +159 -0
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +5 -0
- data/lib/plutonium/action/base.rb +8 -0
- data/lib/plutonium/configuration.rb +12 -0
- data/lib/plutonium/definition/index_views.rb +16 -0
- data/lib/plutonium/kanban/column.rb +80 -27
- data/lib/plutonium/models/has_cents.rb +30 -2
- data/lib/plutonium/resource/controller.rb +22 -1
- data/lib/plutonium/resource/controllers/crud_actions.rb +8 -0
- data/lib/plutonium/resource/controllers/kanban_actions.rb +489 -93
- data/lib/plutonium/resource/policy.rb +6 -0
- data/lib/plutonium/routing/mapper_extensions.rb +1 -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/form/base.rb +6 -0
- 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/uppy.rb +20 -2
- data/lib/plutonium/ui/form/kanban_move.rb +46 -0
- data/lib/plutonium/ui/form/options/inferred_types.rb +6 -0
- data/lib/plutonium/ui/form/resource.rb +12 -0
- data/lib/plutonium/ui/form/theme.rb +7 -0
- data/lib/plutonium/ui/grid/card.rb +40 -13
- data/lib/plutonium/ui/kanban/column.rb +111 -24
- data/lib/plutonium/ui/kanban/resource.rb +118 -11
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/options/has_cents_field.rb +21 -0
- data/lib/plutonium/ui/page/index.rb +1 -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 +7 -2
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +33 -0
- data/lib/plutonium/version.rb +1 -1
- data/package.json +5 -3
- data/src/css/components.css +5 -0
- data/src/js/controllers/currency_input_controller.js +39 -0
- data/src/js/controllers/intl_tel_input_controller.js +4 -0
- data/src/js/controllers/kanban_controller.js +442 -55
- data/src/js/controllers/register_controllers.js +2 -0
- data/yarn.lock +674 -4
- metadata +14 -2
|
@@ -10,6 +10,12 @@ module Plutonium
|
|
|
10
10
|
authorize :entity_scope, allow_nil: true
|
|
11
11
|
authorize :parent, optional: true
|
|
12
12
|
authorize :parent_association, optional: true
|
|
13
|
+
# Supplied only during a kanban drag-move (kanban_move? authorization): the
|
|
14
|
+
# source and destination Plutonium::Kanban::Column. Optional — nil for every
|
|
15
|
+
# other authorization. Lets a policy gate a specific transition, e.g.
|
|
16
|
+
# def kanban_move? = kanban_to&.key == :closed_won ? user.admin? : super
|
|
17
|
+
authorize :kanban_from, optional: true
|
|
18
|
+
authorize :kanban_to, optional: true
|
|
13
19
|
|
|
14
20
|
relation_scope do |relation|
|
|
15
21
|
default_relation_scope(relation)
|
|
@@ -151,6 +151,7 @@ module Plutonium
|
|
|
151
151
|
as: :interactive_record_action
|
|
152
152
|
post "record_actions/:interactive_action", action: :commit_interactive_record_action,
|
|
153
153
|
as: :commit_interactive_record_action
|
|
154
|
+
get "kanban_move_form", action: :kanban_move_form, as: :kanban_move_form
|
|
154
155
|
post "kanban_move", action: :kanban_move, as: :kanban_move
|
|
155
156
|
end
|
|
156
157
|
end
|
|
@@ -6,16 +6,52 @@ module Plutonium
|
|
|
6
6
|
module UI
|
|
7
7
|
module Display
|
|
8
8
|
module Components
|
|
9
|
-
# Renders a numeric value as currency (delimited, 2 decimals).
|
|
10
|
-
# by
|
|
11
|
-
#
|
|
9
|
+
# Renders a numeric value as currency (delimited, 2 decimals). The symbol
|
|
10
|
+
# is resolved by {resolve_unit}: an explicit `unit:` (a literal "£", a
|
|
11
|
+
# Symbol read off the record for per-row currencies, or `false` for no
|
|
12
|
+
# symbol) → the record's `has_cents` unit → `default_currency_unit` /
|
|
13
|
+
# the i18n `number.currency.format.unit` ($ in en).
|
|
12
14
|
#
|
|
13
|
-
# display :price, as: :currency
|
|
15
|
+
# display :price, as: :currency # → configured/i18n default unit
|
|
14
16
|
# display :price, as: :currency, unit: "£"
|
|
15
17
|
# display :price, as: :currency, unit: :currency_symbol
|
|
18
|
+
# display :price, as: :currency, unit: false # no symbol
|
|
16
19
|
class Currency < Phlexi::Display::Components::Base
|
|
17
20
|
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
18
21
|
|
|
22
|
+
# Resolves the currency unit string for a value, shared by this
|
|
23
|
+
# component and the grid/kanban {Grid::Card} so both format currency
|
|
24
|
+
# identically. Precedence, where `nil` means "not set, keep looking"
|
|
25
|
+
# and `false` means "explicitly no symbol, stop":
|
|
26
|
+
# explicit unit → record's has_cents unit → configured/i18n default.
|
|
27
|
+
#
|
|
28
|
+
# @param explicit [String, Symbol, false, nil] a per-display `unit:`.
|
|
29
|
+
# @param record [Object] the record being rendered.
|
|
30
|
+
# @param key [Symbol] the attribute name.
|
|
31
|
+
# @return [String] the unit to pass to number_to_currency ("" for none).
|
|
32
|
+
def self.resolve_unit(explicit, record, key)
|
|
33
|
+
unit = explicit
|
|
34
|
+
unit = record.has_cents_unit_for(key) if unit.nil? && record.respond_to?(:has_cents_unit_for)
|
|
35
|
+
unit = default_unit if unit.nil?
|
|
36
|
+
|
|
37
|
+
case unit
|
|
38
|
+
when nil, false then ""
|
|
39
|
+
when Symbol then record.public_send(unit).to_s
|
|
40
|
+
else unit.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The unit used when nothing more specific is configured. Returns the
|
|
45
|
+
# `default_currency_unit` config verbatim when set (including `false`
|
|
46
|
+
# to disable the symbol); otherwise the i18n `number.currency.format.unit`
|
|
47
|
+
# *if the locale defines it*, else no symbol. We don't hardcode a "$".
|
|
48
|
+
def self.default_unit
|
|
49
|
+
config = Plutonium.configuration.default_currency_unit
|
|
50
|
+
return config unless config.nil?
|
|
51
|
+
|
|
52
|
+
I18n.t("number.currency.format.unit", default: "")
|
|
53
|
+
end
|
|
54
|
+
|
|
19
55
|
def render_value(value)
|
|
20
56
|
p(**attributes) { format_currency(value) }
|
|
21
57
|
end
|
|
@@ -35,11 +71,7 @@ module Plutonium
|
|
|
35
71
|
end
|
|
36
72
|
|
|
37
73
|
def resolved_unit
|
|
38
|
-
|
|
39
|
-
when nil then ""
|
|
40
|
-
when Symbol then field.object.public_send(@unit)
|
|
41
|
-
else @unit
|
|
42
|
-
end
|
|
74
|
+
self.class.resolve_unit(@unit, field.object, field.key)
|
|
43
75
|
end
|
|
44
76
|
|
|
45
77
|
def normalize_value(value) = value
|
|
@@ -5,6 +5,8 @@ module Plutonium
|
|
|
5
5
|
module Display
|
|
6
6
|
module Options
|
|
7
7
|
module InferredTypes
|
|
8
|
+
include Plutonium::UI::Options::HasCentsField
|
|
9
|
+
|
|
8
10
|
private
|
|
9
11
|
|
|
10
12
|
def infer_field_component
|
|
@@ -23,11 +25,6 @@ module Plutonium
|
|
|
23
25
|
super
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
|
-
|
|
27
|
-
def has_cents_field?
|
|
28
|
-
klass = object.class
|
|
29
|
-
klass.respond_to?(:has_cents_decimal_attribute?) && klass.has_cents_decimal_attribute?(key)
|
|
30
|
-
end
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
end
|
|
@@ -63,6 +63,12 @@ module Plutonium
|
|
|
63
63
|
create_component(Components::Flatpickr, :flatpickr, **, &)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Money input: a number field with an optional currency-unit prefix.
|
|
67
|
+
# See {Components::Currency} for how the unit resolves.
|
|
68
|
+
def currency_tag(**, &)
|
|
69
|
+
create_component(Components::Currency, :currency, **, &)
|
|
70
|
+
end
|
|
71
|
+
|
|
66
72
|
def int_tel_input_tag(**, &)
|
|
67
73
|
create_component(Components::IntlTelInput, :int_tel_input, **, &)
|
|
68
74
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Form
|
|
6
|
+
module Components
|
|
7
|
+
# A numeric (money) input with an OPTIONAL currency-unit prefix. The unit
|
|
8
|
+
# is resolved by the SAME rules as the currency *display*
|
|
9
|
+
# ({Display::Components::Currency.resolve_unit}), so the input and the
|
|
10
|
+
# show/index/summary render the same symbol: an explicit `unit:` (a
|
|
11
|
+
# literal "£", a Symbol read off the record, or `false` for none) → the
|
|
12
|
+
# record's `has_cents` unit → `default_currency_unit` / the i18n default.
|
|
13
|
+
# When nothing resolves (or `unit: false`) the prefix is omitted and it's
|
|
14
|
+
# a plain number input.
|
|
15
|
+
#
|
|
16
|
+
# input :price, as: :currency # → configured/i18n default unit
|
|
17
|
+
# input :price, as: :currency, unit: "£"
|
|
18
|
+
# input :price, as: :currency, unit: false # no prefix
|
|
19
|
+
class Currency < Phlexi::Form::Components::Input
|
|
20
|
+
def view_template
|
|
21
|
+
return super if @unit_prefix.blank?
|
|
22
|
+
|
|
23
|
+
# Overlay the unit at the input's left edge; the currency-input
|
|
24
|
+
# Stimulus controller measures this prefix and sets the input's
|
|
25
|
+
# left padding to match, so digits always clear it whatever the
|
|
26
|
+
# symbol's width ("$" vs "GH₵").
|
|
27
|
+
div(class: "relative", data: {controller: "currency-input"}) do
|
|
28
|
+
span(
|
|
29
|
+
class: "pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-sm text-[var(--pu-text-muted)]",
|
|
30
|
+
aria_hidden: "true",
|
|
31
|
+
data: {currency_input_target: "prefix"}
|
|
32
|
+
) { plain @unit_prefix }
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def build_input_attributes
|
|
40
|
+
@unit_prefix = resolve_unit_prefix
|
|
41
|
+
attributes[:type] = :number
|
|
42
|
+
attributes[:inputmode] = "decimal"
|
|
43
|
+
attributes[:step] ||= "0.01"
|
|
44
|
+
# Mark the input so the currency-input controller can measure the
|
|
45
|
+
# prefix and set the exact left padding at connect (see the JS).
|
|
46
|
+
if @unit_prefix.present?
|
|
47
|
+
attributes[:data] = (attributes[:data] || {}).merge(currency_input_target: "field")
|
|
48
|
+
end
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Resolve the prefix and strip `unit:` from the attributes so it never
|
|
55
|
+
# leaks onto the <input>. Returns "" when there's no unit to show.
|
|
56
|
+
def resolve_unit_prefix
|
|
57
|
+
explicit = attributes.delete(:unit)
|
|
58
|
+
Plutonium::UI::Display::Components::Currency.resolve_unit(explicit, field.object, field.key)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -6,7 +6,10 @@ module Plutonium
|
|
|
6
6
|
module Components
|
|
7
7
|
class IntlTelInput < Phlexi::Form::Components::Input
|
|
8
8
|
def view_template
|
|
9
|
-
div(
|
|
9
|
+
div(data: {
|
|
10
|
+
controller: "intl-tel-input",
|
|
11
|
+
intl_tel_input_options_value: @intl_options.to_json
|
|
12
|
+
}) {
|
|
10
13
|
super
|
|
11
14
|
}
|
|
12
15
|
end
|
|
@@ -16,6 +19,29 @@ module Plutonium
|
|
|
16
19
|
def build_input_attributes
|
|
17
20
|
super
|
|
18
21
|
attributes[:data_intl_tel_input_target] = tokens(attributes[:data_intl_tel_input_target], :input)
|
|
22
|
+
@intl_options = build_intl_options
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Options forwarded to the intl-tel-input library via the Stimulus
|
|
26
|
+
# controller's `options` value. Supports a convenient `initial_country:`
|
|
27
|
+
# shortcut plus an `intl_options:` hash for any other library option
|
|
28
|
+
# (keys are the library's own camelCase names, e.g. `separateDialCode`).
|
|
29
|
+
# Both are deleted from `attributes` so they don't leak onto the <input>.
|
|
30
|
+
#
|
|
31
|
+
# input :phone, as: :phone, initial_country: "gh"
|
|
32
|
+
# input :phone, as: :phone, intl_options: {separateDialCode: true, strictMode: false}
|
|
33
|
+
#
|
|
34
|
+
# When no country is given, falls back to
|
|
35
|
+
# `Plutonium.configuration.default_phone_country`.
|
|
36
|
+
def build_intl_options
|
|
37
|
+
options = {}
|
|
38
|
+
if (country = attributes.delete(:initial_country) || Plutonium.configuration.default_phone_country)
|
|
39
|
+
options[:initialCountry] = country
|
|
40
|
+
end
|
|
41
|
+
if (extra = attributes.delete(:intl_options))
|
|
42
|
+
options.merge!(extra)
|
|
43
|
+
end
|
|
44
|
+
options
|
|
19
45
|
end
|
|
20
46
|
end
|
|
21
47
|
end
|
|
@@ -80,8 +80,13 @@ module Plutonium
|
|
|
80
80
|
},
|
|
81
81
|
title: attachment.filename.to_s
|
|
82
82
|
) do
|
|
83
|
-
# Hidden field to preserve the uploaded file
|
|
84
|
-
|
|
83
|
+
# Hidden field to preserve the uploaded file across a re-render.
|
|
84
|
+
# Only when we actually have a signed_id — on a non-JS submit or a
|
|
85
|
+
# validation-failure re-render the blob may be unsaved, and asking
|
|
86
|
+
# for its signed_id would raise. Nothing to preserve → omit it.
|
|
87
|
+
if (signed_id = preservable_signed_id(attachment))
|
|
88
|
+
input(type: :hidden, name: input_name, multiple: attributes[:multiple], value: signed_id, autocomplete: "off", hidden: true)
|
|
89
|
+
end
|
|
85
90
|
|
|
86
91
|
render_preview_content(attachment)
|
|
87
92
|
render_filename(attachment)
|
|
@@ -89,6 +94,19 @@ module Plutonium
|
|
|
89
94
|
end
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
# ActiveStorage delegates signed_id to the blob, and a *new* blob raises
|
|
98
|
+
# "Cannot get a signed_id for a new record" — so skip it there. Other
|
|
99
|
+
# backends (active_shrine, the wizard's Resolved) sign the file data
|
|
100
|
+
# itself and work fine on an unsaved record, so we must NOT gate them on
|
|
101
|
+
# the parent being persisted (that would drop a still-valid token on a
|
|
102
|
+
# validation re-render). Guarding only the new-blob case covers all three.
|
|
103
|
+
def preservable_signed_id(attachment)
|
|
104
|
+
blob = attachment.try(:blob)
|
|
105
|
+
return if blob&.new_record?
|
|
106
|
+
|
|
107
|
+
attachment.signed_id
|
|
108
|
+
end
|
|
109
|
+
|
|
92
110
|
def render_preview_content(attachment)
|
|
93
111
|
a(
|
|
94
112
|
href: attachment.url,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Form
|
|
6
|
+
# The drop-interaction form used by the kanban card-drop modal.
|
|
7
|
+
#
|
|
8
|
+
# Behaves exactly like a standard interaction form (same fields, same
|
|
9
|
+
# namespacing under `interaction[...]`, same submit button) with two
|
|
10
|
+
# deliberate differences:
|
|
11
|
+
#
|
|
12
|
+
# 1. It POSTs to the record's `kanban_move` member route rather than the
|
|
13
|
+
# interaction's own commit URL, so Task 4's atomic handler runs the
|
|
14
|
+
# interaction AND repositions the card in one request.
|
|
15
|
+
# 2. It carries the move context (from_column/to_column/to_index) as
|
|
16
|
+
# top-level hidden fields so the move handler knows where the card
|
|
17
|
+
# came from and where it landed.
|
|
18
|
+
class KanbanMove < Interaction
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# POST to <member>/kanban_move for the dropped record.
|
|
22
|
+
def form_action
|
|
23
|
+
resource_url_for(resource_record!, action: :kanban_move)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def form_template
|
|
27
|
+
render_kanban_move_context
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Emit the move context as top-level params (NOT namespaced under
|
|
32
|
+
# interaction[...]) — the move handler reads params[:from_column] etc.
|
|
33
|
+
# Values come from the GET request's query string.
|
|
34
|
+
def render_kanban_move_context
|
|
35
|
+
{
|
|
36
|
+
from_column: params[:from_column],
|
|
37
|
+
to_column: params[:to_column],
|
|
38
|
+
to_index: params[:to_index]
|
|
39
|
+
}.each do |name, value|
|
|
40
|
+
input(type: :hidden, name: name.to_s, value: value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -5,6 +5,8 @@ module Plutonium
|
|
|
5
5
|
module Form
|
|
6
6
|
module Options
|
|
7
7
|
module InferredTypes
|
|
8
|
+
include Plutonium::UI::Options::HasCentsField
|
|
9
|
+
|
|
8
10
|
private
|
|
9
11
|
|
|
10
12
|
def infer_field_component
|
|
@@ -16,6 +18,10 @@ module Plutonium
|
|
|
16
18
|
# `*_key`, `salt`, ...) — see #secret_field_name?.
|
|
17
19
|
return :password if inferred_string_field_type == :password || secret_field_name?
|
|
18
20
|
|
|
21
|
+
# has_cents decimal accessors render as a currency input (number field
|
|
22
|
+
# + unit prefix), mirroring the display — no explicit `as: :currency`.
|
|
23
|
+
return :currency if has_cents_field?
|
|
24
|
+
|
|
19
25
|
case inferred_field_type
|
|
20
26
|
when :rich_text
|
|
21
27
|
return :markdown
|
|
@@ -202,6 +202,18 @@ module Plutonium
|
|
|
202
202
|
hidden: true,
|
|
203
203
|
data: {controller: "capture-url"}
|
|
204
204
|
|
|
205
|
+
# Kanban quick-add: carry the column key through to the create POST so
|
|
206
|
+
# the controller can apply that column's on_enter + positioning to the
|
|
207
|
+
# new record post-create (the "+ Add" link opens this form with
|
|
208
|
+
# ?kanban_column=<key>). Only emitted when present, so it never appears
|
|
209
|
+
# on ordinary new/edit forms.
|
|
210
|
+
if request.params[:kanban_column].present?
|
|
211
|
+
input name: "kanban_column",
|
|
212
|
+
value: request.params[:kanban_column],
|
|
213
|
+
type: :hidden,
|
|
214
|
+
hidden: true
|
|
215
|
+
end
|
|
216
|
+
|
|
205
217
|
if in_modal?
|
|
206
218
|
div(class: "shrink-0 px-6 py-3 " \
|
|
207
219
|
"bg-[var(--pu-surface)] border-t border-[var(--pu-border)] " \
|
|
@@ -80,6 +80,13 @@ module Plutonium
|
|
|
80
80
|
invalid_flatpickr: :invalid_input,
|
|
81
81
|
neutral_flatpickr: :neutral_input,
|
|
82
82
|
|
|
83
|
+
# Currency (money input; the number field styles as a normal input,
|
|
84
|
+
# the unit prefix is overlaid by the component)
|
|
85
|
+
currency: :input,
|
|
86
|
+
valid_currency: :valid_input,
|
|
87
|
+
invalid_currency: :invalid_input,
|
|
88
|
+
neutral_currency: :neutral_input,
|
|
89
|
+
|
|
83
90
|
# Int tel input
|
|
84
91
|
int_tel_input: :input,
|
|
85
92
|
valid_int_tel_input: :valid_input,
|
|
@@ -127,14 +127,13 @@ module Plutonium
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def render_meta_slot
|
|
130
|
-
|
|
131
|
-
values = fields.map { |f| field_value(f) }.reject(&:blank?)
|
|
130
|
+
pairs = Array(slots[:meta]).map { |name| [name, field_value(name)] }.reject { |_, value| value.blank? }
|
|
132
131
|
|
|
133
132
|
div(class: "flex flex-wrap items-center gap-1.5 mt-1") do
|
|
134
|
-
if
|
|
133
|
+
if pairs.empty?
|
|
135
134
|
render_blank_placeholder
|
|
136
135
|
else
|
|
137
|
-
|
|
136
|
+
pairs.each { |name, value| render_meta_badge(name, value) }
|
|
138
137
|
end
|
|
139
138
|
end
|
|
140
139
|
end
|
|
@@ -158,21 +157,38 @@ module Plutonium
|
|
|
158
157
|
# rendered by the timeago Stimulus controller.
|
|
159
158
|
raw safe(helpers.display_datetime_value(value))
|
|
160
159
|
elsif currency_field?(name)
|
|
161
|
-
plain helpers.number_to_currency(value, unit:
|
|
160
|
+
plain helpers.number_to_currency(value, unit: currency_unit_for(name))
|
|
162
161
|
else
|
|
163
162
|
plain helpers.display_name_of(value)
|
|
164
163
|
end
|
|
165
164
|
end
|
|
166
165
|
|
|
167
|
-
# Renders a meta value as a colored pill, borrowing the Badge
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
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)
|
|
172
177
|
badge = Plutonium::UI::Display::Components::Badge
|
|
173
|
-
|
|
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
|
+
|
|
174
190
|
span(class: tokens("pu-badge", "pu-badge-#{variant}")) do
|
|
175
|
-
plain
|
|
191
|
+
plain label
|
|
176
192
|
end
|
|
177
193
|
end
|
|
178
194
|
|
|
@@ -181,6 +197,17 @@ module Plutonium
|
|
|
181
197
|
klass.respond_to?(:has_cents_decimal_attribute?) && klass.has_cents_decimal_attribute?(name.to_sym)
|
|
182
198
|
end
|
|
183
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
|
+
|
|
184
211
|
# A declared slot with no value renders a muted em-dash rather than
|
|
185
212
|
# collapsing, so cards in a grid keep an even height instead of
|
|
186
213
|
# ragged rows when some records lack the field.
|
|
@@ -243,7 +270,7 @@ module Plutonium
|
|
|
243
270
|
|
|
244
271
|
def row_actions
|
|
245
272
|
@row_actions ||= resource_definition.defined_actions.values.select { |a|
|
|
246
|
-
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:)
|
|
247
274
|
}
|
|
248
275
|
end
|
|
249
276
|
|