plutonium 0.51.0 → 0.53.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-app/SKILL.md +2 -0
- data/.claude/skills/plutonium-auth/SKILL.md +6 -4
- data/.claude/skills/plutonium-behavior/SKILL.md +1 -1
- data/.claude/skills/plutonium-resource/SKILL.md +6 -4
- data/.claude/skills/plutonium-tenancy/SKILL.md +31 -7
- data/.claude/skills/plutonium-testing/SKILL.md +3 -1
- data/.claude/skills/plutonium-ui/SKILL.md +32 -8
- data/CHANGELOG.md +33 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +258 -11
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +39 -39
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_resource_header.html.erb +2 -1
- data/docs/.vitepress/config.ts +2 -2
- data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +53 -0
- data/docs/.vitepress/theme/components/HomeCta.vue +108 -0
- data/docs/.vitepress/theme/components/HomeHero.vue +70 -0
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +74 -0
- data/docs/.vitepress/theme/components/HomePillars.vue +42 -0
- data/docs/.vitepress/theme/components/HomeStopWriting.vue +49 -0
- data/docs/.vitepress/theme/components/HomeWalkthrough.vue +111 -0
- data/docs/.vitepress/theme/components/SectionLanding.vue +115 -0
- data/docs/.vitepress/theme/custom.css +144 -0
- data/docs/.vitepress/theme/index.ts +58 -1
- data/docs/getting-started/index.md +33 -50
- data/docs/getting-started/tutorial/02-first-resource.md +17 -8
- data/docs/getting-started/tutorial/03-authentication.md +31 -23
- data/docs/getting-started/tutorial/05-custom-actions.md +9 -4
- data/docs/getting-started/tutorial/06-nested-resources.md +7 -1
- data/docs/getting-started/tutorial/07-author-portal.md +8 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +4 -0
- data/docs/guides/authentication.md +11 -6
- data/docs/guides/authorization.md +3 -3
- data/docs/guides/creating-packages.md +8 -11
- data/docs/guides/custom-actions.md +8 -2
- data/docs/guides/customizing-ui.md +259 -0
- data/docs/guides/index.md +49 -32
- data/docs/guides/multi-tenancy.md +14 -6
- data/docs/guides/nested-resources.md +69 -0
- data/docs/guides/search-filtering.md +6 -0
- data/docs/guides/testing.md +5 -1
- data/docs/guides/theming.md +14 -1
- data/docs/guides/user-invites.md +10 -4
- data/docs/guides/user-profile.md +8 -0
- data/docs/index.md +10 -219
- data/docs/public/asciinema/home-scaffold.cast +305 -0
- data/docs/public/images/components/avatar.png +0 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/reference/app/generators.md +4 -4
- data/docs/reference/auth/accounts.md +7 -8
- data/docs/reference/auth/index.md +1 -1
- data/docs/reference/behavior/policies.md +2 -2
- data/docs/reference/configuration.md +61 -0
- data/docs/reference/index.md +67 -55
- data/docs/reference/resource/actions.md +2 -1
- data/docs/reference/resource/definition.md +5 -4
- data/docs/reference/tenancy/entity-scoping.md +14 -8
- data/docs/reference/tenancy/index.md +1 -1
- data/docs/reference/tenancy/invites.md +12 -5
- data/docs/reference/ui/components.md +53 -0
- data/docs/reference/ui/forms.md +1 -1
- data/docs/reference/ui/pages.md +6 -5
- data/docs/reference/ui/tables.md +8 -4
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md +1648 -0
- data/docs/superpowers/plans/2026-05-15-public-pages-overhaul.md.tasks.json +109 -0
- data/docs/superpowers/specs/2026-05-15-public-pages-overhaul-design.md +263 -0
- data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +10 -0
- data/lib/generators/pu/invites/install_generator.rb +44 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +1 -0
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
- data/lib/generators/pu/profile/conn_generator.rb +2 -2
- data/lib/generators/pu/res/conn/conn_generator.rb +33 -6
- data/lib/generators/pu/res/model/templates/model.rb.tt +4 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -1
- data/lib/generators/pu/rodauth/admin_generator.rb +0 -2
- data/lib/generators/pu/rodauth/migration_generator.rb +0 -2
- data/lib/generators/pu/rodauth/views_generator.rb +0 -2
- data/lib/generators/pu/saas/membership/USAGE +4 -1
- data/lib/generators/pu/saas/setup_generator.rb +16 -4
- data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +1 -1
- data/lib/plutonium/action/base.rb +43 -63
- data/lib/plutonium/configuration.rb +7 -0
- data/lib/plutonium/definition/actions.rb +10 -11
- data/lib/plutonium/definition/base.rb +29 -0
- data/lib/plutonium/helpers/assets_helper.rb +0 -30
- data/lib/plutonium/helpers/content_helper.rb +0 -44
- data/lib/plutonium/helpers/display_helper.rb +0 -62
- data/lib/plutonium/helpers/turbo_helper.rb +17 -2
- data/lib/plutonium/helpers.rb +0 -2
- data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/resource/definition.rb +0 -42
- data/lib/plutonium/ui/action_button.rb +4 -3
- data/lib/plutonium/ui/avatar.rb +182 -0
- data/lib/plutonium/ui/component/kit.rb +2 -0
- data/lib/plutonium/ui/component/methods.rb +1 -0
- data/lib/plutonium/ui/form/base.rb +32 -2
- data/lib/plutonium/ui/form/components/secure_association.rb +14 -8
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/resource.rb +58 -0
- data/lib/plutonium/ui/form/theme.rb +8 -4
- data/lib/plutonium/ui/grid/card.rb +10 -26
- data/lib/plutonium/ui/modal/base.rb +36 -1
- data/lib/plutonium/ui/modal/centered.rb +24 -6
- data/lib/plutonium/ui/modal/slideover.rb +26 -11
- data/lib/plutonium/ui/nav_user.rb +3 -23
- data/lib/plutonium/ui/page/edit.rb +7 -4
- data/lib/plutonium/ui/page/interactive_action.rb +5 -3
- data/lib/plutonium/ui/page/new.rb +7 -4
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
- data/lib/plutonium/ui/table/components/filter_form.rb +12 -4
- data/lib/plutonium/version.rb +1 -1
- data/package.json +4 -1
- data/src/css/components.css +38 -1
- data/src/css/slim_select.css +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +165 -0
- data/src/js/controllers/form_controller.js +5 -4
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/remote_modal_controller.js +53 -19
- data/src/js/turbo/index.js +1 -0
- data/src/js/turbo/turbo_confirm.js +128 -0
- data/yarn.lock +108 -1
- metadata +52 -6
- data/lib/plutonium/helpers/attachment_helper.rb +0 -73
- data/lib/plutonium/helpers/table_helper.rb +0 -35
- /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1575a79af57aa89f49041cbdb1e31364cea08fb3d541398e095772470a3da6b8
|
|
4
|
+
data.tar.gz: a3d4dfebbec5836c9557c903393f7deebcfb17b94ca8d61ccb5a1dd1ffbd7aee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c6954af1d7e64be809c28e06140b5660f58e94ec254fe2960c4d0b28b615f515df84d107528899d791ab3cc5bf38ac18cf623aa4c00d3a683bf7b963b96bc47
|
|
7
|
+
data.tar.gz: a5f6a8ea0ad4e9fafc6559aac4b488c131a6c796c58506a49a49e8088dd3408cd04641ab4ffef357b66a373f88bd6c99476eb2d1cd7cb79eaaf2c993c6322cf5
|
|
@@ -505,6 +505,8 @@ register_resource ::Post
|
|
|
505
505
|
register_resource ::Profile, singular: true # if --singular
|
|
506
506
|
```
|
|
507
507
|
|
|
508
|
+
Re-running `pu:res:conn` for the same resource is **idempotent** — already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
|
|
509
|
+
|
|
508
510
|
### Generated controller
|
|
509
511
|
|
|
510
512
|
```ruby
|
|
@@ -45,7 +45,6 @@ rails generate pu:rodauth:account user [options]
|
|
|
45
45
|
|---|---|
|
|
46
46
|
| `--defaults` | Enables login, logout, remember, password reset |
|
|
47
47
|
| `--kitchen_sink` | Enables ALL features |
|
|
48
|
-
| `--primary` | Mark as primary account (no URL prefix) |
|
|
49
48
|
| `--no-mails` | Skip mailer setup |
|
|
50
49
|
| `--argon2` | Use Argon2 instead of bcrypt |
|
|
51
50
|
| `--api_only` | JSON API only (no sessions) |
|
|
@@ -90,12 +89,15 @@ rails generate pu:rodauth:admin admin --extra-attributes=name:string,department:
|
|
|
90
89
|
enum :role, super_admin: 0, admin: 1
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
Rake task for direct admin creation:
|
|
92
|
+
Rake task for direct admin creation (namespace is `rodauth`, task name is the account name):
|
|
94
93
|
|
|
95
94
|
```bash
|
|
96
|
-
|
|
95
|
+
EMAIL=admin@example.com rails rodauth:admin
|
|
96
|
+
# (run without EMAIL to be prompted)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
Creates the account and sends a verification email; the admin sets their own password through the flow. No password is passed on the command line.
|
|
100
|
+
|
|
99
101
|
### SaaS setup — `pu:saas:setup` (meta-generator)
|
|
100
102
|
|
|
101
103
|
Creates the User + Entity + Membership trio AND runs:
|
|
@@ -109,7 +111,7 @@ Don't generate another entity portal after this. Pass `--force` to re-run.
|
|
|
109
111
|
|
|
110
112
|
```bash
|
|
111
113
|
rails g pu:saas:setup --user Customer --entity Organization
|
|
112
|
-
rails g pu:saas:setup --user Customer --entity Organization --roles=member
|
|
114
|
+
rails g pu:saas:setup --user Customer --entity Organization --roles=admin,member
|
|
113
115
|
rails g pu:saas:setup --user Customer --entity Organization --no-allow-signup
|
|
114
116
|
rails g pu:saas:setup --user Customer --entity Organization \
|
|
115
117
|
--user-attributes=name:string --entity-attributes=slug:string
|
|
@@ -18,7 +18,7 @@ For tenant-scoped `relation_scope` and entity scoping, load [[plutonium-tenancy]
|
|
|
18
18
|
- **`ActiveRecord::RecordInvalid` is NOT rescued automatically in interactions.** Always rescue when using `create!` / `update!` / `save!`, return `failed(e.record.errors)`.
|
|
19
19
|
- **Return `succeed(...)` or `failed(...)`** from `execute` — the controller can't tell what happened otherwise.
|
|
20
20
|
- **Redirect is automatic on success** — only use `with_redirect_response` for a *different* destination.
|
|
21
|
-
- **`relation_scope` must
|
|
21
|
+
- **`relation_scope` must end up calling `default_relation_scope(relation)` somewhere in the chain.** Prefer calling it explicitly. `super` works when extending a parent policy (e.g., a package base) that itself calls it. See [[plutonium-tenancy]].
|
|
22
22
|
- **For `has_cents` fields, use the virtual name (`:price`), not `:price_cents`** in `permitted_attributes_for_*`.
|
|
23
23
|
- **Custom action ⇒ policy method.** `action :publish` needs `def publish?` on the policy (undefined methods return `false`).
|
|
24
24
|
- **Named custom routes.** When adding custom routes, always pass `as:` so `resource_url_for` can build URLs.
|
|
@@ -725,9 +725,10 @@ class PostDefinition < ResourceDefinition
|
|
|
725
725
|
# nil (default) = auto (hidden for singular, shown for plural)
|
|
726
726
|
submit_and_continue false
|
|
727
727
|
|
|
728
|
-
# How :new / :edit render
|
|
728
|
+
# How :new / :edit + interactive actions render
|
|
729
729
|
# :slideover (default), :centered, or false (full pages)
|
|
730
|
-
|
|
730
|
+
# size: :sm / :md (default) / :lg / :xl / :auto / :full
|
|
731
|
+
modal :centered, size: :lg
|
|
731
732
|
|
|
732
733
|
# Titles
|
|
733
734
|
index_page_title "All Posts"
|
|
@@ -757,7 +758,7 @@ class PostDefinition < ResourceDefinition
|
|
|
757
758
|
end
|
|
758
759
|
```
|
|
759
760
|
|
|
760
|
-
`modal:`
|
|
761
|
+
`modal:` is the default for framework `:new` / `:edit` *and* every interactive action on this definition. Per-action `modal:` / `size:` overrides win.
|
|
761
762
|
|
|
762
763
|
## Metadata Panel (show page)
|
|
763
764
|
|
|
@@ -983,7 +984,8 @@ action :name,
|
|
|
983
984
|
confirmation: "Are you sure?",
|
|
984
985
|
turbo_frame: "_top",
|
|
985
986
|
route_options: {action: :foo},
|
|
986
|
-
modal: :slideover
|
|
987
|
+
modal: :slideover, # :slideover / :centered — overrides definition's modal mode
|
|
988
|
+
size: :lg # :sm / :md / :lg / :xl / :auto / :full — overrides definition's modal size
|
|
987
989
|
```
|
|
988
990
|
|
|
989
991
|
`Action#with(...)` — actions are frozen value objects; clone with overrides:
|
|
@@ -15,7 +15,7 @@ Cross-references back to [[plutonium-resource]] (models, definitions) and [[plut
|
|
|
15
15
|
|
|
16
16
|
## 🚨 Critical (read first)
|
|
17
17
|
|
|
18
|
-
- **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins to the entity triggers `verify_default_relation_scope_applied!`.
|
|
18
|
+
- **Never bypass `default_relation_scope`.** Overriding `relation_scope` with `where(organization: ...)` or manual joins to the entity triggers `verify_default_relation_scope_applied!`. Make sure the chain ends up calling `default_relation_scope(relation)` — explicitly, or via `super(relation)` (the framework base calls it).
|
|
19
19
|
- **Always declare an association path from model to entity.** Direct `belongs_to`, `has_one :through`, or a custom `associated_with_<entity>` scope. If `associated_with` can't resolve, Plutonium raises. Fix the **model**, not the policy.
|
|
20
20
|
- **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.
|
|
21
21
|
- **One level of nesting only.** Grandparent → parent → child nested routes are NOT supported. Use top-level routes for deeper relationships.
|
|
@@ -170,7 +170,12 @@ relation_scope { |r| r.joins(:project).where(projects: {organization_id: current
|
|
|
170
170
|
relation_scope { |r| r.where(published: true) }
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
**`default_relation_scope(relation)` must end up being called somewhere in the chain** — runtime verification just checks it was hit, not that you wrote it in this class. Both work:
|
|
174
|
+
|
|
175
|
+
- `default_relation_scope(relation).where(...)` — explicit, always safe
|
|
176
|
+
- `super(relation).where(...)` — `Plutonium::Resource::Policy`'s `relation_scope` block calls `default_relation_scope`, so chaining through `super` picks it up
|
|
177
|
+
|
|
178
|
+
Pick the one that reads better for the situation.
|
|
174
179
|
|
|
175
180
|
### Intentionally skipping
|
|
176
181
|
|
|
@@ -199,7 +204,7 @@ module AdminPortal
|
|
|
199
204
|
end
|
|
200
205
|
```
|
|
201
206
|
|
|
202
|
-
Routes become
|
|
207
|
+
Routes become `/<mount>/:organization_scoped/posts` (resolving to `/<mount>/42/posts` at request time — the entity id is the first path segment after the mount). Portal extracts `params[:organization_scoped]` and loads the entity automatically. The `_scoped` suffix on the param name avoids colliding with `params[:organization]` from a `belongs_to :organization` on child models.
|
|
203
208
|
|
|
204
209
|
### Custom strategy (subdomain, session, etc.)
|
|
205
210
|
|
|
@@ -235,6 +240,7 @@ entity_scope
|
|
|
235
240
|
|
|
236
241
|
- **Multiple associations to the same entity class.** E.g. `Match belongs_to :home_team, :away_team` both pointing at `Team`. Plutonium raises — override `scoped_entity_association` on the controller to pick one (`def scoped_entity_association = :home_team`).
|
|
237
242
|
- **`param_key` differs from association name.** Fine — Plutonium matches by **class**, not param key. `scope_to_entity Competition::Team, param_key: :team` works with `belongs_to :competition_team`.
|
|
243
|
+
- **Default `param_key` includes `_scoped` suffix.** `scope_to_entity Organization` reads `params[:organization_scoped]` (not `params[:organization]`) so it doesn't collide with `params[:organization]` from a `belongs_to :organization` on child models. The URL itself is unchanged — the entity id is just the first path segment after the mount (`/<mount>/42/posts`). Pass `param_key:` only if you want a different param name in your controllers.
|
|
238
244
|
- **Forgetting compound uniqueness.** `validates :code, uniqueness: true` leaks across tenants. Use `uniqueness: {scope: :organization_id}`.
|
|
239
245
|
- **"Temporary" `where` bypass for debugging.** Use `skip_default_relation_scope!` explicitly. Never leave a `where` bypass in code.
|
|
240
246
|
|
|
@@ -421,10 +427,18 @@ rails generate pu:invites:install
|
|
|
421
427
|
| `--entity-model=NAME` | `Entity` | Entity model name |
|
|
422
428
|
| `--user-model=NAME` | `User` | User model name |
|
|
423
429
|
| `--invite-model=NAME` | `<EntityModel><UserModel>Invite` | Invite class name (omit for single-flow apps) |
|
|
424
|
-
| `--membership-model=NAME` | `EntityUser` | Membership join model |
|
|
425
|
-
| `--roles=ROLES` | `member,admin` | Comma-separated |
|
|
430
|
+
| `--membership-model=NAME` | `EntityUser` | Membership join model (must already exist; roles are read from its `enum :role`) |
|
|
426
431
|
| `--rodauth=NAME` | `user` | Rodauth configuration for signup |
|
|
427
432
|
| `--enforce-domain` | `false` | Require invited email domain to match entity |
|
|
433
|
+
| `--dest=PACKAGE` | `main_app` | Package where the entity model lives (controls where `invite_user_interaction.rb` is generated) |
|
|
434
|
+
|
|
435
|
+
::: 🚨 No `--roles` flag here
|
|
436
|
+
Role list is derived from the membership model's `enum :role`. Set roles via `pu:saas:membership --roles=...` (or edit the 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 (`roles[1]`).
|
|
437
|
+
:::
|
|
438
|
+
|
|
439
|
+
::: 🚨 ActiveRecord encryption keys required
|
|
440
|
+
The invite model uses `encrypts :token, deterministic: true`. Without configured AR encryption keys, creating or accepting an invite raises `ActiveRecord::Encryption::Errors::Configuration`. The generator detects this and warns at install time — generate keys with `bin/rails db:encryption:init`, then paste the printed `active_record_encryption:` block into `config/credentials.yml.enc` (or set the equivalent `ACTIVE_RECORD_ENCRYPTION_*` ENV vars in production).
|
|
441
|
+
:::
|
|
428
442
|
|
|
429
443
|
### What gets created
|
|
430
444
|
|
|
@@ -619,13 +633,23 @@ class Invites::UserInvite < Invites::ResourceRecord
|
|
|
619
633
|
end
|
|
620
634
|
```
|
|
621
635
|
|
|
622
|
-
### Domain enforcement
|
|
636
|
+
### Domain enforcement
|
|
623
637
|
|
|
624
638
|
```bash
|
|
625
639
|
rails g pu:invites:install --enforce-domain
|
|
626
|
-
rails g pu:invites:install --roles=viewer,editor,admin,owner
|
|
627
640
|
```
|
|
628
641
|
|
|
642
|
+
### Custom roles
|
|
643
|
+
|
|
644
|
+
Set roles when generating the membership model (ordering: index 0 = most privileged):
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
rails g pu:saas:membership --user Customer --entity Organization --roles=admin,editor,viewer
|
|
648
|
+
# → enum :role, { owner: 0, admin: 1, editor: 2, viewer: 3 } (owner auto-prepended)
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Or edit `enum :role` on the existing membership model directly. Then run `pu:invites:install`.
|
|
652
|
+
|
|
629
653
|
## Portal connection
|
|
630
654
|
|
|
631
655
|
```ruby
|
|
@@ -207,7 +207,9 @@ current_account(portal: :admin)
|
|
|
207
207
|
with_portal(:org) { ... } # scoped portal switch
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
**
|
|
210
|
+
**Default Rodauth login expects `password: "password123"`** — `login_as` POSTs to `/<account_table>/login` with that hardcoded password. Either seed test accounts with it (fixtures/factories) or override via `sign_in_for_tests` below.
|
|
211
|
+
|
|
212
|
+
**Override hook for non-Rodauth apps (or to bypass Rodauth in tests):** define `sign_in_for_tests(account, portal:)` in your test class (or in `test/support/plutonium_testing.rb` for project-wide use). `AuthHelpers` will defer to it.
|
|
211
213
|
|
|
212
214
|
```ruby
|
|
213
215
|
def sign_in_for_tests(account, portal:)
|
|
@@ -351,8 +351,8 @@ end
|
|
|
351
351
|
class PostDefinition < ResourceDefinition
|
|
352
352
|
class Table < Table
|
|
353
353
|
def view_template
|
|
354
|
-
|
|
355
|
-
|
|
354
|
+
render_toolbar
|
|
355
|
+
render_scopes_pills
|
|
356
356
|
|
|
357
357
|
if collection.empty?
|
|
358
358
|
render_empty_card
|
|
@@ -371,7 +371,7 @@ end
|
|
|
371
371
|
|
|
372
372
|
| Method | Purpose |
|
|
373
373
|
|---|---|
|
|
374
|
-
| `
|
|
374
|
+
| `render_toolbar`, `render_scopes_pills`, `render_filter_pills`, `render_bulk_actions_toolbar` | Toolbar pieces |
|
|
375
375
|
| `render_table` | Default table |
|
|
376
376
|
| `render_empty_card` | Empty state |
|
|
377
377
|
| `render_footer` | Pagination |
|
|
@@ -390,6 +390,7 @@ Inside any `Plutonium::UI::Component::Base` (or any page/form/display):
|
|
|
390
390
|
PageHeader(title: "Dashboard", description: "...", actions: [...])
|
|
391
391
|
Panel(class: "mt-4") { p { "Content" } }
|
|
392
392
|
Block { TabList(items: tabs) }
|
|
393
|
+
Avatar(user) # profile image: src → Navii fallback → icon
|
|
393
394
|
EmptyCard("No items found")
|
|
394
395
|
ActionButton(action, url: "/posts/new")
|
|
395
396
|
DynaFrameHost(src: "/some/path", loading: :lazy)
|
|
@@ -401,6 +402,28 @@ TablePagination(pagy)
|
|
|
401
402
|
Breadcrumbs()
|
|
402
403
|
```
|
|
403
404
|
|
|
405
|
+
## Avatar
|
|
406
|
+
|
|
407
|
+
`Avatar(subject = nil, src: nil, size: :md, alt: nil, **attrs)` — profile image with a deterministic [Navii](https://navii.dev) fallback. Registered in the kit.
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
Avatar(user) # Navii fallback seeded from the record
|
|
411
|
+
Avatar(user, src: :avatar) # user.avatar if present, else Navii fallback
|
|
412
|
+
Avatar(user, src: user.avatar) # pass the attachment/uploader/URL directly
|
|
413
|
+
Avatar("acme-team") # String subject = deterministic seed
|
|
414
|
+
Avatar("https://.../p.png") # URL-shaped subject is shown as the image
|
|
415
|
+
Avatar(src: avatar_url) # bare image, no subject/fallback
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- **subject** (positional): record → PII-free hashed seed + default `alt` (display name); String → seed. A URL-shaped String (`http(s)://…` or `/…`) is routed to `src` (shown as the image), not used as a seed.
|
|
419
|
+
- **src**: a Symbol is sent to the subject (`:avatar` → `subject.avatar`, a **contract** — raises if absent); otherwise an ActiveStorage attachment, active_shrine/Shrine uploader, or URL string. ActiveStorage resolves via `helpers.url_for`; everything else via its own `#url`.
|
|
420
|
+
- **size**: `:xs 24 / :sm 32 / :md 40 / :lg 48 / :xl 64`, or a raw Integer.
|
|
421
|
+
- **Privacy**: the value sent to Navii is **always** a SHA256 hash — no ids, emails, or seed strings leave the app. Deterministic per subject.
|
|
422
|
+
- **Resolution order**: resolved `src` → Navii (from subject) → generic user icon.
|
|
423
|
+
- **Config**: `config.navii_host_url` (default `https://api.navii.dev`); the component appends `/avatar/:seed`.
|
|
424
|
+
|
|
425
|
+
🚨 Ejected shells: `Avatar` only shows a Navii avatar when `NavUser` is passed `record:`. The gem's `_resource_header.html.erb` passes `record: (current_user if current_user.respond_to?(:id))`; portals that **ejected** the header before this must re-eject (`rails g pu:eject:shell --dest=<portal>`) or add the `record:` line, otherwise they keep the icon fallback. Pass a record only — a String `current_user` (e.g. a guest) would otherwise be seeded as a literal identity.
|
|
426
|
+
|
|
404
427
|
## Custom Phlex components
|
|
405
428
|
|
|
406
429
|
```ruby
|
|
@@ -450,17 +473,18 @@ All pages inherit this. Modals and frame navigation work without special handlin
|
|
|
450
473
|
|
|
451
474
|
# Part 5 — Modals, Slideovers, Tabs
|
|
452
475
|
|
|
453
|
-
## Modal/slideover for `:new` / `:edit`
|
|
476
|
+
## Modal/slideover for `:new` / `:edit` + interactive actions
|
|
454
477
|
|
|
455
478
|
```ruby
|
|
456
479
|
class PostDefinition < ResourceDefinition
|
|
457
|
-
modal :slideover
|
|
458
|
-
# modal :centered
|
|
459
|
-
# modal
|
|
480
|
+
modal :slideover # default — slide-in panel from the right
|
|
481
|
+
# modal :centered # centered dialog
|
|
482
|
+
# modal :centered, size: :lg # centered, wider container
|
|
483
|
+
# modal false # full standalone page
|
|
460
484
|
end
|
|
461
485
|
```
|
|
462
486
|
|
|
463
|
-
|
|
487
|
+
Drives both framework `:new` / `:edit` and every interactive action on the definition. `size:` accepts `:sm`, `:md` (default), `:lg`, `:xl`, `:auto` (hugs content), or `:full`. Per-action `modal:` / `size:` overrides win. See [[plutonium-resource]] › Action Options.
|
|
464
488
|
|
|
465
489
|
## Tabs on the show page
|
|
466
490
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## [0.53.0] - 2026-05-31
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- *(ui)* Add Avatar component with Navii fallback (#59)
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fixes
|
|
8
|
+
|
|
9
|
+
- *(docs)* Correct broken cross-page anchors in guides
|
|
10
|
+
- *(docs)* Retract incorrect "never super" guidance in relation_scope
|
|
11
|
+
- *(docs)* Correct scoped-URL shape in multi-tenancy docs
|
|
12
|
+
- *(rodauth)* Match change_password_notify mailer template name
|
|
13
|
+
|
|
14
|
+
### 🚜 Refactor
|
|
15
|
+
|
|
16
|
+
- *(helpers)* Remove dead view helpers superseded by Phlex components
|
|
17
|
+
## [0.52.0] - 2026-05-21
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug Fixes
|
|
20
|
+
|
|
21
|
+
- *(generators)* Use inclusion validation for required booleans
|
|
22
|
+
- *(generators/assets)* Warn on failed yarn add instead of silently continuing
|
|
23
|
+
- *(docs)* Let StopWriting terminals scroll horizontally instead of overflowing the column
|
|
24
|
+
- *(docs)* Drop misleading 15-min claim — hero CTA → 'Tutorial', getting-started title → 'Learn Plutonium by building'
|
|
25
|
+
- *(generators,docs)* Tutorial walkthrough + reference audit (#57)
|
|
26
|
+
- *(ui/form)* Scope form ids per turbo frame to prevent stream-replace collisions
|
|
27
|
+
- *(ui/form)* Hide secure_association "+" inside secondary modal
|
|
28
|
+
- *(js/form)* Dedupe pre_submit hidden field on repeat change events
|
|
29
|
+
- *(ui)* Form error alert margin + include model name in New/Edit page titles
|
|
30
|
+
|
|
31
|
+
### ⚙️ Miscellaneous Tasks
|
|
32
|
+
|
|
33
|
+
- *(appraisal)* Refresh rails-8.1 gemfile.lock for v0.51.0
|
|
1
34
|
## [0.51.0] - 2026-05-14
|
|
2
35
|
|
|
3
36
|
### 🚀 Features
|