plutonium 0.41.0 → 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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-create-resource/SKILL.md +13 -0
- data/CHANGELOG.md +30 -0
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +46 -1
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +4 -4
- data/docs/guides/user-invites.md +1 -1
- data/docs/reference/generators/index.md +57 -1
- 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/invites/templates/app/interactions/invite_user_interaction.rb.tt +2 -0
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +3 -1
- data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +3 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +1 -1
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +84 -15
- data/lib/generators/pu/rodauth/account_generator.rb +20 -5
- data/lib/generators/pu/rodauth/admin_generator.rb +1 -1
- data/lib/generators/pu/rodauth/concerns/configuration.rb +1 -0
- data/lib/generators/pu/rodauth/concerns/gem_helpers.rb +19 -0
- data/lib/generators/pu/rodauth/install_generator.rb +7 -3
- data/lib/generators/pu/rodauth/migration/active_record/base.erb +4 -4
- data/lib/generators/pu/rodauth/migration_generator.rb +7 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +1 -1
- data/lib/generators/pu/saas/USAGE +10 -1
- data/lib/generators/pu/saas/api_client/USAGE +32 -0
- data/lib/generators/pu/saas/api_client/templates/app/interactions/create_interaction.rb.tt +80 -0
- data/lib/generators/pu/saas/api_client/templates/app/interactions/disable_interaction.rb.tt +15 -0
- data/lib/generators/pu/saas/api_client/templates/lib/tasks/api_client.rake.tt +48 -0
- data/lib/generators/pu/saas/api_client_generator.rb +254 -0
- data/lib/generators/pu/saas/entity_generator.rb +5 -3
- data/lib/generators/pu/saas/membership_generator.rb +21 -9
- data/lib/generators/pu/saas/setup_generator.rb +23 -0
- data/lib/plutonium/api_client/concerns/create_api_client.rb +256 -0
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +64 -0
- data/lib/plutonium/api_client.rb +21 -0
- data/lib/plutonium/interaction/concerns/scoping.rb +68 -0
- data/lib/plutonium/interaction/response/render.rb +16 -1
- data/lib/plutonium/invites/concerns/invite_user.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/js/controllers/clipboard_controller.js +37 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/remote_modal_controller.js +18 -4
- metadata +13 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module ApiClient
|
|
5
|
+
module Concerns
|
|
6
|
+
# CreateApiClient provides the core logic for creating API client accounts.
|
|
7
|
+
#
|
|
8
|
+
# Include this in your CreateInteraction and implement the required methods.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# class ApiClient::CreateInteraction < Plutonium::Resource::Interaction
|
|
12
|
+
# include Plutonium::ApiClient::Concerns::CreateApiClient
|
|
13
|
+
#
|
|
14
|
+
# input :role, as: :select, choices: OrganizationApiClient.roles.keys
|
|
15
|
+
#
|
|
16
|
+
# def membership_class
|
|
17
|
+
# OrganizationApiClient
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def role
|
|
21
|
+
# attributes[:role] || "read_only"
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
module CreateApiClient
|
|
26
|
+
extend ActiveSupport::Concern
|
|
27
|
+
include Plutonium::Interaction::Concerns::Scoping
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
presents label: "Create API Client", icon: Phlex::TablerIcons::Key
|
|
31
|
+
|
|
32
|
+
attribute :login, :string
|
|
33
|
+
|
|
34
|
+
validates :login, presence: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute
|
|
38
|
+
password = generate_secure_password
|
|
39
|
+
|
|
40
|
+
rodauth_instance.create_account(
|
|
41
|
+
login: login,
|
|
42
|
+
password: password
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Rodauth internal_request returns nil, so we need to find the account
|
|
46
|
+
api_client = api_client_class.find_by!(login: login)
|
|
47
|
+
|
|
48
|
+
create_membership!(api_client) if entity_scoped_api_client?
|
|
49
|
+
|
|
50
|
+
succeed(api_client).with_render_response(
|
|
51
|
+
credentials_page_class.new(
|
|
52
|
+
login: api_client.login,
|
|
53
|
+
password: password,
|
|
54
|
+
parent: scoped_parent
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
58
|
+
failed(login: "Failed to create account: #{e.message}")
|
|
59
|
+
rescue => e
|
|
60
|
+
failed(login: e.message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Override to specify the Rodauth configuration name
|
|
66
|
+
# @return [Symbol]
|
|
67
|
+
def rodauth_name
|
|
68
|
+
raise NotImplementedError, "#{self.class}#rodauth_name must return the Rodauth configuration name (e.g., :api_client)"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Override to specify the API client model class
|
|
72
|
+
# @return [Class]
|
|
73
|
+
def api_client_class
|
|
74
|
+
raise NotImplementedError, "#{self.class}#api_client_class must return the API client model class"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Override to specify the entity model class for scoping
|
|
78
|
+
# @return [Class, nil]
|
|
79
|
+
def entity_class
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Override to specify the membership model class
|
|
84
|
+
# @return [Class, nil]
|
|
85
|
+
def membership_class
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Override to specify the role to assign
|
|
90
|
+
# @return [String, Symbol, nil]
|
|
91
|
+
def role
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Override to add additional attributes when creating the membership
|
|
96
|
+
# @return [Hash]
|
|
97
|
+
def additional_membership_attributes
|
|
98
|
+
{}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Override to customize the credentials page class
|
|
102
|
+
# @return [Class]
|
|
103
|
+
def credentials_page_class
|
|
104
|
+
CredentialsPage
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Override to customize password generation
|
|
108
|
+
# @return [String]
|
|
109
|
+
def generate_secure_password
|
|
110
|
+
SecureRandom.base64(32)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def rodauth_instance
|
|
114
|
+
RodauthApp.rodauth(rodauth_name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def entity_scoped_api_client?
|
|
118
|
+
entity_class.present? && membership_class.present? && scoped_entity_id.present?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def scoped_entity
|
|
122
|
+
return unless entity_class
|
|
123
|
+
|
|
124
|
+
scoped_record_of_type(entity_class)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scoped_entity_id
|
|
128
|
+
scoped_entity&.id
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def create_membership!(api_client)
|
|
132
|
+
attrs = {
|
|
133
|
+
entity_foreign_key => scoped_entity_id,
|
|
134
|
+
api_client_foreign_key => api_client.id,
|
|
135
|
+
**additional_membership_attributes
|
|
136
|
+
}
|
|
137
|
+
attrs[:role] = role if role.present?
|
|
138
|
+
|
|
139
|
+
membership_class.create!(attrs)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def entity_foreign_key
|
|
143
|
+
:"#{entity_class.model_name.singular}_id"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def api_client_foreign_key
|
|
147
|
+
:"#{api_client_class.model_name.singular}_id"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Default credentials page - can be overridden
|
|
151
|
+
class CredentialsPage < Plutonium::UI::Page::Base
|
|
152
|
+
def initialize(login:, password:, parent: nil)
|
|
153
|
+
@login = login
|
|
154
|
+
@password = password
|
|
155
|
+
@parent = parent
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def view_template
|
|
159
|
+
div(class: "max-w-2xl mx-auto py-8") do
|
|
160
|
+
render_success_banner
|
|
161
|
+
render_credentials_card
|
|
162
|
+
render_action_buttons
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def render_success_banner
|
|
169
|
+
div(class: "bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6 mb-6") do
|
|
170
|
+
div(class: "flex items-center gap-3 mb-4") do
|
|
171
|
+
render_check_icon
|
|
172
|
+
h2(class: "text-xl font-semibold text-green-800 dark:text-green-200") { success_title }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
p(class: "text-green-700 dark:text-green-300") do
|
|
176
|
+
strong { "Important: " }
|
|
177
|
+
plain "Save these credentials now. The password cannot be retrieved later."
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def render_credentials_card
|
|
183
|
+
div(class: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 space-y-4") do
|
|
184
|
+
render_credential_field("Login", @login)
|
|
185
|
+
render_credential_field("Password", @password)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def render_credential_field(label, value)
|
|
190
|
+
div(class: "space-y-1", data: {controller: "clipboard"}) do
|
|
191
|
+
label(class: "block text-sm font-medium text-gray-700 dark:text-gray-300") { label }
|
|
192
|
+
div(class: "flex items-center gap-2") do
|
|
193
|
+
input(
|
|
194
|
+
type: "text",
|
|
195
|
+
value: value,
|
|
196
|
+
readonly: true,
|
|
197
|
+
data: {clipboard_target: "source"},
|
|
198
|
+
class: "flex-1 px-3 py-2 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-md font-mono text-sm select-all focus:ring-2 focus:ring-primary-500"
|
|
199
|
+
)
|
|
200
|
+
button(
|
|
201
|
+
type: "button",
|
|
202
|
+
data: {action: "clipboard#copy"},
|
|
203
|
+
class: "px-3 py-2 text-sm bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
|
204
|
+
) { "Copy" }
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def render_action_buttons
|
|
210
|
+
credentials_text = "Login: #{@login}\nPassword: #{@password}"
|
|
211
|
+
|
|
212
|
+
div(class: "mt-6 flex gap-4", data: {controller: "clipboard"}) do
|
|
213
|
+
input(type: "hidden", value: credentials_text, data: {clipboard_target: "source"})
|
|
214
|
+
button(
|
|
215
|
+
type: "button",
|
|
216
|
+
data: {action: "clipboard#copy"},
|
|
217
|
+
class: "px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors"
|
|
218
|
+
) { "Copy All" }
|
|
219
|
+
|
|
220
|
+
a(
|
|
221
|
+
href: done_url,
|
|
222
|
+
class: "px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition-colors"
|
|
223
|
+
) { "Done" }
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def render_check_icon
|
|
228
|
+
svg(
|
|
229
|
+
class: "w-8 h-8 text-green-600 dark:text-green-400",
|
|
230
|
+
fill: "none",
|
|
231
|
+
stroke: "currentColor",
|
|
232
|
+
viewBox: "0 0 24 24"
|
|
233
|
+
) do |s|
|
|
234
|
+
s.path(
|
|
235
|
+
stroke_linecap: "round",
|
|
236
|
+
stroke_linejoin: "round",
|
|
237
|
+
stroke_width: "2",
|
|
238
|
+
d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Override in subclass to customize
|
|
244
|
+
def success_title
|
|
245
|
+
"API Client Created Successfully"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Override in subclass to customize the done URL
|
|
249
|
+
def done_url
|
|
250
|
+
helpers.url_for(action: :index)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -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
|
-
|
|
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
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|