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
data/docs/reference/ui/forms.md
CHANGED
|
@@ -4,7 +4,7 @@ Built on [Phlexi::Form](https://github.com/radioactive-labs/phlexi-form). Overri
|
|
|
4
4
|
|
|
5
5
|
## 🚨 Critical
|
|
6
6
|
|
|
7
|
-
- **`render_actions` is mandatory in custom `form_template
|
|
7
|
+
- **`render_actions` is mandatory in custom `form_template`**: without it, the form has no submit button.
|
|
8
8
|
- **Configure inputs in the definition, render them with `render_resource_field`** in the form template. Don't reimplement field widgets from scratch.
|
|
9
9
|
- **Override via nested classes** (`class Form < Form; end`) inside the definition. Don't replace the root `Plutonium::UI::Form::Resource` class.
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ class PostDefinition < ResourceDefinition
|
|
|
25
25
|
class Form < Form
|
|
26
26
|
def form_template
|
|
27
27
|
render_fields # render every permitted field
|
|
28
|
-
render_actions # submit buttons
|
|
28
|
+
render_actions # submit buttons, REQUIRED
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -47,9 +47,9 @@ end
|
|
|
47
47
|
|
|
48
48
|
## Custom layouts
|
|
49
49
|
|
|
50
|
-
### Sectioned form (declarative
|
|
50
|
+
### Sectioned form (declarative: preferred)
|
|
51
51
|
|
|
52
|
-
Declare sections in the **definition** using `form_layout`. The form picks up the layout automatically
|
|
52
|
+
Declare sections in the **definition** using `form_layout`. The form picks up the layout automatically, no `Form` subclass needed for common cases.
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
55
|
class PostDefinition < ResourceDefinition
|
|
@@ -66,11 +66,11 @@ class PostDefinition < ResourceDefinition
|
|
|
66
66
|
end
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
This handles headings, collapsible panels, per-section column counts, and `condition:`-based visibility
|
|
69
|
+
This handles headings, collapsible panels, per-section column counts, and `condition:`-based visibility, all with no view code. See [Resource › Definition › Form layout](/reference/resource/definition#form-layout) for the full DSL reference, including `ungrouped`, `condition:`, `columns:`, and the "On interactions" note.
|
|
70
70
|
|
|
71
71
|
### Full control: override `render_fields`
|
|
72
72
|
|
|
73
|
-
When the declarative DSL doesn't cover your use case
|
|
73
|
+
When the declarative DSL doesn't cover your use case (asymmetric multi-column layouts, embedding a panel widget between sections, etc.) override `render_fields` in a nested `Form` class:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
76
|
class PostDefinition < ResourceDefinition
|
|
@@ -109,7 +109,7 @@ class PostDefinition < ResourceDefinition
|
|
|
109
109
|
end
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Prefer `form_layout` in the definition
|
|
112
|
+
Prefer `form_layout` in the definition; it keeps layout config out of view code and works for interactions too.
|
|
113
113
|
|
|
114
114
|
### Two-column layout
|
|
115
115
|
|
|
@@ -137,7 +137,7 @@ end
|
|
|
137
137
|
|
|
138
138
|
## Field builder (`field(:foo).input_tag`)
|
|
139
139
|
|
|
140
|
-
`render_resource_field` uses the input config from the definition. For ad-hoc rendering
|
|
140
|
+
`render_resource_field` uses the input config from the definition. For ad-hoc rendering (when you want fine-grained control over a specific field) use `field(...) ` directly:
|
|
141
141
|
|
|
142
142
|
```ruby
|
|
143
143
|
render field(:title).wrapped { |f| f.input_tag } # wrapped: label + hint + errors
|
|
@@ -150,9 +150,9 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
150
150
|
| Tag | Input |
|
|
151
151
|
|---|---|
|
|
152
152
|
| `input_tag` | text (auto-detected type) |
|
|
153
|
-
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `
|
|
154
|
-
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
155
|
-
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`)
|
|
153
|
+
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `hidden_tag` | standard HTML inputs |
|
|
154
|
+
| `checkbox_tag`, `select_tag`, `radio_button_tag`, `collection_radio_buttons_tag`, `collection_checkboxes_tag` | standard |
|
|
155
|
+
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`), the **default** for boolean columns; same behavior as a checkbox. Use `checkbox_tag` (`as: :boolean`) for a plain checkbox. |
|
|
156
156
|
|
|
157
157
|
### Plutonium-enhanced tags
|
|
158
158
|
|
|
@@ -164,12 +164,17 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
164
164
|
| `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
|
|
165
165
|
| `currency_tag` | Money input (number field + optional unit prefix) |
|
|
166
166
|
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
167
|
+
| `binary_tag` | Plain file input whose bytes go into a `binary` column |
|
|
168
|
+
| `rich_text_tag` | Action Text editor (Trix, or Lexxy once installed) |
|
|
169
|
+
| `list_tag` | Chip input for array values |
|
|
170
|
+
| `tags_tag` | Chip input for a no_fly_list `has_tags` context |
|
|
171
|
+
| `rating_tag` | Star picker (radio buttons, no JS); an optional rating gets a Clear option |
|
|
167
172
|
| `secure_association_tag` | Association with policy-checked options (inline `+` add, typeahead) |
|
|
168
173
|
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
169
174
|
| `key_value_store_tag` | Key/value pairs editor |
|
|
170
175
|
|
|
171
176
|
```ruby
|
|
172
|
-
render field(:published_at).wrapped { |f| f.flatpickr_tag
|
|
177
|
+
render field(:published_at).wrapped { |f| f.flatpickr_tag }
|
|
173
178
|
|
|
174
179
|
render field(:avatar).wrapped do |f|
|
|
175
180
|
f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes)
|
|
@@ -180,8 +185,8 @@ end
|
|
|
180
185
|
|
|
181
186
|
`as: :phone` renders an [intl-tel-input](https://github.com/jackocnr/intl-tel-input) field. Forward library options two ways:
|
|
182
187
|
|
|
183
|
-
- `initial_country
|
|
184
|
-
- `intl_options
|
|
188
|
+
- `initial_country:`: a convenient shortcut for the library's `initialCountry` (ISO2, e.g. `"gh"`). This preselects a country so the widget doesn't show *"No country selected"* and a bare local number validates.
|
|
189
|
+
- `intl_options:`: any other library option, using the library's own camelCase names. Merged over the shortcut, so it wins on conflict.
|
|
185
190
|
|
|
186
191
|
```ruby
|
|
187
192
|
input :phone, as: :phone, initial_country: "gh"
|
|
@@ -207,14 +212,14 @@ The field defaults to `strictMode: true`; override it via `intl_options: {strict
|
|
|
207
212
|
```ruby
|
|
208
213
|
input :price, as: :currency # unit from has_cents / config / i18n
|
|
209
214
|
input :price, as: :currency, unit: "£" # a literal symbol
|
|
210
|
-
input :price, as: :currency, unit: false # no prefix
|
|
215
|
+
input :price, as: :currency, unit: false # no prefix, a plain number input
|
|
211
216
|
```
|
|
212
217
|
|
|
213
218
|
When nothing resolves (or `unit: false`), the prefix is omitted and it's an ordinary number input.
|
|
214
219
|
|
|
215
|
-
**`has_cents` fields infer it automatically
|
|
220
|
+
**`has_cents` fields infer it automatically**: just like the display. A bare `input :price` on a `has_cents` attribute renders the currency input with the unit read off `has_cents`; no `as: :currency` needed. Use the explicit `as: :currency` for a non-`has_cents` decimal, or to override the unit.
|
|
216
221
|
|
|
217
|
-
In a **wizard** step the data snapshot has no `has_cents` reflection, so there's nothing to infer from
|
|
222
|
+
In a **wizard** step the data snapshot has no `has_cents` reflection, so there's nothing to infer from; declare `as: :currency` and pass `unit:` explicitly (`input :price, as: :currency, unit: "$"`); the review summary reads it back and formats the value as currency.
|
|
218
223
|
|
|
219
224
|
### Password & secret fields {#password-fields}
|
|
220
225
|
|
|
@@ -222,10 +227,10 @@ In a **wizard** step the data snapshot has no `has_cents` reflection, so there's
|
|
|
222
227
|
|
|
223
228
|
| field state | result |
|
|
224
229
|
|---|---|
|
|
225
|
-
| untouched (sentinel) | kept
|
|
230
|
+
| untouched (sentinel) | kept, the stored secret is left unchanged |
|
|
226
231
|
| edited to a new value, then failed re-render | comes back **blank + `required`** so the user re-types it (a submitted secret is never echoed back) |
|
|
227
|
-
| cleared, then failed re-render | comes back blank, **not** `required
|
|
228
|
-
| emptied | explicit clear (clear-by-blank)
|
|
232
|
+
| cleared, then failed re-render | comes back blank, **not** `required`; the clear may be intentional, so it's allowed to stand |
|
|
233
|
+
| emptied | explicit clear (clear-by-blank), the `required` guard only prevents an *accidental* blank submit |
|
|
229
234
|
| typed | set as the new value |
|
|
230
235
|
|
|
231
236
|
The sentinel is guarded client-side by the `password-sentinel` Stimulus controller: the first edit (a keystroke, paste, or **backspace**) wipes the whole field, so a partial edit can't corrupt the sentinel into a literal new password. New records and interaction forms (set-password, reset-password) render an honest empty field.
|
|
@@ -237,33 +242,78 @@ The sentinel is guarded client-side by the `password-sentinel` Stimulus controll
|
|
|
237
242
|
- ends with `_password`, `_digest`, `_hash`, `_token`, `_key`, or `_salt`;
|
|
238
243
|
- contains `secret`.
|
|
239
244
|
|
|
240
|
-
|
|
245
|
+
The same names are masked everywhere the value would otherwise print: the show page, table cells, the wizard summary, and grid and kanban card slots all render `••••••••` instead of the value. A `field`/`display` `as:` overrides the heuristic on those surfaces too.
|
|
246
|
+
|
|
247
|
+
This is a naming convenience, **not** a security guarantee, tune it per field:
|
|
241
248
|
|
|
242
249
|
```ruby
|
|
243
250
|
# Opt OUT: render the value as a normal, readable text input
|
|
244
|
-
field :api_token, as: :string # a token the admin needs to copy
|
|
251
|
+
field :api_token, as: :string # a token the admin needs to copy (shown on the show page and cards too)
|
|
245
252
|
field :content_hash, as: :string # a checksum, not a secret
|
|
246
253
|
field :public_key, as: :string # *_key matches, but a public key is not secret
|
|
247
254
|
|
|
248
255
|
# Opt IN: mask a secret the heuristic still misses (e.g. no telltale name)
|
|
249
256
|
field :recovery_phrase, as: :password
|
|
257
|
+
field :pin, as: :secret # alias of :password
|
|
250
258
|
```
|
|
251
259
|
|
|
252
260
|
> [!WARNING]
|
|
253
261
|
> The heuristic is name-based and best-effort. A secret column with an unconventional name (e.g. `recovery_phrase`, `pin`) still renders its value into the page unless you set `as: :password`. Audit secret-bearing columns explicitly.
|
|
254
262
|
|
|
263
|
+
### Rich text (Action Text and Lexxy) {#rich-text-fields}
|
|
264
|
+
|
|
265
|
+
A `has_rich_text` field renders Action Text's own editor tag, so it gets whichever editor the app has set up: Trix by default, or [Lexxy](https://lexxy.dev) once the `lexxy` gem is in the bundle (it takes over Action Text's helpers on Rails 8.0/8.1 and registers itself as the editor on 8.2). Lexxy needs Rails 8.0.2 or later. The editor's JavaScript and CSS come from the app's bundle, as Action Text's or Lexxy's install instructions describe. The show page renders the HTML through Action Text, which sanitizes it.
|
|
266
|
+
|
|
267
|
+
### Lists and tags {#list-fields}
|
|
268
|
+
|
|
269
|
+
`as: :list` edits an array value (a JSON or array column) as chips. Options go on `input`:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
field :nicknames, as: :list
|
|
273
|
+
input :nicknames, suggestions: %w[Basin Tub], limit: 5 # addable: false allows only suggestions
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Removing every chip saves an empty array.
|
|
277
|
+
|
|
278
|
+
[no_fly_list](https://github.com/radioactive-labs/no_fly_list) `has_tags` contexts are detected automatically. Permit them by context name (`labels`, not `labels_list`) and they render as chips on the form, the show page and the table:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class Article < ResourceRecord
|
|
282
|
+
include NoFlyList::TaggableRecord
|
|
283
|
+
has_tags :labels, scope: :organization # per-tenant vocabulary
|
|
284
|
+
has_tags :topics, restrict_to_existing: true, limit: 3
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# policy
|
|
288
|
+
def permitted_attributes_for_create = %i[title labels topics]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- The input suggests tags that already exist in the record's tag scope.
|
|
292
|
+
- `restrict_to_existing` only allows picking existing tags. Plutonium doesn't decide where that vocabulary is managed: create the tags however suits the app (register the tag model as a resource, seed them, or add them in an interaction).
|
|
293
|
+
- `limit` caps how many tags can be chosen.
|
|
294
|
+
- Add `filter :labels, with: :tags` for a table filter.
|
|
295
|
+
- The table reads tags through the context's association, so preload it (`includes(:labels)`) to avoid a query per row.
|
|
296
|
+
|
|
297
|
+
### Binary columns {#binary-fields}
|
|
298
|
+
|
|
299
|
+
A `binary` column renders `binary_tag`: a plain file input. The uploaded file's bytes are written to the column, and the show page and table print its size (`Binary data (12 KB)`), never the bytes.
|
|
300
|
+
|
|
301
|
+
- Saving without choosing a file keeps the stored bytes. There is no control to clear the column.
|
|
302
|
+
- Browsers can't refill a file input, so after a failed validation the user has to choose the file again.
|
|
303
|
+
- The whole file is read into memory and stored in the row. Use Active Storage (`has_one_attached` + `:uppy`) for anything that isn't small.
|
|
304
|
+
|
|
255
305
|
### Wrapped vs unwrapped
|
|
256
306
|
|
|
257
|
-
- `wrapped
|
|
258
|
-
- Bare tag
|
|
259
|
-
- `wrapped(class: "...")
|
|
307
|
+
- `wrapped`: includes label, hint, and error rendering. Use for normal form fields.
|
|
308
|
+
- Bare tag, just the input element. Use when you're laying out custom wrappers.
|
|
309
|
+
- `wrapped(class: "...")`: pass classes to the wrapper div.
|
|
260
310
|
|
|
261
311
|
## Association inputs (`secure_association_tag`) {#association-inputs}
|
|
262
312
|
|
|
263
313
|
Association inputs render with two affordances out of the box:
|
|
264
314
|
|
|
265
|
-
- **Inline `+` add
|
|
266
|
-
- **Typeahead
|
|
315
|
+
- **Inline `+` add**: a button next to the select opens the target resource's `:new` action. Inherits the target's modal mode. If the parent form is already in a modal, the `+` opens a **stacked secondary modal** (see [Pages › Stacked modals](./pages#stacked-modals-secondary-frame)) so the in-progress form isn't lost; on success the secondary closes and the parent reloads.
|
|
316
|
+
- **Typeahead**: server-side autocomplete is on by default. Uses the target's `search` block if defined; otherwise falls back to a `LIKE` on the input's `label_method:` column or the first match from `[name, title, label, slug, display_name, email]`. See [Resource › Query › Search](/reference/resource/query#search) for the typeahead fallback details.
|
|
267
317
|
|
|
268
318
|
```ruby
|
|
269
319
|
# Opt out of the + button
|
|
@@ -280,7 +330,7 @@ input :author, label_method: :email
|
|
|
280
330
|
```
|
|
281
331
|
|
|
282
332
|
::: tip Large association tables
|
|
283
|
-
For large target tables, write an explicit `search` block on the target resource definition
|
|
333
|
+
For large target tables, write an explicit `search` block on the target resource definition; the fallback's leading-wildcard `LIKE` can't use a b-tree index.
|
|
284
334
|
:::
|
|
285
335
|
|
|
286
336
|
## Submit buttons
|
|
@@ -291,7 +341,7 @@ Control the secondary button via the definition:
|
|
|
291
341
|
|
|
292
342
|
```ruby
|
|
293
343
|
class PostDefinition < ResourceDefinition
|
|
294
|
-
submit_and_continue false # nil (default
|
|
344
|
+
submit_and_continue false # nil (default, auto), true (always show), false (always hide)
|
|
295
345
|
end
|
|
296
346
|
```
|
|
297
347
|
|
|
@@ -313,9 +363,9 @@ end
|
|
|
313
363
|
|
|
314
364
|
These all live in the definition layer:
|
|
315
365
|
|
|
316
|
-
- **Pre-submit / dynamic forms
|
|
317
|
-
- **Nested inputs** (`nested_input :variants`)
|
|
318
|
-
- **Interaction forms
|
|
366
|
+
- **Pre-submit / dynamic forms**: see [Resource › Definition › Dynamic forms](/reference/resource/definition#dynamic-forms-pre-submit)
|
|
367
|
+
- **Nested inputs** (`nested_input :variants`), see [Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs)
|
|
368
|
+
- **Interaction forms**: interactions define their own `attribute` / `input` and inherit `Plutonium::UI::Form::Interaction`; see [Behavior › Interactions](/reference/behavior/interactions)
|
|
319
369
|
|
|
320
370
|
## Theming
|
|
321
371
|
|
|
@@ -342,7 +392,7 @@ end
|
|
|
342
392
|
```
|
|
343
393
|
|
|
344
394
|
::: warning Always `super.merge(...)`
|
|
345
|
-
Don't replace the theme wholesale
|
|
395
|
+
Don't replace the theme wholesale, Plutonium's defaults handle invalid states, focus rings, and dark mode. `super.merge` keeps them.
|
|
346
396
|
:::
|
|
347
397
|
|
|
348
398
|
### Theme keys
|
|
@@ -384,9 +434,9 @@ end
|
|
|
384
434
|
|
|
385
435
|
## Related
|
|
386
436
|
|
|
387
|
-
- [Pages](./pages)
|
|
388
|
-
- [Components](./components)
|
|
389
|
-
- [Assets](./assets)
|
|
390
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
391
|
-
- [Behavior › Interactions](/reference/behavior/interactions)
|
|
392
|
-
- [Tenancy › Nested resources](/reference/tenancy/nested-resources)
|
|
437
|
+
- [Pages](./pages): `NewPage` / `EditPage` page hooks
|
|
438
|
+
- [Components](./components): building reusable Phlex components for forms
|
|
439
|
+
- [Assets](./assets): `.pu-*` classes, design tokens, dark mode
|
|
440
|
+
- [Resource › Definition](/reference/resource/definition): input configuration (`as:`, `hint:`, `condition:`, blocks)
|
|
441
|
+
- [Behavior › Interactions](/reference/behavior/interactions): interaction forms (`Plutonium::UI::Form::Interaction`)
|
|
442
|
+
- [Tenancy › Nested resources](/reference/tenancy/nested-resources): parent fields hidden by URL
|
data/docs/reference/ui/index.md
CHANGED
|
@@ -4,27 +4,29 @@ Plutonium uses [Phlex](https://www.phlex.fun/) for all view components and Tailw
|
|
|
4
4
|
|
|
5
5
|
## Sub-pages
|
|
6
6
|
|
|
7
|
-
- [Pages](./pages)
|
|
8
|
-
- [Forms](./forms)
|
|
9
|
-
- [Displays](./displays)
|
|
10
|
-
- [Tables](./tables)
|
|
11
|
-
- [Components](./components)
|
|
12
|
-
- [Layouts](./layouts)
|
|
13
|
-
- [Assets](./assets)
|
|
7
|
+
- [Pages](./pages): `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
|
|
8
|
+
- [Forms](./forms): `Form` class, field builder, association inputs (typeahead + inline add), themes
|
|
9
|
+
- [Displays](./displays): `Display` class, custom rendering, block-form displays
|
|
10
|
+
- [Tables](./tables): `Table` class, custom rendering, search/scopes bar
|
|
11
|
+
- [Components](./components): built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
|
|
12
|
+
- [Layouts](./layouts): shell config, ejecting chrome, custom `ResourceLayout` class
|
|
13
|
+
- [Assets](./assets): Tailwind config, Stimulus controllers, design tokens, `.pu-*` component classes, Phlexi themes
|
|
14
14
|
|
|
15
15
|
## 🚨 Critical (applies across all sub-pages)
|
|
16
16
|
|
|
17
17
|
- **Override via nested classes in the definition.** `class ShowPage < ShowPage; end`, `class Form < Form; end`. Don't replace the entire view layer.
|
|
18
18
|
- **Use render hooks, not `view_template`.** `render_before_content`, `render_after_content`, `render_before_toolbar`, etc. exist so you don't reimplement the whole page.
|
|
19
|
-
- **All pages inherit `DynaFrameContent
|
|
20
|
-
- **Custom components inherit `Plutonium::UI::Component::Base
|
|
21
|
-
- **`render_actions` is mandatory in custom `form_template
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
19
|
+
- **All pages inherit `DynaFrameContent`**: turbo-frame requests render only the content. Don't fight it; modals and frame nav "just work".
|
|
20
|
+
- **Custom components inherit `Plutonium::UI::Component::Base`**: gives you the component kit (`PageHeader`, `Panel`, `Block`), resource helpers, and the `helpers` proxy for Rails helpers.
|
|
21
|
+
- **`render_actions` is mandatory in custom `form_template`**: without it, the form has no submit button.
|
|
22
|
+
- **Custom CSS, brand colors, or your own Stimulus controllers need `pu:core:assets` first.** Out of the box the app serves the gem's prebuilt `plutonium.css` / `plutonium.min.js`; the generator switches it to your own bundles. Don't hand-write the Tailwind/PostCSS pipeline.
|
|
23
|
+
- **Once the app owns its JS bundle, `registerControllers(application)`** must be in `app/javascript/controllers/index.js` (`pu:core:assets` adds it). Your bundle replaces the gem's, so without it Plutonium's Stimulus controllers (color-mode, form, slim-select, flatpickr, easymde, etc.) are dead.
|
|
24
|
+
- **Use `plutoniumTailwindConfig.merge`** when extending Tailwind theme, plain object merge drops Plutonium's defaults.
|
|
25
|
+
- **Style with `.pu-*` classes first, `var(--pu-*)` tokens second, raw palette pairs last.** Banners, badges, cards and buttons all have a `.pu-*` class that carries its own `.dark` rule. A hand-written `bg-warning-50 dark:bg-warning-950/30` pair duplicates that, drifts from the theme, and may not even exist in the prebuilt CSS.
|
|
26
|
+
- **User-facing copy goes through `t(...)` with a locale key**, in components, pages, displays and definitions alike. See [i18n](/reference/i18n).
|
|
25
27
|
- **Configure inputs in the definition; render them with `render_resource_field` in the form.** Don't reimplement field widgets from scratch.
|
|
26
28
|
|
|
27
29
|
## Related
|
|
28
30
|
|
|
29
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
30
|
-
- [Behavior › Controllers](/reference/behavior/controllers)
|
|
31
|
+
- [Resource › Definition](/reference/resource/definition): field-level rendering (`field :foo, as: :markdown`, `display :status do |f| … end`)
|
|
32
|
+
- [Behavior › Controllers](/reference/behavior/controllers): controller render-context hooks (`present_parent?`, `submit_parent?`)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Layouts
|
|
2
2
|
|
|
3
|
-
The overall page chrome
|
|
3
|
+
The overall page chrome, topbar, sidebar, footer, body wrapping. Plutonium ships three shells; you can eject the templates or write a custom `ResourceLayout` for total control.
|
|
4
4
|
|
|
5
5
|
## Shell
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
Plutonium.configure do |config|
|
|
9
|
-
config.shell = :modern # default
|
|
9
|
+
config.shell = :modern # default, topbar + icon rail
|
|
10
10
|
# config.shell = :plain # topbar, no icon rail (rail-less app)
|
|
11
11
|
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
12
12
|
end
|
|
@@ -20,9 +20,9 @@ If you're starting fresh, use `:modern`. `:classic` exists so apps upgrading fro
|
|
|
20
20
|
|
|
21
21
|
The shell variant selects the page chrome:
|
|
22
22
|
|
|
23
|
-
- `:modern` (default)
|
|
24
|
-
- `:plain
|
|
25
|
-
- `:classic
|
|
23
|
+
- `:modern` (default), Topbar plus the desktop icon rail.
|
|
24
|
+
- `:plain`: Topbar but **no** icon rail. The Topbar is kept; only the rail is removed, so the surface is rail-less.
|
|
25
|
+
- `:classic`: legacy Header/Sidebar (upgrade paths only).
|
|
26
26
|
|
|
27
27
|
### Resolving the shell (global → engine → controller)
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ The shell resolves across three layers, each overriding the one above it:
|
|
|
32
32
|
# 1. Global default (config/initializers/plutonium.rb)
|
|
33
33
|
Plutonium.configure { |config| config.shell = :modern }
|
|
34
34
|
|
|
35
|
-
# 2. Per-engine
|
|
35
|
+
# 2. Per-engine: set it on a portal engine (lib/engine.rb)
|
|
36
36
|
module CustomerPortal
|
|
37
37
|
class Engine < Rails::Engine
|
|
38
38
|
include Plutonium::Portal::Engine
|
|
@@ -43,19 +43,19 @@ module CustomerPortal
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# 3. Per-controller
|
|
46
|
+
# 3. Per-controller: overrides the engine/global default for one controller (and subclasses)
|
|
47
47
|
class DashboardController < ResourceController
|
|
48
48
|
shell :modern # opt this controller back into the rail
|
|
49
49
|
end
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`shell` takes a plain symbol, so it's safe in the class body too
|
|
52
|
+
`shell` takes a plain symbol, so it's safe in the class body too, but the generated engine already has a `config.after_initialize` block (where `scope_to_entity` lives), so keeping it there is the consistent home.
|
|
53
53
|
|
|
54
54
|
An unset engine/controller value (`nil`) falls through to the next layer. Read the resolved value with the `shell` helper (`controller.shell`).
|
|
55
55
|
|
|
56
|
-
### `rail
|
|
56
|
+
### `rail`: a controller-level rail toggle
|
|
57
57
|
|
|
58
|
-
Alongside `shell`, any resource controller exposes a class-level `rail` DSL that flips just the icon rail without changing the shell. It's a `class_attribute`, so it's inherited
|
|
58
|
+
Alongside `shell`, any resource controller exposes a class-level `rail` DSL that flips just the icon rail without changing the shell. It's a `class_attribute`, so it's inherited; a portal opts its entire surface in or out by calling `rail false` (or `rail true`) once in its controller concern:
|
|
59
59
|
|
|
60
60
|
```ruby
|
|
61
61
|
module CustomerPortal
|
|
@@ -68,15 +68,15 @@ module CustomerPortal
|
|
|
68
68
|
end
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`rail nil` (the default) inherits the resolved shell
|
|
71
|
+
`rail nil` (the default) inherits the resolved shell, the rail shows when the resolved `shell == :modern`. Read the resolved value with the `rail?` predicate.
|
|
72
72
|
|
|
73
73
|
### Stable CSS hooks
|
|
74
74
|
|
|
75
75
|
Rail-less rendering exposes a few stable hooks for custom overrides:
|
|
76
76
|
|
|
77
|
-
- `pu-topbar
|
|
78
|
-
- `pu-sticky-footer
|
|
79
|
-
- `html.pu-no-rail
|
|
77
|
+
- `pu-topbar`: class on the Topbar nav.
|
|
78
|
+
- `pu-sticky-footer`: class on the form sticky-footer div.
|
|
79
|
+
- `html.pu-no-rail`: root class present whenever the current page is rail-less.
|
|
80
80
|
|
|
81
81
|
A built-in rule cancels the desktop rail inset on `.pu-topbar` and `.pu-sticky-footer` under `html.pu-no-rail`; target these hooks to layer your own CSS.
|
|
82
82
|
|
|
@@ -87,7 +87,7 @@ rails generate pu:eject:shell --dest=admin_portal
|
|
|
87
87
|
rails generate pu:eject:layout
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
`pu:eject:shell` copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`. The eject is independent of `shell
|
|
90
|
+
`pu:eject:shell` copies `_resource_header.html.erb` and `_resource_sidebar.html.erb` into the portal's `app/views/plutonium/`. The eject is independent of `shell`; you can run it on either.
|
|
91
91
|
|
|
92
92
|
`pu:eject:layout` copies `layouts/resource.html.erb` for layout-level edits.
|
|
93
93
|
|
|
@@ -111,7 +111,7 @@ The sidebar/icon-rail navigation is built with `Phlexi::Menu::Builder` in the ej
|
|
|
111
111
|
|
|
112
112
|
### Per-item link attributes
|
|
113
113
|
|
|
114
|
-
Any extra options you pass to `item` are spread straight onto the rendered `<a
|
|
114
|
+
Any extra options you pass to `item` are spread straight onto the rendered `<a>`, so a menu entry can opt into `target`, `rel`, `data-*`, `aria-*`, etc. Useful for items that open in their own tab or drive a Stimulus/Turbo behavior:
|
|
115
115
|
|
|
116
116
|
```ruby
|
|
117
117
|
m.item "Inbox",
|
|
@@ -122,7 +122,7 @@ m.item "Inbox",
|
|
|
122
122
|
data: {turbo_frame: "_top"}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
This works across both shells
|
|
125
|
+
This works across both shells, the `:modern` icon-rail (leaf items, parent flyout triggers, and flyout children) and the `:classic` sidebar. Framework attributes always win on conflict: a custom `class:` is **merged** with the component's base classes, and on a parent trigger your `data:` / `aria:` merge with the flyout's own wiring (so you can't accidentally break the toggle). The `:active` key is reserved by Phlexi for [custom active-state logic](https://github.com/radioactive-labs/phlexi-menu) and is never emitted as an attribute.
|
|
126
126
|
|
|
127
127
|
## Custom layout class
|
|
128
128
|
|
|
@@ -190,7 +190,7 @@ See [Assets › Tailwind config](./assets#tailwind-config) for the full merge st
|
|
|
190
190
|
|
|
191
191
|
## Dark mode
|
|
192
192
|
|
|
193
|
-
`selector` strategy
|
|
193
|
+
`selector` strategy, toggle by adding/removing `dark` on `<html>`. The bundled `color-mode` Stimulus controller handles toggling; Plutonium ships a switcher.
|
|
194
194
|
|
|
195
195
|
```javascript
|
|
196
196
|
// Manual toggle if needed
|
|
@@ -199,6 +199,6 @@ document.documentElement.classList.toggle('dark')
|
|
|
199
199
|
|
|
200
200
|
## Related
|
|
201
201
|
|
|
202
|
-
- [Assets](./assets)
|
|
203
|
-
- [Components](./components)
|
|
204
|
-
- [Pages](./pages)
|
|
202
|
+
- [Assets](./assets): Tailwind config, design tokens, `.pu-*` classes
|
|
203
|
+
- [Components](./components): custom components used in layout hooks (`AnnouncementBanner`, etc.)
|
|
204
|
+
- [Pages](./pages): page-level hooks (a lighter alternative for per-page chrome)
|
data/docs/reference/ui/pages.md
CHANGED
|
@@ -32,12 +32,10 @@ end
|
|
|
32
32
|
|
|
33
33
|
```ruby
|
|
34
34
|
class PostDefinition < ResourceDefinition
|
|
35
|
-
index_page_title "
|
|
36
|
-
index_page_description "
|
|
37
|
-
show_page_title "Article Details"
|
|
38
|
-
|
|
39
|
-
new_page_title "Create Post"
|
|
40
|
-
edit_page_title -> { "Edit: #{current_record!.title}" }
|
|
35
|
+
index_page_title t("blog.posts.index.title") # lazy: resolved per request, in its locale
|
|
36
|
+
index_page_description t("blog.posts.index.description")
|
|
37
|
+
show_page_title "Article Details" # a literal also works (fixed, untranslated)
|
|
38
|
+
new_page_title t("blog.posts.new.title")
|
|
41
39
|
|
|
42
40
|
breadcrumbs true # global default
|
|
43
41
|
index_page_breadcrumbs false # per-page override
|
|
@@ -48,9 +46,11 @@ class PostDefinition < ResourceDefinition
|
|
|
48
46
|
end
|
|
49
47
|
```
|
|
50
48
|
|
|
49
|
+
The class-level `t` in a definition returns a lazy proc; calling `I18n.t` directly in the class body would resolve once, at load time. A title that depends on the record belongs in a `page_title` override on the nested page class (see the hooks example below), not in a lambda here: the setter's proc is called with no record context.
|
|
50
|
+
|
|
51
51
|
## Page hooks (preferred over `view_template`)
|
|
52
52
|
|
|
53
|
-
Every page inherits these
|
|
53
|
+
Every page inherits these, use them instead of overriding `view_template` to preserve breadcrumbs, header, and DynaFrame behavior:
|
|
54
54
|
|
|
55
55
|
| Hook | Position |
|
|
56
56
|
|---|---|
|
|
@@ -68,12 +68,12 @@ class ShowPage < ShowPage
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
70
|
def page_title
|
|
71
|
-
"#{object.title}
|
|
71
|
+
"#{object.title}, #{object.author.name}"
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def render_before_content
|
|
75
|
-
div(class: "alert alert-info") do
|
|
76
|
-
|
|
75
|
+
div(class: "pu-alert pu-alert-info", role: "status") do
|
|
76
|
+
div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -105,7 +105,7 @@ The default view simply renders the page class:
|
|
|
105
105
|
<%= render current_definition.show_page_class.new %>
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
Mix
|
|
108
|
+
Mix, keep the default and add chrome around it:
|
|
109
109
|
|
|
110
110
|
```erb
|
|
111
111
|
<div class="announcement-banner">Special announcement</div>
|
|
@@ -125,7 +125,7 @@ Use to pin action strips, omit nav chrome, or swap layouts.
|
|
|
125
125
|
|
|
126
126
|
### Stacked modals (secondary frame)
|
|
127
127
|
|
|
128
|
-
Association inputs include an inline `+` button. When the parent form is itself rendered in a modal, the `+` opens a **second stacked modal** in `Plutonium::REMOTE_MODAL_SECONDARY_FRAME` instead of replacing the primary modal. On successful create, the secondary closes and the primary frame reloads so the new record appears in the select
|
|
128
|
+
Association inputs include an inline `+` button. When the parent form is itself rendered in a modal, the `+` opens a **second stacked modal** in `Plutonium::REMOTE_MODAL_SECONDARY_FRAME` instead of replacing the primary modal. On successful create, the secondary closes and the primary frame reloads so the new record appears in the select, no developer wiring.
|
|
129
129
|
|
|
130
130
|
For custom flows: `helpers.turbo_stream_close_frame(frame_id)` and `helpers.turbo_stream_reload_frame(frame_id)` are available.
|
|
131
131
|
|
|
@@ -133,11 +133,11 @@ See [Forms › Association inputs](./forms#association-inputs).
|
|
|
133
133
|
|
|
134
134
|
## Modals & slideovers
|
|
135
135
|
|
|
136
|
-
The framework's `:new` / `:edit` actions and any interactive action render inline inside a modal. Choose the chrome (and optional width) per-resource via the definition
|
|
136
|
+
The framework's `:new` / `:edit` actions and any interactive action render inline inside a modal. Choose the chrome (and optional width) per-resource via the definition, interactive actions inherit the same default:
|
|
137
137
|
|
|
138
138
|
```ruby
|
|
139
139
|
class PostDefinition < ResourceDefinition
|
|
140
|
-
modal :slideover # default
|
|
140
|
+
modal :slideover # default, slide-in panel from the right
|
|
141
141
|
# modal :centered # centered dialog
|
|
142
142
|
# modal :centered, size: :lg # centered, wider container
|
|
143
143
|
# modal false # full standalone pages (no modal)
|
|
@@ -148,7 +148,7 @@ end
|
|
|
148
148
|
|
|
149
149
|
## Tabs on the show page
|
|
150
150
|
|
|
151
|
-
Show pages with `permitted_associations` (see [Behavior › Policy](/reference/behavior/policies#association-permissions)) render a tablist: **Details** tab first, then one tab per association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports
|
|
151
|
+
Show pages with `permitted_associations` (see [Behavior › Policy](/reference/behavior/policies#association-permissions)) render a tablist: **Details** tab first, then one tab per association. The active tab is reflected in the URL hash (`#products`, `#refund-requests`) so the page deep-links and the active state survives reload / back navigation. Tab rows scroll horizontally on narrow viewports, they don't wrap.
|
|
152
152
|
|
|
153
153
|
If the policy permits **no fields**, the empty Details tab is dropped and the first association tab leads instead.
|
|
154
154
|
|
|
@@ -169,7 +169,7 @@ end
|
|
|
169
169
|
|
|
170
170
|
## Available context
|
|
171
171
|
|
|
172
|
-
Inside any page / form / display / Phlex component, the same set of helpers is available
|
|
172
|
+
Inside any page / form / display / Phlex component, the same set of helpers is available (model accessors, definition/policy methods, URL helpers, `current_user`; for the full list, see [Behavior › Controllers › Key methods](/reference/behavior/controllers#key-methods)). Pages inherit the same surface.
|
|
173
173
|
|
|
174
174
|
In Phlex components, Rails helpers are accessed via the `helpers` proxy:
|
|
175
175
|
|
|
@@ -184,9 +184,9 @@ end
|
|
|
184
184
|
|
|
185
185
|
## Related
|
|
186
186
|
|
|
187
|
-
- [Forms](./forms)
|
|
188
|
-
- [Displays](./displays)
|
|
189
|
-
- [Tables](./tables)
|
|
190
|
-
- [Components](./components)
|
|
191
|
-
- [Layouts](./layouts)
|
|
192
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
187
|
+
- [Forms](./forms): Form class, field builder, themes
|
|
188
|
+
- [Displays](./displays): show-page Display class
|
|
189
|
+
- [Tables](./tables): index-page Table class
|
|
190
|
+
- [Components](./components): built-in component kit, custom Phlex components, DynaFrame
|
|
191
|
+
- [Layouts](./layouts): overall shell, eject, ResourceLayout
|
|
192
|
+
- [Resource › Definition](/reference/resource/definition): page titles, breadcrumbs, modal mode, metadata panel
|
data/docs/reference/ui/tables.md
CHANGED
|
@@ -52,11 +52,11 @@ class PostDefinition < ResourceDefinition
|
|
|
52
52
|
column :status, align: :center
|
|
53
53
|
column :amount, align: :end
|
|
54
54
|
|
|
55
|
-
# formatter
|
|
55
|
+
# formatter, receives just the value
|
|
56
56
|
column :description, formatter: ->(value) { value&.truncate(30) }
|
|
57
57
|
column :price, formatter: ->(value) { "$%.2f" % value if value }
|
|
58
58
|
|
|
59
|
-
# block
|
|
59
|
+
# block, receives the full record
|
|
60
60
|
column :full_name do |record|
|
|
61
61
|
"#{record.first_name} #{record.last_name}"
|
|
62
62
|
end
|
|
@@ -67,7 +67,7 @@ See [Resource › Definition › Column options](/reference/resource/definition#
|
|
|
67
67
|
|
|
68
68
|
## Grid view
|
|
69
69
|
|
|
70
|
-
For card-based layouts as a switchable alternative to the table, use the built-in Grid view
|
|
70
|
+
For card-based layouts as a switchable alternative to the table, use the built-in Grid view, declare `grid_fields` in the definition:
|
|
71
71
|
|
|
72
72
|
```ruby
|
|
73
73
|
class UserDefinition < ResourceDefinition
|
|
@@ -115,7 +115,7 @@ end
|
|
|
115
115
|
|
|
116
116
|
## Related
|
|
117
117
|
|
|
118
|
-
- [Pages](./pages)
|
|
119
|
-
- [Components](./components)
|
|
120
|
-
- [Resource › Definition](/reference/resource/definition)
|
|
121
|
-
- [Resource › Query](/reference/resource/query)
|
|
118
|
+
- [Pages](./pages): `IndexPage` render hooks (a lighter alternative for top/bottom chrome)
|
|
119
|
+
- [Components](./components): `PostCardComponent` and other reusable Phlex pieces
|
|
120
|
+
- [Resource › Definition](/reference/resource/definition): column configuration, grid view
|
|
121
|
+
- [Resource › Query](/reference/resource/query): search, filters, scopes, sort
|