plutonium 0.64.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +44 -41
- data/.claude/skills/plutonium-app/SKILL.md +115 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +168 -0
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +236 -145
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +171 -95
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +282 -210
- data/README.md +9 -9
- data/SECURITY.md +1 -1
- data/app/assets/plutonium-charts.js +20604 -0
- data/app/assets/plutonium-charts.js.map +7 -0
- data/app/assets/plutonium-charts.min.js +41 -0
- data/app/assets/plutonium-charts.min.js.map +7 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +794 -629
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +48 -48
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash_alerts.html.erb +2 -2
- data/app/views/plutonium/_resource_header.html.erb +2 -2
- data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
- data/app/views/plutonium/_toast.html.erb +2 -2
- data/app/views/rodauth/_login_form.html.erb +1 -1
- data/app/views/rodauth/_password_visibility.html.erb +1 -1
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +2 -2
- data/app/views/rodauth/create_account.html.erb +4 -4
- data/app/views/rodauth/reset_password_request.html.erb +1 -1
- data/app/views/rodauth/verify_account_resend.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/config/brakeman.ignore +4 -4
- data/config/initializers/rabl.rb +0 -40
- data/config/locales/en/api_client.yml +22 -0
- data/config/locales/en/async.yml +34 -0
- data/config/locales/en/dashboard.yml +25 -0
- data/config/locales/en/invites.yml +37 -0
- data/config/locales/en/js.yml +40 -0
- data/config/locales/en/kanban.yml +28 -0
- data/config/locales/en/plutonium.yml +26 -0
- data/config/locales/en/positioning.yml +14 -0
- data/config/locales/en/profile.yml +29 -0
- data/config/locales/en/resource.yml +60 -0
- data/config/locales/en/ui.yml +93 -0
- data/config/locales/en/wizard.yml +46 -0
- data/docs/.vitepress/config.ts +34 -1
- data/docs/.vitepress/sync-skills.mjs +6 -3
- data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
- data/docs/.vitepress/theme/custom.css +34 -377
- data/docs/blog/introducing-plutonium-dashboards.md +174 -0
- data/docs/blog/introducing-plutonium-i18n.md +90 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +4 -3
- data/docs/guides/kanban.md +56 -56
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/reference/app/generators.md +24 -24
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +77 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +88 -31
- data/docs/reference/dashboard/dsl.md +123 -0
- data/docs/reference/dashboard/index.md +35 -0
- data/docs/reference/dashboard/registration.md +76 -0
- data/docs/reference/generators/lite.md +7 -7
- data/docs/reference/i18n.md +262 -0
- data/docs/reference/index.md +11 -2
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +33 -33
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +18 -16
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +166 -88
- data/docs/reference/resource/export.md +11 -7
- data/docs/reference/resource/index.md +16 -14
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
- data/docs/reference/resource/query.md +14 -12
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +65 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +19 -22
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +45 -45
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- data/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/active_model/validations/array_validator.rb +1 -1
- data/lib/active_model/validations/attached_validator.rb +1 -1
- data/lib/active_model/validations/url_validator.rb +2 -2
- data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
- data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +3 -2
- data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
- data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
- data/lib/generators/pu/docker/install/install_generator.rb +58 -0
- data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
- data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
- data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/invites/invitable_generator.rb +18 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
- data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
- data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
- data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
- data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
- data/lib/generators/pu/profile/conn_generator.rb +6 -0
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
- data/lib/plutonium/action/base.rb +27 -3
- data/lib/plutonium/action/interactive.rb +17 -4
- data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
- data/lib/plutonium/auth/sequel_adapter.rb +1 -1
- data/lib/plutonium/configuration.rb +5 -0
- data/lib/plutonium/core/controller.rb +25 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/dashboard/base.rb +114 -0
- data/lib/plutonium/dashboard/card.rb +200 -0
- data/lib/plutonium/dashboard/controller.rb +101 -0
- data/lib/plutonium/dashboard/dsl.rb +104 -0
- data/lib/plutonium/dashboard/register.rb +40 -0
- data/lib/plutonium/dashboard/route_resolution.rb +30 -0
- data/lib/plutonium/dashboard.rb +42 -0
- data/lib/plutonium/definition/actions.rb +6 -1
- data/lib/plutonium/definition/base.rb +41 -3
- data/lib/plutonium/definition/index_views.rb +1 -1
- data/lib/plutonium/definition/positioning.rb +1 -1
- data/lib/plutonium/definition/presentable.rb +2 -2
- data/lib/plutonium/helpers/assets_helper.rb +15 -2
- data/lib/plutonium/helpers/display_helper.rb +10 -2
- data/lib/plutonium/interaction/async/context.rb +6 -8
- data/lib/plutonium/interaction/async/executor.rb +6 -7
- data/lib/plutonium/interaction/async/run.rb +2 -2
- data/lib/plutonium/interaction/async/run_definition.rb +1 -1
- data/lib/plutonium/interaction/base.rb +2 -0
- data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
- data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
- data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
- data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
- data/lib/plutonium/invites/controller.rb +11 -11
- data/lib/plutonium/kanban/column.rb +13 -3
- data/lib/plutonium/kanban/dsl.rb +7 -4
- data/lib/plutonium/models/has_cents.rb +1 -1
- data/lib/plutonium/profile/security_section.rb +10 -18
- data/lib/plutonium/query/filter.rb +10 -1
- data/lib/plutonium/query/filters/association.rb +27 -2
- data/lib/plutonium/query/filters/boolean.rb +4 -4
- data/lib/plutonium/query/filters/date.rb +4 -17
- data/lib/plutonium/query/filters/date_range.rb +2 -2
- data/lib/plutonium/query/filters/select.rb +10 -1
- data/lib/plutonium/query/filters/text.rb +9 -12
- data/lib/plutonium/railtie.rb +17 -0
- data/lib/plutonium/resource/controller.rb +10 -5
- data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
- data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
- data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
- data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
- data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
- data/lib/plutonium/resource/query_object.rb +30 -3
- data/lib/plutonium/resource/record/associated_with.rb +23 -2
- data/lib/plutonium/routing/dashboard_registration.rb +137 -0
- data/lib/plutonium/routing/resource_registration.rb +5 -0
- data/lib/plutonium/routing/route_set_extensions.rb +2 -1
- data/lib/plutonium/translation.rb +227 -0
- data/lib/plutonium/ui/actions_dropdown.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +3 -3
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/behaviour.rb +46 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/dashboard/board.rb +80 -0
- data/lib/plutonium/ui/dashboard/card.rb +127 -0
- data/lib/plutonium/ui/dashboard/chart.rb +43 -0
- data/lib/plutonium/ui/dashboard/custom.rb +32 -0
- data/lib/plutonium/ui/dashboard/frame.rb +25 -0
- data/lib/plutonium/ui/dashboard/metric.rb +137 -0
- data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
- data/lib/plutonium/ui/display/components/attachment.rb +1 -1
- data/lib/plutonium/ui/display/components/badge.rb +7 -1
- data/lib/plutonium/ui/display/components/boolean.rb +2 -2
- data/lib/plutonium/ui/display/resource.rb +23 -16
- data/lib/plutonium/ui/export_button.rb +3 -3
- data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
- data/lib/plutonium/ui/form/components/password.rb +2 -2
- data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
- data/lib/plutonium/ui/form/components/uppy.rb +11 -3
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
- data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
- data/lib/plutonium/ui/form/query.rb +8 -8
- data/lib/plutonium/ui/form/resource.rb +54 -26
- data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
- data/lib/plutonium/ui/grid/card.rb +3 -3
- data/lib/plutonium/ui/grid/resource.rb +3 -3
- data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
- data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
- data/lib/plutonium/ui/kanban/column.rb +6 -6
- data/lib/plutonium/ui/layout/base.rb +19 -1
- data/lib/plutonium/ui/layout/header.rb +1 -1
- data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/layout/topbar.rb +2 -2
- data/lib/plutonium/ui/modal/base.rb +4 -4
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
- data/lib/plutonium/ui/nav_user.rb +2 -2
- data/lib/plutonium/ui/page/base.rb +10 -2
- data/lib/plutonium/ui/page/dashboard.rb +54 -0
- data/lib/plutonium/ui/page/edit.rb +3 -3
- data/lib/plutonium/ui/page/interactive_action.rb +2 -2
- data/lib/plutonium/ui/page/new.rb +3 -3
- data/lib/plutonium/ui/page/show.rb +2 -2
- data/lib/plutonium/ui/page/wizard.rb +22 -16
- data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
- data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
- data/lib/plutonium/ui/skeleton_table.rb +1 -1
- data/lib/plutonium/ui/table/base.rb +2 -2
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
- data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
- data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
- data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
- data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
- data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
- data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
- data/lib/plutonium/ui/table/resource.rb +53 -17
- data/lib/plutonium/ui/wizard/review.rb +7 -7
- data/lib/plutonium/ui/wizard/stepper.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/base.rb +2 -0
- data/lib/plutonium/wizard/driving.rb +8 -2
- data/lib/plutonium/wizard/dsl.rb +4 -2
- data/lib/plutonium/wizard/field_importer.rb +1 -8
- data/lib/plutonium/wizard/review_step.rb +8 -1
- data/lib/plutonium/wizard/step.rb +14 -3
- data/package.json +4 -2
- data/plutonium.gemspec +4 -1
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +101 -10
- data/src/css/slim_select.css +0 -38
- data/src/css/tokens.css +24 -0
- data/src/js/controllers/attachment_input_controller.js +15 -6
- data/src/js/controllers/chart_controller.js +162 -0
- data/src/js/controllers/clipboard_controller.js +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +4 -3
- data/src/js/controllers/easymde_controller.js +3 -0
- data/src/js/controllers/filter_panel_controller.js +15 -6
- data/src/js/controllers/flatpickr_controller.js +8 -0
- data/src/js/controllers/frame_refresh_controller.js +59 -0
- data/src/js/controllers/intl_tel_input_controller.js +5 -0
- data/src/js/controllers/register_controllers.js +4 -0
- data/src/js/controllers/slim_select_controller.js +79 -176
- data/src/js/core.js +9 -1
- data/src/js/i18n.js +69 -0
- data/src/js/plutonium-charts.js +15 -0
- data/src/js/turbo/turbo_confirm.js +5 -3
- metadata +64 -16
- data/.yarnrc.yml +0 -8
- data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
- data/lib/generators/pu/field/input/input_generator.rb +0 -32
- data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
- data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
- data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
- data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
- data/public/plutonium-assets/application.js +0 -31419
- data/public/plutonium-assets/plutonium.ico +0 -0
- data/yarn.lock +0 -6858
|
@@ -28,7 +28,7 @@ All declared in the definition.
|
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
class PostDefinition < ResourceDefinition
|
|
31
|
-
# Search box
|
|
31
|
+
# Search box: searches title and body
|
|
32
32
|
search do |scope, query|
|
|
33
33
|
scope.where("title ILIKE :q OR body ILIKE :q", q: "%#{query}%")
|
|
34
34
|
end
|
|
@@ -81,7 +81,7 @@ end
|
|
|
81
81
|
|
|
82
82
|
When an association input targets this resource, the dropdown's autocomplete calls the resource's `search` block. Same code, two surfaces.
|
|
83
83
|
|
|
84
|
-
### Without a `search` block
|
|
84
|
+
### Without a `search` block: typeahead fallback
|
|
85
85
|
|
|
86
86
|
The framework falls back to a case-insensitive `LIKE` on the first column it finds, in priority order:
|
|
87
87
|
|
|
@@ -89,7 +89,7 @@ The framework falls back to a case-insensitive `LIKE` on the first column it fin
|
|
|
89
89
|
2. Otherwise the first match from `[name, title, label, slug, display_name, email]`.
|
|
90
90
|
3. Otherwise the relation is returned unfiltered (capped).
|
|
91
91
|
|
|
92
|
-
For large tables, write an explicit `search` block
|
|
92
|
+
For large tables, write an explicit `search` block: the leading-wildcard `LIKE` can't use a b-tree index. See [Reference › Resource › Query › Search](/reference/resource/query#search).
|
|
93
93
|
|
|
94
94
|
## Filters
|
|
95
95
|
|
|
@@ -154,7 +154,7 @@ class PostDefinition < ResourceDefinition
|
|
|
154
154
|
scope :published # uses Post.published
|
|
155
155
|
scope :draft # uses Post.draft
|
|
156
156
|
|
|
157
|
-
# Inline scope
|
|
157
|
+
# Inline scope: block runs with scope as argument
|
|
158
158
|
scope(:recent) { |s| s.where('created_at > ?', 1.week.ago) }
|
|
159
159
|
|
|
160
160
|
# Scope with controller context
|
|
@@ -206,9 +206,9 @@ Query params are namespaced under `q`:
|
|
|
206
206
|
## Performance tips
|
|
207
207
|
|
|
208
208
|
- **Add indexes** for filtered and sorted columns.
|
|
209
|
-
- **Use `.distinct`** when joining associations in search
|
|
209
|
+
- **Use `.distinct`** when joining associations in search: duplicate rows otherwise.
|
|
210
210
|
- **Prefer scopes over filters** for queries used often (no input parsing).
|
|
211
|
-
- **`LIKE '%q%'` can't use a b-tree index
|
|
211
|
+
- **`LIKE '%q%'` can't use a b-tree index**: for large tables, use `pg_search` or a trigram/GIN/full-text index.
|
|
212
212
|
|
|
213
213
|
## Full-text search with `pg_search`
|
|
214
214
|
|
|
@@ -227,13 +227,13 @@ end
|
|
|
227
227
|
|
|
228
228
|
## Common issues
|
|
229
229
|
|
|
230
|
-
- **Filter not showing up
|
|
231
|
-
- **Slow search on large tables
|
|
232
|
-
- **Duplicate rows in results
|
|
233
|
-
- **Typeahead works on small dev tables but slows in production
|
|
230
|
+
- **Filter not showing up**: make sure the attribute is in `permitted_attributes_for_index` on the policy.
|
|
231
|
+
- **Slow search on large tables**: `LIKE '%q%'` can't be indexed by a b-tree. Switch to FTS or trigram.
|
|
232
|
+
- **Duplicate rows in results**: add `.distinct` when joining associations.
|
|
233
|
+
- **Typeahead works on small dev tables but slows in production**: same b-tree issue. Write an explicit `search` block backed by a proper index.
|
|
234
234
|
|
|
235
235
|
## Related
|
|
236
236
|
|
|
237
|
-
- [Reference › Resource › Query](/reference/resource/query)
|
|
238
|
-
- [Adding resources](./adding-resources)
|
|
239
|
-
- [Authorization](./authorization)
|
|
237
|
+
- [Reference › Resource › Query](/reference/resource/query): full surface
|
|
238
|
+
- [Adding resources](./adding-resources): basic resource setup
|
|
239
|
+
- [Authorization](./authorization): `permitted_attributes_for_index` gates which fields can be filtered
|
data/docs/guides/testing.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
-
Plutonium ships `Plutonium::Testing
|
|
3
|
+
Plutonium ships `Plutonium::Testing`, opt-in Minitest concerns that give your app default test coverage for resources, policies, definitions, interactions, models, nested scoping, portal access, and authentication.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -75,7 +75,7 @@ resource_tests_for ResourceClass,
|
|
|
75
75
|
has_cents: %i[price] # ResourceModel only
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
The **portal symbol** drives path prefix, default auth strategy, and scoping expectations. The resolver walks `Rails.application.routes.routes` for the engine mount
|
|
78
|
+
The **portal symbol** drives path prefix, default auth strategy, and scoping expectations. The resolver walks `Rails.application.routes.routes` for the engine mount, with no manual configuration.
|
|
79
79
|
|
|
80
80
|
## Concerns
|
|
81
81
|
|
|
@@ -84,15 +84,17 @@ The **portal symbol** drives path prefix, default auth strategy, and scoping exp
|
|
|
84
84
|
| `ResourceCrud` | index/show/new/create/edit/update/destroy | `create_resource!`, `valid_create_params`, `valid_update_params` |
|
|
85
85
|
| `ResourcePolicy` | permit? × role × action matrix + relation_scope smoke | `policy_roles`, `policy_record`, `policy_matrix` |
|
|
86
86
|
| `ResourceDefinition` | definition class + defineable prop smoke | none |
|
|
87
|
-
| `ResourceInteraction` | `assert_interaction_success/failure` helpers |
|
|
87
|
+
| `ResourceInteraction` | `assert_interaction_success/failure` helpers | none |
|
|
88
88
|
| `ResourceModel` | `associated_with`, SGID, `has_cents` | `model_test_record` |
|
|
89
89
|
| `NestedResource` | nested CRUD + sibling-tenant boundaries | `parent_record!`, `other_parent_record!`, `create_resource!(parent:)` |
|
|
90
90
|
| `PortalAccess` | cross-portal access matrix | `login_as_role`, `portal_root_path` |
|
|
91
91
|
|
|
92
|
-
Mix and match
|
|
92
|
+
Mix and match: `include` only what you want.
|
|
93
93
|
|
|
94
94
|
## Auth helpers
|
|
95
95
|
|
|
96
|
+
`login_as` and friends come from `Plutonium::Testing::AuthHelpers`. Only `ResourceCrud`, `NestedResource` and `PortalAccess` include it; the policy, definition, model and interaction concerns don't. A bare `login_as(account)` takes its portal from `resource_tests_for`, so in a `PortalAccess` class (or a hand-written test that adds `include Plutonium::Testing::AuthHelpers` itself) pass `portal:` every time.
|
|
97
|
+
|
|
96
98
|
```ruby
|
|
97
99
|
login_as(account) # uses portal from DSL
|
|
98
100
|
login_as(account, portal: :admin) # explicit override
|
|
@@ -130,7 +132,7 @@ Idempotent. Adds the require line and creates the override stub.
|
|
|
130
132
|
|---|---|---|
|
|
131
133
|
| `--portals=admin,org` | required | Emit one file per portal |
|
|
132
134
|
| `--concerns=...` | `crud,policy,definition` | Subset of concerns to include |
|
|
133
|
-
| `--parent=organization` | none |
|
|
135
|
+
| `--parent=organization` | none | Adds `parent:` to `resource_tests_for`; the `NestedResource` include and stubs also need `nested` in `--concerns` (`--concerns=crud,nested --parent=organization`) |
|
|
134
136
|
| `--dest=main_app\|<package>` | `main_app` | Output destination |
|
|
135
137
|
|
|
136
138
|
Output: `test/integration/<portal>_portal/<resource>_test.rb`.
|
|
@@ -144,16 +146,18 @@ Output: `test/integration/<portal>_portal/<resource>_test.rb`.
|
|
|
144
146
|
|
|
145
147
|
## Common pitfalls
|
|
146
148
|
|
|
147
|
-
- **Forgotten stubs raise `NotImplementedError`** with the stub name
|
|
149
|
+
- **Forgotten stubs raise `NotImplementedError`** with the stub name: look for the missing method.
|
|
148
150
|
- **Portal mismatch:** `:admin` expects `AdminPortal::Engine`. Pass `path_prefix:` if your engine is named differently.
|
|
149
151
|
- **Tenant leakage in stubs:** for an org portal, `create_resource!` must return a record bound to the test's `@org`.
|
|
150
|
-
- **`policy_record` for tenant-scoped resources** must belong to a tenant the role can access
|
|
151
|
-
- **Nested
|
|
152
|
+
- **`policy_record` for tenant-scoped resources** must belong to a tenant the role can access, otherwise even allowed roles see `false`.
|
|
153
|
+
- **Nested paths come from `parent_record!.id`**, so it must return the same persisted tenant on every call (e.g. `@org`). `parent:` in the DSL documents the relationship; the concern doesn't read it.
|
|
154
|
+
- **Entity-scoped (`:path`) portals need two classes** for CRUD and tenant isolation, since `ResourceCrud` needs the tenant in `current_path_prefix` and `NestedResource` adds it itself. See [Reference › Testing](/reference/testing/#entity-scoped-portals-crud-tenant-isolation).
|
|
155
|
+
- **`valid_update_params` is compared literally** after the PATCH: use strings for enums and leave association SGIDs out.
|
|
152
156
|
- **`PortalAccess` uses `portal_access_for`**, not `resource_tests_for`. Don't mix them on the same class.
|
|
153
157
|
|
|
154
158
|
## Related
|
|
155
159
|
|
|
156
|
-
- [Reference › Testing](/reference/testing/)
|
|
157
|
-
- [Authorization](./authorization)
|
|
158
|
-
- [Multi-tenancy](./multi-tenancy)
|
|
159
|
-
- [Authentication](./authentication)
|
|
160
|
+
- [Reference › Testing](/reference/testing/): full DSL reference, all concern stubs, override hooks
|
|
161
|
+
- [Authorization](./authorization): write the policy this concern verifies
|
|
162
|
+
- [Multi-tenancy](./multi-tenancy): entity scoping that drives nested-resource tests
|
|
163
|
+
- [Authentication](./authentication): Rodauth setup behind the default login flow
|
data/docs/guides/theming.md
CHANGED
|
@@ -18,11 +18,12 @@ Adapt Plutonium's defaults to match your brand: primary color, fonts, logo, dark
|
|
|
18
18
|
|
|
19
19
|
## 🚨 Critical
|
|
20
20
|
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
21
|
+
- **Run `pu:core:assets` before any CSS or brand-color change.** Out of the box the app serves the gem's prebuilt `plutonium.css` / `plutonium.min.js`, so app-side Tailwind classes, palette changes and token overrides have nowhere to compile. Don't hand-write the Tailwind/PostCSS pipeline.
|
|
22
|
+
- **Once the app owns its JS bundle, register Stimulus controllers** with `registerControllers(application)` (`pu:core:assets` adds it). Your bundle replaces the gem's, so without it the entire interactive layer is dead.
|
|
23
|
+
- **Use `plutoniumTailwindConfig.merge`** when overriding Tailwind theme: plain object spread drops Plutonium's defaults.
|
|
24
|
+
- **Tokens are CSS variables, not Tailwind keys**: `bg-[var(--pu-surface)]`, NOT `bg-pu-surface`.
|
|
25
|
+
- **Dark mode is `selector`, not `class`**: toggle by adding/removing `dark` on `<html>`.
|
|
26
|
+
- **Style with `.pu-*` classes first, `var(--pu-*)` tokens second, raw palette pairs last.** Banners, badges, cards and buttons all have a `.pu-*` class that carries its own `.dark` rule; a hand-written `bg-warning-50 dark:bg-warning-950/30` pair duplicates that and drifts from the theme.
|
|
26
27
|
|
|
27
28
|
## Step 1: Run the assets generator
|
|
28
29
|
|
|
@@ -30,7 +31,9 @@ Adapt Plutonium's defaults to match your brand: primary color, fonts, logo, dark
|
|
|
30
31
|
rails generate pu:core:assets
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
This installs npm packages, creates `tailwind.config.js`, imports Plutonium CSS, registers Stimulus controllers, and points `Plutonium.configure` at your asset files. Run once per app.
|
|
34
|
+
This installs npm packages, creates `tailwind.config.js` and `postcss.config.js`, imports Plutonium CSS, registers Stimulus controllers, and points `Plutonium.configure` at your asset files. Run once per app.
|
|
35
|
+
|
|
36
|
+
It aborts unless `app/assets/stylesheets/application.tailwind.css` and `app/javascript/controllers/index.js` exist (an app created with `-j esbuild -c tailwind` plus Stimulus). For an app without them, run `bin/rails javascript:install:esbuild`, `css:install:tailwind` and `stimulus:install` first. See [Reference › UI › Assets › Generator](/reference/ui/assets#generator).
|
|
34
37
|
|
|
35
38
|
## Step 2: Asset configuration
|
|
36
39
|
|
|
@@ -75,6 +78,8 @@ theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
|
75
78
|
})
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
These palettes are compiled into the CSS at build time (`.pu-btn-primary` is `@apply bg-primary-600 ...`), so a brand color change needs this `merge` plus a rebuild (the `build:css` script, or the running `bin/dev` watcher). A `--pu-*` override won't recolor `primary`.
|
|
82
|
+
|
|
78
83
|
### Default palette
|
|
79
84
|
|
|
80
85
|
| Color | Use |
|
|
@@ -105,7 +110,15 @@ theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
|
105
110
|
}
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
Tokens auto-switch when the user toggles dark mode.
|
|
113
|
+
Tokens auto-switch when the user toggles dark mode.
|
|
114
|
+
|
|
115
|
+
::: warning Mirror every `:root` override in `.dark`
|
|
116
|
+
Your stylesheet loads after Plutonium's and `:root` / `.dark` have equal specificity, so a token overridden only in `:root` wins in dark mode too and ships your light value there. Re-assert every customized color token in `.dark`, shadows included: `--pu-shadow-sm/md/lg` have their own dark values in `src/css/tokens.css`.
|
|
117
|
+
|
|
118
|
+
Put dark values in a `.dark { ... }` block, not `@media (prefers-color-scheme: dark)`. Dark mode is the `dark` class on `<html>`, so a media query ignores the user's toggle.
|
|
119
|
+
:::
|
|
120
|
+
|
|
121
|
+
See [Reference › UI › Assets › Design tokens](/reference/ui/assets#design-tokens) for the full token catalog.
|
|
109
122
|
|
|
110
123
|
## Using tokens in your code
|
|
111
124
|
|
|
@@ -145,6 +158,8 @@ Pre-styled ready-to-use components:
|
|
|
145
158
|
| Buttons | `.pu-btn`, `.pu-btn-md/-sm/-xs`, `.pu-btn-primary/-secondary/-danger/-success/-warning/-info/-accent`, `.pu-btn-ghost/-outline`, `.pu-btn-soft-*` |
|
|
146
159
|
| Inputs | `.pu-input/-invalid/-valid`, `.pu-label/-required`, `.pu-hint`, `.pu-error`, `.pu-checkbox` |
|
|
147
160
|
| Cards | `.pu-card`, `.pu-card-body`, `.pu-panel-header`, `.pu-panel-title`, `.pu-panel-description` |
|
|
161
|
+
| Badges | `.pu-badge`, `.pu-badge-neutral/-primary/-secondary/-success/-danger/-warning/-info/-accent` |
|
|
162
|
+
| Alerts (inline banners) | `.pu-alert`, `.pu-alert-success/-warning/-danger/-info`, `.pu-alert-message`, `.pu-alert-close` |
|
|
148
163
|
| Tables | `.pu-table-wrapper`, `.pu-table`, `-header`, `-header-cell`, `-body-row`, `-body-row-selected`, `-body-cell`, `.pu-selection-cell` |
|
|
149
164
|
| Toolbars / empty states | `.pu-toolbar`, `-text`, `-actions`; `.pu-empty-state`, `-icon`, `-title`, `-description` |
|
|
150
165
|
|
|
@@ -184,7 +199,7 @@ end
|
|
|
184
199
|
```
|
|
185
200
|
|
|
186
201
|
::: warning Always `super.merge(...)`
|
|
187
|
-
Don't replace the theme wholesale
|
|
202
|
+
Don't replace the theme wholesale (Plutonium's defaults handle invalid states, focus rings, and dark mode). `super.merge` keeps them.
|
|
188
203
|
:::
|
|
189
204
|
|
|
190
205
|
Full theme key catalog: [Reference › UI › Assets › Phlexi component themes](/reference/ui/assets#phlexi-component-themes).
|
|
@@ -225,7 +240,7 @@ document.documentElement.classList.toggle('dark')
|
|
|
225
240
|
|
|
226
241
|
If you've overridden tokens via `:root` and `.dark`, both modes Just Work.
|
|
227
242
|
|
|
228
|
-
## Per-portal chrome
|
|
243
|
+
## Per-portal chrome: eject the shell
|
|
229
244
|
|
|
230
245
|
For per-portal headers/sidebars:
|
|
231
246
|
|
|
@@ -245,7 +260,7 @@ Copies `layouts/resource.html.erb` for layout-level edits.
|
|
|
245
260
|
|
|
246
261
|
```ruby
|
|
247
262
|
Plutonium.configure do |config|
|
|
248
|
-
config.shell = :modern # default
|
|
263
|
+
config.shell = :modern # default: topbar + icon rail
|
|
249
264
|
# config.shell = :classic # legacy header + sidebar (only when upgrading)
|
|
250
265
|
end
|
|
251
266
|
```
|
|
@@ -268,13 +283,13 @@ Bundled controllers: `color-mode`, `form` (pre-submit), `nested-resource-form-fi
|
|
|
268
283
|
|
|
269
284
|
## Common issues
|
|
270
285
|
|
|
271
|
-
- **Stimulus controllers silently fail
|
|
272
|
-
- **`plutoniumTailwindConfig.merge` is mandatory
|
|
273
|
-
- **Tokens not switching in dark mode
|
|
274
|
-
- **`.pu-btn` styles not applying
|
|
286
|
+
- **Stimulus controllers silently fail**: once the app serves its own JS bundle, if `registerControllers(application)` isn't called, the entire UI's interactive layer is dead (color-mode toggle, slim-select, flatpickr, easymde, pre-submit). No error: just no behavior.
|
|
287
|
+
- **`plutoniumTailwindConfig.merge` is mandatory**: plain spread drops defaults silently.
|
|
288
|
+
- **Tokens not switching in dark mode**: you used `bg-pu-surface` instead of `bg-[var(--pu-surface)]`. Tokens are CSS variables, not Tailwind keys.
|
|
289
|
+
- **`.pu-btn` styles not applying**: check that Plutonium CSS is imported BEFORE Tailwind: `@import "gem:plutonium/src/css/plutonium.css";` then `@import "tailwindcss";`.
|
|
275
290
|
|
|
276
291
|
## Related
|
|
277
292
|
|
|
278
|
-
- [Reference › UI › Assets](/reference/ui/assets)
|
|
279
|
-
- [Reference › UI › Layouts](/reference/ui/layouts)
|
|
280
|
-
- [Reference › UI › Forms › Theming](/reference/ui/forms#theming)
|
|
293
|
+
- [Reference › UI › Assets](/reference/ui/assets): full Tailwind / Stimulus / design tokens / component classes surface
|
|
294
|
+
- [Reference › UI › Layouts](/reference/ui/layouts): shell, eject, ResourceLayout
|
|
295
|
+
- [Reference › UI › Forms › Theming](/reference/ui/forms#theming): Form theme keys
|
|
@@ -79,6 +79,6 @@ If you encounter an issue not covered here, please [open an issue](https://githu
|
|
|
79
79
|
|
|
80
80
|
- [Nested resources](./nested-resources)
|
|
81
81
|
- [Adding resources](./adding-resources)
|
|
82
|
-
- [Reference › Behavior › Controllers](/reference/behavior/controllers)
|
|
83
|
-
- [Reference › Tenancy › Nested resources](/reference/tenancy/nested-resources)
|
|
82
|
+
- [Reference › Behavior › Controllers](/reference/behavior/controllers): `controller_for`, `resource_url_for`, `current_parent`
|
|
83
|
+
- [Reference › Tenancy › Nested resources](/reference/tenancy/nested-resources): nested URL generation
|
|
84
84
|
- [Rails Inflections](https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html)
|
data/docs/guides/user-invites.md
CHANGED
|
@@ -8,7 +8,7 @@ An admin enters an email, the user gets an invite link, clicks it, signs up (or
|
|
|
8
8
|
|
|
9
9
|
## Prerequisites
|
|
10
10
|
|
|
11
|
-
You need a user model, an entity model, and a membership model. The fastest path is `pu:saas:setup
|
|
11
|
+
You need a user model, an entity model, and a membership model. The fastest path is `pu:saas:setup`: it creates all three and runs `pu:invites:install` automatically:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
rails g pu:saas:setup --user Customer --entity Organization
|
|
@@ -43,7 +43,7 @@ rails g pu:invites:install \
|
|
|
43
43
|
| `--enforce-domain` | `false` | Require email domain to match entity |
|
|
44
44
|
|
|
45
45
|
::: info Roles come from the membership model
|
|
46
|
-
`pu:invites:install` reads the role list from the membership model's `enum :role
|
|
46
|
+
`pu:invites:install` reads the role list from the membership model's `enum :role`; it does not accept a `--roles=` flag. Define roles when you generate the membership model (`pu:saas:membership --roles=...`), or edit the enum directly. **Index 0 is the most privileged** (typically `owner`); the invite interaction excludes `owner` from selectable choices and defaults new invitees to the second role.
|
|
47
47
|
:::
|
|
48
48
|
|
|
49
49
|
### 2. Migrate
|
|
@@ -112,7 +112,7 @@ Users land on `/welcome` where pending invites are shown. Including `Plutonium::
|
|
|
112
112
|
include Plutonium::Invites::PendingInviteCheck
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
## Invitables
|
|
115
|
+
## Invitables: app models notified on acceptance
|
|
116
116
|
|
|
117
117
|
An invitable is a model that gets notified when its invitation is accepted. Examples: `Tenant`, `TeamMember`, `ProjectCollaborator`.
|
|
118
118
|
|
|
@@ -137,7 +137,7 @@ end
|
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
::: warning Without `on_invite_accepted`
|
|
140
|
-
The invitable never learns about the new user
|
|
140
|
+
The invitable never learns about the new user: the invite is consumed but your app doesn't update its state.
|
|
141
141
|
:::
|
|
142
142
|
|
|
143
143
|
## Multiple invite flows in one app
|
|
@@ -158,7 +158,7 @@ rails g pu:invites:install \
|
|
|
158
158
|
|
|
159
159
|
Each invocation creates an independent flow: model, controller, route, helper all named for the invite-model.
|
|
160
160
|
|
|
161
|
-
The shared `Invites::WelcomeController` accumulates each new class into its `invite_classes` array
|
|
161
|
+
The shared `Invites::WelcomeController` accumulates each new class into its `invite_classes` array; `pending_invite` checks all flows in priority order (first-match wins).
|
|
162
162
|
|
|
163
163
|
See [Reference › Tenancy › Invites › Multiple invite flows](/reference/tenancy/invites#multiple-invite-flows).
|
|
164
164
|
|
|
@@ -222,27 +222,27 @@ entity.user_invites.pending # list pending
|
|
|
222
222
|
|
|
223
223
|
## Security
|
|
224
224
|
|
|
225
|
-
- **Token security
|
|
226
|
-
- **Email validation
|
|
227
|
-
- **Rate limiting
|
|
225
|
+
- **Token security**: `SecureRandom.urlsafe_base64(32)`, 256 bits, URL-safe. Stored hashed, raw token shown only at creation.
|
|
226
|
+
- **Email validation**: `enforce_email?` is `true` by default. The accepting user's email must match the invited email; this prevents account hijacking via invite forwarding.
|
|
227
|
+
- **Rate limiting**: use Rack::Attack or similar to throttle invite creation per admin and acceptance attempts per IP.
|
|
228
228
|
|
|
229
229
|
::: danger Don't disable enforce_email?
|
|
230
230
|
```ruby
|
|
231
231
|
def enforce_email? = false # ← only if you fully understand the trade-off
|
|
232
232
|
```
|
|
233
|
-
Without this, anyone with the token can sign up
|
|
233
|
+
Without this, anyone with the token can sign up, which defeats the purpose of an invitation system.
|
|
234
234
|
:::
|
|
235
235
|
|
|
236
236
|
## Common issues
|
|
237
237
|
|
|
238
|
-
- **"Invitation not found or expired"
|
|
239
|
-
- **Email mismatch error
|
|
240
|
-
- **Rodauth redirect after login doesn't go to `/welcome
|
|
241
|
-
- **`on_invite_accepted` not called
|
|
238
|
+
- **"Invitation not found or expired"**: token expired (default 1 week), invite cancelled, or no longer `pending`.
|
|
239
|
+
- **Email mismatch error**: the accepting user's email doesn't match the invited email. This is by design (security).
|
|
240
|
+
- **Rodauth redirect after login doesn't go to `/welcome`**: check `login_redirect "/welcome"` in the rodauth plugin's `configure` block.
|
|
241
|
+
- **`on_invite_accepted` not called**: ensure the invitable model `include Plutonium::Invites::Concerns::Invitable` and defines `on_invite_accepted`.
|
|
242
242
|
|
|
243
243
|
## Related
|
|
244
244
|
|
|
245
|
-
- [Reference › Tenancy › Invites](/reference/tenancy/invites)
|
|
246
|
-
- [Multi-tenancy](./multi-tenancy)
|
|
247
|
-
- [Authentication](./authentication)
|
|
248
|
-
- [User profile](./user-profile)
|
|
245
|
+
- [Reference › Tenancy › Invites](/reference/tenancy/invites): full surface, multi-flow apps, customization
|
|
246
|
+
- [Multi-tenancy](./multi-tenancy): entity scoping (invites are entity-scoped automatically)
|
|
247
|
+
- [Authentication](./authentication): Rodauth setup
|
|
248
|
+
- [User profile](./user-profile): account-settings page
|
data/docs/guides/user-profile.md
CHANGED
|
@@ -8,15 +8,15 @@ A `/profile` URL that shows the user's personal fields plus a "Security" section
|
|
|
8
8
|
|
|
9
9
|
## 🚨 Critical
|
|
10
10
|
|
|
11
|
-
- **Profile association is always `:profile`** regardless of the model class
|
|
12
|
-
- **Profile needs `pu:profile:conn` to be visible
|
|
13
|
-
- **
|
|
11
|
+
- **Profile association is always `:profile`** regardless of the model class: `current_user.profile`, `build_profile`, `params.require(:profile)` always work.
|
|
12
|
+
- **Profile needs `pu:profile:conn` to be visible.** Without it there is no `/profile` route, and `profile_url` stays `nil` so the user menu has no Profile link.
|
|
13
|
+
- **A user without a profile row is fine.** `profile_url` sends them to the new-profile form. Creating the row up front is optional (see step 4).
|
|
14
14
|
|
|
15
15
|
The show page renders the user's fields, then the **Security Settings** block with Rodauth-backed actions:
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
-
Editing produces a regular Plutonium form
|
|
19
|
+
Editing produces a regular Plutonium form, same form generator the rest of your resources use:
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ rails g pu:profile:setup date_of_birth:date bio:text \
|
|
|
28
28
|
--portal=competition_portal
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`pu:profile:setup` is a meta-generator
|
|
31
|
+
`pu:profile:setup` is a meta-generator: runs `pu:profile:install` + `pu:profile:conn` in one shot.
|
|
32
32
|
|
|
33
33
|
## Step-by-step
|
|
34
34
|
|
|
@@ -64,31 +64,38 @@ rails db:prepare
|
|
|
64
64
|
rails g pu:profile:conn --dest=customer_portal
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
This registers the profile as a **singular** resource
|
|
67
|
+
This registers the profile as a **singular** resource: exposes `/profile` (no `:id`) and the `profile_url` helper.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
It also generates a portal policy scoped to the current user: `create?` is `user.profile.nil?` (one profile per user), `update?` is `true`, and `destroy?` is `false`. The controller sets `user` from `current_user`, so `:user` is not a permitted attribute.
|
|
70
|
+
|
|
71
|
+
::: tip Profile can't be edited?
|
|
72
|
+
Policies generated by older versions of `pu:profile:conn` lack `update?`, so it falls back to `create?` and the profile becomes read-only once it exists. Add `def update? = true` to the portal's profile policy.
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
### 4. (Optional) Create the row up front
|
|
76
|
+
|
|
77
|
+
Without a profile row, the first visit to the profile is the "create profile" form, and the Security section appears once it is saved. To land users on the show page from the first click, create the row when the user is created:
|
|
70
78
|
|
|
71
79
|
```ruby
|
|
72
|
-
# app/models/user.rb (
|
|
80
|
+
# app/models/user.rb (has_one added by pu:profile:install)
|
|
73
81
|
class User < ApplicationRecord
|
|
74
82
|
has_one :profile, class_name: "UserProfile", dependent: :destroy
|
|
75
83
|
|
|
76
|
-
after_create :create_profile
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
def create_profile! = create_profile
|
|
84
|
+
after_create :create_profile
|
|
80
85
|
end
|
|
81
86
|
```
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
and backfill existing users:
|
|
84
89
|
|
|
85
90
|
```bash
|
|
86
|
-
rails runner "User.find_each
|
|
91
|
+
rails runner "User.find_each { |u| u.profile || u.create_profile }"
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
Skip this when the profile has required fields the user must fill in, or when other code already creates profiles.
|
|
95
|
+
|
|
89
96
|
## What you get
|
|
90
97
|
|
|
91
|
-
The generated definition injects a custom `ShowPage` that renders `SecuritySection
|
|
98
|
+
The generated definition injects a custom `ShowPage` that renders `SecuritySection`: dynamically lists Rodauth security links based on which features are enabled:
|
|
92
99
|
|
|
93
100
|
| Feature enabled | Link rendered |
|
|
94
101
|
|---|---|
|
|
@@ -100,15 +107,34 @@ The generated definition injects a custom `ShowPage` that renders `SecuritySecti
|
|
|
100
107
|
| `active_sessions` | Active Sessions |
|
|
101
108
|
| `close_account` | Close Account |
|
|
102
109
|
|
|
103
|
-
If a feature isn't enabled, its link doesn't render
|
|
110
|
+
If a feature isn't enabled, its link doesn't render; no configuration needed.
|
|
104
111
|
|
|
105
112
|
## Linking to the profile
|
|
106
113
|
|
|
114
|
+
The topbar avatar menu already shows a "Profile" entry once `pu:profile:conn` has run. `Plutonium::Auth::Rodauth` defines `profile_url` as `nil`, and the generator overrides it in the portal's controller concern. To link from your own views:
|
|
115
|
+
|
|
107
116
|
```ruby
|
|
108
|
-
link_to("Profile", profile_url) if
|
|
117
|
+
link_to("Profile", profile_url) if profile_url
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Account features on the profile page
|
|
121
|
+
|
|
122
|
+
Per-user settings that aren't profile fields (API tokens, connected accounts, notification preferences) belong on the profile page. Link them from the same `ShowPage` hook, next to `SecuritySection`:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class ShowPage < ShowPage
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def render_after_content
|
|
129
|
+
render Plutonium::Profile::SecuritySection.new
|
|
130
|
+
div(class: "mt-8") do
|
|
131
|
+
a(href: resource_url_for(ApiToken), class: "font-medium text-[var(--pu-text)] hover:underline") { "API tokens" }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
109
135
|
```
|
|
110
136
|
|
|
111
|
-
|
|
137
|
+
See [Reference › Auth › Profile](/reference/auth/profile#account-features-on-the-profile-page) for scoping such a resource to the user.
|
|
112
138
|
|
|
113
139
|
## Customizing the definition
|
|
114
140
|
|
|
@@ -152,12 +178,13 @@ rails g pu:profile:conn StaffUserProfile --dest=admin_portal
|
|
|
152
178
|
|
|
153
179
|
## Common issues
|
|
154
180
|
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
181
|
+
- **First visit shows a "create profile" form.** The user has no profile row yet. That's expected; see step 4 if they should land on the show page.
|
|
182
|
+
- **Profile is read-only after creation.** The policy predates the generated `update?`. Add `def update? = true`.
|
|
183
|
+
- **No Profile link in the user menu.** The profile isn't connected to this portal, so `profile_url` returns `nil`. Run `pu:profile:conn --dest=<portal>`.
|
|
184
|
+
- **`SecuritySection` shows nothing**: none of the relevant Rodauth features are enabled. Enable `change_password`, `otp`, etc. on the Rodauth plugin.
|
|
158
185
|
|
|
159
186
|
## Related
|
|
160
187
|
|
|
161
|
-
- [Reference › Auth › Profile](/reference/auth/profile)
|
|
162
|
-
- [Reference › Auth › Accounts](/reference/auth/accounts)
|
|
163
|
-
- [Authentication](./authentication)
|
|
188
|
+
- [Reference › Auth › Profile](/reference/auth/profile): full surface
|
|
189
|
+
- [Reference › Auth › Accounts](/reference/auth/accounts): Rodauth feature flags that gate SecuritySection
|
|
190
|
+
- [Authentication](./authentication): the underlying auth setup
|