plutonium 0.33.1 → 0.34.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/# Plutonium: The pre-alpha demo.md +4 -2
  3. data/.claude/skills/assets/SKILL.md +416 -0
  4. data/.claude/skills/connect-resource/SKILL.md +112 -0
  5. data/.claude/skills/controller/SKILL.md +302 -0
  6. data/.claude/skills/create-resource/SKILL.md +240 -0
  7. data/.claude/skills/definition/SKILL.md +218 -0
  8. data/.claude/skills/definition-actions/SKILL.md +386 -0
  9. data/.claude/skills/definition-fields/SKILL.md +474 -0
  10. data/.claude/skills/definition-query/SKILL.md +334 -0
  11. data/.claude/skills/forms/SKILL.md +439 -0
  12. data/.claude/skills/installation/SKILL.md +300 -0
  13. data/.claude/skills/interaction/SKILL.md +382 -0
  14. data/.claude/skills/model/SKILL.md +267 -0
  15. data/.claude/skills/model-features/SKILL.md +286 -0
  16. data/.claude/skills/nested-resources/SKILL.md +274 -0
  17. data/.claude/skills/package/SKILL.md +191 -0
  18. data/.claude/skills/policy/SKILL.md +352 -0
  19. data/.claude/skills/portal/SKILL.md +400 -0
  20. data/.claude/skills/resource/SKILL.md +281 -0
  21. data/.claude/skills/rodauth/SKILL.md +452 -0
  22. data/.claude/skills/views/SKILL.md +563 -0
  23. data/Appraisals +46 -4
  24. data/CHANGELOG.md +32 -1
  25. data/app/assets/plutonium.css +2 -2
  26. data/config/brakeman.ignore +239 -0
  27. data/config/initializers/action_policy.rb +1 -1
  28. data/docs/.vitepress/config.ts +132 -47
  29. data/docs/concepts/architecture.md +226 -0
  30. data/docs/concepts/auto-detection.md +254 -0
  31. data/docs/concepts/index.md +61 -0
  32. data/docs/concepts/packages-portals.md +304 -0
  33. data/docs/concepts/resources.md +224 -0
  34. data/docs/cookbook/blog.md +412 -0
  35. data/docs/cookbook/index.md +289 -0
  36. data/docs/cookbook/saas.md +481 -0
  37. data/docs/getting-started/index.md +56 -0
  38. data/docs/getting-started/installation.md +146 -0
  39. data/docs/getting-started/tutorial/01-setup.md +118 -0
  40. data/docs/getting-started/tutorial/02-first-resource.md +180 -0
  41. data/docs/getting-started/tutorial/03-authentication.md +246 -0
  42. data/docs/getting-started/tutorial/04-authorization.md +170 -0
  43. data/docs/getting-started/tutorial/05-custom-actions.md +202 -0
  44. data/docs/getting-started/tutorial/06-nested-resources.md +147 -0
  45. data/docs/getting-started/tutorial/07-customizing-ui.md +254 -0
  46. data/docs/getting-started/tutorial/index.md +64 -0
  47. data/docs/guides/adding-resources.md +420 -0
  48. data/docs/guides/authentication.md +551 -0
  49. data/docs/guides/authorization.md +468 -0
  50. data/docs/guides/creating-packages.md +380 -0
  51. data/docs/guides/custom-actions.md +523 -0
  52. data/docs/guides/index.md +45 -0
  53. data/docs/guides/multi-tenancy.md +302 -0
  54. data/docs/guides/nested-resources.md +411 -0
  55. data/docs/guides/search-filtering.md +266 -0
  56. data/docs/guides/theming.md +321 -0
  57. data/docs/index.md +67 -26
  58. data/docs/public/CLAUDE.md +64 -21
  59. data/docs/reference/assets/index.md +496 -0
  60. data/docs/reference/controller/index.md +363 -0
  61. data/docs/reference/definition/actions.md +400 -0
  62. data/docs/reference/definition/fields.md +350 -0
  63. data/docs/reference/definition/index.md +252 -0
  64. data/docs/reference/definition/query.md +342 -0
  65. data/docs/reference/generators/index.md +469 -0
  66. data/docs/reference/index.md +49 -0
  67. data/docs/reference/interaction/index.md +445 -0
  68. data/docs/reference/model/features.md +248 -0
  69. data/docs/reference/model/index.md +219 -0
  70. data/docs/reference/policy/index.md +385 -0
  71. data/docs/reference/portal/index.md +382 -0
  72. data/docs/reference/views/forms.md +396 -0
  73. data/docs/reference/views/index.md +479 -0
  74. data/gemfiles/rails_7.gemfile +9 -2
  75. data/gemfiles/rails_7.gemfile.lock +146 -111
  76. data/gemfiles/rails_8.0.gemfile +20 -0
  77. data/gemfiles/rails_8.0.gemfile.lock +417 -0
  78. data/gemfiles/rails_8.1.gemfile +20 -0
  79. data/gemfiles/rails_8.1.gemfile.lock +419 -0
  80. data/lib/generators/pu/gem/dotenv/templates/.env +2 -0
  81. data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -1
  82. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +13 -16
  83. data/lib/generators/pu/pkg/portal/USAGE +65 -0
  84. data/lib/generators/pu/pkg/portal/portal_generator.rb +22 -9
  85. data/lib/generators/pu/res/conn/USAGE +71 -0
  86. data/lib/generators/pu/res/model/USAGE +106 -110
  87. data/lib/generators/pu/res/model/templates/model.rb.tt +6 -2
  88. data/lib/generators/pu/res/scaffold/USAGE +85 -0
  89. data/lib/generators/pu/rodauth/install_generator.rb +2 -6
  90. data/lib/generators/pu/rodauth/templates/config/initializers/url_options.rb +17 -0
  91. data/lib/generators/pu/skills/sync/USAGE +14 -0
  92. data/lib/generators/pu/skills/sync/sync_generator.rb +66 -0
  93. data/lib/plutonium/action_policy/sti_policy_lookup.rb +1 -1
  94. data/lib/plutonium/core/controller.rb +2 -2
  95. data/lib/plutonium/interaction/base.rb +1 -0
  96. data/lib/plutonium/package/engine.rb +2 -2
  97. data/lib/plutonium/query/adhoc_block.rb +6 -2
  98. data/lib/plutonium/query/model_scope.rb +1 -1
  99. data/lib/plutonium/railtie.rb +4 -0
  100. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  101. data/lib/plutonium/resource/query_object.rb +38 -8
  102. data/lib/plutonium/ui/table/components/scopes_bar.rb +39 -34
  103. data/lib/plutonium/version.rb +1 -1
  104. data/lib/tasks/release.rake +19 -4
  105. data/package.json +1 -1
  106. metadata +76 -39
  107. data/brakeman.ignore +0 -28
  108. data/docs/api-examples.md +0 -49
  109. data/docs/guide/claude-code-guide.md +0 -74
  110. data/docs/guide/deep-dive/authorization.md +0 -189
  111. data/docs/guide/deep-dive/multitenancy.md +0 -256
  112. data/docs/guide/deep-dive/resources.md +0 -390
  113. data/docs/guide/getting-started/01-installation.md +0 -165
  114. data/docs/guide/index.md +0 -28
  115. data/docs/guide/introduction/01-what-is-plutonium.md +0 -211
  116. data/docs/guide/introduction/02-core-concepts.md +0 -440
  117. data/docs/guide/tutorial/01-project-setup.md +0 -75
  118. data/docs/guide/tutorial/02-creating-a-feature-package.md +0 -45
  119. data/docs/guide/tutorial/03-defining-resources.md +0 -90
  120. data/docs/guide/tutorial/04-creating-a-portal.md +0 -101
  121. data/docs/guide/tutorial/05-customizing-the-ui.md +0 -128
  122. data/docs/guide/tutorial/06-adding-custom-actions.md +0 -101
  123. data/docs/guide/tutorial/07-implementing-authorization.md +0 -90
  124. data/docs/markdown-examples.md +0 -85
  125. data/docs/modules/action.md +0 -244
  126. data/docs/modules/authentication.md +0 -236
  127. data/docs/modules/configuration.md +0 -599
  128. data/docs/modules/controller.md +0 -443
  129. data/docs/modules/core.md +0 -316
  130. data/docs/modules/definition.md +0 -1308
  131. data/docs/modules/display.md +0 -759
  132. data/docs/modules/form.md +0 -495
  133. data/docs/modules/generator.md +0 -400
  134. data/docs/modules/index.md +0 -167
  135. data/docs/modules/interaction.md +0 -642
  136. data/docs/modules/package.md +0 -151
  137. data/docs/modules/policy.md +0 -176
  138. data/docs/modules/portal.md +0 -710
  139. data/docs/modules/query.md +0 -297
  140. data/docs/modules/resource_record.md +0 -618
  141. data/docs/modules/routing.md +0 -690
  142. data/docs/modules/table.md +0 -301
  143. data/docs/modules/ui.md +0 -631
