plutonium 0.51.0 → 0.52.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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-app/SKILL.md +2 -0
  3. data/.claude/skills/plutonium-auth/SKILL.md +6 -4
  4. data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
  5. data/.claude/skills/plutonium-tenancy/SKILL.md +25 -6
  6. data/.claude/skills/plutonium-testing/SKILL.md +3 -1
  7. data/.claude/skills/plutonium-ui/SKILL.md +3 -3
  8. data/CHANGELOG.md +17 -0
  9. data/app/assets/plutonium.css +1 -1
  10. data/app/assets/plutonium.js +1 -0
  11. data/app/assets/plutonium.js.map +3 -3
  12. data/app/assets/plutonium.min.js +1 -1
  13. data/app/assets/plutonium.min.js.map +3 -3
  14. data/docs/.vitepress/config.ts +1 -2
  15. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
  16. data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
  17. data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
  18. data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
  19. data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
  20. data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
  21. data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
  22. data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
  23. data/docs/.vitepress/theme/custom.css +144 -0
  24. data/docs/.vitepress/theme/index.ts +58 -1
  25. data/docs/getting-started/index.md +33 -50
  26. data/docs/getting-started/tutorial/02-first-resource.md +17 -8
  27. data/docs/getting-started/tutorial/03-authentication.md +31 -23
  28. data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
  29. data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
  30. data/docs/getting-started/tutorial/07-author-portal.md +8 -0
  31. data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
  32. data/docs/guides/authentication.md +10 -5
  33. data/docs/guides/authorization.md +3 -3
  34. data/docs/guides/creating-packages.md +8 -11
  35. data/docs/guides/custom-actions.md +6 -1
  36. data/docs/guides/customizing-ui.md +258 -0
  37. data/docs/guides/index.md +49 -32
  38. data/docs/guides/multi-tenancy.md +10 -2
  39. data/docs/guides/nested-resources.md +69 -0
  40. data/docs/guides/search-filtering.md +6 -0
  41. data/docs/guides/testing.md +5 -1
  42. data/docs/guides/theming.md +13 -0
  43. data/docs/guides/user-invites.md +10 -4
  44. data/docs/guides/user-profile.md +8 -0
  45. data/docs/index.md +10 -219
  46. data/docs/public/asciinema/home-scaffold.cast +305 -0
  47. data/docs/public/images/guides/custom-actions-bulk.png +0 -0
  48. data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
  49. data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
  50. data/docs/public/images/guides/nested-inputs.png +0 -0
  51. data/docs/public/images/guides/nested-resources-tab.png +0 -0
  52. data/docs/public/images/guides/search-filtering-index.png +0 -0
  53. data/docs/public/images/guides/search-filtering-panel.png +0 -0
  54. data/docs/public/images/guides/theming-after.png +0 -0
  55. data/docs/public/images/guides/theming-before.png +0 -0
  56. data/docs/public/images/guides/user-invites-landing.png +0 -0
  57. data/docs/public/images/guides/user-profile-edit.png +0 -0
  58. data/docs/public/images/guides/user-profile-show.png +0 -0
  59. data/docs/public/images/home-index.png +0 -0
  60. data/docs/public/images/home-new.png +0 -0
  61. data/docs/public/images/home-show.png +0 -0
  62. data/docs/public/images/tutorial/02-empty-index.png +0 -0
  63. data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
  64. data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
  65. data/docs/public/images/tutorial/02-new-form.png +0 -0
  66. data/docs/public/images/tutorial/03-create-account.png +0 -0
  67. data/docs/public/images/tutorial/03-login.png +0 -0
  68. data/docs/public/images/tutorial/04-admin-index.png +0 -0
  69. data/docs/public/images/tutorial/05-actions-menu.png +0 -0
  70. data/docs/public/images/tutorial/05-row-actions.png +0 -0
  71. data/docs/public/images/tutorial/06-comments-tab.png +0 -0
  72. data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
  73. data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
  74. data/docs/public/images/tutorial/07-author-portal.png +0 -0
  75. data/docs/public/images/tutorial/08-customized-index.png +0 -0
  76. data/docs/reference/app/generators.md +4 -4
  77. data/docs/reference/auth/accounts.md +6 -7
  78. data/docs/reference/auth/index.md +1 -1
  79. data/docs/reference/behavior/policies.md +1 -1
  80. data/docs/reference/index.md +67 -55
  81. data/docs/reference/resource/definition.md +1 -1
  82. data/docs/reference/tenancy/entity-scoping.md +8 -1
  83. data/docs/reference/tenancy/index.md +1 -1
  84. data/docs/reference/tenancy/invites.md +12 -5
  85. data/docs/reference/ui/tables.md +8 -4
  86. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
  87. data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
  88. data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
  89. data/gemfiles/rails_7.gemfile.lock +1 -1
  90. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  91. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  92. data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
  93. data/lib/generators/pu/invites/install_generator.rb +44 -0
  94. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
  95. data/lib/generators/pu/profile/conn_generator.rb +2 -2
  96. data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
  97. data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
  98. data/lib/generators/pu/rodauth/account_generator.rb +2 -1
  99. data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
  100. data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
  101. data/lib/generators/pu/rodauth/views_generator.rb +0 -2
  102. data/lib/generators/pu/saas/membership/USAGE +4 -1
  103. data/lib/generators/pu/saas/setup_generator.rb +16 -4
  104. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
  105. data/lib/plutonium/helpers/turbo_helper.rb +19 -0
  106. data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
  107. data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
  108. data/lib/plutonium/ui/component/methods.rb +1 -0
  109. data/lib/plutonium/ui/form/base.rb +17 -1
  110. data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
  111. data/lib/plutonium/ui/form/interaction.rb +1 -1
  112. data/lib/plutonium/ui/form/theme.rb +1 -1
  113. data/lib/plutonium/ui/page/edit.rb +1 -1
  114. data/lib/plutonium/ui/page/new.rb +1 -1
  115. data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
  116. data/lib/plutonium/version.rb +1 -1
  117. data/package.json +4 -1
  118. data/src/js/controllers/form_controller.js +5 -4
  119. data/yarn.lock +108 -1
  120. metadata +45 -3
