plutonium 0.56.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +1 -1
- data/.claude/skills/plutonium-app/SKILL.md +1 -1
- data/.claude/skills/plutonium-auth/SKILL.md +1 -1
- data/.claude/skills/plutonium-resource/SKILL.md +35 -1
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +38 -16
- data/docs/getting-started/installation.md +2 -2
- data/docs/getting-started/tutorial/02-first-resource.md +1 -1
- data/docs/getting-started/tutorial/03-authentication.md +3 -3
- data/docs/guides/adding-resources.md +1 -1
- data/docs/guides/authentication.md +1 -1
- data/docs/guides/creating-packages.md +1 -1
- data/docs/guides/multi-tenancy.md +1 -1
- data/docs/guides/nested-resources.md +1 -1
- data/docs/guides/user-invites.md +1 -1
- data/docs/guides/user-profile.md +1 -1
- data/docs/reference/app/generators.md +3 -3
- data/docs/reference/app/index.md +1 -1
- data/docs/reference/app/portals.md +1 -1
- data/docs/reference/auth/profile.md +1 -1
- data/docs/reference/resource/actions.md +55 -0
- data/docs/reference/resource/index.md +1 -1
- data/docs/reference/tenancy/invites.md +1 -1
- 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/typespec/typespec_generator.rb +1 -1
- data/lib/generators/pu/saas/welcome_generator.rb +1 -1
- data/lib/plutonium/action/base.rb +19 -2
- data/lib/plutonium/action/condition_context.rb +33 -0
- data/lib/plutonium/ui/grid/card.rb +3 -2
- data/lib/plutonium/ui/page/index.rb +1 -1
- data/lib/plutonium/ui/page/show.rb +1 -1
- data/lib/plutonium/ui/table/resource.rb +2 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17f6996a0503aeccb6070cb0c379d09bfd253e2356a839745368bcaf0110e377
|
|
4
|
+
data.tar.gz: 0603760cfc2a10d6bedaa4b225b33fc5cf5b632a36829c4e6c9e9c4ce7b7971b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b77a88b0c0e04b5a9e143c88b292ec305f7d5c8ef74efe3e0891f20c772e63eef35383a7785338863f4cd9a942c5b76c94c1fa6fedc2995c41da8df680f1e390
|
|
7
|
+
data.tar.gz: 7dd9fd4420c8145227929c4f6ec434520c6701ea387767d9395bb74b413d2e99ce451611c89797604a1cf5a62bb51368a4aa2f5c79d1b698a1c11842979dcb29
|
|
@@ -127,7 +127,7 @@ Meta-generators (`pu:saas:setup`) propagate flags to the generators they chain.
|
|
|
127
127
|
|
|
128
128
|
1. **Load the bootstrap bundle** (or the targeted skill from the router table).
|
|
129
129
|
2. **Generate** — `rails g pu:res:scaffold Model field:type ... --dest=main_app`.
|
|
130
|
-
3. **Migrate** — `rails db:
|
|
130
|
+
3. **Migrate** — `rails db:prepare`.
|
|
131
131
|
4. **Connect** — `rails g pu:res:conn Model --dest=portal_name`.
|
|
132
132
|
5. **Customize** — edit definition / policy as needed.
|
|
133
133
|
6. **Verify** — hit the route in the browser.
|
|
@@ -62,7 +62,7 @@ rails generate pu:pkg:portal admin --auth=user
|
|
|
62
62
|
|
|
63
63
|
# 4. First resource
|
|
64
64
|
rails generate pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
65
|
-
rails db:
|
|
65
|
+
rails db:prepare
|
|
66
66
|
|
|
67
67
|
# 5. Connect resource to portal
|
|
68
68
|
rails generate pu:res:conn Post --dest=admin_portal
|
|
@@ -279,7 +279,7 @@ rails g pu:profile:setup date_of_birth:date bio:text \
|
|
|
279
279
|
rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
|
|
280
280
|
--dest=customer
|
|
281
281
|
|
|
282
|
-
rails db:
|
|
282
|
+
rails db:prepare
|
|
283
283
|
|
|
284
284
|
rails generate pu:profile:conn --dest=customer_portal
|
|
285
285
|
```
|
|
@@ -29,7 +29,7 @@ For tenancy / `associated_with` / `relation_scope`, load [[plutonium-tenancy]].
|
|
|
29
29
|
1. Pick destination: `--dest=main_app` or `--dest=package_name`.
|
|
30
30
|
2. Run `rails g pu:res:scaffold ResourceName field:type ... --dest=<dest>`.
|
|
31
31
|
3. Review the generated migration — add cascade deletes, composite indexes, defaults.
|
|
32
|
-
4. `rails db:
|
|
32
|
+
4. `rails db:prepare`.
|
|
33
33
|
5. `rails g pu:res:conn ResourceName --dest=<portal_name>`.
|
|
34
34
|
6. Customize the policy's `permitted_attributes_for_*` as needed.
|
|
35
35
|
7. Open the portal route in the browser.
|
|
@@ -1050,6 +1050,10 @@ action :name,
|
|
|
1050
1050
|
collection_record_action: true,
|
|
1051
1051
|
bulk_action: true,
|
|
1052
1052
|
|
|
1053
|
+
# Conditional visibility — display-only toggle, NOT authorization (see below).
|
|
1054
|
+
# `-> { false }` keeps the route live but hides the button (e.g. API-only).
|
|
1055
|
+
condition: -> { params[:beta] == "1" },
|
|
1056
|
+
|
|
1053
1057
|
# Grouping
|
|
1054
1058
|
category: :primary, # :primary, :secondary, :danger
|
|
1055
1059
|
position: 50,
|
|
@@ -1062,6 +1066,36 @@ action :name,
|
|
|
1062
1066
|
size: :lg # :sm / :md / :lg / :xl / :auto / :full — overrides definition's modal size
|
|
1063
1067
|
```
|
|
1064
1068
|
|
|
1069
|
+
### Conditional Actions (`condition:`)
|
|
1070
|
+
|
|
1071
|
+
Like `condition:` on inputs/displays/columns — define an action but render its **button** only when a runtime proc is truthy. The action and its route stay live either way; `condition:` only toggles the UI.
|
|
1072
|
+
|
|
1073
|
+
Headline use case: **expose an action's endpoint without a button** — one you call from the API, a webhook, or another service. Hide it with an always-falsy condition; the route still works:
|
|
1074
|
+
|
|
1075
|
+
```ruby
|
|
1076
|
+
# Defined and callable (API / programmatic), but no button anywhere:
|
|
1077
|
+
action :sync_inventory, interaction: SyncInventoryInteraction, condition: -> { false }
|
|
1078
|
+
|
|
1079
|
+
# Per-record display state — object is the row/shown record:
|
|
1080
|
+
action :reopen, interaction: ReopenInteraction, condition: -> { object.closed? }
|
|
1081
|
+
|
|
1082
|
+
# View/request-level toggle (feature flag, beta mode):
|
|
1083
|
+
action :preview, interaction: PreviewInteraction, condition: -> { params[:beta] == "1" }
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Inside the proc, `object`/`record` is the contextual record — the row/shown record for **record** and **collection-record** actions, **nil** for **resource** and **bulk** actions (guard with `object&.…` if shared). Every other call delegates to the **view context**: `current_user`, `current_parent`, `params`, `request`, `allowed_to?`, `resource_record!`, etc. `object` is evaluated per row in tables/grids, so per-record show/hide works there.
|
|
1087
|
+
|
|
1088
|
+
🚨 **`condition:` is NOT authorization — it only hides the button.** A hidden action still has a live route; anyone with the URL can trigger it. "Who may run this" belongs in the policy:
|
|
1089
|
+
|
|
1090
|
+
```ruby
|
|
1091
|
+
# 🚫 WRONG — does not stop non-admins; the route is live.
|
|
1092
|
+
action :wipe, interaction: WipeInteraction, condition: -> { current_user.admin? }
|
|
1093
|
+
# ✅ RIGHT — authorization in the policy, enforced regardless of condition:
|
|
1094
|
+
def wipe? = current_user.admin?
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
The two compose: an action's button shows only when the policy permits **and** the condition is truthy; execution is gated by the policy alone. Use `object` in `condition:` for per-record *display*; use the policy for per-record *authorization*.
|
|
1098
|
+
|
|
1065
1099
|
`Action#with(...)` — actions are frozen value objects; clone with overrides:
|
|
1066
1100
|
|
|
1067
1101
|
```ruby
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## [0.56.1] - 2026-06-05
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- *(actions)* Add display-only condition: to actions
|
|
6
|
+
|
|
7
|
+
### 🐛 Bug Fixes
|
|
8
|
+
|
|
9
|
+
- *(dummy)* Version-adapt kitchen_sinks migration
|
|
10
|
+
|
|
11
|
+
### 📚 Documentation
|
|
12
|
+
|
|
13
|
+
- Recommend db:prepare, firm up README, document conditional actions
|
|
1
14
|
## [0.56.0] - 2026-06-05
|
|
2
15
|
|
|
3
16
|
### 🚀 Features
|
data/CONTRIBUTING.md
CHANGED
data/README.md
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/plutonium)
|
|
4
4
|
[](https://github.com/radioactive-labs/plutonium-core/actions/workflows/main.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
**The Rails framework for things you should never write again.**
|
|
8
|
+
|
|
9
|
+
Convention over configuration, extended to everything you keep rebuilding: **CRUD. Auth. Authorization. Multi-tenancy. Admin portals. Search, filters, bulk actions.** All generated. All customizable. All Rails.
|
|
7
10
|
|
|
8
11
|
## Quick Start
|
|
9
12
|
|
|
@@ -12,20 +15,36 @@ rails new myapp -a propshaft -j esbuild -c tailwind \
|
|
|
12
15
|
-m https://radioactive-labs.github.io/plutonium-core/templates/plutonium.rb
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
Then create
|
|
18
|
+
Then scaffold a resource, create a portal, and connect them:
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
21
|
cd myapp
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
# Scaffold a resource — model, migration, definition, policy
|
|
24
|
+
rails g pu:res:scaffold Post title:string body:text published_at:datetime --dest=main_app
|
|
25
|
+
rails db:prepare
|
|
26
|
+
|
|
27
|
+
# Create a portal (web interface) and connect the resource to it
|
|
28
|
+
rails g pu:pkg:portal app --public
|
|
29
|
+
rails g pu:res:conn Post --dest=app_portal
|
|
30
|
+
|
|
21
31
|
bin/dev
|
|
22
32
|
```
|
|
23
33
|
|
|
24
|
-
Visit `http://localhost:3000`
|
|
34
|
+
Visit `http://localhost:3000/app/posts` — you have a complete CRUD interface.
|
|
25
35
|
|
|
26
|
-
## What You
|
|
36
|
+
## What You Stop Writing
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
Same scaffold command you already know. A very different surface area.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
rails g scaffold Post ... # Rails: just CRUD
|
|
42
|
+
rails g pu:res:scaffold Post ... # Plutonium: full CRUD + search + filters + bulk actions
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
And it doesn't stop at scaffolds:
|
|
46
|
+
|
|
47
|
+
**Resource-oriented architecture** — models, policies, definitions, and controllers that work together:
|
|
29
48
|
|
|
30
49
|
```ruby
|
|
31
50
|
# Policy controls WHO can do WHAT
|
|
@@ -47,7 +66,7 @@ class PostDefinition < ResourceDefinition
|
|
|
47
66
|
end
|
|
48
67
|
```
|
|
49
68
|
|
|
50
|
-
**Packages
|
|
69
|
+
**Packages and portals** — split your app into feature engines and themed web interfaces:
|
|
51
70
|
|
|
52
71
|
```bash
|
|
53
72
|
rails g pu:pkg:package blogging # Business logic
|
|
@@ -65,7 +84,7 @@ rails g pu:rodauth:account user
|
|
|
65
84
|
**Multi-tenancy** with entity scoping:
|
|
66
85
|
|
|
67
86
|
```ruby
|
|
68
|
-
# In portal engine
|
|
87
|
+
# In a portal engine
|
|
69
88
|
scope_to_entity Organization, strategy: :path
|
|
70
89
|
# Routes become /organizations/:organization_id/posts
|
|
71
90
|
```
|
|
@@ -86,6 +105,13 @@ class PublishInteraction < ResourceInteraction
|
|
|
86
105
|
end
|
|
87
106
|
```
|
|
88
107
|
|
|
108
|
+
## Why Plutonium
|
|
109
|
+
|
|
110
|
+
- **Convention over configuration** — extended to resources, policies, portals, and tenancy, not just routes and views.
|
|
111
|
+
- **It's just Rails** — generated code lives in your repo. Edit it, override it, delete it. The "magic" is regular Ruby mixins you can read.
|
|
112
|
+
- **Multi-tenant ready** — path or domain tenancy, scoped relations, invites and memberships out of the box.
|
|
113
|
+
- **AI-readable** — predictable file layout and naming, plus built-in [Claude Code skills](.claude/skills) that teach AI assistants the patterns.
|
|
114
|
+
|
|
89
115
|
## Documentation
|
|
90
116
|
|
|
91
117
|
Full documentation at **[radioactive-labs.github.io/plutonium-core](https://radioactive-labs.github.io/plutonium-core/)**
|
|
@@ -97,18 +123,14 @@ Full documentation at **[radioactive-labs.github.io/plutonium-core](https://radi
|
|
|
97
123
|
|
|
98
124
|
## Requirements
|
|
99
125
|
|
|
100
|
-
- Ruby 3.2+
|
|
101
|
-
- Rails 7.
|
|
126
|
+
- Ruby 3.2.2+
|
|
127
|
+
- Rails 7.2+ (Rails 8 recommended)
|
|
102
128
|
- Node.js 18+
|
|
103
129
|
|
|
104
130
|
## Contributing
|
|
105
131
|
|
|
106
132
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
107
133
|
|
|
108
|
-
## Status
|
|
109
|
-
|
|
110
|
-
Plutonium is used in production but still evolving. APIs may change between minor versions. Pin your version in Gemfile.
|
|
111
|
-
|
|
112
134
|
## License
|
|
113
135
|
|
|
114
|
-
MIT License
|
|
136
|
+
MIT License — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -15,7 +15,7 @@ After the template completes:
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
cd myapp
|
|
18
|
-
rails db:
|
|
18
|
+
rails db:prepare
|
|
19
19
|
bin/dev
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -51,7 +51,7 @@ rails generate pu:core:install
|
|
|
51
51
|
```bash
|
|
52
52
|
rails generate pu:rodauth:install
|
|
53
53
|
rails generate pu:rodauth:account user
|
|
54
|
-
rails db:
|
|
54
|
+
rails db:prepare
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
For account options and customization, see [Reference › Auth](/reference/auth/) and [Guides › Authentication](/guides/authentication).
|
|
@@ -28,7 +28,7 @@ Plutonium supports multiple account types. For admins, use the dedicated `pu:rod
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
rails generate pu:rodauth:admin admin
|
|
31
|
-
rails db:
|
|
31
|
+
rails db:prepare
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
For self-service user accounts, the corresponding command is `rails generate pu:rodauth:account user`.
|
|
@@ -151,7 +151,7 @@ Our blog needs users (authors) who can create posts. Let's create a User account
|
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
153
|
rails generate pu:rodauth:account user
|
|
154
|
-
rails db:
|
|
154
|
+
rails db:prepare
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
This creates a `User` model similar to `Admin`, but with public registration enabled.
|
|
@@ -177,7 +177,7 @@ end
|
|
|
177
177
|
Run the migration:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
rails db:
|
|
180
|
+
rails db:prepare
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
Update the Post model to include the association:
|
|
@@ -42,7 +42,7 @@ rails g pu:res:scaffold Organization name:string:uniq slug:string:uniq --dest=ma
|
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
rails g pu:res:scaffold Post organization:belongs_to title:string content:text --dest=main_app
|
|
45
|
-
rails db:
|
|
45
|
+
rails db:prepare
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
### 3. Scope the portal to the entity
|
|
@@ -20,7 +20,7 @@ All of this happens with no manual route wiring — Plutonium generates it from
|
|
|
20
20
|
```bash
|
|
21
21
|
rails g pu:res:scaffold Company name:string --dest=main_app
|
|
22
22
|
rails g pu:res:scaffold Property company:belongs_to name:string --dest=main_app
|
|
23
|
-
rails db:
|
|
23
|
+
rails db:prepare
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### 2. Connect both to the portal
|
data/docs/guides/user-invites.md
CHANGED
data/docs/guides/user-profile.md
CHANGED
|
@@ -455,7 +455,7 @@ rails generate pu:pkg:portal admin --auth=admin
|
|
|
455
455
|
rails generate pu:res:conn Post Comment --dest=admin_portal
|
|
456
456
|
|
|
457
457
|
# 6. Migrate
|
|
458
|
-
rails db:
|
|
458
|
+
rails db:prepare
|
|
459
459
|
|
|
460
460
|
# 7. Create the first admin
|
|
461
461
|
rails rodauth_admin:create[admin@example.com,password123]
|
|
@@ -465,7 +465,7 @@ rails rodauth_admin:create[admin@example.com,password123]
|
|
|
465
465
|
|
|
466
466
|
```bash
|
|
467
467
|
rails g pu:res:scaffold Product name:string price_cents:integer --dest=main_app
|
|
468
|
-
rails db:
|
|
468
|
+
rails db:prepare
|
|
469
469
|
rails g pu:res:conn Product --dest=admin_portal
|
|
470
470
|
```
|
|
471
471
|
|
|
@@ -474,7 +474,7 @@ rails g pu:res:conn Product --dest=admin_portal
|
|
|
474
474
|
```bash
|
|
475
475
|
rails g pu:pkg:portal customer --auth=user --scope=Organization
|
|
476
476
|
rails g pu:res:conn Order --dest=customer_portal
|
|
477
|
-
rails db:
|
|
477
|
+
rails db:prepare
|
|
478
478
|
```
|
|
479
479
|
|
|
480
480
|
## Undoing generators
|
data/docs/reference/app/index.md
CHANGED
|
@@ -50,7 +50,7 @@ rails generate pu:pkg:portal admin --auth=user
|
|
|
50
50
|
|
|
51
51
|
# 4. First resource
|
|
52
52
|
rails generate pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
53
|
-
rails db:
|
|
53
|
+
rails db:prepare
|
|
54
54
|
|
|
55
55
|
# 5. Connect resource to portal
|
|
56
56
|
rails generate pu:res:conn Post --dest=admin_portal
|
|
@@ -195,7 +195,7 @@ rails g pu:res:conn Profile --dest=customer_portal --singular
|
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
::: tip Run after migrations
|
|
198
|
-
The generator reads model columns to seed the policy's `permitted_attributes_for_*`. Run `rails db:
|
|
198
|
+
The generator reads model columns to seed the policy's `permitted_attributes_for_*`. Run `rails db:prepare` first.
|
|
199
199
|
:::
|
|
200
200
|
|
|
201
201
|
### What gets generated
|
|
@@ -24,7 +24,7 @@ Meta-generator: runs `pu:profile:install` + `pu:profile:conn` in one shot.
|
|
|
24
24
|
rails generate pu:profile:install bio:text avatar:attachment 'timezone:string?' \
|
|
25
25
|
--dest=customer
|
|
26
26
|
|
|
27
|
-
rails db:
|
|
27
|
+
rails db:prepare
|
|
28
28
|
|
|
29
29
|
rails generate pu:profile:conn --dest=customer_portal
|
|
30
30
|
```
|
|
@@ -61,6 +61,9 @@ action :name,
|
|
|
61
61
|
collection_record_action: true,
|
|
62
62
|
bulk_action: true,
|
|
63
63
|
|
|
64
|
+
# Conditional visibility — display-only proc, NOT authorization (see below)
|
|
65
|
+
condition: -> { params[:beta] == "1" },
|
|
66
|
+
|
|
64
67
|
# Grouping
|
|
65
68
|
category: :primary, # :primary, :secondary, :danger
|
|
66
69
|
position: 50, # display order (lower = first)
|
|
@@ -84,6 +87,58 @@ def customize_actions
|
|
|
84
87
|
end
|
|
85
88
|
```
|
|
86
89
|
|
|
90
|
+
## Conditional visibility — `condition:`
|
|
91
|
+
|
|
92
|
+
Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown** — the action (and its route) stays fully live either way.
|
|
93
|
+
|
|
94
|
+
The headline use case: **expose an action's endpoint without surfacing it in the UI** — e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Defined and callable (API / programmatic), but no button anywhere in the UI:
|
|
98
|
+
action :sync_inventory, interaction: SyncInventoryInteraction, condition: -> { false }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It also works as a dynamic toggle driven by the **record** or the **view/request** context:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# object → the row/shown record (record & collection-record actions):
|
|
105
|
+
action :reopen, interaction: ReopenInteraction, condition: -> { object.closed? }
|
|
106
|
+
# view/request state — feature flag, preview/beta mode:
|
|
107
|
+
action :preview, interaction: PreviewInteraction, condition: -> { params[:beta] == "1" }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Inside the proc, `object`/`record` is the contextual record, and every other call delegates to the **view context**:
|
|
111
|
+
|
|
112
|
+
| Available | Notes |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `object` / `record` | The row/shown record for **record** and **collection-record** actions; **`nil`** for resource and bulk actions (no single record). Guard with `object&.…` if a condition is shared across action kinds. |
|
|
115
|
+
| `params`, `request` | Current request. |
|
|
116
|
+
| `current_user`, `current_parent` | The signed-in user and (nested) parent. |
|
|
117
|
+
| `resource_record!` | The shown record on the show page; raises on index/table — prefer `object`. |
|
|
118
|
+
| `allowed_to?`, `policy_for`, other helpers | The usual view helpers. |
|
|
119
|
+
|
|
120
|
+
`object` is evaluated **per row** in tables and grids, so per-record show/hide works there too.
|
|
121
|
+
|
|
122
|
+
::: danger `condition:` is NOT authorization — it only hides the button
|
|
123
|
+
A hidden action still has a **live route**: anyone who knows the URL can still trigger it. `condition:` decides whether the *button renders*, never whether the *request is allowed*.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# 🚫 WRONG — this does NOT stop non-admins. The route is live; they can POST to it.
|
|
127
|
+
action :wipe, interaction: WipeInteraction, condition: -> { current_user.admin? }
|
|
128
|
+
|
|
129
|
+
# ✅ RIGHT — authorization belongs in the policy. The action only runs if this returns true.
|
|
130
|
+
class WidgetPolicy < ResourcePolicy
|
|
131
|
+
def wipe? = current_user.admin?
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose — an action appears only when the policy permits **and** the condition is truthy.
|
|
136
|
+
:::
|
|
137
|
+
|
|
138
|
+
::: tip Per-record display vs. per-record authorization
|
|
139
|
+
`condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy — `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
|
|
140
|
+
:::
|
|
141
|
+
|
|
87
142
|
## Simple actions (navigation)
|
|
88
143
|
|
|
89
144
|
Link to an existing route. The target route MUST exist.
|
|
@@ -14,7 +14,7 @@ A **resource** is the unit Plutonium gives you full CRUD for — list, show, cre
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
rails g pu:res:scaffold Post user:belongs_to title:string 'content:text?' --dest=main_app
|
|
17
|
-
rails db:
|
|
17
|
+
rails db:prepare
|
|
18
18
|
rails g pu:res:conn Post --dest=admin_portal
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -42,7 +42,7 @@ module Pu
|
|
|
42
42
|
say_status :pending, "#{migration.version} - #{migration.name}", :yellow
|
|
43
43
|
end
|
|
44
44
|
say ""
|
|
45
|
-
say "Run `bin/rails db:
|
|
45
|
+
say "Run `bin/rails db:prepare` before generating TypeSpec specifications.", :red
|
|
46
46
|
raise Thor::Error, "Cannot generate TypeSpec with pending migrations"
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -135,7 +135,7 @@ module Pu
|
|
|
135
135
|
say "Next steps:"
|
|
136
136
|
say "\n"
|
|
137
137
|
say "1. Run migrations (if you haven't already):"
|
|
138
|
-
say " rails db:
|
|
138
|
+
say " rails db:prepare"
|
|
139
139
|
say "\n"
|
|
140
140
|
say "2. Customize the onboarding view to match your app:"
|
|
141
141
|
say " app/views/welcome/onboarding.html.erb"
|
|
@@ -6,7 +6,7 @@ module Plutonium
|
|
|
6
6
|
module Action
|
|
7
7
|
# Base class for all actions in the Plutonium framework.
|
|
8
8
|
class Base
|
|
9
|
-
attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :color, :category, :position, :return_to
|
|
9
|
+
attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :color, :category, :position, :return_to, :condition
|
|
10
10
|
|
|
11
11
|
def initialize(name, **options)
|
|
12
12
|
@name = name.to_sym
|
|
@@ -27,6 +27,7 @@ module Plutonium
|
|
|
27
27
|
@position = options[:position] || 50
|
|
28
28
|
@modal_mode = options[:modal]
|
|
29
29
|
@modal_size = options[:size]
|
|
30
|
+
@condition = options[:condition]
|
|
30
31
|
validate_modal_mode!
|
|
31
32
|
validate_modal_size!
|
|
32
33
|
|
|
@@ -62,6 +63,21 @@ module Plutonium
|
|
|
62
63
|
policy.allowed_to?(:"#{name}?")
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# Display-only visibility gate, mirroring the `condition:` proc on
|
|
67
|
+
# inputs/displays/columns. Returns true when no condition is set.
|
|
68
|
+
#
|
|
69
|
+
# The proc is evaluated against a ConditionContext: `object`/`record` is
|
|
70
|
+
# the contextual record (nil for resource/bulk actions), and every other
|
|
71
|
+
# call delegates to the view context (current_user, params, request,
|
|
72
|
+
# allowed_to?, resource_record!, …).
|
|
73
|
+
#
|
|
74
|
+
# NOT an authorization boundary — a hidden action still has a live route;
|
|
75
|
+
# keep authorization in the policy.
|
|
76
|
+
def condition_met?(view_context, record: nil)
|
|
77
|
+
return true if @condition.nil?
|
|
78
|
+
ConditionContext.new(view_context, record).instance_exec(&@condition)
|
|
79
|
+
end
|
|
80
|
+
|
|
65
81
|
# Returns a new Action with the given options merged over this one.
|
|
66
82
|
def with(**overrides)
|
|
67
83
|
self.class.new(name, **to_options.merge(overrides))
|
|
@@ -90,7 +106,8 @@ module Plutonium
|
|
|
90
106
|
category: @category.to_sym,
|
|
91
107
|
position: @position,
|
|
92
108
|
modal: @modal_mode,
|
|
93
|
-
size: @modal_size
|
|
109
|
+
size: @modal_size,
|
|
110
|
+
condition: @condition
|
|
94
111
|
}
|
|
95
112
|
end
|
|
96
113
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module Plutonium
|
|
6
|
+
module Action
|
|
7
|
+
# Evaluation scope for an action's `condition:` proc.
|
|
8
|
+
#
|
|
9
|
+
# Exposes the contextual record as both `object` and `record`, and delegates
|
|
10
|
+
# everything else to the request's **view context** — the object render
|
|
11
|
+
# components forward their helpers to — so a condition can use current_user,
|
|
12
|
+
# params, request, allowed_to?, resource_record!, etc. directly, exactly
|
|
13
|
+
# like the `condition:` procs on inputs/displays/columns.
|
|
14
|
+
#
|
|
15
|
+
# Delegating to the view context (not a render component) matters: the
|
|
16
|
+
# component exposes params/request only as PRIVATE methods, which a delegator
|
|
17
|
+
# can't forward; the view context exposes them publicly.
|
|
18
|
+
#
|
|
19
|
+
# `record`/`object` is the row/shown record for record and
|
|
20
|
+
# collection-record actions, and nil for resource/bulk actions (no single
|
|
21
|
+
# record in scope) — so guard with `object&.…` in conditions shared across
|
|
22
|
+
# action kinds.
|
|
23
|
+
class ConditionContext < SimpleDelegator
|
|
24
|
+
attr_reader :record
|
|
25
|
+
alias_method :object, :record
|
|
26
|
+
|
|
27
|
+
def initialize(view_context, record)
|
|
28
|
+
super(view_context)
|
|
29
|
+
@record = record
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -232,12 +232,13 @@ module Plutonium
|
|
|
232
232
|
|
|
233
233
|
def row_actions
|
|
234
234
|
@row_actions ||= resource_definition.defined_actions.values.select { |a|
|
|
235
|
-
a.collection_record_action? && a.permitted_by?(record_policy)
|
|
235
|
+
a.collection_record_action? && a.permitted_by?(record_policy) && a.condition_met?(view_context, record:)
|
|
236
236
|
}
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
def can_show?
|
|
240
|
-
resource_definition.defined_actions[:show]
|
|
240
|
+
action = resource_definition.defined_actions[:show]
|
|
241
|
+
action&.permitted_by?(record_policy) && action.condition_met?(view_context, record:)
|
|
241
242
|
end
|
|
242
243
|
|
|
243
244
|
def record_policy
|
|
@@ -33,7 +33,7 @@ module Plutonium
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def page_actions
|
|
36
|
-
super || current_definition.defined_actions.values.select { |a| a.resource_action? && a.permitted_by?(current_policy) }
|
|
36
|
+
super || current_definition.defined_actions.values.select { |a| a.resource_action? && a.permitted_by?(current_policy) && a.condition_met?(view_context) }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def render_default_content
|
|
@@ -15,7 +15,7 @@ module Plutonium
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def page_actions
|
|
18
|
-
super || current_definition.defined_actions.values.select { |a| a.record_action? && a.permitted_by?(current_policy) }
|
|
18
|
+
super || current_definition.defined_actions.values.select { |a| a.record_action? && a.permitted_by?(current_policy) && a.condition_met?(view_context, record: resource_record!) }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def render_default_content
|
|
@@ -134,7 +134,7 @@ module Plutonium
|
|
|
134
134
|
policy = policy_for(record:)
|
|
135
135
|
|
|
136
136
|
actions = resource_definition.defined_actions
|
|
137
|
-
.select { |k, a| a.collection_record_action? && policy.allowed_to?(:"#{k}?") }
|
|
137
|
+
.select { |k, a| a.collection_record_action? && policy.allowed_to?(:"#{k}?") && a.condition_met?(view_context, record:) }
|
|
138
138
|
.values
|
|
139
139
|
|
|
140
140
|
primary_actions = actions.select { |a| a.category.primary? }.sort_by(&:position)
|
|
@@ -161,7 +161,7 @@ module Plutonium
|
|
|
161
161
|
|
|
162
162
|
def bulk_actions
|
|
163
163
|
@bulk_actions ||= resource_definition.defined_actions
|
|
164
|
-
.select { |k, a| a.bulk_action? }
|
|
164
|
+
.select { |k, a| a.bulk_action? && a.condition_met?(view_context) }
|
|
165
165
|
.values
|
|
166
166
|
end
|
|
167
167
|
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: plutonium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.56.
|
|
4
|
+
version: 0.56.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stefan Froelich
|
|
@@ -956,6 +956,7 @@ files:
|
|
|
956
956
|
- lib/plutonium.rb
|
|
957
957
|
- lib/plutonium/action/README.md
|
|
958
958
|
- lib/plutonium/action/base.rb
|
|
959
|
+
- lib/plutonium/action/condition_context.rb
|
|
959
960
|
- lib/plutonium/action/interactive.rb
|
|
960
961
|
- lib/plutonium/action/route_options.rb
|
|
961
962
|
- lib/plutonium/action/simple.rb
|