plutonium 0.63.0 → 0.65.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 +4 -1
- data/.claude/skills/plutonium-app/SKILL.md +14 -0
- data/.claude/skills/plutonium-dashboard/SKILL.md +161 -0
- data/.claude/skills/plutonium-resource/SKILL.md +94 -14
- data/.claude/skills/plutonium-ui/SKILL.md +22 -1
- data/.standard.yml +1 -1
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +282 -210
- 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/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 +175 -0
- data/docs/blog/introducing-plutonium-i18n.md +91 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +1 -0
- data/docs/guides/kanban.md +2 -2
- 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 +2 -2
- data/docs/reference/app/portals.md +2 -0
- data/docs/reference/configuration.md +84 -27
- 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/i18n.md +239 -0
- data/docs/reference/index.md +10 -1
- data/docs/reference/kanban/dsl.md +1 -1
- data/docs/reference/kanban/positioning.md +2 -2
- data/docs/reference/resource/actions.md +1 -1
- data/docs/reference/resource/definition.md +96 -18
- data/docs/reference/resource/export.md +5 -1
- data/docs/reference/resource/index.md +2 -0
- data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
- data/docs/reference/resource/query.md +2 -0
- data/docs/reference/ui/assets.md +8 -0
- data/docs/reference/ui/displays.md +5 -8
- data/docs/reference/ui/index.md +1 -1
- data/docs/reference/wizard/dsl.md +2 -2
- data/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +3 -3
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +2 -2
- 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/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/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/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 +15 -30
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +91 -0
- 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 +71 -31
- 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
|
Binary file
|
|
@@ -344,7 +344,7 @@ rails g pu:core:install
|
|
|
344
344
|
|
|
345
345
|
### `pu:core:assets`
|
|
346
346
|
|
|
347
|
-
Set up the custom Tailwind + Stimulus toolchain. Installs npm packages, creates `tailwind.config.js`, imports Plutonium CSS, registers Stimulus controllers.
|
|
347
|
+
Set up the custom Tailwind + Stimulus toolchain. Installs npm packages with the app's package manager (bun, yarn 1, yarn 2+, npm or pnpm, detected from the lockfile, else from what is on PATH), creates `tailwind.config.js`, imports Plutonium CSS, registers Stimulus controllers.
|
|
348
348
|
|
|
349
349
|
```bash
|
|
350
350
|
rails g pu:core:assets
|
|
@@ -354,7 +354,7 @@ See [UI › Assets](/reference/ui/assets) for what gets configured.
|
|
|
354
354
|
|
|
355
355
|
### `pu:core:update`
|
|
356
356
|
|
|
357
|
-
Update the plutonium gem and npm package together.
|
|
357
|
+
Update the plutonium gem and npm package together, using the app's package manager.
|
|
358
358
|
|
|
359
359
|
```bash
|
|
360
360
|
rails g pu:core:update
|
|
@@ -327,6 +327,8 @@ For the full multi-tenancy story, see [Tenancy › Entity scoping](/reference/te
|
|
|
327
327
|
|
|
328
328
|
## Dashboard / non-resource pages
|
|
329
329
|
|
|
330
|
+
The generated `dashboard#index` is a plain page listing the registered resources. To replace it with a [dashboard](/guides/dashboards) of metric and chart cards, run `rails g pu:dashboard Home --dest=<portal> --at=/`, which swaps the `root to:` line for a `register_dashboard ... at: "/"` registration.
|
|
331
|
+
|
|
330
332
|
```ruby
|
|
331
333
|
# config/routes.rb
|
|
332
334
|
AdminPortal::Engine.routes.draw do
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
# Configuration
|
|
2
2
|
|
|
3
|
-
Plutonium is configured through `Plutonium.configure` in an initializer.
|
|
3
|
+
Plutonium is configured through `Plutonium.configure` in an initializer. `pu:core:install` writes `config/initializers/plutonium.rb`:
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
|
-
#
|
|
6
|
+
# Configure plutonium
|
|
7
|
+
|
|
7
8
|
Plutonium.configure do |config|
|
|
8
9
|
config.load_defaults 1.0
|
|
9
10
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
config.assets.logo = "plutonium.png"
|
|
15
|
-
config.assets.favicon = "plutonium.ico"
|
|
16
|
-
config.assets.stylesheet = "plutonium.css"
|
|
17
|
-
config.assets.script = "plutonium.min.js"
|
|
11
|
+
# Shell variant: :modern (icon rail), :plain (no rail), or :classic (legacy).
|
|
12
|
+
config.shell = :modern
|
|
13
|
+
# Configure plutonium above.
|
|
18
14
|
end
|
|
19
15
|
```
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
Everything else is opt-in. Read the live config anywhere via `Plutonium.configuration`, and query development mode with `Plutonium.configuration.development?`.
|
|
18
|
+
|
|
19
|
+
Other generators edit this file in place. `pu:core:assets` rewrites two lines to point at your own bundles:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
config.assets.stylesheet = "application"
|
|
23
|
+
config.assets.script = "application"
|
|
24
|
+
```
|
|
22
25
|
|
|
23
26
|
## Versioned defaults
|
|
24
27
|
|
|
@@ -26,41 +29,95 @@ Access the live config anywhere via `Plutonium.configuration`.
|
|
|
26
29
|
config.load_defaults 1.0
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
Applies the baseline defaults for a framework version, and every earlier version in order. Call it first, before any option you set yourself, or the defaults overwrite you. `1.0` is currently the only version. Read back what resolved with `config.defaults_version`, which is `nil` until you call this.
|
|
33
|
+
|
|
34
|
+
Passing a version older than the earliest available raises rather than silently applying nothing.
|
|
30
35
|
|
|
31
|
-
##
|
|
36
|
+
## Core
|
|
32
37
|
|
|
33
38
|
| Option | Default | Description |
|
|
34
39
|
|--------|---------|-------------|
|
|
35
40
|
| `load_defaults(version)` | — | Apply versioned framework defaults. Call first. |
|
|
36
|
-
| `development` | `ENV["PLUTONIUM_DEV"]` | Development mode for the framework itself (local assets, hot reload, verbose errors). Query with `config.development?`.
|
|
37
|
-
| `cache_discovery` | `true` outside `development` env | Cache resource/route discovery. Disable to pick up new resources without a reboot. |
|
|
38
|
-
| `enable_hotreload` | `true` in `development` env | Hot-reload Plutonium components on change. |
|
|
41
|
+
| `development` | `ENV["PLUTONIUM_DEV"]` | Development mode for the framework itself (local assets, hot reload, verbose errors). Query with `config.development?`. Apps rarely set this, see [Development mode](#development-mode). |
|
|
42
|
+
| `cache_discovery` | `true` outside the `development` env | Cache resource/route discovery. Disable to pick up new resources without a reboot. |
|
|
43
|
+
| `enable_hotreload` | `true` in the `development` env | Hot-reload Plutonium components on change. |
|
|
44
|
+
|
|
45
|
+
## Appearance
|
|
46
|
+
|
|
47
|
+
| Option | Default | Description |
|
|
48
|
+
|--------|---------|-------------|
|
|
39
49
|
| `shell` | `:modern` | Chrome style: `:modern` (topbar + icon rail), `:plain` (topbar, no icon rail), or `:classic` (legacy header + sidebar, only for upgrades). See [Layouts](./ui/layouts). |
|
|
50
|
+
| `default_page_width` | `:md` | Width of detail-style pages: the show page and resource forms. One of `:sm` `:md` `:lg` `:xl` `:full` (`:full` opts out of any constraint). Index and table pages are unaffected. Override per-resource with `page_width` / `form_width` / `display_width`, see [Definition › Page width](./resource/definition#page-width). |
|
|
40
51
|
| `navii_host_url` | `"https://api.navii.dev"` | Host of the [Navii](https://navii.dev) avatar service used by [`Avatar`](./ui/components#avatar). The component appends `/avatar/:seed`. Repoint to self-host or proxy. |
|
|
41
|
-
| `auto_eager_load_collections` | `true` | Index pages, kanban boards and CSV exports preload the associations and attachments they render. Set `false` to disable globally, or override `auto_eager_load_collections?` in a controller. See [Performance](/guides/performance). |
|
|
42
|
-
| `default_page_width` | `:md` | Width of detail-style pages — the show page and resource forms. One of `:sm :md :lg :xl :full` (`:full` = unconstrained). Index and table pages are unaffected. Override per-resource with `page_width` / `form_width` / `display_width`; see [Definition › Page width](./resource/definition#page-width). |
|
|
43
|
-
| `wizards.width` | `:md` | Default width of wizard step pages. **Independent of `default_page_width`** — a wizard is a self-contained flow, so widening resource pages leaves wizards alone. Override per wizard with `width`. Same size tokens. |
|
|
44
|
-
| `nested_association_routes` | `:detected` | Where a resource's nested routes come from. `:detected` draws one for every `has_many` / `has_one` whose child is registered. `:declared` draws only what `register_resource ..., associations:` names, so a resource that names none gets none. See [Nested resources › Declaring which associations get routes](./tenancy/nested-resources#declaring-which-associations-get-routes). |
|
|
45
52
|
| `assets.logo` | `"plutonium.png"` | Brand logo asset. See [Assets](./ui/assets). |
|
|
46
53
|
| `assets.favicon` | `"plutonium.ico"` | Favicon asset. |
|
|
47
|
-
| `assets.stylesheet` | `"plutonium.css"` | Stylesheet entry. |
|
|
48
|
-
| `assets.script` | `"plutonium.min.js"` | JavaScript entry. |
|
|
54
|
+
| `assets.stylesheet` | `"plutonium.css"` | Stylesheet entry. `pu:core:assets` sets this to `"application"`. |
|
|
55
|
+
| `assets.script` | `"plutonium.min.js"` | JavaScript entry. `pu:core:assets` sets this to `"application"`. |
|
|
56
|
+
|
|
57
|
+
## Rendering and routing
|
|
58
|
+
|
|
59
|
+
| Option | Default | Description |
|
|
60
|
+
|--------|---------|-------------|
|
|
61
|
+
| `auto_eager_load_collections` | `true` | Index pages, kanban boards and CSV exports preload the associations and attachments they render. The field set comes from the policy, so it is known before the collection loads. Set `false` to disable globally, or override `auto_eager_load_collections?` in a controller. See [Performance](/guides/performance). |
|
|
62
|
+
| `nested_association_routes` | `:detected` | Where a resource's nested routes come from. `:detected` draws one for every `has_many` / `has_one` whose child is a registered resource. `:declared` draws only what `register_resource ..., associations:` names, so a resource that names none gets none. Any other value raises `ArgumentError` rather than drawing the wrong route table for a typo. See [Nested resources](./tenancy/nested-resources#declaring-which-associations-get-routes). |
|
|
63
|
+
| `default_currency_unit` | `nil` | Symbol used when rendering a currency value with no unit set on `has_cents` or the display. `nil` falls back to the i18n `number.currency.format.unit` when the locale defines one, otherwise no symbol. Set a literal like `"£"`, or `false` (or `""`) for no symbol application-wide. See [`has_cents`](./resource/model#has-cents) and [Currency fields](./ui/forms#currency-fields). |
|
|
64
|
+
| `default_phone_country` | `nil` | Default country (ISO2, e.g. `"gh"`) for `as: :phone` inputs that set no `initial_country:`. `nil` leaves it to intl-tel-input, with no country preselected. Stored verbatim; `config.normalized_default_phone_country` returns it downcased, so `"GH"` and `"gh"` are interchangeable. See [Phone fields](./ui/forms#phone-fields). |
|
|
65
|
+
|
|
66
|
+
## Attachments
|
|
67
|
+
|
|
68
|
+
`attachment_backend` picks which library stages a file that travels as a plain string: `:active_storage` or `:shrine`. It is the shared default, and each subsystem layers its own override on top, so setting it once covers both and setting one of theirs narrows it to that subsystem.
|
|
69
|
+
|
|
70
|
+
| Option | Default | Description |
|
|
71
|
+
|--------|---------|-------------|
|
|
72
|
+
| `attachment_backend` | `nil` | Shared default for staged attachments. `nil` auto-detects. |
|
|
73
|
+
| `wizards.attachment_backend` | `nil` | Overrides the above for wizard attachment fields. `nil` falls through. |
|
|
74
|
+
| `async_interactions.attachment_backend` | `nil` | Overrides the above for run dispatch. `nil` falls through. |
|
|
75
|
+
|
|
76
|
+
Resolution runs first match wins:
|
|
77
|
+
|
|
78
|
+
1. The field's own `backend:` option.
|
|
79
|
+
2. The subsystem setting, `wizards.attachment_backend` or `async_interactions.attachment_backend`.
|
|
80
|
+
3. `config.attachment_backend`.
|
|
81
|
+
4. Auto-detection: `:shrine` if `ActiveShrine` is loaded, else `:active_storage`.
|
|
82
|
+
|
|
83
|
+
Only plain uploads are affected. A direct-upload field already arrives as a token and ignores all of this.
|
|
84
|
+
|
|
85
|
+
## Wizards
|
|
86
|
+
|
|
87
|
+
`enabled` gates the subsystem and its migrations. Full detail in [Wizards › Storage & config](./wizard/storage-config).
|
|
88
|
+
|
|
89
|
+
| Option | Default | Description |
|
|
90
|
+
|--------|---------|-------------|
|
|
91
|
+
| `wizards.enabled` | `false` | Enable wizards and their migrations. See [Enabling the subsystem](./wizard/storage-config#enabling-the-subsystem). |
|
|
92
|
+
| `wizards.width` | `:md` | Width of wizard step pages. **Independent of `default_page_width`**: a wizard is a self-contained flow, so widening resource pages leaves wizards where they are. Same size tokens. Override per wizard with `width`. |
|
|
93
|
+
| `wizards.cleanup_after` | `14.days` | How long completed and abandoned sessions are kept before `SweepJob` removes them. See [Cleanup & the SweepJob](./wizard/storage-config#cleanup-the-sweepjob). |
|
|
94
|
+
| `wizards.database` | `:primary` | Which database the wizard tables live in. |
|
|
95
|
+
| `wizards.encrypt_data` | `false` | Encrypt every wizard's staged `data` at rest. Off by default because it needs ActiveRecord encryption keys configured. A wizard can still opt in with `encrypt_data` or out with `encrypt_data false` regardless. See [Encryption](./wizard/storage-config#encryption). |
|
|
96
|
+
| `wizards.attachment_backend` | `nil` | See [Attachments](#attachments). |
|
|
97
|
+
|
|
98
|
+
## Async interactions
|
|
99
|
+
|
|
100
|
+
`enabled` gates the runs subsystem and its migrations. Full detail in [Async interactions](./behavior/async-interactions).
|
|
101
|
+
|
|
102
|
+
| Option | Default | Description |
|
|
103
|
+
|--------|---------|-------------|
|
|
104
|
+
| `async_interactions.enabled` | `false` | Enable persisted runs and their migrations. See [Enabling](./behavior/async-interactions#enabling). |
|
|
105
|
+
| `async_interactions.queue` | `:default` | ActiveJob queue for run jobs. |
|
|
106
|
+
| `async_interactions.stall_after` | `1.hour` | How long a run may sit with no progress write before `ReapJob` treats it as stalled. See [Stalled runs and ReapJob](./behavior/async-interactions#stalled-runs-and-reapjob). |
|
|
107
|
+
| `async_interactions.attachment_backend` | `nil` | See [Attachments](#attachments). |
|
|
49
108
|
|
|
50
109
|
## Development mode
|
|
51
110
|
|
|
52
|
-
`config.development?` is driven by the `PLUTONIUM_DEV` environment variable, not
|
|
111
|
+
`config.development?` is driven by the `PLUTONIUM_DEV` environment variable, not by the initializer. It is for working **on the Plutonium gem**: it uses local `src/` assets, enables hot reloading, and shows more detailed errors. Applications leave it unset.
|
|
53
112
|
|
|
54
113
|
```bash
|
|
55
114
|
export PLUTONIUM_DEV=1
|
|
56
115
|
```
|
|
57
116
|
|
|
58
|
-
## Assets
|
|
59
|
-
|
|
60
|
-
Asset entries live under `config.assets` and point the framework at your compiled stylesheet/script and brand imagery. The `pu:core:assets` generator wires these up. See [Assets](./ui/assets) for the full asset/Tailwind/Stimulus setup.
|
|
61
|
-
|
|
62
117
|
## Related
|
|
63
118
|
|
|
64
119
|
- [Assets](./ui/assets) — stylesheet, script, Tailwind, and design tokens
|
|
65
120
|
- [Layouts](./ui/layouts) — the `shell` option and ejecting chrome
|
|
66
121
|
- [Components › Avatar](./ui/components#avatar) — `navii_host_url`
|
|
122
|
+
- [Wizards › Storage & config](./wizard/storage-config) — the `wizards.*` settings in context
|
|
123
|
+
- [Async interactions](./behavior/async-interactions) — the `async_interactions.*` settings in context
|
|
@@ -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; no frame, never refreshes. See the [guide](/guides/dashboards#lazy-and-inline-cards) |
|
|
52
|
+
| `refresh:` | Integer seconds, or `false` | dashboard `refresh` | Reload interval; requires `lazy: true`. `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.
|
|
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.
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
## Derived labels
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
en:
|
|
129
|
+
plutonium:
|
|
130
|
+
actions:
|
|
131
|
+
blogging/post:
|
|
132
|
+
publish: "Publish now"
|
|
133
|
+
scopes:
|
|
134
|
+
blogging/post:
|
|
135
|
+
drafts: "Drafts"
|
|
136
|
+
filters:
|
|
137
|
+
blogging/post:
|
|
138
|
+
author: "Written by"
|
|
139
|
+
kanban_columns:
|
|
140
|
+
blogging/post:
|
|
141
|
+
in_review: "In review"
|
|
142
|
+
wizard_steps:
|
|
143
|
+
onboarding_wizard:
|
|
144
|
+
billing: "Billing details"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The model segment is the resource's `model_name.i18n_key`. For wizard steps it is the wizard's `model_name.i18n_key`.
|
|
148
|
+
|
|
149
|
+
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:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
en:
|
|
153
|
+
activerecord:
|
|
154
|
+
attributes:
|
|
155
|
+
blogging/post:
|
|
156
|
+
status/draft: "Draft"
|
|
157
|
+
status/published: "Live"
|
|
158
|
+
plutonium:
|
|
159
|
+
values:
|
|
160
|
+
blogging/post:
|
|
161
|
+
status:
|
|
162
|
+
archived: "Archived"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Portals
|
|
166
|
+
|
|
167
|
+
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.
|
|
168
|
+
|
|
169
|
+
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:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
# packages/admin_portal/config/locales/en.yml
|
|
173
|
+
en:
|
|
174
|
+
plutonium:
|
|
175
|
+
portals:
|
|
176
|
+
admin_portal:
|
|
177
|
+
fields:
|
|
178
|
+
blogging/post:
|
|
179
|
+
title:
|
|
180
|
+
placeholder: "Internal working title"
|
|
181
|
+
actions:
|
|
182
|
+
blogging/post:
|
|
183
|
+
publish: "Publish to site"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
Locale keys vary by portal, never by tenant. Tenant-specific wording is a custom I18n backend concern and is out of scope.
|
|
189
|
+
|
|
190
|
+
## JavaScript
|
|
191
|
+
|
|
192
|
+
The Stimulus controllers bundled with the gem read their strings from a JSON blob the layout renders once per page, in the current locale:
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<meta name="pu-i18n" content="{"clipboard":{"copied":"Copied!"},...}">
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
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:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
window.Plutonium.t("plutonium.js.turbo_confirm.confirm")
|
|
202
|
+
window.Plutonium.t("plutonium.js.attachment_input.delete_all", { count: 3 })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Third-party widgets keep their own locale mechanisms. The blob carries optional pass-through settings under `plutonium.js.libraries`:
|
|
206
|
+
|
|
207
|
+
| Key | Passed to |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `slim_select` | Slim Select `settings` (`placeholderText`, `searchText`, `searchPlaceholder`, `searchingText`) |
|
|
210
|
+
| `flatpickr.locale` | flatpickr's `locale` option. Loading the matching `l10n` bundle is the host's job. |
|
|
211
|
+
| `uppy.strings` | Uppy `locale.strings` |
|
|
212
|
+
| `intl_tel_input` | intl-tel-input `i18n` |
|
|
213
|
+
|
|
214
|
+
## Auth
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
|
|
218
|
+
## Testing
|
|
219
|
+
|
|
220
|
+
Turn on missing-translation errors in the test environment so a rendered component with a missing key fails instead of printing its key:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# config/environments/test.rb
|
|
224
|
+
config.i18n.raise_on_missing_translations = true
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
## Adding a language
|
|
230
|
+
|
|
231
|
+
For a second locale, provide:
|
|
232
|
+
|
|
233
|
+
1. A [rails-i18n](https://github.com/svenfuchs/rails-i18n) locale for Rails' own strings, dates and numbers.
|
|
234
|
+
2. A Pagy dictionary for pagination, which Pagy ships for most languages.
|
|
235
|
+
3. `rodauth-i18n` if you use Rodauth.
|
|
236
|
+
4. A `plutonium` namespace translated from the gem's `config/locales/en/*.yml`.
|
|
237
|
+
5. Your models, attributes and field text under the keys above.
|
|
238
|
+
|
|
239
|
+
Plutonium itself ships only `en`.
|