@@ -1,113 +1,109 @@
1
1
  Description:
2
- Generates a new model. Pass the model name, either CamelCased or
3
- under_scored, and an optional list of attribute pairs as arguments.
4
-
5
- Attribute pairs are field:type arguments specifying the
6
- model's attributes. Timestamps are added by default, so you don't have to
7
- specify them by hand as 'created_at:datetime updated_at:datetime'.
8
-
9
- As a special case, specifying 'password:digest' will generate a
10
- password_digest field of string type, and configure your generated model and
11
- tests for use with Active Model has_secure_password (assuming the default ORM
12
- and test framework are being used).
13
-
14
- You don't have to think up every attribute up front, but it helps to
15
- sketch out a few so you can start working with the model immediately.
16
-
17
- This generator invokes your configured ORM and test framework, which
18
- defaults to Active Record and TestUnit.
19
-
20
- Finally, if --parent option is given, it's used as superclass of the
21
- created model. This allows you create Single Table Inheritance models.
22
-
23
- If you pass a namespaced model name (e.g. admin/account or Admin::Account)
24
- then the generator will create a module with a table_name_prefix method
25
- to prefix the model's table name with the module name (e.g. admin_accounts)
26
-
27
- Available field types:
28
-
29
- Just after the field name you can specify a type like text or boolean.
30
- It will generate the column with the associated SQL type. For instance:
31
-
32
- `bin/rails generate model post title:string body:text`
33
-
34
- will generate a title column with a varchar type and a body column with a text
35
- type. If no type is specified the string type will be used by default.
36
- You can use the following types:
37
-
38
- integer
39
- primary_key
40
- decimal
41
- float
42
- boolean
43
- binary
44
- string
45
- text
46
- date
47
- time
48
- datetime
49
-
50
- You can also consider `references` as a kind of type. For instance, if you run:
51
-
52
- `bin/rails generate model photo title:string album:references`
53
-
54
- It will generate an `album_id` column. You should generate these kinds of fields when
55
- you will use a `belongs_to` association, for instance. `references` also supports
56
- polymorphism, you can enable polymorphism like this:
57
-
58
- `bin/rails generate model product supplier:references{polymorphic}`
59
-
60
- For integer, string, text and binary fields, an integer in curly braces will
61
- be set as the limit:
62
-
63
- `bin/rails generate model user pseudo:string{30}`
64
-
65
- For decimal, two integers separated by a comma in curly braces will be used
66
- for precision and scale:
67
-
68
- `bin/rails generate model product 'price:decimal{10,2}'`
69
-
70
- You can add a `:uniq` or `:index` suffix for unique or standard indexes
71
- respectively:
72
-
73
- `bin/rails generate model user pseudo:string:uniq`
74
- `bin/rails generate model user pseudo:string:index`
75
-
76
- You can combine any single curly brace option with the index options:
77
-
78
- `bin/rails generate model user username:string{30}:uniq`
79
- `bin/rails generate model product supplier:references{polymorphic}:index`
80
-
81
- If you require a `password_digest` string column for use with
82
- has_secure_password, you can specify `password:digest`:
83
-
84
- `bin/rails generate model user password:digest`
85
-
86
- If you require a `token` string column for use with
87
- has_secure_token, you can specify `auth_token:token`:
88
-
89
- `bin/rails generate model user auth_token:token`
2
+ Generates a Plutonium model with migration.
3
+
4
+ Usage:
5
+ rails g pu:res:model NAME [field:type field:type] [options]
6
+
7
+ Options:
8
+ --dest=DESTINATION Target destination (required to avoid interactive prompts)
9
+ Use 'main_app' for main application resources
10
+ Use 'package_name' for feature package resources
11
+
12
+ Field Syntax:
13
+ name:type Required field
14
+ 'name:type?' Nullable field (quote to prevent shell expansion)
15
+ name:type:index Field with index
16
+ name:type:uniq Field with unique index
17
+
18
+ Supported Types:
19
+ string VARCHAR
20
+ text TEXT
21
+ integer INTEGER
22
+ float FLOAT
23
+ decimal DECIMAL
24
+ boolean BOOLEAN
25
+ date DATE
26
+ datetime DATETIME
27
+ time TIME
28
+ binary BINARY
29
+ belongs_to Foreign key with association
30
+ references Same as belongs_to
31
+ rich_text ActionText (has_rich_text)
32
+ attachment Single file (has_one_attached)
33
+ attachments Multiple files (has_many_attached)
34
+ token Secure token (has_secure_token, auto unique index)
35
+ password_digest Secure password (has_secure_password)
36
+
37
+ Type Options (in curly braces):
38
+ 'field:decimal{10,2}' Decimal precision and scale
39
+ 'field:decimal?{10,2}' Nullable with precision
40
+ 'field:string{100}' String limit (optional)
41
+ 'ref:references{polymorphic}' Polymorphic association (optional)
42
+
43
+ Note: For default values, edit the migration manually after generation.
44
+
45
+ Nullable Fields:
46
+ Append ? to any type to make it nullable. Must quote to prevent shell expansion:
47
+
48
+ 'name:string?' null: true
49
+ 'score:integer?' null: true
50
+ 'parent:belongs_to?' null: true, optional: true in model
51
+ 'amount:decimal?{10,2}' null: true with precision
52
+
53
+ Index Types:
54
+ email:string:index Regular index
55
+ email:string:uniq Unique index
56
+ 'code:string{50}:uniq' With limit and unique index
90
57
 
