plutonium 0.42.0 → 0.43.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-controller/SKILL.md +38 -1
- data/.claude/skills/plutonium-definition/SKILL.md +14 -0
- data/.claude/skills/plutonium-forms/SKILL.md +16 -1
- data/.claude/skills/plutonium-profile/SKILL.md +276 -0
- data/.claude/skills/plutonium-views/SKILL.md +23 -1
- data/CHANGELOG.md +42 -0
- data/app/assets/plutonium.css +2 -2
- data/app/views/plutonium/_resource_header.html.erb +6 -27
- data/app/views/plutonium/_resource_sidebar.html.erb +1 -2
- data/app/views/resource/_resource_details.rabl +3 -2
- data/app/views/resource/index.rabl +3 -2
- data/app/views/resource/show.rabl +3 -2
- data/docs/guides/user-profile.md +322 -0
- data/docs/reference/controller/index.md +38 -1
- data/docs/reference/definition/index.md +16 -0
- data/docs/reference/views/forms.md +15 -0
- data/docs/reference/views/index.md +23 -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/assets/assets_generator.rb +12 -0
- data/lib/generators/pu/core/install/templates/app/controllers/resource_controller.rb.tt +11 -0
- data/lib/generators/pu/core/typespec/templates/common.tsp.tt +95 -0
- data/lib/generators/pu/core/typespec/templates/main.tsp.tt +27 -0
- data/lib/generators/pu/core/typespec/templates/main_multi.tsp.tt +25 -0
- data/lib/generators/pu/core/typespec/templates/model.tsp.tt +226 -0
- data/lib/generators/pu/core/typespec/typespec_generator.rb +342 -0
- data/lib/generators/pu/invites/USAGE +0 -1
- data/lib/generators/pu/invites/install_generator.rb +62 -15
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +2 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +1 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +5 -5
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +4 -4
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +29 -0
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +6 -23
- data/lib/generators/pu/pkg/portal/portal_generator.rb +5 -1
- data/lib/generators/pu/profile/USAGE +59 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +27 -0
- data/lib/generators/pu/profile/conn/USAGE +33 -0
- data/lib/generators/pu/profile/conn_generator.rb +167 -0
- data/lib/generators/pu/profile/install_generator.rb +119 -0
- data/lib/generators/pu/profile/setup/USAGE +42 -0
- data/lib/generators/pu/profile/setup_generator.rb +73 -0
- data/lib/generators/pu/rodauth/account_generator.rb +2 -4
- data/lib/generators/pu/rodauth/install_generator.rb +2 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/api_client_generator.rb +0 -2
- data/lib/generators/pu/saas/membership_generator.rb +68 -19
- data/lib/generators/pu/saas/setup_generator.rb +7 -2
- data/lib/generators/pu/saas/user_generator.rb +0 -2
- data/lib/plutonium/auth/rodauth.rb +8 -0
- data/lib/plutonium/core/controller.rb +7 -4
- data/lib/plutonium/core/controllers/authorizable.rb +5 -1
- data/lib/plutonium/definition/base.rb +7 -0
- data/lib/plutonium/helpers/display_helper.rb +6 -0
- data/lib/plutonium/profile/security_section.rb +118 -0
- data/lib/plutonium/resource/controller.rb +17 -7
- data/lib/plutonium/resource/controllers/interactive_actions.rb +11 -25
- data/lib/plutonium/resource/controllers/presentable.rb +46 -3
- data/lib/plutonium/resource/record/associated_with.rb +7 -1
- data/lib/plutonium/routing/mapper_extensions.rb +18 -18
- data/lib/plutonium/routing/route_set_extensions.rb +23 -2
- data/lib/plutonium/ui/breadcrumbs.rb +111 -131
- data/lib/plutonium/ui/dyna_frame/content.rb +12 -2
- data/lib/plutonium/ui/form/resource.rb +26 -19
- data/lib/plutonium/ui/page/base.rb +14 -14
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -74
- data/lib/plutonium/ui/table/components/selection_column.rb +6 -2
- data/lib/plutonium/ui/table/resource.rb +3 -2
- data/lib/plutonium/version.rb +1 -1
- data/lib/tasks/release.rake +6 -6
- data/package.json +1 -1
- metadata +17 -3
- data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +0 -19
|
@@ -3,39 +3,18 @@
|
|
|
3
3
|
<%= resource_logo_tag(classname: "h-10 rounded-md") %>
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
|
-
<% header.with_action do %>
|
|
7
|
-
<%=
|
|
8
|
-
render Plutonium::UI::NavGridMenu.new(label: "Apps", icon: Phlex::TablerIcons::Apps) do |menu|
|
|
9
|
-
menu.with_item(name: "Dashboard", icon: Phlex::TablerIcons::Dashboard, href: "#")
|
|
10
|
-
menu.with_item(name: "Settings", icon: Phlex::TablerIcons::Settings, href: "#")
|
|
11
|
-
menu.with_item(name: "Profile", icon: Phlex::TablerIcons::User, href: "#")
|
|
12
|
-
end
|
|
13
|
-
%>
|
|
14
|
-
<% end %>
|
|
15
|
-
|
|
16
6
|
<% header.with_action do %>
|
|
17
7
|
<%=
|
|
18
8
|
render Plutonium::UI::NavUser.new(
|
|
19
9
|
name: nil,
|
|
20
10
|
email: current_user.try(:email) || current_user
|
|
21
11
|
) do |nav|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
section.with_link(label: "My likes", href: "#") do |link|
|
|
29
|
-
link.with_leading do
|
|
30
|
-
render Phlex::TablerIcons::Heart.new(class: "mr-2 text-gray-400 w-4 h-4")
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
section.with_link(label: "Pro version", href: "#") do |link|
|
|
34
|
-
link.with_leading do
|
|
35
|
-
render Phlex::TablerIcons::Flame.new(class: "mr-2 text-primary-600 dark:text-primary-500 w-4 h-4")
|
|
36
|
-
end
|
|
37
|
-
link.with_trailing do
|
|
38
|
-
render Phlex::TablerIcons::CaretRight.new(class: "w-3 h-3")
|
|
12
|
+
if try(:profile_url)
|
|
13
|
+
nav.with_section do |section|
|
|
14
|
+
section.with_link(label: "Profile", href: profile_url) do |link|
|
|
15
|
+
link.with_leading do
|
|
16
|
+
render Phlex::TablerIcons::User.new(class: "mr-2 text-gray-400 w-4 h-4")
|
|
17
|
+
end
|
|
39
18
|
end
|
|
40
19
|
end
|
|
41
20
|
end
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
m.item "Resources", icon: Phlex::TablerIcons::GridDots do |n|
|
|
10
10
|
registered_resources.each do |resource|
|
|
11
|
-
n.item resource
|
|
12
|
-
url: resource_url_for(resource, parent: nil)
|
|
11
|
+
n.item resource_label(resource), url: resource_url_for(resource, parent: nil)
|
|
13
12
|
end
|
|
14
13
|
end
|
|
15
14
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
attributes
|
|
2
|
-
attributes :created_at
|
|
1
|
+
attributes resource_class.primary_key.to_sym
|
|
2
|
+
attributes :created_at if resource_class.column_names.include?("created_at")
|
|
3
|
+
attributes :updated_at if resource_class.column_names.include?("updated_at")
|
|
3
4
|
|
|
4
5
|
node(:sgid) { |resource| resource.to_signed_global_id.to_s }
|
|
5
6
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
collection @resource_records, root: resource_class.to_s.demodulize.underscore.pluralize.to_sym, object_root: false
|
|
2
2
|
|
|
3
|
-
attributes
|
|
4
|
-
attributes :created_at
|
|
3
|
+
attributes resource_class.primary_key.to_sym
|
|
4
|
+
attributes :created_at if resource_class.column_names.include?("created_at")
|
|
5
|
+
attributes :updated_at if resource_class.column_names.include?("updated_at")
|
|
5
6
|
|
|
6
7
|
node(:sgid) { |resource| resource.to_signed_global_id.to_s }
|
|
7
8
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
object @resource_record
|
|
2
2
|
|
|
3
|
-
attributes
|
|
4
|
-
attributes :created_at
|
|
3
|
+
attributes resource_class.primary_key.to_sym
|
|
4
|
+
attributes :created_at if resource_class.column_names.include?("created_at")
|
|
5
|
+
attributes :updated_at if resource_class.column_names.include?("updated_at")
|
|
5
6
|
|
|
6
7
|
node(:sgid) { |resource| resource.to_signed_global_id.to_s }
|
|
7
8
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# User Profile
|
|
2
|
+
|
|
3
|
+
Plutonium provides a Profile resource generator for user account settings. The Profile page allows users to manage their personal information and access Rodauth security features like password changes, two-factor authentication, and session management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The profile system provides:
|
|
8
|
+
- **Profile Resource**: A standard Plutonium resource linked to the User model
|
|
9
|
+
- **Security Section**: Automatic display of enabled Rodauth security features
|
|
10
|
+
- **Customizable Fields**: Add any fields you need (bio, avatar, preferences, etc.)
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
Before installing the profile, ensure you have:
|
|
15
|
+
|
|
16
|
+
1. **User Authentication**: A Rodauth user account set up
|
|
17
|
+
2. **Model Markers**: The User model with marker comments
|
|
18
|
+
|
|
19
|
+
The easiest way to set this up is with the SaaS generator:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
rails g pu:saas:user User
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### Step 1: Install the Profile Resource
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
rails generate pu:profile:install --dest=main_app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With custom fields:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
rails g pu:profile:install \
|
|
37
|
+
bio:text \
|
|
38
|
+
avatar:attachment \
|
|
39
|
+
'timezone:string?' \
|
|
40
|
+
notifications_enabled:boolean \
|
|
41
|
+
--dest=main_app
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Options
|
|
45
|
+
|
|
46
|
+
| Option | Default | Description |
|
|
47
|
+
|--------|---------|-------------|
|
|
48
|
+
| `--dest` | (prompts) | Target destination package or main_app |
|
|
49
|
+
| `--user-model` | User | Rodauth user model name |
|
|
50
|
+
|
|
51
|
+
### Step 2: Run Migrations
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
rails db:migrate
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Step 3: Connect to Portal
|
|
58
|
+
|
|
59
|
+
Connect the Profile to your portal using the profile connect generator:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
rails g pu:profile:conn --dest=customer_portal
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This:
|
|
66
|
+
- Registers the Profile as a singular resource (`/profile` instead of `/profiles/:id`)
|
|
67
|
+
- Configures the `profile_url` helper to enable the "Profile" link in the user menu
|
|
68
|
+
|
|
69
|
+
## Generated Files
|
|
70
|
+
|
|
71
|
+
The generator creates a standard Plutonium resource:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
app/models/profile.rb
|
|
75
|
+
db/migrate/xxx_create_profiles.rb
|
|
76
|
+
app/controllers/profiles_controller.rb
|
|
77
|
+
app/policies/profile_policy.rb
|
|
78
|
+
app/definitions/profile_definition.rb
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
And modifies:
|
|
82
|
+
- **User model**: Adds `has_one :profile, dependent: :destroy`
|
|
83
|
+
- **Definition**: Injects custom ShowPage with security links
|
|
84
|
+
|
|
85
|
+
## Security Section
|
|
86
|
+
|
|
87
|
+
The ShowPage automatically displays links to enabled Rodauth security features.
|
|
88
|
+
|
|
89
|
+
Available features (only shown if enabled in Rodauth):
|
|
90
|
+
|
|
91
|
+
| Feature | Link Label | Description |
|
|
92
|
+
|---------|------------|-------------|
|
|
93
|
+
| `change_password` | Change Password | Update account password |
|
|
94
|
+
| `change_login` | Change Email | Update email address |
|
|
95
|
+
| `otp` | Two-Factor Authentication | Set up TOTP authenticator |
|
|
96
|
+
| `recovery_codes` | Recovery Codes | View or regenerate backup codes |
|
|
97
|
+
| `webauthn` | Security Keys | Manage passkeys and hardware keys |
|
|
98
|
+
| `active_sessions` | Active Sessions | View and revoke sessions |
|
|
99
|
+
| `close_account` | Close Account | Permanently delete account |
|
|
100
|
+
|
|
101
|
+
## Creating Profiles for Users
|
|
102
|
+
|
|
103
|
+
Users need a profile created before they can access it. Choose one approach:
|
|
104
|
+
|
|
105
|
+
### Option A: Automatic Creation on User Signup
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
# app/models/user.rb
|
|
109
|
+
class User < ApplicationRecord
|
|
110
|
+
has_one :profile, dependent: :destroy
|
|
111
|
+
|
|
112
|
+
after_create :create_profile!
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def create_profile!
|
|
117
|
+
create_profile
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# add has_one associations above.
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Option B: Create on First Access
|
|
125
|
+
|
|
126
|
+
In your portal's application controller:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
class ApplicationController < PlutoniumController
|
|
130
|
+
before_action :ensure_profile
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def ensure_profile
|
|
135
|
+
return unless current_user
|
|
136
|
+
current_user.profile || current_user.create_profile
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Option C: Find or Create in Profile Controller
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# app/controllers/profiles_controller.rb
|
|
145
|
+
class ProfilesController < ResourceController
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def current_resource
|
|
149
|
+
@current_resource ||= current_user.profile || current_user.create_profile
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Customization
|
|
155
|
+
|
|
156
|
+
### Adding Custom Fields
|
|
157
|
+
|
|
158
|
+
Edit the generated definition to configure how fields appear:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# app/definitions/profile_definition.rb
|
|
162
|
+
class ProfileDefinition < Plutonium::Resource::Definition
|
|
163
|
+
form do |f|
|
|
164
|
+
f.field :bio, as: :text
|
|
165
|
+
f.field :avatar, as: :attachment
|
|
166
|
+
f.field :timezone, collection: ActiveSupport::TimeZone.all.map(&:name)
|
|
167
|
+
f.field :notifications_enabled
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
display do |d|
|
|
171
|
+
d.field :bio
|
|
172
|
+
d.field :avatar
|
|
173
|
+
d.field :timezone
|
|
174
|
+
d.field :notifications_enabled
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class ShowPage < ShowPage
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def render_after_content
|
|
181
|
+
render Plutonium::Profile::SecuritySection.new
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Custom Security Section
|
|
188
|
+
|
|
189
|
+
Create your own security section component:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# app/components/custom_security_section.rb
|
|
193
|
+
class CustomSecuritySection < Plutonium::UI::Component::Base
|
|
194
|
+
def view_template
|
|
195
|
+
div(class: "mt-8") do
|
|
196
|
+
h2(class: "text-lg font-semibold") { "Account Security" }
|
|
197
|
+
|
|
198
|
+
if rodauth.features.include?(:change_password)
|
|
199
|
+
a(href: rodauth.change_password_path) { "Change Password" }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Add your custom links
|
|
203
|
+
a(href: "/settings/notifications") { "Notification Preferences" }
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Use it in your definition:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class ProfileDefinition < Plutonium::Resource::Definition
|
|
213
|
+
class ShowPage < ShowPage
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def render_after_content
|
|
217
|
+
render CustomSecuritySection.new
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Adding Profile Actions
|
|
224
|
+
|
|
225
|
+
Add custom actions to the profile:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class ProfileDefinition < Plutonium::Resource::Definition
|
|
229
|
+
action :export_data,
|
|
230
|
+
interaction: Profile::ExportDataInteraction,
|
|
231
|
+
icon: Phlex::TablerIcons::Download
|
|
232
|
+
|
|
233
|
+
action :verify_email,
|
|
234
|
+
interaction: Profile::VerifyEmailInteraction,
|
|
235
|
+
category: :secondary
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Profile Link in Navigation
|
|
240
|
+
|
|
241
|
+
Add a profile link to your application layout or header:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# In your header component
|
|
245
|
+
if current_user && respond_to?(:profile_path)
|
|
246
|
+
a(href: profile_path) { "My Profile" }
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Policy Configuration
|
|
251
|
+
|
|
252
|
+
The generated policy controls access:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# app/policies/profile_policy.rb
|
|
256
|
+
class ProfilePolicy < Plutonium::Resource::Policy
|
|
257
|
+
# Users can only access their own profile
|
|
258
|
+
def read?
|
|
259
|
+
resource.user == user
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def update?
|
|
263
|
+
resource.user == user
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def destroy?
|
|
267
|
+
false # Disable deletion through the UI
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Only allow editing these attributes
|
|
271
|
+
def permitted_attributes_for_update
|
|
272
|
+
[:bio, :avatar, :timezone, :notifications_enabled]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Troubleshooting
|
|
278
|
+
|
|
279
|
+
### "Profile not found" Error
|
|
280
|
+
|
|
281
|
+
Ensure the user has a profile created. Use one of the creation strategies above.
|
|
282
|
+
|
|
283
|
+
### Security Links Not Showing
|
|
284
|
+
|
|
285
|
+
Security links only appear for features enabled in your Rodauth configuration:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# app/rodauth/user_rodauth_plugin.rb
|
|
289
|
+
class UserRodauthPlugin < Plutonium::Auth::RodauthPlugin
|
|
290
|
+
configure do
|
|
291
|
+
enable :change_password, :change_login, :otp, :active_sessions
|
|
292
|
+
# Only these features will show in the security section
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Profile Routes Conflicting
|
|
298
|
+
|
|
299
|
+
Ensure you use `--singular` when connecting:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
rails g pu:res:conn Profile --dest=my_portal --singular
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
This creates `/profile` (singular) instead of `/profiles/:id`.
|
|
306
|
+
|
|
307
|
+
### Changes Not Saving
|
|
308
|
+
|
|
309
|
+
Check your policy's `permitted_attributes_for_update`:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
def permitted_attributes_for_update
|
|
313
|
+
[:bio, :avatar, :timezone, :notifications_enabled]
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Next Steps
|
|
318
|
+
|
|
319
|
+
- [Authentication](/guides/authentication) - Set up Rodauth features
|
|
320
|
+
- [Custom Actions](/guides/custom-actions) - Add profile actions
|
|
321
|
+
- [Views](/guides/views) - Customize the profile page
|
|
322
|
+
- [Authorization](/guides/authorization) - Configure profile policies
|
|
@@ -303,9 +303,46 @@ end
|
|
|
303
303
|
|
|
304
304
|
Controllers automatically:
|
|
305
305
|
- Scope all queries to the entity
|
|
306
|
-
- Exclude entity field from forms
|
|
306
|
+
- Exclude entity field from forms (detected by association class)
|
|
307
|
+
- Inject entity value on create/update
|
|
307
308
|
- Provide `current_scoped_entity` method
|
|
308
309
|
|
|
310
|
+
### Association Detection
|
|
311
|
+
|
|
312
|
+
Plutonium auto-detects which `belongs_to` association points to the scoped entity class. This works even when `param_key` differs from the association name:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
# Portal config
|
|
316
|
+
scope_to_entity Competition::Team, param_key: :team
|
|
317
|
+
|
|
318
|
+
# Model (association name differs from param_key)
|
|
319
|
+
class Match < ApplicationRecord
|
|
320
|
+
belongs_to :competition_team # Plutonium finds this by class
|
|
321
|
+
end
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Multiple Associations to Same Class
|
|
325
|
+
|
|
326
|
+
If a model has multiple associations to the scoped entity class, Plutonium raises an error:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
Match has multiple associations to Competition::Team: home_team, away_team.
|
|
330
|
+
Plutonium cannot auto-detect which one to use for entity scoping.
|
|
331
|
+
Override `scoped_entity_association` in your controller to specify the association.
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Resolve by overriding `scoped_entity_association`:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
class MatchesController < ::ResourceController
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def scoped_entity_association
|
|
341
|
+
:home_team # Return the association name as a symbol
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
309
346
|
## Specifying Resource Class
|
|
310
347
|
|
|
311
348
|
The resource class is inferred from the controller name. Override if needed:
|
|
@@ -153,6 +153,22 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
|
153
153
|
end
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
## Form Configuration
|
|
157
|
+
|
|
158
|
+
Control form behavior:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
class PostDefinition < Plutonium::Resource::Definition
|
|
162
|
+
# Controls "Save and add another" / "Update and continue editing" buttons
|
|
163
|
+
# nil (default) = auto-detect (hidden for singular resources, shown for plural)
|
|
164
|
+
# true = always show
|
|
165
|
+
# false = always hide
|
|
166
|
+
submit_and_continue false
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Singular resources (e.g., `resource :profile` routes or `has_one` nested) auto-hide the secondary submit button since creating "another" doesn't make sense.
|
|
171
|
+
|
|
156
172
|
## Custom Page Classes
|
|
157
173
|
|
|
158
174
|
Override default page components:
|
|
@@ -301,6 +301,21 @@ When `post_type` changes, the form re-renders via Turbo and shows/hides conditio
|
|
|
301
301
|
|
|
302
302
|
## Form Actions
|
|
303
303
|
|
|
304
|
+
### Submit and Continue Button
|
|
305
|
+
|
|
306
|
+
Forms include a secondary button ("Create and add another" for new records, "Update and continue editing" for existing). Control this in your definition:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
class PostDefinition < ResourceDefinition
|
|
310
|
+
# nil (default) = auto-detect (hidden for singular resources, shown for plural)
|
|
311
|
+
# true = always show
|
|
312
|
+
# false = always hide
|
|
313
|
+
submit_and_continue false
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Singular resources (e.g., `resource :profile` routes or `has_one` nested) auto-hide this button.
|
|
318
|
+
|
|
304
319
|
### Default Actions
|
|
305
320
|
|
|
306
321
|
```ruby
|
|
@@ -267,13 +267,35 @@ Available kit methods:
|
|
|
267
267
|
| `TabList(items:)` | Tab navigation |
|
|
268
268
|
| `EmptyCard(message)` | Empty state card |
|
|
269
269
|
| `ActionButton(action, url:)` | Action button |
|
|
270
|
-
| `DynaFrameHost(
|
|
270
|
+
| `DynaFrameHost(src:, loading:)` | Lazy-loading turbo frame |
|
|
271
|
+
| `DynaFrameContent(content) { \|frame\| ... }` | Frame-aware content wrapper |
|
|
271
272
|
| `TableSearchBar()` | Search bar for tables |
|
|
272
273
|
| `TableScopesBar()` | Scope tabs for tables |
|
|
273
274
|
| `TableInfo(pagy)` | Pagination info |
|
|
274
275
|
| `TablePagination(pagy)` | Pagination links |
|
|
275
276
|
| `FrameNavigatorPanel(title:, src:, panel_id:)` | Frame navigation panel |
|
|
276
277
|
|
|
278
|
+
## DynaFrameContent Pattern
|
|
279
|
+
|
|
280
|
+
`DynaFrameContent` enables frame-aware rendering. For turbo-frame requests, only the content is rendered inside the frame. For regular requests, the full page renders with header/footer.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# How Page::Base uses DynaFrameContent
|
|
284
|
+
def view_template(&block)
|
|
285
|
+
DynaFrameContent(page_content(block)) do |frame|
|
|
286
|
+
render_header # Skipped for frame requests
|
|
287
|
+
frame.render_content # Always rendered (wrapped in turbo-frame for frame requests)
|
|
288
|
+
render_footer # Skipped for frame requests
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
This pattern means:
|
|
294
|
+
- **Regular page load**: Full page with header, content, footer
|
|
295
|
+
- **Turbo-frame request**: Only `<turbo-frame id="...">` with content inside
|
|
296
|
+
|
|
297
|
+
All pages automatically inherit this behavior - modals, lazy-loaded frames, and navigation all work correctly without special handling.
|
|
298
|
+
|
|
277
299
|
## Custom Components
|
|
278
300
|
|
|
279
301
|
### Creating a Phlex Component
|
|
@@ -17,6 +17,7 @@ module Pu
|
|
|
17
17
|
configure_application
|
|
18
18
|
replace_build_script
|
|
19
19
|
import_styles
|
|
20
|
+
fix_layout_stylesheet_tag
|
|
20
21
|
rescue => e
|
|
21
22
|
exception "#{self.class} failed:", e
|
|
22
23
|
end
|
|
@@ -72,6 +73,17 @@ module Pu
|
|
|
72
73
|
prepend_to_file "app/assets/stylesheets/application.tailwind.css",
|
|
73
74
|
"@import \"gem:plutonium/src/css/plutonium.css\";\n\n"
|
|
74
75
|
end
|
|
76
|
+
|
|
77
|
+
# Rails 8 generates layouts with `stylesheet_link_tag :app` which includes all
|
|
78
|
+
# stylesheets in the asset paths. With cssbundling-rails, this causes both the
|
|
79
|
+
# built CSS and the source CSS to be served, leading to errors when the browser
|
|
80
|
+
# tries to load the unprocessed source file containing PostCSS directives.
|
|
81
|
+
def fix_layout_stylesheet_tag
|
|
82
|
+
layout_file = "app/views/layouts/application.html.erb"
|
|
83
|
+
return unless File.exist?(layout_file)
|
|
84
|
+
|
|
85
|
+
gsub_file layout_file, /stylesheet_link_tag\s+:app\b/, 'stylesheet_link_tag "application"'
|
|
86
|
+
end
|
|
75
87
|
end
|
|
76
88
|
end
|
|
77
89
|
end
|
|
@@ -20,4 +20,15 @@ class ResourceController < PlutoniumController
|
|
|
20
20
|
|
|
21
21
|
helper_method :logout_url
|
|
22
22
|
<%- end -%>
|
|
23
|
+
<%- if !ApplicationController.new.respond_to?(:profile_url, true) -%>
|
|
24
|
+
|
|
25
|
+
private def profile_url
|
|
26
|
+
# return a profile url to render a profile link in the user menu
|
|
27
|
+
# e.g. rodauth.change_password_path or your custom profile_path
|
|
28
|
+
end
|
|
29
|
+
helper_method :profile_url
|
|
30
|
+
<%- elsif !ApplicationController._helper_methods.include?(:profile_url) -%>
|
|
31
|
+
|
|
32
|
+
helper_method :profile_url
|
|
33
|
+
<%- end -%>
|
|
23
34
|
end
|