plutonium 0.65.0 → 0.67.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 +158 -141
- 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 +161 -98
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +43 -0
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +56 -3
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +4 -4
- data/config/brakeman.ignore +1 -1
- data/config/locales/en/js.yml +4 -0
- data/config/locales/en/ui.yml +16 -0
- 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 +85 -80
- 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 +24 -14
- 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 +32 -15
- data/docs/reference/ui/forms.md +89 -39
- 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_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +13 -3
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +10 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +10 -1
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/plutonium/auth/rodauth.rb +7 -0
- data/lib/plutonium/core/controller.rb +0 -2
- data/lib/plutonium/helpers/content_helper.rb +0 -3
- data/lib/plutonium/query/filter.rb +2 -1
- data/lib/plutonium/query/filters/tags.rb +36 -0
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
- data/lib/plutonium/ui/display/base.rb +50 -2
- data/lib/plutonium/ui/display/components/badge.rb +1 -1
- data/lib/plutonium/ui/display/components/binary.rb +20 -0
- data/lib/plutonium/ui/display/components/boolean.rb +1 -1
- data/lib/plutonium/ui/display/components/code.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +14 -6
- data/lib/plutonium/ui/display/components/duration.rb +31 -0
- data/lib/plutonium/ui/display/components/file_size.rb +22 -0
- data/lib/plutonium/ui/display/components/key_value.rb +37 -0
- data/lib/plutonium/ui/display/components/list.rb +33 -0
- data/lib/plutonium/ui/display/components/number.rb +32 -0
- data/lib/plutonium/ui/display/components/progress.rb +36 -0
- data/lib/plutonium/ui/display/components/rating.rb +36 -0
- data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
- data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
- data/lib/plutonium/ui/display/components/tags.rb +18 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +24 -2
- data/lib/plutonium/ui/form/base.rb +30 -1
- data/lib/plutonium/ui/form/components/binary.rb +40 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
- data/lib/plutonium/ui/form/components/list.rb +46 -0
- data/lib/plutonium/ui/form/components/rating.rb +51 -0
- data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
- data/lib/plutonium/ui/form/components/tags.rb +45 -0
- data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
- data/lib/plutonium/ui/form/components/uppy.rb +1 -1
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
- data/lib/plutonium/ui/form/theme.rb +20 -4
- data/lib/plutonium/ui/grid/card.rb +13 -1
- data/lib/plutonium/ui/layout/base.rb +10 -1
- data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
- data/lib/plutonium/ui/options/secret_field.rb +32 -0
- data/lib/plutonium/ui/options/tags_field.rb +23 -0
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
- data/lib/plutonium/ui/table/display_theme.rb +30 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +53 -10
- data/src/css/flatpickr.css +91 -0
- data/src/js/controllers/easymde_controller.js +1 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/slim_select_controller.js +32 -3
- data/src/js/controllers/timeago_controller.js +40 -0
- metadata +25 -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
|
|
|
@@ -72,6 +72,7 @@ Plutonium detects, from the model:
|
|
|
72
72
|
| `boolean` | `:boolean` |
|
|
73
73
|
| `date`, `datetime`, `time` | `:date` / `:datetime` / `:time` |
|
|
74
74
|
| `json`, `jsonb` | `:json` |
|
|
75
|
+
| `binary` | `:binary` |
|
|
75
76
|
|
|
76
77
|
Validations on the model inform the UI too: `validates :title, presence: true` → required field; `validates :role, inclusion: { in: [...] }` → select choices.
|
|
77
78
|
|
|
@@ -99,28 +100,32 @@ end
|
|
|
99
100
|
|
|
100
101
|
| Category | Types |
|
|
101
102
|
|---|---|
|
|
102
|
-
| Text | `:string`, `:text`, `:email`, `:url`, `:
|
|
103
|
-
| Rich text | `:
|
|
104
|
-
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
105
|
-
| Boolean | `:toggle` / `:switch` (switch
|
|
103
|
+
| Text | `:string`, `:text`, `:email`, `:url`, `:phone`, `:password` (alias `:secret`) |
|
|
104
|
+
| Rich text | `:rich_text` (Action Text editor: Trix, or Lexxy once installed; **default** for `has_rich_text`), `:markdown` (EasyMDE) |
|
|
105
|
+
| Numeric | `:number`, `:integer`, `:decimal`, `:range`, `:rating` (star picker, `max:`) |
|
|
106
|
+
| Boolean | `:toggle` / `:switch` (switch: **default** for boolean columns), `:boolean` (plain checkbox) |
|
|
106
107
|
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
|
-
| Selection | `:select`, `:slim_select`, `:
|
|
108
|
-
|
|
|
108
|
+
| Selection | `:select`, `:slim_select`, `:collection_radio_buttons`, `:collection_checkboxes` |
|
|
109
|
+
| Lists | `:list` (chip input for array values; `suggestions:`, `addable:`, `limit:`), `:tags` (no_fly_list `has_tags` contexts, **auto-detected**) |
|
|
110
|
+
| Files | `:file`, `:uppy`, `:attachment` (Active Storage), `:binary` (bytes in a `binary` column) |
|
|
109
111
|
| Associations | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
|
|
110
|
-
| Special | `:hidden`, `:color`, `:
|
|
112
|
+
| Special | `:hidden`, `:color`, `:json`, `:key_value` |
|
|
111
113
|
|
|
112
114
|
### Display types (show / index)
|
|
113
115
|
|
|
114
|
-
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
|
|
116
|
+
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`, `:key_value`, `:binary`, `:list`, `:tags`, `:rich_text`, `:relative_time`, `:code`, `:duration`, `:file_size`, `:progress`, `:rating`
|
|
115
117
|
|
|
116
118
|
#### Auto-inferred display formatting
|
|
117
119
|
|
|
118
|
-
These render automatically
|
|
120
|
+
These render automatically; declare an `as:` only to override or pass options:
|
|
119
121
|
|
|
120
122
|
| Column | Renders as | Notes |
|
|
121
123
|
|---|---|---|
|
|
122
124
|
| `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No"; override with `true_label:` / `false_label:` |
|
|
123
125
|
| `enum` | status badge (`:badge`) | known statuses auto-colored; unknown values get a stable decorative color; override per-value with `colors:` |
|
|
126
|
+
| `binary` | size (`:binary`) | `Binary data (12 KB)`, never the bytes |
|
|
127
|
+
| `has_rich_text` | HTML (`:rich_text`) | rendered and sanitized by Action Text |
|
|
128
|
+
| no_fly_list `has_tags` | chips (`:tags`) | declare by context name (`:labels`), not `labels_list` |
|
|
124
129
|
| `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals; symbol from `unit:` on `has_cents` (model-wide) or per-display, else `config.default_currency_unit` / the i18n default (see below) |
|
|
125
130
|
|
|
126
131
|
```ruby
|
|
@@ -132,11 +137,11 @@ display :active, as: :boolean, true_label: "Live", false_label: "Off"
|
|
|
132
137
|
**Currency symbol.** The `unit:` can be set on the model's `has_cents` declaration
|
|
133
138
|
(`has_cents :price_cents, unit: "£"`, or `unit: :currency_symbol` to read a method
|
|
134
139
|
off the record for per-row currencies). That model-level unit is used everywhere the
|
|
135
|
-
value renders as currency
|
|
140
|
+
value renders as currency: the show page, tables, **and grid/kanban cards**. A
|
|
136
141
|
per-display `unit:` overrides it for that one display; `unit: false` explicitly
|
|
137
142
|
renders no symbol. When neither is set, currency falls back to
|
|
138
143
|
`Plutonium.configuration.default_currency_unit` (default: the i18n
|
|
139
|
-
`number.currency.format.unit` if the locale defines it
|
|
144
|
+
`number.currency.format.unit` if the locale defines it, `$` in `en`, else no symbol).
|
|
140
145
|
|
|
141
146
|
## Field options
|
|
142
147
|
|
|
@@ -206,22 +211,22 @@ Keep a `field` condition to context that exists on every surface (`Rails.env`, a
|
|
|
206
211
|
feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
|
|
207
212
|
|
|
208
213
|
::: 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_
|
|
214
|
+
`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
215
|
:::
|
|
211
216
|
|
|
212
217
|
## Options that vary per render
|
|
213
218
|
|
|
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
|
|
219
|
+
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
220
|
|
|
216
221
|
```ruby
|
|
217
222
|
input :tier, as: :select, choices: ->(form) { form.object.account.available_tiers }
|
|
218
223
|
input :notes, placeholder: -> { "Updated #{Time.current.year}" }
|
|
219
224
|
```
|
|
220
225
|
|
|
221
|
-
- **`-> { … }`** is called as-is, keeping whatever it closed over
|
|
226
|
+
- **`-> { … }`** 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
227
|
- **`->(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
228
|
|
|
224
|
-
The rule holds on wizard steps too
|
|
229
|
+
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
230
|
|
|
226
231
|
### `condition:` is not an option
|
|
227
232
|
|
|
@@ -229,10 +234,10 @@ The rule holds on wizard steps too — but there a zero-argument proc closes ove
|
|
|
229
234
|
|
|
230
235
|
| | asks | so it | receiver |
|
|
231
236
|
|---|---|---|---|
|
|
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
|
|
237
|
+
| 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 |
|
|
238
|
+
| **`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
239
|
|
|
235
|
-
So `condition:` always runs **against** its context and reads it with no argument
|
|
240
|
+
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
241
|
|
|
237
242
|
```ruby
|
|
238
243
|
input :notes, condition: -> { object.published? } # form
|
|
@@ -253,7 +258,7 @@ class QuestionDefinition < ResourceDefinition
|
|
|
253
258
|
choices: %w[text choice scale],
|
|
254
259
|
pre_submit: true
|
|
255
260
|
|
|
256
|
-
# Dependents
|
|
261
|
+
# Dependents: no `as:` needed when the model column type matches
|
|
257
262
|
input :max_length, condition: -> { object.question_type == "text" }
|
|
258
263
|
input :choices, condition: -> { object.question_type == "choice" }
|
|
259
264
|
input :min_value, condition: -> { object.question_type == "scale" }
|
|
@@ -341,7 +346,7 @@ See [UI › Components](/reference/ui/components) for writing reusable Phlex com
|
|
|
341
346
|
|
|
342
347
|
### Custom component class
|
|
343
348
|
|
|
344
|
-
`as:` takes a **field component
|
|
349
|
+
`as:` takes a **field component**: Plutonium constructs it as
|
|
345
350
|
`YourComponent.new(field, **attributes)`, so it subclasses
|
|
346
351
|
`Phlexi::Form::Components::Base` (inputs) or `Phlexi::Display::Components::Base`
|
|
347
352
|
(displays) and reads the value off `field`:
|
|
@@ -352,7 +357,7 @@ display :chart, as: ChartComponent
|
|
|
352
357
|
```
|
|
353
358
|
|
|
354
359
|
A component with its own constructor (e.g. `PostCardComponent.new(post:)`) is not
|
|
355
|
-
an `as:` candidate
|
|
360
|
+
an `as:` candidate; it would raise `ArgumentError`. Build it in a block instead:
|
|
356
361
|
|
|
357
362
|
```ruby
|
|
358
363
|
display :card do |field|
|
|
@@ -453,7 +458,7 @@ end
|
|
|
453
458
|
|---|---|
|
|
454
459
|
| `limit` | Max records (auto-detected from model; default 10) |
|
|
455
460
|
| `allow_destroy` | Show delete checkbox (auto-detected) |
|
|
456
|
-
| `update_only` | Hide "Add" button
|
|
461
|
+
| `update_only` | Hide "Add" button: only edit existing |
|
|
457
462
|
| `description` | Help text above the section |
|
|
458
463
|
| `condition` | Proc to show/hide |
|
|
459
464
|
| `using` | Another Definition class |
|
|
@@ -468,13 +473,13 @@ end
|
|
|
468
473
|
end
|
|
469
474
|
```
|
|
470
475
|
- **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
|
|
476
|
+
- **`update_only: true` hides the Add button**: for `has_one` and "settings"-style associations.
|
|
477
|
+
- **Custom class names**: use `class_name:` in the model AND `using:` in the definition.
|
|
473
478
|
|
|
474
479
|
## Structured inputs
|
|
475
480
|
|
|
476
481
|
Classless inline fieldsets backed by a JSON/jsonb column. No model associations
|
|
477
|
-
required
|
|
482
|
+
required: the whole sub-form is serialised into a single column as a hash
|
|
478
483
|
(single form) or an array of hashes (repeater).
|
|
479
484
|
|
|
480
485
|

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

|
|
522
527
|
|
|
523
528
|
### Policy
|
|
524
529
|
|
|
525
|
-
Permit the column name as a plain symbol
|
|
530
|
+
Permit the column name as a plain symbol; Plutonium handles the nested hash
|
|
526
531
|
params automatically:
|
|
527
532
|
|
|
528
533
|
```ruby
|
|
@@ -544,13 +549,13 @@ attribute is declared automatically; `execute` receives the value as a `Hash`
|
|
|
544
549
|
The fields are classless render declarations, so there is nothing for Plutonium
|
|
545
550
|
to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
|
|
546
551
|
records run their own model validations). Whatever the form submits is stored
|
|
547
|
-
as-is, after blank rows are dropped
|
|
552
|
+
as-is, after blank rows are dropped: **no per-field server-side validation**.
|
|
548
553
|
:::
|
|
549
554
|
|
|
550
555
|
Specifically:
|
|
551
556
|
|
|
552
557
|
- **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
|
|
558
|
+
select's `choices:` guide the browser but are **not** enforced on the server:
|
|
554
559
|
an API call or a crafted request can submit anything.
|
|
555
560
|
- **Selects silently drop unknown values.** If a stored value is not among a
|
|
556
561
|
`as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
|
|
@@ -560,7 +565,7 @@ Specifically:
|
|
|
560
565
|
`choices:` can drift. Keep `choices:` a stable superset, or use a free-text
|
|
561
566
|
input, when values can change over time.
|
|
562
567
|
|
|
563
|
-
To enforce anything, add the validation yourself
|
|
568
|
+
To enforce anything, add the validation yourself; it runs server-side:
|
|
564
569
|
|
|
565
570
|
```ruby
|
|
566
571
|
# resource: validate the JSON column on the model
|
|
@@ -607,7 +612,7 @@ end
|
|
|
607
612
|
|
|
608
613
|
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
614
|
|
|
610
|
-
With no `form_layout` declared the form renders unchanged as a single responsive grid
|
|
615
|
+
With no `form_layout` declared the form renders unchanged as a single responsive grid, fully backwards-compatible.
|
|
611
616
|
|
|
612
617
|
### `section(key, *fields, **opts)`
|
|
613
618
|
|
|
@@ -615,16 +620,16 @@ Groups a set of fields under an optional heading.
|
|
|
615
620
|
|
|
616
621
|
| Argument | Description |
|
|
617
622
|
|---|---|
|
|
618
|
-
| `key` | Symbol. `:ungrouped` is reserved
|
|
623
|
+
| `key` | Symbol. `:ungrouped` is reserved: use the `ungrouped` macro instead (raises `ArgumentError` otherwise). |
|
|
619
624
|
| `*fields` | Ordered field keys to place in this section. |
|
|
620
625
|
| `label:` | Section heading. Defaults to `key.to_s.humanize` (e.g. `:shipping_address` → `"Shipping address"`). |
|
|
621
626
|
| `description:` | Optional help line rendered below the heading. |
|
|
622
627
|
| `collapsible:` | Boolean (default `false`). Wraps the section in a native `<details>/<summary>` (no JS). |
|
|
623
628
|
| `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
|
|
629
|
+
| `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.) |
|
|
630
|
+
| `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
631
|
|
|
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
|
|
632
|
+
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
633
|
|
|
629
634
|
```ruby
|
|
630
635
|
section :advanced, :seo_title, :notes,
|
|
@@ -634,19 +639,19 @@ section :advanced, :seo_title, :notes,
|
|
|
634
639
|
```
|
|
635
640
|
|
|
636
641
|
::: 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
|
|
642
|
+
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
643
|
|
|
639
644
|
```ruby
|
|
640
645
|
- collapsed: -> { object.persisted? }
|
|
641
646
|
+ collapsed: ->(form) { form.object.persisted? }
|
|
642
647
|
```
|
|
643
648
|
|
|
644
|
-
It fails loudly, never silently. `condition:` is unchanged
|
|
649
|
+
It fails loudly, never silently. `condition:` is unchanged: it is still evaluated against the form and still reads `object` with no argument.
|
|
645
650
|
:::
|
|
646
651
|
|
|
647
|
-
A section that resolves to **zero fields**
|
|
652
|
+
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
653
|
|
|
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
|
|
654
|
+
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
655
|
|
|
651
656
|
```ruby
|
|
652
657
|
form_layout do
|
|
@@ -667,10 +672,10 @@ section :shipping, :address, :city, :postcode,
|
|
|
667
672
|
|
|
668
673
|
### `ungrouped(**opts)`
|
|
669
674
|
|
|
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
|
|
675
|
+
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
676
|
|
|
672
677
|
- Accepts the same options as `section`: `label:`, `description:`, `collapsible:`, `collapsed:`, `columns:`, `condition:`.
|
|
673
|
-
- **Position
|
|
678
|
+
- **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
679
|
- Declaring `ungrouped` more than once in a single `form_layout` raises `ArgumentError`.
|
|
675
680
|
|
|
676
681
|
```ruby
|
|
@@ -688,9 +693,9 @@ end
|
|
|
688
693
|
|
|
689
694
|
### Layout references keys; config stays on `input`
|
|
690
695
|
|
|
691
|
-
`form_layout` and `section` carry section-level options only. All per-field rendering config
|
|
696
|
+
`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
697
|
|
|
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
|
|
698
|
+
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
699
|
|
|
695
700
|
```ruby
|
|
696
701
|
input :notes, wrapper: {class: "col-span-full"} # spans the whole row...
|
|
@@ -702,7 +707,7 @@ end
|
|
|
702
707
|
|
|
703
708
|
```ruby
|
|
704
709
|
class ArticleDefinition < ResourceDefinition
|
|
705
|
-
# per-field config on input
|
|
710
|
+
# per-field config on input: untouched by form_layout
|
|
706
711
|
input :body, as: :markdown
|
|
707
712
|
input :published_at, hint: "Leave blank to save as draft"
|
|
708
713
|
input :visibility, as: :select, choices: %w[public private unlisted]
|
|
@@ -716,13 +721,13 @@ end
|
|
|
716
721
|
|
|
717
722
|
### Fields not in the permitted set are skipped
|
|
718
723
|
|
|
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
|
|
724
|
+
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
725
|
|
|
721
726
|
### On interactions
|
|
722
727
|
|
|
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
|
|
728
|
+
`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
729
|
|
|
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
|
|
730
|
+
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
731
|
|
|
727
732
|
```ruby
|
|
728
733
|
class PublishPostInteraction < Plutonium::Interaction::Base
|
|
@@ -742,7 +747,7 @@ end
|
|
|
742
747
|
|
|
743
748
|
## Display layout
|
|
744
749
|
|
|
745
|
-
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules
|
|
750
|
+
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
751
|
|
|
747
752
|
```ruby
|
|
748
753
|
class PostDefinition < ResourceDefinition
|
|
@@ -756,7 +761,7 @@ class PostDefinition < ResourceDefinition
|
|
|
756
761
|
end
|
|
757
762
|
```
|
|
758
763
|
|
|
759
|
-
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid
|
|
764
|
+
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid, fully backwards-compatible.
|
|
760
765
|
|
|
761
766
|
### Independent of `form_layout`
|
|
762
767
|
|
|
@@ -782,9 +787,9 @@ Raising rather than ignoring the option means a `form_layout` block copied acros
|
|
|
782
787
|
|
|
783
788
|
### Section options
|
|
784
789
|
|
|
785
|
-
`label:`, `description:`, `collapsible:`, `collapsed:`, `condition
|
|
790
|
+
`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
791
|
|
|
787
|
-
Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses
|
|
792
|
+
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
793
|
|
|
789
794
|
```ruby
|
|
790
795
|
section :audit, :created_at, collapsible: true, collapsed: ->(display) { display.object.active? }
|
|
@@ -794,11 +799,11 @@ section :audit, :created_at, collapsible: true, collapsed: ->(display) { display
|
|
|
794
799
|
|
|
795
800
|
### Rendering
|
|
796
801
|
|
|
797
|
-
Each section renders as its own card, stacked by a `sections_wrapper` container
|
|
802
|
+
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
803
|
|
|
799
804
|
## Page width
|
|
800
805
|
|
|
801
|
-
Detail-style pages
|
|
806
|
+
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
807
|
|
|
803
808
|
Index and table pages are deliberately **not** affected; a table wants every pixel.
|
|
804
809
|
|
|
@@ -816,7 +821,7 @@ end
|
|
|
816
821
|
`: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
822
|
|
|
818
823
|
::: 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
|
|
824
|
+
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
825
|
|
|
821
826
|
| Token | Page width | Centered modal | Slideover |
|
|
822
827
|
|---|---|---|---|
|
|
@@ -846,7 +851,7 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
|
|
|
846
851
|
|
|
847
852
|
### Scope
|
|
848
853
|
|
|
849
|
-
- **Modals are unaffected
|
|
854
|
+
- **Modals are unaffected**: a dialog sizes itself via `modal_size`.
|
|
850
855
|
- **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
|
|
851
856
|
- **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
857
|
|
|
@@ -866,9 +871,9 @@ input :documents, as: :uppy,
|
|
|
866
871
|
|
|
867
872
|
Inside `condition:` procs and block-form `input`/`display`:
|
|
868
873
|
|
|
869
|
-
- `object
|
|
874
|
+
- `object`: the record being edited or displayed
|
|
870
875
|
- `current_user`
|
|
871
|
-
- `current_parent
|
|
876
|
+
- `current_parent`: parent record for nested resources
|
|
872
877
|
- `request`, `params`
|
|
873
878
|
- All view helpers (via the same context as controllers)
|
|
874
879
|
|
|
@@ -922,23 +927,23 @@ interactive_action_page_breadcrumbs true
|
|
|
922
927
|
```ruby
|
|
923
928
|
class PostDefinition < ResourceDefinition
|
|
924
929
|
# "Save and add another" / "Update and continue editing"
|
|
925
|
-
# nil (default)
|
|
926
|
-
# true
|
|
927
|
-
# false
|
|
930
|
+
# nil (default): auto: hidden for singular resources, shown for plural
|
|
931
|
+
# true: always show
|
|
932
|
+
# false: always hide
|
|
928
933
|
submit_and_continue false
|
|
929
934
|
|
|
930
935
|
# How :new / :edit and interactive actions render
|
|
931
|
-
# :slideover (default)
|
|
932
|
-
# :centered
|
|
933
|
-
# false
|
|
936
|
+
# :slideover (default): slide-in panel from the right
|
|
937
|
+
# :centered: centered dialog
|
|
938
|
+
# false: full standalone pages (no modal)
|
|
934
939
|
# size: optional, one of :sm, :md (default), :lg, :xl, :auto, :full
|
|
935
|
-
# (widths are per-surface
|
|
940
|
+
# (widths are per-surface: see Page width; a slideover's :md is 480px,
|
|
936
941
|
# a centered dialog's is 576px, a page's is 896px)
|
|
937
942
|
modal :centered, size: :lg
|
|
938
943
|
end
|
|
939
944
|
```
|
|
940
945
|
|
|
941
|
-
`modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win
|
|
946
|
+
`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
947
|
|
|
943
948
|
### `show_in` {#show_in}
|
|
944
949
|
|
|
@@ -951,8 +956,8 @@ end
|
|
|
951
956
|
|
|
952
957
|
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
958
|
|
|
954
|
-
- `:page` (default)
|
|
955
|
-
- `:modal
|
|
959
|
+
- `:page` (default): full-page navigation to the show route.
|
|
960
|
+
- `: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
961
|
|
|
957
962
|
An unknown mode raises `ArgumentError`.
|
|
958
963
|
|
|
@@ -972,7 +977,7 @@ Behavior:
|
|
|
972
977
|
- **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
|
|
973
978
|
- **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
|
|
974
979
|
- **Responsive.** Side-by-side at `lg+`, stacked below.
|
|
975
|
-
- **Formatting inherits.** Field labels and `as:` declarations propagate
|
|
980
|
+
- **Formatting inherits.** Field labels and `as:` declarations propagate: the metadata panel uses the same field-rendering machinery as the main card.
|
|
976
981
|
|
|
977
982
|
## Index views (Table & Grid)
|
|
978
983
|
|
|
@@ -980,7 +985,7 @@ Resources can offer both Table and Grid views. The user switches via the toolbar
|
|
|
980
985
|
|
|
981
986
|
```ruby
|
|
982
987
|
class UserDefinition < ResourceDefinition
|
|
983
|
-
# No `index_views :table, :grid` needed
|
|
988
|
+
# No `index_views :table, :grid` needed: declaring grid_fields auto-enables :grid.
|
|
984
989
|
grid_fields(
|
|
985
990
|
image: :avatar, # ActiveStorage attachment, Shrine, or URL
|
|
986
991
|
header: :name, # falls back to to_label
|
|
@@ -990,7 +995,7 @@ class UserDefinition < ResourceDefinition
|
|
|
990
995
|
footer: :last_seen_at # falls back to :created_at
|
|
991
996
|
)
|
|
992
997
|
|
|
993
|
-
default_index_view :grid # optional
|
|
998
|
+
default_index_view :grid # optional: initial view when no cookie
|
|
994
999
|
grid_layout :media # :compact (default) or :media
|
|
995
1000
|
grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
|
|
996
1001
|
end
|
|
@@ -1004,13 +1009,13 @@ end
|
|
|
1004
1009
|
| `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
|
|
1005
1010
|
| `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
|
|
1006
1011
|
|
|
1007
|
-
Grid slots
|
|
1012
|
+
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
1013
|
|
|
1009
1014
|
Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
|
|
1010
1015
|
|
|
1011
1016
|
## Custom page classes
|
|
1012
1017
|
|
|
1013
|
-
Override the rendered page entirely
|
|
1018
|
+
Override the rendered page entirely: full control via Phlex:
|
|
1014
1019
|
|
|
1015
1020
|
```ruby
|
|
1016
1021
|
class PostDefinition < ResourceDefinition
|
|
@@ -1037,8 +1042,8 @@ See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms)
|
|
|
1037
1042
|
|
|
1038
1043
|
## Related
|
|
1039
1044
|
|
|
1040
|
-
- [Query](./query)
|
|
1041
|
-
- [Actions](./actions)
|
|
1042
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
1043
|
-
- [UI › Forms](/reference/ui/forms)
|
|
1044
|
-
- [UI › Pages](/reference/ui/pages)
|
|
1045
|
+
- [Query](./query): search, filters, scopes, sorting
|
|
1046
|
+
- [Actions](./actions): custom + bulk actions
|
|
1047
|
+
- [Behavior › Policy](/reference/behavior/policies): `permitted_attributes_for_*`, authorization
|
|
1048
|
+
- [UI › Forms](/reference/ui/forms): field builder, association inputs, theming
|
|
1049
|
+
- [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
|