91
58
  Examples:
92
- `bin/rails generate model account`
93
-
94
- For Active Record and TestUnit it creates:
95
-
96
- Model: app/models/account.rb
97
- Test: test/models/account_test.rb
98
- Fixtures: test/fixtures/accounts.yml
99
- Migration: db/migrate/XXX_create_accounts.rb
100
-
101
- `bin/rails generate model post title:string body:text published:boolean`
102
-
103
- Creates a Post model with a string title, text body, and published flag.
104
-
105
- `bin/rails generate model admin/account`
106
-
107
- For Active Record and TestUnit it creates:
108
-
109
- Module: app/models/admin.rb
110
- Model: app/models/admin/account.rb
111
- Test: test/models/admin/account_test.rb
112
- Fixtures: test/fixtures/admin/accounts.yml
113
- Migration: db/migrate/XXX_create_admin_accounts.rb
59
+ # Main app model
60
+ rails g pu:res:model post title:string 'body:text?' --dest=main_app
61
+
62
+ # Package model
63
+ rails g pu:res:model post title:string 'body:text?' --dest=blogging
64
+
65
+ # With associations
66
+ rails g pu:res:model article \
67
+ user:belongs_to \
68
+ 'category:belongs_to?' \
69
+ title:string \
70
+ slug:string:uniq \
71
+ --dest=blogging
72
+
73
+ # With decimal precision
74
+ rails g pu:res:model product \
75
+ name:string \
76
+ 'price:decimal{10,2}' \
77
+ 'cost:decimal?{10,2}' \
78
+ --dest=inventory
79
+
80
+ # Cross-package reference
81
+ rails g pu:res:model order_item \
82
+ order:belongs_to \
83
+ inventory/product:belongs_to \
84
+ quantity:integer \
85
+ 'unit_price:decimal{10,2}' \
86
+ --dest=sales
87
+
88
+ # With attachments and rich text
89
+ rails g pu:res:model article \
90
+ title:string \
91
+ content:rich_text \
92
+ cover:attachment \
93
+ gallery:attachments \
94
+ --dest=blogging
95
+
96
+ # With token
97
+ rails g pu:res:model api_key \
98
+ user:belongs_to \
99
+ access_token:token \
100
+ 'expires_at:datetime?' \
101
+ --dest=auth
102
+
103
+ Generated Files (main_app):
104
+ Model: app/models/[name].rb
105
+ Migration: db/migrate/xxx_create_[names].rb
106
+
107
+ Generated Files (package):
108
+ Model: app/models/[package]/[name].rb
109
+ Migration: db/migrate/xxx_create_[package]_[names].rb
@@ -6,13 +6,17 @@ require_relative "../<%= class_path.last.underscore %>"
6
6
  class <%= class_name %> < <%= [feature_package_name, "ResourceRecord"].join "::" %>
