plutonium 0.50.0 → 0.51.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +85 -102
  3. data/.claude/skills/plutonium-app/SKILL.md +572 -0
  4. data/.claude/skills/plutonium-auth/SKILL.md +163 -300
  5. data/.claude/skills/plutonium-behavior/SKILL.md +838 -0
  6. data/.claude/skills/plutonium-resource/SKILL.md +1176 -0
  7. data/.claude/skills/plutonium-tenancy/SKILL.md +655 -0
  8. data/.claude/skills/plutonium-testing/SKILL.md +6 -5
  9. data/.claude/skills/plutonium-ui/SKILL.md +900 -0
  10. data/CHANGELOG.md +27 -2
  11. data/Rakefile +2 -1
  12. data/app/assets/plutonium.css +1 -11
  13. data/app/assets/plutonium.js +1009 -1214
  14. data/app/assets/plutonium.js.map +3 -3
  15. data/app/assets/plutonium.min.js +52 -51
  16. data/app/assets/plutonium.min.js.map +3 -3
  17. data/docs/.vitepress/config.ts +37 -27
  18. data/docs/getting-started/index.md +22 -29
  19. data/docs/getting-started/installation.md +37 -80
  20. data/docs/getting-started/tutorial/index.md +4 -5
  21. data/docs/guides/adding-resources.md +66 -377
  22. data/docs/guides/authentication.md +94 -463
  23. data/docs/guides/authorization.md +124 -370
  24. data/docs/guides/creating-packages.md +94 -296
  25. data/docs/guides/custom-actions.md +121 -441
  26. data/docs/guides/index.md +22 -42
  27. data/docs/guides/multi-tenancy.md +116 -187
  28. data/docs/guides/nested-resources.md +103 -431
  29. data/docs/guides/search-filtering.md +123 -240
  30. data/docs/guides/testing.md +5 -4
  31. data/docs/guides/theming.md +157 -407
  32. data/docs/guides/troubleshooting.md +5 -3
  33. data/docs/guides/user-invites.md +106 -425
  34. data/docs/guides/user-profile.md +76 -243
  35. data/docs/index.md +1 -1
  36. data/docs/reference/app/generators.md +517 -0
  37. data/docs/reference/app/index.md +158 -0
  38. data/docs/reference/app/packages.md +146 -0
  39. data/docs/reference/app/portals.md +377 -0
  40. data/docs/reference/auth/accounts.md +230 -0
  41. data/docs/reference/auth/index.md +88 -0
  42. data/docs/reference/auth/profile.md +185 -0
  43. data/docs/reference/behavior/controllers.md +395 -0
  44. data/docs/reference/behavior/index.md +22 -0
  45. data/docs/reference/behavior/interactions.md +341 -0
  46. data/docs/reference/behavior/policies.md +417 -0
  47. data/docs/reference/index.md +56 -49
  48. data/docs/reference/resource/actions.md +423 -0
  49. data/docs/reference/resource/definition.md +508 -0
  50. data/docs/reference/resource/index.md +50 -0
  51. data/docs/reference/resource/model.md +348 -0
  52. data/docs/reference/resource/query.md +305 -0
  53. data/docs/reference/tenancy/entity-scoping.md +361 -0
  54. data/docs/reference/tenancy/index.md +36 -0
  55. data/docs/reference/tenancy/invites.md +393 -0
  56. data/docs/reference/tenancy/nested-resources.md +267 -0
  57. data/docs/reference/testing/index.md +287 -0
  58. data/docs/reference/ui/assets.md +400 -0
  59. data/docs/reference/ui/components.md +165 -0
  60. data/docs/reference/ui/displays.md +104 -0
  61. data/docs/reference/ui/forms.md +284 -0
  62. data/docs/reference/ui/index.md +30 -0
  63. data/docs/reference/ui/layouts.md +106 -0
  64. data/docs/reference/ui/pages.md +189 -0
  65. data/docs/reference/ui/tables.md +117 -0
  66. data/docs/superpowers/specs/2026-05-09-typeahead-endpoint-design.md +203 -0
  67. data/docs/superpowers/specs/2026-05-12-skill-compaction-design.md +99 -0
  68. data/docs/superpowers/specs/2026-05-13-docs-restructure-design.md +186 -0
  69. data/gemfiles/rails_7.gemfile.lock +1 -1
  70. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  71. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  72. data/lib/generators/pu/core/update/update_generator.rb +0 -20
  73. data/lib/generators/pu/invites/install_generator.rb +1 -0
  74. data/lib/plutonium/definition/base.rb +1 -1
  75. data/lib/plutonium/definition/{views.rb → index_views.rb} +21 -20
  76. data/lib/plutonium/helpers/turbo_helper.rb +11 -0
  77. data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +14 -0
  78. data/lib/plutonium/resource/controller.rb +1 -0
  79. data/lib/plutonium/resource/controllers/crud_actions.rb +19 -1
  80. data/lib/plutonium/resource/controllers/typeahead.rb +180 -0
  81. data/lib/plutonium/resource/policy.rb +7 -0
  82. data/lib/plutonium/routing/mapper_extensions.rb +15 -0
  83. data/lib/plutonium/ui/component/methods.rb +4 -0
  84. data/lib/plutonium/ui/form/base.rb +6 -2
  85. data/lib/plutonium/ui/form/components/json.rb +58 -0
  86. data/lib/plutonium/ui/form/components/resource_select.rb +62 -8
  87. data/lib/plutonium/ui/form/components/secure_association.rb +98 -22
  88. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +83 -0
  89. data/lib/plutonium/ui/form/resource.rb +0 -4
  90. data/lib/plutonium/ui/grid/resource.rb +1 -1
  91. data/lib/plutonium/ui/layout/base.rb +1 -0
  92. data/lib/plutonium/ui/page/base.rb +0 -7
  93. data/lib/plutonium/ui/page/index.rb +4 -4
  94. data/lib/plutonium/ui/table/resource.rb +1 -1
  95. data/lib/plutonium/version.rb +1 -1
  96. data/lib/plutonium.rb +8 -0
  97. data/lib/tasks/release.rake +15 -1
  98. data/package.json +10 -10
  99. data/src/css/slim_select.css +4 -0
  100. data/src/js/controllers/slim_select_controller.js +61 -0
  101. data/src/js/turbo/turbo_actions.js +33 -0
  102. data/yarn.lock +553 -543
  103. metadata +44 -33
  104. data/.claude/skills/plutonium-assets/SKILL.md +0 -512
  105. data/.claude/skills/plutonium-controller/SKILL.md +0 -396
  106. data/.claude/skills/plutonium-create-resource/SKILL.md +0 -303
  107. data/.claude/skills/plutonium-definition/SKILL.md +0 -1223
  108. data/.claude/skills/plutonium-entity-scoping/SKILL.md +0 -317
  109. data/.claude/skills/plutonium-forms/SKILL.md +0 -465
  110. data/.claude/skills/plutonium-installation/SKILL.md +0 -331
  111. data/.claude/skills/plutonium-interaction/SKILL.md +0 -413
  112. data/.claude/skills/plutonium-invites/SKILL.md +0 -408
  113. data/.claude/skills/plutonium-model/SKILL.md +0 -440
  114. data/.claude/skills/plutonium-nested-resources/SKILL.md +0 -360
  115. data/.claude/skills/plutonium-package/SKILL.md +0 -198
  116. data/.claude/skills/plutonium-policy/SKILL.md +0 -456
  117. data/.claude/skills/plutonium-portal/SKILL.md +0 -410
  118. data/.claude/skills/plutonium-views/SKILL.md +0 -651
  119. data/docs/reference/assets/index.md +0 -496
  120. data/docs/reference/controller/index.md +0 -412
  121. data/docs/reference/definition/actions.md +0 -462
  122. data/docs/reference/definition/fields.md +0 -383
  123. data/docs/reference/definition/index.md +0 -326
  124. data/docs/reference/definition/query.md +0 -351
  125. data/docs/reference/generators/index.md +0 -648
  126. data/docs/reference/interaction/index.md +0 -449
  127. data/docs/reference/model/features.md +0 -248
  128. data/docs/reference/model/index.md +0 -218
  129. data/docs/reference/policy/index.md +0 -456
  130. data/docs/reference/portal/index.md +0 -379
  131. data/docs/reference/views/forms.md +0 -411
  132. data/docs/reference/views/index.md +0 -544
