plutonium 0.41.1 → 0.42.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/app/assets/plutonium.css +2 -2
  4. data/app/assets/plutonium.js +46 -1
  5. data/app/assets/plutonium.js.map +4 -4
  6. data/app/assets/plutonium.min.js +32 -32
  7. data/app/assets/plutonium.min.js.map +4 -4
  8. data/docs/guides/user-invites.md +1 -1
  9. data/docs/reference/generators/index.md +43 -0
  10. data/gemfiles/rails_7.gemfile.lock +1 -1
  11. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  12. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  13. data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +2 -0
  14. data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +3 -1
  15. data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +3 -1
  16. data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +1 -1
  17. data/lib/generators/pu/rodauth/account_generator.rb +20 -5
  18. data/lib/generators/pu/rodauth/admin_generator.rb +1 -1
  19. data/lib/generators/pu/rodauth/concerns/configuration.rb +1 -0
  20. data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +19 -0
  21. data/lib/generators/pu/rodauth/install_generator.rb +7 -3
  22. data/lib/generators/pu/rodauth/migration/active_record/base.erb +4 -4
  23. data/lib/generators/pu/rodauth/migration_generator.rb +7 -0
  24. data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +1 -1
  25. data/lib/generators/pu/saas/USAGE +10 -1
  26. data/lib/generators/pu/saas/api_client/USAGE +32 -0
  27. data/lib/generators/pu/saas/api_client/templates/app/interactions/create_interaction.rb.tt +80 -0
  28. data/lib/generators/pu/saas/api_client/templates/app/interactions/disable_interaction.rb.tt +15 -0
  29. data/lib/generators/pu/saas/api_client/templates/lib/tasks/api_client.rake.tt +48 -0
  30. data/lib/generators/pu/saas/api_client_generator.rb +254 -0
  31. data/lib/generators/pu/saas/entity_generator.rb +5 -3
  32. data/lib/generators/pu/saas/membership_generator.rb +21 -9
  33. data/lib/generators/pu/saas/setup_generator.rb +23 -0
  34. data/lib/plutonium/api_client/concerns/create_api_client.rb +256 -0
  35. data/lib/plutonium/api_client/concerns/disable_api_client.rb +64 -0
  36. data/lib/plutonium/api_client.rb +21 -0
  37. data/lib/plutonium/interaction/concerns/scoping.rb +68 -0
  38. data/lib/plutonium/interaction/response/render.rb +16 -1
  39. data/lib/plutonium/invites/concerns/invite_user.rb +1 -1
  40. data/lib/plutonium/version.rb +1 -1
  41. data/package.json +1 -1
  42. data/src/js/controllers/clipboard_controller.js +37 -0
  43. data/src/js/controllers/register_controllers.js +2 -0
  44. data/src/js/controllers/remote_modal_controller.js +18 -4
  45. metadata +13 -2
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module ApiClient
5
+ module Concerns
6
+ # DisableApiClient provides the core logic for disabling API client accounts.
7
+ #
8
+ # Include this in your DisableInteraction and implement the required methods.
9
+ #
10
+ # @example Basic usage
11
+ # class ApiClient::DisableInteraction < Plutonium::Resource::Interaction
12
+ # include Plutonium::ApiClient::Concerns::DisableApiClient
13
+ #
14
+ # def rodauth_name
15
+ # :api_client
16
+ # end
17
+ # end
18
+ #
19
+ module DisableApiClient
20
+ extend ActiveSupport::Concern
21
+
22
+ included do
23
+ presents label: "Disable",
24
+ description: "Disable this API client (cannot be undone)",
25
+ icon: Phlex::TablerIcons::Ban,
26
+ color: :danger
27
+
28
+ attribute :resource
29
+
30
+ validates :resource, presence: true
31
+ end
32
+
33
+ def execute
34
+ login = resource.login
35
+
36
+ rodauth_instance.close_account(account_login: login)
37
+
38
+ succeed(resource).with_message(success_message(login))
39
+ rescue => e
40
+ failed(base: e.message)
41
+ end
42
+
43
+ private
44
+
45
+ # Override to specify the Rodauth configuration name
46
+ # @return [Symbol]
47
+ def rodauth_name
48
+ raise NotImplementedError, "#{self.class}#rodauth_name must return the Rodauth configuration name (e.g., :api_client)"
49
+ end
50
+
51
+ # Override to customize success message
52
+ # @param login [String] the login of the disabled API client
53
+ # @return [String]
54
+ def success_message(login)
55
+ "API client '#{login}' has been disabled"
56
+ end
57
+
58
+ def rodauth_instance
59
+ RodauthApp.rodauth(rodauth_name)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ # ApiClient module provides concerns for building API client account interactions.
5
+ #
6
+ # @example Creating an API client interaction
7
+ # class ApiClient::CreateInteraction < Plutonium::Resource::Interaction
8
+ # include Plutonium::ApiClient::Concerns::CreateApiClient
9
+ #
10
+ # def rodauth_name
11
+ # :api_client
12
+ # end
13
+ #
14
+ # def api_client_class
15
+ # ApiClient
16
+ # end
17
+ # end
18
+ #
19
+ module ApiClient
20
+ end
21
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module Interaction
5
+ module Concerns
6
+ # Scoping concern provides access to scoped records from the controller context.
7
+ #
8
+ # This handles both:
9
+ # - Entity scoping: Portal-level multi-tenancy via `scope_to_entity` (accessed via `current_scoped_entity`)
10
+ # - Parent scoping: Nested routes (accessed via `current_parent`)
11
+ #
12
+ # The `scoped_record_of_type` method checks both contexts and ensures type safety.
13
+ #
14
+ # @example Using in an interaction
15
+ # class MyInteraction < Plutonium::Resource::Interaction
16
+ # include Plutonium::Interaction::Concerns::Scoping
17
+ #
18
+ # def execute
19
+ # organization = scoped_record_of_type(Organization)
20
+ # # Returns the Organization from either entity or parent scope
21
+ # end
22
+ # end
23
+ #
24
+ module Scoping
25
+ extend ActiveSupport::Concern
26
+
27
+ private
28
+
29
+ # Returns a scoped record that matches the expected type.
30
+ #
31
+ # Checks both entity scoping (`current_scoped_entity`) and parent scoping (`current_parent`),
32
+ # returning the first match that is an instance of the specified class.
33
+ #
34
+ # @param klass [Class] the expected model class
35
+ # @return [Object, nil] the scoped record if found and type matches, nil otherwise
36
+ def scoped_record_of_type(klass)
37
+ [current_scoped_entity, current_parent].find { |record| record.is_a?(klass) }
38
+ end
39
+
40
+ # Returns the parent record from the controller (nested routes).
41
+ #
42
+ # @return [Object, nil] the current parent or nil
43
+ def current_parent
44
+ view_context.controller.current_parent
45
+ rescue NoMethodError
46
+ nil
47
+ end
48
+
49
+ # Returns the entity record from the controller (portal multi-tenancy).
50
+ #
51
+ # @return [Object, nil] the current scoped entity or nil
52
+ def current_scoped_entity
53
+ view_context.controller.current_scoped_entity
54
+ rescue NoMethodError
55
+ nil
56
+ end
57
+
58
+ # Returns the appropriate parent for URL generation.
59
+ # Prefers entity scope over parent scope.
60
+ #
61
+ # @return [Object, nil] the entity or parent, whichever is available
62
+ def scoped_parent
63
+ current_scoped_entity || current_parent
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -12,7 +12,22 @@ module Plutonium
12
12
  # @param controller [ActionController::Base] The controller instance.