7
7
  # add concerns above.
8
8
 
9
+ # add constants above.
10
+
11
+ # add enums above.
12
+
9
13
  <% attributes.select(&:cents?).each do |attribute| -%>
10
14
  has_cents :<%= attribute.name %>
11
15
  <% end -%>
12
16
  # add model configurations above.
13
17
 
14
18
  <% attributes.select(&:reference?).each do |attribute| -%>
15
- belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %><%= ", class_name: \"#{attribute.attr_options[:class_name]}\"" if attribute.attr_options[:class_name] %>
19
+ belongs_to :<%= attribute.name %><%= ", optional: true" unless attribute.required? %><%= ", polymorphic: true" if attribute.polymorphic? %><%= ", class_name: \"#{attribute.attr_options[:class_name]}\"" if attribute.attr_options[:class_name] %>
16
20
  <% end -%>
17
21
  # add belongs_to associations above.
18
22
 
@@ -51,6 +55,6 @@ class <%= class_name %> < <%= [feature_package_name, "ResourceRecord"].join "::"
51
55
  <% end -%>
52
56
  # add misc attribute macros above.
53
57
 
54
- # add methods above.
58
+ # add methods above. add private methods below.
55
59
  end
56
60
  <% end -%>
@@ -0,0 +1,85 @@
1
+ Description:
2
+ Generates a complete Plutonium resource with model, migration, controller,
3
+ policy, and definition.
4
+
5
+ Usage:
6
+ rails g pu:res:scaffold NAME [field:type field:type] --dest=DESTINATION
7
+
8
+ Options:
9
+ --dest=DESTINATION Target destination (required to avoid interactive prompts)
10
+ Use 'main_app' for main application resources
11
+ Use 'package_name' for feature package resources
12
+ --no-model Skip model and migration generation
13
+
14
+ Field Syntax:
15
+ name:type Required field
16
+ 'name:type?' Nullable field (quote to prevent shell expansion)
17
+ name:type:index Field with index
18
+ name:type:uniq Field with unique index
19
+
20
+ Supported Types:
21
+ string, text, integer, float, decimal, boolean, date, datetime, time,
22
+ binary, belongs_to, references, rich_text, attachment, attachments, token
23
+
24
+ Type Options (decimal only):
25
+ 'field:decimal{10,2}' Precision and scale
26
+ 'field:decimal?{10,2}' Nullable with precision
27
+
28
+ Note: The {precision,scale} syntax only works for decimal types.
29
+ For default values on other types, edit the migration manually.
30
+
31
+ Examples:
32
+ # Main app resource
33
+ rails g pu:res:scaffold post title:string 'body:text?' --dest=main_app
34
+
35
+ # Package resource with associations and indexes
36
+ rails g pu:res:scaffold article \
37
+ user:belongs_to \
38
+ title:string \
39
+ slug:string:uniq \
40
+ 'content:text?' \
41
+ 'published_at:datetime?' \
42
+ --dest=blogging
43
+
44
+ # Nullable association
45
+ rails g pu:res:scaffold comment \
46
+ user:belongs_to \
47
+ 'parent:belongs_to?' \
48
+ body:text \
49
+ --dest=main_app
50
+
51
+ # With decimal precision
52
+ rails g pu:res:scaffold product \
53
+ name:string \
54
+ 'price:decimal{10,2}' \
55
+ 'weight:decimal?{8,3}' \
56
+ --dest=inventory
57
+
58
+ # Scaffold existing model (imports columns automatically)
59
+ rails g pu:res:scaffold ExistingModel --dest=main_app
60
+
61
+ # Cross-package reference
62
+ rails g pu:res:scaffold order \
63
+ customer:belongs_to \
64
+ inventory/product:belongs_to \
65
+ quantity:integer \
66
+ --dest=sales
67
+
68
+ Generated Files (main_app):
69
+ Model: app/models/[name].rb
70
+ Migration: db/migrate/xxx_create_[names].rb
71
+ Controller: app/controllers/[names]_controller.rb
72
+ Policy: app/policies/[name]_policy.rb
73
+ Definition: app/definitions/[name]_definition.rb
74
+
75
+ Generated Files (package):
76
+ Model: app/models/[package]/[name].rb
77
+ Migration: db/migrate/xxx_create_[package]_[names].rb
78
+ Controller: packages/[package]/app/controllers/[package]/[names]_controller.rb
79
+ Policy: packages/[package]/app/policies/[package]/[name]_policy.rb
80
+ Definition: packages/[package]/app/definitions/[package]/[name]_definition.rb
81
+
82
+ After Generation:
83
+ 1. Review migration (add cascade delete, defaults, composite indexes)
84
+ 2. Run: rails db:migrate
85
+ 3. Connect to portal: rails g pu:res:conn Post --dest=admin_portal
@@ -40,12 +40,8 @@ module Pu
40
40
  template "app/rodauth/rodauth_plugin.rb"
