plutonium 0.45.3 → 0.46.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 +146 -0
- data/.claude/skills/plutonium-assets/SKILL.md +248 -157
- data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
- data/.claude/skills/plutonium-controller/SKILL.md +9 -2
- data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
- data/.claude/skills/plutonium-definition/SKILL.md +521 -7
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
- data/.claude/skills/plutonium-forms/SKILL.md +8 -1
- data/.claude/skills/plutonium-installation/SKILL.md +25 -2
- data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
- data/.claude/skills/plutonium-invites/SKILL.md +11 -7
- data/.claude/skills/plutonium-model/SKILL.md +50 -50
- data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
- data/.claude/skills/plutonium-package/SKILL.md +8 -1
- data/.claude/skills/plutonium-policy/SKILL.md +69 -78
- data/.claude/skills/plutonium-portal/SKILL.md +26 -70
- data/.claude/skills/plutonium-views/SKILL.md +9 -2
- data/CHANGELOG.md +28 -0
- data/app/assets/plutonium.css +1 -1
- data/app/views/rodauth/_login_form.html.erb +0 -3
- data/app/views/rodauth/confirm_password.html.erb +0 -4
- data/app/views/rodauth/create_account.html.erb +0 -3
- data/app/views/rodauth/logout.html.erb +0 -3
- data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
- data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -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/update/update_generator.rb +8 -0
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
- data/lib/generators/pu/invites/install_generator.rb +8 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
- data/lib/generators/pu/profile/conn_generator.rb +9 -12
- data/lib/generators/pu/profile/install_generator.rb +5 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/portal_generator.rb +4 -9
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
- data/lib/plutonium/engine.rb +18 -5
- data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +7 -8
- data/.claude/skills/plutonium/skill.md +0 -130
- data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
- data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
- data/.claude/skills/plutonium-profile/SKILL.md +0 -276
- data/.claude/skills/plutonium-theming/SKILL.md +0 -424
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plutonium-definition
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use BEFORE editing a resource definition — adding fields, inputs, displays, columns, search, filters, scopes, custom actions, or bulk actions.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Plutonium Resource Definitions
|
|
7
7
|
|
|
8
|
+
## 🚨 Critical (read first)
|
|
9
|
+
- **Use generators.** `pu:res:scaffold` creates the base definition, `pu:res:conn` creates portal-specific overrides, `pu:field:input` / `pu:field:renderer` create custom components.
|
|
10
|
+
- **Let auto-detection work.** Only declare fields/inputs/displays/columns when overriding defaults — Plutonium reads your model.
|
|
11
|
+
- **Authorization goes in policies, not `condition:` procs.** Use `condition` for UI state logic (e.g. "only show `published_at` when published"). Use **policy** `permitted_attributes_for_*` for "who can see this field".
|
|
12
|
+
- **Custom actions require a policy method** — `action :publish` requires `def publish?` on the policy.
|
|
13
|
+
- **Related skills:** `plutonium-policy` (permitted attributes, action permissions), `plutonium-interaction` (business logic for actions), `plutonium-forms` (custom form templates), `plutonium-views` (custom page classes).
|
|
14
|
+
|
|
15
|
+
## Quick checklist
|
|
16
|
+
|
|
17
|
+
Editing / extending a definition:
|
|
18
|
+
|
|
19
|
+
1. Confirm the definition was generated by `pu:res:scaffold` or `pu:res:conn`.
|
|
20
|
+
2. Let auto-detection handle fields; only `field`/`input`/`display`/`column` when overriding defaults.
|
|
21
|
+
3. For search/filter/sort, add `search`, `filter :name, with: :text/:select/:date/...`, `scope :name`, `sort :name`.
|
|
22
|
+
4. For custom actions, define an interaction class and register it: `action :name, interaction: MyInteraction`.
|
|
23
|
+
5. For bulk actions, make the interaction accept `attribute :resources` (plural).
|
|
24
|
+
6. Add policy methods matching each custom action (`def publish?`, `def archive?`, etc.).
|
|
25
|
+
7. For per-portal overrides, edit `packages/<portal>/app/definitions/<portal>/<resource>_definition.rb`.
|
|
26
|
+
8. Test the index page, show page, new/edit form, and any actions in the browser.
|
|
27
|
+
|
|
28
|
+
## Contents
|
|
29
|
+
|
|
30
|
+
This skill covers three concerns. Jump to the section you need:
|
|
31
|
+
|
|
32
|
+
**Fields, inputs, displays, columns** (this top section)
|
|
33
|
+
- [Definition Structure](#definition-structure) · [Definition Hierarchy](#definition-hierarchy) · [Core Methods](#core-methods)
|
|
34
|
+
- [Available Field Types](#available-field-types) · [Field Options](#field-options) · [Select/Choices](#selectchoices)
|
|
35
|
+
- [Conditional Rendering](#conditional-rendering) · [Dynamic Forms (pre_submit)](#dynamic-forms-pre_submit)
|
|
36
|
+
- [Custom Rendering](#custom-rendering) · [Column Options](#column-options) · [Nested Inputs](#nested-inputs)
|
|
37
|
+
- [File Uploads](#file-uploads) · [Runtime Customization Hooks](#runtime-customization-hooks)
|
|
38
|
+
- [Form Configuration](#form-configuration) · [Page Customization](#page-customization)
|
|
39
|
+
|
|
40
|
+
**[Query: Search, Filters, Scopes, Sorting](#query-search-filters-scopes-sorting)**
|
|
41
|
+
- Search · Filters (text, boolean, date, date_range, select, association) · Custom Filters · Scopes · Sorting · URL Parameters
|
|
42
|
+
|
|
43
|
+
**[Actions: Custom and Bulk](#actions-custom-and-bulk)**
|
|
44
|
+
- Action Types · Simple Actions · Interactive Actions · Action Options
|
|
45
|
+
- Creating an Interaction · Bulk Actions · Resource Actions
|
|
46
|
+
- Interaction Responses · Default CRUD Actions · Authorization · Immediate vs Form Actions
|
|
47
|
+
|
|
8
48
|
**Definitions are generated automatically** - never create them manually:
|
|
9
49
|
- `rails g pu:res:scaffold` creates the base definition
|
|
10
50
|
- `rails g pu:res:conn` creates portal-specific definitions
|
|
@@ -486,10 +526,12 @@ end
|
|
|
486
526
|
|
|
487
527
|
### Gotchas
|
|
488
528
|
|
|
489
|
-
- Model must have `accepts_nested_attributes_for
|
|
490
|
-
-
|
|
491
|
-
- `
|
|
492
|
-
-
|
|
529
|
+
- Model must have `accepts_nested_attributes_for`.
|
|
530
|
+
- The `belongs_to` on the child model **must** declare `inverse_of: :parent_assoc`. Without it, in-memory validation of nested children fails with "Parent must exist" because the parent isn't yet saved.
|
|
531
|
+
- **Don't put `*_attributes` hashes in the policy's `permitted_attributes_for_*`.** Plutonium extracts nested params via the form definition (`build_form(...).extract_input(...)`), not the policy. Hash entries like `{variants_attributes: [:id, :name, :_destroy]}` get rendered as literal text inputs. The policy should permit just the association name (e.g. `:variants`); the `nested_input :variants` declaration in the definition handles the rest.
|
|
532
|
+
- For custom class names, use `class_name:` in both model and `using:` in definition.
|
|
533
|
+
- `update_only: true` hides the Add button.
|
|
534
|
+
- Limit is enforced in UI (Add button hidden when reached).
|
|
493
535
|
|
|
494
536
|
## File Uploads
|
|
495
537
|
|
|
@@ -616,9 +658,481 @@ end
|
|
|
616
658
|
4. **Use policies for authorization** - Not `condition` procs
|
|
617
659
|
5. **Group related declarations** - Use comments to organize sections
|
|
618
660
|
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
# Query: Search, Filters, Scopes, Sorting
|
|
664
|
+
|
|
665
|
+
Configure how users can search, filter, and sort resource collections.
|
|
666
|
+
|
|
667
|
+
### Query Overview
|
|
668
|
+
|
|
669
|
+
```ruby
|
|
670
|
+
class PostDefinition < ResourceDefinition
|
|
671
|
+
search do |scope, query|
|
|
672
|
+
scope.where("title ILIKE ?", "%#{query}%")
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
filter :title, with: :text, predicate: :contains
|
|
676
|
+
filter :status, with: :select, choices: %w[draft published archived]
|
|
677
|
+
filter :published, with: :boolean
|
|
678
|
+
filter :created_at, with: :date_range
|
|
679
|
+
filter :category, with: :association
|
|
680
|
+
|
|
681
|
+
scope :published
|
|
682
|
+
scope :draft
|
|
683
|
+
default_scope :published
|
|
684
|
+
|
|
685
|
+
sort :title
|
|
686
|
+
sort :created_at
|
|
687
|
+
default_sort :created_at, :desc
|
|
688
|
+
end
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Search
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
# Single field
|
|
695
|
+
search do |scope, query|
|
|
696
|
+
scope.where("title ILIKE ?", "%#{query}%")
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# Multiple fields
|
|
700
|
+
search do |scope, query|
|
|
701
|
+
scope.where(
|
|
702
|
+
"title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
|
|
703
|
+
q: "%#{query}%"
|
|
704
|
+
)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# With associations
|
|
708
|
+
search do |scope, query|
|
|
709
|
+
scope.joins(:author).where(
|
|
710
|
+
"posts.title ILIKE :q OR users.name ILIKE :q",
|
|
711
|
+
q: "%#{query}%"
|
|
712
|
+
).distinct
|
|
713
|
+
end
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Filters
|
|
717
|
+
|
|
718
|
+
Plutonium provides **6 built-in filter types**. Use shorthand symbols or full class names.
|
|
719
|
+
|
|
720
|
+
#### Text Filter
|
|
721
|
+
|
|
722
|
+
```ruby
|
|
723
|
+
filter :title, with: :text, predicate: :contains
|
|
724
|
+
filter :status, with: :text, predicate: :eq
|
|
725
|
+
filter :title, with: Plutonium::Query::Filters::Text, predicate: :contains
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Predicates:** `:eq`, `:not_eq`, `:contains`, `:not_contains`, `:starts_with`, `:ends_with`, `:matches`, `:not_matches`
|
|
729
|
+
|
|
730
|
+
#### Boolean Filter
|
|
731
|
+
|
|
732
|
+
```ruby
|
|
733
|
+
filter :active, with: :boolean
|
|
734
|
+
filter :published, with: :boolean, true_label: "Published", false_label: "Draft"
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
#### Date Filter
|
|
738
|
+
|
|
739
|
+
```ruby
|
|
740
|
+
filter :created_at, with: :date, predicate: :gteq
|
|
741
|
+
filter :due_date, with: :date, predicate: :lt
|
|
742
|
+
filter :published_at, with: :date, predicate: :eq
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Predicates:** `:eq`, `:not_eq`, `:lt`, `:lteq`, `:gt`, `:gteq`
|
|
746
|
+
|
|
747
|
+
#### Date Range Filter
|
|
748
|
+
|
|
749
|
+
```ruby
|
|
750
|
+
filter :created_at, with: :date_range
|
|
751
|
+
filter :published_at, with: :date_range,
|
|
752
|
+
from_label: "Published from",
|
|
753
|
+
to_label: "Published to"
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### Select Filter
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
filter :status, with: :select, choices: %w[draft published archived]
|
|
760
|
+
filter :category, with: :select, choices: -> { Category.pluck(:name) }
|
|
761
|
+
filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
#### Association Filter
|
|
765
|
+
|
|
766
|
+
```ruby
|
|
767
|
+
filter :category, with: :association
|
|
768
|
+
filter :author, with: :association, class_name: User
|
|
769
|
+
filter :tags, with: :association, class_name: Tag, multiple: true
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
#### Custom Filter Class
|
|
773
|
+
|
|
774
|
+
```ruby
|
|
775
|
+
class PriceRangeFilter < Plutonium::Query::Filter
|
|
776
|
+
def apply(scope, min: nil, max: nil)
|
|
777
|
+
scope = scope.where("price >= ?", min) if min.present?
|
|
778
|
+
scope = scope.where("price <= ?", max) if max.present?
|
|
779
|
+
scope
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def customize_inputs
|
|
783
|
+
input :min, as: :number
|
|
784
|
+
input :max, as: :number
|
|
785
|
+
field :min, placeholder: "Min price..."
|
|
786
|
+
field :max, placeholder: "Max price..."
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
filter :price, with: PriceRangeFilter
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Scopes
|
|
794
|
+
|
|
795
|
+
Scopes appear as quick filter buttons.
|
|
796
|
+
|
|
797
|
+
```ruby
|
|
798
|
+
class PostDefinition < ResourceDefinition
|
|
799
|
+
scope :published # Uses Post.published
|
|
800
|
+
scope :draft # Uses Post.draft
|
|
801
|
+
|
|
802
|
+
# Inline scopes
|
|
803
|
+
scope(:recent) { |scope| scope.where('created_at > ?', 1.week.ago) }
|
|
804
|
+
scope(:mine) { |scope| scope.where(author: current_user) }
|
|
805
|
+
|
|
806
|
+
default_scope :published # Applied by default
|
|
807
|
+
end
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
When a default scope is set:
|
|
811
|
+
- Applied on initial page load
|
|
812
|
+
- Default scope button is highlighted (not "All")
|
|
813
|
+
- Clicking "All" shows all records without any scope filter
|
|
814
|
+
|
|
815
|
+
### Sorting
|
|
816
|
+
|
|
817
|
+
```ruby
|
|
818
|
+
sort :title
|
|
819
|
+
sort :created_at
|
|
820
|
+
sorts :title, :created_at, :view_count # multiple at once
|
|
821
|
+
|
|
822
|
+
default_sort :created_at, :desc
|
|
823
|
+
default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### URL Parameters
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
/posts?q[search]=rails
|
|
830
|
+
/posts?q[title][query]=widget
|
|
831
|
+
/posts?q[status][value]=published
|
|
832
|
+
/posts?q[created_at][from]=2024-01-01&q[created_at][to]=2024-12-31
|
|
833
|
+
/posts?q[scope]=recent
|
|
834
|
+
/posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Filter Summary Table
|
|
838
|
+
|
|
839
|
+
| Type | Symbol | Input Params | Options |
|
|
840
|
+
|------|--------|--------------|---------|
|
|
841
|
+
| Text | `:text` | `query` | `predicate:` |
|
|
842
|
+
| Boolean | `:boolean` | `value` | `true_label:`, `false_label:` |
|
|
843
|
+
| Date | `:date` | `value` | `predicate:` |
|
|
844
|
+
| Date Range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
|
|
845
|
+
| Select | `:select` | `value` | `choices:`, `multiple:` |
|
|
846
|
+
| Association | `:association` | `value` | `class_name:`, `multiple:` |
|
|
847
|
+
|
|
848
|
+
### Query Performance Tips
|
|
849
|
+
|
|
850
|
+
1. Add indexes for filtered/sorted columns
|
|
851
|
+
2. Use `.distinct` when joining associations in search
|
|
852
|
+
3. Consider `pg_search` for complex full-text search
|
|
853
|
+
4. Limit search fields to indexed columns
|
|
854
|
+
5. Use scopes instead of filters for common queries
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
# Actions: Custom and Bulk
|
|
859
|
+
|
|
860
|
+
Actions define custom operations on resources. They can be simple (navigation) or interactive (with business logic via Interactions).
|
|
861
|
+
|
|
862
|
+
### Action Types
|
|
863
|
+
|
|
864
|
+
| Type | Shows In | Use Case |
|
|
865
|
+
|------|----------|----------|
|
|
866
|
+
| `resource_action` | Index page | Import, Export, Create |
|
|
867
|
+
| `record_action` | Show page | Edit, Delete, Archive |
|
|
868
|
+
| `collection_record_action` | Table rows | Quick actions per row |
|
|
869
|
+
| `bulk_action` | Selected records | Bulk operations |
|
|
870
|
+
|
|
871
|
+
### Simple Actions (Navigation)
|
|
872
|
+
|
|
873
|
+
Simple actions link to existing routes. **The target route must already exist.**
|
|
874
|
+
|
|
875
|
+
```ruby
|
|
876
|
+
class PostDefinition < ResourceDefinition
|
|
877
|
+
# Link to external URL
|
|
878
|
+
action :documentation,
|
|
879
|
+
label: "Documentation",
|
|
880
|
+
route_options: {url: "https://docs.example.com"},
|
|
881
|
+
icon: Phlex::TablerIcons::Book,
|
|
882
|
+
resource_action: true
|
|
883
|
+
|
|
884
|
+
# Link to custom controller action
|
|
885
|
+
action :reports,
|
|
886
|
+
route_options: {action: :reports},
|
|
887
|
+
icon: Phlex::TablerIcons::ChartBar,
|
|
888
|
+
resource_action: true
|
|
889
|
+
end
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Important:** When adding custom routes for actions, always use the `as:` option to name them:
|
|
893
|
+
|
|
894
|
+
```ruby
|
|
895
|
+
resources :posts do
|
|
896
|
+
collection do
|
|
897
|
+
get :reports, as: :reports # Named route required!
|
|
898
|
+
end
|
|
899
|
+
end
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
**Note:** For custom operations with business logic, use **Interactive Actions** with an Interaction class instead.
|
|
903
|
+
|
|
904
|
+
### Interactive Actions (with Interaction)
|
|
905
|
+
|
|
906
|
+
```ruby
|
|
907
|
+
class PostDefinition < ResourceDefinition
|
|
908
|
+
action :publish,
|
|
909
|
+
interaction: PublishInteraction,
|
|
910
|
+
icon: Phlex::TablerIcons::Send
|
|
911
|
+
|
|
912
|
+
action :archive,
|
|
913
|
+
interaction: ArchiveInteraction,
|
|
914
|
+
color: :danger,
|
|
915
|
+
category: :danger,
|
|
916
|
+
position: 1000,
|
|
917
|
+
confirmation: "Are you sure?"
|
|
918
|
+
end
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Action Options
|
|
922
|
+
|
|
923
|
+
```ruby
|
|
924
|
+
action :name,
|
|
925
|
+
# Display
|
|
926
|
+
label: "Custom Label",
|
|
927
|
+
description: "What it does",
|
|
928
|
+
icon: Phlex::TablerIcons::Star,
|
|
929
|
+
color: :danger, # :primary, :secondary, :danger
|
|
930
|
+
|
|
931
|
+
# Visibility
|
|
932
|
+
resource_action: true,
|
|
933
|
+
record_action: true,
|
|
934
|
+
collection_record_action: true,
|
|
935
|
+
bulk_action: true,
|
|
936
|
+
|
|
937
|
+
# Grouping
|
|
938
|
+
category: :primary, # :primary, :secondary, :danger
|
|
939
|
+
position: 50,
|
|
940
|
+
|
|
941
|
+
# Behavior
|
|
942
|
+
confirmation: "Are you sure?",
|
|
943
|
+
turbo_frame: "_top",
|
|
944
|
+
route_options: {action: :foo}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Creating an Interaction
|
|
948
|
+
|
|
949
|
+
#### Basic Structure
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
# app/interactions/resource_interaction.rb (generated during install)
|
|
953
|
+
class ResourceInteraction < Plutonium::Resource::Interaction
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
# app/interactions/archive_interaction.rb
|
|
957
|
+
class ArchiveInteraction < ResourceInteraction
|
|
958
|
+
presents label: "Archive",
|
|
959
|
+
icon: Phlex::TablerIcons::Archive,
|
|
960
|
+
description: "Archive this record"
|
|
961
|
+
|
|
962
|
+
attribute :resource
|
|
963
|
+
|
|
964
|
+
def execute
|
|
965
|
+
resource.archived!
|
|
966
|
+
succeed(resource).with_message("Record archived successfully.")
|
|
967
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
968
|
+
failed(e.record.errors)
|
|
969
|
+
rescue => error
|
|
970
|
+
failed("Archive failed. Please try again.")
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
#### With Additional Inputs
|
|
976
|
+
|
|
977
|
+
```ruby
|
|
978
|
+
class Company::InviteUserInteraction < Plutonium::Resource::Interaction
|
|
979
|
+
presents label: "Invite User", icon: Phlex::TablerIcons::Mail
|
|
980
|
+
|
|
981
|
+
attribute :resource
|
|
982
|
+
attribute :email
|
|
983
|
+
attribute :role
|
|
984
|
+
|
|
985
|
+
input :email, as: :email, hint: "User's email address"
|
|
986
|
+
input :role, as: :select, choices: %w[admin member viewer]
|
|
987
|
+
|
|
988
|
+
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
989
|
+
validates :role, presence: true, inclusion: {in: %w[admin member viewer]}
|
|
990
|
+
|
|
991
|
+
def execute
|
|
992
|
+
UserInvite.create!(
|
|
993
|
+
company: resource,
|
|
994
|
+
email: email,
|
|
995
|
+
role: role,
|
|
996
|
+
invited_by: current_user
|
|
997
|
+
)
|
|
998
|
+
succeed(resource).with_message("Invitation sent to #{email}.")
|
|
999
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
1000
|
+
failed(e.record.errors)
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
#### Bulk Action (Multiple Records)
|
|
1006
|
+
|
|
1007
|
+
Bulk actions operate on multiple selected records at once. The resource table automatically shows selection checkboxes and a bulk actions toolbar.
|
|
1008
|
+
|
|
1009
|
+
```ruby
|
|
1010
|
+
# 1. Create the interaction (note: plural `resources` attribute)
|
|
1011
|
+
class BulkArchiveInteraction < Plutonium::Resource::Interaction
|
|
1012
|
+
presents label: "Archive Selected", icon: Phlex::TablerIcons::Archive
|
|
1013
|
+
|
|
1014
|
+
attribute :resources # Array of records (plural)
|
|
1015
|
+
|
|
1016
|
+
def execute
|
|
1017
|
+
count = 0
|
|
1018
|
+
resources.each do |record|
|
|
1019
|
+
record.archived!
|
|
1020
|
+
count += 1
|
|
1021
|
+
end
|
|
1022
|
+
succeed(resources).with_message("#{count} records archived.")
|
|
1023
|
+
rescue => error
|
|
1024
|
+
failed("Bulk archive failed: #{error.message}")
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
# 2. Register the action in the definition
|
|
1029
|
+
class PostDefinition < ResourceDefinition
|
|
1030
|
+
action :bulk_archive, interaction: BulkArchiveInteraction
|
|
1031
|
+
# bulk_action: true is automatically inferred from `resources` attribute
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
# 3. Add policy method
|
|
1035
|
+
class PostPolicy < ResourcePolicy
|
|
1036
|
+
def bulk_archive?
|
|
1037
|
+
create?
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
**Authorization for bulk actions:**
|
|
1043
|
+
- Policy method is checked **per record** — fails the entire request if any record is not authorized
|
|
1044
|
+
- Records are fetched via `current_authorized_scope`
|
|
1045
|
+
- The UI only shows action buttons that **all** selected records support
|
|
1046
|
+
|
|
1047
|
+
#### Resource Action (No Record)
|
|
1048
|
+
|
|
1049
|
+
```ruby
|
|
1050
|
+
class ImportInteraction < Plutonium::Resource::Interaction
|
|
1051
|
+
presents label: "Import CSV", icon: Phlex::TablerIcons::Upload
|
|
1052
|
+
|
|
1053
|
+
# No :resource or :resources attribute = resource action
|
|
1054
|
+
attribute :file
|
|
1055
|
+
|
|
1056
|
+
input :file, as: :file
|
|
1057
|
+
validates :file, presence: true
|
|
1058
|
+
|
|
1059
|
+
def execute
|
|
1060
|
+
succeed(nil).with_message("Import completed.")
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
### Interaction Responses
|
|
1066
|
+
|
|
1067
|
+
```ruby
|
|
1068
|
+
def execute
|
|
1069
|
+
succeed(resource).with_message("Done!")
|
|
1070
|
+
succeed(resource)
|
|
1071
|
+
.with_redirect_response(custom_dashboard_path)
|
|
1072
|
+
.with_message("Redirecting...")
|
|
1073
|
+
failed(resource.errors)
|
|
1074
|
+
failed("Something went wrong")
|
|
1075
|
+
failed("Invalid value", :email)
|
|
1076
|
+
end
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
**Note:** Redirect is automatic on success. Only use `with_redirect_response` for a different destination.
|
|
1080
|
+
|
|
1081
|
+
### Default CRUD Actions
|
|
1082
|
+
|
|
1083
|
+
```ruby
|
|
1084
|
+
action :new, resource_action: true, position: 10
|
|
1085
|
+
action :show, collection_record_action: true, position: 10
|
|
1086
|
+
action :edit, record_action: true, position: 20
|
|
1087
|
+
action :destroy, record_action: true, position: 100, category: :danger
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Action Authorization
|
|
1091
|
+
|
|
1092
|
+
```ruby
|
|
1093
|
+
class PostPolicy < ResourcePolicy
|
|
1094
|
+
def publish?
|
|
1095
|
+
user.admin? || record.author == user
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def archive?
|
|
1099
|
+
user.admin?
|
|
1100
|
+
end
|
|
1101
|
+
end
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
The action only appears if the policy method returns `true`.
|
|
1105
|
+
|
|
1106
|
+
### Immediate vs Form Actions
|
|
1107
|
+
|
|
1108
|
+
**Immediate** — executes without showing a form (when interaction has no extra inputs beyond `resource`):
|
|
1109
|
+
|
|
1110
|
+
```ruby
|
|
1111
|
+
class ArchiveInteraction < Plutonium::Resource::Interaction
|
|
1112
|
+
attribute :resource
|
|
1113
|
+
def execute
|
|
1114
|
+
resource.archived!
|
|
1115
|
+
succeed(resource)
|
|
1116
|
+
end
|
|
1117
|
+
end
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**Form** — shows a form first (when interaction has additional inputs):
|
|
1121
|
+
|
|
1122
|
+
```ruby
|
|
1123
|
+
class InviteUserInteraction < Plutonium::Resource::Interaction
|
|
1124
|
+
attribute :resource
|
|
1125
|
+
attribute :email
|
|
1126
|
+
input :email
|
|
1127
|
+
# Has inputs = shows form first
|
|
1128
|
+
end
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
---
|
|
1132
|
+
|
|
619
1133
|
## Related Skills
|
|
620
1134
|
|
|
621
|
-
- `plutonium-definition-actions` - Actions and interactions
|
|
622
|
-
- `plutonium-definition-query` - Search, filters, scopes, sorting
|
|
623
1135
|
- `plutonium-views` - Custom page, form, display, and table classes
|
|
624
1136
|
- `plutonium-forms` - Custom form templates and field builders
|
|
1137
|
+
- `plutonium-interaction` - Writing interaction classes
|
|
1138
|
+
- `plutonium-policy` - Controlling action access
|