@@ -185,7 +185,7 @@ rails rodauth_admin:create[admin@example.com,password123]
185
185
 
186
186
  ```bash
187
187
  rails g pu:saas:setup --user Customer --entity Organization
188
- rails g pu:saas:setup --user Customer --entity Organization --roles=member,admin
188
+ rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
189
189
  rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
190
190
  rails g pu:saas:setup --user Customer --entity Organization \
191
191
  --user-attributes=name:string --entity-attributes=slug:string
@@ -216,7 +216,7 @@ For when you don't want the full `pu:saas:setup` meta-generator:
216
216
  ```bash
217
217
  rails g pu:saas:user Customer
218
218
  rails g pu:saas:entity Organization --extra-attributes=slug:string
219
- rails g pu:saas:membership --user Customer --entity Organization --roles=member,admin
219
+ rails g pu:saas:membership --user Customer --entity Organization --roles=admin,member
220
220
  rails g pu:saas:portal customer --entity Organization
221
221
  rails g pu:saas:welcome --user Customer --entity Organization
222
222
  ```
@@ -305,10 +305,10 @@ rails g pu:invites:install --entity-model=Organization --user-model=Customer --i
305
305
  | `--entity-model=NAME` | `Entity` | Entity model name |
306
306
  | `--user-model=NAME` | `User` | User model name |
307
307
  | `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name |
308
- | `--membership-model=NAME` | `EntityUser` | Membership join model |
309
- | `--roles` | `member,admin` | Comma-separated |
308
+ | `--membership-model=NAME` | `EntityUser` | Membership join model (must already exist; roles read from its `enum :role`) |
310
309
  | `--rodauth=NAME` | `user` | Rodauth configuration for signup |
311
310
  | `--enforce-domain` | `false` | Require email domain to match entity |
311
+ | `--dest=PACKAGE` | `main_app` | Package where the entity model lives (controls where `invite_user_interaction.rb` is generated) |
312
312
 
313
313
  Multiple invite flows are supported — run `pu:invites:install` once per flow.
314
314
 
@@ -14,7 +14,6 @@ rails generate pu:rodauth:account user [options]
14
14
  |---|---|
15
15
  | `--defaults` | Enables login, logout, remember, password reset |
16
16
  | `--kitchen_sink` | Enables ALL features |
17
- | `--primary` | Mark as primary account (no URL prefix) |
18
17
  | `--no-mails` | Skip mailer setup |
19
18
  | `--argon2` | Use Argon2 instead of bcrypt |
20
19
  | `--api_only` | JSON API only (no sessions) |
@@ -50,9 +49,6 @@ rails generate pu:rodauth:account user [options]
50
49
  # Basic account