41
41
  end
42
42
 
43
- def add_dev_config
44
- return if Rails.version.to_f >= 8.0
45
-
46
- insert_into_file "config/environments/development.rb",
47
- "\n config.action_mailer.default_url_options = { host: '127.0.0.1', port: ENV.fetch('PORT', 3000) }\n",
48
- before: /^end/
43
+ def create_url_options_initializer
44
+ template "config/initializers/url_options.rb"
49
45
  end
50
46
 
51
47
  def create_install_migration
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.after_initialize do
4
+ default_url = ENV["RAILS_DEFAULT_URL"]
5
+ if default_url && Rails.application.config.action_mailer.default_url_options.blank?
6
+ uri = URI.parse(default_url)
7
+ Rails.application.config.action_mailer.default_url_options = {
8
+ host: uri.host,
9
+ port: uri.port,
10
+ protocol: uri.scheme
11
+ }
12
+ end
13
+
14
+ if Rails.application.routes.default_url_options.blank?
15
+ Rails.application.routes.default_url_options = Rails.application.config.action_mailer.default_url_options
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Syncs Claude Code skills from the Plutonium gem into your project's
3
+ .claude/skills/ directory.
4
+
5
+ This gives Claude Code access to Plutonium-specific skills for:
6
+ - Creating resources (scaffold generator)
7
+ - Connecting resources to portals
8
+ - Definition configuration (fields, actions, queries)
9
+
10
+ Example:
11
+ rails generate pu:skills:sync
12
+
13
+ This will copy all skills from the Plutonium gem to:
14
+ .claude/skills/
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/plutonium_generators"
4
+
5
+ module Pu
6
+ module Skills
7
+ class SyncGenerator < Rails::Generators::Base
8
+ include PlutoniumGenerators::Generator
9
+
10
+ desc "Sync Claude Code skills from Plutonium into your project"
11
+
12
+ def start
13
+ source_dir = Plutonium.root.join(".claude", "skills")
14
+ destination_dir = Rails.root.join(".claude", "skills")
15
+
16
+ unless File.directory?(source_dir)
17
+ say_status("error", "Source skills directory not found: #{source_dir}", :red)
18
+ return
19
+ end
20
+
21
+ # Create destination directory if it doesn't exist
22
+ FileUtils.mkdir_p(destination_dir)
23
+
24
+ # Get all skill directories
25
+ skill_dirs = Dir.children(source_dir).select { |f| File.directory?(source_dir.join(f)) }
26
+
27
+ if skill_dirs.empty?
28
+ say_status("info", "No skills found to sync", :yellow)
29
+ return
30
+ end
31
+
32
+ say_status("info", "Syncing #{skill_dirs.size} skills from Plutonium...", :blue)
33
+
34
+ skill_dirs.each do |skill_name|
35
+ sync_skill(source_dir, destination_dir, skill_name)
36
+ end
37
+
38
+ say_status("success", "Skills synced successfully!", :green)
39
+ rescue => e
40
+ exception "#{self.class} failed:", e
41
+ end
42
+
43
+ private
44
+
45
+ def sync_skill(source_dir, destination_dir, skill_name)
46
+ source_skill_dir = source_dir.join(skill_name)
47
+ dest_skill_dir = destination_dir.join(skill_name)
48
+
49
+ # Create skill directory
50
+ FileUtils.mkdir_p(dest_skill_dir)
51
+
52
+ # Copy all files in the skill directory
53
+ Dir.glob(source_skill_dir.join("*")).each do |source_file|
54
+ next unless File.file?(source_file)
55
+
56
+ filename = File.basename(source_file)
57
+ dest_file = dest_skill_dir.join(filename)
58
+
59
+ FileUtils.cp(source_file, dest_file)
60
+ end
61
+
62
+ say_status("synced", skill_name, :green)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -40,4 +40,4 @@ module Plutonium
40
40
  end