13
13
  # @return [void]
14
14
  def execute(controller)
15
- controller.render(*@args, @options)
15
+ render_args = @args
16
+ render_options = @options
17
+
18
+ controller.instance_eval do
19
+ respond_to do |format|
20
+ format.turbo_stream do
21
+ # For Turbo requests, replace the form with the rendered content
22
+ render turbo_stream: turbo_stream.replace(
23
+ "interaction-form",
24
+ view_context.render(*render_args, **render_options)
25
+ )
26
+ end
27
+
28
+ format.any { render(*render_args, **render_options) }
29
+ end
30
+ end
16
31
  end
17
32
  end
18
33
  end
@@ -24,7 +24,7 @@ module Plutonium
24
24
  extend ActiveSupport::Concern
25
25
 
26
26
  included do
27
- presents label: "Invite User", icon: Phlex::TablerIcons::Mail
27
+ include Plutonium::Interaction::Concerns::Scoping
28
28
 
29
29
  attribute :resource
30
30
  attribute :email
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.41.1"
2
+ VERSION = "0.42.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.41.1",
3
+ "version": "0.42.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -0,0 +1,37 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["source"]
5
+
6
+ copy(event) {
7
+ const text = this.sourceTarget.value || this.sourceTarget.textContent
8
+ const button = event.currentTarget
9
+ const originalText = button.textContent
10
+
11
+ navigator.clipboard.writeText(text).then(() => {
12
+ button.textContent = "Copied!"
13
+ setTimeout(() => {
14
+ button.textContent = originalText
15
+ }, 2000)
16
+ }).catch((err) => {
17
+ // Fallback for browsers that don't support clipboard API
18
+ console.warn("Clipboard API failed, using fallback:", err)
19
+ this.fallbackCopy(text)
20
+ button.textContent = "Copied!"
21
+ setTimeout(() => {
22
+ button.textContent = originalText
23
+ }, 2000)
24
+ })
25
+ }
26
+
27
+ fallbackCopy(text) {
28
+ const textarea = document.createElement("textarea")
29
+ textarea.value = text
30
+ textarea.style.position = "fixed"
31
+ textarea.style.opacity = "0"
32
+ document.body.appendChild(textarea)
33
+ textarea.select()
34
+ document.execCommand("copy")
35
+ document.body.removeChild(textarea)
36
+ }
37
+ }
@@ -23,6 +23,7 @@ import KeyValueStoreController from "./key_value_store_controller.js"
23
23
  import BulkActionsController from "./bulk_actions_controller.js"
