plutonium 0.45.2 → 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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +146 -0
  3. data/.claude/skills/plutonium-assets/SKILL.md +248 -157
  4. data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
  5. data/.claude/skills/plutonium-controller/SKILL.md +9 -2
  6. data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
  7. data/.claude/skills/plutonium-definition/SKILL.md +521 -7
  8. data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
  9. data/.claude/skills/plutonium-forms/SKILL.md +8 -1
  10. data/.claude/skills/plutonium-installation/SKILL.md +25 -2
  11. data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
  12. data/.claude/skills/plutonium-invites/SKILL.md +11 -7
  13. data/.claude/skills/plutonium-model/SKILL.md +50 -50
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
  15. data/.claude/skills/plutonium-package/SKILL.md +8 -1
  16. data/.claude/skills/plutonium-policy/SKILL.md +69 -78
  17. data/.claude/skills/plutonium-portal/SKILL.md +26 -70
  18. data/.claude/skills/plutonium-views/SKILL.md +9 -2
  19. data/CHANGELOG.md +33 -0
  20. data/app/assets/plutonium.css +1 -1
  21. data/app/views/rodauth/_login_form.html.erb +0 -3
  22. data/app/views/rodauth/confirm_password.html.erb +0 -4
  23. data/app/views/rodauth/create_account.html.erb +0 -3
  24. data/app/views/rodauth/logout.html.erb +0 -3
  25. data/config/initializers/pagy.rb +1 -1
  26. data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
  27. data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
  28. data/gemfiles/rails_7.gemfile.lock +1 -1
  29. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  31. data/lib/generators/pu/core/update/update_generator.rb +8 -0
  32. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
  33. data/lib/generators/pu/invites/install_generator.rb +8 -1
  34. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
  35. data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
  36. data/lib/generators/pu/profile/conn_generator.rb +9 -12
  37. data/lib/generators/pu/profile/install_generator.rb +5 -2
  38. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  39. data/lib/generators/pu/saas/portal_generator.rb +4 -9
  40. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
  41. data/lib/plutonium/engine.rb +18 -5
  42. data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
  43. data/lib/plutonium/version.rb +1 -1
  44. data/package.json +1 -1
  45. metadata +7 -8
  46. data/.claude/skills/plutonium/skill.md +0 -130
  47. data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
  48. data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
  49. data/.claude/skills/plutonium-profile/SKILL.md +0 -276
  50. data/.claude/skills/plutonium-theming/SKILL.md +0 -424
@@ -1,10 +1,50 @@
1
1
  ---
2
2
  name: plutonium-definition
3
- description: Use when configuring resource definitions - field types, inputs, displays, columns, conditional rendering, nested inputs, or definition structure
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
- - For custom class names, use `class_name:` in both model and `using:` in definition
491
- - `update_only: true` hides the Add button
492
- - Limit is enforced in UI (Add button hidden when reached)
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