plutonium 0.65.0 → 0.66.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 +43 -43
- data/.claude/skills/plutonium-app/SKILL.md +101 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +11 -4
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +144 -133
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +151 -96
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +21 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/blog/introducing-plutonium-dashboards.md +4 -5
- data/docs/blog/introducing-plutonium-i18n.md +4 -5
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +1 -1
- data/docs/guides/index.md +3 -3
- data/docs/guides/kanban.md +55 -55
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/reference/app/generators.md +22 -22
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +75 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +6 -6
- data/docs/reference/dashboard/dsl.md +2 -2
- data/docs/reference/dashboard/index.md +1 -1
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +23 -0
- data/docs/reference/index.md +1 -1
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +32 -32
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +17 -15
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +73 -73
- data/docs/reference/resource/export.md +6 -6
- data/docs/reference/resource/index.md +16 -16
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/resource/positioning.md +78 -76
- data/docs/reference/resource/query.md +13 -13
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +57 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +14 -14
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +44 -44
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +10 -10
- metadata +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Definition
|
|
2
2
|
|
|
3
|
-
Definitions configure **how** a resource is rendered and interacted with
|
|
3
|
+
Definitions configure **how** a resource is rendered and interacted with: which fields appear, how they render, what page chrome looks like. Auto-detection from the model handles the defaults; declare only what you're overriding.
|
|
4
4
|
|
|
5
5
|
For search/filters/scopes/sorting see [Query](./query). For custom actions see [Actions](./actions).
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ For search/filters/scopes/sorting see [Query](./query). For custom actions see [
|
|
|
10
10
|
- **Use `condition:` for UI state, the policy for authorization.** `condition: -> { object.published? }` is fine. "Only admins see this field" belongs in `permitted_attributes_for_*`.
|
|
11
11
|
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (see [Behavior › Policy](/reference/behavior/policies)).
|
|
12
12
|
- **`has_cents` fields use the virtual name** (`field :price`), never `:price_cents`.
|
|
13
|
-
- **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to
|
|
13
|
+
- **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to`**, without `inverse_of:`, validation fails with "Parent must exist" because the parent isn't saved yet.
|
|
14
14
|
|
|
15
15
|
## File location
|
|
16
16
|
|
|
@@ -102,7 +102,7 @@ end
|
|
|
102
102
|
| Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
103
103
|
| Rich text | `:markdown` (EasyMDE editor) |
|
|
104
104
|
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
105
|
-
| Boolean | `:toggle` / `:switch` (switch
|
|
105
|
+
| Boolean | `:toggle` / `:switch` (switch: **default** for boolean columns), `:boolean` (plain checkbox) |
|
|
106
106
|
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
107
|
| Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
|
|
108
108
|
| Files | `:file`, `:uppy`, `:attachment` |
|
|
@@ -115,7 +115,7 @@ end
|
|
|
115
115
|
|
|
116
116
|
#### Auto-inferred display formatting
|
|
117
117
|
|
|
118
|
-
These render automatically
|
|
118
|
+
These render automatically; declare an `as:` only to override or pass options:
|
|
119
119
|
|
|
120
120
|
| Column | Renders as | Notes |
|
|
121
121
|
|---|---|---|
|
|
@@ -132,11 +132,11 @@ display :active, as: :boolean, true_label: "Live", false_label: "Off"
|
|
|
132
132
|
**Currency symbol.** The `unit:` can be set on the model's `has_cents` declaration
|
|
133
133
|
(`has_cents :price_cents, unit: "£"`, or `unit: :currency_symbol` to read a method
|
|
134
134
|
off the record for per-row currencies). That model-level unit is used everywhere the
|
|
135
|
-
value renders as currency
|
|
135
|
+
value renders as currency: the show page, tables, **and grid/kanban cards**. A
|
|
136
136
|
per-display `unit:` overrides it for that one display; `unit: false` explicitly
|
|
137
137
|
renders no symbol. When neither is set, currency falls back to
|
|
138
138
|
`Plutonium.configuration.default_currency_unit` (default: the i18n
|
|
139
|
-
`number.currency.format.unit` if the locale defines it
|
|
139
|
+
`number.currency.format.unit` if the locale defines it, `$` in `en`, else no symbol).
|
|
140
140
|
|
|
141
141
|
## Field options
|
|
142
142
|
|
|
@@ -206,22 +206,22 @@ Keep a `field` condition to context that exists on every surface (`Rails.env`, a
|
|
|
206
206
|
feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
|
|
207
207
|
|
|
208
208
|
::: warning UI state, not authorization
|
|
209
|
-
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_
|
|
209
|
+
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*`; see [Behavior › Policy](/reference/behavior/policies).
|
|
210
210
|
:::
|
|
211
211
|
|
|
212
212
|
## Options that vary per render
|
|
213
213
|
|
|
214
|
-
Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL
|
|
214
|
+
Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL: `field`, `input`, `section`/`ungrouped`, `structured_input` and nested inputs. Arity says **whether you want the form**:
|
|
215
215
|
|
|
216
216
|
```ruby
|
|
217
217
|
input :tier, as: :select, choices: ->(form) { form.object.account.available_tiers }
|
|
218
218
|
input :notes, placeholder: -> { "Updated #{Time.current.year}" }
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
- **`-> { … }`** is called as-is, keeping whatever it closed over
|
|
221
|
+
- **`-> { … }`** is called as-is, keeping whatever it closed over: it means what it reads like where you wrote it. Nothing rebinds `self`. That is what lets an option declared inside an interaction's `customize_inputs` reach the interaction, private helpers included: `choices: -> { reviewer_choices }`.
|
|
222
222
|
- **`->(form) { … }`** is handed the form, so `object` (the record being edited), `params` and view helpers are reachable. Use it whenever the value depends on what is being rendered.
|
|
223
223
|
|
|
224
|
-
The rule holds on wizard steps too
|
|
224
|
+
The rule holds on wizard steps too, but there a zero-argument proc closes over an internal field recorder, so options must take the form and read the run off it: `->(form) { form.wizard.anchor.tiers }`. See [Wizard DSL › Runtime input options](/reference/wizard/dsl#runtime-input-options).
|
|
225
225
|
|
|
226
226
|
### `condition:` is not an option
|
|
227
227
|
|
|
@@ -229,10 +229,10 @@ The rule holds on wizard steps too — but there a zero-argument proc closes ove
|
|
|
229
229
|
|
|
230
230
|
| | asks | so it | receiver |
|
|
231
231
|
|---|---|---|---|
|
|
232
|
-
| an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render
|
|
233
|
-
| **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition
|
|
232
|
+
| an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render, so it means what it reads like where you wrote it, and takes `form` when it does care | its own closure, or the form |
|
|
233
|
+
| **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition, there is no useful reading of it that ignores that context | always the thing doing the rendering |
|
|
234
234
|
|
|
235
|
-
So `condition:` always runs **against** its context and reads it with no argument
|
|
235
|
+
So `condition:` always runs **against** its context and reads it with no argument, and "its context" is whatever is rendering: the form for a field, section or nested input; the component for a `column` or `display`; the **wizard** for a step's `condition:` (evaluated in the runner to decide which steps exist, before any form is built); a condition context for an action or scope.
|
|
236
236
|
|
|
237
237
|
```ruby
|
|
238
238
|
input :notes, condition: -> { object.published? } # form
|
|
@@ -253,7 +253,7 @@ class QuestionDefinition < ResourceDefinition
|
|
|
253
253
|
choices: %w[text choice scale],
|
|
254
254
|
pre_submit: true
|
|
255
255
|
|
|
256
|
-
# Dependents
|
|
256
|
+
# Dependents: no `as:` needed when the model column type matches
|
|
257
257
|
input :max_length, condition: -> { object.question_type == "text" }
|
|
258
258
|
input :choices, condition: -> { object.question_type == "choice" }
|
|
259
259
|
input :min_value, condition: -> { object.question_type == "scale" }
|
|
@@ -341,7 +341,7 @@ See [UI › Components](/reference/ui/components) for writing reusable Phlex com
|
|
|
341
341
|
|
|
342
342
|
### Custom component class
|
|
343
343
|
|
|
344
|
-
`as:` takes a **field component
|
|
344
|
+
`as:` takes a **field component**: Plutonium constructs it as
|
|
345
345
|
`YourComponent.new(field, **attributes)`, so it subclasses
|
|
346
346
|
`Phlexi::Form::Components::Base` (inputs) or `Phlexi::Display::Components::Base`
|
|
347
347
|
(displays) and reads the value off `field`:
|
|
@@ -352,7 +352,7 @@ display :chart, as: ChartComponent
|
|
|
352
352
|
```
|
|
353
353
|
|
|
354
354
|
A component with its own constructor (e.g. `PostCardComponent.new(post:)`) is not
|
|
355
|
-
an `as:` candidate
|
|
355
|
+
an `as:` candidate; it would raise `ArgumentError`. Build it in a block instead:
|
|
356
356
|
|
|
357
357
|
```ruby
|
|
358
358
|
display :card do |field|
|
|
@@ -453,7 +453,7 @@ end
|
|
|
453
453
|
|---|---|
|
|
454
454
|
| `limit` | Max records (auto-detected from model; default 10) |
|
|
455
455
|
| `allow_destroy` | Show delete checkbox (auto-detected) |
|
|
456
|
-
| `update_only` | Hide "Add" button
|
|
456
|
+
| `update_only` | Hide "Add" button: only edit existing |
|
|
457
457
|
| `description` | Help text above the section |
|
|
458
458
|
| `condition` | Proc to show/hide |
|
|
459
459
|
| `using` | Another Definition class |
|
|
@@ -468,13 +468,13 @@ end
|
|
|
468
468
|
end
|
|
469
469
|
```
|
|
470
470
|
- **Don't put `*_attributes` hashes in the policy.** Plutonium extracts nested params from the form definition, not the policy. The policy permits just the association name (`:variants`); `nested_input :variants` handles the rest. Adding `{variants_attributes: [...]}` to `permitted_attributes_for_create` renders as a literal text input. See [Behavior › Policy](/reference/behavior/policies).
|
|
471
|
-
- **`update_only: true` hides the Add button
|
|
472
|
-
- **Custom class names
|
|
471
|
+
- **`update_only: true` hides the Add button**: for `has_one` and "settings"-style associations.
|
|
472
|
+
- **Custom class names**: use `class_name:` in the model AND `using:` in the definition.
|
|
473
473
|
|
|
474
474
|
## Structured inputs
|
|
475
475
|
|
|
476
476
|
Classless inline fieldsets backed by a JSON/jsonb column. No model associations
|
|
477
|
-
required
|
|
477
|
+
required: the whole sub-form is serialised into a single column as a hash
|
|
478
478
|
(single form) or an array of hashes (repeater).
|
|
479
479
|
|
|
480
480
|

|
|
@@ -513,16 +513,16 @@ end
|
|
|
513
513
|
### Removing rows
|
|
514
514
|
|
|
515
515
|
Each repeater row has a **Remove** button. Removing a row collapses it to a
|
|
516
|
-
compact
|
|
516
|
+
compact bar (a _Removed_ label and a **Restore** button) and disables its inputs, so the browser omits
|
|
517
517
|
them from the submission. The server simply rebuilds the JSON column from the
|
|
518
|
-
rows it receives
|
|
518
|
+
rows it receives; there is no `_destroy` marker. **Restore** brings the row
|
|
519
519
|
back before saving.
|
|
520
520
|
|
|
521
521
|

|
|
522
522
|
|
|
523
523
|
### Policy
|
|
524
524
|
|
|
525
|
-
Permit the column name as a plain symbol
|
|
525
|
+
Permit the column name as a plain symbol; Plutonium handles the nested hash
|
|
526
526
|
params automatically:
|
|
527
527
|
|
|
528
528
|
```ruby
|
|
@@ -544,13 +544,13 @@ attribute is declared automatically; `execute` receives the value as a `Hash`
|
|
|
544
544
|
The fields are classless render declarations, so there is nothing for Plutonium
|
|
545
545
|
to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
|
|
546
546
|
records run their own model validations). Whatever the form submits is stored
|
|
547
|
-
as-is, after blank rows are dropped
|
|
547
|
+
as-is, after blank rows are dropped: **no per-field server-side validation**.
|
|
548
548
|
:::
|
|
549
549
|
|
|
550
550
|
Specifically:
|
|
551
551
|
|
|
552
552
|
- **HTML constraints are client-side only.** A field's `required:` and a
|
|
553
|
-
select's `choices:` guide the browser but are **not** enforced on the server
|
|
553
|
+
select's `choices:` guide the browser but are **not** enforced on the server:
|
|
554
554
|
an API call or a crafted request can submit anything.
|
|
555
555
|
- **Selects silently drop unknown values.** If a stored value is not among a
|
|
556
556
|
`as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
|
|
@@ -560,7 +560,7 @@ Specifically:
|
|
|
560
560
|
`choices:` can drift. Keep `choices:` a stable superset, or use a free-text
|
|
561
561
|
input, when values can change over time.
|
|
562
562
|
|
|
563
|
-
To enforce anything, add the validation yourself
|
|
563
|
+
To enforce anything, add the validation yourself; it runs server-side:
|
|
564
564
|
|
|
565
565
|
```ruby
|
|
566
566
|
# resource: validate the JSON column on the model
|
|
@@ -607,7 +607,7 @@ end
|
|
|
607
607
|
|
|
608
608
|
The block is evaluated once and stored on the class. Re-declaring `form_layout` in a subclass replaces the parent layout as a unit; per-field `input` config inherits normally.
|
|
609
609
|
|
|
610
|
-
With no `form_layout` declared the form renders unchanged as a single responsive grid
|
|
610
|
+
With no `form_layout` declared the form renders unchanged as a single responsive grid, fully backwards-compatible.
|
|
611
611
|
|
|
612
612
|
### `section(key, *fields, **opts)`
|
|
613
613
|
|
|
@@ -615,16 +615,16 @@ Groups a set of fields under an optional heading.
|
|
|
615
615
|
|
|
616
616
|
| Argument | Description |
|
|
617
617
|
|---|---|
|
|
618
|
-
| `key` | Symbol. `:ungrouped` is reserved
|
|
618
|
+
| `key` | Symbol. `:ungrouped` is reserved: use the `ungrouped` macro instead (raises `ArgumentError` otherwise). |
|
|
619
619
|
| `*fields` | Ordered field keys to place in this section. |
|
|
620
620
|
| `label:` | Section heading. Defaults to `key.to_s.humanize` (e.g. `:shipping_address` → `"Shipping address"`). |
|
|
621
621
|
| `description:` | Optional help line rendered below the heading. |
|
|
622
622
|
| `collapsible:` | Boolean (default `false`). Wraps the section in a native `<details>/<summary>` (no JS). |
|
|
623
623
|
| `collapsed:` | Boolean (default `false`). Initial collapsed state when `collapsible: true`. |
|
|
624
|
-
| `columns:` | Positive Integer. Overrides the section grid column count (e.g. `columns: 2`). Omit to use the form's default responsive grid. Must be a positive Integer
|
|
625
|
-
| `condition:` | Lambda evaluated in the form instance context
|
|
624
|
+
| `columns:` | Positive Integer. Overrides the section grid column count (e.g. `columns: 2`). Omit to use the form's default responsive grid. Must be a positive Integer, any other value raises. (Literal only, not dynamic.) |
|
|
625
|
+
| `condition:` | Lambda evaluated in the form instance context: same semantics as `input ..., condition:`. `object`, `current_user`, helpers etc. are all available. A falsey result hides the entire section and withholds its fields (they do not spill into `ungrouped`). |
|
|
626
626
|
|
|
627
|
-
Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware
|
|
627
|
+
Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware, e.g. collapse a section by default only for existing records:
|
|
628
628
|
|
|
629
629
|
```ruby
|
|
630
630
|
section :advanced, :seo_title, :notes,
|
|
@@ -634,19 +634,19 @@ section :advanced, :seo_title, :notes,
|
|
|
634
634
|
```
|
|
635
635
|
|
|
636
636
|
::: warning Breaking change in 0.63
|
|
637
|
-
Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding
|
|
637
|
+
Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding, and a `form_layout` block is evaluated against the layout builder, so `object` there is a `NameError`.
|
|
638
638
|
|
|
639
639
|
```ruby
|
|
640
640
|
- collapsed: -> { object.persisted? }
|
|
641
641
|
+ collapsed: ->(form) { form.object.persisted? }
|
|
642
642
|
```
|
|
643
643
|
|
|
644
|
-
It fails loudly, never silently. `condition:` is unchanged
|
|
644
|
+
It fails loudly, never silently. `condition:` is unchanged: it is still evaluated against the form and still reads `object` with no argument.
|
|
645
645
|
:::
|
|
646
646
|
|
|
647
|
-
A section that resolves to **zero fields**
|
|
647
|
+
A section that resolves to **zero fields** (every declared field filtered out by the permitted set, or no field assigned) renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
|
|
648
648
|
|
|
649
|
-
The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records
|
|
649
|
+
The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records, re-evaluated on `pre_submit`, can be grouped in a section without any extra wiring:
|
|
650
650
|
|
|
651
651
|
```ruby
|
|
652
652
|
form_layout do
|
|
@@ -667,10 +667,10 @@ section :shipping, :address, :city, :postcode,
|
|
|
667
667
|
|
|
668
668
|
### `ungrouped(**opts)`
|
|
669
669
|
|
|
670
|
-
A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list
|
|
670
|
+
A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list**; its fields are computed at render time.
|
|
671
671
|
|
|
672
672
|
- Accepts the same options as `section`: `label:`, `description:`, `collapsible:`, `collapsed:`, `columns:`, `condition:`.
|
|
673
|
-
- **Position
|
|
673
|
+
- **Position**: where you call `ungrouped` in the block is where leftovers appear. Omit it entirely and leftovers render **last**, after every declared section, with no heading. (Declaring `ungrouped` at the very end is therefore equivalent to omitting it, except that the explicit form lets you add a `label:` and other options.)
|
|
674
674
|
- Declaring `ungrouped` more than once in a single `form_layout` raises `ArgumentError`.
|
|
675
675
|
|
|
676
676
|
```ruby
|
|
@@ -688,9 +688,9 @@ end
|
|
|
688
688
|
|
|
689
689
|
### Layout references keys; config stays on `input`
|
|
690
690
|
|
|
691
|
-
`form_layout` and `section` carry section-level options only. All per-field rendering config
|
|
691
|
+
`form_layout` and `section` carry section-level options only. All per-field rendering config (`as:`, the field's own `label:`, `choices:`, per-field `condition:`, `pre_submit:`, blocks) remains on the `input` declaration. Layout never duplicates field config.
|
|
692
692
|
|
|
693
|
-
This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it
|
|
693
|
+
This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it; a field-level span always wins, so you can opt one field back to full width inside a multi-column section:
|
|
694
694
|
|
|
695
695
|
```ruby
|
|
696
696
|
input :notes, wrapper: {class: "col-span-full"} # spans the whole row...
|
|
@@ -702,7 +702,7 @@ end
|
|
|
702
702
|
|
|
703
703
|
```ruby
|
|
704
704
|
class ArticleDefinition < ResourceDefinition
|
|
705
|
-
# per-field config on input
|
|
705
|
+
# per-field config on input: untouched by form_layout
|
|
706
706
|
input :body, as: :markdown
|
|
707
707
|
input :published_at, hint: "Leave blank to save as draft"
|
|
708
708
|
input :visibility, as: :select, choices: %w[public private unlisted]
|
|
@@ -716,13 +716,13 @@ end
|
|
|
716
716
|
|
|
717
717
|
### Fields not in the permitted set are skipped
|
|
718
718
|
|
|
719
|
-
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there
|
|
719
|
+
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there (a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting) is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out. And when every field a section lists is dropped this way, the section's chrome is dropped with it (see the zero-fields note above), so the same layout can serve a richly-permitted `edit` and a minimal `new` without leaving empty headings behind.
|
|
720
720
|
|
|
721
721
|
### On interactions
|
|
722
722
|
|
|
723
|
-
`form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically
|
|
723
|
+
`form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically, no extra wiring needed.
|
|
724
724
|
|
|
725
|
-
Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource
|
|
725
|
+
Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource`, so e.g. `collapsed: ->(form) { form.object.resource.archived? }`, and `condition: -> { object.resource.archived? }` (which is form-evaluated, so it needs no argument).
|
|
726
726
|
|
|
727
727
|
```ruby
|
|
728
728
|
class PublishPostInteraction < Plutonium::Interaction::Base
|
|
@@ -742,7 +742,7 @@ end
|
|
|
742
742
|
|
|
743
743
|
## Display layout
|
|
744
744
|
|
|
745
|
-
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules
|
|
745
|
+
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules: first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely, applied to the show page instead of the form.
|
|
746
746
|
|
|
747
747
|
```ruby
|
|
748
748
|
class PostDefinition < ResourceDefinition
|
|
@@ -756,7 +756,7 @@ class PostDefinition < ResourceDefinition
|
|
|
756
756
|
end
|
|
757
757
|
```
|
|
758
758
|
|
|
759
|
-
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid
|
|
759
|
+
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid, fully backwards-compatible.
|
|
760
760
|
|
|
761
761
|
### Independent of `form_layout`
|
|
762
762
|
|
|
@@ -782,9 +782,9 @@ Raising rather than ignoring the option means a `form_layout` block copied acros
|
|
|
782
782
|
|
|
783
783
|
### Section options
|
|
784
784
|
|
|
785
|
-
`label:`, `description:`, `collapsible:`, `collapsed:`, `condition
|
|
785
|
+
`label:`, `description:`, `collapsible:`, `collapsed:`, `condition:`, the same set as [`section(key, *fields, **opts)`](#section-key-fields-opts) minus `columns:`. A collapsible display section behaves exactly as a form one does, `collapsed:` included.
|
|
786
786
|
|
|
787
|
-
Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses
|
|
787
|
+
Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses: a zero-arity proc keeps its own binding, a one-arity proc is handed the display:
|
|
788
788
|
|
|
789
789
|
```ruby
|
|
790
790
|
section :audit, :created_at, collapsible: true, collapsed: ->(display) { display.object.active? }
|
|
@@ -794,11 +794,11 @@ section :audit, :created_at, collapsible: true, collapsed: ->(display) { display
|
|
|
794
794
|
|
|
795
795
|
### Rendering
|
|
796
796
|
|
|
797
|
-
Each section renders as its own card, stacked by a `sections_wrapper` container
|
|
797
|
+
Each section renders as its own card, stacked by a `sections_wrapper` container, so a sectioned show page has **no single outer card**. Fields declared via [`metadata`](#metadata-panel-show-page) are excluded from the sections and render in the metadata panel instead. Section chrome is themeable; see [UI › Displays › Theming](/reference/ui/displays#theming).
|
|
798
798
|
|
|
799
799
|
## Page width
|
|
800
800
|
|
|
801
|
-
Detail-style pages
|
|
801
|
+
Detail-style pages, the show page and resource forms, are constrained to a readable column by default. Inputs and values stretch to their container, so at full content width they become ~1200px-wide text boxes: past a comfortable measure, and a long eye-travel between a label and the value beside it.
|
|
802
802
|
|
|
803
803
|
Index and table pages are deliberately **not** affected; a table wants every pixel.
|
|
804
804
|
|
|
@@ -816,7 +816,7 @@ end
|
|
|
816
816
|
`:sm` `:md` `:lg` `:xl` `:full`. `:full` opts out of any constraint. An unknown value raises `ArgumentError` at declaration rather than silently rendering at some other width.
|
|
817
817
|
|
|
818
818
|
::: warning Size tokens are relative to their surface
|
|
819
|
-
These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable
|
|
819
|
+
These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable: a "small page" is reasonably larger than a "small dialog":
|
|
820
820
|
|
|
821
821
|
| Token | Page width | Centered modal | Slideover |
|
|
822
822
|
|---|---|---|---|
|
|
@@ -846,7 +846,7 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
|
|
|
846
846
|
|
|
847
847
|
### Scope
|
|
848
848
|
|
|
849
|
-
- **Modals are unaffected
|
|
849
|
+
- **Modals are unaffected**: a dialog sizes itself via `modal_size`.
|
|
850
850
|
- **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
|
|
851
851
|
- **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `:md` independently of `default_page_width`, so widening resource pages leaves wizard steps where they are. Set both if you want them to match.
|
|
852
852
|
|
|
@@ -866,9 +866,9 @@ input :documents, as: :uppy,
|
|
|
866
866
|
|
|
867
867
|
Inside `condition:` procs and block-form `input`/`display`:
|
|
868
868
|
|
|
869
|
-
- `object
|
|
869
|
+
- `object`: the record being edited or displayed
|
|
870
870
|
- `current_user`
|
|
871
|
-
- `current_parent
|
|
871
|
+
- `current_parent`: parent record for nested resources
|
|
872
872
|
- `request`, `params`
|
|
873
873
|
- All view helpers (via the same context as controllers)
|
|
874
874
|
|
|
@@ -922,23 +922,23 @@ interactive_action_page_breadcrumbs true
|
|
|
922
922
|
```ruby
|
|
923
923
|
class PostDefinition < ResourceDefinition
|
|
924
924
|
# "Save and add another" / "Update and continue editing"
|
|
925
|
-
# nil (default)
|
|
926
|
-
# true
|
|
927
|
-
# false
|
|
925
|
+
# nil (default): auto: hidden for singular resources, shown for plural
|
|
926
|
+
# true: always show
|
|
927
|
+
# false: always hide
|
|
928
928
|
submit_and_continue false
|
|
929
929
|
|
|
930
930
|
# How :new / :edit and interactive actions render
|
|
931
|
-
# :slideover (default)
|
|
932
|
-
# :centered
|
|
933
|
-
# false
|
|
931
|
+
# :slideover (default): slide-in panel from the right
|
|
932
|
+
# :centered: centered dialog
|
|
933
|
+
# false: full standalone pages (no modal)
|
|
934
934
|
# size: optional, one of :sm, :md (default), :lg, :xl, :auto, :full
|
|
935
|
-
# (widths are per-surface
|
|
935
|
+
# (widths are per-surface: see Page width; a slideover's :md is 480px,
|
|
936
936
|
# a centered dialog's is 576px, a page's is 896px)
|
|
937
937
|
modal :centered, size: :lg
|
|
938
938
|
end
|
|
939
939
|
```
|
|
940
940
|
|
|
941
|
-
`modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win
|
|
941
|
+
`modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win; see [Actions](./actions).
|
|
942
942
|
|
|
943
943
|
### `show_in` {#show_in}
|
|
944
944
|
|
|
@@ -951,8 +951,8 @@ end
|
|
|
951
951
|
|
|
952
952
|
Controls how the **show page** opens when a record is clicked in the table or grid (and serves as the default for a [kanban board](/reference/kanban/dsl#show_in), which can override it per-board):
|
|
953
953
|
|
|
954
|
-
- `:page` (default)
|
|
955
|
-
- `:modal
|
|
954
|
+
- `:page` (default): full-page navigation to the show route.
|
|
955
|
+
- `:modal`, the show page opens in a **centered** dialog. This is deliberately independent of `modal:`/`modal_mode` above (which styles `:new`/`:edit`), show is always centered, never a slideover. From inside the modal an expand icon opens the full page in a new tab; ⌘/Ctrl-click (or middle-click) on the row/card does the same directly.
|
|
956
956
|
|
|
957
957
|
An unknown mode raises `ArgumentError`.
|
|
958
958
|
|
|
@@ -972,7 +972,7 @@ Behavior:
|
|
|
972
972
|
- **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
|
|
973
973
|
- **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
|
|
974
974
|
- **Responsive.** Side-by-side at `lg+`, stacked below.
|
|
975
|
-
- **Formatting inherits.** Field labels and `as:` declarations propagate
|
|
975
|
+
- **Formatting inherits.** Field labels and `as:` declarations propagate: the metadata panel uses the same field-rendering machinery as the main card.
|
|
976
976
|
|
|
977
977
|
## Index views (Table & Grid)
|
|
978
978
|
|
|
@@ -980,7 +980,7 @@ Resources can offer both Table and Grid views. The user switches via the toolbar
|
|
|
980
980
|
|
|
981
981
|
```ruby
|
|
982
982
|
class UserDefinition < ResourceDefinition
|
|
983
|
-
# No `index_views :table, :grid` needed
|
|
983
|
+
# No `index_views :table, :grid` needed: declaring grid_fields auto-enables :grid.
|
|
984
984
|
grid_fields(
|
|
985
985
|
image: :avatar, # ActiveStorage attachment, Shrine, or URL
|
|
986
986
|
header: :name, # falls back to to_label
|
|
@@ -990,7 +990,7 @@ class UserDefinition < ResourceDefinition
|
|
|
990
990
|
footer: :last_seen_at # falls back to :created_at
|
|
991
991
|
)
|
|
992
992
|
|
|
993
|
-
default_index_view :grid # optional
|
|
993
|
+
default_index_view :grid # optional: initial view when no cookie
|
|
994
994
|
grid_layout :media # :compact (default) or :media
|
|
995
995
|
grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
|
|
996
996
|
end
|
|
@@ -1004,13 +1004,13 @@ end
|
|
|
1004
1004
|
| `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
|
|
1005
1005
|
| `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
|
|
1006
1006
|
|
|
1007
|
-
Grid slots
|
|
1007
|
+
Grid slots, `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer`, are all optional. `:meta` accepts an array; the rest are single fields. Slots pointing at policy-blocked fields collapse silently.
|
|
1008
1008
|
|
|
1009
1009
|
Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
|
|
1010
1010
|
|
|
1011
1011
|
## Custom page classes
|
|
1012
1012
|
|
|
1013
|
-
Override the rendered page entirely
|
|
1013
|
+
Override the rendered page entirely: full control via Phlex:
|
|
1014
1014
|
|
|
1015
1015
|
```ruby
|
|
1016
1016
|
class PostDefinition < ResourceDefinition
|
|
@@ -1037,8 +1037,8 @@ See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms)
|
|
|
1037
1037
|
|
|
1038
1038
|
## Related
|
|
1039
1039
|
|
|
1040
|
-
- [Query](./query)
|
|
1041
|
-
- [Actions](./actions)
|
|
1042
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
1043
|
-
- [UI › Forms](/reference/ui/forms)
|
|
1044
|
-
- [UI › Pages](/reference/ui/pages)
|
|
1040
|
+
- [Query](./query): search, filters, scopes, sorting
|
|
1041
|
+
- [Actions](./actions): custom + bulk actions
|
|
1042
|
+
- [Behavior › Policy](/reference/behavior/policies): `permitted_attributes_for_*`, authorization
|
|
1043
|
+
- [UI › Forms](/reference/ui/forms): field builder, association inputs, theming
|
|
1044
|
+
- [UI › Pages](/reference/ui/pages): custom page classes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CSV Export
|
|
2
2
|
|
|
3
3
|
Every resource ships with a streamed CSV export, **disabled by default**. It is *not* an
|
|
4
|
-
[action](./actions.md)
|
|
4
|
+
[action](./actions.md) (it streams a file and opens in a new tab), so it is enabled
|
|
5
5
|
through the policy rather than declared with `action :export_csv`. The route
|
|
6
6
|
(`GET /<resources>/export_csv`) is auto-mounted on every resource; a split "Export"
|
|
7
7
|
button appears on the index page once the policy permits it.
|
|
@@ -25,8 +25,8 @@ The control is a split button with two behaviours:
|
|
|
25
25
|
|
|
26
26
|
| | Source | Filename |
|
|
27
27
|
|---|---|---|
|
|
28
|
-
| **Export** (primary) | The current view
|
|
29
|
-
| **Export all** (dropdown) | The entire authorized scope
|
|
28
|
+
| **Export** (primary) | The current view: selected scope + filters + search (the index's `?q`), **all** matching rows (not just the visible page) | `posts_<date>.csv` |
|
|
29
|
+
| **Export all** (dropdown) | The entire authorized scope: ignores scope, filters, search, and default scope | `posts_all_<date>.csv` |
|
|
30
30
|
|
|
31
31
|
"Export all" always exports everything the user is authorized to read, regardless of the
|
|
32
32
|
current scope/filters.
|
|
@@ -73,12 +73,12 @@ For a column **without** an `export` block, the value is read straight off the r
|
|
|
73
73
|
(`record.public_send(name)`):
|
|
74
74
|
|
|
75
75
|
- **Scalars** (strings, numbers, booleans, dates) are written as-is.
|
|
76
|
-
- **Associations** render as their display label
|
|
77
|
-
uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined)
|
|
76
|
+
- **Associations** render as their display label: the same `display_name_of` the index
|
|
77
|
+
uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined), never
|
|
78
78
|
`#<User:0x…>`. Add an `export` block to export a specific field instead (e.g. the email).
|
|
79
79
|
- A name that is **neither** an `export` block **nor** a real method on the record renders
|
|
80
80
|
the placeholder `<<invalid column>>` rather than aborting the (already-streaming) download.
|
|
81
|
-
To export a computed or virtual column, give it an `export` block
|
|
81
|
+
To export a computed or virtual column, give it an `export` block: a `label:`-only
|
|
82
82
|
`export` does **not** supply a value, so it too renders the placeholder.
|
|
83
83
|
|
|
84
84
|
## Notes & limits
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Resource Reference
|
|
2
2
|
|
|
3
|
-
A **resource** is the unit Plutonium gives you full CRUD for
|
|
3
|
+
A **resource** is the unit Plutonium gives you full CRUD for (list, show, create, edit, delete) automatically. It's four cooperating layers, plus an optional fifth for business logic.
|
|
4
4
|
|
|
5
5
|
| Layer | File | What it controls |
|
|
6
6
|
|---|---|---|
|
|
7
7
|
| [Model](./model) | `app/models/post.rb` | Data, validations, associations |
|
|
8
|
-
| [Definition](./definition) | `app/definitions/post_definition.rb` | UI
|
|
9
|
-
| Policy | `app/policies/post_policy.rb` | Authorization
|
|
10
|
-
| Controller | `app/controllers/posts_controller.rb` | Request handling
|
|
11
|
-
| Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions
|
|
8
|
+
| [Definition](./definition) | `app/definitions/post_definition.rb` | UI: which fields, how they render, what actions exist |
|
|
9
|
+
| Policy | `app/policies/post_policy.rb` | Authorization: see [Behavior › Policy](/reference/behavior/policies) |
|
|
10
|
+
| Controller | `app/controllers/posts_controller.rb` | Request handling: see [Behavior › Controller](/reference/behavior/controllers) |
|
|
11
|
+
| Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions, see [Behavior › Interaction](/reference/behavior/interactions) |
|
|
12
12
|
|
|
13
13
|
## How a resource is born
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ That single scaffold gives you a working model + migration + controller + policy
|
|
|
22
22
|
|
|
23
23
|
## Auto-detection is the default
|
|
24
24
|
|
|
25
|
-
Plutonium reads your model and renders every attribute automatically
|
|
25
|
+
Plutonium reads your model and renders every attribute automatically: type, label, form widget, display formatter, table column. You only declare overrides:
|
|
26
26
|
|
|
27
27
|
```ruby
|
|
28
28
|
class PostDefinition < Plutonium::Resource::Definition
|
|
@@ -33,20 +33,20 @@ end
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
::: warning Don't declare for completeness
|
|
36
|
-
A `field :title` with no options that matches what Plutonium would auto-detect is **dead code
|
|
36
|
+
A `field :title` with no options that matches what Plutonium would auto-detect is **dead code**; it does nothing and clutters the file. Declare ONLY when you need a different type, an option, a `condition:`, a block, or a custom component.
|
|
37
37
|
:::
|
|
38
38
|
|
|
39
39
|
## Sub-pages
|
|
40
40
|
|
|
41
|
-
- [Model](./model)
|
|
42
|
-
- [Definition](./definition)
|
|
43
|
-
- [Query](./query)
|
|
44
|
-
- [Actions](./actions)
|
|
45
|
-
- [Positioning & drag-to-reorder](./positioning)
|
|
46
|
-
- [CSV Export](./export)
|
|
41
|
+
- [Model](./model): `Plutonium::Resource::Record`, `has_cents`, SGID, custom routing, labeling
|
|
42
|
+
- [Definition](./definition): fields, inputs, displays, columns, page chrome, metadata panel, index views
|
|
43
|
+
- [Query](./query): search, filters, scopes, sorting
|
|
44
|
+
- [Actions](./actions): custom actions, bulk actions, interaction integration
|
|
45
|
+
- [Positioning & drag-to-reorder](./positioning): `positioned_on`, `position_on`, the drag grip, `reposition?`
|
|
46
|
+
- [CSV Export](./export): streamed export, opt-in through the policy
|
|
47
47
|
|
|
48
48
|
## Related
|
|
49
49
|
|
|
50
|
-
- [Guides › Adding Resources](/guides/adding-resources)
|
|
51
|
-
- [App › Generators](/reference/app/generators)
|
|
52
|
-
- [Tenancy](/reference/tenancy/)
|
|
50
|
+
- [Guides › Adding Resources](/guides/adding-resources): task recipe
|
|
51
|
+
- [App › Generators](/reference/app/generators): `pu:res:scaffold` / `pu:res:conn` reference
|
|
52
|
+
- [Tenancy](/reference/tenancy/): multi-tenant scoping
|