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
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Native drag-and-drop for positioned resources
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-31
|
|
4
|
+
**Status:** Design approved, pending implementation plan
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
`Plutonium::Positioning` (`lib/plutonium/positioning.rb`) is already standalone and
|
|
9
|
+
kanban-independent. A model calls `positioned_on :position, scope: :project_id` and gets
|
|
10
|
+
automatic position assignment on create, `reposition!(prev_record:, next_record:)` with
|
|
11
|
+
fractional midpoints and gap rebalancing, and a `t.position` migration helper.
|
|
12
|
+
|
|
13
|
+
Nothing consumes it except kanban. A resource whose model is positioned still renders an
|
|
14
|
+
index table with no way to reorder it. The drag-and-drop machinery that would do so exists,
|
|
15
|
+
but is welded into `src/js/controllers/kanban_controller.js` (717 lines) and
|
|
16
|
+
`Plutonium::Resource::Controllers::KanbanActions`.
|
|
17
|
+
|
|
18
|
+
This design exposes reordering on ordinary collection surfaces — index tables, nested
|
|
19
|
+
association tables, and grid/card views — and extracts the drag mechanics so kanban and the
|
|
20
|
+
new surfaces share one implementation.
|
|
21
|
+
|
|
22
|
+
## Goals
|
|
23
|
+
|
|
24
|
+
- A resource opts in with one line in its definition and gets drag-to-reorder on every
|
|
25
|
+
collection surface.
|
|
26
|
+
- The reposition endpoint is a first-class *hidden action*: real route, real policy
|
|
27
|
+
predicate, rendered nowhere.
|
|
28
|
+
- One drag implementation, shared with kanban.
|
|
29
|
+
- One positioning strategy machine, shared with kanban — including the block escape hatch
|
|
30
|
+
that lets `acts_as_list`, `positioning`, `ranked-model`, or anything else own the write.
|
|
31
|
+
- No regression to kanban's behaviour.
|
|
32
|
+
|
|
33
|
+
## Non-goals
|
|
34
|
+
|
|
35
|
+
- Cross-page dragging. Reordering is within-page only.
|
|
36
|
+
- Realtime broadcast of reorders. Kanban has it; tables do not, and the design does not
|
|
37
|
+
preclude adding it later.
|
|
38
|
+
- Touch drag. Native HTML5 DnD does not fire on touch devices. This limitation is inherited
|
|
39
|
+
from the existing kanban implementation and is not addressed here.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. Hidden actions
|
|
44
|
+
|
|
45
|
+
### Current state
|
|
46
|
+
|
|
47
|
+
`Action::Base` carries a one-off `@kanban_drop` flag (`lib/plutonium/action/base.rb:26`,
|
|
48
|
+
`:84`, `:145`) meaning "this action exists for its route, policy predicate, and params
|
|
49
|
+
machinery, but must never render." Its only producer is
|
|
50
|
+
`lib/plutonium/definition/index_views.rb:148`, which auto-registers a column's
|
|
51
|
+
`enter_interaction`.
|
|
52
|
+
|
|
53
|
+
Four render sites filter on it:
|
|
54
|
+
|
|
55
|
+
| Site | Selects |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `lib/plutonium/ui/page/index.rb:36` | resource actions |
|
|
58
|
+
| `lib/plutonium/ui/page/show.rb:18` | record actions |
|
|
59
|
+
| `lib/plutonium/ui/table/resource.rb:163` | row actions |
|
|
60
|
+
| `lib/plutonium/ui/grid/card.rb:303` | card actions |
|
|
61
|
+
|
|
62
|
+
Two further sites do **not** filter — both bulk-action selectors:
|
|
63
|
+
|
|
64
|
+
| Site | Selects |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `lib/plutonium/ui/grid/resource.rb:95` | `.select { \|k, a\| a.bulk_action? }` |
|
|
67
|
+
| `lib/plutonium/ui/table/resource.rb:189` | `.select { \|k, a\| a.bulk_action? && a.condition_met?(view_context) }` |
|
|
68
|
+
|
|
69
|
+
Today that gap is plugged by an `ArgumentError` in `Plutonium::Kanban::Column`
|
|
70
|
+
(`lib/plutonium/kanban/column.rb:38`) which rejects collection-shaped `enter_interaction`s
|
|
71
|
+
specifically so they cannot be classified as bulk actions and leak into the bulk bar.
|
|
72
|
+
|
|
73
|
+
### Change
|
|
74
|
+
|
|
75
|
+
Rename the concept to `hidden`, and expose it on the action DSL:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
action :reposition, hidden: true
|
|
79
|
+
action :ping_webhook, interaction: PingWebhook, hidden: true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A hidden action gets its route, its `name?` policy predicate, and — if interactive — its
|
|
83
|
+
form and param extraction. It renders in no toolbar, row dropdown, card, or bulk bar.
|
|
84
|
+
|
|
85
|
+
- `Action::Base#kanban_drop?` becomes `#hidden?`; `kanban_drop:` is removed from
|
|
86
|
+
`initialize` and `to_options`. It is internal, so there is no deprecation path.
|
|
87
|
+
- All six sites above filter on `!a.hidden?`, including both bulk-action selectors. The
|
|
88
|
+
`ArgumentError` in `Kanban::Column` stays as a defence-in-depth check with its comment
|
|
89
|
+
updated — it is no longer the only thing preventing the leak.
|
|
90
|
+
- `index_views.rb:148` passes `hidden: true`.
|
|
91
|
+
|
|
92
|
+
`hidden?` is a **display** gate, not an authorization boundary — the same contract as
|
|
93
|
+
`condition:` (`lib/plutonium/action/base.rb:98`). Authorization stays in the policy.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2. Shared positioning strategy
|
|
98
|
+
|
|
99
|
+
`Plutonium::Kanban::Positioning::Config` (`lib/plutonium/kanban/positioning.rb`) already
|
|
100
|
+
implements exactly the strategy machine tables need — three modes, built by kanban's
|
|
101
|
+
`position_on` DSL (`lib/plutonium/kanban/dsl.rb:46`):
|
|
102
|
+
|
|
103
|
+
| Mode | Built by | Ordering | Write |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| A `:delegate` | `position_on` / `position_on :attr` | `reorder(attr)` | `record.reposition!(prev_record:, next_record:)` |
|
|
106
|
+
| B `:block` | `position_on(:attr) { \|move\| … }` | `reorder(attr)` | the block, given a `Move` |
|
|
107
|
+
| C `:disabled` | `position_on false` | unchanged | no-op |
|
|
108
|
+
|
|
109
|
+
Mode B is the escape hatch for other positioning gems. The framework orders by the declared
|
|
110
|
+
attribute and delegates the write entirely:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
position_on :position do |move|
|
|
114
|
+
move.record.insert_at(move.index + 1) # acts_as_list
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It is promoted out of the kanban namespace to `Plutonium::Positioning::Config`, alongside
|
|
119
|
+
`Plutonium::Positioning::Move`. Both the kanban block and the definition build the same
|
|
120
|
+
Config. `Move#column` is `nil` outside a board; every other field is identical.
|
|
121
|
+
`Plutonium::Kanban::Positioning` becomes a thin alias so kanban's existing `position_on`
|
|
122
|
+
surface is unchanged.
|
|
123
|
+
|
|
124
|
+
`column` is defaulted on `Config#reposition!`, not on `Move` itself. `Move` has exactly one
|
|
125
|
+
construction site — inside `reposition!` — so defaulting the member rather than the parameter
|
|
126
|
+
would mean every caller passing an explicit `column: nil` to satisfy a required keyword,
|
|
127
|
+
which is the opposite of what the default is for.
|
|
128
|
+
|
|
129
|
+
This mirrors the JS drag-core extraction (§5.1) — same reasoning, same shape: one machine,
|
|
130
|
+
two consumers.
|
|
131
|
+
|
|
132
|
+
### 2.0 Namespace layout
|
|
133
|
+
|
|
134
|
+
`Plutonium::Positioning` becomes a **pure namespace**; the ActiveRecord concern moves to
|
|
135
|
+
`Plutonium::Positioning::Model`.
|
|
136
|
+
|
|
137
|
+
| Constant | Kind | Notes |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `Plutonium::Positioning` | namespace | `EPSILON`, `.position_between`, `.gap_exhausted?` |
|
|
140
|
+
| `Plutonium::Positioning::Model` | AR concern | `positioned_on`, `reposition!`, `backfill_positions!` |
|
|
141
|
+
| `Plutonium::Positioning::Config` | strategy | the three modes |
|
|
142
|
+
| `Plutonium::Positioning::Move` | value object | Mode B block argument |
|
|
143
|
+
| `Plutonium::Positioning::MigrationHelpers` | AR table-definition mixin | `t.position` |
|
|
144
|
+
|
|
145
|
+
This is a **breaking change**: `include Plutonium::Positioning` becomes
|
|
146
|
+
`include Plutonium::Positioning::Model`. The gem is pre-1.0 and the update is mechanical
|
|
147
|
+
(two dummy models, two test files, four doc locations).
|
|
148
|
+
|
|
149
|
+
The reason is constant leakage. A concern that users `include` into their models puts every
|
|
150
|
+
constant nested inside it into those models' constant lookup — so with `Config` directly
|
|
151
|
+
under the concern, a bare `Config` written inside such a model resolves to Plutonium's and
|
|
152
|
+
silently shadows the application's own:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class Task < ApplicationRecord
|
|
156
|
+
include Plutonium::Positioning # the old shape
|
|
157
|
+
def self.thing = Config # => Plutonium::Positioning::Config, not ::Config
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`Config` and `Move` are both names an application is likely to own. Making the namespace pure
|
|
162
|
+
means nothing Plutonium-owned enters a user's ancestor chain at all, which is why this is
|
|
163
|
+
preferred over the cheaper fix of renaming the constants to something less collision-prone.
|
|
164
|
+
|
|
165
|
+
`MigrationHelpers` stays on the namespace: the railtie mixes it into ActiveRecord's
|
|
166
|
+
table-definition classes (`lib/plutonium/railtie.rb:77-78`), never into user models, so it
|
|
167
|
+
leaks nowhere.
|
|
168
|
+
|
|
169
|
+
## 2.1 `position_on` — the definition DSL
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class TaskDefinition < Plutonium::Resource::Definition
|
|
173
|
+
position_on
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class Task < ApplicationRecord
|
|
177
|
+
include Plutonium::Positioning::Model
|
|
178
|
+
positioned_on :position, scope: :project_id
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Deliberately the **same verb as kanban's**, not a new one. The signature is identical:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
position_on # Mode A, attribute :position
|
|
186
|
+
position_on :sort_order # Mode A, custom attribute
|
|
187
|
+
position_on(:rank) { |move| … } # Mode B, another gem owns the write
|
|
188
|
+
position_on false # Mode C, ordering off
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Reusing the verb keeps the framework at **two** positioning verbs rather than three:
|
|
192
|
+
`positioned_on` on the model (how positions are stored) and `position_on` everywhere in the
|
|
193
|
+
definition layer (what is orderable). A third name would have been one more thing to learn
|
|
194
|
+
and one more near-miss to typo.
|
|
195
|
+
|
|
196
|
+
`positioned_on` and `position_on` differ by two letters, which is a genuine footgun — but a
|
|
197
|
+
**pre-existing** one, since both already coexist today. Reusing `position_on` does not
|
|
198
|
+
worsen it; introducing a third verb would have.
|
|
199
|
+
|
|
200
|
+
In **Mode A** the model remains the single owner of *how* positions are stored — column and
|
|
201
|
+
scope. The definition restates neither; `positioned_on`'s `scope:` is never duplicated at the
|
|
202
|
+
definition layer. A boot-time error fires if the model does not `include Plutonium::Positioning::Model`.
|
|
203
|
+
|
|
204
|
+
In **Mode B** that check does not apply — the model need not include the concern at all, since
|
|
205
|
+
it never calls `reposition!`. The attribute is still required, because ordering and the
|
|
206
|
+
drag-enabled check (§4) key off it.
|
|
207
|
+
|
|
208
|
+
`position_on` expands to:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
sort <attribute>
|
|
212
|
+
default_sort <attribute>, :asc
|
|
213
|
+
action :reposition, hidden: true
|
|
214
|
+
# + drag affordances on the resource's collection surfaces
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Registering `sort <attribute>` is load-bearing, not cosmetic. Dragging is only permitted when
|
|
218
|
+
the collection is ordered by that attribute (§4); without a registered sort, `?sort=position`
|
|
219
|
+
is not a permitted sort and there is no route back out of the disabled state.
|
|
220
|
+
|
|
221
|
+
Each expansion is a normal declaration and can be overridden after the fact. A definition
|
|
222
|
+
that writes `default_sort :name` after `position_on` gets the disabled-grip state by default,
|
|
223
|
+
which is a legitimate choice.
|
|
224
|
+
|
|
225
|
+
Mode C registers nothing: no sort, no action, no grips. It exists so a resource can inherit a
|
|
226
|
+
positioned definition and switch ordering off.
|
|
227
|
+
|
|
228
|
+
### 2.2 Board inheritance
|
|
229
|
+
|
|
230
|
+
A kanban board inherits the definition's `position_on`; a `position_on` inside the
|
|
231
|
+
`kanban do…end` block overrides it for the board only.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
class TaskDefinition < Plutonium::Resource::Definition
|
|
235
|
+
position_on :position # table, grid, nested tables, AND the board
|
|
236
|
+
|
|
237
|
+
kanban do
|
|
238
|
+
columns :todo, :doing, :done
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This is what makes the shared verb honest. Without inheritance, `position_on` would mean two
|
|
244
|
+
different things depending on which scope it was written in — worse than having two different
|
|
245
|
+
names. With it, there is one concept, declared once, overridable where it needs to differ.
|
|
246
|
+
|
|
247
|
+
It mirrors `show_in`, which already works this way: `Board#show_in_for(definition)`
|
|
248
|
+
(`lib/plutonium/kanban/board.rb:37`) is `@show_in || definition.show_in`.
|
|
249
|
+
|
|
250
|
+
**Implementation note.** A board currently *always* has a config —
|
|
251
|
+
`Plutonium::Kanban::DSL` seeds `@position_config = Positioning::Config.default` in its
|
|
252
|
+
constructor (`lib/plutonium/kanban/dsl.rb:18`) — so "not declared" and "declared as the
|
|
253
|
+
default" are indistinguishable. That seed must become `nil`, with resolution moving to a new
|
|
254
|
+
`Board#position_config_for(definition)`:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
def position_config_for(definition)
|
|
258
|
+
@position_config || definition.defined_position_config || Positioning::Config.default
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Resolution must be **lazy**, not resolved at board-build time. `kanban` eagerly compiles the
|
|
263
|
+
board at class-load (`lib/plutonium/definition/index_views.rb:121`), so a board built before
|
|
264
|
+
a later `position_on` line would silently miss it — making the declaration order-dependent,
|
|
265
|
+
which is exactly the kind of footgun this feature should not ship with.
|
|
266
|
+
|
|
267
|
+
The five `board.position_config` call sites in `KanbanActions` (lines 136, 259, 581, 733, 784)
|
|
268
|
+
become `board.position_config_for(current_definition)`. This is kanban's third touchpoint in
|
|
269
|
+
this change, and the most mechanical of the three.
|
|
270
|
+
|
|
271
|
+
### Scope on nested resources
|
|
272
|
+
|
|
273
|
+
If `Comment` is `positioned_on :position` with no scope but `CommentDefinition` is nested
|
|
274
|
+
under `Post`, positions are global across all comments. Reordering within one post still
|
|
275
|
+
behaves correctly — a fractional insert between two visible neighbours lands in the right
|
|
276
|
+
place regardless. Only a rebalance renumbers more rows than the author expects.
|
|
277
|
+
|
|
278
|
+
This is a documented contract, not a runtime check. No boot warning, no derivation of scope
|
|
279
|
+
from the request. `scope:` is the model author's job, covered in the docs and the
|
|
280
|
+
`plutonium-resource` skill.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 3. Server — the reposition endpoint
|
|
285
|
+
|
|
286
|
+
`POST <member>/reposition`, params `{prev_id:, next_id:}` — the ids of the dropped row's
|
|
287
|
+
visible neighbours. Either is nullable for a drop at an end of the list.
|
|
288
|
+
|
|
289
|
+
New concern `Plutonium::Resource::Controllers::PositionActions`, routed from
|
|
290
|
+
`lib/plutonium/routing/mapper_extensions.rb` alongside the kanban routes (lines 154-155).
|
|
291
|
+
|
|
292
|
+
### Flow
|
|
293
|
+
|
|
294
|
+
1. `record = current_authorized_scope.find(params[:id])` — satisfies the scope verifier.
|
|
295
|
+
2. `authorize_current! record, to: :reposition?`. `reposition?` is added to
|
|
296
|
+
`Plutonium::Resource::Policy` defaulting to `update?`.
|
|
297
|
+
3. Resolve `prev_id` and `next_id` **within the same authorized scope**. A neighbour id that
|
|
298
|
+
does not resolve is treated as drift, not as `nil` — a `nil` would silently mean "drop at
|
|
299
|
+
the end", which is a different and wrong outcome.
|
|
300
|
+
4. `config.reposition!(record:, column: nil, prev_record:, next_record:, index:)` — the shared
|
|
301
|
+
Config from §2, so Mode A and Mode B are dispatched identically to how kanban does it. In
|
|
302
|
+
Mode A this reaches `record.reposition!`, which already handles the exhausted-gap rebalance
|
|
303
|
+
internally (`lib/plutonium/positioning.rb:88`).
|
|
304
|
+
5. Respond per §3.1.
|
|
305
|
+
|
|
306
|
+
### 3.1 Response
|
|
307
|
+
|
|
308
|
+
The client has already moved the row optimistically, so the common case needs no payload.
|
|
309
|
+
|
|
310
|
+
| Case | Response |
|
|
311
|
+
|---|---|
|
|
312
|
+
| Mode A, clean drop, no rebalance, both neighbours resolved | `204 No Content` |
|
|
313
|
+
| Mode A, `reposition!` triggered a rebalance | `turbo_stream.update` the tbody |
|
|
314
|
+
| Mode B, any successful drop | `turbo_stream.update` the tbody |
|
|
315
|
+
| A neighbour id did not resolve in scope (drift) | `turbo_stream.update` the tbody |
|
|
316
|
+
| Policy denial | `403` + unchanged tbody + toast |
|
|
317
|
+
| Validation failure / record gone | `422` + unchanged tbody + toast |
|
|
318
|
+
|
|
319
|
+
Mode B **always** reconciles. The block is an opaque write — the framework cannot know what it
|
|
320
|
+
did, and gems in this space routinely renumber the whole group on every move (`acts_as_list`
|
|
321
|
+
does). Optimistically returning `204` there would leave the client's view stale in the common
|
|
322
|
+
case rather than the rare one, so Mode B trades the empty-response optimisation for
|
|
323
|
+
correctness.
|
|
324
|
+
|
|
325
|
+
Rejections mirror `render_kanban_rejection` (`kanban_actions.rb:803`): re-render the
|
|
326
|
+
collection unchanged so the row snaps back, and append a toast explaining why.
|
|
327
|
+
|
|
328
|
+
### 3.2 Rebalance signal
|
|
329
|
+
|
|
330
|
+
The Mode A row above needs to know whether `reposition!` rebalanced. Today it returns the
|
|
331
|
+
result of `update!` and discards that fact (`lib/plutonium/positioning.rb:84-94`).
|
|
332
|
+
|
|
333
|
+
`reposition!` will return a small result value carrying `rebalanced?` rather than have the
|
|
334
|
+
controller re-derive it by comparing positions before and after — which would be both racy
|
|
335
|
+
and a duplication of the concern's own gap logic.
|
|
336
|
+
|
|
337
|
+
`Config#reposition!` surfaces this: `rebalanced?` in Mode A, and unconditionally `true` in
|
|
338
|
+
Mode B (per §3.1), so the controller has one thing to branch on and no mode-awareness of its
|
|
339
|
+
own.
|
|
340
|
+
|
|
341
|
+
### 3.3 Rescues
|
|
342
|
+
|
|
343
|
+
Mirror the kanban handlers (`kanban_actions.rb:331-372`), which encode several non-obvious
|
|
344
|
+
lessons worth preserving:
|
|
345
|
+
|
|
346
|
+
- `rescue ::ActionPolicy::Unauthorized` — the leading `::` is required. `Plutonium::ActionPolicy`
|
|
347
|
+
exists, so a bare constant resolves to that namespace and never matches.
|
|
348
|
+
- `ActiveRecord::RecordNotFound` — the row was destroyed between render and drop.
|
|
349
|
+
- `ActiveRecord::RecordInvalid` — a model callback left the record invalid.
|
|
350
|
+
|
|
351
|
+
Each streams a snap-back rather than letting an HTML error page get morphed into the table.
|
|
352
|
+
Where a rescue fires before `authorize_current!` has bumped its counter, call
|
|
353
|
+
`skip_verify_authorize_current!`.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 4. When dragging is permitted
|
|
358
|
+
|
|
359
|
+
Dragging is enabled only when the collection's effective ordering is the attribute declared
|
|
360
|
+
on `position_on` (§2.1) — `:position` by default, whatever the author named otherwise.
|
|
361
|
+
|
|
362
|
+
| State | Behaviour |
|
|
363
|
+
|---|---|
|
|
364
|
+
| Ordered by the declared attribute | Grips live |
|
|
365
|
+
| Sorted by any other column | Grips disabled |
|
|
366
|
+
| Search / filter / scope active, ordered by the attribute | Grips live |
|
|
367
|
+
| Page 2+ | Grips live, within-page only |
|
|
368
|
+
| Mode C (`position_on false`) | No grips at all |
|
|
369
|
+
|
|
370
|
+
Filters stay draggable. Dropping between two visible neighbours is well-defined even when
|
|
371
|
+
rows are hidden between them — the record lands somewhere among the hidden rows, exactly as
|
|
372
|
+
kanban behaves on a filtered board.
|
|
373
|
+
|
|
374
|
+
A foreign sort is different in kind, not degree: under a name sort the visible neighbours are
|
|
375
|
+
in arbitrary position order, so `prev` may hold a *higher* position than `next` and
|
|
376
|
+
`position_between` would produce a value that is not between them. Disabling is a
|
|
377
|
+
correctness requirement, not a UX preference.
|
|
378
|
+
|
|
379
|
+
### Escaping the disabled state
|
|
380
|
+
|
|
381
|
+
A disabled grip is a `<button>` that applies the position sort when clicked, labelled
|
|
382
|
+
"Sort by position to reorder". The disabled affordance is itself the way to re-enable it.
|
|
383
|
+
|
|
384
|
+
This is why `position_on` registers `sort <attribute>`. It also sidesteps a real problem: the
|
|
385
|
+
position column is a decimal nobody wants rendered, and
|
|
386
|
+
`lib/plutonium/ui/table/resource.rb:154` only produces a sort control
|
|
387
|
+
(`current_query_object.sort_params_for(name)`) for *displayed* columns. There is no header to
|
|
388
|
+
click, so the grip carries the control instead.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 5. Client
|
|
393
|
+
|
|
394
|
+
### 5.1 Shared core
|
|
395
|
+
|
|
396
|
+
Extract the pure drag mechanics from `kanban_controller.js` into `src/js/drag/sortable.js`:
|
|
397
|
+
dragstart/dragover/drop wiring, drop-indicator placement, insertion-index computation, and
|
|
398
|
+
snap-back. It knows nothing about columns, WIP limits, or boards.
|
|
399
|
+
|
|
400
|
+
Consumers:
|
|
401
|
+
|
|
402
|
+
- `src/js/controllers/positioned_controller.js` (new) — tables, nested tables, grids.
|
|
403
|
+
Registered in `src/js/controllers/register_controllers.js`.
|
|
404
|
+
- `src/js/controllers/kanban_controller.js` (migrated) — retains everything board-specific:
|
|
405
|
+
columns, collapse, lazy frames, WIP, drop interactions, realtime broadcast.
|
|
406
|
+
|
|
407
|
+
The extraction is strictly behaviour-preserving. No drag semantics change while code moves.
|
|
408
|
+
Kanban's existing suite is the regression gate.
|
|
409
|
+
|
|
410
|
+
### 5.2 The affordance
|
|
411
|
+
|
|
412
|
+
The grip lives inside the row's **first cell**, in its left padding. There is no dedicated
|
|
413
|
+
grip column.
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
┌──────┬───────────────┬─────────┐
|
|
417
|
+
│ ☐ │ Name │ Status │
|
|
418
|
+
├──────┼───────────────┼─────────┤
|
|
419
|
+
│ ☐ │ Ship v2 │ Open │ idle — grip hidden
|
|
420
|
+
│ ☐ │☷ Fix login │ Open │ hovered — grip revealed
|
|
421
|
+
│ ☐ │ Write docs │ Done │ idle
|
|
422
|
+
└──────┴───────────────┴─────────┘
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
- Revealed on row hover; always visible on keyboard focus.
|
|
426
|
+
- **Only the grip carries `draggable="true"`**, never the `<tr>`.
|
|
427
|
+
|
|
428
|
+
That last point is the whole reason for this shape. `draggable="true"` on an element
|
|
429
|
+
disables text selection within it in every major browser — on a kanban card that costs
|
|
430
|
+
nothing, but on a data table it would silently remove the ability to select and copy a cell
|
|
431
|
+
value. A draggable row would also fight `row_click_controller.js`
|
|
432
|
+
(`src/js/controllers/row_click_controller.js:16`), which already makes the entire row the
|
|
433
|
+
Show affordance: a drag that starts and drops in place fires a click and navigates away.
|
|
434
|
+
|
|
435
|
+
Kanban keeps whole-card dragging. The inconsistency is deliberate — neither concern applies
|
|
436
|
+
to a card — and should be stated in the docs rather than smoothed over.
|
|
437
|
+
|
|
438
|
+
### 5.3 Keyboard support
|
|
439
|
+
|
|
440
|
+
The grip is a real `<button>`, so it is focusable and reachable by tab. When focused,
|
|
441
|
+
`ArrowUp` / `ArrowDown` move the row one slot, posting to the same reposition endpoint with
|
|
442
|
+
the appropriate neighbours.
|
|
443
|
+
|
|
444
|
+
Native HTML5 DnD is mouse-only, so without this the feature is unusable by keyboard. The
|
|
445
|
+
cost is small because the endpoint and neighbour computation already exist.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## 6. Surfaces
|
|
450
|
+
|
|
451
|
+
| Surface | Affordance | Notes |
|
|
452
|
+
|---|---|---|
|
|
453
|
+
| Index table | Hover grip in first cell | Grip doubles as sort-by-position when disabled |
|
|
454
|
+
| Nested / association table | Same | Neighbours resolve within the parent-scoped `current_authorized_scope` |
|
|
455
|
+
| Grid / card index | Hover grip on the card | `lib/plutonium/ui/grid/card.rb` |
|
|
456
|
+
| Kanban board | Unchanged — whole card | Migrated onto the shared core, behaviour identical |
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## 7. Testing
|
|
461
|
+
|
|
462
|
+
- `test/plutonium/positioning_test.rb` — extend for the `rebalanced?` result value (§3.2).
|
|
463
|
+
- `test/plutonium/kanban/positioning_test.rb` — must pass unchanged against the promoted
|
|
464
|
+
`Plutonium::Positioning::Config` (§2), proving the move is behaviour-preserving. New cases
|
|
465
|
+
for `Move#column` being nil off-board, and for `position_on` building each of the three modes.
|
|
466
|
+
- Mode B integration: a dummy-app resource backed by a non-Plutonium positioning strategy,
|
|
467
|
+
asserting the block receives a correct `Move` and that the response always reconciles (§3.1).
|
|
468
|
+
- New hidden-action unit tests: route and policy predicate live; absent from all six render
|
|
469
|
+
sites, explicitly including both bulk-action selectors
|
|
470
|
+
(`lib/plutonium/ui/grid/resource.rb:95`, `lib/plutonium/ui/table/resource.rb:189`), which
|
|
471
|
+
are the gap this change closes.
|
|
472
|
+
- New `reposition` controller tests: authorization, neighbour-outside-scope drift, rebalance
|
|
473
|
+
reconciliation, each rejection path's snap-back.
|
|
474
|
+
- Kanban's existing suite is the regression gate for the JS extraction — it must pass
|
|
475
|
+
unchanged.
|
|
476
|
+
- System test driving a real drag in `test/dummy`, plus a keyboard reorder.
|
|
477
|
+
|
|
478
|
+
## Risks
|
|
479
|
+
|
|
480
|
+
**Kanban is touched three times** — the `Positioning::Config` promotion (§2), the lazy
|
|
481
|
+
`position_config_for` resolution (§2.2), and the JS drag-core extraction (§5.1). All three
|
|
482
|
+
reach into a large, working, heavily-tested feature. They are independent of one another,
|
|
483
|
+
which is the mitigation: each is behaviour-preserving, each has kanban's existing suite as
|
|
484
|
+
its gate, and any can be abandoned without the others.
|
|
485
|
+
|
|
486
|
+
They are not equally risky. The Config promotion is a namespace move behind an alias, and
|
|
487
|
+
`position_config_for` is a mechanical change across five call sites — both are low. The JS
|
|
488
|
+
extraction is the outlier, because it touches drag behaviour itself. If it proves invasive,
|
|
489
|
+
shipping `positioned_controller.js` standalone and migrating kanban separately remains a
|
|
490
|
+
valid fallback, at the cost of two drag implementations that will drift.
|
|
491
|
+
|
|
492
|
+
**`default_sort` override is implicit.** `position_on` silently changes a resource's default
|
|
493
|
+
ordering from `:id, :desc` (`lib/plutonium/definition/sorting.rb:25`) to `:position, :asc`.
|
|
494
|
+
This is the correct default, but it is action at a distance and must be documented
|
|
495
|
+
prominently.
|
|
496
|
+
|
|
497
|
+
## Documentation
|
|
498
|
+
|
|
499
|
+
- `docs/reference/` — a positioning page covering `positioned_on`, `position_on`, the model/
|
|
500
|
+
definition split, the three modes, and the nested-scope contract from §2.1.
|
|
501
|
+
- A worked Mode B example for at least one third-party gem (`acts_as_list` is the most
|
|
502
|
+
common), since "can I use my existing positioning gem" is the first question this feature
|
|
503
|
+
will attract.
|
|
504
|
+
- Hidden actions in the actions reference.
|
|
505
|
+
- Update the `plutonium-resource` and `plutonium-ui` skills.
|
|
506
|
+
- Note the deliberate table-vs-kanban affordance difference (§5.2).
|