@@ -1,408 +0,0 @@
1
- ---
2
- name: plutonium-invites
3
- description: Use BEFORE setting up user invitations, pu:invites:install, or entity membership in a multi-tenant Plutonium app. Also load plutonium-entity-scoping.
4
- ---
5
-
6
- # Plutonium User Invites
7
-
8
- ## 🚨 Critical (read first)
9
- - **Use the generators.** `pu:invites:install` and `pu:invites:invitable` — never hand-write invite models, mailers, or controllers. Prerequisites: user model, entity model, membership model (`pu:saas:setup` creates all three).
10
- - **Invite email must match the accepting user's email.** This is a security feature. Don't disable `enforce_email?` unless you fully understand the implications.
11
- - **Entity scoping applies to invites** — invites are automatically filtered by the current entity. See `plutonium-entity-scoping`.
12
- - **Implement `on_invite_accepted` on invitable models.** Plutonium calls it when the invite is accepted; without it, the invitable never learns about the new user.
13
- - **Related skills:** `plutonium-entity-scoping` (tenant scoping for invites), `plutonium-auth` (Rodauth signup flow), `plutonium-portal` (portal connection), `plutonium-interaction` (custom invite logic).
14
-
15
- Plutonium provides a complete user invitation system for multi-tenant applications. The system handles:
16
- - Sending email invitations to new users
17
- - Token-based invite acceptance flow
18
- - Integration with Rodauth authentication
19
- - Entity membership creation on acceptance
20
- - Support for invitable models that get notified when invites are accepted
21
-
22
- ## Installation
23
-
24
- ### Prerequisites
25
-
26
- Before installing invites, ensure you have:
27
- 1. A user model with Rodauth authentication
28
- 2. An entity model (Organization, Company, Team, etc.)
29
- 3. A membership model linking users to entities
30
-
31
- Use `pu:saas:setup` to generate all three:
32
-
33
- ```bash
34
- rails g pu:saas:setup --user Customer --entity Organization
35
- ```
36
-
37
- ### Install the Invites Package
38
-
39
- ```bash
40
- rails generate pu:invites:install
41
- ```
42
-
43
- **Options:**
44
-
45
- | Option | Default | Description |
46
- |--------|---------|-------------|
47
- | `--entity-model=NAME` | Entity | Entity model name for scoping |
48
- | `--user-model=NAME` | User | User model name |
49
- | `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name. Omit for single-flow apps; set per-invocation when running the generator more than once. |
50
- | `--membership-model=NAME` | EntityUser | Membership join model |
51
- | `--roles=ROLES` | member,admin | Comma-separated roles |
52
- | `--rodauth=NAME` | user | Rodauth configuration for signup |
53
- | `--enforce-domain` | false | Require email domain to match entity |
54
-
55
- **Example with custom models:**
56
-
57
- ```bash
58
- rails g pu:invites:install \
59
- --entity-model=Organization \
60
- --user-model=Customer \
61
- --membership-model=OrganizationMember \
62
- --roles=member,manager,admin
63
- ```
64
-
65
- ### What Gets Created
66
-
67
- ```
68
- packages/invites/
69
- ├── app/
70
- │ ├── controllers/invites/
71
- │ │ ├── user_invitations_controller.rb
72
- │ │ └── welcome_controller.rb
73
- │ ├── definitions/invites/
74
- │ │ └── user_invite_definition.rb
75
- │ ├── interactions/invites/
76
- │ │ ├── cancel_invite_interaction.rb
77
- │ │ └── resend_invite_interaction.rb
78
- │ ├── mailers/invites/
79
- │ │ └── user_invite_mailer.rb
80
- │ ├── models/invites/
81
- │ │ └── user_invite.rb
82
- │ ├── policies/invites/
83
- │ │ └── user_invite_policy.rb
84
- │ └── views/invites/
85
- │ ├── user_invitations/
86
- │ │ ├── error.html.erb
87
- │ │ ├── landing.html.erb
88
- │ │ ├── show.html.erb
89
- │ │ └── signup.html.erb
90
- │ ├── user_invite_mailer/
91
- │ │ ├── invitation.html.erb
92
- │ │ └── invitation.text.erb
93
- │ └── welcome/
94
- │ └── pending_invitation.html.erb
95
-
96
- app/interactions/
97
- ├── entity/
98
- │ └── invite_user_interaction.rb
99
- └── user/
100
- └── invite_user_interaction.rb
101
-
102
- db/migrate/
103
- └── TIMESTAMP_create_user_invites.rb
104
- ```
105
-
106
- ### Routes Added
107
-
108
- ```ruby
109
- # Public invitation routes (unauthenticated)
110
- get "welcome", to: "invites/welcome#index"
111
- get "invitations/:token", to: "invites/user_invitations#show"
112
- post "invitations/:token/accept", to: "invites/user_invitations#accept"
113
- get "invitations/:token/signup", to: "invites/user_invitations#signup"
114
- post "invitations/:token/signup", to: "invites/user_invitations#signup"
115
- ```
116
-
117
- ## Multiple invite flows in one app
118
-
119
- A single app can run several independent invite flows side-by-side — for example, one for inviting customers to organizations and another for inviting funders to projects. Run `pu:invites:install` once per flow.
120
-
121
- **Default derivation rule.** When `--invite_model` is omitted, the generator derives the class name as `<EntityModel><UserModel>Invite`. So with the defaults (`--entity_model=Organization --user_model=User`) the generated class is `Invites::OrganizationUserInvite` — there is no literal `UserInvite` default. Single-flow apps don't need to pass `--invite_model` at all.
122
-
123
- Multi-flow apps typically vary `--entity_model` / `--user_model` per invocation; the derived names diverge automatically, so `--invite_model` is only needed when you want a custom class name.
124
-
125
- ```bash
126
- rails g pu:invites:install \
127
- --entity_model=FunderOrganization \
128
- --user_model=SpenderAccount \
129
- --invite_model=FunderInvite
130
-
131
- rails g pu:invites:install \
132
- --entity_model=Project \
133
- --user_model=Member \
134
- --invite_model=ProjectInvite
135
- ```
136
-
137
- Each invocation creates an independent flow: model `Invites::FunderInvite` on `funder_invites`, controller `Invites::FunderInvitationsController` on `/funder_invitations/:token`, helper `funder_invitation_path`, etc. The shared `Invites::WelcomeController` accumulates each new class into its `invite_classes` array, so `pending_invite` checks all flows in priority order (first-match wins).
138
-
139
- Override hooks at the model level:
140
- - `def user_attribute; :spender_account; end` — when `belongs_to :spender_account` instead of `:user`.
141
- - `def invite_entity_attribute; :funder_organization; end` — when `belongs_to :funder_organization` instead of `:entity`.
142
-
143
- Override hooks at the controller level (auto-generated by the install generator, shown here so you understand what it emits and can tweak it):
144
-
145
- ```ruby
146
- # packages/invites/app/controllers/invites/welcome_controller.rb
147
- def invite_classes
148
- [::Invites::FunderInvite, ::Invites::ProjectInvite]
149
- end
150
-
151
- # packages/invites/app/controllers/invites/funder_invitations_controller.rb
152
- def invitation_path_for(token)
153
- funder_invitation_path(token: token)
154
- end
155
- ```
156
-
157
- ## Connecting Invitables
158
-
159
- Invitables are models that trigger invitations and get notified when they're accepted. Common examples:
160
- - `Tenant` - A tenant record that needs a user assigned
161
- - `TeamMember` - A membership record created by admin, waiting for user signup
162
- - `ProjectCollaborator` - A project role waiting for user acceptance
163
-
164
- ### Generate an Invitable
165
-
166
- ```bash
167
- rails generate pu:invites:invitable Tenant
168
- rails generate pu:invites:invitable TeamMember --role=member
169
- rails generate pu:invites:invitable Tenant --dest=my_package
170
- ```
171
-
172
- **Options:**
173
-
174
- | Option | Default | Description |
175
- |--------|---------|-------------|
176
- | `--role=ROLE` | member | Role to assign to invited users |
177
- | `--user-model=NAME` | User | User model name |
178
- | `--membership-model=NAME` | EntityUser | Membership model |
179
- | `--dest=PACKAGE` | main_app | Destination package |
180
- | `--[no-]email-templates` | true | Generate custom email templates |
181
-
182
- ### Implement the Callback
183
-
184
- After generation, implement `on_invite_accepted` in your invitable model:
185
-
186
- ```ruby
187
- # app/models/tenant.rb
188
- class Tenant < ApplicationRecord
189
- include Plutonium::Invites::Concerns::Invitable
190
-
191
- belongs_to :entity
192
- belongs_to :user, optional: true
193
-
194
- def on_invite_accepted(user)
195
- update!(user: user, status: :active)
196
- end
197
- end
198
- ```
199
-
200
- ## How the Flow Works
201
-
202
- ### 1. Admin Sends Invite
203
-
204
- An admin uses the "Invite User" action on an entity or invitable:
205
-
206
- ```ruby
207
- # From entity context
208
- entity.invite_user(email: "user@example.com", role: :member)
209
-
210
- # From invitable context (e.g., Tenant)
211
- tenant.invite_user(email: "user@example.com")
212
- ```
213
-
214
- ### 2. Email Sent
215
-
216
- The system sends an email with a secure invitation link:
217
-
218
- ```
219
- Subject: You've been invited to join Acme Corp
220
-
221
- Click here to accept: https://app.example.com/invitations/abc123...
222
- ```
223
-
224
- ### 3. User Accepts Invite
225
-
226
- **Existing User Flow:**
227
- 1. User clicks invite link
228
- 2. User logs in (or is already logged in)
229
- 3. System validates email matches
230
- 4. Membership created, invitable notified
231
-
232
- **New User Flow:**
233
- 1. User clicks invite link
234
- 2. User clicks "Create Account"
235
- 3. User signs up with the invited email
236
- 4. System validates email matches
237
- 5. Membership created, invitable notified
238
-
239
- ### 4. Pending Invite Check
240
-
241
- After login, users are redirected to `/welcome` where pending invites are shown:
242
-
243
- ```ruby
244
- # In your controller
245
- include Plutonium::Invites::PendingInviteCheck
246
-
247
- # Automatically shows pending invites after login
248
- ```
249
-
250
- ## UserInvite Model
251
-
252
- The generated `Invites::UserInvite` model includes:
253
-
254
- ```ruby
255
- class Invites::UserInvite < Invites::ResourceRecord
256
- include Plutonium::Invites::Concerns::InviteToken
257
-
258
- # Associations
259
- belongs_to :entity
260
- belongs_to :invited_by, polymorphic: true
261
- belongs_to :user, optional: true
262
- belongs_to :invitable, polymorphic: true, optional: true
263
-
264
- # States: pending, accepted, expired, cancelled
265
- enum :state, pending: 0, accepted: 1, expired: 2, cancelled: 3
266
-
267
- # Roles
268
- enum :role, member: 0, admin: 1
269
- end
270
- ```
271
-
272
- ### Key Methods
273
-
274
- ```ruby
275
- # Find valid invite for acceptance
276
- invite = Invites::UserInvite.find_for_acceptance(token)
277
-
278
- # Accept for a user
279
- invite.accept_for_user!(current_user)
280
-
281
- # Resend invitation email
282
- invite.resend!
283
-
284
- # Cancel invitation
285
- invite.cancel!
286
- ```
287
-
288
- ## Customization
289
-
290
- ### Custom Email Templates
291
-
292
- Override templates in your package:
293
-
294
- ```erb
295
- <%# packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb %>
296
- <h1>Welcome to <%= @invite.entity.name %>!</h1>
297
- <p><%= @invite.invited_by.email %> has invited you to join.</p>
298
- <p><%= link_to "Accept Invitation", @invitation_url %></p>
299
- ```
300
-
301
- ### Custom Validation
302
-
303
- Extend the invite model:
304
-
305
- ```ruby
306
- # packages/invites/app/models/invites/user_invite.rb
307
- class Invites::UserInvite < Invites::ResourceRecord
308
- validate :email_not_already_member
309
-
310
- private
311
-
312
- def email_not_already_member
313
- existing = membership_model.joins(:user)
314
- .where(entity: entity, users: { email: email })
315
- .exists?
316
-
317
- errors.add(:email, "is already a member") if existing
318
- end
319
- end
320
- ```
321
-
322
- ### Domain Enforcement
323
-
324
- Enable domain matching in the install:
325
-
326
- ```bash
327
- rails g pu:invites:install --enforce-domain
328
- ```
329
-
330
- This requires the invited email domain to match the entity's domain.
331
-
332
- ### Custom Roles
333
-
334
- Specify roles during install:
335
-
336
- ```bash
337
- rails g pu:invites:install --roles=viewer,editor,admin,owner
338
- ```
339
-
340
- ## Integration with Portals
341
-
342
- ### Connect Invites to Your Portal
343
-
344
- ```ruby
345
- # packages/customer_portal/lib/engine.rb
346
- module CustomerPortal
347
- class Engine < Rails::Engine
348
- include Plutonium::Portal::Engine
349
-
350
- # Register the invites package for this portal
351
- register_package Invites::Engine
352
- end
353
- end
354
- ```
355
-
356
- ### Entity-Scoped Invite Management
357
-
358
- Invites are automatically filtered by the current entity — admins only see invites for their organization. This works because `Invites::UserInvite` has `belongs_to :entity`, which `associated_with` picks up.
359
-
360
- > **For how entity scoping works end-to-end (model shapes, `default_relation_scope`, portal strategies), see the [plutonium-entity-scoping](../plutonium-entity-scoping/SKILL.md) skill. It is the single source of truth.**
361
-
362
- ## Troubleshooting
363
-
364
- ### Invite Not Found
365
-
366
- - Check the token hasn't expired (default: 1 week)
367
- - Verify the invite hasn't been cancelled
368
- - Ensure the invite is still in `pending` state
369
-
370
- ### Email Mismatch Error
371
-
372
- The system requires the accepting user's email to match the invited email:
373
-
374
- ```
375
- "This invitation is for user@example.com. You must use an account with that email address."
376
- ```
377
-
378
- To allow any email (not recommended for security):
379
-
380
- ```ruby
381
- # In your UserInvite model
382
- def enforce_email?
383
- false
384
- end
385
- ```
386
-
387
- ### Rodauth Integration Issues
388
-
389
- Ensure the Rodauth plugin is configured:
390
-
391
- ```ruby
392
- # app/rodauth/user_rodauth_plugin.rb
393
- configure do
394
- login_return_to_requested_location? true
395
- login_redirect "/welcome"
396
-
397
- after_login do
398
- session[:after_welcome_redirect] = session.delete(:login_redirect)
399
- end
400
- end
401
- ```
402
-
403
- ## Related Skills
404
-
405
- - `plutonium-auth` - Authentication setup
406
- - `plutonium-interaction` - Custom business logic
407
- - `plutonium-portal` - Portal configuration
408
- - `plutonium-policy` - Authorization for invite actions