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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Kanban
|
|
5
|
+
class Column
|
|
6
|
+
ROLE_PRESETS = {
|
|
7
|
+
backlog: {add: true},
|
|
8
|
+
# Terminal columns: collapsed by default, colour signals the outcome.
|
|
9
|
+
# :done is the positive close (green); :lost is the negative close
|
|
10
|
+
# (red) — the natural pair for won/lost pipelines (leads, deals, tickets).
|
|
11
|
+
done: {color: :green, collapsed: true},
|
|
12
|
+
lost: {color: :red, collapsed: true}
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :key, :label, :color, :wip, :scope, :on_enter, :on_exit, :accepts, :actions, :enter_interaction
|
|
16
|
+
|
|
17
|
+
def initialize(key, label: nil, color: nil, wip: nil, scope: nil, on_enter: nil, on_exit: nil, on_drop: nil,
|
|
18
|
+
collapsed: nil, add: nil, accepts: nil, locked: nil, role: nil, enter_interaction: nil, drop_interaction: nil)
|
|
19
|
+
# on_drop:/drop_interaction: were renamed to on_enter:/enter_interaction:.
|
|
20
|
+
# Resolve the deprecated aliases first (dev/test raise; deployed envs warn
|
|
21
|
+
# and map — see resolve_renamed_option) so the rest of initialize only
|
|
22
|
+
# ever sees the new names.
|
|
23
|
+
on_enter = resolve_renamed_option(:on_drop, on_drop, :on_enter, on_enter)
|
|
24
|
+
enter_interaction = resolve_renamed_option(:drop_interaction, drop_interaction, :enter_interaction, enter_interaction)
|
|
25
|
+
|
|
26
|
+
preset = role ? ROLE_PRESETS.fetch(role) { raise ArgumentError, "Unknown column role: #{role.inspect}. Valid: #{ROLE_PRESETS.keys.inspect}" } : {}
|
|
27
|
+
if enter_interaction && !(enter_interaction.is_a?(Class) && enter_interaction < Plutonium::Resource::Interaction)
|
|
28
|
+
raise ArgumentError, "enter_interaction: must be a Plutonium::Resource::Interaction subclass, got #{enter_interaction.inspect}"
|
|
29
|
+
end
|
|
30
|
+
# An enter_interaction acts on the SINGLE dropped card — the move handler
|
|
31
|
+
# binds it as `resource:`. It must therefore be record-shaped (declare a
|
|
32
|
+
# `resource` attribute), never collection/bulk-shaped (`resources`). Reject
|
|
33
|
+
# anything else at definition time so a mis-shaped interaction can't (a) blow
|
|
34
|
+
# up at drop time on the `resource=` assignment, or (b) get auto-classified
|
|
35
|
+
# by Action::Interactive::Factory as a bulk action and leak into the
|
|
36
|
+
# bulk-actions bar (which does not filter kanban_drop actions).
|
|
37
|
+
if enter_interaction && !enter_interaction.attribute_names.map(&:to_sym).include?(:resource)
|
|
38
|
+
raise ArgumentError, "enter_interaction: #{enter_interaction} must operate on a single record (declare `attribute :resource`); collection/bulk interactions cannot be used as an enter_interaction."
|
|
39
|
+
end
|
|
40
|
+
# accepts: is purely structural (topology + client drop hints): true/false
|
|
41
|
+
# or an Array of source keys. The Proc form was removed — record/user
|
|
42
|
+
# conditions belong in the kanban_move? policy, which sees the record and
|
|
43
|
+
# the from/to columns. Fail loud rather than silently treating a stale Proc
|
|
44
|
+
# as permissive (which would OPEN UP a column that used to restrict drops).
|
|
45
|
+
if accepts.is_a?(Proc)
|
|
46
|
+
raise ArgumentError, "kanban column `accepts:` no longer accepts a Proc; use true/false or an Array of source keys, and put record/user conditions in the kanban_move? policy."
|
|
47
|
+
end
|
|
48
|
+
@key = key.to_sym
|
|
49
|
+
@label = label || key.to_s.titleize
|
|
50
|
+
@color = color.nil? ? preset[:color] : color
|
|
51
|
+
@wip = wip
|
|
52
|
+
@scope = scope
|
|
53
|
+
@on_enter = on_enter
|
|
54
|
+
@on_exit = on_exit
|
|
55
|
+
@collapsed = collapsed.nil? ? preset[:collapsed] : collapsed
|
|
56
|
+
@add = add.nil? ? preset[:add] : add
|
|
57
|
+
@accepts = accepts.nil? || accepts
|
|
58
|
+
@locked = locked || false
|
|
59
|
+
@enter_interaction = enter_interaction
|
|
60
|
+
@actions = []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A column may run an input-collecting Interaction when a card ENTERS it
|
|
64
|
+
# (e.g. "mark lead as lost with a reason"). When set, the drop opens the
|
|
65
|
+
# interaction's form as a modal before the move is committed.
|
|
66
|
+
def enter_interaction? = !!@enter_interaction
|
|
67
|
+
|
|
68
|
+
# Internal action-registration key for the enter interaction, scoped to the
|
|
69
|
+
# column: :blocked → :blocked_enter_interaction. Nil when unset.
|
|
70
|
+
#
|
|
71
|
+
# Column-scoped (not class-name-derived) so it is unique by construction — a
|
|
72
|
+
# column has at most one enter_interaction, so two columns can never collide
|
|
73
|
+
# even if they reuse the same interaction class. This key is ONLY an internal
|
|
74
|
+
# form/param routing handle; it is NOT an authorization name. The move (and
|
|
75
|
+
# therefore the interaction) is authorized solely by kanban_move? — the
|
|
76
|
+
# interaction has no policy method of its own.
|
|
77
|
+
def enter_interaction_key
|
|
78
|
+
return nil unless @enter_interaction
|
|
79
|
+
:"#{key}_enter_interaction"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def action(key, interaction:, on: :all, label: nil, icon: nil, confirmation: nil)
|
|
83
|
+
@actions << Action.new(key: key.to_sym, interaction:, on:, label:, icon:, confirmation:)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collapsed? = !!@collapsed
|
|
87
|
+
def add? = !!@add
|
|
88
|
+
def locked? = @locked
|
|
89
|
+
|
|
90
|
+
# Whether a card from `source_key` may be dropped into this column. Purely
|
|
91
|
+
# structural — @accepts is normalized to true/false or an Array of source
|
|
92
|
+
# keys (the constructor rejects a Proc). Drives both the server-side gate in
|
|
93
|
+
# the move handler and the client-side drop hint (data-kanban-accepts).
|
|
94
|
+
def accepts?(source_key)
|
|
95
|
+
case @accepts
|
|
96
|
+
when Array then @accepts.include?(source_key)
|
|
97
|
+
else @accepts # true or false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Bridge a renamed column option. `on_drop:`/`drop_interaction:` became
|
|
104
|
+
# `on_enter:`/`enter_interaction:`. To avoid breaking live deployments on
|
|
105
|
+
# upgrade we DON'T hard-fail in deployed envs — we log a deprecation and map
|
|
106
|
+
# the old value onto the new name. But local envs (development/test) raise,
|
|
107
|
+
# so the rename is caught during development and can't silently ship. If
|
|
108
|
+
# both the old and new names are given, the new one wins.
|
|
109
|
+
def resolve_renamed_option(old_name, old_value, new_name, new_value)
|
|
110
|
+
return new_value if old_value.nil?
|
|
111
|
+
|
|
112
|
+
if Rails.env.local?
|
|
113
|
+
raise ArgumentError,
|
|
114
|
+
"kanban column `#{old_name}:` has been renamed to `#{new_name}:` — update your definition."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Rails.logger.warn { "[plutonium] kanban column `#{old_name}:` is deprecated; rename it to `#{new_name}:`." }
|
|
118
|
+
new_value.nil? ? old_value : new_value
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Kanban
|
|
7
|
+
# Evaluation scope for dynamic `columns do…end` blocks at request time.
|
|
8
|
+
#
|
|
9
|
+
# Delegates everything to the request's view_context so the block can call
|
|
10
|
+
# current_user, current_scoped_entity, params, helpers, etc. directly —
|
|
11
|
+
# exactly like Plutonium::Action::ConditionContext does for action conditions.
|
|
12
|
+
class Context < SimpleDelegator
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Kanban
|
|
5
|
+
class DSL
|
|
6
|
+
def self.build(&block)
|
|
7
|
+
dsl = new
|
|
8
|
+
dsl.instance_eval(&block) if block
|
|
9
|
+
dsl.to_board
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@columns = []
|
|
14
|
+
@columns_block = nil
|
|
15
|
+
@card_fields = nil
|
|
16
|
+
@per_column = nil
|
|
17
|
+
@realtime = false
|
|
18
|
+
@position_config = Positioning::Config.default
|
|
19
|
+
@lazy = true
|
|
20
|
+
@show_in = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def column(key, **opts, &blk)
|
|
24
|
+
col = Column.new(key, **opts)
|
|
25
|
+
col.instance_eval(&blk) if blk
|
|
26
|
+
@columns << col
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fluent DSL setters — `attr_writer` would change the call syntax
|
|
30
|
+
# (`per_column 25` → `self.per_column = 25`), so keep them as methods.
|
|
31
|
+
# standard:disable Style/TrivialAccessors
|
|
32
|
+
def columns(&blk) = @columns_block = blk
|
|
33
|
+
def card_fields(**slots) = @card_fields = slots
|
|
34
|
+
def per_column(n) = @per_column = n
|
|
35
|
+
def realtime(v = true) = @realtime = v
|
|
36
|
+
def lazy(v = true) = @lazy = v
|
|
37
|
+
# standard:enable Style/TrivialAccessors
|
|
38
|
+
|
|
39
|
+
# Overrides where a card click opens the record's show page, for this
|
|
40
|
+
# board only:
|
|
41
|
+
# :modal — open in a centered modal dialog
|
|
42
|
+
# :page — navigate the whole page to the show route
|
|
43
|
+
# When unset, the board inherits the definition's `show_in` (default :page).
|
|
44
|
+
def show_in(mode) = @show_in = mode # standard:disable Style/TrivialAccessors
|
|
45
|
+
|
|
46
|
+
def position_on(attr = :position, &blk)
|
|
47
|
+
@position_config =
|
|
48
|
+
if attr == false
|
|
49
|
+
Positioning::Config.disabled
|
|
50
|
+
elsif blk
|
|
51
|
+
Positioning::Config.with_block(attr, blk)
|
|
52
|
+
else
|
|
53
|
+
Positioning::Config.attribute(attr)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_board
|
|
58
|
+
Board.new(
|
|
59
|
+
columns: @columns,
|
|
60
|
+
columns_block: @columns_block,
|
|
61
|
+
card_fields: @card_fields,
|
|
62
|
+
per_column: @per_column,
|
|
63
|
+
realtime: @realtime,
|
|
64
|
+
position_config: @position_config,
|
|
65
|
+
lazy: @lazy,
|
|
66
|
+
show_in: @show_in
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Kanban
|
|
5
|
+
# Groups an already-authorized, query-applied, UN-paginated relation into
|
|
6
|
+
# ordered, per_column-capped column entries.
|
|
7
|
+
module Grouping
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Returns [{column:, cards: [records], total: Integer}, ...] in column order.
|
|
11
|
+
def call(board:, relation:, context:)
|
|
12
|
+
columns = resolve_columns(board, context)
|
|
13
|
+
pos = board.position_config
|
|
14
|
+
columns.map do |col|
|
|
15
|
+
scoped = apply_scope(relation, col.scope)
|
|
16
|
+
ordered = pos.order(scoped)
|
|
17
|
+
if board.per_column
|
|
18
|
+
total = ordered.count
|
|
19
|
+
cards = ordered.limit(board.per_column).to_a
|
|
20
|
+
else
|
|
21
|
+
cards = ordered.to_a
|
|
22
|
+
total = cards.size
|
|
23
|
+
end
|
|
24
|
+
{column: col, cards: cards, total: total}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resolves the column list from a board. For dynamic boards, evaluates
|
|
29
|
+
# the columns_block against the context (which exposes current_user,
|
|
30
|
+
# params, etc. via delegation to view_context). Public so Task 7 (move
|
|
31
|
+
# handler) can call Grouping.resolve_columns(board, context) directly.
|
|
32
|
+
def resolve_columns(board, context)
|
|
33
|
+
return board.columns unless board.dynamic?
|
|
34
|
+
Array(context.instance_exec(&board.columns_block)).flatten
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Applies a column scope to a relation.
|
|
38
|
+
# Symbol → relation.public_send(sym) (named scope)
|
|
39
|
+
# Proc → relation.instance_exec(&scope) (inline lambda, e.g. -> { where(status: "todo") })
|
|
40
|
+
# nil → relation unchanged
|
|
41
|
+
def apply_scope(relation, scope)
|
|
42
|
+
case scope
|
|
43
|
+
when Symbol then relation.public_send(scope)
|
|
44
|
+
when Proc then relation.instance_exec(&scope)
|
|
45
|
+
when nil then relation
|
|
46
|
+
else raise ArgumentError, "Unsupported column scope: #{scope.inspect} (expected Symbol, Proc, or nil)"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/positioning"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Kanban
|
|
7
|
+
module Positioning
|
|
8
|
+
# Value object passed to Mode B blocks, carrying the full drop context.
|
|
9
|
+
Move = Data.define(:record, :column, :prev, :next, :index)
|
|
10
|
+
|
|
11
|
+
# Strategy configuration object created by the `position_on` DSL.
|
|
12
|
+
#
|
|
13
|
+
# Three modes:
|
|
14
|
+
# Mode A (:delegate) — delegate reposition! to Plutonium::Positioning concern
|
|
15
|
+
# Mode B (:block) — call a user-supplied block with a Move
|
|
16
|
+
# Mode C (:disabled) — no ordering; relation returned unchanged
|
|
17
|
+
class Config
|
|
18
|
+
# Mode A, default attribute :position
|
|
19
|
+
def self.default
|
|
20
|
+
new(:delegate, :position, nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Mode A, custom attribute
|
|
24
|
+
def self.attribute(attr)
|
|
25
|
+
new(:delegate, attr.to_sym, nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Mode B — orders by attr, write delegated to block
|
|
29
|
+
def self.with_block(attr, block)
|
|
30
|
+
new(:block, attr.to_sym, block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Mode C — disabled
|
|
34
|
+
def self.disabled
|
|
35
|
+
new(:disabled, nil, nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_reader :attribute
|
|
39
|
+
|
|
40
|
+
def initialize(mode, attribute, block)
|
|
41
|
+
@mode = mode
|
|
42
|
+
@attribute = attribute
|
|
43
|
+
@block = block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def disabled?
|
|
47
|
+
@mode == :disabled
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Apply positional ordering to a relation.
|
|
51
|
+
# Mode A/B: relation.reorder(attribute)
|
|
52
|
+
# Mode C: return relation unchanged
|
|
53
|
+
def order(relation)
|
|
54
|
+
return relation if disabled?
|
|
55
|
+
relation.reorder(@attribute)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Persist the new position for a dropped record.
|
|
59
|
+
# Mode A: delegate to record.reposition!(prev_record:, next_record:)
|
|
60
|
+
# Mode B: call the user block with a Move
|
|
61
|
+
# Mode C: no-op
|
|
62
|
+
def reposition!(record:, column:, prev_record:, next_record:, index:)
|
|
63
|
+
case @mode
|
|
64
|
+
when :delegate
|
|
65
|
+
record.reposition!(prev_record:, next_record:)
|
|
66
|
+
when :block
|
|
67
|
+
@block.call(Move.new(record:, column:, prev: prev_record, next: next_record, index:))
|
|
68
|
+
when :disabled
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "plutonium/positioning"
|
|
4
|
+
require "plutonium/kanban/positioning"
|
|
5
|
+
require "plutonium/kanban/action"
|
|
6
|
+
require "plutonium/kanban/column"
|
|
7
|
+
require "plutonium/kanban/board"
|
|
8
|
+
require "plutonium/kanban/dsl"
|
|
9
|
+
require "plutonium/kanban/context"
|
|
10
|
+
require "plutonium/kanban/grouping"
|
|
11
|
+
require "plutonium/kanban/broadcaster"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
# Registry of per-feature migration paths.
|
|
5
|
+
#
|
|
6
|
+
# Features register their migration directory against a configuration key. A
|
|
7
|
+
# path is only surfaced by {.enabled_paths} when its feature's config
|
|
8
|
+
# (+Plutonium.configuration.<feature>+) responds to +.enabled+ and it is true.
|
|
9
|
+
module Migrations
|
|
10
|
+
@registry = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Register a migration path for a feature.
|
|
14
|
+
#
|
|
15
|
+
# @param feature [Symbol, String] the configuration key (e.g. +:wizards+)
|
|
16
|
+
# @param path [String] the absolute migration directory path
|
|
17
|
+
# @return [String] the registered path
|
|
18
|
+
def register(feature, path)
|
|
19
|
+
@registry[feature.to_sym] = path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Clear the registry. Intended for tests.
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash]
|
|
25
|
+
def reset!
|
|
26
|
+
@registry = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Migration paths for features that are currently enabled.
|
|
30
|
+
#
|
|
31
|
+
# @return [Array<String>]
|
|
32
|
+
def enabled_paths
|
|
33
|
+
@registry.filter_map do |feature, path|
|
|
34
|
+
cfg = Plutonium.configuration.public_send(feature) if Plutonium.configuration.respond_to?(feature)
|
|
35
|
+
path if cfg&.respond_to?(:enabled) && cfg.enabled
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -76,6 +76,24 @@ module Plutonium
|
|
|
76
76
|
class_attribute :has_cents_attributes, instance_writer: false, default: {}
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Resolves the currency unit (symbol) configured for a has_cents decimal
|
|
80
|
+
# accessor, e.g. :price for `has_cents :price_cents, unit: "£"`.
|
|
81
|
+
#
|
|
82
|
+
# @param decimal_attribute [Symbol, String] The decimal accessor name.
|
|
83
|
+
# @return [String, nil] The unit — a String verbatim, the value of the
|
|
84
|
+
# method named by a Symbol unit (per-row currencies), or nil when the
|
|
85
|
+
# attribute has no unit configured / is not a has_cents accessor.
|
|
86
|
+
def has_cents_unit_for(decimal_attribute)
|
|
87
|
+
decimal_attribute = decimal_attribute.to_sym
|
|
88
|
+
config = self.class.has_cents_attributes.values.find { |opts| opts[:name] == decimal_attribute }
|
|
89
|
+
return unless config
|
|
90
|
+
|
|
91
|
+
case (unit = config[:unit])
|
|
92
|
+
when Symbol then public_send(unit)
|
|
93
|
+
else unit
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
79
97
|
module ClassMethods
|
|
80
98
|
# # Inherit validations from cents attribute to decimal attribute
|
|
81
99
|
# def validate(*args, &block)
|
|
@@ -109,10 +127,20 @@ module Plutonium
|
|
|
109
127
|
# - rate: 1000 for dollars/mils (1 dollar = 1000 mils)
|
|
110
128
|
# - rate: 1 for a whole number representation
|
|
111
129
|
# @param suffix [String] The suffix to append to the cents_name if name is not provided (default: "amount").
|
|
130
|
+
# @param unit [String, Symbol, nil] The currency unit used when the value is rendered as currency.
|
|
131
|
+
# A String is used verbatim as the symbol ("£"); a Symbol names a method read off the record for
|
|
132
|
+
# per-row currencies (`unit: :currency_symbol`); nil (default) renders with no symbol. Consumers
|
|
133
|
+
# (the Currency display component, grid/kanban cards) read this via {#has_cents_unit_for}.
|
|
112
134
|
#
|
|
113
135
|
# @example Standard currency (dollars and cents)
|
|
114
136
|
# has_cents :price_cents
|
|
115
137
|
#
|
|
138
|
+
# @example Static currency symbol
|
|
139
|
+
# has_cents :price_cents, unit: "£"
|
|
140
|
+
#
|
|
141
|
+
# @example Per-row currency symbol read off the record
|
|
142
|
+
# has_cents :price_cents, unit: :currency_symbol
|
|
143
|
+
#
|
|
116
144
|
# @example Custom rate for a different currency division
|
|
117
145
|
# has_cents :amount_cents, name: :cost, rate: 1000
|
|
118
146
|
#
|
|
@@ -121,14 +149,14 @@ module Plutonium
|
|
|
121
149
|
#
|
|
122
150
|
# @example Using custom suffix
|
|
123
151
|
# has_cents :total_cents, suffix: "value"
|
|
124
|
-
def has_cents(cents_name, name: nil, rate: 100, suffix: "amount")
|
|
152
|
+
def has_cents(cents_name, name: nil, rate: 100, suffix: "amount", unit: nil)
|
|
125
153
|
cents_name = cents_name.to_sym
|
|
126
154
|
name ||= cents_name.to_s.gsub(/_cents$/, "")
|
|
127
155
|
name = name.to_sym
|
|
128
156
|
name = (name == cents_name) ? :"#{cents_name}_#{suffix}" : name
|
|
129
157
|
|
|
130
158
|
self.has_cents_attributes = has_cents_attributes.merge(
|
|
131
|
-
cents_name => {name: name, rate: rate}
|
|
159
|
+
cents_name => {name: name, rate: rate, unit: unit}
|
|
132
160
|
)
|
|
133
161
|
|
|
134
162
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
# Standalone decimal/fractional ordering. Kanban-independent.
|
|
5
|
+
#
|
|
6
|
+
# Including this concern and calling `positioned_on` gives a model:
|
|
7
|
+
# - automatic position assignment on create (appends to the end of its scope group)
|
|
8
|
+
# - `reposition!(prev_record:, next_record:)` for drag-and-drop reordering
|
|
9
|
+
# - `backfill_positions!` class method to number existing rows
|
|
10
|
+
#
|
|
11
|
+
# Pure math helpers are exposed as module-level methods so they can be
|
|
12
|
+
# called without an AR instance:
|
|
13
|
+
# Plutonium::Positioning.position_between(1.0, 3.0) # => 2.0
|
|
14
|
+
# Plutonium::Positioning.gap_exhausted?(1.0, 1.0) # => true
|
|
15
|
+
module Positioning
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
EPSILON = 1e-6
|
|
19
|
+
|
|
20
|
+
# Returns the position that sits between +prev_val+ and +next_val+.
|
|
21
|
+
#
|
|
22
|
+
# Rules:
|
|
23
|
+
# both nil → 0.0 (first item in an empty list)
|
|
24
|
+
# prev nil → next_val - 1 (prepend)
|
|
25
|
+
# next nil → prev_val + 1 (append)
|
|
26
|
+
# else → midpoint
|
|
27
|
+
def self.position_between(prev_val, next_val)
|
|
28
|
+
return 0.0 if prev_val.nil? && next_val.nil?
|
|
29
|
+
return next_val - 1 if prev_val.nil?
|
|
30
|
+
return prev_val + 1 if next_val.nil?
|
|
31
|
+
(prev_val + next_val) / 2.0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns true when +prev_val+ and +next_val+ are so close together
|
|
35
|
+
# that inserting a new midpoint would produce a duplicate.
|
|
36
|
+
def self.gap_exhausted?(prev_val, next_val)
|
|
37
|
+
return false if prev_val.nil? || next_val.nil?
|
|
38
|
+
(next_val - prev_val).abs < EPSILON
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
included do
|
|
42
|
+
class_attribute :positioning_column, instance_accessor: false, default: :position
|
|
43
|
+
class_attribute :positioning_scope_attr, instance_accessor: false, default: nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class_methods do
|
|
47
|
+
# Opt in to positional ordering.
|
|
48
|
+
#
|
|
49
|
+
# positioned_on :position, scope: :status
|
|
50
|
+
#
|
|
51
|
+
# @param column [Symbol] the decimal column that stores positions
|
|
52
|
+
# @param scope [Symbol, nil] group rows by this column; nil = single global group
|
|
53
|
+
def positioned_on(column = :position, scope: nil)
|
|
54
|
+
self.positioning_column = column
|
|
55
|
+
self.positioning_scope_attr = scope
|
|
56
|
+
before_create :assign_initial_position
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Number every row in the table per scope group as 1.0, 2.0, … in
|
|
60
|
+
# +order+ order. Safe to call on an empty table.
|
|
61
|
+
#
|
|
62
|
+
# @param order [Symbol] column to sort by when assigning positions
|
|
63
|
+
def backfill_positions!(order: :created_at)
|
|
64
|
+
groups = positioning_scope_attr ? all.group_by(&positioning_scope_attr) : {nil => all.to_a}
|
|
65
|
+
groups.each_value do |rows|
|
|
66
|
+
ActiveRecord::Base.transaction do
|
|
67
|
+
rows.sort_by { |r| r.public_send(order) }.each_with_index do |row, i|
|
|
68
|
+
row.update_column(positioning_column, (i + 1).to_f)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Move this record so it sits between +prev_record+ and +next_record+
|
|
76
|
+
# within its scope group. Pass nil for either neighbor to move to an end.
|
|
77
|
+
#
|
|
78
|
+
# If the gap between the two neighbors is exhausted (too small to split)
|
|
79
|
+
# the scope group is rebalanced first so that fresh integer positions are
|
|
80
|
+
# available, then the record is positioned between the reloaded neighbors.
|
|
81
|
+
#
|
|
82
|
+
# @param prev_record [ActiveRecord::Base, nil]
|
|
83
|
+
# @param next_record [ActiveRecord::Base, nil]
|
|
84
|
+
def reposition!(prev_record:, next_record:)
|
|
85
|
+
col = self.class.positioning_column
|
|
86
|
+
prev_val = prev_record&.public_send(col)
|
|
87
|
+
next_val = next_record&.public_send(col)
|
|
88
|
+
if Plutonium::Positioning.gap_exhausted?(prev_val, next_val)
|
|
89
|
+
rebalance_scope_group!
|
|
90
|
+
prev_val = prev_record&.reload&.public_send(col)
|
|
91
|
+
next_val = next_record&.reload&.public_send(col)
|
|
92
|
+
end
|
|
93
|
+
update!(col => Plutonium::Positioning.position_between(prev_val, next_val))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def assign_initial_position
|
|
99
|
+
col = self.class.positioning_column
|
|
100
|
+
return if public_send(col).present?
|
|
101
|
+
max = positioning_group_relation.maximum(col) || 0.0
|
|
102
|
+
public_send(:"#{col}=", max + 1)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def positioning_group_relation
|
|
106
|
+
rel = self.class.all
|
|
107
|
+
attr = self.class.positioning_scope_attr
|
|
108
|
+
attr ? rel.where(attr => public_send(attr)) : rel
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def rebalance_scope_group!
|
|
112
|
+
col = self.class.positioning_column
|
|
113
|
+
ActiveRecord::Base.transaction do
|
|
114
|
+
positioning_group_relation.order(col).each_with_index do |row, i|
|
|
115
|
+
row.update_column(col, (i + 1).to_f)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Migration helper that adds a position column pre-tuned for fractional
|
|
121
|
+
# ordering. Mixed into ActiveRecord's table-definition classes by the
|
|
122
|
+
# railtie, so it is available in both create_table and change_table:
|
|
123
|
+
#
|
|
124
|
+
# create_table :tasks do |t|
|
|
125
|
+
# t.position # decimal :position, precision: 16, scale: 8
|
|
126
|
+
# t.position :sort_order # custom column name
|
|
127
|
+
# t.position index: true # also add a single-column index
|
|
128
|
+
# end
|
|
129
|
+
#
|
|
130
|
+
# change_table :tasks do |t|
|
|
131
|
+
# t.position
|
|
132
|
+
# end
|
|
133
|
+
#
|
|
134
|
+
# The precision/scale give midpoints ample headroom over the rebalance
|
|
135
|
+
# threshold (EPSILON = 1e-6) — a too-small scale lets the last subdivision
|
|
136
|
+
# round to a neighbor. Pass precision:/scale: to override.
|
|
137
|
+
module MigrationHelpers
|
|
138
|
+
DEFAULT_PRECISION = 16
|
|
139
|
+
DEFAULT_SCALE = 8
|
|
140
|
+
|
|
141
|
+
def position(name = :position, **options)
|
|
142
|
+
column name, :decimal, precision: DEFAULT_PRECISION, scale: DEFAULT_SCALE, **options
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/plutonium/railtie.rb
CHANGED
|
@@ -39,6 +39,30 @@ module Plutonium
|
|
|
39
39
|
load_plutonium_initializers
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
initializer "plutonium.register_migrations" do
|
|
43
|
+
Plutonium::Migrations.register(:wizards, Plutonium.root.join("db/migrate/wizard").to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Runs after the host's config/initializers/plutonium.rb (load_config_initializers)
|
|
47
|
+
# so feature flags like config.wizards.enabled are honoured. Railtie
|
|
48
|
+
# initializers otherwise run before host initializers, leaving the flag false.
|
|
49
|
+
initializer "plutonium.migrations", after: :load_config_initializers do |app|
|
|
50
|
+
# v1 supports `config.wizards.database == :primary` only; the option is
|
|
51
|
+
# reserved for future multi-db routing of wizard sessions. Fail loudly on an
|
|
52
|
+
# unsupported value rather than silently registering on the primary database.
|
|
53
|
+
if Plutonium.configuration.wizards.enabled &&
|
|
54
|
+
Plutonium.configuration.wizards.database != :primary
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"config.wizards.database = #{Plutonium.configuration.wizards.database.inspect} is not " \
|
|
57
|
+
"supported (only :primary). Multi-database routing for wizard sessions is a roadmap follow-up."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Plutonium::Migrations.enabled_paths.each do |path|
|
|
61
|
+
app.config.paths["db/migrate"] << path
|
|
62
|
+
ActiveRecord::Migrator.migrations_paths << path unless ActiveRecord::Migrator.migrations_paths.include?(path)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
42
66
|
initializer "plutonium.asset_server" do
|
|
43
67
|
setup_development_asset_server if Plutonium.configuration.development?
|
|
44
68
|
end
|
|
@@ -47,6 +71,14 @@ module Plutonium
|
|
|
47
71
|
extend_action_dispatch
|
|
48
72
|
end
|
|
49
73
|
|
|
74
|
+
# Make `t.position` available in create_table / change_table blocks.
|
|
75
|
+
initializer "plutonium.positioning" do
|
|
76
|
+
ActiveSupport.on_load(:active_record) do
|
|
77
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.include(Plutonium::Positioning::MigrationHelpers)
|
|
78
|
+
ActiveRecord::ConnectionAdapters::Table.include(Plutonium::Positioning::MigrationHelpers)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
50
82
|
initializer "plutonium.phlexi_themes" do
|
|
51
83
|
setup_phlexi_themes
|
|
52
84
|
end
|
|
@@ -97,6 +129,7 @@ module Plutonium
|
|
|
97
129
|
|
|
98
130
|
def extend_action_dispatch
|
|
99
131
|
ActionDispatch::Routing::Mapper.prepend Plutonium::Routing::MapperExtensions
|
|
132
|
+
ActionDispatch::Routing::Mapper.prepend Plutonium::Routing::WizardRegistration
|
|
100
133
|
ActionDispatch::Routing::RouteSet.prepend Plutonium::Routing::RouteSetExtensions
|
|
101
134
|
Rails::Engine.include Plutonium::Routing::ResourceRegistration
|
|
102
135
|
end
|