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.
- 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-tenancy/SKILL.md +25 -6
- data/.claude/skills/plutonium-testing/SKILL.md +3 -1
- data/.claude/skills/plutonium-ui/SKILL.md +3 -3
- data/CHANGELOG.md +17 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +1 -0
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +1 -1
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/.vitepress/config.ts +1 -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 +10 -5
- data/docs/guides/authorization.md +3 -3
- data/docs/guides/creating-packages.md +8 -11
- data/docs/guides/custom-actions.md +6 -1
- data/docs/guides/customizing-ui.md +258 -0
- data/docs/guides/index.md +49 -32
- data/docs/guides/multi-tenancy.md +10 -2
- 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 +13 -0
- 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/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 +6 -7
- data/docs/reference/auth/index.md +1 -1
- data/docs/reference/behavior/policies.md +1 -1
- data/docs/reference/index.md +67 -55
- data/docs/reference/resource/definition.md +1 -1
- data/docs/reference/tenancy/entity-scoping.md +8 -1
- data/docs/reference/tenancy/index.md +1 -1
- data/docs/reference/tenancy/invites.md +12 -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/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/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/helpers/turbo_helper.rb +19 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +4 -4
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -3
- data/lib/plutonium/ui/component/methods.rb +1 -0
- data/lib/plutonium/ui/form/base.rb +17 -1
- data/lib/plutonium/ui/form/components/secure_association.rb +11 -6
- data/lib/plutonium/ui/form/interaction.rb +1 -1
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/page/edit.rb +1 -1
- data/lib/plutonium/ui/page/new.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/js/controllers/form_controller.js +5 -4
- data/yarn.lock +108 -1
- metadata +45 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3015426fb7d1a742cd436928a9a8e2c24597f391d68657b8f00ed31058cf59c
|
|
4
|
+
data.tar.gz: 20bf4101ff956923bf01b28ad890bc5000760a5181ab65ac07f83f2bb739b0d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14b0e28c435b199564a46eae98b84c4273c2783344a3257dcb9adc872002c8e761e058f7f76fbe909b3419facd0e90e1d9ee1a3c8fa5f128189eb1df6e365761
|
|
7
|
+
data.tar.gz: 19e6dfaf2c2526fcb4cfadba2eb2e09fa294141abb2175f52348f45d050d975aa92b59541d8aa5eb4c14b2bd973f60b45abea9498ea20bc7340529d8e2f96370
|
|
@@ -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.
|
|
@@ -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 `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.
|
|
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,7 @@ 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
|
+
**Prefer calling `default_relation_scope(relation)` explicitly** instead of relying on `super`. `super` works when you're extending a parent policy (e.g., a package-level base) that itself calls `default_relation_scope` — but it's brittle against `Plutonium::Resource::Policy` directly because `super`'s semantics depend on how ActionPolicy's DSL registered the scope. The runtime verification checks `default_relation_scope` was hit somewhere — not that you wrote it in this class.
|
|
174
174
|
|
|
175
175
|
### Intentionally skipping
|
|
176
176
|
|
|
@@ -235,6 +235,7 @@ entity_scope
|
|
|
235
235
|
|
|
236
236
|
- **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
237
|
- **`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`.
|
|
238
|
+
- **Default `param_key` includes `_scoped` suffix.** `scope_to_entity Organization` produces routes like `/organization_scoped/:organization_scoped_id/posts` to avoid colliding with a `belongs_to :organization` on child models. Pass `param_key:` (and optionally `route_key:`) to override for cleaner URLs.
|
|
238
239
|
- **Forgetting compound uniqueness.** `validates :code, uniqueness: true` leaks across tenants. Use `uniqueness: {scope: :organization_id}`.
|
|
239
240
|
- **"Temporary" `where` bypass for debugging.** Use `skip_default_relation_scope!` explicitly. Never leave a `where` bypass in code.
|
|
240
241
|
|
|
@@ -421,10 +422,18 @@ rails generate pu:invites:install
|
|
|
421
422
|
| `--entity-model=NAME` | `Entity` | Entity model name |
|
|
422
423
|
| `--user-model=NAME` | `User` | User model name |
|
|
423
424
|
| `--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 |
|
|
425
|
+
| `--membership-model=NAME` | `EntityUser` | Membership join model (must already exist; roles are read from its `enum :role`) |
|
|
426
426
|
| `--rodauth=NAME` | `user` | Rodauth configuration for signup |
|
|
427
427
|
| `--enforce-domain` | `false` | Require invited email domain to match entity |
|
|
428
|
+
| `--dest=PACKAGE` | `main_app` | Package where the entity model lives (controls where `invite_user_interaction.rb` is generated) |
|
|
429
|
+
|
|
430
|
+
::: 🚨 No `--roles` flag here
|
|
431
|
+
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]`).
|
|
432
|
+
:::
|
|
433
|
+
|
|
434
|
+
::: 🚨 ActiveRecord encryption keys required
|
|
435
|
+
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).
|
|
436
|
+
:::
|
|
428
437
|
|
|
429
438
|
### What gets created
|
|
430
439
|
|
|
@@ -619,13 +628,23 @@ class Invites::UserInvite < Invites::ResourceRecord
|
|
|
619
628
|
end
|
|
620
629
|
```
|
|
621
630
|
|
|
622
|
-
### Domain enforcement
|
|
631
|
+
### Domain enforcement
|
|
623
632
|
|
|
624
633
|
```bash
|
|
625
634
|
rails g pu:invites:install --enforce-domain
|
|
626
|
-
rails g pu:invites:install --roles=viewer,editor,admin,owner
|
|
627
635
|
```
|
|
628
636
|
|
|
637
|
+
### Custom roles
|
|
638
|
+
|
|
639
|
+
Set roles when generating the membership model (ordering: index 0 = most privileged):
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
rails g pu:saas:membership --user Customer --entity Organization --roles=admin,editor,viewer
|
|
643
|
+
# → enum :role, { owner: 0, admin: 1, editor: 2, viewer: 3 } (owner auto-prepended)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Or edit `enum :role` on the existing membership model directly. Then run `pu:invites:install`.
|
|
647
|
+
|
|
629
648
|
## Portal connection
|
|
630
649
|
|
|
631
650
|
```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 |
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [0.52.0] - 2026-05-21
|
|
2
|
+
|
|
3
|
+
### 🐛 Bug Fixes
|
|
4
|
+
|
|
5
|
+
- *(generators)* Use inclusion validation for required booleans
|
|
6
|
+
- *(generators/assets)* Warn on failed yarn add instead of silently continuing
|
|
7
|
+
- *(docs)* Let StopWriting terminals scroll horizontally instead of overflowing the column
|
|
8
|
+
- *(docs)* Drop misleading 15-min claim — hero CTA → 'Tutorial', getting-started title → 'Learn Plutonium by building'
|
|
9
|
+
- *(generators,docs)* Tutorial walkthrough + reference audit (#57)
|
|
10
|
+
- *(ui/form)* Scope form ids per turbo frame to prevent stream-replace collisions
|
|
11
|
+
- *(ui/form)* Hide secure_association "+" inside secondary modal
|
|
12
|
+
- *(js/form)* Dedupe pre_submit hidden field on repeat change events
|
|
13
|
+
- *(ui)* Form error alert margin + include model name in New/Edit page titles
|
|
14
|
+
|
|
15
|
+
### ⚙️ Miscellaneous Tasks
|
|
16
|
+
|
|
17
|
+
- *(appraisal)* Refresh rails-8.1 gemfile.lock for v0.51.0
|
|
1
18
|
## [0.51.0] - 2026-05-14
|
|
2
19
|
|
|
3
20
|
### 🚀 Features
|