41
41
  end
42
42
  end
43
- end
43
+ end
@@ -8,8 +8,8 @@ module Plutonium
8
8
 
9
9
  included do
10
10
  add_flash_types :success, :warning, :error
11
-
12
- protect_from_forgery with: :null_session, if: -> { request.headers['Authorization'].present? }
11
+
12
+ protect_from_forgery with: :null_session, if: -> { request.headers["Authorization"].present? }
13
13
 
14
14
  before_action do
15
15
  next unless defined?(ActiveStorage)
@@ -27,6 +27,7 @@ module Plutonium
27
27
  include Plutonium::Definition::Presentable
28
28
  include Plutonium::Definition::NestedInputs
29
29
  include Plutonium::Interaction::NestedAttributes
30
+
30
31
  # include Plutonium::Interaction::Concerns::WorkflowDSL
31
32
 
32
33
  class Form < Plutonium::UI::Form::Interaction; end
@@ -10,17 +10,17 @@ module Plutonium
10
10
  config.before_configuration do
11
11
  # this touches the internals of rails, but I could not find a good way of doing this
12
12
  # we get the initializer instance and set the block property to a noop
13
- # There is no error handling, to ensure we know when it breaks.
14
13
  add_view_paths_initializer = Rails.application.initializers.find do |a|
