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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Dashboard DSL Reference
|
|
2
|
+
|
|
3
|
+
::: warning Experimental
|
|
4
|
+
Dashboards are experimental: the DSL and behavior may change in a future release.
|
|
5
|
+
:::
|
|
6
|
+
|
|
7
|
+
A dashboard is a subclass of `Plutonium::Dashboard::Base`. Everything is declared at class level; an instance is built per request with the view context and is the receiver of every card block.
|
|
8
|
+
|
|
9
|
+
## Presentation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
presents label: "Sales", description: "Orders and revenue", icon: Phlex::TablerIcons::ChartBar
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Key | Default |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `label` | `plutonium.dashboards.<key>.label`, then the class name without `Dashboard` |
|
|
18
|
+
| `description` | `plutonium.dashboards.<key>.description`, then nothing |
|
|
19
|
+
| `icon` | `Phlex::TablerIcons::LayoutDashboard` (used in the sidebar) |
|
|
20
|
+
|
|
21
|
+
`<key>` is `Class.i18n_key`: the class name underscored without the suffix (`AdminPortal::SalesDashboard` → `admin_portal/sales`). `label` and `description` accept the class-level lazy `t("...")`.
|
|
22
|
+
|
|
23
|
+
## Board-level options
|
|
24
|
+
|
|
25
|
+
### refresh(seconds)
|
|
26
|
+
|
|
27
|
+
Default reload interval for every lazy card. `nil` (the default) disables it.
|
|
28
|
+
|
|
29
|
+
### width(token)
|
|
30
|
+
|
|
31
|
+
Page width, one of `:sm`, `:md`, `:lg`, `:xl`, `:full`. Default `:full`.
|
|
32
|
+
|
|
33
|
+
## Cards
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
metric(key, **options) { ... }
|
|
37
|
+
chart(key, **options) { ... }
|
|
38
|
+
card(key, **options) { ... }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keys are unique per dashboard; a duplicate raises at class load. Cards render in declaration order.
|
|
42
|
+
|
|
43
|
+
### Common options
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Meaning |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `label:` | String, lazy `t` | convention, then key titleized | Title |
|
|
48
|
+
| `description:` | String, lazy `t` | convention | Caption |
|
|
49
|
+
| `icon:` | Phlex icon class | none | Shown beside the title |
|
|
50
|
+
| `span:` | `1`..`12`, `:full` | metric `3`, chart `6`, card `6` | Columns of the 12-column grid. `:full` is `12`. On tablets (2 columns) a span of `6` or more takes the row; phones are one column |
|
|
51
|
+
| `lazy:` | Boolean | `true` | `true`: own lazy turbo frame, block runs in a separate request. `false`: inline, block runs in the page request (adding its query time to every page load); no frame, never refreshes. When `config.consider_all_requests_local` is true (development and test), an inline card's exception fails the whole page. See the [guide](/guides/dashboards#lazy-and-inline-cards) |
|
|
52
|
+
| `refresh:` | Integer seconds, or `false` | dashboard `refresh` | Reload interval; requires `lazy: true` (a number on a `lazy: false` card raises `ArgumentError`). `false` opts the card out of the dashboard's `refresh` |
|
|
53
|
+
| `condition:` | Proc, Symbol | none | Hides the card and 404s its endpoint when false |
|
|
54
|
+
| `href:` | String, Proc | none | Links the title |
|
|
55
|
+
|
|
56
|
+
Procs (`condition:`, `href:`, the block) run on the dashboard instance with `instance_exec`; a one-argument proc receives the instance instead.
|
|
57
|
+
|
|
58
|
+
### Locale conventions
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
plutonium.dashboards.<key>.cards.<card>.label
|
|
62
|
+
plutonium.dashboards.<key>.cards.<card>.description
|
|
63
|
+
plutonium.portals.<portal>.dashboards.<key>...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### metric
|
|
67
|
+
|
|
68
|
+
The block returns a value or a hash.
|
|
69
|
+
|
|
70
|
+
| Hash key | Meaning |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `value` | The number or string. `nil` renders `plutonium.dashboard.metric.empty` |
|
|
73
|
+
| `previous` | Computes `change` as a percentage from this value; a zero previous yields no percentage |
|
|
74
|
+
| `change` | Numeric percentage points (`2.5` → `+2.5%`) or a string shown as-is |
|
|
75
|
+
| `trend` | `:up`, `:down`, `:flat`; inferred from the sign of `change` |
|
|
76
|
+
| `change_label` | Caption after the change |
|
|
77
|
+
|
|
78
|
+
| Option | Values | Meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `format:` | `:number` (default), `:currency`, `:percentage`, `:human`, Proc | How `value` is rendered. A proc receives the value and runs on the dashboard |
|
|
81
|
+
| `precision:` | Integer | Decimal places; defaults per format (2 for currency, 1 for percentage, 3 significant for human) |
|
|
82
|
+
| `unit:` | String | Currency symbol for `:currency` |
|
|
83
|
+
| `prefix:` / `suffix:` | String | Wrapped around the formatted value |
|
|
84
|
+
| `positive:` | `:up` (default), `:down` | Which direction is good; drives the change colour |
|
|
85
|
+
| `change_label:` | String | Same as the hash key |
|
|
86
|
+
|
|
87
|
+
### chart
|
|
88
|
+
|
|
89
|
+
The block returns Chartkick data: `{label => value}`, `[[label, value], ...]` or `[{name:, data:}, ...]`. `Date` and `Time` keys serialise as ISO strings and draw a time axis.
|
|
90
|
+
|
|
91
|
+
| Option | Values | Meaning |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `type:` | `:line` (default), `:area`, `:column`, `:bar`, `:pie`, `:donut`, `:scatter` | Chartkick chart class |
|
|
94
|
+
| `height:` | CSS length | Drawing area height, default `"240px"` |
|
|
95
|
+
| anything else | | Passed to Chartkick unchanged: `colors`, `stacked`, `min`, `max`, `prefix`, `suffix`, `thousands`, `decimal`, `xtitle`, `ytitle`, `legend`, `curve`, `points`, `discrete`, `download`, `library`, ... |
|
|
96
|
+
|
|
97
|
+
The `chart` Stimulus controller merges these over Plutonium's defaults: series colours from `--pu-chart-1` to `--pu-chart-8`, axis text from `--pu-text-muted`, grid lines from `--pu-border`, and the empty-data message from `plutonium.js.charts.empty`. A colour-mode switch redraws every chart.
|
|
98
|
+
|
|
99
|
+
### card
|
|
100
|
+
|
|
101
|
+
No kind-specific options. The block is `instance_exec`ed in `Plutonium::UI::Dashboard::Custom`, a Phlex component: it emits markup directly, and any method the component does not define is forwarded to the dashboard instance.
|
|
102
|
+
|
|
103
|
+
## Instance API
|
|
104
|
+
|
|
105
|
+
| Method | Meaning |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `authorize?` | Override to gate the page and every card. Default `true` |
|
|
108
|
+
| `visible_cards` | Cards whose `condition:` passes |
|
|
109
|
+
| `visible_card!(key)` | The card, or `Plutonium::Dashboard::UnknownCardError` (404) |
|
|
110
|
+
| `refresh_for(card)` | The card's interval, else the dashboard's; `nil` for a card declared `refresh: false` |
|
|
111
|
+
| `view_context` / `helpers` | The Rails view context |
|
|
112
|
+
| `current_user`, `current_scoped_entity`, `scoped_to_entity?`, `params`, `request`, `controller`, `current_engine`, `resource_url_for`, `authorized_resource_scope`, `allowed_to?`, `policy_for`, `registered_resources`, `root_path` | Delegated to the view context |
|
|
113
|
+
|
|
114
|
+
## Class API
|
|
115
|
+
|
|
116
|
+
| Method | Meaning |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `cards` | All declared cards, in order |
|
|
119
|
+
| `find_card(key)` / `find_card!(key)` | Lookup by key |
|
|
120
|
+
| `label`, `description`, `icon` | Resolved presentation |
|
|
121
|
+
| `i18n_key`, `route_name` | `admin_portal/sales`, `sales` |
|
|
122
|
+
|
|
123
|
+
Subclasses inherit the parent's cards and options by copy, so a portal-specific subclass can add cards without touching the parent.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Dashboard Reference
|
|
2
|
+
|
|
3
|
+
::: warning Experimental
|
|
4
|
+
Dashboards are experimental: the DSL and behavior may change in a future release.
|
|
5
|
+
:::
|
|
6
|
+
|
|
7
|
+
Reference documentation for Plutonium dashboards: pages of metric, chart and free-form cards, each loaded in its own lazy turbo frame by default.
|
|
8
|
+
|
|
9
|
+
## In this section
|
|
10
|
+
|
|
11
|
+
| Page | What it covers |
|
|
12
|
+
|------|---------------|
|
|
13
|
+
| [DSL](/reference/dashboard/dsl) | `Plutonium::Dashboard::Base`, `presents`, `refresh`, `width`, and the `metric` / `chart` / `card` macros with every option |
|
|
14
|
+
| [Registration](/reference/dashboard/registration) | `register_dashboard`, the routes it draws, the synthesized controller, the sidebar, and the `pu:dashboard` generator |
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# packages/admin_portal/app/dashboards/admin_portal/sales_dashboard.rb
|
|
20
|
+
module AdminPortal
|
|
21
|
+
class SalesDashboard < Plutonium::Dashboard::Base
|
|
22
|
+
presents label: "Sales", icon: Phlex::TablerIcons::ChartBar
|
|
23
|
+
|
|
24
|
+
metric(:orders) { {value: Order.this_month.count, previous: Order.last_month.count} }
|
|
25
|
+
chart(:revenue, type: :area, span: 8) { Order.group_by_day(:created_at).sum(:total) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# packages/admin_portal/config/routes.rb
|
|
30
|
+
AdminPortal::Engine.routes.draw do
|
|
31
|
+
register_dashboard AdminPortal::SalesDashboard, at: "sales"
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See the [Dashboards guide](/guides/dashboards) for a full walkthrough.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Dashboard Registration
|
|
2
|
+
|
|
3
|
+
::: warning Experimental
|
|
4
|
+
Dashboards are experimental: the DSL and behavior may change in a future release.
|
|
5
|
+
:::
|
|
6
|
+
|
|
7
|
+
## register_dashboard
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
AdminPortal::Engine.routes.draw do
|
|
11
|
+
register_dashboard AdminPortal::HomeDashboard, at: "/" # the portal root
|
|
12
|
+
register_dashboard AdminPortal::SalesDashboard, at: "sales" # /admin/dashboards/sales
|
|
13
|
+
register_dashboard Reports::WeeklyDashboard, at: "reports/weekly", as: "weekly"
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Argument | Meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `at:` | Path under the portal's `dashboards/` segment. `"/"` or `""` mounts at the engine root |
|
|
20
|
+
| `prefix:` | Leading path segment, `"dashboards"` by default. `nil` draws the mount at the bare `at:` path (`/admin/sales`); a string swaps the segment (`prefix: "reports"` → `/admin/reports/sales`). Does not affect helper names |
|
|
21
|
+
| `as:` | Route helper prefix. Defaults to `at:` with slashes replaced, or the class slug for a root mount |
|
|
22
|
+
|
|
23
|
+
### Routes drawn
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
GET /dashboards/sales → DashboardsController#show sales_dashboard_path
|
|
27
|
+
GET /dashboards/sales/cards/:card → DashboardsController#card sales_dashboard_card_path
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every path is drawn under `dashboards/`, so a dashboard never shadows a `register_resource` route of the same name (`at: "sales"` next to a `Sale` resource). The route helpers carry no prefix.
|
|
31
|
+
|
|
32
|
+
With `prefix: nil` nothing reserves the path, so keep `at:` clear of your resource routes yourself.
|
|
33
|
+
|
|
34
|
+
A root mount draws `root` for the page and `/dashboards/<as>/cards/:card` for the cards (`/<as>/cards/:card` with `prefix: nil`). Replace the portal's generated `root to: "dashboard#index"` line with the registration; two root routes clash. The generated `DashboardController` and its view are then unrouted and can be deleted. The [guide](/guides/dashboards#replacing-the-portal-s-default-page) walks through it.
|
|
35
|
+
|
|
36
|
+
On a `:path` entity-scoped portal the routes sit inside the scope segment and the helpers are prefixed (`organization_scoped_sales_dashboard_path`). URLs built by the framework thread the current tenant through; when building your own use `dashboard_path_for(SalesDashboard)`, available in controllers, views and components.
|
|
37
|
+
|
|
38
|
+
### The controller
|
|
39
|
+
|
|
40
|
+
`register_dashboard` synthesizes `<Portal>::DashboardsController < <Portal>::PlutoniumController` including `Plutonium::Dashboard::Controller` and the portal's `Concerns::Controller`, so it carries the portal's authentication, entity scoping and layout. On the main app it synthesizes a bare `::DashboardsController < ApplicationController`.
|
|
41
|
+
|
|
42
|
+
Define the class yourself to take over; the synthesized one is only created when the constant is missing.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
module AdminPortal
|
|
46
|
+
class DashboardsController < PlutoniumController
|
|
47
|
+
include Plutonium::Dashboard::Controller
|
|
48
|
+
|
|
49
|
+
before_action { set_page_title("Reports") }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The concern provides `show` and `card`, `current_dashboard`, `current_dashboard_class`, `authorize_dashboard!` and `dashboard_card_path(card)`.
|
|
55
|
+
|
|
56
|
+
### The card endpoint
|
|
57
|
+
|
|
58
|
+
`GET <mount>/cards/:card` runs `authorize?`, looks the key up among the dashboard's cards, checks its `condition:`, and renders the card inside `<turbo-frame id="pu-dashboard-card-<key>">`. Unknown or hidden cards respond 404 (`Plutonium::Dashboard::UnknownCardError`); a denied `authorize?` responds 403. A request without a `Turbo-Frame` header renders the card inside the full layout.
|
|
59
|
+
|
|
60
|
+
## Sidebar
|
|
61
|
+
|
|
62
|
+
`registered_dashboards` (a controller helper, also available in components) lists the dashboards mounted on the current engine, and `dashboard_path_for(klass)` builds each page path. The gem's `_resource_sidebar.html.erb` groups them under a Dashboards item (`plutonium.resource.nav.dashboards`) after the Home link (`plutonium.resource.nav.home`), skipping the one mounted at the root, which the Home link already opens.
|
|
63
|
+
|
|
64
|
+
## Generator
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rails g pu:dashboard Sales --dest=admin_portal # /admin/dashboards/sales
|
|
68
|
+
rails g pu:dashboard Home --dest=admin_portal --at=/ # portal root
|
|
69
|
+
rails g pu:dashboard Reporting --dest=main_app --at=reports
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Writes `app/dashboards/<portal>/<name>_dashboard.rb` in the package (or `app/dashboards/` for `main_app`) and adds the `register_dashboard` line to the routes. Idempotent.
|
|
73
|
+
|
|
74
|
+
## Assets
|
|
75
|
+
|
|
76
|
+
Chart cards load `plutonium-charts.js` (Chart.js and Chartkick) on demand; the file ships with the gem beside `plutonium.js` and is precompiled with it. `Plutonium.configuration.assets.charts_script` names the asset (`"plutonium-charts.min.js"` by default). When `PLUTONIUM_DEV` is set the file comes from the `src/build` manifest like the main bundle.
|
|
@@ -16,10 +16,10 @@ rails g pu:lite:tune
|
|
|
16
16
|
|
|
17
17
|
It writes a `pragmas:` mapping:
|
|
18
18
|
|
|
19
|
-
- `cache_size: -64000
|
|
20
|
-
- `temp_store: 2
|
|
21
|
-
- `mmap_size: 536870912
|
|
22
|
-
- `wal_autocheckpoint: 10000
|
|
19
|
+
- `cache_size: -64000`: 64 MB page cache (the ~2 MB default is too small).
|
|
20
|
+
- `temp_store: 2`: MEMORY; sorts and temp indexes stay off disk.
|
|
21
|
+
- `mmap_size: 536870912`: 512 MB memory-mapped I/O.
|
|
22
|
+
- `wal_autocheckpoint: 10000`: checkpoint roughly every 40 MB of WAL.
|
|
23
23
|
|
|
24
24
|
On Rails < 8.1 it also writes the baseline pragmas (`journal_mode: WAL`,
|
|
25
25
|
`synchronous: NORMAL`, `foreign_keys: true`, `journal_size_limit`) that Rails 8.1+
|
|
@@ -30,7 +30,7 @@ constant-poll busy handler (`busy_handler_timeout`), which has better tail-laten
|
|
|
30
30
|
than SQLite's internal exponential backoff. Setting a busy-timeout pragma would
|
|
31
31
|
replace the better handler with the worse one, so this generator never emits it.
|
|
32
32
|
|
|
33
|
-
The generator is idempotent
|
|
33
|
+
The generator is idempotent: re-running it detects the existing pragmas and skips.
|
|
34
34
|
It only ever touches the `default:` block, so a `pragmas:` mapping nested under
|
|
35
35
|
another environment is left untouched.
|
|
36
36
|
|
|
@@ -47,7 +47,7 @@ rails g pu:lite:maintenance --schedule="every day at 4am"
|
|
|
47
47
|
|
|
48
48
|
The job runs `PRAGMA optimize` on every configured SQLite database and `VACUUM`
|
|
49
49
|
only on databases without live 24/7 writers (`primary`, `errors`, `rails_pulse`
|
|
50
|
-
by default
|
|
50
|
+
by default; edit `VACUUM_DBS` in the generated job to suit your app).
|
|
51
51
|
|
|
52
52
|
**Why VACUUM only some databases?** SolidQueue, Solid Cache and Solid Cable write
|
|
53
53
|
to their databases constantly. `VACUUM` takes a global *exclusive* lock for its
|
|
@@ -61,5 +61,5 @@ Databases listed in the job that don't exist in `config/database.yml` are skippe
|
|
|
61
61
|
at runtime, so the same job is safe regardless of which `pu:lite:*` generators you
|
|
62
62
|
have run.
|
|
63
63
|
|
|
64
|
-
If `solid_queue` is not installed, the job file is still created but not scheduled
|
|
64
|
+
If `solid_queue` is not installed, the job file is still created but not scheduled:
|
|
65
65
|
add a `sqlite_maintenance` entry to whatever scheduler you use.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Internationalization
|
|
2
|
+
|
|
3
|
+
Every string Plutonium renders comes from a locale file, and every label it derives from a key (an action, a scope, a filter, a kanban column, a wizard step, a field's placeholder) can be supplied by one. Nothing in a definition has to change for an app to translate its UI or reword the defaults.
|
|
4
|
+
|
|
5
|
+
## Where the strings live
|
|
6
|
+
|
|
7
|
+
Plutonium ships its own text under `config/locales/en/*.yml` in the gem, all under the `plutonium` namespace. Rails loads it at the lowest precedence:
|
|
8
|
+
|
|
9
|
+
1. The host application's `config/locales` wins over everything.
|
|
10
|
+
2. Package and portal engines' `config/locales`, which Rails loads automatically.
|
|
11
|
+
3. The gem's defaults.
|
|
12
|
+
|
|
13
|
+
So a portal can reword a Plutonium string for itself, and the app can override the portal.
|
|
14
|
+
|
|
15
|
+
To change a fixed string, copy its key into your own locale file. The gem's locale files are the catalogue:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# config/locales/en.yml
|
|
19
|
+
en:
|
|
20
|
+
plutonium:
|
|
21
|
+
boolean:
|
|
22
|
+
"true": "On"
|
|
23
|
+
"false": "Off"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Pagination is the one surface with its own dictionary. The info sentence, the per-page sentence and the nav aria labels come from [Pagy's](https://ddnexus.github.io/pagy/resources/i18n) locale files, which cover about thirty-five languages. Override a Pagy key by adding a file to `Pagy::I18n.pathnames`, not through Rails I18n.
|
|
27
|
+
|
|
28
|
+
## Switching locale
|
|
29
|
+
|
|
30
|
+
Plutonium reads `I18n.locale` on every render and never sets it. Set the locale the way any Rails app does, typically an `around_action` in the portal's controller concern:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
module AdminPortal
|
|
34
|
+
module Concerns
|
|
35
|
+
module Controller
|
|
36
|
+
extend ActiveSupport::Concern
|
|
37
|
+
|
|
38
|
+
included do
|
|
39
|
+
around_action :switch_locale
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def switch_locale(&)
|
|
43
|
+
I18n.with_locale(current_user&.locale || I18n.default_locale, &)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The Pagy locale is synced to `I18n.locale` on each pagination render, so the same switch covers pagination.
|
|
51
|
+
|
|
52
|
+
## Models and attributes
|
|
53
|
+
|
|
54
|
+
Model and attribute names follow the Rails convention and already drive every label, column header, page title and flash:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
en:
|
|
58
|
+
activerecord:
|
|
59
|
+
models:
|
|
60
|
+
blogging/post:
|
|
61
|
+
one: "Post"
|
|
62
|
+
other: "Posts"
|
|
63
|
+
attributes:
|
|
64
|
+
blogging/post:
|
|
65
|
+
title: "Title"
|
|
66
|
+
published_at: "Published"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The model key is `model_name.i18n_key`: `blogging/post` for `Blogging::Post`. When a model defines `one` and `other`, Plutonium uses them for the plural forms in page titles, empty states and pagination. Without them it falls back to English inflection.
|
|
70
|
+
|
|
71
|
+
## Fields: placeholder, hint, description
|
|
72
|
+
|
|
73
|
+
A definition never has to declare help text to make it translatable. When an input or display leaves a slot blank, Plutonium looks it up by convention:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
en:
|
|
77
|
+
plutonium:
|
|
78
|
+
fields:
|
|
79
|
+
blogging/post:
|
|
80
|
+
title:
|
|
81
|
+
placeholder: "A short, descriptive title"
|
|
82
|
+
hint: "Shown in search results"
|
|
83
|
+
body:
|
|
84
|
+
description: "Rendered as Markdown"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`placeholder` and `hint` apply to forms. `description` applies to displays. The lookup walks the model's ancestors like `human_attribute_name` does, so a key on `blogging/post` also serves an STI subclass.
|
|
88
|
+
|
|
89
|
+
For each slot the resolution order is:
|
|
90
|
+
|
|
91
|
+
1. An explicit option on `input` or `display`, then on `field`.
|
|
92
|
+
2. `plutonium.portals.<portal>.fields.<model>.<attr>.<slot>` when rendering inside a portal.
|
|
93
|
+
3. `plutonium.fields.<model>.<attr>.<slot>`.
|
|
94
|
+
4. `helpers.placeholder.<model>.<attr>`, the Rails `form_with` convention, for placeholders only.
|
|
95
|
+
5. Nothing.
|
|
96
|
+
|
|
97
|
+
An explicit option can still be a translation. Use the definition's class-level `t`, which returns a lazy value resolved on every render in the request's locale:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
101
|
+
input :email, placeholder: t("forms.shared.email_placeholder")
|
|
102
|
+
input :quantity, hint: t("forms.stock.hint", count: 3)
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`t` is sugar over the proc form, which also works and has always been locale-safe:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
input :email, placeholder: -> { I18n.t("forms.shared.email_placeholder") }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Never call `I18n.t` directly in a class body. It runs once, at load time, in whatever locale happened to be active. Literal strings remain allowed and are used as they are.
|
|
113
|
+
|
|
114
|
+
The page-title setters take the same values. Pass a literal to fix it, or the lazy `t` to translate it per request:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
index_page_title t("blog.index.title")
|
|
118
|
+
index_page_description t("blog.index.description")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When a definition sets no title, the page falls back to the resource's translated model name.
|
|
122
|
+
|
|
123
|
+
The setter's proc is called with no record context, so a title that depends on the record belongs in a `page_title` override on the nested page class (`class ShowPage < ShowPage`), where `object` is available.
|
|
124
|
+
|
|
125
|
+
## Your own components and pages
|
|
126
|
+
|
|
127
|
+
Text you add to the UI goes in a locale file too. Which `t` you call depends on where the code runs:
|
|
128
|
+
|
|
129
|
+
| Where | Call |
|
|
130
|
+
|---|---|
|
|
131
|
+
| Components, pages, and nested `Form` / `Display` / `Table` classes (render hooks included) | `t("full.key", **opts)`, a protected method from `Plutonium::UI::Component::Behaviour` |
|
|
132
|
+
| `display` / `input` / `column` blocks in a definition | `t("full.key")`, since the block runs inside the rendering component |
|
|
133
|
+
| Definition class body (`label:`, `hint:`, `placeholder:`, page titles) | the class-level lazy `t("full.key")` above |
|
|
134
|
+
| A bare Phlexi field component that does not include `Behaviour` | `Plutonium::Translation.t("full.key")` |
|
|
135
|
+
|
|
136
|
+
All of them take full keys only. Use one key per sentence with `%{name}` placeholders rather than concatenating fragments around a value, and `count:` for plurals:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
def render_before_content
|
|
140
|
+
div(class: "pu-alert pu-alert-info", role: "status") do
|
|
141
|
+
div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Derived labels
|
|
147
|
+
|
|
148
|
+
Actions, scopes, filters, kanban columns and wizard steps default to a humanized version of their key. Each has a convention key that takes precedence over that fallback, and a `label:` option that takes precedence over both. For an action backed by an interaction, the interaction's explicit `presents label:` also counts as declared and wins over the convention; only the class-name default yields to it.
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
en:
|
|
152
|
+
plutonium:
|
|
153
|
+
actions:
|
|
154
|
+
blogging/post:
|
|
155
|
+
publish: "Publish now"
|
|
156
|
+
scopes:
|
|
157
|
+
blogging/post:
|
|
158
|
+
drafts: "Drafts"
|
|
159
|
+
filters:
|
|
160
|
+
blogging/post:
|
|
161
|
+
author: "Written by"
|
|
162
|
+
kanban_columns:
|
|
163
|
+
blogging/post:
|
|
164
|
+
in_review: "In review"
|
|
165
|
+
wizard_steps:
|
|
166
|
+
onboarding_wizard:
|
|
167
|
+
billing: "Billing details"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The model segment is the resource's `model_name.i18n_key`. For wizard steps it is the wizard's `model_name.i18n_key`.
|
|
171
|
+
|
|
172
|
+
Enum values shown as badges, and the value pills of a select filter, resolve the way Rails resolves enum attribute values, with a Plutonium key as an alternative:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
en:
|
|
176
|
+
activerecord:
|
|
177
|
+
attributes:
|
|
178
|
+
blogging/post:
|
|
179
|
+
status/draft: "Draft"
|
|
180
|
+
status/published: "Live"
|
|
181
|
+
plutonium:
|
|
182
|
+
values:
|
|
183
|
+
blogging/post:
|
|
184
|
+
status:
|
|
185
|
+
archived: "Archived"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Portals
|
|
189
|
+
|
|
190
|
+
Every convention key has a portal-scoped variant under `plutonium.portals.<portal>`, where `<portal>` is the package namespace (`admin_portal`). It wins over the global key while rendering inside that portal, and the main app has no portal segment.
|
|
191
|
+
|
|
192
|
+
Portal packages are Rails engines, so their `config/locales` directory loads automatically. The portal generator scaffolds `packages/<portal>/config/locales/en.yml` with the prefix in place:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
# packages/admin_portal/config/locales/en.yml
|
|
196
|
+
en:
|
|
197
|
+
plutonium:
|
|
198
|
+
portals:
|
|
199
|
+
admin_portal:
|
|
200
|
+
fields:
|
|
201
|
+
blogging/post:
|
|
202
|
+
title:
|
|
203
|
+
placeholder: "Internal working title"
|
|
204
|
+
actions:
|
|
205
|
+
blogging/post:
|
|
206
|
+
publish: "Publish to site"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Feature packages get the same file for their models' names, attributes and field text. Keys without the portal prefix apply everywhere, and load order settles conflicts: the app's files beat a package's, and a package's beat the gem's.
|
|
210
|
+
|
|
211
|
+
Locale keys vary by portal, never by tenant. Tenant-specific wording is a custom I18n backend concern and is out of scope.
|
|
212
|
+
|
|
213
|
+
## JavaScript
|
|
214
|
+
|
|
215
|
+
The Stimulus controllers bundled with the gem read their strings from a JSON blob the layout renders once per page, in the current locale:
|
|
216
|
+
|
|
217
|
+
```html
|
|
218
|
+
<meta name="pu-i18n" content="{"clipboard":{"copied":"Copied!"},...}">
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The blob is the `plutonium.js` subtree of the locale merged over the default locale, so overriding a key in YAML changes the bundled JavaScript's text without rebuilding it, and a partially translated locale falls back per key. Host code can read the same blob:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
window.Plutonium.t("plutonium.js.turbo_confirm.confirm")
|
|
225
|
+
window.Plutonium.t("plutonium.js.attachment_input.delete_all", { count: 3 })
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Third-party widgets keep their own locale mechanisms. The blob carries optional pass-through settings under `plutonium.js.libraries`:
|
|
229
|
+
|
|
230
|
+
| Key | Passed to |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `slim_select` | Slim Select `settings` (`placeholderText`, `searchText`, `searchPlaceholder`, `searchingText`) |
|
|
233
|
+
| `flatpickr.locale` | flatpickr's `locale` option. Loading the matching `l10n` bundle is the host's job. |
|
|
234
|
+
| `uppy.strings` | Uppy `locale.strings` |
|
|
235
|
+
| `intl_tel_input` | intl-tel-input `i18n` |
|
|
236
|
+
|
|
237
|
+
## Auth
|
|
238
|
+
|
|
239
|
+
Rodauth's own text (labels, buttons, flashes, email subjects) is translated by the [rodauth-i18n](https://github.com/janko/rodauth-i18n) gem, which Plutonium's Rodauth views pick up unchanged because they render through Rodauth's configuration methods. The mailer templates the Rodauth generator copies into your app are plain ERB and are yours to translate.
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
Turn on missing-translation errors in the test environment so a rendered component with a missing key fails instead of printing its key:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# config/environments/test.rb
|
|
247
|
+
config.i18n.raise_on_missing_translations = true
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Convention lookups are exempt. They probe with `default: nil` and render nothing when a key is absent, which is the intended behaviour, so they never raise.
|
|
251
|
+
|
|
252
|
+
## Adding a language
|
|
253
|
+
|
|
254
|
+
For a second locale, provide:
|
|
255
|
+
|
|
256
|
+
1. A [rails-i18n](https://github.com/svenfuchs/rails-i18n) locale for Rails' own strings, dates and numbers.
|
|
257
|
+
2. A Pagy dictionary for pagination, which Pagy ships for most languages.
|
|
258
|
+
3. `rodauth-i18n` if you use Rodauth.
|
|
259
|
+
4. A `plutonium` namespace translated from the gem's `config/locales/en/*.yml`.
|
|
260
|
+
5. Your models, attributes and field text under the keys above.
|
|
261
|
+
|
|
262
|
+
Plutonium itself ships only `en`.
|
data/docs/reference/index.md
CHANGED
|
@@ -7,14 +7,16 @@ aside: false
|
|
|
7
7
|
<SectionLanding
|
|
8
8
|
eyebrow="Reference"
|
|
9
9
|
title="Every API, in one place."
|
|
10
|
-
lede="The full surface area of Plutonium
|
|
10
|
+
lede="The full surface area of Plutonium: controllers, policies, definitions, fields, interactions, generators."
|
|
11
11
|
mode="categorized"
|
|
12
12
|
:rail="[
|
|
13
13
|
{ group: 'App', items: [
|
|
14
14
|
{ name: 'Overview', link: '/plutonium-core/reference/app/' },
|
|
15
|
+
{ name: 'Configuration', link: '/plutonium-core/reference/configuration' },
|
|
15
16
|
{ name: 'Packages', link: '/plutonium-core/reference/app/packages' },
|
|
16
17
|
{ name: 'Portals', link: '/plutonium-core/reference/app/portals' },
|
|
17
18
|
{ name: 'Generators', link: '/plutonium-core/reference/app/generators' },
|
|
19
|
+
{ name: 'Lite (SQLite) generators', link: '/plutonium-core/reference/generators/lite' },
|
|
18
20
|
]},
|
|
19
21
|
{ group: 'Resource', items: [
|
|
20
22
|
{ name: 'Overview', link: '/plutonium-core/reference/resource/' },
|
|
@@ -22,13 +24,15 @@ aside: false
|
|
|
22
24
|
{ name: 'Definition', link: '/plutonium-core/reference/resource/definition' },
|
|
23
25
|
{ name: 'Query', link: '/plutonium-core/reference/resource/query' },
|
|
24
26
|
{ name: 'Actions', link: '/plutonium-core/reference/resource/actions' },
|
|
25
|
-
{ name: 'Positioning', link: '/plutonium-core/reference/positioning' },
|
|
27
|
+
{ name: 'Positioning', link: '/plutonium-core/reference/resource/positioning' },
|
|
28
|
+
{ name: 'CSV Export', link: '/plutonium-core/reference/resource/export' },
|
|
26
29
|
]},
|
|
27
30
|
{ group: 'Behavior', items: [
|
|
28
31
|
{ name: 'Overview', link: '/plutonium-core/reference/behavior/' },
|
|
29
32
|
{ name: 'Controllers', link: '/plutonium-core/reference/behavior/controllers' },
|
|
30
33
|
{ name: 'Policies', link: '/plutonium-core/reference/behavior/policies' },
|
|
31
34
|
{ name: 'Interactions', link: '/plutonium-core/reference/behavior/interactions' },
|
|
35
|
+
{ name: 'Async interactions', link: '/plutonium-core/reference/behavior/async-interactions' },
|
|
32
36
|
]},
|
|
33
37
|
{ group: 'UI', items: [
|
|
34
38
|
{ name: 'Overview', link: '/plutonium-core/reference/ui/' },
|
|
@@ -65,6 +69,11 @@ aside: false
|
|
|
65
69
|
{ name: 'Positioning', link: '/plutonium-core/reference/kanban/positioning' },
|
|
66
70
|
{ name: 'Authorization', link: '/plutonium-core/reference/kanban/authorization' },
|
|
67
71
|
]},
|
|
72
|
+
{ group: 'Dashboard', items: [
|
|
73
|
+
{ name: 'Overview', link: '/plutonium-core/reference/dashboard/' },
|
|
74
|
+
{ name: 'DSL', link: '/plutonium-core/reference/dashboard/dsl' },
|
|
75
|
+
{ name: 'Registration', link: '/plutonium-core/reference/dashboard/registration' },
|
|
76
|
+
]},
|
|
68
77
|
{ group: 'Testing', items: [
|
|
69
78
|
{ name: 'Overview', link: '/plutonium-core/reference/testing/' },
|
|
70
79
|
]},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Every drag-and-drop move is authorized through the `kanban_move?` method on the resource's policy. The default implementation delegates to `update?`:
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
# Plutonium::Resource::Policy
|
|
8
|
+
# Plutonium::Resource::Policy (built-in default)
|
|
9
9
|
def kanban_move?
|
|
10
10
|
update?
|
|
11
11
|
end
|
|
@@ -29,7 +29,7 @@ end
|
|
|
29
29
|
|
|
30
30
|
## Gating a specific transition (`from` / `to` context)
|
|
31
31
|
|
|
32
|
-
`kanban_move?` is the **single** authorization for every move
|
|
32
|
+
`kanban_move?` is the **single** authorization for every move (plain moves and `enter_interaction:` columns alike). To gate a *specific* transition, read the source and destination columns from the authorization context. They are exposed as the optional `kanban_from` / `kanban_to` policy readers (the `Plutonium::Kanban::Column` objects), and are `nil` for every non-move authorization:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
35
|
class DealPolicy < ResourcePolicy
|
|
@@ -42,28 +42,28 @@ class DealPolicy < ResourcePolicy
|
|
|
42
42
|
end
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Rules take no positional arguments in ActionPolicy
|
|
45
|
+
Rules take no positional arguments in ActionPolicy; the columns arrive via context, which the controller supplies on the `kanban_move?` check (`context: { kanban_from:, kanban_to: }`). This replaces per-column policy methods: an `enter_interaction:` column is authorized by `kanban_move?` too, so there is no separate `mark_lost?`-style predicate to define.
|
|
46
46
|
|
|
47
47
|
Both `kanban_from` and `kanban_to` are **trustworthy** to authorize on. `to` is where the card ends up; and although `from_column` arrives from the client, the move handler **verifies the record actually resides in the claimed source column** before it proceeds (a mismatch snaps the drag back), so a spoofed or stale `from` can never drive a move past a `kanban_from`-based rule.
|
|
48
48
|
|
|
49
49
|
## Read-only board
|
|
50
50
|
|
|
51
|
-
When `kanban_move?` returns `false` for the current user, the board is rendered read-only. Cards are displayed but dragging is disabled
|
|
51
|
+
When `kanban_move?` returns `false` for the current user, the board is rendered read-only. Cards are displayed but dragging is disabled: no drag handles appear and the Stimulus controller does not register drop zones.
|
|
52
52
|
|
|
53
53
|
## Authorization flow on a move
|
|
54
54
|
|
|
55
55
|
When a card is dropped, the server:
|
|
56
56
|
|
|
57
57
|
1. Finds the record within the current authorized scope (the same policy `relation_scope` used by the index action).
|
|
58
|
-
2. Calls `authorize_current!(record, to: :kanban_move?, context: { kanban_from:, kanban_to: })
|
|
59
|
-
3. Verifies the record actually resides in the claimed source column (`from_column` is client-supplied). A mismatch responds 422 and snaps the card back
|
|
60
|
-
4. Validates the drop against the destination column's `accepts:` policy and `locked:` flag. A rejected drop responds with HTTP 422 and re-renders the source column (the Stimulus controller snaps the card back).
|
|
58
|
+
2. Calls `authorize_current!(record, to: :kanban_move?, context: { kanban_from:, kanban_to: })`: the single authorization for the move (an `enter_interaction:` column rides on this same check, with no policy method of its own). A `false` result halts the action with HTTP 403.
|
|
59
|
+
3. Verifies the record actually resides in the claimed source column (`from_column` is client-supplied). A mismatch responds 422 and snaps the card back; this is what makes `kanban_from` safe to authorize on.
|
|
60
|
+
4. Validates the drop against the destination column's `accepts:` policy and the source column's `locked:` flag (a locked column lets no card leave). A rejected drop responds with HTTP 422 and re-renders the source column (the Stimulus controller snaps the card back).
|
|
61
61
|
5. Enforces the destination column's `wip:` limit (cross-column moves only). Exceeding the WIP cap also responds 422.
|
|
62
|
-
6. Calls `on_enter
|
|
62
|
+
6. Calls the source column's `on_exit`, then the destination's `on_enter`, and repositions the record, all inside a transaction.
|
|
63
63
|
|
|
64
64
|
## No permitted attributes for moves
|
|
65
65
|
|
|
66
|
-
Kanban moves do **not** pass through `permitted_attributes_for_update` / `permitted_attributes_for_kanban_move`. The `on_enter` callback is author code that runs with full model access
|
|
66
|
+
Kanban moves do **not** pass through `permitted_attributes_for_update` / `permitted_attributes_for_kanban_move`. The `on_enter` callback is author code that runs with full model access; it is the responsibility of the `on_enter` implementation to assign only the attributes appropriate for a column transition. This is intentional: the callback is trusted Ruby, not user-supplied form data.
|
|
67
67
|
|
|
68
68
|
## Column-level drop policies
|
|
69
69
|
|