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
|
@@ -112,39 +112,39 @@ class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
|
112
112
|
index_page_title "Blog Posts"
|
|
113
113
|
index_page_description "Manage your blog content"
|
|
114
114
|
|
|
115
|
-
show_page_title { |record| record.title }
|
|
116
115
|
show_page_description "View post details"
|
|
117
116
|
end
|
|
118
117
|
```
|
|
119
118
|
|
|
119
|
+
These setters take a literal, or the definition's lazy `t("some.key")` to translate per request. They have no record context, so a title built from the record goes in a `page_title` override on the page class (below).
|
|
120
|
+
|
|
120
121
|
The default "Posts" heading becomes your branded title and description:
|
|
121
122
|
|
|
122
123
|

|
|
123
124
|
|
|
124
|
-
For more advanced customization,
|
|
125
|
+
For more advanced customization, override the page class nested in the definition:
|
|
125
126
|
|
|
126
127
|
```ruby
|
|
127
|
-
|
|
128
|
-
class
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def page_title
|
|
132
|
-
"Blog Posts"
|
|
133
|
-
end
|
|
128
|
+
class Blogging::PostDefinition < Blogging::ResourceDefinition
|
|
129
|
+
class ShowPage < ShowPage
|
|
130
|
+
private
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
def page_title
|
|
133
|
+
object.title
|
|
134
|
+
end
|
|
138
135
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
# Add content after the page header
|
|
137
|
+
def render_after_page_header
|
|
138
|
+
div(class: "pu-alert pu-alert-info", role: "status") do
|
|
139
|
+
div(class: "pu-alert-message") { t("blogging.posts.show.comment_count", count: object.comments.count) }
|
|
140
|
+
end
|
|
143
141
|
end
|
|
144
142
|
end
|
|
145
143
|
end
|
|
146
144
|
```
|
|
147
145
|
|
|
146
|
+
`pu-alert pu-alert-<success|warning|danger|info>` is the banner the flash messages use, with dark-mode colors built in. The text comes from a locale key (see [i18n](/reference/i18n#your-own-components-and-pages)).
|
|
147
|
+
|
|
148
148
|
## Custom Form Layout
|
|
149
149
|
|
|
150
150
|
Control form layout using wrapper options in definitions:
|
|
@@ -171,20 +171,35 @@ end
|
|
|
171
171
|
|
|
172
172
|
## Theming with TailwindCSS
|
|
173
173
|
|
|
174
|
-
Plutonium uses TailwindCSS 4.
|
|
174
|
+
Plutonium uses TailwindCSS 4. Out of the box the app serves the gem's prebuilt CSS, so first switch it to your own build:
|
|
175
175
|
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
```bash
|
|
177
|
+
rails generate pu:core:assets
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Brand colors are Tailwind palette colors compiled into the CSS, so change them in the generated `tailwind.config.js` through `plutoniumTailwindConfig.merge`, then rebuild:
|
|
181
|
+
|
|
182
|
+
```javascript
|
|
183
|
+
// tailwind.config.js
|
|
184
|
+
theme: plutoniumTailwindConfig.merge(plutoniumTailwindConfig.theme, {
|
|
185
|
+
extend: {
|
|
186
|
+
colors: {
|
|
187
|
+
primary: { 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca' }, // Indigo
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
```
|
|
180
192
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
Surfaces, borders, radii and shadows are `--pu-*` tokens. Override them after the Plutonium import in `app/assets/stylesheets/application.tailwind.css`, and repeat every one you change in a `.dark` block so dark mode gets its own value:
|
|
194
|
+
|
|
195
|
+
```css
|
|
196
|
+
:root {
|
|
197
|
+
--pu-radius-md: 0.5rem;
|
|
198
|
+
--pu-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
199
|
+
}
|
|
185
200
|
|
|
186
|
-
|
|
187
|
-
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.
|
|
201
|
+
.dark {
|
|
202
|
+
--pu-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
|
|
188
203
|
}
|
|
189
204
|
```
|
|
190
205
|
|
|
@@ -201,9 +216,9 @@ class StatusBadge < Plutonium::UI::Component::Base
|
|
|
201
216
|
|
|
202
217
|
def view_template
|
|
203
218
|
if @published
|
|
204
|
-
span(class: "
|
|
219
|
+
span(class: "pu-badge pu-badge-success") { t("blogging.posts.status.published") }
|
|
205
220
|
else
|
|
206
|
-
span(class: "
|
|
221
|
+
span(class: "pu-badge pu-badge-warning") { t("blogging.posts.status.draft") }
|
|
207
222
|
end
|
|
208
223
|
end
|
|
209
224
|
end
|
|
@@ -224,7 +239,7 @@ class CustomLayout < Plutonium::UI::Layout::ResourceLayout
|
|
|
224
239
|
|
|
225
240
|
# Customize body classes
|
|
226
241
|
def body_attributes
|
|
227
|
-
{class: "antialiased min-h-
|
|
242
|
+
{class: "antialiased pu-min-h-viewport bg-[var(--pu-surface)] text-[var(--pu-text)]"}
|
|
228
243
|
end
|
|
229
244
|
|
|
230
245
|
# Add content before the main section
|
|
@@ -60,7 +60,7 @@ Customize forms, tables, and views to match your requirements.
|
|
|
60
60
|
If you get stuck:
|
|
61
61
|
|
|
62
62
|
- Check the [Guides](/guides/) for task-oriented walkthroughs.
|
|
63
|
-
- Browse the [Reference](/reference/) for full API surface
|
|
63
|
+
- Browse the [Reference](/reference/) for full API surface: [App](/reference/app/), [Resource](/reference/resource/), [Behavior](/reference/behavior/), [UI](/reference/ui/), [Auth](/reference/auth/), [Tenancy](/reference/tenancy/), [Testing](/reference/testing/).
|
|
64
64
|
- Visit [GitHub Issues](https://github.com/radioactive-labs/plutonium-core/issues).
|
|
65
65
|
|
|
66
66
|
[Begin Chapter 1: Project Setup →](./01-setup)
|
|
@@ -40,6 +40,8 @@ rails g pu:res:conn Post --dest=admin_portal
|
|
|
40
40
|
|
|
41
41
|
This creates the portal-specific controller, policy, and definition, plus registers the resource in the portal's routes. Until you do this, the resource has no URL.
|
|
42
42
|
|
|
43
|
+
Run it after the migration. When there is no base policy to inherit from, `pu:res:conn` seeds the policy's attribute lists from the table's columns; on an unmigrated table it logs an error and writes empty lists.
|
|
44
|
+
|
|
43
45
|
For singular resources (`/profile`, `/settings`), add `--singular`:
|
|
44
46
|
|
|
45
47
|
```bash
|
|
@@ -48,7 +50,7 @@ rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
|
48
50
|
|
|
49
51
|
### 5. Trim the generated policy
|
|
50
52
|
|
|
51
|
-
The generator is liberal
|
|
53
|
+
The generator is liberal: it seeds `permitted_attributes_for_*` from your model columns. Open `packages/admin_portal/app/policies/admin_portal/post_policy.rb` and:
|
|
52
54
|
|
|
53
55
|
- Drop `_id` fields when the form should use the association name (e.g. `:user`, not `:user_id`).
|
|
54
56
|
- Replace `:price_cents` with `:price` if the model uses `has_cents`.
|
|
@@ -83,7 +85,7 @@ You should see:
|
|
|
83
85
|
|
|
84
86
|
Two paths:
|
|
85
87
|
|
|
86
|
-
**Migration only.** Add a new column with a standard Rails migration. Plutonium auto-detects it
|
|
88
|
+
**Migration only.** Add a new column with a standard Rails migration. Plutonium auto-detects it and shows it in all CRUD pages.
|
|
87
89
|
|
|
88
90
|
**Field with custom rendering.** Add the column, then declare it in the definition:
|
|
89
91
|
|
|
@@ -117,6 +119,7 @@ rails g pu:res:conn Post --dest=admin_portal
|
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
121
|
rails g pu:res:scaffold Blogging::Post title:string --dest=blogging
|
|
122
|
+
rails db:prepare
|
|
120
123
|
rails g pu:res:conn Blogging::Post --dest=admin_portal
|
|
121
124
|
```
|
|
122
125
|
|
|
@@ -132,8 +135,8 @@ The `blogging/post` syntax expands to `Blogging::Post`.
|
|
|
132
135
|
|
|
133
136
|
## Related
|
|
134
137
|
|
|
135
|
-
- [Reference › App › Generators](/reference/app/generators)
|
|
136
|
-
- [Reference › Resource](/reference/resource/)
|
|
137
|
-
- [Reference › App › Portals](/reference/app/portals)
|
|
138
|
-
- [Creating packages](./creating-packages)
|
|
139
|
-
- [Nested resources](./nested-resources)
|
|
138
|
+
- [Reference › App › Generators](/reference/app/generators): full generator catalog
|
|
139
|
+
- [Reference › Resource](/reference/resource/): model + definition + query + actions
|
|
140
|
+
- [Reference › App › Portals](/reference/app/portals): `pu:res:conn` details
|
|
141
|
+
- [Creating packages](./creating-packages): resources in feature packages
|
|
142
|
+
- [Nested resources](./nested-resources): parent/child relationships
|
|
@@ -6,7 +6,7 @@ Add Rodauth-based authentication to your Plutonium app.
|
|
|
6
6
|
|
|
7
7
|
Authenticated users can sign up, log in, change passwords, and reset forgotten passwords. Pages in protected portals are gated.
|
|
8
8
|
|
|
9
|
-
## Quick path
|
|
9
|
+
## Quick path: basic user auth
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# 1. Install Rodauth
|
|
@@ -22,7 +22,7 @@ rails db:prepare
|
|
|
22
22
|
# (when you run `pu:pkg:portal admin --auth=user`, this happens automatically)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
If you generated the portal with `--auth=user`, the engine is already mounted with the `Rodauth::Rails.authenticate(:user)` constraint
|
|
25
|
+
If you generated the portal with `--auth=user`, the engine is already mounted with the `Rodauth::Rails.authenticate(:user)` constraint: open `packages/admin_portal/config/routes.rb` to see it. The wiring looks like:
|
|
26
26
|
|
|
27
27
|
```ruby
|
|
28
28
|
# packages/admin_portal/config/routes.rb (generated)
|
|
@@ -64,13 +64,13 @@ EMAIL=admin@example.com rails rodauth:admin
|
|
|
64
64
|
|
|
65
65
|
The task creates the account and triggers a verification email; the admin sets their own password through that flow. No password is passed on the command line.
|
|
66
66
|
|
|
67
|
-
### Multi-tenant SaaS
|
|
67
|
+
### Multi-tenant SaaS: user + entity + membership in one shot
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
rails generate pu:saas:setup --user Customer --entity Organization
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
⚠️ This is a **meta-generator
|
|
73
|
+
⚠️ This is a **meta-generator**: it also runs `pu:saas:portal`, `pu:profile:setup`, `pu:saas:welcome`, and `pu:invites:install`. Don't re-run those manually. See [Reference › Auth › Accounts › SaaS setup](/reference/auth/accounts#saas-setup).
|
|
74
74
|
|
|
75
75
|
### API-only (JWT)
|
|
76
76
|
|
|
@@ -103,7 +103,7 @@ module CustomerPortal::Concerns::Controller
|
|
|
103
103
|
end
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Multiple account types
|
|
106
|
+
Multiple account types (different portals use different Rodauth instances):
|
|
107
107
|
|
|
108
108
|
```ruby
|
|
109
109
|
# Admin portal
|
|
@@ -117,40 +117,40 @@ See [Reference › App › Portals](/reference/app/portals#controller-concern-au
|
|
|
117
117
|
|
|
118
118
|
## Multiple portals in one browser
|
|
119
119
|
|
|
120
|
-
A person can hold a session in several portals at once
|
|
120
|
+
A person can hold a session in several portals at once (signed into the admin portal and the customer portal in the same browser, with neither evicting the other). Two settings make that work, and the generators emit both:
|
|
121
121
|
|
|
122
122
|
```ruby
|
|
123
|
-
# app/rodauth/rodauth_plugin.rb
|
|
123
|
+
# app/rodauth/rodauth_plugin.rb: the shared base
|
|
124
124
|
configure do
|
|
125
125
|
enable :session_isolation
|
|
126
126
|
end
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
```ruby
|
|
130
|
-
# app/rodauth/admin_rodauth_plugin.rb
|
|
130
|
+
# app/rodauth/admin_rodauth_plugin.rb: once per account type
|
|
131
131
|
configure do
|
|
132
132
|
session_key_prefix "admin_"
|
|
133
133
|
remember_cookie_key "_admin_remember"
|
|
134
134
|
end
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Both are required. `session_key_prefix` namespaces *every* session key a configuration touches
|
|
137
|
+
Both are required. `session_key_prefix` namespaces *every* session key a configuration touches, the account id plus `authenticated_by`, `login_redirect`, `two_factor_auth_setup` and the rest. `session_isolation` uses that prefix to decide which entries belong to whom, and stops one configuration's login from clearing another's.
|
|
138
138
|
|
|
139
139
|
::: danger Do not also set `session_key`
|
|
140
|
-
An explicit `session_key` is **not** prefixed (`convert_session_key`, `rodauth/features/base.rb:686
|
|
140
|
+
An explicit `session_key` is **not** prefixed (`convert_session_key`, `rodauth/features/base.rb:686`, only the default value passes through it). Setting both leaves the account id on a different name from every other key, so they stop rotating together. A session that holds an account id but no `authenticated_by` makes Rodauth raise on *every request*, `logged_in_via_remember_key?` calls `nil.include?` (`remember.rb:175`).
|
|
141
141
|
|
|
142
142
|
A config without a prefix at all is also not isolated: its keys are Rodauth's unprefixed defaults, indistinguishable from any other unprefixed config's, so `session_isolation` carries nothing for it.
|
|
143
143
|
:::
|
|
144
144
|
|
|
145
145
|
::: details Why this is needed
|
|
146
|
-
Rodauth resets the session on every login
|
|
146
|
+
Rodauth resets the session on every login, including the `remember` feature's `load_memory` autologin, to defend against session fixation, and rodauth-rails implements that reset as a full `reset_session`.
|
|
147
147
|
|
|
148
148
|
Without `session_isolation`, signing into one portal wipes every other portal's session. And because `RodauthApp#route` calls `load_memory` for *every* configuration on *every* request, the evicted configuration immediately autologins from its remember cookie and evicts the new one right back. The last `load_memory` call in the route block wins permanently, so the other portal can never hold a session at all.
|
|
149
149
|
|
|
150
150
|
`session_isolation` carries only the *other configurations'* session entries across the reset. The session id is still rotated and application session data is still cleared, so session fixation is still defeated.
|
|
151
151
|
:::
|
|
152
152
|
|
|
153
|
-
Reading a raw Rodauth session key? Go through its accessor, never the literal
|
|
153
|
+
Reading a raw Rodauth session key? Go through its accessor, never the literal: with a prefix set, the literal is the wrong key:
|
|
154
154
|
|
|
155
155
|
```ruby
|
|
156
156
|
after_login do
|
|
@@ -159,9 +159,9 @@ after_login do
|
|
|
159
159
|
end
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
**Upgrading an app generated before this existed:** add `enable :session_isolation` once in the base plugin and `session_key_prefix` in each account plugin
|
|
162
|
+
**Upgrading an app generated before this existed:** add `enable :session_isolation` once in the base plugin and `session_key_prefix` in each account plugin, and **delete the existing `session_key "_x_session"` line** while you're there, for the reason above.
|
|
163
163
|
|
|
164
|
-
Every key name changes together, so old session cookies stop matching and are ignored
|
|
164
|
+
Every key name changes together, so old session cookies stop matching and are ignored (the safe outcome). Keeping the old `session_key` to "preserve" logins is what produces the half-migrated session that crashes.
|
|
165
165
|
|
|
166
166
|
In practice only *unremembered* sessions drop: `remember_cookie_key` is a cookie name and isn't touched by the prefix, so anyone holding a valid `_x_remember` cookie is restored by `load_memory` on their next request, which writes the new prefixed keys. Expect logouts for users who never ticked "Remember Me", not a full sign-out.
|
|
167
167
|
|
|
@@ -180,9 +180,9 @@ after_login { remember_login }
|
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
::: warning Compare the value, don't just check presence
|
|
183
|
-
`param_or_nil(remember_param)` on its own is a truthiness check, and `remember_param` is shared with Rodauth's `/remember` settings page
|
|
183
|
+
`param_or_nil(remember_param)` on its own is a truthiness check, and `remember_param` is shared with Rodauth's `/remember` settings page, which submits `forget` and `disable` as well as `remember`. A bare presence check means `remember=disable` would remember you. Compare against `remember_remember_param_value`, as above.
|
|
184
184
|
|
|
185
|
-
Plutonium's login form also passes `include_hidden: false` to `check_box`, so an unticked box sends nothing at all rather than Rails' default `"0"`. With the value comparison that's belt-and-braces rather than load-bearing (`"0" == "remember"` is already false)
|
|
185
|
+
Plutonium's login form also passes `include_hidden: false` to `check_box`, so an unticked box sends nothing at all rather than Rails' default `"0"`. With the value comparison that's belt-and-braces rather than load-bearing (`"0" == "remember"` is already false), but it keeps a junk param off the wire and keeps the form correct for anyone who reverts the hook to a bare truthiness check.
|
|
186
186
|
:::
|
|
187
187
|
|
|
188
188
|
Logged-in users can change the setting later on Rodauth's `/remember` page (Remember / Forget / Disable).
|
|
@@ -254,15 +254,15 @@ user
|
|
|
254
254
|
|
|
255
255
|
## Common issues
|
|
256
256
|
|
|
257
|
-
- **"You need to set up Rodauth"
|
|
258
|
-
- **Portal redirects to login even though you're authenticated
|
|
259
|
-
- **Email confirmation never arrives in development
|
|
260
|
-
- **Signing into one portal signs you out of another
|
|
257
|
+
- **"You need to set up Rodauth"**: run `pu:rodauth:install` first.
|
|
258
|
+
- **Portal redirects to login even though you're authenticated**: the portal mount constraint references a different Rodauth account than the portal's controller concern uses. Match them up.
|
|
259
|
+
- **Email confirmation never arrives in development**: Plutonium sets ActionMailer to `:test` by default. Check `tmp/letter_opener/` or your mail interceptor. In production, configure SMTP (see above).
|
|
260
|
+
- **Signing into one portal signs you out of another**: or one portal can never stay signed in at all. Missing `enable :session_isolation` and/or `session_key_prefix`; see [Multiple portals in one browser](#multiple-portals-in-one-browser).
|
|
261
261
|
|
|
262
262
|
## Related
|
|
263
263
|
|
|
264
|
-
- [Reference › Auth](/reference/auth/)
|
|
265
|
-
- [Authorization](./authorization)
|
|
266
|
-
- [Multi-tenancy](./multi-tenancy)
|
|
267
|
-
- [User invites](./user-invites)
|
|
268
|
-
- [User profile](./user-profile)
|
|
264
|
+
- [Reference › Auth](/reference/auth/): full auth surface
|
|
265
|
+
- [Authorization](./authorization): controlling who can do what AFTER login
|
|
266
|
+
- [Multi-tenancy](./multi-tenancy): entity scoping for SaaS apps
|
|
267
|
+
- [User invites](./user-invites): invitation-based onboarding
|
|
268
|
+
- [User profile](./user-profile): account-settings page
|
|
@@ -10,9 +10,9 @@ For each resource, decide who can create / read / update / destroy / run custom
|
|
|
10
10
|
|
|
11
11
|
Every policy controls three things:
|
|
12
12
|
|
|
13
|
-
1. **Action permissions
|
|
14
|
-
2. **Attribute permissions
|
|
15
|
-
3. **Collection scope
|
|
13
|
+
1. **Action permissions**: `create?`, `read?`, `update?`, `destroy?`, plus your custom action methods.
|
|
14
|
+
2. **Attribute permissions**: `permitted_attributes_for_create`, `_for_read`, etc.
|
|
15
|
+
3. **Collection scope**: `relation_scope` (which records show up in lists).
|
|
16
16
|
|
|
17
17
|
## 🚨 Critical
|
|
18
18
|
|
|
@@ -39,7 +39,7 @@ class PostPolicy < ResourcePolicy
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
These default to `false
|
|
42
|
+
These default to `false`, without an explicit override, nobody can create or read records.
|
|
43
43
|
|
|
44
44
|
### 3. Override derived methods only when rules differ
|
|
45
45
|
|
|
@@ -55,7 +55,7 @@ def destroy?
|
|
|
55
55
|
end
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
🚨 **`record` is the resource CLASS on collection routes.** `read?` backs both `show?` (called with the record) and `index?` (called with the class
|
|
58
|
+
🚨 **`record` is the resource CLASS on collection routes.** `read?` backs both `show?` (called with the record) and `index?` (called with the class: there is no single record to pass). The same goes for `create?`/`new?`, `export_csv?`, `search?`, and resource-action gates. So `def read? = record.published?` raises `NoMethodError` the moment the index renders. Keep record-state rules out of these methods: filter what the list shows in `relation_scope` (step 6 below), and gate individual records in `show?` (which always receives the record). Record-action methods like `publish?` are safe; they are always evaluated against an instance.
|
|
59
59
|
|
|
60
60
|
### 4. Declare attribute permissions
|
|
61
61
|
|
|
@@ -69,8 +69,8 @@ def permitted_attributes_for_read
|
|
|
69
69
|
end
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
::: warning Index has no `record`
|
|
73
|
-
`permitted_attributes_for_index` runs at collection level
|
|
72
|
+
::: warning Index has no `record` instance
|
|
73
|
+
`permitted_attributes_for_index` runs at collection level, where `record` is the resource class. `_for_index` falls back to `_for_read` (and `_for_export` to `_for_index`), so if you write a `record`-dependent `_for_read`, you MUST also declare an explicit `_for_index` that never touches `record`. See [Reference › Behavior › Policies › Index has no record](/reference/behavior/policies#index-has-no-record).
|
|
74
74
|
:::
|
|
75
75
|
|
|
76
76
|
### 5. Custom action methods
|
|
@@ -87,7 +87,7 @@ end
|
|
|
87
87
|
|
|
88
88
|
The method name matches the action name plus `?`. Undefined methods return `false`.
|
|
89
89
|
|
|
90
|
-
### 6. Optionally filter the collection
|
|
90
|
+
### 6. Optionally filter the collection: `relation_scope`
|
|
91
91
|
|
|
92
92
|
```ruby
|
|
93
93
|
relation_scope do |relation|
|
|
@@ -95,7 +95,7 @@ relation_scope do |relation|
|
|
|
95
95
|
end
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
🚨 `default_relation_scope(relation)` must be called somewhere in the chain
|
|
98
|
+
🚨 `default_relation_scope(relation)` must be called somewhere in the chain, otherwise `verify_default_relation_scope_applied!` raises at runtime. Calling it explicitly here is safest. `super` works only when the parent policy also calls it.
|
|
99
99
|
|
|
100
100
|
## Common patterns
|
|
101
101
|
|
|
@@ -143,7 +143,7 @@ def update?
|
|
|
143
143
|
end
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
## Bulk action authorization
|
|
146
|
+
## Bulk action authorization: per record
|
|
147
147
|
|
|
148
148
|
```ruby
|
|
149
149
|
def bulk_archive?
|
|
@@ -154,7 +154,7 @@ end
|
|
|
154
154
|
- **Backend:** if any selected record fails, the entire request is rejected.
|
|
155
155
|
- **UI:** only actions ALL selected records support are shown (intersection).
|
|
156
156
|
|
|
157
|
-
Records come from `current_authorized_scope
|
|
157
|
+
Records come from `current_authorized_scope`; users can only select records they can access.
|
|
158
158
|
|
|
159
159
|
## Portal-specific policies
|
|
160
160
|
|
|
@@ -163,7 +163,7 @@ class PostPolicy < ResourcePolicy
|
|
|
163
163
|
def create? = user.present?
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
# Admin
|
|
166
|
+
# Admin: more permissive
|
|
167
167
|
class AdminPortal::PostPolicy < ::PostPolicy
|
|
168
168
|
include AdminPortal::ResourcePolicy
|
|
169
169
|
|
|
@@ -171,7 +171,7 @@ class AdminPortal::PostPolicy < ::PostPolicy
|
|
|
171
171
|
def permitted_attributes_for_create = %i[title content featured internal_notes]
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
# Public
|
|
174
|
+
# Public: read-only
|
|
175
175
|
class PublicPortal::PostPolicy < ::PostPolicy
|
|
176
176
|
include PublicPortal::ResourcePolicy
|
|
177
177
|
def create? = false
|
|
@@ -186,7 +186,7 @@ def permitted_associations
|
|
|
186
186
|
end
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
Drives the show-page tablist. Each named association must exist on the model AND be a registered Plutonium resource. See [Reference › Behavior › Policies › Association permissions](/reference/behavior/policies#association-permissions).
|
|
189
|
+
Drives the show-page tablist. Each named association must exist on the model AND be a registered Plutonium resource in every portal that uses the policy; a portal that doesn't register the child raises `ArgumentError ... is not a registered resource` on its show page. To add a tab in one portal only, put it in a portal-specific policy (`rails g pu:res:conn <Resource> --dest=<portal> --policy`). See [Reference › Behavior › Policies › Association permissions](/reference/behavior/policies#association-permissions).
|
|
190
190
|
|
|
191
191
|
::: warning Not for nested forms
|
|
192
192
|
`permitted_associations` is for show-page navigation tabs, NOT nested forms. Nested forms come from `nested_input :variants` in the definition. See [Reference › Resource › Definition › Nested inputs](/reference/resource/definition#nested-inputs).
|
|
@@ -194,7 +194,7 @@ Drives the show-page tablist. Each named association must exist on the model AND
|
|
|
194
194
|
|
|
195
195
|
## Multi-tenant scoping
|
|
196
196
|
|
|
197
|
-
When the portal sets `scope_to_entity Organization`, the inherited `relation_scope` automatically filters everything to the current org
|
|
197
|
+
When the portal sets `scope_to_entity Organization`, the inherited `relation_scope` automatically filters everything to the current org, no work in the policy. To add filters on top:
|
|
198
198
|
|
|
199
199
|
```ruby
|
|
200
200
|
relation_scope do |relation|
|
|
@@ -243,15 +243,15 @@ end
|
|
|
243
243
|
|
|
244
244
|
## Common issues
|
|
245
245
|
|
|
246
|
-
- **Undefined custom action policy method
|
|
247
|
-
- **`record.X` crashes during index
|
|
248
|
-
- **`verify_default_relation_scope_applied!` raises
|
|
249
|
-
- **`super` in `relation_scope
|
|
246
|
+
- **Undefined custom action policy method**: the button silently disappears (undefined returns `false`). Add `def my_action?` to the policy.
|
|
247
|
+
- **`record.X` crashes during index**: `record` is the resource class on index, not an instance. Add an explicit `permitted_attributes_for_index` that doesn't depend on `record`.
|
|
248
|
+
- **`verify_default_relation_scope_applied!` raises**: your custom `relation_scope` doesn't call `default_relation_scope(relation)`. Fix by composing: `default_relation_scope(relation).where(...)`.
|
|
249
|
+
- **`super` in `relation_scope`**; works when you're extending a parent policy that itself calls `default_relation_scope`. If you're not sure (or you're inheriting from `Plutonium::Resource::Policy` directly), call `default_relation_scope(relation)` explicitly. The runtime check verifies `default_relation_scope` was hit somewhere: not that you wrote it in this class.
|
|
250
250
|
|
|
251
251
|
## Related
|
|
252
252
|
|
|
253
|
-
- [Reference › Behavior › Policies](/reference/behavior/policies)
|
|
254
|
-
- [Reference › Tenancy › Entity scoping](/reference/tenancy/entity-scoping)
|
|
255
|
-
- [Authentication](./authentication)
|
|
256
|
-
- [Multi-tenancy](./multi-tenancy)
|
|
257
|
-
- [Custom actions](./custom-actions)
|
|
253
|
+
- [Reference › Behavior › Policies](/reference/behavior/policies): full policy surface
|
|
254
|
+
- [Reference › Tenancy › Entity scoping](/reference/tenancy/entity-scoping): `default_relation_scope`, multi-tenant patterns
|
|
255
|
+
- [Authentication](./authentication): who's the user in the first place
|
|
256
|
+
- [Multi-tenancy](./multi-tenancy): entity scoping setup
|
|
257
|
+
- [Custom actions](./custom-actions): defining the actions that need policy methods
|
|
@@ -13,7 +13,7 @@ Domain code (models, policies, definitions, interactions) lives in **feature pac
|
|
|
13
13
|
| **Feature** | Business logic | `pu:pkg:package NAME` | `blogging`, `billing`, `inventory` |
|
|
14
14
|
| **Portal** | Web interface | `pu:pkg:portal NAME` | `admin_portal`, `customer_portal`, `public_portal` |
|
|
15
15
|
|
|
16
|
-
🚨 Don't mix the two. Feature packages own the **domain code
|
|
16
|
+
🚨 Don't mix the two. Feature packages own the **domain code**: models, interactions, policies/definitions for resources owned by that feature. Portal packages own the **web surface**: controllers, routes, auth, and portal-specific policy/definition *overrides* for resources they expose.
|
|
17
17
|
|
|
18
18
|
## Feature package
|
|
19
19
|
|
|
@@ -59,12 +59,12 @@ rails g pu:pkg:portal admin --auth=user
|
|
|
59
59
|
|
|
60
60
|
Options:
|
|
61
61
|
|
|
62
|
-
- `--auth=NAME
|
|
63
|
-
- `--public
|
|
64
|
-
- `--byo
|
|
65
|
-
- `--scope=CLASS
|
|
62
|
+
- `--auth=NAME`: Rodauth account to authenticate with.
|
|
63
|
+
- `--public`: public access, no auth.
|
|
64
|
+
- `--byo`: bring your own auth.
|
|
65
|
+
- `--scope=CLASS`: entity class for multi-tenancy.
|
|
66
66
|
|
|
67
|
-
The generator mounts the engine for you
|
|
67
|
+
The generator mounts the engine for you: at `/admin` in this case, wrapped in `constraints Rodauth::Rails.authenticate(:user)` because you passed `--auth=user`. Open `packages/admin_portal/config/routes.rb` to see the generated mount.
|
|
68
68
|
|
|
69
69
|
### 2. Connect resources
|
|
70
70
|
|
|
@@ -87,7 +87,7 @@ Every file under `app/<kind>/blogging/` resolves to `Blogging::*`:
|
|
|
87
87
|
- `app/models/blogging/post.rb` → `Blogging::Post`
|
|
88
88
|
- `app/policies/blogging/post_policy.rb` → `Blogging::PostPolicy`
|
|
89
89
|
|
|
90
|
-
Each feature package gets base classes
|
|
90
|
+
Each feature package gets base classes, `Blogging::ApplicationRecord`, `Blogging::ResourceRecord`, `Blogging::ResourcePolicy`, `Blogging::ResourceDefinition`, `Blogging::ResourceInteraction`, that inherit from the main app's.
|
|
91
91
|
|
|
92
92
|
## Cross-package references
|
|
93
93
|
|
|
@@ -129,7 +129,7 @@ packages/
|
|
|
129
129
|
└── customer_portal/ # Portal: customer dashboard
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
The portals expose the features. A single feature can be exposed by multiple portals
|
|
132
|
+
The portals expose the features. A single feature can be exposed by multiple portals, usually with different policies and definitions per portal.
|
|
133
133
|
|
|
134
134
|
## Package loading
|
|
135
135
|
|
|
@@ -147,13 +147,13 @@ Loaded from `config/application.rb`. Migrations from all packages are picked up
|
|
|
147
147
|
## Per-portal overrides
|
|
148
148
|
|
|
149
149
|
```ruby
|
|
150
|
-
# Definition
|
|
150
|
+
# Definition: how fields render per portal
|
|
151
151
|
class AdminPortal::PostDefinition < ::PostDefinition
|
|
152
152
|
scope :pending_review
|
|
153
153
|
input :internal_notes, hint: "Not shown to the author"
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
# Policy
|
|
156
|
+
# Policy: which fields exist, and who may act
|
|
157
157
|
# `internal_notes` appears for admins because THIS permits it,
|
|
158
158
|
# not because the definition above mentions it.
|
|
159
159
|
class AdminPortal::PostPolicy < ::PostPolicy
|
|
@@ -166,13 +166,13 @@ end
|
|
|
166
166
|
|
|
167
167
|
## Common issues
|
|
168
168
|
|
|
169
|
-
- **Class not loading
|
|
170
|
-
- **Migration not running
|
|
171
|
-
- **Cross-package association fails
|
|
169
|
+
- **Class not loading**: namespace must match the directory: `app/models/blogging/post.rb` MUST be `Blogging::Post`.
|
|
170
|
+
- **Migration not running**: package migrations are auto-included. If they aren't running, check `config/packages.rb` is loaded from `application.rb`.
|
|
171
|
+
- **Cross-package association fails**: use `blogging/post:belongs_to` in `pu:res:scaffold`, OR manually set `class_name: "Blogging::Post"` on the `belongs_to`.
|
|
172
172
|
|
|
173
173
|
## Related
|
|
174
174
|
|
|
175
|
-
- [Reference › App › Packages](/reference/app/packages)
|
|
176
|
-
- [Reference › App › Portals](/reference/app/portals)
|
|
177
|
-
- [Adding resources](./adding-resources)
|
|
178
|
-
- [Authentication](./authentication)
|
|
175
|
+
- [Reference › App › Packages](/reference/app/packages): full package surface
|
|
176
|
+
- [Reference › App › Portals](/reference/app/portals): portal-specific configuration
|
|
177
|
+
- [Adding resources](./adding-resources): `pu:res:scaffold` and `pu:res:conn`
|
|
178
|
+
- [Authentication](./authentication): portal auth setup
|