plutonium 0.64.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +44 -41
- data/.claude/skills/plutonium-app/SKILL.md +115 -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 +168 -0
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +236 -145
- 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 +171 -95
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +282 -210
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium-charts.js +20604 -0
- data/app/assets/plutonium-charts.js.map +7 -0
- data/app/assets/plutonium-charts.min.js +41 -0
- data/app/assets/plutonium-charts.min.js.map +7 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +794 -629
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +48 -48
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash_alerts.html.erb +2 -2
- data/app/views/plutonium/_resource_header.html.erb +2 -2
- data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
- data/app/views/plutonium/_toast.html.erb +2 -2
- data/app/views/rodauth/_login_form.html.erb +1 -1
- data/app/views/rodauth/_password_visibility.html.erb +1 -1
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +2 -2
- data/app/views/rodauth/create_account.html.erb +4 -4
- data/app/views/rodauth/reset_password_request.html.erb +1 -1
- data/app/views/rodauth/verify_account_resend.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/config/brakeman.ignore +4 -4
- data/config/initializers/rabl.rb +0 -40
- data/config/locales/en/api_client.yml +22 -0
- data/config/locales/en/async.yml +34 -0
- data/config/locales/en/dashboard.yml +25 -0
- data/config/locales/en/invites.yml +37 -0
- data/config/locales/en/js.yml +40 -0
- data/config/locales/en/kanban.yml +28 -0
- data/config/locales/en/plutonium.yml +26 -0
- data/config/locales/en/positioning.yml +14 -0
- data/config/locales/en/profile.yml +29 -0
- data/config/locales/en/resource.yml +60 -0
- data/config/locales/en/ui.yml +93 -0
- data/config/locales/en/wizard.yml +46 -0
- data/docs/.vitepress/config.ts +34 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
- data/docs/.vitepress/theme/custom.css +34 -377
- data/docs/blog/introducing-plutonium-dashboards.md +174 -0
- data/docs/blog/introducing-plutonium-i18n.md +90 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- 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 +300 -0
- data/docs/guides/index.md +4 -3
- data/docs/guides/kanban.md +56 -56
- 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/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/reference/app/generators.md +24 -24
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +77 -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 +88 -31
- data/docs/reference/dashboard/dsl.md +123 -0
- data/docs/reference/dashboard/index.md +35 -0
- data/docs/reference/dashboard/registration.md +76 -0
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +262 -0
- data/docs/reference/index.md +11 -2
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +33 -33
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +18 -16
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +166 -88
- data/docs/reference/resource/export.md +11 -7
- data/docs/reference/resource/index.md +16 -14
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
- data/docs/reference/resource/query.md +14 -12
- 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 +65 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +19 -22
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +45 -45
- 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/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/active_model/validations/array_validator.rb +1 -1
- data/lib/active_model/validations/attached_validator.rb +1 -1
- data/lib/active_model/validations/url_validator.rb +2 -2
- data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
- data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +3 -2
- data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
- data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
- data/lib/generators/pu/docker/install/install_generator.rb +58 -0
- data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
- data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
- data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/invites/invitable_generator.rb +18 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
- data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
- data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
- data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
- data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
- data/lib/plutonium/action/base.rb +27 -3
- data/lib/plutonium/action/interactive.rb +17 -4
- data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
- data/lib/plutonium/auth/sequel_adapter.rb +1 -1
- data/lib/plutonium/configuration.rb +5 -0
- data/lib/plutonium/core/controller.rb +25 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/dashboard/base.rb +114 -0
- data/lib/plutonium/dashboard/card.rb +200 -0
- data/lib/plutonium/dashboard/controller.rb +101 -0
- data/lib/plutonium/dashboard/dsl.rb +104 -0
- data/lib/plutonium/dashboard/register.rb +40 -0
- data/lib/plutonium/dashboard/route_resolution.rb +30 -0
- data/lib/plutonium/dashboard.rb +42 -0
- data/lib/plutonium/definition/actions.rb +6 -1
- data/lib/plutonium/definition/base.rb +41 -3
- data/lib/plutonium/definition/index_views.rb +1 -1
- data/lib/plutonium/definition/positioning.rb +1 -1
- data/lib/plutonium/definition/presentable.rb +2 -2
- data/lib/plutonium/helpers/assets_helper.rb +15 -2
- data/lib/plutonium/helpers/display_helper.rb +10 -2
- data/lib/plutonium/interaction/async/context.rb +6 -8
- data/lib/plutonium/interaction/async/executor.rb +6 -7
- data/lib/plutonium/interaction/async/run.rb +2 -2
- data/lib/plutonium/interaction/async/run_definition.rb +1 -1
- data/lib/plutonium/interaction/base.rb +2 -0
- data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
- data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
- data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
- data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
- data/lib/plutonium/invites/controller.rb +11 -11
- data/lib/plutonium/kanban/column.rb +13 -3
- data/lib/plutonium/kanban/dsl.rb +7 -4
- data/lib/plutonium/models/has_cents.rb +1 -1
- data/lib/plutonium/profile/security_section.rb +10 -18
- data/lib/plutonium/query/filter.rb +10 -1
- data/lib/plutonium/query/filters/association.rb +27 -2
- data/lib/plutonium/query/filters/boolean.rb +4 -4
- data/lib/plutonium/query/filters/date.rb +4 -17
- data/lib/plutonium/query/filters/date_range.rb +2 -2
- data/lib/plutonium/query/filters/select.rb +10 -1
- data/lib/plutonium/query/filters/text.rb +9 -12
- data/lib/plutonium/railtie.rb +17 -0
- data/lib/plutonium/resource/controller.rb +10 -5
- data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
- data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
- data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
- data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
- data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
- data/lib/plutonium/resource/query_object.rb +30 -3
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/routing/dashboard_registration.rb +137 -0
- data/lib/plutonium/routing/resource_registration.rb +5 -0
- data/lib/plutonium/routing/route_set_extensions.rb +2 -1
- data/lib/plutonium/translation.rb +227 -0
- data/lib/plutonium/ui/actions_dropdown.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +3 -3
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/behaviour.rb +46 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/dashboard/board.rb +80 -0
- data/lib/plutonium/ui/dashboard/card.rb +127 -0
- data/lib/plutonium/ui/dashboard/chart.rb +43 -0
- data/lib/plutonium/ui/dashboard/custom.rb +32 -0
- data/lib/plutonium/ui/dashboard/frame.rb +25 -0
- data/lib/plutonium/ui/dashboard/metric.rb +137 -0
- data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
- data/lib/plutonium/ui/display/components/attachment.rb +1 -1
- data/lib/plutonium/ui/display/components/badge.rb +7 -1
- data/lib/plutonium/ui/display/components/boolean.rb +2 -2
- data/lib/plutonium/ui/display/resource.rb +23 -16
- data/lib/plutonium/ui/export_button.rb +3 -3
- data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
- data/lib/plutonium/ui/form/components/password.rb +2 -2
- data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
- data/lib/plutonium/ui/form/components/uppy.rb +11 -3
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/ui/form/query.rb +8 -8
- data/lib/plutonium/ui/form/resource.rb +54 -26
- data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
- data/lib/plutonium/ui/grid/card.rb +3 -3
- data/lib/plutonium/ui/grid/resource.rb +3 -3
- data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
- data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
- data/lib/plutonium/ui/kanban/column.rb +6 -6
- data/lib/plutonium/ui/layout/base.rb +19 -1
- data/lib/plutonium/ui/layout/header.rb +1 -1
- data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/layout/topbar.rb +2 -2
- data/lib/plutonium/ui/modal/base.rb +4 -4
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
- data/lib/plutonium/ui/nav_user.rb +2 -2
- data/lib/plutonium/ui/page/base.rb +10 -2
- data/lib/plutonium/ui/page/dashboard.rb +54 -0
- data/lib/plutonium/ui/page/edit.rb +3 -3
- data/lib/plutonium/ui/page/interactive_action.rb +2 -2
- data/lib/plutonium/ui/page/new.rb +3 -3
- data/lib/plutonium/ui/page/show.rb +2 -2
- data/lib/plutonium/ui/page/wizard.rb +22 -16
- data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
- data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
- data/lib/plutonium/ui/skeleton_table.rb +1 -1
- data/lib/plutonium/ui/table/base.rb +2 -2
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
- data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
- data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
- data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
- data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
- data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
- data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
- data/lib/plutonium/ui/table/resource.rb +53 -17
- data/lib/plutonium/ui/wizard/review.rb +7 -7
- data/lib/plutonium/ui/wizard/stepper.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/base.rb +2 -0
- data/lib/plutonium/wizard/driving.rb +8 -2
- data/lib/plutonium/wizard/dsl.rb +4 -2
- data/lib/plutonium/wizard/field_importer.rb +1 -8
- data/lib/plutonium/wizard/review_step.rb +8 -1
- data/lib/plutonium/wizard/step.rb +14 -3
- data/package.json +4 -2
- data/plutonium.gemspec +4 -1
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +101 -10
- data/src/css/slim_select.css +0 -38
- data/src/css/tokens.css +24 -0
- data/src/js/controllers/attachment_input_controller.js +15 -6
- data/src/js/controllers/chart_controller.js +162 -0
- data/src/js/controllers/clipboard_controller.js +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +4 -3
- data/src/js/controllers/easymde_controller.js +3 -0
- data/src/js/controllers/filter_panel_controller.js +15 -6
- data/src/js/controllers/flatpickr_controller.js +8 -0
- data/src/js/controllers/frame_refresh_controller.js +59 -0
- data/src/js/controllers/intl_tel_input_controller.js +5 -0
- data/src/js/controllers/register_controllers.js +4 -0
- data/src/js/controllers/slim_select_controller.js +79 -176
- data/src/js/core.js +9 -1
- data/src/js/i18n.js +69 -0
- data/src/js/plutonium-charts.js +15 -0
- data/src/js/turbo/turbo_confirm.js +5 -3
- metadata +64 -16
- data/.yarnrc.yml +0 -8
- data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
- data/lib/generators/pu/field/input/input_generator.rb +0 -32
- data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
- data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
- data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
- data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
- data/public/plutonium-assets/application.js +0 -31419
- data/public/plutonium-assets/plutonium.ico +0 -0
- data/yarn.lock +0 -6858
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Definition
|
|
2
2
|
|
|
3
|
-
Definitions configure **how** a resource is rendered and interacted with
|
|
3
|
+
Definitions configure **how** a resource is rendered and interacted with: which fields appear, how they render, what page chrome looks like. Auto-detection from the model handles the defaults; declare only what you're overriding.
|
|
4
4
|
|
|
5
5
|
For search/filters/scopes/sorting see [Query](./query). For custom actions see [Actions](./actions).
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ For search/filters/scopes/sorting see [Query](./query). For custom actions see [
|
|
|
10
10
|
- **Use `condition:` for UI state, the policy for authorization.** `condition: -> { object.published? }` is fine. "Only admins see this field" belongs in `permitted_attributes_for_*`.
|
|
11
11
|
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (see [Behavior › Policy](/reference/behavior/policies)).
|
|
12
12
|
- **`has_cents` fields use the virtual name** (`field :price`), never `:price_cents`.
|
|
13
|
-
- **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to
|
|
13
|
+
- **Nested inputs need `accepts_nested_attributes_for` AND `inverse_of:` on the child's `belongs_to`**, without `inverse_of:`, validation fails with "Parent must exist" because the parent isn't saved yet.
|
|
14
14
|
|
|
15
15
|
## File location
|
|
16
16
|
|
|
@@ -102,7 +102,7 @@ end
|
|
|
102
102
|
| Text | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
103
103
|
| Rich text | `:markdown` (EasyMDE editor) |
|
|
104
104
|
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
105
|
-
| Boolean | `:toggle` / `:switch` (switch
|
|
105
|
+
| Boolean | `:toggle` / `:switch` (switch: **default** for boolean columns), `:boolean` (plain checkbox) |
|
|
106
106
|
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
107
|
| Selection | `:select`, `:slim_select`, `:radio_buttons`, `:check_boxes` |
|
|
108
108
|
| Files | `:file`, `:uppy`, `:attachment` |
|
|
@@ -115,7 +115,7 @@ end
|
|
|
115
115
|
|
|
116
116
|
#### Auto-inferred display formatting
|
|
117
117
|
|
|
118
|
-
These render automatically
|
|
118
|
+
These render automatically; declare an `as:` only to override or pass options:
|
|
119
119
|
|
|
120
120
|
| Column | Renders as | Notes |
|
|
121
121
|
|---|---|---|
|
|
@@ -132,11 +132,11 @@ display :active, as: :boolean, true_label: "Live", false_label: "Off"
|
|
|
132
132
|
**Currency symbol.** The `unit:` can be set on the model's `has_cents` declaration
|
|
133
133
|
(`has_cents :price_cents, unit: "£"`, or `unit: :currency_symbol` to read a method
|
|
134
134
|
off the record for per-row currencies). That model-level unit is used everywhere the
|
|
135
|
-
value renders as currency
|
|
135
|
+
value renders as currency: the show page, tables, **and grid/kanban cards**. A
|
|
136
136
|
per-display `unit:` overrides it for that one display; `unit: false` explicitly
|
|
137
137
|
renders no symbol. When neither is set, currency falls back to
|
|
138
138
|
`Plutonium.configuration.default_currency_unit` (default: the i18n
|
|
139
|
-
`number.currency.format.unit` if the locale defines it
|
|
139
|
+
`number.currency.format.unit` if the locale defines it, `$` in `en`, else no symbol).
|
|
140
140
|
|
|
141
141
|
## Field options
|
|
142
142
|
|
|
@@ -159,6 +159,8 @@ input :title,
|
|
|
159
159
|
wrapper: {class: "col-span-full"}
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
+
Label, hint, placeholder and description need no declaration to be translatable: leave them out and Plutonium looks them up under `activerecord.attributes` and `plutonium.fields.<model>.<attr>` respectively. An explicit value can be a translation too, via the class-level `t`. See [Internationalization](../i18n#fields-placeholder-hint-description).
|
|
163
|
+
|
|
162
164
|
## Select / choices
|
|
163
165
|
|
|
164
166
|
### Static
|
|
@@ -196,23 +198,30 @@ display :rejection_reason, condition: -> { object.rejected? }
|
|
|
196
198
|
field :debug_info, condition: -> { Rails.env.development? }
|
|
197
199
|
```
|
|
198
200
|
|
|
201
|
+
A surface's own `condition:` wins. Otherwise the form, the show page and the table
|
|
202
|
+
each fall back to the `field` condition, so it runs everywhere. The table has no
|
|
203
|
+
`object`, which means a `field` condition must not reference the record:
|
|
204
|
+
`-> { object.published? }` works on the form and show page and raises on the index.
|
|
205
|
+
Keep a `field` condition to context that exists on every surface (`Rails.env`, a
|
|
206
|
+
feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
|
|
207
|
+
|
|
199
208
|
::: warning UI state, not authorization
|
|
200
|
-
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_
|
|
209
|
+
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*`; see [Behavior › Policy](/reference/behavior/policies).
|
|
201
210
|
:::
|
|
202
211
|
|
|
203
212
|
## Options that vary per render
|
|
204
213
|
|
|
205
|
-
Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL
|
|
214
|
+
Any option may be a **proc**, resolved on every render rather than frozen when the class loads. This holds across the whole form DSL: `field`, `input`, `section`/`ungrouped`, `structured_input` and nested inputs. Arity says **whether you want the form**:
|
|
206
215
|
|
|
207
216
|
```ruby
|
|
208
217
|
input :tier, as: :select, choices: ->(form) { form.object.account.available_tiers }
|
|
209
218
|
input :notes, placeholder: -> { "Updated #{Time.current.year}" }
|
|
210
219
|
```
|
|
211
220
|
|
|
212
|
-
- **`-> { … }`** is called as-is, keeping whatever it closed over
|
|
221
|
+
- **`-> { … }`** is called as-is, keeping whatever it closed over: it means what it reads like where you wrote it. Nothing rebinds `self`. That is what lets an option declared inside an interaction's `customize_inputs` reach the interaction, private helpers included: `choices: -> { reviewer_choices }`.
|
|
213
222
|
- **`->(form) { … }`** is handed the form, so `object` (the record being edited), `params` and view helpers are reachable. Use it whenever the value depends on what is being rendered.
|
|
214
223
|
|
|
215
|
-
The rule holds on wizard steps too
|
|
224
|
+
The rule holds on wizard steps too, but there a zero-argument proc closes over an internal field recorder, so options must take the form and read the run off it: `->(form) { form.wizard.anchor.tiers }`. See [Wizard DSL › Runtime input options](/reference/wizard/dsl#runtime-input-options).
|
|
216
225
|
|
|
217
226
|
### `condition:` is not an option
|
|
218
227
|
|
|
@@ -220,10 +229,10 @@ The rule holds on wizard steps too — but there a zero-argument proc closes ove
|
|
|
220
229
|
|
|
221
230
|
| | asks | so it | receiver |
|
|
222
231
|
|---|---|---|---|
|
|
223
|
-
| an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render
|
|
224
|
-
| **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition
|
|
232
|
+
| an **option** (`choices:`, `label:`, `collapsed:`, …) | "what value should this have?" | may or may not care about the render, so it means what it reads like where you wrote it, and takes `form` when it does care | its own closure, or the form |
|
|
233
|
+
| **`condition:`** | "should this render *here, now*?" | is a question about the render context by definition, there is no useful reading of it that ignores that context | always the thing doing the rendering |
|
|
225
234
|
|
|
226
|
-
So `condition:` always runs **against** its context and reads it with no argument
|
|
235
|
+
So `condition:` always runs **against** its context and reads it with no argument, and "its context" is whatever is rendering: the form for a field, section or nested input; the component for a `column` or `display`; the **wizard** for a step's `condition:` (evaluated in the runner to decide which steps exist, before any form is built); a condition context for an action or scope.
|
|
227
236
|
|
|
228
237
|
```ruby
|
|
229
238
|
input :notes, condition: -> { object.published? } # form
|
|
@@ -244,7 +253,7 @@ class QuestionDefinition < ResourceDefinition
|
|
|
244
253
|
choices: %w[text choice scale],
|
|
245
254
|
pre_submit: true
|
|
246
255
|
|
|
247
|
-
# Dependents
|
|
256
|
+
# Dependents: no `as:` needed when the model column type matches
|
|
248
257
|
input :max_length, condition: -> { object.question_type == "text" }
|
|
249
258
|
input :choices, condition: -> { object.question_type == "choice" }
|
|
250
259
|
input :min_value, condition: -> { object.question_type == "scale" }
|
|
@@ -293,29 +302,46 @@ input :birth_date do |f|
|
|
|
293
302
|
end
|
|
294
303
|
```
|
|
295
304
|
|
|
296
|
-
###
|
|
305
|
+
### Inline markup in a display
|
|
297
306
|
|
|
298
|
-
|
|
307
|
+
A `display` block is evaluated in the display component's Phlex context, so HTML tag
|
|
308
|
+
methods (`span`, `div`, `a`) and Tailwind classes are first-class, and `field.object`
|
|
309
|
+
is the record. It renders once, even for a `has_many` field.
|
|
299
310
|
|
|
300
311
|
```ruby
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
display :priority do |field|
|
|
313
|
+
variant = {"high" => "danger", "medium" => "warning"}.fetch(field.value.to_s, "info")
|
|
314
|
+
span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
|
|
315
|
+
end
|
|
303
316
|
|
|
304
|
-
#
|
|
305
|
-
display :
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
when 'medium' then span(class: "badge badge-warning") { "Medium" }
|
|
309
|
-
else span(class: "badge badge-info") { "Low" }
|
|
317
|
+
# Reaches the record; renders once for a collection
|
|
318
|
+
display :tags do |field|
|
|
319
|
+
div(class: "flex flex-wrap gap-1.5") do
|
|
320
|
+
field.object.tags.each { |tag| span(class: "pu-badge") { tag.name } }
|
|
310
321
|
end
|
|
311
|
-
|
|
322
|
+
end
|
|
312
323
|
```
|
|
313
324
|
|
|
325
|
+
Phlex also renders whatever the block returns, so a block that emits markup must end
|
|
326
|
+
with a tag call (or `nil`). The `div` wrapper above does that. A bare
|
|
327
|
+
`tags.each { span … }` as the last line returns the collection, which renders a
|
|
328
|
+
second time as text.
|
|
329
|
+
|
|
330
|
+
::: warning `as: :phlexi_render` is deprecated
|
|
331
|
+
`display :x, as: :phlexi_render, with: ...` emits a deprecation warning and will be
|
|
332
|
+
removed. Use the block form above instead. The old `with:` proc did not run in a
|
|
333
|
+
Phlex context (its `self` was the definition, so `span`/`div` in the body raised), it
|
|
334
|
+
received the value already stringified so it could not reach the record, and it fired
|
|
335
|
+
once per element for a `has_many` field. The block form has none of those limits. This
|
|
336
|
+
deprecation applies only to the `as: :phlexi_render` display option; the internal
|
|
337
|
+
`phlexi_render` helper is unaffected.
|
|
338
|
+
:::
|
|
339
|
+
|
|
314
340
|
See [UI › Components](/reference/ui/components) for writing reusable Phlex components.
|
|
315
341
|
|
|
316
342
|
### Custom component class
|
|
317
343
|
|
|
318
|
-
`as:` takes a **field component
|
|
344
|
+
`as:` takes a **field component**: Plutonium constructs it as
|
|
319
345
|
`YourComponent.new(field, **attributes)`, so it subclasses
|
|
320
346
|
`Phlexi::Form::Components::Base` (inputs) or `Phlexi::Display::Components::Base`
|
|
321
347
|
(displays) and reads the value off `field`:
|
|
@@ -326,7 +352,7 @@ display :chart, as: ChartComponent
|
|
|
326
352
|
```
|
|
327
353
|
|
|
328
354
|
A component with its own constructor (e.g. `PostCardComponent.new(post:)`) is not
|
|
329
|
-
an `as:` candidate
|
|
355
|
+
an `as:` candidate; it would raise `ArgumentError`. Build it in a block instead:
|
|
330
356
|
|
|
331
357
|
```ruby
|
|
332
358
|
display :card do |field|
|
|
@@ -338,27 +364,65 @@ See [UI › Components › Field components](/reference/ui/components#field-comp
|
|
|
338
364
|
|
|
339
365
|
## Column options
|
|
340
366
|
|
|
367
|
+
`align:` and `label:` configure the column header. They can be declared on `field`,
|
|
368
|
+
`display` or `column`; the most specific wins.
|
|
369
|
+
|
|
341
370
|
```ruby
|
|
371
|
+
field :amount, align: :end # every surface that has a notion of alignment
|
|
342
372
|
column :title, align: :start # default
|
|
343
373
|
column :status, align: :center
|
|
344
|
-
column :amount,
|
|
374
|
+
column :amount, label: "Total"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### What a column inherits from the display
|
|
378
|
+
|
|
379
|
+
With no `column` declared, the table column renders exactly as the display does: the
|
|
380
|
+
display's `as:` and its attributes flow to the cell, so `display :status, as: :badge,
|
|
381
|
+
colors: {…}` colors the badge on the show page and in the table.
|
|
382
|
+
|
|
383
|
+
A `column` that only touches the header (`align:`, `label:`, `condition:`) keeps that
|
|
384
|
+
inheritance. A `column` that says anything about rendering (an `as:`, a component
|
|
385
|
+
attribute, or a block) renders alone: its own `as:` and attributes, nothing from the
|
|
386
|
+
display.
|
|
387
|
+
|
|
388
|
+
A `condition:` on `field` or `column` hides the column. A `condition:` on `display`
|
|
389
|
+
applies to the show page only, because it may reference the record and the table has
|
|
390
|
+
no single one.
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
display :status, as: :badge, colors: {published: :accent}
|
|
394
|
+
column :status, align: :center # still a badge, still colored
|
|
395
|
+
column :status, as: :badge # a badge, but with default colors
|
|
396
|
+
column :status, formatter: ->(v) { v.upcase } # plain text; the display is out
|
|
345
397
|
```
|
|
346
398
|
|
|
347
399
|
### Value formatting
|
|
348
400
|
|
|
349
401
|
`formatter:` receives just the value. Use a block when you need the full record.
|
|
402
|
+
A column block runs in the table page's Phlex context, the same way a display block
|
|
403
|
+
runs in the show page's: it can emit markup directly, or return a String, a number
|
|
404
|
+
or a component. `self` is the table page, so `current_user`, `helpers` and
|
|
405
|
+
`resource_definition` are available, but methods defined on the definition class are
|
|
406
|
+
not; reach through the record you are passed instead.
|
|
350
407
|
|
|
351
408
|
```ruby
|
|
352
409
|
column :description, formatter: ->(value) { value&.truncate(30) }
|
|
353
410
|
column :price, formatter: ->(value) { "$%.2f" % value if value }
|
|
354
|
-
column :status, formatter: ->(value) { value&.humanize&.upcase }
|
|
355
411
|
|
|
356
|
-
# Block
|
|
412
|
+
# Block with full record access: emit markup...
|
|
413
|
+
column :status do |record|
|
|
414
|
+
span(class: "pu-badge") { record.status.humanize }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# ...or return a String
|
|
357
418
|
column :full_name do |record|
|
|
358
419
|
"#{record.first_name} #{record.last_name}"
|
|
359
420
|
end
|
|
360
421
|
```
|
|
361
422
|
|
|
423
|
+
As with display blocks, a block that emits markup must end with a tag call (or
|
|
424
|
+
`nil`), because Phlex renders the return value too.
|
|
425
|
+
|
|
362
426
|
## Nested inputs
|
|
363
427
|
|
|
364
428
|
Inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
|
|
@@ -389,7 +453,7 @@ end
|
|
|
389
453
|
|---|---|
|
|
390
454
|
| `limit` | Max records (auto-detected from model; default 10) |
|
|
391
455
|
| `allow_destroy` | Show delete checkbox (auto-detected) |
|
|
392
|
-
| `update_only` | Hide "Add" button
|
|
456
|
+
| `update_only` | Hide "Add" button: only edit existing |
|
|
393
457
|
| `description` | Help text above the section |
|
|
394
458
|
| `condition` | Proc to show/hide |
|
|
395
459
|
| `using` | Another Definition class |
|
|
@@ -404,13 +468,13 @@ end
|
|
|
404
468
|
end
|
|
405
469
|
```
|
|
406
470
|
- **Don't put `*_attributes` hashes in the policy.** Plutonium extracts nested params from the form definition, not the policy. The policy permits just the association name (`:variants`); `nested_input :variants` handles the rest. Adding `{variants_attributes: [...]}` to `permitted_attributes_for_create` renders as a literal text input. See [Behavior › Policy](/reference/behavior/policies).
|
|
407
|
-
- **`update_only: true` hides the Add button
|
|
408
|
-
- **Custom class names
|
|
471
|
+
- **`update_only: true` hides the Add button**: for `has_one` and "settings"-style associations.
|
|
472
|
+
- **Custom class names**: use `class_name:` in the model AND `using:` in the definition.
|
|
409
473
|
|
|
410
474
|
## Structured inputs
|
|
411
475
|
|
|
412
476
|
Classless inline fieldsets backed by a JSON/jsonb column. No model associations
|
|
413
|
-
required
|
|
477
|
+
required: the whole sub-form is serialised into a single column as a hash
|
|
414
478
|
(single form) or an array of hashes (repeater).
|
|
415
479
|
|
|
416
480
|

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

|
|
458
522
|
|
|
459
523
|
### Policy
|
|
460
524
|
|
|
461
|
-
Permit the column name as a plain symbol
|
|
525
|
+
Permit the column name as a plain symbol; Plutonium handles the nested hash
|
|
462
526
|
params automatically:
|
|
463
527
|
|
|
464
528
|
```ruby
|
|
@@ -480,13 +544,13 @@ attribute is declared automatically; `execute` receives the value as a `Hash`
|
|
|
480
544
|
The fields are classless render declarations, so there is nothing for Plutonium
|
|
481
545
|
to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
|
|
482
546
|
records run their own model validations). Whatever the form submits is stored
|
|
483
|
-
as-is, after blank rows are dropped
|
|
547
|
+
as-is, after blank rows are dropped: **no per-field server-side validation**.
|
|
484
548
|
:::
|
|
485
549
|
|
|
486
550
|
Specifically:
|
|
487
551
|
|
|
488
552
|
- **HTML constraints are client-side only.** A field's `required:` and a
|
|
489
|
-
select's `choices:` guide the browser but are **not** enforced on the server
|
|
553
|
+
select's `choices:` guide the browser but are **not** enforced on the server:
|
|
490
554
|
an API call or a crafted request can submit anything.
|
|
491
555
|
- **Selects silently drop unknown values.** If a stored value is not among a
|
|
492
556
|
`as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
|
|
@@ -496,7 +560,7 @@ Specifically:
|
|
|
496
560
|
`choices:` can drift. Keep `choices:` a stable superset, or use a free-text
|
|
497
561
|
input, when values can change over time.
|
|
498
562
|
|
|
499
|
-
To enforce anything, add the validation yourself
|
|
563
|
+
To enforce anything, add the validation yourself; it runs server-side:
|
|
500
564
|
|
|
501
565
|
```ruby
|
|
502
566
|
# resource: validate the JSON column on the model
|
|
@@ -543,7 +607,7 @@ end
|
|
|
543
607
|
|
|
544
608
|
The block is evaluated once and stored on the class. Re-declaring `form_layout` in a subclass replaces the parent layout as a unit; per-field `input` config inherits normally.
|
|
545
609
|
|
|
546
|
-
With no `form_layout` declared the form renders unchanged as a single responsive grid
|
|
610
|
+
With no `form_layout` declared the form renders unchanged as a single responsive grid, fully backwards-compatible.
|
|
547
611
|
|
|
548
612
|
### `section(key, *fields, **opts)`
|
|
549
613
|
|
|
@@ -551,16 +615,16 @@ Groups a set of fields under an optional heading.
|
|
|
551
615
|
|
|
552
616
|
| Argument | Description |
|
|
553
617
|
|---|---|
|
|
554
|
-
| `key` | Symbol. `:ungrouped` is reserved
|
|
618
|
+
| `key` | Symbol. `:ungrouped` is reserved: use the `ungrouped` macro instead (raises `ArgumentError` otherwise). |
|
|
555
619
|
| `*fields` | Ordered field keys to place in this section. |
|
|
556
620
|
| `label:` | Section heading. Defaults to `key.to_s.humanize` (e.g. `:shipping_address` → `"Shipping address"`). |
|
|
557
621
|
| `description:` | Optional help line rendered below the heading. |
|
|
558
622
|
| `collapsible:` | Boolean (default `false`). Wraps the section in a native `<details>/<summary>` (no JS). |
|
|
559
623
|
| `collapsed:` | Boolean (default `false`). Initial collapsed state when `collapsible: true`. |
|
|
560
|
-
| `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
|
|
561
|
-
| `condition:` | Lambda evaluated in the form instance context
|
|
624
|
+
| `columns:` | Positive Integer. Overrides the section grid column count (e.g. `columns: 2`). Omit to use the form's default responsive grid. Must be a positive Integer, any other value raises. (Literal only, not dynamic.) |
|
|
625
|
+
| `condition:` | Lambda evaluated in the form instance context: same semantics as `input ..., condition:`. `object`, `current_user`, helpers etc. are all available. A falsey result hides the entire section and withholds its fields (they do not spill into `ungrouped`). |
|
|
562
626
|
|
|
563
|
-
Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware
|
|
627
|
+
Every option except `columns:` may be either a literal **or a proc** resolved at render time, following the same arity rule as every other option ([Options that vary per render](#options-that-vary-per-render)): take a `form` argument to read the render context. This makes the layout record-aware, e.g. collapse a section by default only for existing records:
|
|
564
628
|
|
|
565
629
|
```ruby
|
|
566
630
|
section :advanced, :seo_title, :notes,
|
|
@@ -570,29 +634,43 @@ section :advanced, :seo_title, :notes,
|
|
|
570
634
|
```
|
|
571
635
|
|
|
572
636
|
::: warning Breaking change in 0.63
|
|
573
|
-
Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding
|
|
637
|
+
Section options previously took a **zero-argument** proc evaluated against the form (`collapsed: -> { object.persisted? }`). They now follow the same rule as every other option, where a zero-argument proc keeps its own binding, and a `form_layout` block is evaluated against the layout builder, so `object` there is a `NameError`.
|
|
574
638
|
|
|
575
639
|
```ruby
|
|
576
640
|
- collapsed: -> { object.persisted? }
|
|
577
641
|
+ collapsed: ->(form) { form.object.persisted? }
|
|
578
642
|
```
|
|
579
643
|
|
|
580
|
-
It fails loudly, never silently. `condition:` is unchanged
|
|
644
|
+
It fails loudly, never silently. `condition:` is unchanged: it is still evaluated against the form and still reads `object` with no argument.
|
|
581
645
|
:::
|
|
582
646
|
|
|
583
|
-
A section that resolves to **zero fields**
|
|
647
|
+
A section that resolves to **zero fields** (every declared field filtered out by the permitted set, or no field assigned) renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
|
|
648
|
+
|
|
649
|
+
The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records, re-evaluated on `pre_submit`, can be grouped in a section without any extra wiring:
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
form_layout do
|
|
653
|
+
section :timing, :duration, :starts_on, label: "Timing" # gone when both are hidden
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
input :employment_type, as: :select, pre_submit: true, choices: %w[full_time contract]
|
|
657
|
+
input :duration, condition: -> { object.contract? }
|
|
658
|
+
input :starts_on, condition: -> { object.contract? }
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
A section's own `condition:` still hides it as a unit, whatever its fields say:
|
|
584
662
|
|
|
585
663
|
```ruby
|
|
586
664
|
section :shipping, :address, :city, :postcode,
|
|
587
|
-
condition: -> { object.requires_shipping? }
|
|
665
|
+
condition: -> { object.requires_shipping? }
|
|
588
666
|
```
|
|
589
667
|
|
|
590
668
|
### `ungrouped(**opts)`
|
|
591
669
|
|
|
592
|
-
A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list
|
|
670
|
+
A macro (not a `section` call) that configures the implicit bucket collecting every permitted field not claimed by any `section`. Takes **no field list**; its fields are computed at render time.
|
|
593
671
|
|
|
594
672
|
- Accepts the same options as `section`: `label:`, `description:`, `collapsible:`, `collapsed:`, `columns:`, `condition:`.
|
|
595
|
-
- **Position
|
|
673
|
+
- **Position**: where you call `ungrouped` in the block is where leftovers appear. Omit it entirely and leftovers render **last**, after every declared section, with no heading. (Declaring `ungrouped` at the very end is therefore equivalent to omitting it, except that the explicit form lets you add a `label:` and other options.)
|
|
596
674
|
- Declaring `ungrouped` more than once in a single `form_layout` raises `ArgumentError`.
|
|
597
675
|
|
|
598
676
|
```ruby
|
|
@@ -610,9 +688,9 @@ end
|
|
|
610
688
|
|
|
611
689
|
### Layout references keys; config stays on `input`
|
|
612
690
|
|
|
613
|
-
`form_layout` and `section` carry section-level options only. All per-field rendering config
|
|
691
|
+
`form_layout` and `section` carry section-level options only. All per-field rendering config (`as:`, the field's own `label:`, `choices:`, per-field `condition:`, `pre_submit:`, blocks) remains on the `input` declaration. Layout never duplicates field config.
|
|
614
692
|
|
|
615
|
-
This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it
|
|
693
|
+
This includes a field's **column span**. In a section with `columns:`, fields flow into single grid cells by default; a field that declares its own span via `wrapper: {class: "col-span-..."}` keeps it; a field-level span always wins, so you can opt one field back to full width inside a multi-column section:
|
|
616
694
|
|
|
617
695
|
```ruby
|
|
618
696
|
input :notes, wrapper: {class: "col-span-full"} # spans the whole row...
|
|
@@ -624,7 +702,7 @@ end
|
|
|
624
702
|
|
|
625
703
|
```ruby
|
|
626
704
|
class ArticleDefinition < ResourceDefinition
|
|
627
|
-
# per-field config on input
|
|
705
|
+
# per-field config on input: untouched by form_layout
|
|
628
706
|
input :body, as: :markdown
|
|
629
707
|
input :published_at, hint: "Leave blank to save as draft"
|
|
630
708
|
input :visibility, as: :select, choices: %w[public private unlisted]
|
|
@@ -638,13 +716,13 @@ end
|
|
|
638
716
|
|
|
639
717
|
### Fields not in the permitted set are skipped
|
|
640
718
|
|
|
641
|
-
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there
|
|
719
|
+
A `section` only renders the fields that are actually in the form's permitted set for the current request. A key it lists that isn't there (a typo, or a field excluded by policy, per-action `permitted_attributes`, entity scoping, or nesting) is **silently dropped**, never an error. This lets a single `form_layout` reference conditionally-permitted fields without crashing the form in the contexts where they're filtered out. And when every field a section lists is dropped this way, the section's chrome is dropped with it (see the zero-fields note above), so the same layout can serve a richly-permitted `edit` and a minimal `new` without leaving empty headings behind.
|
|
642
720
|
|
|
643
721
|
### On interactions
|
|
644
722
|
|
|
645
|
-
`form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically
|
|
723
|
+
`form_layout` is also available on `Plutonium::Interaction::Base`. The same DSL groups the interaction's `attribute` declarations into sections. Interaction forms (`Plutonium::UI::Form::Interaction`) pick up the layout automatically, no extra wiring needed.
|
|
646
724
|
|
|
647
|
-
Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource
|
|
725
|
+
Dynamic options and `condition:` work here too, with one difference: on an interaction form the form's `object` is the **interaction instance** (not a record). For a record action, the record is `object.resource`, so e.g. `collapsed: ->(form) { form.object.resource.archived? }`, and `condition: -> { object.resource.archived? }` (which is form-evaluated, so it needs no argument).
|
|
648
726
|
|
|
649
727
|
```ruby
|
|
650
728
|
class PublishPostInteraction < Plutonium::Interaction::Base
|
|
@@ -664,7 +742,7 @@ end
|
|
|
664
742
|
|
|
665
743
|
## Display layout
|
|
666
744
|
|
|
667
|
-
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules
|
|
745
|
+
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules: first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely, applied to the show page instead of the form.
|
|
668
746
|
|
|
669
747
|
```ruby
|
|
670
748
|
class PostDefinition < ResourceDefinition
|
|
@@ -678,7 +756,7 @@ class PostDefinition < ResourceDefinition
|
|
|
678
756
|
end
|
|
679
757
|
```
|
|
680
758
|
|
|
681
|
-
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid
|
|
759
|
+
With no `display_layout` declared the show page renders unchanged as a single card holding one responsive grid, fully backwards-compatible.
|
|
682
760
|
|
|
683
761
|
### Independent of `form_layout`
|
|
684
762
|
|
|
@@ -704,9 +782,9 @@ Raising rather than ignoring the option means a `form_layout` block copied acros
|
|
|
704
782
|
|
|
705
783
|
### Section options
|
|
706
784
|
|
|
707
|
-
`label:`, `description:`, `collapsible:`, `collapsed:`, `condition
|
|
785
|
+
`label:`, `description:`, `collapsible:`, `collapsed:`, `condition:`, the same set as [`section(key, *fields, **opts)`](#section-key-fields-opts) minus `columns:`. A collapsible display section behaves exactly as a form one does, `collapsed:` included.
|
|
708
786
|
|
|
709
|
-
Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses
|
|
787
|
+
Every option except `condition:` may be a proc, resolved at render under the same arity rule the form uses: a zero-arity proc keeps its own binding, a one-arity proc is handed the display:
|
|
710
788
|
|
|
711
789
|
```ruby
|
|
712
790
|
section :audit, :created_at, collapsible: true, collapsed: ->(display) { display.object.active? }
|
|
@@ -716,11 +794,11 @@ section :audit, :created_at, collapsible: true, collapsed: ->(display) { display
|
|
|
716
794
|
|
|
717
795
|
### Rendering
|
|
718
796
|
|
|
719
|
-
Each section renders as its own card, stacked by a `sections_wrapper` container
|
|
797
|
+
Each section renders as its own card, stacked by a `sections_wrapper` container, so a sectioned show page has **no single outer card**. Fields declared via [`metadata`](#metadata-panel-show-page) are excluded from the sections and render in the metadata panel instead. Section chrome is themeable; see [UI › Displays › Theming](/reference/ui/displays#theming).
|
|
720
798
|
|
|
721
799
|
## Page width
|
|
722
800
|
|
|
723
|
-
Detail-style pages
|
|
801
|
+
Detail-style pages, the show page and resource forms, are constrained to a readable column by default. Inputs and values stretch to their container, so at full content width they become ~1200px-wide text boxes: past a comfortable measure, and a long eye-travel between a label and the value beside it.
|
|
724
802
|
|
|
725
803
|
Index and table pages are deliberately **not** affected; a table wants every pixel.
|
|
726
804
|
|
|
@@ -738,7 +816,7 @@ end
|
|
|
738
816
|
`:sm` `:md` `:lg` `:xl` `:full`. `:full` opts out of any constraint. An unknown value raises `ArgumentError` at declaration rather than silently rendering at some other width.
|
|
739
817
|
|
|
740
818
|
::: warning Size tokens are relative to their surface
|
|
741
|
-
These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable
|
|
819
|
+
These are the same token *names* [modal sizes](#modals) use, but **not the same widths**. Each surface has its own scale, because the surfaces aren't comparable: a "small page" is reasonably larger than a "small dialog":
|
|
742
820
|
|
|
743
821
|
| Token | Page width | Centered modal | Slideover |
|
|
744
822
|
|---|---|---|---|
|
|
@@ -768,9 +846,9 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
|
|
|
768
846
|
|
|
769
847
|
### Scope
|
|
770
848
|
|
|
771
|
-
- **Modals are unaffected
|
|
849
|
+
- **Modals are unaffected**: a dialog sizes itself via `modal_size`.
|
|
772
850
|
- **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
|
|
773
|
-
- **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `default_page_width`, so
|
|
851
|
+
- **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `:md` independently of `default_page_width`, so widening resource pages leaves wizard steps where they are. Set both if you want them to match.
|
|
774
852
|
|
|
775
853
|
## File uploads
|
|
776
854
|
|
|
@@ -788,9 +866,9 @@ input :documents, as: :uppy,
|
|
|
788
866
|
|
|
789
867
|
Inside `condition:` procs and block-form `input`/`display`:
|
|
790
868
|
|
|
791
|
-
- `object
|
|
869
|
+
- `object`: the record being edited or displayed
|
|
792
870
|
- `current_user`
|
|
793
|
-
- `current_parent
|
|
871
|
+
- `current_parent`: parent record for nested resources
|
|
794
872
|
- `request`, `params`
|
|
795
873
|
- All view helpers (via the same context as controllers)
|
|
796
874
|
|
|
@@ -844,23 +922,23 @@ interactive_action_page_breadcrumbs true
|
|
|
844
922
|
```ruby
|
|
845
923
|
class PostDefinition < ResourceDefinition
|
|
846
924
|
# "Save and add another" / "Update and continue editing"
|
|
847
|
-
# nil (default)
|
|
848
|
-
# true
|
|
849
|
-
# false
|
|
925
|
+
# nil (default): auto: hidden for singular resources, shown for plural
|
|
926
|
+
# true: always show
|
|
927
|
+
# false: always hide
|
|
850
928
|
submit_and_continue false
|
|
851
929
|
|
|
852
930
|
# How :new / :edit and interactive actions render
|
|
853
|
-
# :slideover (default)
|
|
854
|
-
# :centered
|
|
855
|
-
# false
|
|
931
|
+
# :slideover (default): slide-in panel from the right
|
|
932
|
+
# :centered: centered dialog
|
|
933
|
+
# false: full standalone pages (no modal)
|
|
856
934
|
# size: optional, one of :sm, :md (default), :lg, :xl, :auto, :full
|
|
857
|
-
# (widths are per-surface
|
|
935
|
+
# (widths are per-surface: see Page width; a slideover's :md is 480px,
|
|
858
936
|
# a centered dialog's is 576px, a page's is 896px)
|
|
859
937
|
modal :centered, size: :lg
|
|
860
938
|
end
|
|
861
939
|
```
|
|
862
940
|
|
|
863
|
-
`modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win
|
|
941
|
+
`modal:` is the default for framework `:new`/`:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win; see [Actions](./actions).
|
|
864
942
|
|
|
865
943
|
### `show_in` {#show_in}
|
|
866
944
|
|
|
@@ -873,8 +951,8 @@ end
|
|
|
873
951
|
|
|
874
952
|
Controls how the **show page** opens when a record is clicked in the table or grid (and serves as the default for a [kanban board](/reference/kanban/dsl#show_in), which can override it per-board):
|
|
875
953
|
|
|
876
|
-
- `:page` (default)
|
|
877
|
-
- `:modal
|
|
954
|
+
- `:page` (default): full-page navigation to the show route.
|
|
955
|
+
- `:modal`, the show page opens in a **centered** dialog. This is deliberately independent of `modal:`/`modal_mode` above (which styles `:new`/`:edit`), show is always centered, never a slideover. From inside the modal an expand icon opens the full page in a new tab; ⌘/Ctrl-click (or middle-click) on the row/card does the same directly.
|
|
878
956
|
|
|
879
957
|
An unknown mode raises `ArgumentError`.
|
|
880
958
|
|
|
@@ -894,7 +972,7 @@ Behavior:
|
|
|
894
972
|
- **Policy-aware.** Fields intersect with the policy's permitted attributes. The panel auto-hides when nothing is permitted.
|
|
895
973
|
- **Deduplicated.** Fields listed in `metadata` are removed from the main card so values aren't shown twice.
|
|
896
974
|
- **Responsive.** Side-by-side at `lg+`, stacked below.
|
|
897
|
-
- **Formatting inherits.** Field labels and `as:` declarations propagate
|
|
975
|
+
- **Formatting inherits.** Field labels and `as:` declarations propagate: the metadata panel uses the same field-rendering machinery as the main card.
|
|
898
976
|
|
|
899
977
|
## Index views (Table & Grid)
|
|
900
978
|
|
|
@@ -902,7 +980,7 @@ Resources can offer both Table and Grid views. The user switches via the toolbar
|
|
|
902
980
|
|
|
903
981
|
```ruby
|
|
904
982
|
class UserDefinition < ResourceDefinition
|
|
905
|
-
# No `index_views :table, :grid` needed
|
|
983
|
+
# No `index_views :table, :grid` needed: declaring grid_fields auto-enables :grid.
|
|
906
984
|
grid_fields(
|
|
907
985
|
image: :avatar, # ActiveStorage attachment, Shrine, or URL
|
|
908
986
|
header: :name, # falls back to to_label
|
|
@@ -912,7 +990,7 @@ class UserDefinition < ResourceDefinition
|
|
|
912
990
|
footer: :last_seen_at # falls back to :created_at
|
|
913
991
|
)
|
|
914
992
|
|
|
915
|
-
default_index_view :grid # optional
|
|
993
|
+
default_index_view :grid # optional: initial view when no cookie
|
|
916
994
|
grid_layout :media # :compact (default) or :media
|
|
917
995
|
grid_columns 3 # pin lg+ cols; default is 1/2/3/4 responsive
|
|
918
996
|
end
|
|
@@ -926,13 +1004,13 @@ end
|
|
|
926
1004
|
| `grid_layout :compact \| :media` | `:compact` puts image left of content; `:media` stacks image full-width on top. |
|
|
927
1005
|
| `grid_columns N` | Override responsive column count on `lg+`. Default is 1/2/3/4 at sm/md/lg/xl. |
|
|
928
1006
|
|
|
929
|
-
Grid slots
|
|
1007
|
+
Grid slots, `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer`, are all optional. `:meta` accepts an array; the rest are single fields. Slots pointing at policy-blocked fields collapse silently.
|
|
930
1008
|
|
|
931
1009
|
Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
|
|
932
1010
|
|
|
933
1011
|
## Custom page classes
|
|
934
1012
|
|
|
935
|
-
Override the rendered page entirely
|
|
1013
|
+
Override the rendered page entirely: full control via Phlex:
|
|
936
1014
|
|
|
937
1015
|
```ruby
|
|
938
1016
|
class PostDefinition < ResourceDefinition
|
|
@@ -959,8 +1037,8 @@ See [UI › Pages](/reference/ui/pages) and [UI › Forms](/reference/ui/forms)
|
|
|
959
1037
|
|
|
960
1038
|
## Related
|
|
961
1039
|
|
|
962
|
-
- [Query](./query)
|
|
963
|
-
- [Actions](./actions)
|
|
964
|
-
- [Behavior › Policy](/reference/behavior/policies)
|
|
965
|
-
- [UI › Forms](/reference/ui/forms)
|
|
966
|
-
- [UI › Pages](/reference/ui/pages)
|
|
1040
|
+
- [Query](./query): search, filters, scopes, sorting
|
|
1041
|
+
- [Actions](./actions): custom + bulk actions
|
|
1042
|
+
- [Behavior › Policy](/reference/behavior/policies): `permitted_attributes_for_*`, authorization
|
|
1043
|
+
- [UI › Forms](/reference/ui/forms): field builder, association inputs, theming
|
|
1044
|
+
- [UI › Pages](/reference/ui/pages): custom page classes
|