51
50
  rails g pu:rodauth:account user
52
51
 
53
- # Primary account (no URL prefix)
54
- rails g pu:rodauth:account user --primary
55
-
56
52
  # With 2FA
57
53
  rails g pu:rodauth:account user --otp --recovery_codes
58
54
 
@@ -84,12 +80,15 @@ rails g pu:rodauth:admin admin --extra-attributes=name:string,department:string
84
80
  enum :role, super_admin: 0, admin: 1
85
81
  ```
86
82
 
87
- Rake task for direct admin creation:
83
+ Rake task for direct admin creation (generated alongside the account — namespace is `rodauth`, task name is the account name):
88
84
 
89
85
  ```bash
90
- rails rodauth_admin:create[admin@example.com,password123]
86
+ EMAIL=admin@example.com rails rodauth:admin
87
+ # (run without EMAIL to be prompted)
91
88
  ```
92
89
 
90
+ The task creates the account and triggers a verification email; the admin sets their own password via that flow. No password is passed on the command line.
91
+
93
92
  ## SaaS setup — `pu:saas:setup` (meta-generator)
94
93
 
95
94
  Creates the User + Entity + Membership trio AND runs:
@@ -105,7 +104,7 @@ After `pu:saas:setup` runs, don't separately run `pu:saas:portal`, `pu:profile:s
105
104
 
106
105
  ```bash
107
106
  rails g pu:saas:setup --user Customer --entity Organization
108
- rails g pu:saas:setup --user Customer --entity Organization --roles=member,admin
107
+ rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
109
108
  rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
110
109
  rails g pu:saas:setup --user Customer --entity Organization \
111
110
  --user-attributes=name:string --entity-attributes=slug:string
@@ -43,7 +43,7 @@ class AdminController < PlutoniumController
43
43
  end
44
44
  ```
45
45
 
46
- `Plutonium::Auth::Rodauth(:name)` exposes `current_user`, `logout_url`, and `rodauth` in the controller.
46
+ `Plutonium::Auth::Rodauth(:name)` exposes `current_user`, `logout_url`, `profile_url`, and `rodauth` in the controller (all available as helper methods in views too).
47
47
 
