plutonium 0.62.2 → 0.63.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 +44 -0
- data/.claude/skills/plutonium-app/SKILL.md +3 -3
- data/.claude/skills/plutonium-async-interactions/SKILL.md +191 -0
- data/.claude/skills/plutonium-auth/SKILL.md +36 -0
- data/.claude/skills/plutonium-behavior/SKILL.md +121 -24
- data/.claude/skills/plutonium-kanban/SKILL.md +17 -3
- data/.claude/skills/plutonium-resource/SKILL.md +259 -12
- data/.claude/skills/plutonium-tenancy/SKILL.md +32 -3
- data/.claude/skills/plutonium-ui/SKILL.md +115 -14
- data/.claude/skills/plutonium-wizard/SKILL.md +73 -4
- data/CHANGELOG.md +67 -0
- data/CLAUDE.md +87 -0
- data/Rakefile +34 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +685 -102
- 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/rodauth/_login_form.html.erb +13 -0
- data/db/migrate/async_interactions/20260817000001_create_plutonium_async_runs.rb +170 -0
- data/docs/.vitepress/config.ts +81 -3
- data/docs/.vitepress/theme/blog.data.ts +44 -0
- data/docs/.vitepress/theme/components/BlogIndex.vue +87 -0
- data/docs/.vitepress/theme/components/BlogMeta.vue +47 -0
- data/docs/.vitepress/theme/components/HomeFeatureTour.vue +293 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +3 -3
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +8 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +1 -0
- data/docs/.vitepress/theme/components/HomeWhyPlutonium.vue +84 -0
- data/docs/.vitepress/theme/index.ts +8 -4
- data/docs/blog/association-inputs-post-signed-ids.md +70 -0
- data/docs/blog/fix-the-model-not-the-policy.md +122 -0
- data/docs/blog/fractional-ordering-runs-out-of-room.md +67 -0
- data/docs/blog/half-finished-forms-are-pii.md +76 -0
- data/docs/blog/index.md +13 -0
- data/docs/blog/interactions-are-presentation-objects.md +152 -0
- data/docs/blog/introducing-plutonium.md +253 -0
- data/docs/blog/jobs-are-not-permission-snapshots.md +100 -0
- data/docs/blog/plutonium-and-ai-agents.md +48 -0
- data/docs/blog/realtime-is-one-line-and-four-dependencies.md +70 -0
- data/docs/blog/two-forms-one-dom-id.md +69 -0
- data/docs/blog/whats-new-async-kanban-wizards.md +130 -0
- data/docs/getting-started/tutorial/04-authorization.md +12 -3
- data/docs/getting-started/tutorial/06-nested-resources.md +3 -1
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/guides/authentication.md +73 -0
- data/docs/guides/authorization.md +2 -0
- data/docs/guides/creating-packages.md +5 -3
- data/docs/guides/custom-actions.md +74 -12
- data/docs/guides/customizing-ui.md +9 -2
- data/docs/guides/index.md +1 -0
- data/docs/guides/kanban.md +7 -5
- data/docs/guides/nested-resources.md +11 -1
- data/docs/guides/performance.md +104 -0
- data/docs/guides/user-invites.md +1 -1
- data/docs/guides/wizards.md +9 -1
- data/docs/index.md +3 -3
- data/docs/public/images/home/tour-actions.png +0 -0
- data/docs/public/images/home/tour-async.png +0 -0
- data/docs/public/images/home/tour-kanban.png +0 -0
- data/docs/public/images/home/tour-tenancy.png +0 -0
- data/docs/public/images/home/tour-wizard.png +0 -0
- data/docs/public/images/reference/async-progress-page.png +0 -0
- data/docs/public/images/reference/async-running-banner.png +0 -0
- data/docs/public/templates/experimental.rb +34 -0
- data/docs/public/templates/pluton8.rb +14 -0
- data/docs/reference/app/portals.md +15 -3
- data/docs/reference/auth/accounts.md +19 -0
- data/docs/reference/behavior/async-interactions.md +295 -0
- data/docs/reference/behavior/controllers.md +17 -4
- data/docs/reference/behavior/index.md +7 -1
- data/docs/reference/behavior/interactions.md +152 -22
- data/docs/reference/configuration.md +5 -0
- data/docs/reference/index.md +1 -0
- data/docs/reference/kanban/dsl.md +7 -4
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +26 -4
- data/docs/reference/positioning.md +568 -0
- data/docs/reference/resource/actions.md +97 -4
- data/docs/reference/resource/definition.md +181 -9
- data/docs/reference/tenancy/invites.md +1 -1
- data/docs/reference/tenancy/nested-resources.md +60 -2
- data/docs/reference/ui/assets.md +4 -0
- data/docs/reference/ui/components.md +57 -4
- data/docs/reference/ui/displays.md +20 -10
- data/docs/reference/ui/index.md +1 -1
- data/docs/reference/wizard/dsl.md +33 -0
- data/docs/reference/wizard/storage-config.md +1 -0
- data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md +624 -0
- data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md.tasks.json +32 -0
- data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md +1787 -0
- data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md.tasks.json +91 -0
- data/docs/superpowers/plans/2026-08-17-async-interactions.md +1414 -0
- data/docs/superpowers/plans/2026-08-17-async-interactions.md.tasks.json +66 -0
- data/docs/superpowers/specs/2026-07-16-homepage-depth-upgrade-design.md +111 -0
- data/docs/superpowers/specs/2026-07-17-action-html-attributes-design.md +124 -0
- data/docs/superpowers/specs/2026-07-31-positioned-drag-and-drop-design.md +506 -0
- data/docs/superpowers/specs/2026-08-17-async-interactions-design.md +185 -0
- data/gemfiles/postgres.gemfile.lock +85 -85
- data/gemfiles/rails_7.gemfile.lock +322 -140
- data/gemfiles/rails_8.0.gemfile.lock +125 -115
- data/gemfiles/rails_8.1.gemfile.lock +126 -116
- data/lib/generators/pu/async_interactions/install_generator.rb +111 -0
- data/lib/generators/pu/async_interactions/templates/app/controllers/async_runs_controller.rb.tt +15 -0
- data/lib/generators/pu/core/typespec/typespec_generator.rb +7 -4
- data/lib/generators/pu/invites/install_generator.rb +3 -3
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +2 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +47 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/resource_registration.rb +41 -0
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +1 -1
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +1 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +19 -39
- data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +4 -0
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +15 -6
- data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +7 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +2 -2
- data/lib/generators/pu/wizards/install_generator.rb +78 -0
- data/lib/plutonium/action/base.rb +71 -9
- data/lib/plutonium/action/interactive.rb +9 -0
- data/lib/plutonium/attachments.rb +254 -0
- data/lib/plutonium/configuration.rb +82 -1
- data/lib/plutonium/core/controller.rb +50 -7
- data/lib/plutonium/core/controllers/authorizable.rb +16 -0
- data/lib/plutonium/core/controllers/entity_scoping.rb +12 -2
- data/lib/plutonium/definition/base.rb +51 -0
- data/lib/plutonium/definition/display_layout.rb +112 -0
- data/lib/plutonium/definition/index_views.rb +8 -7
- data/lib/plutonium/definition/input_aliases.rb +38 -0
- data/lib/plutonium/definition/page_widths.rb +65 -0
- data/lib/plutonium/definition/positioning.rb +126 -0
- data/lib/plutonium/definition/sorting.rb +17 -2
- data/lib/plutonium/helpers/turbo_helper.rb +7 -0
- data/lib/plutonium/interaction/README.md +61 -24
- data/lib/plutonium/interaction/async/configuration.rb +38 -0
- data/lib/plutonium/interaction/async/context.rb +419 -0
- data/lib/plutonium/interaction/async/executor.rb +422 -0
- data/lib/plutonium/interaction/async/job.rb +80 -0
- data/lib/plutonium/interaction/async/reap_job.rb +81 -0
- data/lib/plutonium/interaction/async/run.rb +394 -0
- data/lib/plutonium/interaction/async/run_definition.rb +155 -0
- data/lib/plutonium/interaction/async/run_policy.rb +86 -0
- data/lib/plutonium/interaction/base.rb +34 -7
- data/lib/plutonium/interaction/concerns/dispatchable.rb +518 -0
- data/lib/plutonium/interaction/concerns/scoping.rb +70 -9
- data/lib/plutonium/interaction/response/redirect.rb +11 -3
- data/lib/plutonium/kanban/board.rb +14 -0
- data/lib/plutonium/kanban/column.rb +4 -2
- data/lib/plutonium/kanban/dsl.rb +4 -1
- data/lib/plutonium/kanban/grouping.rb +9 -22
- data/lib/plutonium/kanban/positioning.rb +5 -65
- data/lib/plutonium/positioning/config.rb +94 -0
- data/lib/plutonium/positioning/model.rb +128 -0
- data/lib/plutonium/positioning.rb +25 -86
- data/lib/plutonium/railtie.rb +1 -0
- data/lib/plutonium/resource/controller.rb +118 -38
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +32 -2
- data/lib/plutonium/resource/controllers/crud_actions.rb +30 -2
- data/lib/plutonium/resource/controllers/eager_loading.rb +87 -0
- data/lib/plutonium/resource/controllers/export_csv.rb +10 -1
- data/lib/plutonium/resource/controllers/kanban_actions.rb +53 -14
- data/lib/plutonium/resource/controllers/position_actions.rb +390 -0
- data/lib/plutonium/resource/controllers/presentable.rb +19 -13
- data/lib/plutonium/resource/controllers/queryable.rb +5 -1
- data/lib/plutonium/resource/controllers/wizard_actions.rb +21 -0
- data/lib/plutonium/resource/policy.rb +33 -0
- data/lib/plutonium/resource/query_object.rb +36 -0
- data/lib/plutonium/routing/mapper_extensions.rb +100 -8
- data/lib/plutonium/routing/route_set_extensions.rb +15 -1
- data/lib/plutonium/routing/wizard_registration.rb +4 -0
- data/lib/plutonium/testing/resource_policy.rb +6 -2
- data/lib/plutonium/ui/action_button.rb +12 -7
- data/lib/plutonium/ui/actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/block.rb +21 -1
- data/lib/plutonium/ui/breadcrumbs.rb +187 -55
- data/lib/plutonium/ui/component/methods.rb +5 -0
- data/lib/plutonium/ui/component/positionable.rb +112 -0
- data/lib/plutonium/ui/component/resolves_tags.rb +57 -0
- data/lib/plutonium/ui/component/section.rb +185 -0
- data/lib/plutonium/ui/display/base.rb +13 -1
- data/lib/plutonium/ui/display/components/formatted_value.rb +26 -0
- data/lib/plutonium/ui/display/components/section.rb +18 -0
- data/lib/plutonium/ui/display/resource.rb +141 -22
- data/lib/plutonium/ui/display/theme.rb +20 -1
- data/lib/plutonium/ui/export_button.rb +1 -1
- data/lib/plutonium/ui/form/base.rb +8 -7
- data/lib/plutonium/ui/form/components/intl_tel_input.rb +1 -1
- data/lib/plutonium/ui/form/components/section.rb +7 -62
- data/lib/plutonium/ui/form/components/uppy.rb +12 -1
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +16 -3
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -1
- data/lib/plutonium/ui/form/query.rb +2 -4
- data/lib/plutonium/ui/form/resource.rb +92 -15
- data/lib/plutonium/ui/form/theme.rb +17 -0
- data/lib/plutonium/ui/form/wizard.rb +25 -1
- data/lib/plutonium/ui/grid/card.rb +79 -11
- data/lib/plutonium/ui/grid/resource.rb +47 -5
- data/lib/plutonium/ui/interaction/async/run_progress.rb +227 -0
- data/lib/plutonium/ui/interaction/async/running_banner.rb +65 -0
- data/lib/plutonium/ui/kanban/card.rb +2 -1
- data/lib/plutonium/ui/kanban/column.rb +12 -6
- data/lib/plutonium/ui/kanban/resource.rb +6 -7
- data/lib/plutonium/ui/layout/base.rb +10 -3
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -0
- data/lib/plutonium/ui/page/base.rb +19 -0
- data/lib/plutonium/ui/page/edit.rb +4 -1
- data/lib/plutonium/ui/page/index.rb +69 -18
- data/lib/plutonium/ui/page/interactive_action.rb +5 -1
- data/lib/plutonium/ui/page/new.rb +4 -1
- data/lib/plutonium/ui/page/show.rb +27 -10
- data/lib/plutonium/ui/page/wizard.rb +10 -1
- data/lib/plutonium/ui/page/wizard_chooser.rb +36 -11
- data/lib/plutonium/ui/page_width.rb +58 -0
- data/lib/plutonium/ui/table/base.rb +34 -1
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +32 -8
- data/lib/plutonium/ui/table/components/drag_handle.rb +120 -0
- data/lib/plutonium/ui/table/components/filter_form.rb +1 -4
- data/lib/plutonium/ui/table/components/filter_pills.rb +1 -1
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/resource.rb +50 -5
- data/lib/plutonium/ui/table/theme.rb +59 -2
- data/lib/plutonium/ui/wizard/review.rb +4 -2
- data/lib/plutonium/ui/wizard/summary_display.rb +42 -14
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/attachments.rb +32 -197
- data/lib/plutonium/wizard/base.rb +6 -1
- data/lib/plutonium/wizard/configuration.rb +12 -0
- data/lib/plutonium/wizard/controller.rb +14 -0
- data/lib/plutonium/wizard/driving.rb +99 -15
- data/lib/plutonium/wizard/dsl.rb +23 -0
- data/lib/plutonium/wizard/resume.rb +127 -49
- data/lib/plutonium/wizard/runner.rb +46 -2
- data/lib/plutonium/wizard/step_adapter.rb +1 -1
- data/lib/plutonium/wizard/sweep_job.rb +16 -0
- data/lib/plutonium.rb +21 -0
- data/lib/rodauth/features/session_isolation.rb +92 -0
- data/lib/rodauth/plugins.rb +1 -0
- data/package.json +2 -1
- data/plutonium.gemspec +29 -11
- data/src/css/components.css +89 -1
- data/src/css/slim_select.css +20 -0
- data/src/js/controllers/breadcrumbs_controller.js +112 -0
- data/src/js/controllers/bulk_actions_controller.js +10 -2
- data/src/js/controllers/kanban_controller.js +30 -21
- data/src/js/controllers/positioned_controller.js +452 -0
- data/src/js/controllers/register_controllers.js +6 -0
- data/src/js/controllers/resource_drop_down_controller.js +5 -0
- data/src/js/controllers/run_progress_controller.js +73 -0
- data/src/js/drag/sortable.js +186 -0
- data/yarn.lock +108 -63
- metadata +103 -16
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +0 -53
- data/docs/.vitepress/theme/components/HomePillars.vue +0 -42
|
@@ -12,9 +12,14 @@ module Plutonium
|
|
|
12
12
|
include Plutonium::Resource::Controllers::Defineable
|
|
13
13
|
include Plutonium::Resource::Controllers::Authorizable
|
|
14
14
|
include Plutonium::Resource::Controllers::Presentable
|
|
15
|
+
include Plutonium::Resource::Controllers::EagerLoading
|
|
15
16
|
include Plutonium::Resource::Controllers::Queryable
|
|
16
17
|
include Plutonium::Resource::Controllers::CrudActions
|
|
17
18
|
include Plutonium::Resource::Controllers::KanbanActions
|
|
19
|
+
# After CrudActions on purpose: PositionActions overrides
|
|
20
|
+
# pagy_request_context / current_page_* so the reposition drop re-renders
|
|
21
|
+
# the collection for the INDEX page rather than for its own POST path.
|
|
22
|
+
include Plutonium::Resource::Controllers::PositionActions
|
|
18
23
|
include Plutonium::Resource::Controllers::InteractiveActions
|
|
19
24
|
include Plutonium::Resource::Controllers::WizardActions
|
|
20
25
|
include Plutonium::Resource::Controllers::Typeahead
|
|
@@ -24,7 +29,8 @@ module Plutonium
|
|
|
24
29
|
included do
|
|
25
30
|
after_action { response.headers.merge!(@pagy.headers_hash) if @pagy }
|
|
26
31
|
|
|
27
|
-
helper_method :current_parent, :current_nested_association, :resource_record!, :resource_record?, :resource_param_key, :resource_class
|
|
32
|
+
helper_method :current_parent, :current_nested_association, :resource_record!, :resource_record?, :resource_param_key, :resource_class,
|
|
33
|
+
:singular_resource_context?, :singular_resource_route_for?
|
|
28
34
|
|
|
29
35
|
# Use class_attribute for proper inheritance
|
|
30
36
|
class_attribute :_resource_class, instance_accessor: false
|
|
@@ -101,11 +107,14 @@ module Plutonium
|
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
def current_resource_route_config
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
# The nested registration when the route says it is nested, and the
|
|
111
|
+
# top-level one otherwise. Previously rebuilt the nested lookup key from
|
|
112
|
+
# the parent's class and the association — which meant resolving the
|
|
113
|
+
# parent first, and reconstructing a string the router was already
|
|
114
|
+
# carrying.
|
|
115
|
+
@current_resource_route_config ||=
|
|
116
|
+
current_nested_route_config ||
|
|
107
117
|
current_engine.routes.resource_route_config_for(resource_class.model_name.plural)[0]
|
|
108
|
-
end
|
|
109
118
|
end
|
|
110
119
|
|
|
111
120
|
# Returns true if current resource is registered as a singular route
|
|
@@ -115,23 +124,22 @@ module Plutonium
|
|
|
115
124
|
current_resource_route_config&.[](:route_type) == :resource
|
|
116
125
|
end
|
|
117
126
|
|
|
118
|
-
#
|
|
119
|
-
#
|
|
127
|
+
# The same question asked about another resource — a breadcrumb needs to
|
|
128
|
+
# know whether the parent it is linking to has an index before it builds a
|
|
129
|
+
# collection URL for one.
|
|
130
|
+
# @param resource_class [Class]
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def singular_resource_route_for?(resource_class)
|
|
133
|
+
current_engine.routes.singular_resource_route?(resource_class.model_name.plural)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The association this request is nested through, as declared by the
|
|
137
|
+
# route. Previously scraped out of the request path by looking for a
|
|
138
|
+
# "nested_" segment, which meant stripping format extensions and could
|
|
139
|
+
# not survive a parent that contributes no id parameter.
|
|
120
140
|
# @return [Symbol, nil] The association name
|
|
121
141
|
def current_nested_association
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# Extract from request path: find the nested_* segment after the parent param
|
|
125
|
-
# e.g., /posts/123/nested_comments/456 => "comments"
|
|
126
|
-
# Note: Strip format extension (.json, .xml, etc.) from the segment
|
|
127
|
-
prefix = Plutonium::Routing::NESTED_ROUTE_PREFIX
|
|
128
|
-
path_segments = request.path.split("/")
|
|
129
|
-
nested_segment = path_segments.find { |seg| seg.start_with?(prefix) }
|
|
130
|
-
return unless nested_segment
|
|
131
|
-
|
|
132
|
-
# Remove prefix and any format extension (e.g., "nested_versions.json" -> "versions")
|
|
133
|
-
association_name = nested_segment.delete_prefix(prefix).sub(/\.\w+\z/, "")
|
|
134
|
-
association_name.to_sym
|
|
142
|
+
current_nested_route_config&.[](:association_name)
|
|
135
143
|
end
|
|
136
144
|
|
|
137
145
|
def resource_record!
|
|
@@ -185,9 +193,8 @@ module Plutonium
|
|
|
185
193
|
# `:attachment`). Excluded from the extraction-record pre-assignment so a
|
|
186
194
|
# single-read Rack upload isn't consumed before create/update reads it.
|
|
187
195
|
def attachment_input_keys
|
|
188
|
-
file_types = Plutonium::UI::Form::Base::Builder::FILE_INPUT_TYPES
|
|
189
196
|
current_definition.defined_inputs.filter_map { |name, config|
|
|
190
|
-
name.to_s if
|
|
197
|
+
name.to_s if Plutonium::Definition::InputAliases.file_input?(config.dig(:options, :as))
|
|
191
198
|
}
|
|
192
199
|
end
|
|
193
200
|
|
|
@@ -237,24 +244,79 @@ module Plutonium
|
|
|
237
244
|
|
|
238
245
|
# Returns the current parent based on path parameters
|
|
239
246
|
# @return [ActiveRecord::Base, nil] The current parent
|
|
247
|
+
# The record this request is nested under, if any.
|
|
248
|
+
#
|
|
249
|
+
# The route says which parent it nests under (see
|
|
250
|
+
# Plutonium::Routing::PARENT_KEY_PARAM); all that is
|
|
251
|
+
# left is to load it. A plural parent narrows by the id in the path. A
|
|
252
|
+
# singular parent has no id to narrow by — it is whichever record the
|
|
253
|
+
# viewer's scope resolves to, which is what `singular: true` promises.
|
|
254
|
+
#
|
|
255
|
+
# Memoised through `defined?` so a genuine nil is remembered rather than
|
|
256
|
+
# re-resolved on every call.
|
|
240
257
|
def current_parent
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
@current_parent
|
|
244
|
-
|
|
245
|
-
parent_class
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
return @current_parent if defined?(@current_parent)
|
|
259
|
+
|
|
260
|
+
@current_parent = begin
|
|
261
|
+
parent_class = current_parent_class
|
|
262
|
+
if parent_class
|
|
263
|
+
scope = authorized_scope(parent_class.all, context: {entity_scope: entity_scope_for_authorize})
|
|
264
|
+
parent = if singular_resource_route_for?(parent_class)
|
|
265
|
+
# A singular parent puts no id in the path, so the scope must
|
|
266
|
+
# already identify exactly one record. `sole` says so: `first!`
|
|
267
|
+
# would quietly hand back whichever row sorted first if the scope
|
|
268
|
+
# ever resolved to several, and silently nesting under the wrong
|
|
269
|
+
# parent is far worse than failing loudly.
|
|
270
|
+
resolve_singular_parent(scope, parent_class)
|
|
271
|
+
else
|
|
272
|
+
# The id parameter is named by the parent's own route, so it is
|
|
273
|
+
# derived rather than discovered. Scanning path_parameters for
|
|
274
|
+
# anything ending in _id would also match an unrelated parameter
|
|
275
|
+
# an application happens to name that way.
|
|
276
|
+
scope.from_path_param(params[:"#{parent_class.model_name.singular}_id"]).first!
|
|
277
|
+
end
|
|
278
|
+
parent.tap { authorize! parent, to: :read? }
|
|
279
|
+
end
|
|
251
280
|
end
|
|
252
281
|
end
|
|
253
282
|
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
283
|
+
# `sole`, but reporting the registration mistake instead of the symptom.
|
|
284
|
+
#
|
|
285
|
+
# `SoleRecordExceeded` says "Wanted only one User" — true, and useless to
|
|
286
|
+
# the person who has to fix it, because the cause is two files away: the
|
|
287
|
+
# resource was registered `singular: true` while its policy still scopes to
|
|
288
|
+
# many. Say that, and name both halves of the fix.
|
|
289
|
+
# @raise [Plutonium::SingularScopeError]
|
|
290
|
+
def resolve_singular_parent(scope, parent_class)
|
|
291
|
+
scope.sole
|
|
292
|
+
rescue ActiveRecord::SoleRecordExceeded
|
|
293
|
+
# Deliberately says nothing about the viewer: this runs while raising, and
|
|
294
|
+
# reaching for `current_user` here would swap the useful error for a
|
|
295
|
+
# NoMethodError in any context that has no such helper.
|
|
296
|
+
raise Plutonium::SingularScopeError,
|
|
297
|
+
"#{parent_class} is registered `singular: true`, but its authorized scope " \
|
|
298
|
+
"resolved to #{scope.count} records. A singular route carries no :id, so the " \
|
|
299
|
+
"scope has to identify exactly one record. Either give #{parent_class}'s policy " \
|
|
300
|
+
"a `relation_scope` that narrows to one, or register it without `singular: true`."
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# The parent resource class, taken from the registration rather than
|
|
304
|
+
# re-derived from a name. Registration had the class in hand; carrying the
|
|
305
|
+
# name instead would mean looking it back up by a string that two
|
|
306
|
+
# differently-namespaced resources can share.
|
|
307
|
+
# @return [Class, nil]
|
|
308
|
+
def current_parent_class
|
|
309
|
+
current_nested_route_config&.[](:parent_class)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The registration for the nesting this request arrived through, if any.
|
|
313
|
+
# @return [Hash, nil]
|
|
314
|
+
def current_nested_route_config
|
|
315
|
+
return @current_nested_route_config if defined?(@current_nested_route_config)
|
|
316
|
+
|
|
317
|
+
nested_key = request.path_parameters[Plutonium::Routing::NESTED_KEY_PARAM]
|
|
318
|
+
@current_nested_route_config =
|
|
319
|
+
nested_key && current_engine.routes.resource_route_config_lookup[nested_key]
|
|
258
320
|
end
|
|
259
321
|
|
|
260
322
|
# Returns the parent input parameter (the belongs_to association name on the child)
|
|
@@ -275,10 +337,28 @@ module Plutonium
|
|
|
275
337
|
# Try inverse_of first (if explicitly set)
|
|
276
338
|
return parent_assoc.inverse_of.name.to_sym if parent_assoc.inverse_of
|
|
277
339
|
|
|
278
|
-
# Fall back to finding belongs_to by foreign key
|
|
340
|
+
# Fall back to finding belongs_to by foreign key.
|
|
341
|
+
#
|
|
342
|
+
# Polymorphic reflections are skipped rather than compared: asking a
|
|
343
|
+
# polymorphic belongs_to for its klass raises, and one sharing the
|
|
344
|
+
# parent's foreign key name would take the whole request down.
|
|
345
|
+
#
|
|
346
|
+
# The class is matched with is_a? rather than ==, so a parent that is an
|
|
347
|
+
# STI subclass still matches an association declared against its base.
|
|
279
348
|
foreign_key = parent_assoc.foreign_key.to_s
|
|
280
349
|
child_assoc = resource_class.reflect_on_all_associations(:belongs_to).find do |assoc|
|
|
281
|
-
assoc.foreign_key.to_s == foreign_key
|
|
350
|
+
next false unless assoc.foreign_key.to_s == foreign_key
|
|
351
|
+
|
|
352
|
+
if assoc.polymorphic?
|
|
353
|
+
# A polymorphic belongs_to cannot be asked for its class — that
|
|
354
|
+
# raises. Its counterpart is the type column, which is exactly what
|
|
355
|
+
# `has_many :things, as: :owner` names on the parent side, so the
|
|
356
|
+
# two are matched on that instead. Assigning the parent to the
|
|
357
|
+
# association then sets both the id and the type.
|
|
358
|
+
parent_assoc.type.present? && assoc.foreign_type.to_s == parent_assoc.type.to_s
|
|
359
|
+
else
|
|
360
|
+
current_parent.is_a?(assoc.klass)
|
|
361
|
+
end
|
|
282
362
|
end
|
|
283
363
|
child_assoc&.name&.to_sym
|
|
284
364
|
end
|
|
@@ -7,8 +7,38 @@ module Plutonium
|
|
|
7
7
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# `action` names the action whose field set is about to be rendered.
|
|
11
|
+
# It defaults to the current one, and the reposition drop POST passes
|
|
12
|
+
# "index" for the same reason it does when building the collection: it
|
|
13
|
+
# re-renders the index on the index's behalf, and a resource must not
|
|
14
|
+
# have to define permitted_attributes_for_reposition to be draggable.
|
|
15
|
+
def setup_index_action!(action: action_name)
|
|
16
|
+
# Applied HERE, not inside filtered_resource_collection: what to
|
|
17
|
+
# preload depends on what is about to be RENDERED, and
|
|
18
|
+
# filtered_resource_collection is shared with the CSV export, which
|
|
19
|
+
# renders a different column set (`permitted_attributes_for_export`).
|
|
20
|
+
# Reading the index's fields in there resolved the wrong policy
|
|
21
|
+
# method for that action and raised. Keeping the hook to filtering and
|
|
22
|
+
# eager-loading at the point of use also leaves an app's own
|
|
23
|
+
# `filtered_resource_collection` override unaffected.
|
|
24
|
+
collection = auto_eager_load(filtered_resource_collection, presentable_attributes_for(action))
|
|
25
|
+
@pagy, @resource_records = pagy(:offset, collection, request: pagy_request_context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# What Pagy builds its page URLs from. Pagy accepts either the real
|
|
29
|
+
# request object or a plain hash (Pagy::Request), which is the seam we
|
|
30
|
+
# need: an action that re-renders the collection on the index's behalf
|
|
31
|
+
# (the reposition drop POST) must emit page links pointing at the index
|
|
32
|
+
# page, not at its own POST-only path. current_page_path is that page —
|
|
33
|
+
# it equals request.path for a genuine index request, so the default is
|
|
34
|
+
# exactly today's behaviour.
|
|
35
|
+
def pagy_request_context
|
|
36
|
+
{
|
|
37
|
+
base_url: request.base_url,
|
|
38
|
+
path: current_page_path,
|
|
39
|
+
params: request.GET.merge(request.POST).to_h,
|
|
40
|
+
cookie: request.cookies["pagy"]
|
|
41
|
+
}
|
|
12
42
|
end
|
|
13
43
|
|
|
14
44
|
def filtered_resource_collection
|
|
@@ -38,7 +38,7 @@ module Plutonium
|
|
|
38
38
|
authorize_current! resource_class
|
|
39
39
|
set_page_title "Create #{resource_class.model_name.human.titleize}"
|
|
40
40
|
|
|
41
|
-
@resource_record =
|
|
41
|
+
@resource_record = build_resource_record
|
|
42
42
|
maybe_apply_submitted_resource_params!
|
|
43
43
|
|
|
44
44
|
render :new, formats: [:html]
|
|
@@ -49,7 +49,7 @@ module Plutonium
|
|
|
49
49
|
authorize_current! resource_class
|
|
50
50
|
set_page_title "Create #{resource_class.model_name.human.titleize}"
|
|
51
51
|
|
|
52
|
-
@resource_record =
|
|
52
|
+
@resource_record = build_resource_record resource_params
|
|
53
53
|
|
|
54
54
|
respond_to do |format|
|
|
55
55
|
if params[:pre_submit]
|
|
@@ -165,6 +165,34 @@ module Plutonium
|
|
|
165
165
|
|
|
166
166
|
private
|
|
167
167
|
|
|
168
|
+
# Builds the record that :new renders a form for and :create saves.
|
|
169
|
+
#
|
|
170
|
+
# Through a nested route it is built on the parent's association rather
|
|
171
|
+
# than on the class, so an association carrying a scope supplies that
|
|
172
|
+
# scope's attributes as defaults — exactly what `parent.things.new` does
|
|
173
|
+
# in Rails. The list side already honours the scope (the policy merges
|
|
174
|
+
# the relation into that same association), so building on the class
|
|
175
|
+
# instead produces a record the list filters straight back out: the
|
|
176
|
+
# create reports success and the row never appears.
|
|
177
|
+
#
|
|
178
|
+
# Only equality conditions carry over. `where(published: true)` becomes
|
|
179
|
+
# an attribute; `where("expires_at > ?", Time.current)` cannot, because
|
|
180
|
+
# that is all Rails derives create attributes from.
|
|
181
|
+
#
|
|
182
|
+
# The foreign key is still injected as an attribute by
|
|
183
|
+
# override_parent_params, which stays the authority for the value. This
|
|
184
|
+
# only adds what the association declares beyond it.
|
|
185
|
+
def build_resource_record(attributes = {})
|
|
186
|
+
return resource_class.new(attributes) unless current_parent
|
|
187
|
+
|
|
188
|
+
association = current_parent.class.reflect_on_association(current_nested_association)
|
|
189
|
+
if association.collection?
|
|
190
|
+
current_parent.public_send(current_nested_association).new(attributes)
|
|
191
|
+
else
|
|
192
|
+
current_parent.public_send(:"build_#{current_nested_association}", attributes)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
168
196
|
# Hook fired once, immediately after a successful create-save and BEFORE
|
|
169
197
|
# the response is built. No-op by default. KanbanActions overrides it to
|
|
170
198
|
# apply a kanban quick-add column's on_enter + positioning to the freshly
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Resource
|
|
5
|
+
module Controllers
|
|
6
|
+
# Preloads the associations and attachments a collection is about to
|
|
7
|
+
# render, without being told which.
|
|
8
|
+
#
|
|
9
|
+
# Plutonium can do this where a general-purpose Rails app cannot. The
|
|
10
|
+
# rendered field set is DECLARED — resolved from the policy — rather than
|
|
11
|
+
# discovered by running a template, and resolving it never touches the
|
|
12
|
+
# collection. So the exact set of associations a page will read is known
|
|
13
|
+
# before the query is built. There is no `includes` list to write, and none
|
|
14
|
+
# to keep in step with the definition as fields come and go.
|
|
15
|
+
#
|
|
16
|
+
# Every collection rendering passes its OWN field set, because they differ:
|
|
17
|
+
# the index renders `presentable_attributes`, the CSV export renders
|
|
18
|
+
# `permitted_attributes_for_export`, and a kanban card renders its
|
|
19
|
+
# `card_fields` (falling back to the grid fields). Reading one action's
|
|
20
|
+
# fields from another's code path resolves the wrong policy method.
|
|
21
|
+
module EagerLoading
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Whether this controller preloads what it is about to render.
|
|
27
|
+
# Override to opt a single resource out (or in, against a global off).
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def auto_eager_load_collections?
|
|
30
|
+
Plutonium.configuration.auto_eager_load_collections
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param collection [ActiveRecord::Relation]
|
|
34
|
+
# @param fields [Array<Symbol>] the field set about to be rendered
|
|
35
|
+
# @return [ActiveRecord::Relation]
|
|
36
|
+
def auto_eager_load(collection, fields)
|
|
37
|
+
return collection unless auto_eager_load_collections?
|
|
38
|
+
|
|
39
|
+
fields = Array(fields).map(&:to_sym)
|
|
40
|
+
|
|
41
|
+
associations = fields & eager_loadable_associations
|
|
42
|
+
collection = collection.includes(*associations) if associations.any?
|
|
43
|
+
|
|
44
|
+
# Attachments are not associations under their declared name: for BOTH
|
|
45
|
+
# backends `reflect_on_association(:file)` is nil, and the reflections
|
|
46
|
+
# that exist are `file_attachment`/`file_blob`. `with_attached_*` is the
|
|
47
|
+
# supported preload and ActiveStorage and active_shrine both expose it,
|
|
48
|
+
# so one path serves both.
|
|
49
|
+
(fields & eager_loadable_attachments).each do |name|
|
|
50
|
+
collection = collection.public_send(:"with_attached_#{name}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
collection
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Every association kind, `has_many` included.
|
|
57
|
+
#
|
|
58
|
+
# `has_many` is preloaded because of what Plutonium actually renders: an
|
|
59
|
+
# association field renders the associated records' LABELS, not a count.
|
|
60
|
+
# The rows are read either way, so preloading only decides whether that
|
|
61
|
+
# costs one query or one per parent. (Excluding it on the usual "a count
|
|
62
|
+
# beats loading every child row" reasoning is an argument about a
|
|
63
|
+
# rendering this framework doesn't do.)
|
|
64
|
+
#
|
|
65
|
+
# Both lists are memoised on the model (outside dev) by
|
|
66
|
+
# Resource::Record::FieldNames. Its association lists already strip the
|
|
67
|
+
# `_attachment`/`_blob` reflections backing an attachment, so those cannot
|
|
68
|
+
# be loaded twice by the two branches above. Its attachment lists go
|
|
69
|
+
# through `reflect_on_all_attachments`, which — unlike
|
|
70
|
+
# `attachment_reflections`, empty under active_shrine — reports both
|
|
71
|
+
# backends.
|
|
72
|
+
def eager_loadable_associations
|
|
73
|
+
@eager_loadable_associations ||=
|
|
74
|
+
resource_class.belongs_to_association_field_names +
|
|
75
|
+
resource_class.has_one_association_field_names +
|
|
76
|
+
resource_class.has_many_association_field_names
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def eager_loadable_attachments
|
|
80
|
+
@eager_loadable_attachments ||=
|
|
81
|
+
resource_class.has_one_attached_field_names +
|
|
82
|
+
resource_class.has_many_attached_field_names
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -34,6 +34,9 @@ module Plutonium
|
|
|
34
34
|
module ExportCsv
|
|
35
35
|
extend ActiveSupport::Concern
|
|
36
36
|
|
|
37
|
+
# Collections rendered here preload what they are about to show.
|
|
38
|
+
include Plutonium::Resource::Controllers::EagerLoading
|
|
39
|
+
|
|
37
40
|
# Placeholder written when a column is neither an `export` block nor a
|
|
38
41
|
# real attribute on the record, so the export degrades to a usable file
|
|
39
42
|
# instead of a mid-stream NoMethodError (which would truncate the
|
|
@@ -84,7 +87,13 @@ module Plutonium
|
|
|
84
87
|
# object entirely (no scope/filter/search/default-scope).
|
|
85
88
|
# Both still respect tenant/parent scoping (current_authorized_scope).
|
|
86
89
|
def export_csv_collection
|
|
87
|
-
export_all_requested? ? current_authorized_scope : filtered_resource_collection
|
|
90
|
+
scope = export_all_requested? ? current_authorized_scope : filtered_resource_collection
|
|
91
|
+
# Preloaded against the EXPORT's own column set, which is
|
|
92
|
+
# `permitted_attributes_for_export` and need not match the index's.
|
|
93
|
+
# This is the worst N+1 of the three collection renderings: an export
|
|
94
|
+
# streams every matching row, not a page of them, so a per-row
|
|
95
|
+
# association read is unbounded.
|
|
96
|
+
auto_eager_load(scope, exportable_attributes)
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
def export_all_requested?
|
|
@@ -32,6 +32,9 @@ module Plutonium
|
|
|
32
32
|
module KanbanActions
|
|
33
33
|
extend ActiveSupport::Concern
|
|
34
34
|
|
|
35
|
+
# Collections rendered here preload what they are about to show.
|
|
36
|
+
include Plutonium::Resource::Controllers::EagerLoading
|
|
37
|
+
|
|
35
38
|
# Tags board-bound redirects with kanban_reload=1 so the permanent board
|
|
36
39
|
# refreshes its cached column frames on arrival (see #kanban_reload_url).
|
|
37
40
|
# Wraps ALL three redirect helpers — create/update (after_submit), destroy
|
|
@@ -133,7 +136,7 @@ module Plutonium
|
|
|
133
136
|
# neighbor computation and WIP count are correct in all cases
|
|
134
137
|
# (cross-column, same-column reorder, record already in destination).
|
|
135
138
|
dest_scoped = Plutonium::Kanban::Grouping.apply_scope(kanban_base_relation, to.scope)
|
|
136
|
-
dest_cards = board.
|
|
139
|
+
dest_cards = board.position_config_for(current_definition).order(dest_scoped).where.not(id: record.id).to_a
|
|
137
140
|
# to_index is client-supplied. Clamp to [0, dest_cards.size] so a negative
|
|
138
141
|
# value can't wrap via Ruby's negative array indexing (dest_cards[-1] would
|
|
139
142
|
# silently anchor the drop to the LAST card) and an over-large value simply
|
|
@@ -197,7 +200,27 @@ module Plutonium
|
|
|
197
200
|
@resource_record = record
|
|
198
201
|
end
|
|
199
202
|
|
|
200
|
-
ActiveRecord::Base.
|
|
203
|
+
# Opened on the RECORD'S OWN connection, not ActiveRecord::Base's. A
|
|
204
|
+
# transaction lives on the receiver's connection pool, and every write
|
|
205
|
+
# this block owns — the on_exit/on_enter save!, reposition!'s update!
|
|
206
|
+
# (and any rebalance it triggers), the final save! — goes out on
|
|
207
|
+
# `record`'s pool. For a resource on a secondary database
|
|
208
|
+
# ActiveRecord::Base.transaction would BEGIN on primary and leave all of
|
|
209
|
+
# those autocommitting unprotected: a failure mid-move could persist an
|
|
210
|
+
# on_enter column change with no reposition, or a half-applied
|
|
211
|
+
# rebalance. Do not "simplify" this back to ActiveRecord::Base.
|
|
212
|
+
#
|
|
213
|
+
# LIMIT — this is a single-connection guarantee, and the drop
|
|
214
|
+
# interaction (step 2) plus user-supplied on_exit/on_enter blocks may
|
|
215
|
+
# write models the framework knows nothing about. Those are covered iff
|
|
216
|
+
# they sit on the SAME connection as `record` (the overwhelmingly common
|
|
217
|
+
# case: one database, or the same secondary as the resource). Writes to
|
|
218
|
+
# a model on a DIFFERENT database are NOT rolled back by this block, and
|
|
219
|
+
# no choice of receiver can fix that — Rails has no two-phase commit
|
|
220
|
+
# across pools, and nesting a second transaction would only shrink the
|
|
221
|
+
# window, not close it. record.class is the best available receiver
|
|
222
|
+
# because it is the one whose writes this action definitely owns.
|
|
223
|
+
record.class.transaction do
|
|
201
224
|
# (1) Apply on_exit (SOURCE column) then on_enter (DESTINATION column),
|
|
202
225
|
# CROSS-column moves only. A same-column reorder skips both (see
|
|
203
226
|
# cross_column above) and only repositions; on_exit/on_enter represent
|
|
@@ -256,7 +279,7 @@ module Plutonium
|
|
|
256
279
|
# Mode A delegates to record.reposition! (calls update! for position).
|
|
257
280
|
# Mode B calls the user-supplied block.
|
|
258
281
|
# Mode C is a no-op (no ordering; position unchanged).
|
|
259
|
-
board.
|
|
282
|
+
board.position_config_for(current_definition).reposition!(
|
|
260
283
|
record:,
|
|
261
284
|
column: to.key,
|
|
262
285
|
prev_record:,
|
|
@@ -320,7 +343,7 @@ module Plutonium
|
|
|
320
343
|
if run_enter_interaction
|
|
321
344
|
streams += [turbo_stream.update(Plutonium::REMOTE_MODAL_FRAME, "")]
|
|
322
345
|
outcome.messages.each do |msg, type|
|
|
323
|
-
streams += [turbo_stream.append(
|
|
346
|
+
streams += [turbo_stream.append(Plutonium::FLASH_REGION, partial: "plutonium/toast",
|
|
324
347
|
locals: {type: ((type == :notice) ? :success : type), msg:})]
|
|
325
348
|
end
|
|
326
349
|
end
|
|
@@ -519,10 +542,26 @@ module Plutonium
|
|
|
519
542
|
.extract_input(params, view_context:)[:q]
|
|
520
543
|
|
|
521
544
|
base_query = current_authorized_scope
|
|
522
|
-
current_query_object.apply(base_query, query_params, context: self)
|
|
545
|
+
collection = current_query_object.apply(base_query, query_params, context: self)
|
|
546
|
+
# Preloaded here rather than at each `.to_a`, because every card query
|
|
547
|
+
# on the board derives from this relation — the column body, the
|
|
548
|
+
# lazy-loaded frames, the post-move re-render. The sites that `pluck`
|
|
549
|
+
# or `count` off it instantiate no records, so the preload does not
|
|
550
|
+
# run for them.
|
|
551
|
+
auto_eager_load(collection, kanban_card_field_names)
|
|
523
552
|
end
|
|
524
553
|
end
|
|
525
554
|
|
|
555
|
+
# The fields a card actually renders. A board's `card_fields` is a slot
|
|
556
|
+
# layout — each slot holds one field name, several, or `false` to hide the
|
|
557
|
+
# slot — so it is flattened to names. A board that declares no layout falls
|
|
558
|
+
# back to the read attribute set, the same one the column component is
|
|
559
|
+
# handed as `resource_fields`.
|
|
560
|
+
def kanban_card_field_names
|
|
561
|
+
slots = current_kanban_board.card_fields
|
|
562
|
+
slots ? slots.values.flatten.grep(Symbol) : permitted_attributes_for("index")
|
|
563
|
+
end
|
|
564
|
+
|
|
526
565
|
# Intercepts the index action when view=kanban + column= is present.
|
|
527
566
|
# Renders only the turbo-frame body for the requested column and halts.
|
|
528
567
|
def maybe_render_kanban_column
|
|
@@ -535,9 +574,9 @@ module Plutonium
|
|
|
535
574
|
board = current_kanban_board
|
|
536
575
|
|
|
537
576
|
# Resolve only the requested column rather than grouping the whole
|
|
538
|
-
# board:
|
|
539
|
-
# each) on every lazy frame request. We compare keys as
|
|
540
|
-
# avoid interning arbitrary request input into symbols.
|
|
577
|
+
# board: grouping every column would scope+count+limit all of them
|
|
578
|
+
# (~2 queries each) on every lazy frame request. We compare keys as
|
|
579
|
+
# strings to avoid interning arbitrary request input into symbols.
|
|
541
580
|
columns = Plutonium::Kanban::Grouping.resolve_columns(board, kanban_context)
|
|
542
581
|
column = columns.find { |c| c.key.to_s == params[:column] }
|
|
543
582
|
|
|
@@ -578,7 +617,7 @@ module Plutonium
|
|
|
578
617
|
return "".html_safe unless column
|
|
579
618
|
|
|
580
619
|
scoped = Plutonium::Kanban::Grouping.apply_scope(kanban_base_relation, column.scope)
|
|
581
|
-
ordered = board.
|
|
620
|
+
ordered = board.position_config_for(current_definition).order(scoped)
|
|
582
621
|
|
|
583
622
|
if board.per_column
|
|
584
623
|
total = ordered.count
|
|
@@ -729,7 +768,7 @@ module Plutonium
|
|
|
729
768
|
case on.to_sym
|
|
730
769
|
when :visible
|
|
731
770
|
board = current_kanban_board
|
|
732
|
-
ordered = board.
|
|
771
|
+
ordered = board.position_config_for(current_definition).order(scoped)
|
|
733
772
|
limited = board.per_column ? ordered.limit(board.per_column) : ordered
|
|
734
773
|
limited.pluck(resource_class.primary_key)
|
|
735
774
|
else # :all and any unknown value
|
|
@@ -780,8 +819,8 @@ module Plutonium
|
|
|
780
819
|
# nil), via the board's position_config — the same path a drag-drop uses.
|
|
781
820
|
board = current_kanban_board
|
|
782
821
|
dest_scoped = Plutonium::Kanban::Grouping.apply_scope(kanban_base_relation, column.scope)
|
|
783
|
-
dest_cards = board.
|
|
784
|
-
board.
|
|
822
|
+
dest_cards = board.position_config_for(current_definition).order(dest_scoped).where.not(id: record.id).to_a
|
|
823
|
+
board.position_config_for(current_definition).reposition!(
|
|
785
824
|
record:,
|
|
786
825
|
column: column.key,
|
|
787
826
|
prev_record: dest_cards.last,
|
|
@@ -795,7 +834,7 @@ module Plutonium
|
|
|
795
834
|
# unchanged, allowing the Stimulus drag controller to snap the card back.
|
|
796
835
|
#
|
|
797
836
|
# When a reason is given, a single dismissable toast is appended to the
|
|
798
|
-
#
|
|
837
|
+
# page's shared toast region so the snap-back is explained rather than
|
|
799
838
|
# silent. It renders the shared _toast partial directly (not via flash)
|
|
800
839
|
# so a stale, undisplayed flash from an earlier request can't leak into
|
|
801
840
|
# the turbo_stream response — these move POSTs never render the layout
|
|
@@ -810,7 +849,7 @@ module Plutonium
|
|
|
810
849
|
|
|
811
850
|
if reason
|
|
812
851
|
streams << turbo_stream.append(
|
|
813
|
-
|
|
852
|
+
Plutonium::FLASH_REGION,
|
|
814
853
|
partial: "plutonium/toast",
|
|
815
854
|
locals: {type: :warning, msg: reason}
|
|
816
855
|
)
|