plutonium 0.26.2 → 0.26.4
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/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +123 -27
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +18 -18
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/modules/generator.md +1 -1
- data/lib/generators/pu/res/entity/entity_generator.rb +37 -2
- data/lib/generators/pu/rodauth/customer_generator.rb +0 -2
- data/lib/plutonium/ui/form/components/uppy.rb +2 -1
- data/lib/plutonium/ui/form/resource.rb +26 -0
- data/lib/plutonium/ui/layout/basic_layout.rb +15 -0
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/js/controllers/attachment_input_controller.js +43 -2
- data/src/js/controllers/easymde_controller.js +45 -6
- data/src/js/controllers/flatpickr_controller.js +24 -8
- data/src/js/controllers/intl_tel_input_controller.js +23 -5
- data/src/js/controllers/slim_select_controller.js +27 -12
- metadata +3 -2
data/docs/modules/generator.md
CHANGED
@@ -238,10 +238,10 @@ end
|
|
238
238
|
|
239
239
|
# app/models/organization_customer.rb
|
240
240
|
class OrganizationCustomer < ::ResourceRecord
|
241
|
+
enum :role, member: 0, owner: 1
|
241
242
|
|
242
243
|
belongs_to :organization
|
243
244
|
belongs_to :customer
|
244
|
-
enum role: { member: 0, admin: 1 }
|
245
245
|
end
|
246
246
|
```
|
247
247
|
|
@@ -70,6 +70,10 @@ module Pu
|
|
70
70
|
normalized_entity_membership_name,
|
71
71
|
["#{normalized_name}_id", "#{normalized_auth_account_name}_id"]
|
72
72
|
)
|
73
|
+
|
74
|
+
add_default_to_role_column
|
75
|
+
add_role_enum_to_model
|
76
|
+
add_unique_validation_to_model
|
73
77
|
end
|
74
78
|
|
75
79
|
private
|
@@ -88,10 +92,9 @@ module Pu
|
|
88
92
|
migration_dir = File.join("db", "migrate")
|
89
93
|
migration_file = Dir[File.join(migration_dir, "*_create_#{model_name.pluralize}.rb")].first
|
90
94
|
|
91
|
-
|
95
|
+
modify_file_if_exists(migration_file) do |file|
|
92
96
|
index_definition = build_index_definition(model_name, index_columns)
|
93
97
|
insert_into_file migration_file, indent(index_definition, 4), before: /^ end\s*$/
|
94
|
-
success "Added unique index to #{model_name.pluralize}"
|
95
98
|
end
|
96
99
|
end
|
97
100
|
|
@@ -118,6 +121,38 @@ module Pu
|
|
118
121
|
"role:integer"
|
119
122
|
]
|
120
123
|
end
|
124
|
+
|
125
|
+
def add_default_to_role_column
|
126
|
+
migration_dir = File.join("db", "migrate")
|
127
|
+
migration_file = Dir[File.join(migration_dir, "*_create_#{normalized_entity_membership_name.pluralize}.rb")].first
|
128
|
+
|
129
|
+
modify_file_if_exists(migration_file) do |file|
|
130
|
+
gsub_file file, /t\.integer :role, null: false/, "t.integer :role, null: false, default: 0 # Member by default"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def add_role_enum_to_model
|
135
|
+
model_file = File.join("app", "models", "#{normalized_entity_membership_name}.rb")
|
136
|
+
|
137
|
+
modify_file_if_exists(model_file) do |file|
|
138
|
+
enum_definition = "\nenum :role, member: 0, owner: 1"
|
139
|
+
insert_into_file file, indent(enum_definition, 2), before: /^\s*# add model configurations above\./
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def add_unique_validation_to_model
|
144
|
+
model_file = File.join("app", "models", "#{normalized_entity_membership_name}.rb")
|
145
|
+
|
146
|
+
modify_file_if_exists(model_file) do |file|
|
147
|
+
validation_definition = "validates :#{normalized_auth_account_name}, uniqueness: {scope: :#{normalized_name}_id, message: \"is already a member of this entity\"}\n"
|
148
|
+
insert_into_file file, indent(validation_definition, 2), before: /^\s*# add validations above\./
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def modify_file_if_exists(file_path)
|
153
|
+
return unless file_path && File.exist?(file_path)
|
154
|
+
yield(file_path)
|
155
|
+
end
|
121
156
|
end
|
122
157
|
end
|
123
158
|
end
|
@@ -54,7 +54,6 @@ module Pu
|
|
54
54
|
has_many :#{normalized_entity_membership_name.pluralize}
|
55
55
|
has_many :#{normalized_name.pluralize}, through: :#{normalized_entity_membership_name.pluralize}
|
56
56
|
RUBY
|
57
|
-
success "Added relationship to #{normalized_entity_name} model"
|
58
57
|
end
|
59
58
|
|
60
59
|
customer_model_path = File.join("app", "models", "#{normalized_name}.rb")
|
@@ -63,7 +62,6 @@ module Pu
|
|
63
62
|
has_many :#{normalized_entity_membership_name.pluralize}
|
64
63
|
has_many :#{normalized_entity_name.pluralize}, through: :#{normalized_entity_membership_name.pluralize}
|
65
64
|
RUBY
|
66
|
-
success "Added relationship to #{normalized_name} model"
|
67
65
|
end
|
68
66
|
end
|
69
67
|
|
@@ -154,7 +154,8 @@ module Plutonium
|
|
154
154
|
max_file_num: attributes.fetch(:size, field.multiple? ? field.limit : 1),
|
155
155
|
min_file_num: nil,
|
156
156
|
allowed_file_types: nil,
|
157
|
-
required_meta_fields: nil
|
157
|
+
required_meta_fields: nil,
|
158
|
+
endpoint: attributes[:endpoint] || "/upload"
|
158
159
|
}.each do |key, default_value|
|
159
160
|
value = attributes.key?(key) ? attributes.delete(key) : default_value
|
160
161
|
direct_upload_options[:data][:"attachment_input_#{key}_value"] = value
|
@@ -31,6 +31,32 @@ module Plutonium
|
|
31
31
|
}
|
32
32
|
end
|
33
33
|
|
34
|
+
def render_actions
|
35
|
+
input name: "return_to", value: request.params[:return_to], type: :hidden, hidden: true
|
36
|
+
|
37
|
+
actions_wrapper {
|
38
|
+
if object.respond_to?(:new_record?)
|
39
|
+
if object.new_record?
|
40
|
+
button(
|
41
|
+
type: :submit,
|
42
|
+
name: "return_to",
|
43
|
+
value: request.url,
|
44
|
+
class: "px-4 py-2 bg-secondary-600 text-white rounded-md hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-secondary-500"
|
45
|
+
) { "Create and add another" }
|
46
|
+
else
|
47
|
+
button(
|
48
|
+
type: :submit,
|
49
|
+
name: "return_to",
|
50
|
+
value: request.url,
|
51
|
+
class: "px-4 py-2 bg-secondary-600 text-white rounded-md hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-secondary-500"
|
52
|
+
) { "Update and continue editing" }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
render submit_button
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
34
60
|
def form_action
|
35
61
|
return @form_action unless object.present? && @form_action != false && helpers.present?
|
36
62
|
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
@@ -10,6 +10,7 @@ import DomElement from "../support/dom_element"
|
|
10
10
|
export default class extends Controller {
|
11
11
|
static values = {
|
12
12
|
identifier: String,
|
13
|
+
endpoint: String,
|
13
14
|
|
14
15
|
maxFileSize: { type: Number, default: null },
|
15
16
|
minFileSize: { type: Number, default: null },
|
@@ -25,6 +26,8 @@ export default class extends Controller {
|
|
25
26
|
//======= Lifecycle
|
26
27
|
|
27
28
|
connect() {
|
29
|
+
if (this.uppy) return;
|
30
|
+
|
28
31
|
// initialize
|
29
32
|
this.uploadedFiles = []
|
30
33
|
|
@@ -37,10 +40,48 @@ export default class extends Controller {
|
|
37
40
|
this.#buildTriggers()
|
38
41
|
// init state
|
39
42
|
this.#onAttachmentsChanged()
|
43
|
+
|
44
|
+
// Just recreate Uppy after morphing - preserve existing attachments
|
45
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
46
|
+
if (event.target === this.element && !this.morphing) {
|
47
|
+
this.morphing = true;
|
48
|
+
requestAnimationFrame(() => {
|
49
|
+
this.#handleMorph();
|
50
|
+
this.morphing = false;
|
51
|
+
});
|
52
|
+
}
|
53
|
+
});
|
40
54
|
}
|
41
55
|
|
42
56
|
disconnect() {
|
43
|
-
this
|
57
|
+
this.#cleanupUppy();
|
58
|
+
}
|
59
|
+
|
60
|
+
#handleMorph() {
|
61
|
+
if (!this.element.isConnected) return;
|
62
|
+
|
63
|
+
// Clean up the old instance
|
64
|
+
this.#cleanupUppy();
|
65
|
+
|
66
|
+
// Recreate everything - Uppy, triggers, etc.
|
67
|
+
this.uploadedFiles = []
|
68
|
+
this.element.style["display"] = "none"
|
69
|
+
this.configureUppy()
|
70
|
+
this.#buildTriggers()
|
71
|
+
this.#onAttachmentsChanged()
|
72
|
+
}
|
73
|
+
|
74
|
+
#cleanupUppy() {
|
75
|
+
if (this.uppy) {
|
76
|
+
this.uppy.destroy();
|
77
|
+
this.uppy = null;
|
78
|
+
}
|
79
|
+
|
80
|
+
// Clean up triggers
|
81
|
+
if (this.triggerContainer && this.triggerContainer.parentNode) {
|
82
|
+
this.triggerContainer.parentNode.removeChild(this.triggerContainer);
|
83
|
+
this.triggerContainer = null;
|
84
|
+
}
|
44
85
|
}
|
45
86
|
|
46
87
|
attachmentPreviewOutletConnected(outlet, element) {
|
@@ -75,7 +116,7 @@ export default class extends Controller {
|
|
75
116
|
#configureUploader() {
|
76
117
|
this.uppy
|
77
118
|
.use(XHRUpload, {
|
78
|
-
endpoint:
|
119
|
+
endpoint: this.endpointValue, // path to the upload endpoint
|
79
120
|
})
|
80
121
|
}
|
81
122
|
|
@@ -4,21 +4,60 @@ import { marked } from 'marked';
|
|
4
4
|
|
5
5
|
// Connects to data-controller="easymde"
|
6
6
|
export default class extends Controller {
|
7
|
+
static targets = ["textarea"]
|
8
|
+
|
7
9
|
connect() {
|
10
|
+
if (this.easyMDE) return
|
11
|
+
|
12
|
+
this.originalValue = this.element.value
|
8
13
|
this.easyMDE = new EasyMDE(this.#buildOptions())
|
9
|
-
|
14
|
+
|
15
|
+
// Store the editor content before morphing
|
16
|
+
this.element.addEventListener("turbo:before-morph-element", (event) => {
|
17
|
+
if (event.target === this.element && this.easyMDE) {
|
18
|
+
this.storedValue = this.easyMDE.value()
|
19
|
+
}
|
20
|
+
})
|
21
|
+
|
22
|
+
// Restore after morphing
|
23
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
24
|
+
if (event.target === this.element) {
|
25
|
+
requestAnimationFrame(() => this.#handleMorph())
|
26
|
+
}
|
27
|
+
})
|
10
28
|
}
|
11
29
|
|
12
30
|
disconnect() {
|
13
31
|
if (this.easyMDE) {
|
14
|
-
|
32
|
+
try {
|
33
|
+
// Only call toTextArea if the element is still in the DOM
|
34
|
+
if (this.element.isConnected && this.element.parentNode) {
|
35
|
+
this.easyMDE.toTextArea()
|
36
|
+
}
|
37
|
+
} catch (error) {
|
38
|
+
console.warn('EasyMDE cleanup error:', error)
|
39
|
+
}
|
15
40
|
this.easyMDE = null
|
16
41
|
}
|
17
42
|
}
|
18
|
-
|
19
|
-
|
20
|
-
this.
|
21
|
-
|
43
|
+
|
44
|
+
#handleMorph() {
|
45
|
+
if (!this.element.isConnected) return
|
46
|
+
|
47
|
+
// Don't call toTextArea during morph - just clean up references
|
48
|
+
if (this.easyMDE) {
|
49
|
+
// Skip toTextArea cleanup - it causes DOM errors during morphing
|
50
|
+
this.easyMDE = null
|
51
|
+
}
|
52
|
+
|
53
|
+
// Recreate the editor
|
54
|
+
this.easyMDE = new EasyMDE(this.#buildOptions())
|
55
|
+
|
56
|
+
// Restore the stored value if we have it
|
57
|
+
if (this.storedValue !== undefined) {
|
58
|
+
this.easyMDE.value(this.storedValue)
|
59
|
+
this.storedValue = undefined
|
60
|
+
}
|
22
61
|
}
|
23
62
|
|
24
63
|
#buildOptions() {
|
@@ -3,14 +3,21 @@ import { Controller } from "@hotwired/stimulus";
|
|
3
3
|
// Connects to data-controller="flatpickr"
|
4
4
|
export default class extends Controller {
|
5
5
|
connect() {
|
6
|
-
this.
|
6
|
+
if (this.picker) return;
|
7
7
|
|
8
|
+
this.modal = document.querySelector("[data-controller=remote-modal]");
|
8
9
|
this.picker = new flatpickr(this.element, this.#buildOptions());
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
// Just recreate Flatpickr after morphing - the DOM will have correct value
|
12
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
13
|
+
if (event.target === this.element && !this.morphing) {
|
14
|
+
this.morphing = true;
|
15
|
+
requestAnimationFrame(() => {
|
16
|
+
this.#handleMorph();
|
17
|
+
this.morphing = false;
|
18
|
+
});
|
19
|
+
}
|
20
|
+
});
|
14
21
|
}
|
15
22
|
|
16
23
|
disconnect() {
|
@@ -20,9 +27,18 @@ export default class extends Controller {
|
|
20
27
|
}
|
21
28
|
}
|
22
29
|
|
23
|
-
|
24
|
-
this.
|
25
|
-
|
30
|
+
#handleMorph() {
|
31
|
+
if (!this.element.isConnected) return;
|
32
|
+
|
33
|
+
// Clean up the old instance
|
34
|
+
if (this.picker) {
|
35
|
+
this.picker.destroy();
|
36
|
+
this.picker = null;
|
37
|
+
}
|
38
|
+
|
39
|
+
// Recreate the picker - it will pick up the current DOM value
|
40
|
+
this.modal = document.querySelector("[data-controller=remote-modal]");
|
41
|
+
this.picker = new flatpickr(this.element, this.#buildOptions());
|
26
42
|
}
|
27
43
|
|
28
44
|
#buildOptions() {
|
@@ -12,10 +12,20 @@ export default class extends Controller {
|
|
12
12
|
}
|
13
13
|
|
14
14
|
inputTargetConnected() {
|
15
|
-
if (!this.hasInputTarget) return;
|
15
|
+
if (!this.hasInputTarget || this.iti) return;
|
16
16
|
|
17
17
|
this.iti = window.intlTelInput(this.inputTarget, this.#buildOptions())
|
18
|
-
|
18
|
+
|
19
|
+
// Just recreate IntlTelInput after morphing - the DOM will have correct value
|
20
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
21
|
+
if (event.target === this.element && !this.morphing) {
|
22
|
+
this.morphing = true;
|
23
|
+
requestAnimationFrame(() => {
|
24
|
+
this.#handleMorph();
|
25
|
+
this.morphing = false;
|
26
|
+
});
|
27
|
+
}
|
28
|
+
});
|
19
29
|
}
|
20
30
|
|
21
31
|
inputTargetDisconnected() {
|
@@ -25,9 +35,17 @@ export default class extends Controller {
|
|
25
35
|
}
|
26
36
|
}
|
27
37
|
|
28
|
-
|
29
|
-
this.
|
30
|
-
|
38
|
+
#handleMorph() {
|
39
|
+
if (!this.inputTarget || !this.inputTarget.isConnected) return;
|
40
|
+
|
41
|
+
// Clean up the old instance
|
42
|
+
if (this.iti) {
|
43
|
+
this.iti.destroy();
|
44
|
+
this.iti = null;
|
45
|
+
}
|
46
|
+
|
47
|
+
// Recreate the intl tel input - it will pick up the current DOM value
|
48
|
+
this.iti = window.intlTelInput(this.inputTarget, this.#buildOptions());
|
31
49
|
}
|
32
50
|
|
33
51
|
#buildOptions() {
|
@@ -3,6 +3,19 @@ import { Controller } from "@hotwired/stimulus";
|
|
3
3
|
// Connects to data-controller="slim-select"
|
4
4
|
export default class extends Controller {
|
5
5
|
connect() {
|
6
|
+
if (this.slimSelect) return;
|
7
|
+
|
8
|
+
this.#setupSlimSelect();
|
9
|
+
|
10
|
+
// Just recreate SlimSelect after morphing - the DOM will have correct selections
|
11
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
12
|
+
if (event.target === this.element) {
|
13
|
+
requestAnimationFrame(() => this.#handleMorph());
|
14
|
+
}
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
#setupSlimSelect() {
|
6
19
|
const settings = {};
|
7
20
|
const modal = document.querySelector('[data-controller="remote-modal"]');
|
8
21
|
|
@@ -48,11 +61,6 @@ export default class extends Controller {
|
|
48
61
|
|
49
62
|
// Add mutation observer to track aria-expanded attribute
|
50
63
|
this.setupAriaObserver();
|
51
|
-
|
52
|
-
this.element.setAttribute(
|
53
|
-
"data-action",
|
54
|
-
"turbo:morph-element->slim-select#reconnect"
|
55
|
-
);
|
56
64
|
}
|
57
65
|
|
58
66
|
handleDropdownPosition() {
|
@@ -162,6 +170,20 @@ export default class extends Controller {
|
|
162
170
|
}
|
163
171
|
|
164
172
|
disconnect() {
|
173
|
+
this.#cleanupSlimSelect();
|
174
|
+
}
|
175
|
+
|
176
|
+
#handleMorph() {
|
177
|
+
if (!this.element.isConnected) return;
|
178
|
+
|
179
|
+
// Clean up the old instance without DOM manipulation
|
180
|
+
this.#cleanupSlimSelect();
|
181
|
+
|
182
|
+
// Recreate the select - it will automatically pick up the current DOM selections
|
183
|
+
this.#setupSlimSelect();
|
184
|
+
}
|
185
|
+
|
186
|
+
#cleanupSlimSelect() {
|
165
187
|
// Clean up event listeners
|
166
188
|
if (this.element) {
|
167
189
|
if (this.boundHandleDropdownOpen) {
|
@@ -208,11 +230,4 @@ export default class extends Controller {
|
|
208
230
|
this.modifiedSelectWrapper = null;
|
209
231
|
}
|
210
232
|
}
|
211
|
-
|
212
|
-
reconnect() {
|
213
|
-
this.disconnect();
|
214
|
-
// dispatch this on the next frame.
|
215
|
-
// there's some funny issue where my elements get removed from the DOM
|
216
|
-
setTimeout(() => this.connect(), 10);
|
217
|
-
}
|
218
233
|
}
|
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.26.
|
4
|
+
version: 0.26.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-07-
|
11
|
+
date: 2025-07-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -854,6 +854,7 @@ files:
|
|
854
854
|
- lib/plutonium/ui/form/theme.rb
|
855
855
|
- lib/plutonium/ui/frame_navigator_panel.rb
|
856
856
|
- lib/plutonium/ui/layout/base.rb
|
857
|
+
- lib/plutonium/ui/layout/basic_layout.rb
|
857
858
|
- lib/plutonium/ui/layout/header.rb
|
858
859
|
- lib/plutonium/ui/layout/resource_layout.rb
|
859
860
|
- lib/plutonium/ui/layout/rodauth_layout.rb
|