48
48
  For portal wiring (`AdminPortal::Concerns::Controller`), see [App › Portals](/reference/app/portals#controller-concern-auth).
49
49
 
@@ -11,7 +11,7 @@ Authorization for resources. Built on [ActionPolicy](https://actionpolicy.evilma
11
11
 
12
12
  - **`create?` and `read?` default to `false`.** You MUST override them explicitly. Everything else (`update?`, `destroy?`, `index?`, `show?`, …) derives from one of those.
13
13
  - **`permitted_attributes_for_*` must be explicit in production.** Dev auto-detects; production raises.
14
- - **`relation_scope` must call `default_relation_scope(relation)` explicitly** never `super`. Bypassing it triggers `verify_default_relation_scope_applied!`.
14
+ - **`relation_scope` must end up calling `default_relation_scope(relation)` somewhere in the chain.** Prefer calling it explicitly in your override. `super` is fine when extending a parent policy (e.g., a package base) that itself calls it. The runtime check verifies it was hit somewhere — not in this specific class.
15
15
  - **For `has_cents` fields, use the virtual name** (`:price`), NEVER `:price_cents`.
16
16
  - **Don't put `*_attributes` hashes in `permitted_attributes_for_*`.** Nested forms are extracted from the form definition, not the policy. List the association name (`:variants`) and the `nested_input` in the definition handles the rest.
17
17
  - **Custom action ⇒ policy method.** `action :publish` needs `def publish?`. Undefined methods return `false` → action silently disappears.
@@ -1,56 +1,68 @@
1
- # Reference
1
+ ---
2
+ layout: page
3
+ sidebar: false
4
+ aside: false
5
+ ---
2
6
 
3
- Concept-by-concept API documentation. For task-oriented walkthroughs, see [Guides](/guides/).
4
-
5
- ## The seven areas
6
-
7
- ### [App](/reference/app/)
8
- Installation, packages (feature + portal), portal engines, mounting, route registration (including singular and custom routes), connecting resources via `pu:res:conn`, full generator catalog.
9
-
10
- ### [Resource](/reference/resource/)
11
- The four-layer resource — model, definition, query, actions. `pu:res:scaffold` field-type syntax, `has_cents`, SGID, URL routing, definition DSL (fields, inputs, displays, columns), page chrome, metadata panel, index views (table & grid), search, filters, scopes, sorting, custom + bulk actions.
12
-
13
- ### [Behavior](/reference/behavior/)
14
- Controllers, policies, interactions. Controller hooks (redirect, params, presentation), policy action methods and `permitted_attributes_for_*`, `permitted_associations`, `relation_scope`, interaction structure, outcomes, chaining, URL generation.
15
-
16
- ### [UI](/reference/ui/)
17
- Pages, forms, displays, tables, components, layouts, assets. Custom page classes, form field builders, association inputs (typeahead + inline `+`), built-in component kit, custom Phlex components, the shell, design tokens, `.pu-*` component classes, Phlexi themes.
18
-
19
- ### [Auth](/reference/auth/)
20
- Rodauth installation, account types (basic / admin / SaaS), profile resource with the SecuritySection component.
21
-
22
- ### [Tenancy](/reference/tenancy/)
23
- Multi-tenant entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources (parent/child routes, scoping), user invitations.
24
-
25
- ### [Testing](/reference/testing/)
26
- The `Plutonium::Testing::*` concerns — CRUD, policy matrix, definition smoke tests, model concerns, nested resources, portal access, interaction outcomes.
27
-
28
- ## Quick reference
29
-
30
- | I need to… | See |
31
- |---|---|
32
- | Install Plutonium | [App › Index](/reference/app/) |
33
- | Run a generator | [App › Generators](/reference/app/generators) |
34
- | Create a portal | [App › Portals](/reference/app/portals) |
35
- | Scaffold a resource | [App › Generators › `pu:res:scaffold`](/reference/app/generators#pu-res-scaffold) |
36
- | Configure form fields | [Resource › Definition](/reference/resource/definition) |
37
- | Add search / filters | [Resource › Query](/reference/resource/query) |
38
- | Add custom buttons / bulk actions | [Resource › Actions](/reference/resource/actions) |
39
- | Override CRUD redirects / params | [Behavior › Controllers](/reference/behavior/controllers) |
40
- | Control who can see what | [Behavior › Policies](/reference/behavior/policies) |
41
- | Write business logic | [Behavior › Interactions](/reference/behavior/interactions) |
42
- | Customize a page | [UI › Pages](/reference/ui/pages) |
43
- | Customize a form | [UI › Forms](/reference/ui/forms) |
44
- | Style the UI | [UI › Assets](/reference/ui/assets) |
45
- | Set up Rodauth | [Auth › Accounts](/reference/auth/accounts) |
46
- | Add a profile page | [Auth › Profile](/reference/auth/profile) |
47
- | Scope to a tenant | [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) |
48
- | Wire user invitations | [Tenancy › Invites](/reference/tenancy/invites) |
49
- | Test a resource | [Testing](/reference/testing/) |
50
-
51
- ## Reading this reference
52
-
53
- - **🚨 Critical blocks** at the top of each page surface the "you'll regret this" rules. Skim them even if you're skimming the rest.
54
- - **Option / DSL tables** are designed for scanning — find your option name without reading prose.
55
- - **Cross-references** use VitePress relative paths. If a link points somewhere that doesn't exist yet, it's a known gap.
56
- - **Concrete decision rules** ("use X when…, Y when…") sit alongside the option references. Reach for them when in doubt.
7
+ <SectionLanding
8
+ eyebrow="Reference"
9
+ title="Every API, in one place."
10
+ lede="The full surface area of Plutonium — controllers, policies, definitions, fields, interactions, generators."
11
+ mode="categorized"
12
+ :rail="[
13
+ { group: 'App', items: [
14
+ { name: 'Overview', link: '/plutonium-core/reference/app/' },
15
+ { name: 'Packages', link: '/plutonium-core/reference/app/packages' },
16
+ { name: 'Portals', link: '/plutonium-core/reference/app/portals' },
17
+ { name: 'Generators', link: '/plutonium-core/reference/app/generators' },
18
+ ]},
19
+ { group: 'Resource', items: [
20
+ { name: 'Overview', link: '/plutonium-core/reference/resource/' },
21
+ { name: 'Model', link: '/plutonium-core/reference/resource/model' },
22
+ { name: 'Definition', link: '/plutonium-core/reference/resource/definition' },
23
+ { name: 'Query', link: '/plutonium-core/reference/resource/query' },
24
+ { name: 'Actions', link: '/plutonium-core/reference/resource/actions' },
25
+ ]},
26
+ { group: 'Behavior', items: [
27
+ { name: 'Overview', link: '/plutonium-core/reference/behavior/' },
28
+ { name: 'Controllers', link: '/plutonium-core/reference/behavior/controllers' },
29
+ { name: 'Policies', link: '/plutonium-core/reference/behavior/policies' },
30
+ { name: 'Interactions', link: '/plutonium-core/reference/behavior/interactions' },
31
+ ]},
32
+ { group: 'UI', items: [
33
+ { name: 'Overview', link: '/plutonium-core/reference/ui/' },
34
+ { name: 'Pages', link: '/plutonium-core/reference/ui/pages' },
35
+ { name: 'Forms', link: '/plutonium-core/reference/ui/forms' },
36
+ { name: 'Displays', link: '/plutonium-core/reference/ui/displays' },
37
+ { name: 'Tables', link: '/plutonium-core/reference/ui/tables' },
38
+ { name: 'Components', link: '/plutonium-core/reference/ui/components' },
39
+ { name: 'Layouts', link: '/plutonium-core/reference/ui/layouts' },
40
+ { name: 'Assets', link: '/plutonium-core/reference/ui/assets' },
41
+ ]},
42
+ { group: 'Auth', items: [
43
+ { name: 'Overview', link: '/plutonium-core/reference/auth/' },
44
+ { name: 'Accounts', link: '/plutonium-core/reference/auth/accounts' },
45
+ { name: 'Profile', link: '/plutonium-core/reference/auth/profile' },
46
+ ]},
47
+ { group: 'Tenancy', items: [
48
+ { name: 'Overview', link: '/plutonium-core/reference/tenancy/' },
49
+ { name: 'Entity scoping', link: '/plutonium-core/reference/tenancy/entity-scoping' },
50
+ { name: 'Nested resources', link: '/plutonium-core/reference/tenancy/nested-resources' },
51
+ { name: 'Invites', link: '/plutonium-core/reference/tenancy/invites' },
52
+ ]},
53
+ { group: 'Testing', items: [
54
+ { name: 'Overview', link: '/plutonium-core/reference/testing/' },
55
+ ]},
56
+ ]"
57
+ :sidebar="[
58
+ { heading: 'Learning?', items: [
59
+ { label: 'Tutorial', href: '/plutonium-core/getting-started/tutorial/' },
60
+ ]},
61
+ { heading: 'Solving a problem?', items: [
62
+ { label: 'Guides', href: '/plutonium-core/guides/' },
63
+ ]},
64
+ { heading: 'Need help?', items: [
65
+ { label: 'GitHub Discussions', href: 'https://github.com/radioactive-labs/plutonium-core/discussions' },
66
+ ]},
67
+ ]"
68
+ />
@@ -111,7 +111,7 @@ end
111
111
 
112
112
  ### Display types (show / index)
113
113
 
114
- `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`
114
+ `:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
115
115
 
116
116
  ## Field options
117
117
 
@@ -239,10 +239,17 @@ The strategy symbol must match a method name on the controller concern.
239
239
 
240
240
  ### Custom param key
241
241
 
242
- When the param name differs from the entity model name:
242
+ The default `param_key` derives from the entity class — `<singular_route_key>_scoped` — to avoid collisions with a `belongs_to :organization` on child models. So `scope_to_entity Organization` produces routes like `/organization_scoped/:organization_scoped_id/posts`. Override when you want a cleaner URL:
243
243
 
244
244
  ```ruby
245
245
  scope_to_entity Organization, strategy: :path, param_key: :org_id
246
+ # → /org_id/:org_id/posts
247
+ ```
248
+
249
+ Pair with `route_key:` to control the path segment as well:
250
+
251
+ ```ruby
252
+ scope_to_entity Organization, strategy: :path, param_key: :org_id, route_key: :orgs
246
253
  # → /orgs/:org_id/posts
247
254
  ```
248
255
 
@@ -20,7 +20,7 @@ Configure the portal once. The policy and model conventions then carry tenancy a
20
20
 
21
21
  ## 🚨 Critical (applies to all three sub-pages)
22
22
 
23
- - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins triggers `verify_default_relation_scope_applied!`. Always call `default_relation_scope(relation)` explicitlynot `super`.
23
+ - **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins triggers `verify_default_relation_scope_applied!`. Make sure `default_relation_scope(relation)` is called somewhere in the chain explicitly here, or via `super` to a parent policy (e.g., a package base) that calls it.
24
24
  - **Always declare an association path from the model to the entity.** Direct `belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope. If `associated_with` can't resolve, fix the **model**, not the policy.
25
25
  - **Parent scoping beats entity scoping.** When a parent is present (nested resource), `default_relation_scope` scopes via the parent, not via `entity_scope`. Don't double-scope.
26
26
  - **One level of nesting only.** Grandparent → parent → child nested routes are NOT supported. Use top-level routes for deeper relationships.
@@ -36,19 +36,21 @@ rails generate pu:invites:install
36
36
  | `--entity-model=NAME` | `Entity` | Entity model name |
37
37
  | `--user-model=NAME` | `User` | User model name |
38
38
  | `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name (omit for single-flow apps) |
39
- | `--membership-model=NAME` | `EntityUser` | Membership join model |
40
- | `--roles` | `member,admin` | Comma-separated roles |
39
+ | `--membership-model=NAME` | `EntityUser` | Membership join model (must already exist) |
41
40
  | `--rodauth=NAME` | `user` | Rodauth configuration for signup |
42
41
  | `--enforce-domain` | `false` | Require invited email domain to match entity domain |
43
42
 
43
+ ::: info Roles come from the membership model
44
+ The role list is read from the membership model's `enum :role` — there is no `--roles=` flag on `pu:invites:install`. Set roles when generating the membership model (`pu:saas:membership --roles=...`) or edit its enum directly. **Index 0 is the most privileged** (typically `owner`, which the invite UI excludes from selectable choices); new invitees default to the second role.
45
+ :::
46
+
44
47
  Example with custom models:
45
48
 
46
49
  ```bash
47
50
  rails g pu:invites:install \
48
51
  --entity-model=Organization \
49
52
  --user-model=Customer \
50
- --membership-model=OrganizationMember \
51
- --roles=member,manager,admin
53
+ --membership-model=OrganizationMember
52
54
  ```
53
55
 
54
56
  After install:
@@ -317,10 +319,15 @@ Requires the invited email's domain to match the entity's domain.
317
319
 
318
320
  ### Custom roles
319
321
 
322
+ Roles are defined on the membership model, not on the invites generator. Set them at membership generation time (ordering matters — **index 0 is the most privileged**, typically `owner`):
323
+
320
324
  ```bash
321
- rails g pu:invites:install --roles=viewer,editor,admin,owner
325
+ rails g pu:saas:membership --user Customer --entity Organization --roles=admin,editor,viewer
326
+ # → enum :role, { owner: 0, admin: 1, editor: 2, viewer: 3 } (owner is auto-prepended)
322
327
  ```
323
328
 
329
+ Or edit `enum :role` on the existing membership model directly. Then run `pu:invites:install`.
330
+
324
331
  ### Custom expiration
325
332
 
326
333
  Override on the model:
@@ -8,8 +8,9 @@ The index page's table rendering. Override the `Table` nested class in your defi
8
8
  class PostDefinition < ResourceDefinition
9
9
  class Table < Table
10
10
  def view_template
11
- render_search_bar
12
- render_scopes_bar
11
+ render_toolbar # search + view toggle + filter buttons
12
+ render_scopes_pills # scope chips (if any scopes defined)
13
+ render_filter_pills # active-filter chips
13
14
 
14
15
  if collection.empty?
15
16
  render_empty_card
@@ -20,6 +21,7 @@ class PostDefinition < ResourceDefinition
20
21
  end
21
22
  end
22
23
 
24
+ render_bulk_actions_toolbar
23
25
  render_footer
24
26
  end
25
27
  end
@@ -30,8 +32,10 @@ end
30
32
 
31
33
  | Method | Purpose |
32
34
  |---|---|
33
- | `render_search_bar` | Toolbar search input |
34
- | `render_scopes_bar` | Quick-filter scope buttons |
35
+ | `render_toolbar` | Search input + view toggle + filter button |
36
+ | `render_scopes_pills` | Quick-filter scope chips (only renders if scopes defined) |
37
+ | `render_filter_pills` | Active-filter chips |
38
+ | `render_bulk_actions_toolbar` | Bulk action bar (only renders when rows selected) |
35
39
  | `render_table` | Default table rendering |
36
40
  | `render_empty_card` | Empty state |
37
41
  | `render_footer` | Pagination |