plutonium 0.64.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/CHANGELOG.md +59 -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 +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/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 +4 -1
- 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 +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
data/docs/reference/index.md
CHANGED
|
@@ -12,9 +12,11 @@ aside: false
|
|
|
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
|
]},
|
|
@@ -49,7 +49,7 @@ Default: `nil` (unlimited).
|
|
|
49
49
|
|
|
50
50
|
Controls how card positions are persisted after a drag-and-drop. Three modes:
|
|
51
51
|
|
|
52
|
-
**Inherited from the definition.** `position_on` is the same verb outside `kanban do…end`, where it makes the resource's [table and grid drag-reorderable](/reference/positioning). A board with no `position_on` of its own uses the **definition's**, falling back to the historic default (`:position`, Mode A). Declaring it inside the board overrides that. Resolution is lazy, so declaration order in the class body does not matter.
|
|
52
|
+
**Inherited from the definition.** `position_on` is the same verb outside `kanban do…end`, where it makes the resource's [table and grid drag-reorderable](/reference/resource/positioning). A board with no `position_on` of its own uses the **definition's**, falling back to the historic default (`:position`, Mode A). Declaring it inside the board overrides that. Resolution is lazy, so declaration order in the class body does not matter.
|
|
53
53
|
|
|
54
54
|
#### Mode A — delegate to `Plutonium::Positioning::Model` (default)
|
|
55
55
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Kanban Positioning
|
|
2
2
|
|
|
3
3
|
::: tip Positioning is not kanban-only
|
|
4
|
-
The model concern, the arithmetic and the `position_on` modes on this page are shared with **table and grid drag-to-reorder** — see [Positioning & drag-to-reorder](/reference/positioning) for the index-surface half of the feature (the grip, the `reposition` endpoint, `reposition?`, and board inheritance).
|
|
4
|
+
The model concern, the arithmetic and the `position_on` modes on this page are shared with **table and grid drag-to-reorder** — see [Positioning & drag-to-reorder](/reference/resource/positioning) for the index-surface half of the feature (the grip, the `reposition` endpoint, `reposition?`, and board inheritance).
|
|
5
5
|
:::
|
|
6
6
|
|
|
7
7
|
Plutonium uses **decimal fractional positioning** for kanban card ordering. A drop writes a single decimal position (the midpoint between its neighbors), so the common case touches exactly one row — no bulk renumbering. The one exception is rare **rebalancing**: when the same slot has been subdivided ~20 times and the gap between two neighbors shrinks below `1e-6`, Plutonium renumbers that one scope group back to clean integers before inserting (see [Gap exhaustion](#rebalancing)).
|
|
@@ -152,7 +152,7 @@ end
|
|
|
152
152
|
|
|
153
153
|
Plutonium orders the column by `sort_order` for display; your block is responsible only for persisting the new value. The block is called with a single `Plutonium::Positioning::Move` argument (still reachable under its original name, `Plutonium::Kanban::Positioning::Move`) — it is NOT `instance_exec`'d, so `self` is the proc's original binding.
|
|
154
154
|
|
|
155
|
-
On a table or grid the same block runs with `move.column` set to `nil`, since those surfaces have no columns. See [Mode B](/reference/positioning#mode-b) for a worked `acts_as_list` example.
|
|
155
|
+
On a table or grid the same block runs with `move.column` set to `nil`, since those surfaces have no columns. See [Mode B](/reference/resource/positioning#mode-b) for a worked `acts_as_list` example.
|
|
156
156
|
|
|
157
157
|
### Mode C — disabled
|
|
158
158
|
|
|
@@ -181,7 +181,7 @@ An action declared `hidden: true` renders in **no** toolbar, row dropdown, card,
|
|
|
181
181
|
action :reposition, hidden: true
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
The use case is an endpoint reached by **something other than a button** — a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
|
|
184
|
+
The use case is an endpoint reached by **something other than a button** — a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/resource/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
|
|
185
185
|
|
|
186
186
|
### `hidden:` vs `condition: -> { false }`
|
|
187
187
|
|
|
@@ -159,6 +159,8 @@ input :title,
|
|
|
159
159
|
wrapper: {class: "col-span-full"}
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
+
Label, hint, placeholder and description need no declaration to be translatable: leave them out and Plutonium looks them up under `activerecord.attributes` and `plutonium.fields.<model>.<attr>` respectively. An explicit value can be a translation too, via the class-level `t`. See [Internationalization](../i18n#fields-placeholder-hint-description).
|
|
163
|
+
|
|
162
164
|
## Select / choices
|
|
163
165
|
|
|
164
166
|
### Static
|
|
@@ -196,6 +198,13 @@ display :rejection_reason, condition: -> { object.rejected? }
|
|
|
196
198
|
field :debug_info, condition: -> { Rails.env.development? }
|
|
197
199
|
```
|
|
198
200
|
|
|
201
|
+
A surface's own `condition:` wins. Otherwise the form, the show page and the table
|
|
202
|
+
each fall back to the `field` condition, so it runs everywhere. The table has no
|
|
203
|
+
`object`, which means a `field` condition must not reference the record:
|
|
204
|
+
`-> { object.published? }` works on the form and show page and raises on the index.
|
|
205
|
+
Keep a `field` condition to context that exists on every surface (`Rails.env`, a
|
|
206
|
+
feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
|
|
207
|
+
|
|
199
208
|
::: warning UI state, not authorization
|
|
200
209
|
`condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*` — see [Behavior › Policy](/reference/behavior/policies).
|
|
201
210
|
:::
|
|
@@ -293,24 +302,41 @@ input :birth_date do |f|
|
|
|
293
302
|
end
|
|
294
303
|
```
|
|
295
304
|
|
|
296
|
-
###
|
|
305
|
+
### Inline markup in a display
|
|
297
306
|
|
|
298
|
-
|
|
307
|
+
A `display` block is evaluated in the display component's Phlex context, so HTML tag
|
|
308
|
+
methods (`span`, `div`, `a`) and Tailwind classes are first-class, and `field.object`
|
|
309
|
+
is the record. It renders once, even for a `has_many` field.
|
|
299
310
|
|
|
300
311
|
```ruby
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
display :priority do |field|
|
|
313
|
+
variant = {"high" => "danger", "medium" => "warning"}.fetch(field.value.to_s, "info")
|
|
314
|
+
span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
|
|
315
|
+
end
|
|
303
316
|
|
|
304
|
-
#
|
|
305
|
-
display :
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
when 'medium' then span(class: "badge badge-warning") { "Medium" }
|
|
309
|
-
else span(class: "badge badge-info") { "Low" }
|
|
317
|
+
# Reaches the record; renders once for a collection
|
|
318
|
+
display :tags do |field|
|
|
319
|
+
div(class: "flex flex-wrap gap-1.5") do
|
|
320
|
+
field.object.tags.each { |tag| span(class: "pu-badge") { tag.name } }
|
|
310
321
|
end
|
|
311
|
-
|
|
322
|
+
end
|
|
312
323
|
```
|
|
313
324
|
|
|
325
|
+
Phlex also renders whatever the block returns, so a block that emits markup must end
|
|
326
|
+
with a tag call (or `nil`). The `div` wrapper above does that. A bare
|
|
327
|
+
`tags.each { span … }` as the last line returns the collection, which renders a
|
|
328
|
+
second time as text.
|
|
329
|
+
|
|
330
|
+
::: warning `as: :phlexi_render` is deprecated
|
|
331
|
+
`display :x, as: :phlexi_render, with: ...` emits a deprecation warning and will be
|
|
332
|
+
removed. Use the block form above instead. The old `with:` proc did not run in a
|
|
333
|
+
Phlex context (its `self` was the definition, so `span`/`div` in the body raised), it
|
|
334
|
+
received the value already stringified so it could not reach the record, and it fired
|
|
335
|
+
once per element for a `has_many` field. The block form has none of those limits. This
|
|
336
|
+
deprecation applies only to the `as: :phlexi_render` display option; the internal
|
|
337
|
+
`phlexi_render` helper is unaffected.
|
|
338
|
+
:::
|
|
339
|
+
|
|
314
340
|
See [UI › Components](/reference/ui/components) for writing reusable Phlex components.
|
|
315
341
|
|
|
316
342
|
### Custom component class
|
|
@@ -338,27 +364,65 @@ See [UI › Components › Field components](/reference/ui/components#field-comp
|
|
|
338
364
|
|
|
339
365
|
## Column options
|
|
340
366
|
|
|
367
|
+
`align:` and `label:` configure the column header. They can be declared on `field`,
|
|
368
|
+
`display` or `column`; the most specific wins.
|
|
369
|
+
|
|
341
370
|
```ruby
|
|
371
|
+
field :amount, align: :end # every surface that has a notion of alignment
|
|
342
372
|
column :title, align: :start # default
|
|
343
373
|
column :status, align: :center
|
|
344
|
-
column :amount,
|
|
374
|
+
column :amount, label: "Total"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### What a column inherits from the display
|
|
378
|
+
|
|
379
|
+
With no `column` declared, the table column renders exactly as the display does: the
|
|
380
|
+
display's `as:` and its attributes flow to the cell, so `display :status, as: :badge,
|
|
381
|
+
colors: {…}` colors the badge on the show page and in the table.
|
|
382
|
+
|
|
383
|
+
A `column` that only touches the header (`align:`, `label:`, `condition:`) keeps that
|
|
384
|
+
inheritance. A `column` that says anything about rendering (an `as:`, a component
|
|
385
|
+
attribute, or a block) renders alone: its own `as:` and attributes, nothing from the
|
|
386
|
+
display.
|
|
387
|
+
|
|
388
|
+
A `condition:` on `field` or `column` hides the column. A `condition:` on `display`
|
|
389
|
+
applies to the show page only, because it may reference the record and the table has
|
|
390
|
+
no single one.
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
display :status, as: :badge, colors: {published: :accent}
|
|
394
|
+
column :status, align: :center # still a badge, still colored
|
|
395
|
+
column :status, as: :badge # a badge, but with default colors
|
|
396
|
+
column :status, formatter: ->(v) { v.upcase } # plain text; the display is out
|
|
345
397
|
```
|
|
346
398
|
|
|
347
399
|
### Value formatting
|
|
348
400
|
|
|
349
401
|
`formatter:` receives just the value. Use a block when you need the full record.
|
|
402
|
+
A column block runs in the table page's Phlex context, the same way a display block
|
|
403
|
+
runs in the show page's: it can emit markup directly, or return a String, a number
|
|
404
|
+
or a component. `self` is the table page, so `current_user`, `helpers` and
|
|
405
|
+
`resource_definition` are available, but methods defined on the definition class are
|
|
406
|
+
not; reach through the record you are passed instead.
|
|
350
407
|
|
|
351
408
|
```ruby
|
|
352
409
|
column :description, formatter: ->(value) { value&.truncate(30) }
|
|
353
410
|
column :price, formatter: ->(value) { "$%.2f" % value if value }
|
|
354
|
-
column :status, formatter: ->(value) { value&.humanize&.upcase }
|
|
355
411
|
|
|
356
|
-
# Block
|
|
412
|
+
# Block with full record access: emit markup...
|
|
413
|
+
column :status do |record|
|
|
414
|
+
span(class: "pu-badge") { record.status.humanize }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# ...or return a String
|
|
357
418
|
column :full_name do |record|
|
|
358
419
|
"#{record.first_name} #{record.last_name}"
|
|
359
420
|
end
|
|
360
421
|
```
|
|
361
422
|
|
|
423
|
+
As with display blocks, a block that emits markup must end with a tag call (or
|
|
424
|
+
`nil`), because Phlex renders the return value too.
|
|
425
|
+
|
|
362
426
|
## Nested inputs
|
|
363
427
|
|
|
364
428
|
Inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
|
|
@@ -580,11 +644,25 @@ Section options previously took a **zero-argument** proc evaluated against the f
|
|
|
580
644
|
It fails loudly, never silently. `condition:` is unchanged — it is still evaluated against the form and still reads `object` with no argument.
|
|
581
645
|
:::
|
|
582
646
|
|
|
583
|
-
A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
|
|
647
|
+
A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
|
|
648
|
+
|
|
649
|
+
The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records — re-evaluated on `pre_submit` — can be grouped in a section without any extra wiring:
|
|
650
|
+
|
|
651
|
+
```ruby
|
|
652
|
+
form_layout do
|
|
653
|
+
section :timing, :duration, :starts_on, label: "Timing" # gone when both are hidden
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
input :employment_type, as: :select, pre_submit: true, choices: %w[full_time contract]
|
|
657
|
+
input :duration, condition: -> { object.contract? }
|
|
658
|
+
input :starts_on, condition: -> { object.contract? }
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
A section's own `condition:` still hides it as a unit, whatever its fields say:
|
|
584
662
|
|
|
585
663
|
```ruby
|
|
586
664
|
section :shipping, :address, :city, :postcode,
|
|
587
|
-
condition: -> { object.requires_shipping? }
|
|
665
|
+
condition: -> { object.requires_shipping? }
|
|
588
666
|
```
|
|
589
667
|
|
|
590
668
|
### `ungrouped(**opts)`
|
|
@@ -664,7 +742,7 @@ end
|
|
|
664
742
|
|
|
665
743
|
## Display layout
|
|
666
744
|
|
|
667
|
-
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules — first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections dropped entirely — applied to the show page instead of the form.
|
|
745
|
+
The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules — first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely — applied to the show page instead of the form.
|
|
668
746
|
|
|
669
747
|
```ruby
|
|
670
748
|
class PostDefinition < ResourceDefinition
|
|
@@ -770,7 +848,7 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
|
|
|
770
848
|
|
|
771
849
|
- **Modals are unaffected** — a dialog sizes itself via `modal_size`.
|
|
772
850
|
- **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
|
|
773
|
-
- **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `default_page_width`, so
|
|
851
|
+
- **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `:md` independently of `default_page_width`, so widening resource pages leaves wizard steps where they are. Set both if you want them to match.
|
|
774
852
|
|
|
775
853
|
## File uploads
|
|
776
854
|
|
|
@@ -90,5 +90,9 @@ For a column **without** an `export` block, the value is read straight off the r
|
|
|
90
90
|
intercepting the streamed download.
|
|
91
91
|
- **CSV/formula injection** is neutralized: any cell whose value begins with `=`, `+`,
|
|
92
92
|
`-`, `@`, or a leading tab/CR is prefixed with a single quote so spreadsheet apps import
|
|
93
|
-
it as literal text instead of executing it as a formula.
|
|
93
|
+
it as literal text instead of executing it as a formula. The check is run against the
|
|
94
|
+
NFKC-normalized value, so full-width (double-byte) variants (`=`, `+`, `-`, `@`) that
|
|
95
|
+
East Asian locales treat as formula initiators are neutralized too; the original
|
|
96
|
+
(un-normalized) value is what gets the quote prefix, so legitimate full-width content
|
|
97
|
+
is preserved verbatim.
|
|
94
98
|
- `csv` is a runtime dependency of Plutonium (it is no longer a Ruby default gem on 3.4+).
|
|
@@ -42,6 +42,8 @@ A `field :title` with no options that matches what Plutonium would auto-detect i
|
|
|
42
42
|
- [Definition](./definition) — fields, inputs, displays, columns, page chrome, metadata panel, index views
|
|
43
43
|
- [Query](./query) — search, filters, scopes, sorting
|
|
44
44
|
- [Actions](./actions) — custom actions, bulk actions, interaction integration
|
|
45
|
+
- [Positioning & drag-to-reorder](./positioning) — `positioned_on`, `position_on`, the drag grip, `reposition?`
|
|
46
|
+
- [CSV Export](./export) — streamed export, opt-in through the policy
|
|
45
47
|
|
|
46
48
|
## Related
|
|
47
49
|
|
|
@@ -469,7 +469,8 @@ The client moves the row **optimistically**, so the response is deliberately qui
|
|
|
469
469
|
|---|---|
|
|
470
470
|
| Clean Mode A drop, both neighbours resolved | `204 No Content` — nothing repaints |
|
|
471
471
|
| `reposition!` had to rebalance the group | `200` + turbo-stream of the collection |
|
|
472
|
-
| A neighbour did not resolve, or belongs to another positioning group | `200` + stream |
|
|
472
|
+
| A neighbour did not resolve, or belongs to another positioning group, but the other one anchored the drop | `200` + stream — the record **did** move, so there is nothing to explain |
|
|
473
|
+
| Both neighbours rejected | `200` + stream, **no write** — plus a toast when the cause was a foreign group rather than transient drift |
|
|
473
474
|
| Mode B (opaque block write) | `200` + stream |
|
|
474
475
|
| `reposition?` denied | `403` + stream + toast (the row snaps back) |
|
|
475
476
|
| `index?` denied | `403`, **no body** — you may not see the list, so the refusal must not carry it to you |
|
|
@@ -84,6 +84,8 @@ For large tables, write an explicit `search` block backed by a trigram or full-t
|
|
|
84
84
|
|
|
85
85
|
Six built-in filter types. Use the shorthand symbol or the full class name.
|
|
86
86
|
|
|
87
|
+
`search`, `scope`, `sort_fields`, and `sort_directions` are reserved filter names — they're built-in controls in the `q[<name>]` namespace, so `filter :scope` raises `ArgumentError`.
|
|
88
|
+
|
|
87
89
|
| Type | Symbol | Params in URL | Options |
|
|
88
90
|
|---|---|---|---|
|
|
89
91
|
| Text | `:text` | `query` | `predicate:` |
|
data/docs/reference/ui/assets.md
CHANGED
|
@@ -38,6 +38,14 @@ This:
|
|
|
38
38
|
4. Registers Plutonium's Stimulus controllers.
|
|
39
39
|
5. Updates Plutonium config to point at your asset files.
|
|
40
40
|
|
|
41
|
+
### Package managers
|
|
42
|
+
|
|
43
|
+
The generator installs with the package manager `rails javascript:build` will use. The lockfile decides: `bun.lock` or `bun.lockb` means bun, `pnpm-lock.yaml` pnpm, `package-lock.json` npm, `yarn.lock` yarn. With no lockfile, `bun.config.js` means bun, and otherwise the first of bun, yarn, pnpm or npm found on PATH wins (yarn if none is). That is why `rails new -j esbuild` on a machine with bun installed produces a bun app. When the app's jsbundling-rails provides `Jsbundling::PackageManager` (its `main` branch, not yet in a release as of 1.3.1), the generator defers to it instead; that detector ignores `yarn.lock` and picks by PATH. Every package goes through one `add` with versions inline (`tailwindcss@latest`, `@radioactive-labs/plutonium@^<gem version>`), which yarn 1, yarn 2+, bun, npm and pnpm all understand.
|
|
44
|
+
|
|
45
|
+
Yarn 2+ apps get `nodeLinker: node-modules` written to `.yarnrc.yml` if no linker is set. Tailwind's PostCSS plugin does not load under Plug'n'Play.
|
|
46
|
+
|
|
47
|
+
`pu:core:update` and `pu:docker:install` use the same detection, so the Dockerfile installs bun, yarn 1, or yarn 2+ via corepack to match the app.
|
|
48
|
+
|
|
41
49
|
## Tailwind config
|
|
42
50
|
|
|
43
51
|
Generated `tailwind.config.js`:
|
|
@@ -53,14 +53,11 @@ class PostDefinition < ResourceDefinition
|
|
|
53
53
|
StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
display :priority
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
else span(class: "badge badge-info") { "Low" }
|
|
62
|
-
end
|
|
63
|
-
}
|
|
56
|
+
# Block that emits markup directly (evaluated in the display's Phlex context)
|
|
57
|
+
display :priority do |field|
|
|
58
|
+
variant = {"high" => "danger", "medium" => "warning"}.fetch(field.value.to_s, "info")
|
|
59
|
+
span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
|
|
60
|
+
end
|
|
64
61
|
|
|
65
62
|
# Field component class — built as ChartComponent.new(field, **attributes),
|
|
66
63
|
# so it subclasses Phlexi::Display::Components::Base and reads `field`.
|
data/docs/reference/ui/index.md
CHANGED
|
@@ -6,7 +6,7 @@ Plutonium uses [Phlex](https://www.phlex.fun/) for all view components and Tailw
|
|
|
6
6
|
|
|
7
7
|
- [Pages](./pages) — `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
|
|
8
8
|
- [Forms](./forms) — `Form` class, field builder, association inputs (typeahead + inline add), themes
|
|
9
|
-
- [Displays](./displays) — `Display` class, custom rendering,
|
|
9
|
+
- [Displays](./displays) — `Display` class, custom rendering, block-form displays
|
|
10
10
|
- [Tables](./tables) — `Table` class, custom rendering, search/scopes bar
|
|
11
11
|
- [Components](./components) — built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
|
|
12
12
|
- [Layouts](./layouts) — shell config, ejecting chrome, custom `ResourceLayout` class
|
|
@@ -30,7 +30,7 @@ For task-oriented walkthroughs, start with the [Wizards guide](/guides/wizards).
|
|
|
30
30
|
| `one_time` | Retain the completed row at the `concurrency_key` (blocks restart, gate-able). **Requires a `concurrency_key`.** Omit → row deleted on completion (repeatable). See [One-time wizards](/reference/wizard/one-time). |
|
|
31
31
|
| `completed do \|wizard\| … end` | Custom body for the "already completed" page a finished **one-time** wizard shows when re-opened (replaces the default confirmation). See [`completed`](#completed) below and [One-time wizards](/reference/wizard/one-time#re-opening-a-completed-wizard). |
|
|
32
32
|
| `encrypt_data` | Encrypt the staged `data` column at rest using ActiveRecord's encryption keys (off by default), for flows that stage PII. Requires `active_record.encryption` keys — see [Storage & config](/reference/wizard/storage-config#encryption). |
|
|
33
|
-
| `width <size>` | Width of this wizard's step pages: `:sm` `:md` `:lg` `:xl` `:full`.
|
|
33
|
+
| `width <size>` | Width of this wizard's step pages: `:sm` `:md` `:lg` `:xl` `:full`. Inherits to subclasses; otherwise `config.wizards.width` (default `:md`). Deliberately independent of `config.default_page_width` — see [Storage & config](/reference/wizard/storage-config). |
|
|
34
34
|
| `anonymous` | Opt into **guest (unauthenticated) access** — the wizard runs pre-login (auth is required otherwise). The guest's identity is a server-minted run-id in the Rails session; it crosses the auth boundary only at its terminal `execute`. Mount it `public: true` (the default for `anonymous`). **Mutually exclusive with `concurrency_key`/`one_time`** — a guest is already session-keyed and repeatable, so declaring both raises (whichever is declared last). See [Authentication](/reference/wizard/anchoring-resume#authentication). |
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
@@ -235,7 +235,7 @@ What it renders depends on completion state and the `summary:` / block options:
|
|
|
235
235
|
| `summary:` | Show the auto-summary of completed steps (default `true`). When `false`, the complete-state body is your block — or the built-in "ready to complete" panel if there's no block. The summary always renders in the incomplete state. |
|
|
236
236
|
| `header:` | Show the step-header section (the label plus the "check everything over" prompt, which only appears when the summary is shown) above the body (default `true`). `false` drops it for a chromeless finish. |
|
|
237
237
|
|
|
238
|
-
The auto-summary renders each field through the display pipeline, honoring the input's declared `as:`/`label
|
|
238
|
+
The auto-summary renders each field through the display pipeline, honoring the input's declared `as:`/`label:`. An input hidden by its own `condition:` is left out, just as it was left off the step, so the summary never shows an empty row for a question the user wasn't asked:
|
|
239
239
|
|
|
240
240
|
- a **choice input** (`select`/`radio_buttons` with `choices:`) resolves the stored value back to its label (`"pro"` → `"Pro"`) using the same choice mapper the form uses, so the recap matches what the user picked;
|
|
241
241
|
- an **`as: :currency`** input formats the value as currency (`"1500.5"` → `"$1,500.50"`) rather than echoing a bare decimal — pass `unit:` on the input (the data snapshot has no `has_cents` reflection to infer it from).
|
data/esbuild.config.js
CHANGED
|
@@ -6,7 +6,8 @@ if (process.argv.includes("--dev")) {
|
|
|
6
6
|
_context({
|
|
7
7
|
outdir: "src/build",
|
|
8
8
|
entryPoints: [
|
|
9
|
-
"src/js/plutonium.js"
|
|
9
|
+
"src/js/plutonium.js",
|
|
10
|
+
"src/js/plutonium-charts.js"
|
|
10
11
|
],
|
|
11
12
|
plugins: [
|
|
12
13
|
manifestPlugin({
|
|
@@ -24,6 +25,7 @@ else {
|
|
|
24
25
|
outdir: "src/dist/js",
|
|
25
26
|
entryPoints: [
|
|
26
27
|
"src/js/plutonium.js",
|
|
28
|
+
"src/js/plutonium-charts.js"
|
|
27
29
|
],
|
|
28
30
|
minify,
|
|
29
31
|
sourcemap: true,
|
|
@@ -34,7 +36,8 @@ else {
|
|
|
34
36
|
_build({
|
|
35
37
|
outdir: "app/assets",
|
|
36
38
|
entryPoints: [
|
|
37
|
-
"src/js/plutonium.js"
|
|
39
|
+
"src/js/plutonium.js",
|
|
40
|
+
"src/js/plutonium-charts.js"
|
|
38
41
|
],
|
|
39
42
|
minify,
|
|
40
43
|
sourcemap: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.64.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
csv
|
|
7
7
|
listen (~> 3.8)
|
|
@@ -601,7 +601,7 @@ CHECKSUMS
|
|
|
601
601
|
phlexi-form (0.14.3) sha256=47d69c4a49ba22279ba541b9b110e81d62acc4602513f21bb5c3d29fa9c8d5ca
|
|
602
602
|
phlexi-menu (0.4.1) sha256=f6eeb1eb9597caadeee33a53f1520b3613a6ac616cf1e395e9d734a412895fb2
|
|
603
603
|
phlexi-table (0.2.0) sha256=73bee135adbe73fcdb6fdd2672444373556dd9e6bba61e7b5b20ca8b7a1f822c
|
|
604
|
-
plutonium (0.
|
|
604
|
+
plutonium (0.64.0)
|
|
605
605
|
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
|
|
606
606
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
607
607
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
@@ -52,7 +52,7 @@ module ActiveModel
|
|
|
52
52
|
errors = record.errors.delete attribute
|
|
53
53
|
return if errors.nil?
|
|
54
54
|
|
|
55
|
-
@existing_errors += errors.map { |e| "
|
|
55
|
+
@existing_errors += errors.map { |e| I18n.t("errors.messages.array_item", index: index + 1, message: e) }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def skip?(value, validator_options)
|
|
@@ -10,7 +10,7 @@ module ActiveModel
|
|
|
10
10
|
#
|
|
11
11
|
class AttachedValidator < EachValidator
|
|
12
12
|
def validate_each(record, attribute, value)
|
|
13
|
-
record.errors.add(attribute, options[:message] ||
|
|
13
|
+
record.errors.add(attribute, options[:message] || :not_attached) unless value.attached?
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -35,7 +35,7 @@ module ActiveModel
|
|
|
35
35
|
def validate_url
|
|
36
36
|
return true if URL_PATTERN.match?(value)
|
|
37
37
|
|
|
38
|
-
record.errors.add attribute, (options[:message] ||
|
|
38
|
+
record.errors.add attribute, (options[:message] || :invalid_url)
|
|
39
39
|
false
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ module ActiveModel
|
|
|
43
43
|
return true unless options[:image].present?
|
|
44
44
|
return true unless FastImage.type(value).nil?
|
|
45
45
|
|
|
46
|
-
record.errors.add(attribute, options[:message] ||
|
|
46
|
+
record.errors.add(attribute, options[:message] || :invalid_image_url)
|
|
47
47
|
false
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -25,15 +25,23 @@ module Pu
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
-
# The asset pipeline assumes the app was generated with esbuild + Tailwind.
|
|
29
|
-
# Without those, `application.tailwind.css`
|
|
30
|
-
#
|
|
28
|
+
# The asset pipeline assumes the app was generated with esbuild + Tailwind + Stimulus.
|
|
29
|
+
# Without those, `application.tailwind.css` (Tailwind) and/or
|
|
30
|
+
# `app/javascript/controllers/index.js` (Stimulus) don't exist, and the generator
|
|
31
|
+
# later crashes with a cryptic inject_into_file error. Fail early listing every
|
|
32
|
+
# missing file with a fix.
|
|
31
33
|
def verify_prerequisites
|
|
32
|
-
|
|
34
|
+
required = [
|
|
35
|
+
"app/assets/stylesheets/application.tailwind.css",
|
|
36
|
+
"app/javascript/controllers/index.js"
|
|
37
|
+
]
|
|
38
|
+
missing = required.reject { |path| File.exist?(path) }
|
|
39
|
+
return if missing.empty?
|
|
33
40
|
|
|
34
41
|
error <<~MSG
|
|
35
|
-
Plutonium assets require a Rails app generated with esbuild and
|
|
36
|
-
|
|
42
|
+
Plutonium assets require a Rails app generated with esbuild, Tailwind, and Stimulus.
|
|
43
|
+
Missing files:
|
|
44
|
+
- #{missing.join("\n - ")}
|
|
37
45
|
|
|
38
46
|
Re-create the app with the required flags:
|
|
39
47
|
rails new myapp -a propshaft -j esbuild -c tailwind \\
|
|
@@ -46,28 +54,35 @@ module Pu
|
|
|
46
54
|
copy_file "postcss.config.js", force: true
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
# Every package goes through one `add` built for the app's package manager
|
|
58
|
+
# (bun, yarn 1, yarn 2+, npm or pnpm; see Concerns::JsPackageManager).
|
|
59
|
+
# Versions ride inline because `pkg@latest` and `pkg@^x.y.z` are the one
|
|
60
|
+
# spelling all of them share; yarn 1's `upgrade --latest` does not exist
|
|
61
|
+
# on yarn 2+, and running yarn inside a bun app leaves two lockfiles.
|
|
49
62
|
def install_dependencies
|
|
50
|
-
|
|
63
|
+
ensure_yarn_node_modules_linker
|
|
51
64
|
|
|
65
|
+
command = js_add_command(npm_packages)
|
|
66
|
+
return if run(command)
|
|
67
|
+
|
|
68
|
+
say_status :warn,
|
|
69
|
+
"`#{command}` failed — your app may not boot until you re-run it",
|
|
70
|
+
:yellow
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def npm_packages
|
|
52
74
|
[
|
|
53
|
-
|
|
75
|
+
# Pinned to the gem so the two halves never drift; pu:core:update
|
|
76
|
+
# keeps them together afterwards.
|
|
77
|
+
"@radioactive-labs/plutonium@^#{Plutonium::VERSION}",
|
|
78
|
+
# cssbundling-rails already installs the latest Tailwind on new apps;
|
|
79
|
+
# apps created on Tailwind 3 are moved to 4 here.
|
|
80
|
+
"tailwindcss@latest",
|
|
54
81
|
"postcss", "postcss-cli", "postcss-import",
|
|
55
82
|
"@tailwindcss/postcss", "@tailwindcss/forms", "@tailwindcss/typography",
|
|
56
83
|
"cssnano", "marked",
|
|
57
84
|
"flowbite-typography"
|
|
58
|
-
]
|
|
59
|
-
run "yarn add #{package}"
|
|
60
|
-
failed << "yarn add #{package}" unless $?.success?
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
run "yarn upgrade tailwindcss --latest"
|
|
64
|
-
failed << "yarn upgrade tailwindcss --latest" unless $?.success?
|
|
65
|
-
|
|
66
|
-
return if failed.empty?
|
|
67
|
-
|
|
68
|
-
say_status :warn,
|
|
69
|
-
"the following commands failed — your app may not boot until you resolve them:\n #{failed.join("\n ")}",
|
|
70
|
-
:yellow
|
|
85
|
+
]
|
|
71
86
|
end
|
|
72
87
|
|
|
73
88
|
def configure_application
|
|
@@ -3,7 +3,7 @@ const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
|
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
plugins: {
|
|
6
|
-
[`${plutoniumGemPath}/postcss-gem-import.
|
|
6
|
+
[`${plutoniumGemPath}/postcss-gem-import.cjs`]: {},
|
|
7
7
|
'postcss-import': {},
|
|
8
8
|
'@tailwindcss/postcss': {},
|
|
9
9
|
cssnano: {}
|
|
@@ -45,9 +45,10 @@ module Pu
|
|
|
45
45
|
|
|
46
46
|
def update_npm_package
|
|
47
47
|
gem_version = installed_gem_version
|
|
48
|
+
package = gem_version ? "@radioactive-labs/plutonium@^#{gem_version}" : "@radioactive-labs/plutonium@latest"
|
|
48
49
|
|
|
49
|
-
say_status :update, "Updating
|
|
50
|
-
run
|
|
50
|
+
say_status :update, "Updating #{package} with #{js_package_manager}...", :green
|
|
51
|
+
run js_add_command(package)
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def installed_gem_version
|