plutonium 0.34.1 → 0.35.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/skill.md +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- 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/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -277,6 +277,10 @@ end
|
|
|
277
277
|
|
|
278
278
|
### Bulk Action
|
|
279
279
|
|
|
280
|
+
Bulk actions operate on multiple selected records. When registered, the resource table automatically shows:
|
|
281
|
+
- **Selection checkboxes** in each row
|
|
282
|
+
- **Bulk actions toolbar** that appears when records are selected
|
|
283
|
+
|
|
280
284
|
```ruby
|
|
281
285
|
class BulkArchiveInteraction < Plutonium::Resource::Interaction
|
|
282
286
|
presents label: "Archive Selected", icon: Phlex::TablerIcons::Archive
|
|
@@ -296,6 +300,33 @@ class BulkArchiveInteraction < Plutonium::Resource::Interaction
|
|
|
296
300
|
end
|
|
297
301
|
```
|
|
298
302
|
|
|
303
|
+
Register in definition:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
class PostDefinition < ResourceDefinition
|
|
307
|
+
action :bulk_archive, interaction: BulkArchiveInteraction
|
|
308
|
+
# bulk_action: true is automatically inferred from `resources` attribute
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Add the policy method (checked per-record):
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class PostPolicy < ResourcePolicy
|
|
316
|
+
def bulk_archive?
|
|
317
|
+
# Can use record attributes - checked for each selected record
|
|
318
|
+
user.admin? || record.author == user
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
::: tip Bulk Action Authorization
|
|
324
|
+
Bulk actions use **per-record authorization**:
|
|
325
|
+
- Policy method (e.g., `bulk_archive?`) is checked for **each selected record** - you can use `record` attributes
|
|
326
|
+
- Backend rejects the entire request if any record fails authorization
|
|
327
|
+
- UI only shows actions that **all** selected records support
|
|
328
|
+
:::
|
|
329
|
+
|
|
299
330
|
### Resource Action (No Record)
|
|
300
331
|
|
|
301
332
|
```ruby
|
|
@@ -322,12 +353,12 @@ end
|
|
|
322
353
|
|
|
323
354
|
```ruby
|
|
324
355
|
def execute
|
|
325
|
-
# Success with message
|
|
356
|
+
# Success with message (redirects to resource automatically)
|
|
326
357
|
succeed(resource).with_message("Done!")
|
|
327
358
|
|
|
328
|
-
# Success with redirect
|
|
359
|
+
# Success with custom redirect (only if different from default)
|
|
329
360
|
succeed(resource)
|
|
330
|
-
.with_redirect_response(
|
|
361
|
+
.with_redirect_response(custom_dashboard_path)
|
|
331
362
|
.with_message("Redirecting...")
|
|
332
363
|
|
|
333
364
|
# Failure with field errors
|
|
@@ -338,6 +369,10 @@ def execute
|
|
|
338
369
|
end
|
|
339
370
|
```
|
|
340
371
|
|
|
372
|
+
::: tip Automatic Redirect
|
|
373
|
+
Redirect is automatic on success. You can use `with_redirect_response` for a different destination.
|
|
374
|
+
:::
|
|
375
|
+
|
|
341
376
|
## Inherited Actions
|
|
342
377
|
|
|
343
378
|
Actions defined in `ResourceDefinition` are inherited by all definitions:
|
|
@@ -16,7 +16,7 @@ Complete reference for field configuration in definitions.
|
|
|
16
16
|
```ruby
|
|
17
17
|
class PostDefinition < Plutonium::Resource::Definition
|
|
18
18
|
# field - changes type everywhere
|
|
19
|
-
field :content, as: :
|
|
19
|
+
field :content, as: :markdown
|
|
20
20
|
|
|
21
21
|
# input - form-specific
|
|
22
22
|
input :title,
|
|
@@ -43,7 +43,7 @@ end
|
|
|
43
43
|
| Category | Types |
|
|
44
44
|
|----------|-------|
|
|
45
45
|
| **Text** | `:string`, `:text`, `:email`, `:url`, `:tel`, `:password` |
|
|
46
|
-
| **Rich Text** | `:
|
|
46
|
+
| **Rich Text** | `:markdown` (uses EasyMDE editor) |
|
|
47
47
|
| **Numeric** | `:number`, `:integer`, `:decimal`, `:range` |
|
|
48
48
|
| **Boolean** | `:boolean` |
|
|
49
49
|
| **Date/Time** | `:date`, `:time`, `:datetime` |
|
|
@@ -302,7 +302,7 @@ input :documents, as: :uppy,
|
|
|
302
302
|
### Rich Text Content
|
|
303
303
|
|
|
304
304
|
```ruby
|
|
305
|
-
field :content, as: :
|
|
305
|
+
field :content, as: :markdown # Form: EasyMDE editor
|
|
306
306
|
display :content, as: :markdown # Show: rendered markdown
|
|
307
307
|
```
|
|
308
308
|
|
|
@@ -10,7 +10,7 @@ Definitions control how resources render - which fields appear in forms, how tab
|
|
|
10
10
|
class PostDefinition < Plutonium::Resource::Definition
|
|
11
11
|
# Field configuration
|
|
12
12
|
field :title
|
|
13
|
-
field :body, as: :
|
|
13
|
+
field :body, as: :markdown
|
|
14
14
|
|
|
15
15
|
# Form-specific
|
|
16
16
|
input :title, placeholder: "Enter title"
|
|
@@ -81,7 +81,7 @@ Override selectively:
|
|
|
81
81
|
```ruby
|
|
82
82
|
class PostDefinition < Plutonium::Resource::Definition
|
|
83
83
|
# Override just body field
|
|
84
|
-
field :body, as: :
|
|
84
|
+
field :body, as: :markdown
|
|
85
85
|
|
|
86
86
|
# Other fields still auto-detected
|
|
87
87
|
end
|
|
@@ -182,7 +182,6 @@ packages/admin_portal/
|
|
|
182
182
|
│ ├── controllers/admin_portal/
|
|
183
183
|
│ │ ├── concerns/controller.rb
|
|
184
184
|
│ │ ├── plutonium_controller.rb
|
|
185
|
-
│ │ ├── resource_controller.rb
|
|
186
185
|
│ │ └── dashboard_controller.rb
|
|
187
186
|
│ ├── definitions/admin_portal/
|
|
188
187
|
│ ├── policies/admin_portal/
|
|
@@ -156,6 +156,10 @@ rescue ActiveRecord::RecordInvalid => e
|
|
|
156
156
|
end
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
::: warning Handle RecordInvalid
|
|
160
|
+
`ActiveRecord::RecordInvalid` is **not** rescued automatically. Always rescue it when using bang methods (`create!`, `update!`, `save!`).
|
|
161
|
+
:::
|
|
162
|
+
|
|
159
163
|
## Constructor
|
|
160
164
|
|
|
161
165
|
Interactions require `view_context:` and accept attributes as keyword arguments:
|
|
@@ -193,10 +197,14 @@ outcome = interaction.call
|
|
|
193
197
|
|
|
194
198
|
## Success Outcomes
|
|
195
199
|
|
|
200
|
+
::: tip Automatic Redirect
|
|
201
|
+
On success, the controller automatically redirects to the resource.
|
|
202
|
+
:::
|
|
203
|
+
|
|
196
204
|
### Basic Success
|
|
197
205
|
|
|
198
206
|
```ruby
|
|
199
|
-
succeed(resource)
|
|
207
|
+
succeed(resource) # Redirects to resource automatically
|
|
200
208
|
```
|
|
201
209
|
|
|
202
210
|
### With Message
|
|
@@ -206,11 +214,12 @@ succeed(resource).with_message("Post published!")
|
|
|
206
214
|
succeed(resource).with_message("Warning: limited visibility", :alert)
|
|
207
215
|
```
|
|
208
216
|
|
|
209
|
-
### With Redirect
|
|
217
|
+
### With Custom Redirect
|
|
218
|
+
|
|
219
|
+
Useful when redirecting somewhere other than the default:
|
|
210
220
|
|
|
211
221
|
```ruby
|
|
212
|
-
succeed(resource).with_redirect_response(
|
|
213
|
-
succeed(resource).with_redirect_response(resource, status: :see_other)
|
|
222
|
+
succeed(resource).with_redirect_response(custom_dashboard_path)
|
|
214
223
|
```
|
|
215
224
|
|
|
216
225
|
### With File Download
|
|
@@ -327,9 +336,7 @@ class Company::InviteUserInteraction < Plutonium::Resource::Interaction
|
|
|
327
336
|
)
|
|
328
337
|
UserInviteMailer.invitation(invite).deliver_later
|
|
329
338
|
|
|
330
|
-
succeed(resource)
|
|
331
|
-
.with_message("Invitation sent to #{email}")
|
|
332
|
-
.with_redirect_response(resource)
|
|
339
|
+
succeed(resource).with_message("Invitation sent to #{email}")
|
|
333
340
|
rescue ActiveRecord::RecordInvalid => e
|
|
334
341
|
failed(e.record.errors)
|
|
335
342
|
end
|
|
@@ -434,9 +441,6 @@ end
|
|
|
434
441
|
1. **Keep interactions focused** - One action per interaction
|
|
435
442
|
2. **Use validations** - Validate all inputs before execution
|
|
436
443
|
3. **Handle errors gracefully** - Rescue exceptions and return `failed()`
|
|
437
|
-
4. **Return meaningful messages** - Help users understand what happened
|
|
438
|
-
5. **Use `and_then` for chains** - Compose complex workflows from simple interactions
|
|
439
|
-
6. **Declare attributes explicitly** - Always declare `resource` or `resources` attributes
|
|
440
444
|
|
|
441
445
|
## Related
|
|
442
446
|
|
|
@@ -174,40 +174,27 @@ end
|
|
|
174
174
|
|
|
175
175
|
## Controllers
|
|
176
176
|
|
|
177
|
-
### Base Controller
|
|
178
|
-
|
|
179
|
-
```ruby
|
|
180
|
-
# packages/admin_portal/app/controllers/admin_portal/resource_controller.rb
|
|
181
|
-
module AdminPortal
|
|
182
|
-
class ResourceController < Plutonium::Portal::ResourceController
|
|
183
|
-
layout "admin_portal/application"
|
|
184
|
-
|
|
185
|
-
private
|
|
186
|
-
|
|
187
|
-
def after_sign_in_path
|
|
188
|
-
admin_root_path
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
```
|
|
193
|
-
|
|
194
177
|
### Resource Controllers
|
|
195
178
|
|
|
179
|
+
Portal-specific controllers inherit from the feature package's controller and include the portal's controller concern:
|
|
180
|
+
|
|
196
181
|
```ruby
|
|
197
182
|
# packages/admin_portal/app/controllers/admin_portal/posts_controller.rb
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
private
|
|
183
|
+
class AdminPortal::PostsController < ::PostsController
|
|
184
|
+
include AdminPortal::Concerns::Controller
|
|
201
185
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def build_resource
|
|
189
|
+
super.tap do |post|
|
|
190
|
+
post.user = current_user
|
|
206
191
|
end
|
|
207
192
|
end
|
|
208
193
|
end
|
|
209
194
|
```
|
|
210
195
|
|
|
196
|
+
Controllers are auto-created if not defined. When accessing `AdminPortal::PostsController`, Plutonium will dynamically create it by inheriting from `::PostsController` and including `AdminPortal::Concerns::Controller`.
|
|
197
|
+
|
|
211
198
|
## Portal-Specific Overrides
|
|
212
199
|
|
|
213
200
|
### Definitions
|
|
@@ -298,7 +285,7 @@ end
|
|
|
298
285
|
To allow unauthenticated access to specific actions:
|
|
299
286
|
|
|
300
287
|
```ruby
|
|
301
|
-
class AdminPortal::PagesController < AdminPortal::
|
|
288
|
+
class AdminPortal::PagesController < AdminPortal::PlutoniumController
|
|
302
289
|
skip_before_action :authenticate, only: [:health]
|
|
303
290
|
|
|
304
291
|
def health
|
|
@@ -314,7 +301,7 @@ end
|
|
|
314
301
|
```ruby
|
|
315
302
|
# packages/admin_portal/app/controllers/admin_portal/dashboard_controller.rb
|
|
316
303
|
module AdminPortal
|
|
317
|
-
class DashboardController <
|
|
304
|
+
class DashboardController < PlutoniumController
|
|
318
305
|
def index
|
|
319
306
|
@stats = {
|
|
320
307
|
posts: Post.count,
|
|
@@ -377,6 +364,5 @@ end
|
|
|
377
364
|
|
|
378
365
|
## Related
|
|
379
366
|
|
|
380
|
-
- [Packages and Portals Concept](/concepts/packages-portals)
|
|
381
367
|
- [Authentication Guide](/guides/authentication)
|
|
382
368
|
- [Multi-tenancy Guide](/guides/multi-tenancy)
|
|
@@ -28,8 +28,6 @@ module Pu
|
|
|
28
28
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/concerns/controller.rb"
|
|
29
29
|
template "app/controllers/plutonium_controller.rb",
|
|
30
30
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/plutonium_controller.rb"
|
|
31
|
-
template "app/controllers/resource_controller.rb",
|
|
32
|
-
"packages/#{package_namespace}/app/controllers/#{package_namespace}/resource_controller.rb"
|
|
33
31
|
|
|
34
32
|
template "app/controllers/dashboard_controller.rb",
|
|
35
33
|
"packages/#{package_namespace}/app/controllers/#{package_namespace}/dashboard_controller.rb"
|
|
@@ -1,81 +1,37 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<div class="space-y-6">
|
|
2
|
+
<div>
|
|
3
|
+
<h1 class="text-2xl font-bold text-[var(--pu-text)]">Dashboard</h1>
|
|
4
|
+
<p class="text-[var(--pu-text-muted)]">Manage your resources</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
5
7
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
6
8
|
<% registered_resources.each do |resource| %>
|
|
7
9
|
<% next unless allowed_to? :index?, resource %>
|
|
8
|
-
<div
|
|
9
|
-
class="
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<svg
|
|
23
|
-
class="h-6 w-6 text-primary-600 dark:text-primary-400"
|
|
24
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
-
fill="none"
|
|
26
|
-
viewBox="0 0 24 24"
|
|
27
|
-
stroke="currentColor"
|
|
28
|
-
>
|
|
29
|
-
<path
|
|
30
|
-
stroke-linecap="round"
|
|
31
|
-
stroke-linejoin="round"
|
|
32
|
-
stroke-width="2"
|
|
33
|
-
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
|
34
|
-
/>
|
|
35
|
-
</svg>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="ml-4 flex-1">
|
|
38
|
-
<h3
|
|
39
|
-
class="
|
|
40
|
-
text-lg font-semibold text-gray-900 dark:text-white flex items-center
|
|
41
|
-
"
|
|
42
|
-
>
|
|
43
|
-
<%= resource.model_name.human.titleize.pluralize %>
|
|
44
|
-
<span
|
|
45
|
-
class="
|
|
46
|
-
ml-2 px-2 py-0.5 text-xs rounded-full bg-primary-100 text-primary-800
|
|
47
|
-
dark:bg-primary-900 dark:text-primary-200
|
|
48
|
-
"
|
|
49
|
-
><%= authorized_resource_scope(resource).count %></span>
|
|
50
|
-
</h3>
|
|
10
|
+
<div class="pu-card group hover:shadow-[var(--pu-shadow-lg)] transition-shadow">
|
|
11
|
+
<div class="pu-card-body space-y-4">
|
|
12
|
+
<div class="flex items-start justify-between">
|
|
13
|
+
<div class="flex items-center gap-3">
|
|
14
|
+
<div class="flex items-center justify-center h-10 w-10 rounded-[var(--pu-radius-md)] bg-primary-100 dark:bg-primary-900/30">
|
|
15
|
+
<svg class="h-5 w-5 text-primary-600 dark:text-primary-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
17
|
+
</svg>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<h3 class="font-semibold text-[var(--pu-text)]"><%= resource.model_name.human.titleize.pluralize %></h3>
|
|
21
|
+
<p class="text-sm text-[var(--pu-text-muted)]"><%= authorized_resource_scope(resource).count %> records</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
51
24
|
</div>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
>Quick Actions</div>
|
|
59
|
-
<div class="mt-2 flex space-x-2">
|
|
60
|
-
<%# Check for resource creation permission %>
|
|
25
|
+
|
|
26
|
+
<div class="flex items-center gap-2 pt-2 border-t border-[var(--pu-border-muted)]">
|
|
27
|
+
<a href="<%= resource_url_for(resource, parent: nil) %>" class="pu-btn pu-btn-sm pu-btn-primary">
|
|
28
|
+
View All
|
|
29
|
+
</a>
|
|
61
30
|
<% if allowed_to? :new?, resource %>
|
|
62
|
-
<a
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
px-2 py-1 text-xs font-medium rounded bg-white dark:bg-gray-700 text-gray-700
|
|
66
|
-
dark:text-gray-300 border border-gray-200 dark:border-gray-600 hover:bg-gray-50
|
|
67
|
-
dark:hover:bg-gray-600 transition-colors
|
|
68
|
-
"
|
|
69
|
-
>Add New</a>
|
|
31
|
+
<a href="<%= resource_url_for(resource, parent: nil, action: :new) %>" class="pu-btn pu-btn-sm pu-btn-outline">
|
|
32
|
+
Add New
|
|
33
|
+
</a>
|
|
70
34
|
<% end %>
|
|
71
|
-
<a
|
|
72
|
-
href="<%= resource_url_for(resource, parent: nil) %>"
|
|
73
|
-
class="
|
|
74
|
-
px-2 py-1 text-xs font-medium rounded bg-white dark:bg-gray-700 text-gray-700
|
|
75
|
-
dark:text-gray-300 border border-gray-200 dark:border-gray-600 hover:bg-gray-50
|
|
76
|
-
dark:hover:bg-gray-600 transition-colors
|
|
77
|
-
"
|
|
78
|
-
>View All</a>
|
|
79
35
|
</div>
|
|
80
36
|
</div>
|
|
81
37
|
</div>
|
|
@@ -79,7 +79,7 @@ module Plutonium
|
|
|
79
79
|
if attribute_names.include?(:resource)
|
|
80
80
|
:interactive_record_action
|
|
81
81
|
elsif attribute_names.include?(:resources)
|
|
82
|
-
:
|
|
82
|
+
:interactive_bulk_action
|
|
83
83
|
else
|
|
84
84
|
:interactive_resource_action
|
|
85
85
|
end
|
|
@@ -99,7 +99,7 @@ module Plutonium
|
|
|
99
99
|
# @return [Hash] The action options
|
|
100
100
|
def self.determine_action_options(action_type)
|
|
101
101
|
{
|
|
102
|
-
bulk_action: action_type == :
|
|
102
|
+
bulk_action: action_type == :interactive_bulk_action,
|
|
103
103
|
record_action: action_type == :interactive_record_action,
|
|
104
104
|
collection_record_action: action_type == :interactive_record_action,
|
|
105
105
|
resource_action: action_type == :interactive_resource_action
|
|
@@ -101,7 +101,8 @@ module Plutonium
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Preserve the request format unless explicitly specified
|
|
104
|
-
|
|
104
|
+
# Don't preserve turbo_stream as it's for streaming updates, not page navigation
|
|
105
|
+
if !url_args.key?(:format) && request.present? && request.format.present? && !request.format.symbol.in?([:html, :turbo_stream])
|
|
105
106
|
url_args[:format] = request.format.symbol
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -37,11 +37,11 @@ module Plutonium
|
|
|
37
37
|
icon: Phlex::TablerIcons::Plus, position: 10)
|
|
38
38
|
|
|
39
39
|
action(:show, route_options: {action: :show},
|
|
40
|
-
collection_record_action: true,
|
|
40
|
+
collection_record_action: true, category: :primary,
|
|
41
41
|
icon: Phlex::TablerIcons::Eye, position: 10)
|
|
42
42
|
|
|
43
43
|
action(:edit, route_options: {action: :edit},
|
|
44
|
-
record_action: true, collection_record_action: true,
|
|
44
|
+
record_action: true, collection_record_action: true, category: :primary,
|
|
45
45
|
icon: Phlex::TablerIcons::Edit, position: 20)
|
|
46
46
|
|
|
47
47
|
action(:destroy, route_options: {method: :delete},
|
|
@@ -4,6 +4,9 @@ module Plutonium
|
|
|
4
4
|
module Lib
|
|
5
5
|
class DeepFreezer
|
|
6
6
|
def self.freeze(object)
|
|
7
|
+
# Never freeze Class or Module objects - they have mutable state that Rails needs
|
|
8
|
+
return object if object.is_a?(Class) || object.is_a?(Module)
|
|
9
|
+
|
|
7
10
|
# Recursive calling #deep_freeze for enumerable objects.
|
|
8
11
|
if object.respond_to? :each
|
|
9
12
|
if object.instance_of?(Hash)
|
|
@@ -13,13 +16,6 @@ module Plutonium
|
|
|
13
16
|
end
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
# # Freezing of all instance variable values.
|
|
17
|
-
# object.instance_variables.each do |var|
|
|
18
|
-
# frozen_val = instance_variable_get(var)
|
|
19
|
-
# frozen_val.deep_freeze
|
|
20
|
-
# instance_variable_set(var, frozen_val)
|
|
21
|
-
# end
|
|
22
|
-
|
|
23
19
|
if object.frozen?
|
|
24
20
|
object
|
|
25
21
|
else
|
|
@@ -3,6 +3,20 @@ module Plutonium
|
|
|
3
3
|
class Filter < Base
|
|
4
4
|
attr_reader :key
|
|
5
5
|
|
|
6
|
+
class << self
|
|
7
|
+
# Lookup a filter class by type symbol or return the class if already a Filter
|
|
8
|
+
# @param type [Symbol, Class] The type symbol (e.g., :text, :select) or a Filter class
|
|
9
|
+
# @return [Class] The filter class
|
|
10
|
+
def lookup(type)
|
|
11
|
+
return type if type.is_a?(Class) && type < Filter
|
|
12
|
+
|
|
13
|
+
class_name = "Plutonium::Query::Filters::#{type.to_s.classify}"
|
|
14
|
+
class_name.constantize
|
|
15
|
+
rescue NameError
|
|
16
|
+
raise ArgumentError, "Unknown filter type: #{type}. Expected #{class_name} to exist."
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
6
20
|
def initialize(key:)
|
|
7
21
|
super()
|
|
8
22
|
@key = key
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# Select filter for association records
|
|
5
|
+
#
|
|
6
|
+
# @example Basic - infers Category class from :category key
|
|
7
|
+
# filter :category, with: :association
|
|
8
|
+
#
|
|
9
|
+
# @example With explicit class
|
|
10
|
+
# filter :author, with: :association, class_name: User
|
|
11
|
+
#
|
|
12
|
+
# @example With multiple selection
|
|
13
|
+
# filter :tags, with: :association, class_name: Tag, multiple: true
|
|
14
|
+
#
|
|
15
|
+
class Association < Filter
|
|
16
|
+
def initialize(class_name: nil, multiple: false, **)
|
|
17
|
+
super(**)
|
|
18
|
+
@association_class = class_name
|
|
19
|
+
@multiple = multiple
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply(scope, value:)
|
|
23
|
+
return scope if value.blank?
|
|
24
|
+
|
|
25
|
+
foreign_key = :"#{key}_id"
|
|
26
|
+
if @multiple && value.is_a?(Array)
|
|
27
|
+
scope.where(foreign_key => value.reject(&:blank?))
|
|
28
|
+
else
|
|
29
|
+
scope.where(foreign_key => value)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def customize_inputs
|
|
34
|
+
input :value,
|
|
35
|
+
as: :resource_select,
|
|
36
|
+
association_class: association_class,
|
|
37
|
+
multiple: @multiple,
|
|
38
|
+
include_blank: @multiple ? false : "All"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def association_class
|
|
44
|
+
@association_class || key.to_s.classify.constantize
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# Boolean filter for true/false columns
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# filter :active, with: :boolean
|
|
8
|
+
#
|
|
9
|
+
# @example With custom labels
|
|
10
|
+
# filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
|
|
11
|
+
#
|
|
12
|
+
class Boolean < Filter
|
|
13
|
+
def initialize(true_label: "Yes", false_label: "No", **)
|
|
14
|
+
super(**)
|
|
15
|
+
@true_label = true_label
|
|
16
|
+
@false_label = false_label
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def apply(scope, value:)
|
|
20
|
+
return scope if value.blank?
|
|
21
|
+
|
|
22
|
+
bool_value = ActiveModel::Type::Boolean.new.cast(value)
|
|
23
|
+
scope.where(key => bool_value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def customize_inputs
|
|
27
|
+
input :value,
|
|
28
|
+
as: :select,
|
|
29
|
+
choices: [[@true_label, "true"], [@false_label, "false"]],
|
|
30
|
+
include_blank: "All"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|