15
14
  a.context_class == self && a.name.to_s == "add_view_paths"
16
15
  end
17
- add_view_paths_initializer.instance_variable_set(:@block, ->(app) {})
16
+ add_view_paths_initializer&.instance_variable_set(:@block, ->(app) {})
18
17
  end
19
18
 
20
19
  initializer :append_migrations do |app|
21
20
  unless app.root.to_s.match root.to_s
22
21
  config.paths["db/migrate"].expanded.each do |expanded_path|
23
22
  app.config.paths["db/migrate"] << expanded_path
23
+ ActiveRecord::Migrator.migrations_paths << expanded_path
24
24
  end
25
25
  end
26
26
  end
@@ -11,8 +11,12 @@ module Plutonium
11
11
  @body = body
12
12
  end
13
13
 
14
- def apply(scope, **)
15
- body.call(scope, **)
14
+ def apply(scope, context: nil, **)
15
+ if context
16
+ context.instance_exec(scope, **, &body)
17
+ else
18
+ body.call(scope, **)
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -11,7 +11,7 @@ module Plutonium
11
11
  @name = name
12
12
  end
13
13
 
14
- def apply(scope, **)
14
+ def apply(scope, context: nil, **)
15
15
  scope.public_send(name, **)
16
16
  end
17
17
  end
@@ -23,6 +23,10 @@ module Plutonium
23
23
  Rails.application.class.include Plutonium::Engine
24
24
  end
25
25
 
26
+ initializer "plutonium.rescue_responses" do |app|
27
+ app.config.action_dispatch.rescue_responses["ActionPolicy::Unauthorized"] = :forbidden
28
+ end
29
+
26
30
  initializer "plutonium.deprecator" do |app|
27
31
  app.deprecators[:plutonium] = Plutonium.deprecator
28
32
  end
@@ -17,7 +17,7 @@ module Plutonium
17
17
  .extract_input(params, view_context:)[:q]
18
18
 
19
19
  base_query = current_authorized_scope
20
- current_query_object.apply(base_query, query_params)
20
+ current_query_object.apply(base_query, query_params, context: self)
21
21
  end
22
22
  end
23
23
  end
@@ -1,7 +1,7 @@
1
1
  module Plutonium
2
2
  module Resource
3
3
  class QueryObject
4
- attr_reader :search_filter, :search_query
4
+ attr_reader :search_filter, :search_query, :default_scope_name
5
5
  attr_accessor :default_sort_config