24
24
  import FilterPanelController from "./filter_panel_controller.js"
25
25
  import TextareaAutogrowController from "./textarea_autogrow_controller.js"
26
+ import ClipboardController from "./clipboard_controller.js"
26
27
 
27
28
  export default function (application) {
28
29
  // Register controllers here
@@ -50,4 +51,5 @@ export default function (application) {
50
51
  application.register("bulk-actions", BulkActionsController)
51
52
  application.register("filter-panel", FilterPanelController)
52
53
  application.register("textarea-autogrow", TextareaAutogrowController)
54
+ application.register("clipboard", ClipboardController)
53
55
  }
@@ -3,8 +3,13 @@ import { Controller } from "@hotwired/stimulus";
3
3
  // Connects to data-controller="remote-modal"
4
4
  export default class extends Controller {
5
5
  connect() {
6
- // Store original scroll position
6
+ // Store original scroll position and body overflow
7
7
  this.originalScrollPosition = window.scrollY;
8
+ this.originalOverflow = document.body.style.overflow;
9
+ this.bodyStateRestored = false;
10
+
11
+ // Lock body scroll
12
+ document.body.style.overflow = "hidden";
8
13
 
9
14
  // Show the modal
10
15
  this.element.showModal();
@@ -15,17 +20,26 @@ export default class extends Controller {
15
20
  close() {
16
21
  // Close the modal
17
22
  this.element.close();
18
- // Restore the original scroll position
19
- window.scrollTo(0, this.originalScrollPosition);
23
+ this.restoreBodyState();
20
24
  }
21
25
 
22
26
  disconnect() {
23
27
  // Clean up event listener when controller is disconnected
24
28
  this.element.removeEventListener("close", this.handleClose);
29
+ this.restoreBodyState();
25
30
  }
26
31
 
27
32
  handleClose() {
28
- // Restore the original scroll position after dialog closes
33
+ this.restoreBodyState();
34
+ }
35
+
36
+ restoreBodyState() {
37
+ if (this.bodyStateRestored) return;
38
+ this.bodyStateRestored = true;
39
+
40
+ // Restore body overflow
41
+ document.body.style.overflow = this.originalOverflow || "";
42
+ // Restore the original scroll position
29
43
  window.scrollTo(0, this.originalScrollPosition);
30
44
  }
31
45
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.41.1
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-09 00:00:00.000000000 Z
11
+ date: 2026-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -745,6 +745,7 @@ files:
745
745
  - lib/generators/pu/rodauth/concerns/account_selector.rb
746
746
  - lib/generators/pu/rodauth/concerns/configuration.rb
747
747
  - lib/generators/pu/rodauth/concerns/feature_selector.rb
748
+ - lib/generators/pu/rodauth/concerns/gem_helpers.rb
748
749
  - lib/generators/pu/rodauth/install_generator.rb
749
750
  - lib/generators/pu/rodauth/migration/active_record/account_expiration.erb
750
751
  - lib/generators/pu/rodauth/migration/active_record/active_sessions.erb
@@ -799,6 +800,11 @@ files:
799
800
  - lib/generators/pu/rodauth/templates/lib/tasks/rodauth_admin.rake.tt
800
801
  - lib/generators/pu/rodauth/views_generator.rb
801
802
  - lib/generators/pu/saas/USAGE
803
+ - lib/generators/pu/saas/api_client/USAGE
804
+ - lib/generators/pu/saas/api_client/templates/app/interactions/create_interaction.rb.tt
805
+ - lib/generators/pu/saas/api_client/templates/app/interactions/disable_interaction.rb.tt
806
+ - lib/generators/pu/saas/api_client/templates/lib/tasks/api_client.rake.tt
807
+ - lib/generators/pu/saas/api_client_generator.rb
802
808
  - lib/generators/pu/saas/entity/USAGE
803
809
  - lib/generators/pu/saas/entity_generator.rb
804
810
  - lib/generators/pu/saas/membership/USAGE
@@ -825,6 +831,9 @@ files:
825
831
  - lib/plutonium/action/route_options.rb
826
832
  - lib/plutonium/action/simple.rb
827
833
  - lib/plutonium/action_policy/sti_policy_lookup.rb
834
+ - lib/plutonium/api_client.rb
835
+ - lib/plutonium/api_client/concerns/create_api_client.rb
836
+ - lib/plutonium/api_client/concerns/disable_api_client.rb
828
837
  - lib/plutonium/auth.rb
829
838
  - lib/plutonium/auth/public.rb
830
839
  - lib/plutonium/auth/rodauth.rb
@@ -859,6 +868,7 @@ files:
859
868
  - lib/plutonium/helpers/turbo_stream_actions_helper.rb
860
869
  - lib/plutonium/interaction/README.md
861
870
  - lib/plutonium/interaction/base.rb
871
+ - lib/plutonium/interaction/concerns/scoping.rb
862
872
  - lib/plutonium/interaction/concerns/workflow_dsl.rb
863
873
  - lib/plutonium/interaction/nested_attributes.rb
864
874
  - lib/plutonium/interaction/outcome.rb
@@ -1023,6 +1033,7 @@ files:
1023
1033
  - src/js/controllers/attachment_preview_container_controller.js
1024
1034
  - src/js/controllers/attachment_preview_controller.js
1025
1035
  - src/js/controllers/bulk_actions_controller.js
1036
+ - src/js/controllers/clipboard_controller.js
1026
1037
  - src/js/controllers/color_mode_controller.js
1027
1038
  - src/js/controllers/easymde_controller.js
1028
1039
  - src/js/controllers/filter_panel_controller.js