plutonium 0.60.5 → 0.61.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 +313 -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 +549 -439
- data/CLAUDE.md +15 -7
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +895 -193
- 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/_resource_kanban.html.erb +1 -0
- data/db/migrate/wizard/20260615000001_create_plutonium_wizard_sessions.rb +57 -0
- data/docs/.vitepress/config.ts +24 -0
- data/docs/guides/index.md +2 -0
- data/docs/guides/kanban.md +447 -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 +62 -0
- data/docs/reference/kanban/dsl.md +293 -0
- data/docs/reference/kanban/index.md +40 -0
- data/docs/reference/kanban/positioning.md +162 -0
- data/docs/reference/resource/definition.md +16 -0
- data/docs/reference/ui/forms.md +36 -0
- data/docs/reference/ui/pages.md +2 -0
- data/docs/reference/wizard/anchoring-resume.md +194 -0
- data/docs/reference/wizard/dsl.md +332 -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/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/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/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +6 -1
- data/lib/plutonium/action/base.rb +9 -0
- data/lib/plutonium/auth/rodauth.rb +1 -2
- data/lib/plutonium/configuration.rb +4 -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 +54 -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 +69 -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/positioning.rb +146 -0
- data/lib/plutonium/railtie.rb +33 -0
- data/lib/plutonium/resource/controller.rb +2 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +1 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +455 -0
- data/lib/plutonium/resource/controllers/wizard_actions.rb +165 -0
- data/lib/plutonium/resource/policy.rb +8 -0
- data/lib/plutonium/routing/mapper_extensions.rb +44 -0
- data/lib/plutonium/routing/wizard_registration.rb +289 -0
- data/lib/plutonium/ui/display/resource.rb +17 -12
- data/lib/plutonium/ui/form/base.rb +19 -5
- data/lib/plutonium/ui/form/components/password.rb +126 -0
- data/lib/plutonium/ui/form/components/uppy.rb +6 -3
- data/lib/plutonium/ui/form/options/inferred_types.rb +20 -0
- data/lib/plutonium/ui/form/resource.rb +1 -1
- data/lib/plutonium/ui/form/wizard.rb +63 -0
- data/lib/plutonium/ui/grid/card.rb +16 -5
- 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 +324 -0
- data/lib/plutonium/ui/kanban/resource.rb +212 -0
- 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/page/index.rb +1 -0
- data/lib/plutonium/ui/page/show.rb +23 -0
- 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/wizard/review.rb +196 -0
- data/lib/plutonium/ui/wizard/stepper.rb +122 -0
- data/lib/plutonium/ui/wizard/summary_display.rb +59 -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 +3 -3
- data/src/css/components.css +132 -0
- data/src/js/controllers/attachment_input_controller.js +15 -1
- data/src/js/controllers/dirty_form_guard_controller.js +155 -27
- data/src/js/controllers/kanban_controller.js +330 -0
- data/src/js/controllers/password_sentinel_controller.js +39 -0
- data/src/js/controllers/register_controllers.js +6 -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 +271 -282
- metadata +100 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Wizard
|
|
5
|
+
# Builds the "continue where you left off" listing (§4.5): for every
|
|
6
|
+
# in-progress {Session} row owned by a user (optionally narrowed to a tenant
|
|
7
|
+
# +scope+), an enriched {Entry} carrying the wizard's label/icon, the current
|
|
8
|
+
# step (+ its label), `updated_at`, and a resolved `resume_url`.
|
|
9
|
+
#
|
|
10
|
+
# A host renders this on a dashboard:
|
|
11
|
+
#
|
|
12
|
+
# Plutonium::Wizard.in_progress_for(view_context)
|
|
13
|
+
#
|
|
14
|
+
# Resume URLs are built in the CURRENT portal (the one whose `view_context` is
|
|
15
|
+
# passed), so a run is only ever linked from the portal it belongs to:
|
|
16
|
+
#
|
|
17
|
+
# - A `register_wizard` (portal/public) wizard draws a NAMED route carrying a
|
|
18
|
+
# `wizard_class` route default; we find it and build the URL from its helper,
|
|
19
|
+
# threading the tenant scope segment and (for tokened runs) the `:token`.
|
|
20
|
+
# - A `wizard`-macro (resource-mounted) ANCHORED wizard's member URL is built by
|
|
21
|
+
# the same `resource_url_for(record, wizard:, step:)` machinery the launch
|
|
22
|
+
# button uses — portal- and scope-correct by construction — from the row's
|
|
23
|
+
# anchor + the registering definition's wizard name.
|
|
24
|
+
#
|
|
25
|
+
# When a row's mount can't be resolved in this portal (e.g. a non-anchored
|
|
26
|
+
# resource-mounted wizard, whose resource identity isn't on the row, or a wizard
|
|
27
|
+
# not mounted here), the entry is returned with `resume_url: nil` and a
|
|
28
|
+
# `resume_unresolved_reason`, rather than guessing or raising.
|
|
29
|
+
module Resume
|
|
30
|
+
# One enriched in-progress wizard, ready for a dashboard list item.
|
|
31
|
+
Entry = Struct.new(
|
|
32
|
+
:wizard_class,
|
|
33
|
+
:label,
|
|
34
|
+
:icon,
|
|
35
|
+
:current_step,
|
|
36
|
+
:current_step_label,
|
|
37
|
+
:updated_at,
|
|
38
|
+
:resume_url,
|
|
39
|
+
:resume_unresolved_reason,
|
|
40
|
+
:session
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
# In-progress entries for the run owner and tenant scope derived from the
|
|
46
|
+
# current portal's +view_context+ (the same object interactions take). A run
|
|
47
|
+
# belongs to exactly one portal context, so the scope MATCHES it: a scoped
|
|
48
|
+
# portal narrows to its tenant; a non-scoped portal narrows to runs with no
|
|
49
|
+
# scope (never another portal's entity-scoped runs). Resume URLs are built
|
|
50
|
+
# through that same view_context, so they land in THIS portal. Newest first.
|
|
51
|
+
#
|
|
52
|
+
# Optional +anchor:+/+wizard:+ filters narrow IN THE QUERY (before enrichment)
|
|
53
|
+
# so discarded rows are never URL-resolved or anchor-loaded — cheaper than
|
|
54
|
+
# filtering the returned array. Both compose; the `wizard + anchor` pair is
|
|
55
|
+
# index-covered by `[:wizard, :anchor_type, :anchor_id, :status]`.
|
|
56
|
+
#
|
|
57
|
+
# @param view_context [ActionView::Base] the current view context
|
|
58
|
+
# @param anchor [ActiveRecord::Base, nil] narrow to runs anchored against this record
|
|
59
|
+
# @param wizard [Class, nil] narrow to runs of this wizard class
|
|
60
|
+
# @return [Array<Entry>]
|
|
61
|
+
def entries_for(view_context, anchor: nil, wizard: nil)
|
|
62
|
+
controller = view_context.controller
|
|
63
|
+
owner = controller.helpers.current_user
|
|
64
|
+
# A guest has no owner-tracked runs — anonymous runs are session-keyed and
|
|
65
|
+
# ownerless (§4.5). The public surface stubs `current_user` to "Guest", so
|
|
66
|
+
# bail rather than query `where(owner: "Guest")` (a non-record). And never
|
|
67
|
+
# normalize "Guest" to nil: `where(owner: nil)` would match EVERY guest's
|
|
68
|
+
# ownerless run — a cross-guest leak.
|
|
69
|
+
return [] unless owner.present? && owner != "Guest"
|
|
70
|
+
|
|
71
|
+
# `current_scoped_entity` is a helper_method — read it off the view context.
|
|
72
|
+
scope = controller.scoped_to_entity? ? view_context.current_scoped_entity : nil
|
|
73
|
+
# The portal pins the listing: a run is only shown by the portal it was
|
|
74
|
+
# launched in. `scope` still isolates the tenant WITHIN a scoped portal —
|
|
75
|
+
# `engine` alone can't (one engine serves every tenant via path scoping).
|
|
76
|
+
engine = view_context.current_engine.name
|
|
77
|
+
|
|
78
|
+
relation = Session.status_in_progress.where(owner: owner, engine: engine, scope: scope)
|
|
79
|
+
relation = relation.where(anchor: anchor) if anchor
|
|
80
|
+
relation = relation.where(wizard: wizard.name) if wizard
|
|
81
|
+
|
|
82
|
+
relation
|
|
83
|
+
.order(updated_at: :desc)
|
|
84
|
+
.filter_map { |row| entry_for(row, view_context) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Entry, nil] nil when the wizard class can't be loaded
|
|
88
|
+
def entry_for(row, view_context)
|
|
89
|
+
wizard_class = row.wizard.to_s.safe_constantize
|
|
90
|
+
return nil unless wizard_class
|
|
91
|
+
|
|
92
|
+
step = resolve_step(wizard_class, row.current_step)
|
|
93
|
+
resolved = ResumeUrl.new(row, wizard_class, view_context).resolve
|
|
94
|
+
|
|
95
|
+
Entry.new(
|
|
96
|
+
wizard_class: wizard_class,
|
|
97
|
+
label: wizard_class.label,
|
|
98
|
+
icon: wizard_class.icon,
|
|
99
|
+
current_step: row.current_step,
|
|
100
|
+
current_step_label: step&.label,
|
|
101
|
+
updated_at: row.updated_at,
|
|
102
|
+
resume_url: resolved[:url],
|
|
103
|
+
resume_unresolved_reason: resolved[:reason],
|
|
104
|
+
session: row
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def resolve_step(wizard_class, key)
|
|
109
|
+
return nil if key.blank?
|
|
110
|
+
|
|
111
|
+
wizard_class.steps.find { |s| s.key.to_s == key.to_s }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Resolves a single row to its resume URL in the current portal.
|
|
115
|
+
class ResumeUrl
|
|
116
|
+
def initialize(row, wizard_class, view_context)
|
|
117
|
+
@row = row
|
|
118
|
+
@wizard_class = wizard_class
|
|
119
|
+
@view_context = view_context
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @return [Hash] {url:, reason:} — exactly one of the two is non-nil.
|
|
123
|
+
def resolve
|
|
124
|
+
if (named = register_wizard_url)
|
|
125
|
+
return {url: named, reason: nil}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if (member = resource_member_url)
|
|
129
|
+
return {url: member, reason: nil}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
{url: nil, reason: unresolved_reason}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# A `register_wizard` route is named and carries `defaults[:wizard_class]`.
|
|
138
|
+
def register_wizard_url
|
|
139
|
+
route_sets.each do |route_set|
|
|
140
|
+
name = Plutonium::Wizard::RouteResolution.route_name(route_set, @wizard_class, action: "show")
|
|
141
|
+
next unless name
|
|
142
|
+
|
|
143
|
+
return build_url(route_set, name, register_wizard_params)
|
|
144
|
+
end
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Params for a `register_wizard` named helper: the current step, the tenant
|
|
149
|
+
# scope path segment (when the run is scoped), and the URL token for a
|
|
150
|
+
# tokened (no concurrency_key) run.
|
|
151
|
+
def register_wizard_params
|
|
152
|
+
{step: @row.current_step}.merge(scope_param).merge(token_param)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# A resource-mounted ANCHORED wizard's member URL is built by the SAME
|
|
156
|
+
# `resource_url_for(record, wizard:, step:)` machinery the launch button uses
|
|
157
|
+
# (§5.1) — so it's portal- and scope-correct by construction (it resolves on
|
|
158
|
+
# the current portal's `current_engine`, threads the entity segment when the
|
|
159
|
+
# portal is path-scoped, and singularizes the member helper). We pass the
|
|
160
|
+
# row's anchor as the record, the registering definition's wizard name, and
|
|
161
|
+
# the resumed step; a tokened (non-keyed) run also carries its run token.
|
|
162
|
+
def resource_member_url
|
|
163
|
+
anchor = @row.anchor
|
|
164
|
+
return nil if anchor.nil?
|
|
165
|
+
|
|
166
|
+
wizard_name = registered_wizard_name
|
|
167
|
+
return nil if wizard_name.nil?
|
|
168
|
+
|
|
169
|
+
@view_context.resource_url_for(anchor, wizard: wizard_name, step: @row.current_step, **token_param)
|
|
170
|
+
rescue => e
|
|
171
|
+
Rails.logger.warn { "[Plutonium::Wizard] resume url build failed for #{@wizard_class.name}: #{e.message}" }
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Reverse-lookup the `wizard`-macro name registered for this wizard class on
|
|
176
|
+
# the anchor's resource definition. nil when not found.
|
|
177
|
+
def registered_wizard_name
|
|
178
|
+
definition = definition_for(@row.anchor)
|
|
179
|
+
return nil unless definition.respond_to?(:registered_wizards)
|
|
180
|
+
|
|
181
|
+
definition.registered_wizards.find do |_name, reg|
|
|
182
|
+
reg[:wizard_class] == @wizard_class
|
|
183
|
+
end&.first
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def definition_for(record)
|
|
187
|
+
"#{record.class.name}Definition".safe_constantize
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The scope path segment for an entity-scoped portal, keyed by the portal
|
|
191
|
+
# engine's own +scoped_entity_param_key+ (which honors a custom +param_key:+
|
|
192
|
+
# passed to +scope_to_entity+), valued from the row's scope record.
|
|
193
|
+
def scope_param
|
|
194
|
+
scope = @row.scope
|
|
195
|
+
return {} if scope.nil?
|
|
196
|
+
|
|
197
|
+
{scoped_entity_param_key => scope.to_param}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The route's scope param key comes from the engine the resume URL is built
|
|
201
|
+
# in — NOT re-derived from the scope model, which would diverge from the
|
|
202
|
+
# actual route segment whenever the portal set a custom `param_key:`.
|
|
203
|
+
def scoped_entity_param_key
|
|
204
|
+
@view_context.current_engine.scoped_entity_param_key
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A tokened (no concurrency_key) run carries its per-run id in the URL.
|
|
208
|
+
def token_param
|
|
209
|
+
return {} if @wizard_class.concurrency_key?
|
|
210
|
+
return {} if @row.token.blank?
|
|
211
|
+
|
|
212
|
+
{token: @row.token}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def build_url(route_set, route_name, params)
|
|
216
|
+
route_set.url_helpers.public_send(:"#{route_name}_path", **params)
|
|
217
|
+
rescue => e
|
|
218
|
+
Rails.logger.warn { "[Plutonium::Wizard] resume url build failed for #{route_name}: #{e.message}" }
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def unresolved_reason
|
|
223
|
+
if @row.anchor && registered_wizard_name.nil?
|
|
224
|
+
"no `wizard` macro registration found for #{@wizard_class.name} " \
|
|
225
|
+
"on #{@row.anchor.class.name}Definition"
|
|
226
|
+
elsif @row.anchor.nil? && resource_mounted_candidate?
|
|
227
|
+
"non-anchored resource-mounted wizard — the row carries no resource " \
|
|
228
|
+
"identity to rebuild its collection URL"
|
|
229
|
+
else
|
|
230
|
+
"no route found for #{@wizard_class.name} (not registered via " \
|
|
231
|
+
"register_wizard or a `wizard` macro mount this resolver can reach)"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Heuristic for the reason text only: the wizard isn't a register_wizard
|
|
236
|
+
# mount (no named route) and has no anchor on the row.
|
|
237
|
+
def resource_mounted_candidate?
|
|
238
|
+
register_wizard_url.nil?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# The CURRENT portal's route set, plus the main app's (for `public:` mounts).
|
|
242
|
+
# Scoped to this portal so a `register_wizard` wizard mounted in several
|
|
243
|
+
# portals resolves here, not in whichever engine happens to be scanned first.
|
|
244
|
+
def route_sets
|
|
245
|
+
@route_sets ||= [@view_context.current_engine.routes, Rails.application.routes].uniq
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Wizard
|
|
5
|
+
# The built-in terminal review step (§2.5). Declares no fields of its own;
|
|
6
|
+
# auto-summarizes collected `data` and gates Finish → `execute`. Must be the
|
|
7
|
+
# last declared step.
|
|
8
|
+
class ReviewStep < Step
|
|
9
|
+
# Minimal stand-in for a step's field surface — a review step has none.
|
|
10
|
+
class EmptyFields
|
|
11
|
+
def attribute_schema = {}
|
|
12
|
+
|
|
13
|
+
def attribute_options = {}
|
|
14
|
+
|
|
15
|
+
def inputs = {}
|
|
16
|
+
|
|
17
|
+
def validations = []
|
|
18
|
+
|
|
19
|
+
def imported_form_validators = []
|
|
20
|
+
|
|
21
|
+
def imported_validate_fn = nil
|
|
22
|
+
|
|
23
|
+
def form_layout_sections = nil
|
|
24
|
+
|
|
25
|
+
def defined_structured_inputs = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :block, :summary, :header
|
|
29
|
+
|
|
30
|
+
def initialize(key: :review, label: "Review", description: nil, condition: nil, summary: true, header: true, block: nil)
|
|
31
|
+
super(key:, label:, description:, condition:, fields: EmptyFields.new)
|
|
32
|
+
@summary = summary
|
|
33
|
+
@header = header
|
|
34
|
+
@block = block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def review? = true
|
|
38
|
+
|
|
39
|
+
# Whether the auto-summary of completed steps renders in the COMPLETE state
|
|
40
|
+
# (see the `review summary:` macro). Always true in the incomplete state.
|
|
41
|
+
def summary? = @summary
|
|
42
|
+
|
|
43
|
+
# Whether the step-header section (label + prompt) renders above the review
|
|
44
|
+
# body (see the `review header:` macro).
|
|
45
|
+
def header? = @header
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Wizard
|
|
5
|
+
# Resolves a `register_wizard` mount's named routes from a route set by the
|
|
6
|
+
# `wizard_class` route default that every wizard route carries (§5.2/§5.3).
|
|
7
|
+
#
|
|
8
|
+
# `register_wizard` names its helpers from the mount path (`at:`) or an explicit
|
|
9
|
+
# `as:`, NOT from the wizard class name — so `register_wizard W, at: "onboarding"`
|
|
10
|
+
# draws `onboarding_wizard*`, regardless of `W`'s class. Re-deriving a slug from
|
|
11
|
+
# the class name (as the gate once did) only works when the two happen to
|
|
12
|
+
# coincide; everywhere a URL is built for a registered wizard, the route must be
|
|
13
|
+
# looked up by the `wizard_class` default instead.
|
|
14
|
+
#
|
|
15
|
+
# Shared by {Plutonium::Wizard::Controller} (per-step URLs), {Gate} (the entry
|
|
16
|
+
# redirect), and {Resume} (the in-progress listing), so all three track the
|
|
17
|
+
# actual `at:`/`as:` used at registration.
|
|
18
|
+
module RouteResolution
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# The name of the route `register_wizard` drew for the given action and wizard
|
|
22
|
+
# class within +route_set+, or nil if none. Actions: "launch" (the bare mount
|
|
23
|
+
# → resolve/PRG to the run's step), "show" (GET a specific step).
|
|
24
|
+
#
|
|
25
|
+
# @param route_set [ActionDispatch::Routing::RouteSet]
|
|
26
|
+
# @param wizard_class [Class]
|
|
27
|
+
# @param action [String, Symbol] "launch" or "show"
|
|
28
|
+
# @return [Symbol, nil] the route name (e.g. :onboarding_wizard_launch)
|
|
29
|
+
def route_name(route_set, wizard_class, action:)
|
|
30
|
+
route = route_set.routes.find do |r|
|
|
31
|
+
d = r.defaults
|
|
32
|
+
r.name.present? &&
|
|
33
|
+
d[:action].to_s == action.to_s &&
|
|
34
|
+
d[:wizard_class].to_s == wizard_class.name
|
|
35
|
+
end
|
|
36
|
+
route&.name&.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|