6
6
 
7
7
  # Initializes a QueryObject with the given resource_class and parameters.
@@ -31,9 +31,11 @@ module Plutonium
31
31
  #
32
32
  # @param name [Symbol] The name of the scope.
33
33
  # @param body [Proc, nil] The body of the scope.
34
- def define_scope(name, body = nil)
34
+ # @param default [Boolean] Whether this scope is the default.
35
+ def define_scope(name, body = nil, default: false, **)
35
36
  body ||= name
36
37
  scope_definitions[name] = build_query(body)
38
+ @default_scope_name = name.to_s if default
37
39
  end
38
40
 
39
41
  # Defines a sort with the given name and body.
@@ -68,31 +70,53 @@ module Plutonium
68
70
  q = {}
69
71
 
70
72
  q[:search] = options.key?(:search) ? options[:search].presence : search_query
71
- q[:scope] = options.key?(:scope) ? options[:scope].presence : selected_scope_filter
73
+ q[:scope] = if options.key?(:scope)
74
+ options[:scope].presence
75
+ else
76
+ selected_scope_filter
77
+ end
72
78
 
73
79
  q[:sort_directions] = selected_sort_directions.dup
74
80
  q[:sort_fields] = selected_sort_fields.dup
75
81
  handle_sort_options!(q, options)
76
82
 
77
83
  q.merge! params.slice(*filter_definitions.keys)
78
- query_params = deep_compact({q: q}).to_param
84
+ compacted = deep_compact({q: q})
85
+
86
+ # Preserve explicit "All" selection (scope: nil in options means show all)
87
+ if options.key?(:scope) && options[:scope].nil?
88
+ compacted[:q] ||= {}
89
+ compacted[:q][:scope] = ""
90
+ end
91
+
92
+ query_params = compacted.to_param
79
93
  "#{@request_path}?#{query_params}"
80
94
  end
81
95
 
82
96
  # Applies the defined filters and sorts to the given scope.
83
97
  #
84
98
  # @param scope [Object] The initial scope to which filters and sorts are applied.
99
+ # @param params [Hash] The query parameters.
100
+ # @param context [Object] Optional context (e.g., controller) for executing scope blocks.
85
101
  # @return [Object] The modified scope.
86
- def apply(scope, params)
102
+ def apply(scope, params, context: nil)
87
103
  params = deep_compact(params.with_indifferent_access)
88
104
  scope = search_filter.apply(scope, search: params[:search]) if search_filter && params[:search]
89
- scope = scope_definitions[params[:scope]].apply(scope, **{}) if scope_definitions[params[:scope]]
105
+ # Use selected_scope which includes the default when no explicit selection
106
+ effective_scope = @selected_scope_filter
107
+ scope = scope_definitions[effective_scope].apply(scope, context:) if effective_scope && scope_definitions[effective_scope]
90
108
  scope = apply_sorts(scope, params)
91
109
  apply_filters(scope, params)
92
110
  end
93
111
 
94
112
  def scope_definitions = @scope_definitions ||= {}.with_indifferent_access
95
113
 
114
+ # Returns true if user explicitly selected "All" scope (no filtering)
115
+ def all_scope_selected? = @all_scope_selected
116
+
117
+ # Returns the currently selected scope (may be default if none explicitly selected)
118
+ def selected_scope = @selected_scope_filter
119
+
96
120
  def filter_definitions = @filter_definitions ||= {}.with_indifferent_access
97
121
 
98
122
  def sort_definitions = @sort_definitions ||= {}.with_indifferent_access
@@ -125,8 +149,14 @@ module Plutonium
125
149
  #
126
150
  # @param params [Hash] The parameters to extract.
127
151
  def extract_filter_params
128
- @search_query = params[:search]
129
- @selected_scope_filter = params[:scope]
152
+ @search_query = params[:search].presence&.strip
153
+ # Track if user explicitly selected "all" (scope param present but blank)
154
+ @all_scope_selected = params.key?(:scope) && params[:scope].blank?
155
+ @selected_scope_filter = if @all_scope_selected
156
+ nil # User clicked "All"
157
+ else
158
+ params[:scope].presence || default_scope_name
159
+ end
130
160
  end
131
161
 
132
162
  # Extracts sort parameters from the given params.