plutonium 0.25.0 → 0.25.2
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 +2 -2
- data/app/assets/plutonium.js +23948 -23882
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +69 -69
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/javascript/controllers/key_value_store_controller.js +119 -0
- data/lib/plutonium/helpers/turbo_stream_actions_helper.rb +1 -1
- data/lib/plutonium/interaction/response/redirect.rb +12 -1
- data/lib/plutonium/resource/controllers/interactive_actions.rb +3 -23
- data/lib/plutonium/ui/form/base.rb +4 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +234 -0
- data/lib/plutonium/ui/page/interactive_action.rb +10 -8
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/js/controllers/key_value_store_controller.js +109 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/plutonium.js +2 -0
- data/src/js/turbo/index.js +2 -4
- metadata +5 -2
@@ -0,0 +1,119 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
// Connects to data-controller="key-value-store"
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["container", "pair", "template", "addButton", "keyInput", "valueInput"]
|
6
|
+
static values = { limit: Number }
|
7
|
+
|
8
|
+
connect() {
|
9
|
+
this.updateIndices()
|
10
|
+
this.updateAddButtonState()
|
11
|
+
}
|
12
|
+
|
13
|
+
addPair(event) {
|
14
|
+
event.preventDefault()
|
15
|
+
|
16
|
+
if (this.pairTargets.length >= this.limitValue) {
|
17
|
+
return
|
18
|
+
}
|
19
|
+
|
20
|
+
const template = this.templateTarget
|
21
|
+
const newPair = template.content.cloneNode(true)
|
22
|
+
const index = this.pairTargets.length
|
23
|
+
|
24
|
+
// Update the template placeholders with actual indices
|
25
|
+
this.updatePairIndices(newPair, index)
|
26
|
+
|
27
|
+
this.containerTarget.appendChild(newPair)
|
28
|
+
this.updateIndices()
|
29
|
+
this.updateAddButtonState()
|
30
|
+
|
31
|
+
// Focus on the key input of the new pair
|
32
|
+
const newKeyInput = this.containerTarget.lastElementChild.querySelector('[data-key-value-store-target="keyInput"]')
|
33
|
+
if (newKeyInput) {
|
34
|
+
newKeyInput.focus()
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
removePair(event) {
|
39
|
+
event.preventDefault()
|
40
|
+
|
41
|
+
const pair = event.target.closest('[data-key-value-store-target="pair"]')
|
42
|
+
if (pair) {
|
43
|
+
pair.remove()
|
44
|
+
this.updateIndices()
|
45
|
+
this.updateAddButtonState()
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
updateIndices() {
|
50
|
+
this.pairTargets.forEach((pair, index) => {
|
51
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
52
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
53
|
+
|
54
|
+
if (keyInput) {
|
55
|
+
// Update name attribute
|
56
|
+
keyInput.name = keyInput.name.replace(/\[\d+\]/, `[${index}]`)
|
57
|
+
// Update id attribute for Turbo compatibility
|
58
|
+
keyInput.id = keyInput.id.replace(/_\d+_/, `_${index}_`)
|
59
|
+
}
|
60
|
+
if (valueInput) {
|
61
|
+
// Update name attribute
|
62
|
+
valueInput.name = valueInput.name.replace(/\[\d+\]/, `[${index}]`)
|
63
|
+
// Update id attribute for Turbo compatibility
|
64
|
+
valueInput.id = valueInput.id.replace(/_\d+_/, `_${index}_`)
|
65
|
+
}
|
66
|
+
})
|
67
|
+
}
|
68
|
+
|
69
|
+
updatePairIndices(element, index) {
|
70
|
+
const inputs = element.querySelectorAll('input')
|
71
|
+
inputs.forEach(input => {
|
72
|
+
if (input.name) {
|
73
|
+
input.name = input.name.replace('__INDEX__', index)
|
74
|
+
}
|
75
|
+
if (input.id) {
|
76
|
+
input.id = input.id.replace('___INDEX___', `_${index}_`)
|
77
|
+
}
|
78
|
+
})
|
79
|
+
}
|
80
|
+
|
81
|
+
updateAddButtonState() {
|
82
|
+
const addButton = this.addButtonTarget
|
83
|
+
if (this.pairTargets.length >= this.limitValue) {
|
84
|
+
addButton.disabled = true
|
85
|
+
addButton.classList.add('opacity-50', 'cursor-not-allowed')
|
86
|
+
} else {
|
87
|
+
addButton.disabled = false
|
88
|
+
addButton.classList.remove('opacity-50', 'cursor-not-allowed')
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
// Serialize the current key-value pairs to JSON
|
93
|
+
toJSON() {
|
94
|
+
const pairs = {}
|
95
|
+
this.pairTargets.forEach(pair => {
|
96
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
97
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
98
|
+
|
99
|
+
if (keyInput && valueInput && keyInput.value.trim()) {
|
100
|
+
pairs[keyInput.value.trim()] = valueInput.value
|
101
|
+
}
|
102
|
+
})
|
103
|
+
return JSON.stringify(pairs)
|
104
|
+
}
|
105
|
+
|
106
|
+
// Get the current key-value pairs as an object
|
107
|
+
toObject() {
|
108
|
+
const pairs = {}
|
109
|
+
this.pairTargets.forEach(pair => {
|
110
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
111
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
112
|
+
|
113
|
+
if (keyInput && valueInput && keyInput.value.trim()) {
|
114
|
+
pairs[keyInput.value.trim()] = valueInput.value
|
115
|
+
}
|
116
|
+
})
|
117
|
+
return pairs
|
118
|
+
}
|
119
|
+
}
|
@@ -12,7 +12,18 @@ module Plutonium
|
|
12
12
|
# @param controller [ActionController::Base] The controller instance.
|
13
13
|
# @return [void]
|
14
14
|
def execute(controller)
|
15
|
-
controller.
|
15
|
+
controller.instance_eval do
|
16
|
+
url = url_for(*@args)
|
17
|
+
|
18
|
+
format.any { redirect_to(url, **@options) }
|
19
|
+
if helpers.current_turbo_frame == "remote_modal"
|
20
|
+
format.turbo_stream do
|
21
|
+
render turbo_stream: [
|
22
|
+
helpers.turbo_stream_redirect(url)
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
16
27
|
end
|
17
28
|
end
|
18
29
|
end
|
@@ -59,7 +59,7 @@ module Plutonium
|
|
59
59
|
if helpers.current_turbo_frame == "remote_modal"
|
60
60
|
format.turbo_stream do
|
61
61
|
render turbo_stream: [
|
62
|
-
|
62
|
+
helpers.turbo_stream_redirect(return_url)
|
63
63
|
]
|
64
64
|
end
|
65
65
|
end
|
@@ -73,13 +73,6 @@ module Plutonium
|
|
73
73
|
render "errors", status: :unprocessable_entity
|
74
74
|
end
|
75
75
|
|
76
|
-
if helpers.current_turbo_frame == "remote_modal"
|
77
|
-
format.turbo_stream do
|
78
|
-
render turbo_stream: [
|
79
|
-
turbo_stream.replace(:remote_modal, partial: "interactive_action_form")
|
80
|
-
]
|
81
|
-
end
|
82
|
-
end
|
83
76
|
end
|
84
77
|
end
|
85
78
|
end
|
@@ -123,7 +116,7 @@ module Plutonium
|
|
123
116
|
if helpers.current_turbo_frame == "remote_modal"
|
124
117
|
format.turbo_stream do
|
125
118
|
render turbo_stream: [
|
126
|
-
|
119
|
+
helpers.turbo_stream_redirect(return_url)
|
127
120
|
]
|
128
121
|
end
|
129
122
|
end
|
@@ -137,13 +130,6 @@ module Plutonium
|
|
137
130
|
render "errors", status: :unprocessable_entity
|
138
131
|
end
|
139
132
|
|
140
|
-
if helpers.current_turbo_frame == "remote_modal"
|
141
|
-
format.turbo_stream do
|
142
|
-
render turbo_stream: [
|
143
|
-
turbo_stream.replace(:remote_modal, partial: "interactive_action_form")
|
144
|
-
]
|
145
|
-
end
|
146
|
-
end
|
147
133
|
end
|
148
134
|
end
|
149
135
|
end
|
@@ -179,7 +165,7 @@ module Plutonium
|
|
179
165
|
# if helpers.current_turbo_frame == "remote_modal"
|
180
166
|
# format.turbo_stream do
|
181
167
|
# render turbo_stream: [
|
182
|
-
#
|
168
|
+
# helpers.turbo_stream_redirect(resource_url_for(resource_class))
|
183
169
|
# ]
|
184
170
|
# end
|
185
171
|
# end
|
@@ -191,12 +177,6 @@ module Plutonium
|
|
191
177
|
# @errors = @interaction.errors
|
192
178
|
# render "errors", status: :unprocessable_entity
|
193
179
|
# end
|
194
|
-
|
195
|
-
# if helpers.current_turbo_frame == "remote_modal"
|
196
|
-
# format.turbo_stream do
|
197
|
-
# render turbo_stream: turbo_stream.replace(:remote_modal, partial: "interactive_bulk_action_form")
|
198
|
-
# end
|
199
|
-
# end
|
200
180
|
# end
|
201
181
|
# end
|
202
182
|
end
|
@@ -35,6 +35,10 @@ module Plutonium
|
|
35
35
|
alias_method :file_tag, :uppy_tag
|
36
36
|
alias_method :attachment_tag, :uppy_tag
|
37
37
|
|
38
|
+
def key_value_store_tag(**, &)
|
39
|
+
create_component(Components::KeyValueStore, :key_value_store, **, &)
|
40
|
+
end
|
41
|
+
|
38
42
|
def secure_association_tag(**attributes, &)
|
39
43
|
attributes[:data_controller] = tokens(attributes[:data_controller], "slim-select") # TODO: put this behind a config
|
40
44
|
create_component(Components::SecureAssociation, :association, **attributes, &)
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plutonium
|
4
|
+
module UI
|
5
|
+
module Form
|
6
|
+
module Components
|
7
|
+
class KeyValueStore < Phlexi::Form::Components::Base
|
8
|
+
include Phlexi::Form::Components::Concerns::HandlesInput
|
9
|
+
|
10
|
+
DEFAULT_LIMIT = 10
|
11
|
+
|
12
|
+
def view_template
|
13
|
+
div(**container_attributes) do
|
14
|
+
render_key_value_pairs
|
15
|
+
render_add_button
|
16
|
+
render_template
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def build_attributes
|
23
|
+
super
|
24
|
+
attributes[:class] = [attributes[:class], "key-value-store"].compact.join(" ")
|
25
|
+
set_data_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def container_attributes
|
31
|
+
{
|
32
|
+
id: attributes[:id],
|
33
|
+
class: attributes[:class],
|
34
|
+
data: {
|
35
|
+
controller: "key-value-store",
|
36
|
+
key_value_store_limit_value: limit
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_data_attributes
|
42
|
+
attributes[:data] ||= {}
|
43
|
+
attributes[:data][:controller] = "key-value-store"
|
44
|
+
attributes[:data][:key_value_store_limit_value] = limit
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_header
|
48
|
+
div(class: "key-value-store-header") do
|
49
|
+
if attributes[:label]
|
50
|
+
h3(class: "text-lg font-semibold text-gray-900 dark:text-white") do
|
51
|
+
plain attributes[:label]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
if attributes[:description]
|
55
|
+
p(class: "text-sm text-gray-500 dark:text-gray-400") do
|
56
|
+
plain attributes[:description]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_key_value_pairs
|
63
|
+
div(class: "key-value-pairs space-y-2", data_key_value_store_target: "container") do
|
64
|
+
pairs.each_with_index do |(key, value), index|
|
65
|
+
render_key_value_pair(key, value, index)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_key_value_pair(key, value, index)
|
71
|
+
div(
|
72
|
+
class: "key-value-pair flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded",
|
73
|
+
data_key_value_store_target: "pair"
|
74
|
+
) do
|
75
|
+
# Key input
|
76
|
+
input(
|
77
|
+
type: :text,
|
78
|
+
placeholder: "Key",
|
79
|
+
value: key,
|
80
|
+
name: "#{field_name}[#{index}][key]",
|
81
|
+
id: "#{field.dom.id}_#{index}_key",
|
82
|
+
class: "flex-1 px-3 py-1 text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
|
83
|
+
data_key_value_store_target: "keyInput"
|
84
|
+
)
|
85
|
+
|
86
|
+
# Value input
|
87
|
+
input(
|
88
|
+
type: :text,
|
89
|
+
placeholder: "Value",
|
90
|
+
value: value,
|
91
|
+
name: "#{field_name}[#{index}][value]",
|
92
|
+
id: "#{field.dom.id}_#{index}_value",
|
93
|
+
class: "flex-1 px-3 py-1 text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
|
94
|
+
data_key_value_store_target: "valueInput"
|
95
|
+
)
|
96
|
+
|
97
|
+
# Remove button
|
98
|
+
button(
|
99
|
+
type: :button,
|
100
|
+
class: "px-2 py-1 text-red-600 hover:text-red-800 focus:outline-none",
|
101
|
+
data_action: "key-value-store#removePair"
|
102
|
+
) do
|
103
|
+
plain "×"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_add_button
|
109
|
+
div(class: "key-value-store-actions mt-2") do
|
110
|
+
button(
|
111
|
+
type: :button,
|
112
|
+
id: "#{field.dom.id}_add_button",
|
113
|
+
class: "inline-flex items-center px-3 py-1 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-blue-900 dark:text-blue-300 dark:border-blue-700 dark:hover:bg-blue-800",
|
114
|
+
data: {
|
115
|
+
action: "key-value-store#addPair",
|
116
|
+
key_value_store_target: "addButton"
|
117
|
+
}
|
118
|
+
) do
|
119
|
+
plain "+ Add Pair"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def render_template
|
125
|
+
template(data_key_value_store_target: "template") do
|
126
|
+
div(
|
127
|
+
class: "key-value-pair flex items-center gap-2 p-2 border border-gray-200 dark:border-gray-700 rounded",
|
128
|
+
data_key_value_store_target: "pair"
|
129
|
+
) do
|
130
|
+
input(
|
131
|
+
type: :text,
|
132
|
+
placeholder: "Key",
|
133
|
+
name: "#{field_name}[__INDEX__][key]",
|
134
|
+
id: "#{field.dom.id}___INDEX___key",
|
135
|
+
class: "flex-1 px-3 py-1 text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
|
136
|
+
data_key_value_store_target: "keyInput"
|
137
|
+
)
|
138
|
+
|
139
|
+
input(
|
140
|
+
type: :text,
|
141
|
+
placeholder: "Value",
|
142
|
+
name: "#{field_name}[__INDEX__][value]",
|
143
|
+
id: "#{field.dom.id}___INDEX___value",
|
144
|
+
class: "flex-1 px-3 py-1 text-sm border border-gray-300 rounded focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
|
145
|
+
data_key_value_store_target: "valueInput"
|
146
|
+
)
|
147
|
+
|
148
|
+
button(
|
149
|
+
type: :button,
|
150
|
+
class: "px-2 py-1 text-red-600 hover:text-red-800 focus:outline-none",
|
151
|
+
data_action: "key-value-store#removePair"
|
152
|
+
) do
|
153
|
+
plain "×"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def pairs
|
160
|
+
@pairs ||= normalize_value_to_pairs(field.value)
|
161
|
+
end
|
162
|
+
|
163
|
+
def normalize_value_to_pairs(value)
|
164
|
+
case value
|
165
|
+
when Hash
|
166
|
+
# Convert hash to array of [key, value] pairs
|
167
|
+
value.to_a
|
168
|
+
when String
|
169
|
+
parse_json_string(value)
|
170
|
+
else
|
171
|
+
[]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_json_string(value)
|
176
|
+
return [] if value.blank?
|
177
|
+
|
178
|
+
begin
|
179
|
+
parsed = JSON.parse(value)
|
180
|
+
case parsed
|
181
|
+
when Hash
|
182
|
+
parsed.to_a
|
183
|
+
else
|
184
|
+
[]
|
185
|
+
end
|
186
|
+
rescue JSON::ParserError
|
187
|
+
[]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def field_name
|
192
|
+
field.dom.name
|
193
|
+
end
|
194
|
+
|
195
|
+
def limit
|
196
|
+
attributes.fetch(:limit, DEFAULT_LIMIT)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Override from ExtractsInput concern to normalize form parameters
|
200
|
+
def normalize_input(input_value)
|
201
|
+
case input_value
|
202
|
+
when Hash
|
203
|
+
if input_value.keys.all? { |k| k.to_s.match?(/^\d+$/) }
|
204
|
+
# Handle indexed form params: {"0" => {"key" => "foo", "value" => "bar"}}
|
205
|
+
process_indexed_params(input_value)
|
206
|
+
else
|
207
|
+
# Handle direct hash params
|
208
|
+
input_value.reject { |k, v| k.blank? || (v.blank? && v != false) }
|
209
|
+
end
|
210
|
+
when nil
|
211
|
+
{}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
# Process indexed form parameters into a hash
|
218
|
+
def process_indexed_params(params)
|
219
|
+
params.values.each_with_object({}) do |pair, hash|
|
220
|
+
next unless pair.is_a?(Hash)
|
221
|
+
|
222
|
+
key = pair["key"] || pair[:key]
|
223
|
+
value = pair["value"] || pair[:value]
|
224
|
+
|
225
|
+
if key.present?
|
226
|
+
hash[key] = value
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -20,14 +20,16 @@ module Plutonium
|
|
20
20
|
if helpers.current_turbo_frame == "remote_modal"
|
21
21
|
dialog(
|
22
22
|
closedby: "any",
|
23
|
-
class:
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
23
|
+
class: "rounded-md w-full max-w-3xl
|
24
|
+
bg-white dark:bg-gray-800
|
25
|
+
border border-gray-200 dark:border-gray-700
|
26
|
+
shadow-lg dark:shadow-gray-900/20
|
27
|
+
backdrop:bg-black/60 backdrop:backdrop-blur-sm
|
28
|
+
top-auto md:top-1/2 md:-translate-y-1/2 left-1/2 -translate-x-1/2
|
29
|
+
max-h-[80%] p-6
|
30
|
+
hidden open:flex flex-col
|
31
|
+
relative opacity-0 open:opacity-100
|
32
|
+
transition-opacity duration-300 ease-in-out",
|
31
33
|
data: {controller: "remote-modal"}
|
32
34
|
) do
|
33
35
|
render_page_header
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
@@ -0,0 +1,109 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
// Connects to data-controller="key-value-store"
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["container", "pair", "template", "addButton", "keyInput", "valueInput"]
|
6
|
+
static values = { limit: Number }
|
7
|
+
|
8
|
+
connect() {
|
9
|
+
this.updateIndices()
|
10
|
+
this.updateAddButtonState()
|
11
|
+
}
|
12
|
+
|
13
|
+
addPair(event) {
|
14
|
+
event.preventDefault()
|
15
|
+
|
16
|
+
if (this.pairTargets.length >= this.limitValue) {
|
17
|
+
return
|
18
|
+
}
|
19
|
+
|
20
|
+
const template = this.templateTarget
|
21
|
+
const newPair = template.content.cloneNode(true)
|
22
|
+
const index = this.pairTargets.length
|
23
|
+
|
24
|
+
// Update the template placeholders with actual indices
|
25
|
+
this.updatePairIndices(newPair, index)
|
26
|
+
|
27
|
+
this.containerTarget.appendChild(newPair)
|
28
|
+
this.updateAddButtonState()
|
29
|
+
|
30
|
+
// Focus on the key input of the new pair
|
31
|
+
const newKeyInput = this.containerTarget.lastElementChild.querySelector('[data-key-value-store-target="keyInput"]')
|
32
|
+
if (newKeyInput) {
|
33
|
+
newKeyInput.focus()
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
removePair(event) {
|
38
|
+
event.preventDefault()
|
39
|
+
|
40
|
+
const pair = event.target.closest('[data-key-value-store-target="pair"]')
|
41
|
+
if (pair) {
|
42
|
+
pair.remove()
|
43
|
+
this.updateIndices()
|
44
|
+
this.updateAddButtonState()
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
updateIndices() {
|
49
|
+
this.pairTargets.forEach((pair, index) => {
|
50
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
51
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
52
|
+
|
53
|
+
if (keyInput) {
|
54
|
+
keyInput.name = keyInput.name.replace(/\[\d+\]/, `[${index}]`)
|
55
|
+
}
|
56
|
+
if (valueInput) {
|
57
|
+
valueInput.name = valueInput.name.replace(/\[\d+\]/, `[${index}]`)
|
58
|
+
}
|
59
|
+
})
|
60
|
+
}
|
61
|
+
|
62
|
+
updatePairIndices(element, index) {
|
63
|
+
const inputs = element.querySelectorAll('input')
|
64
|
+
inputs.forEach(input => {
|
65
|
+
if (input.name) {
|
66
|
+
input.name = input.name.replace('__INDEX__', index)
|
67
|
+
}
|
68
|
+
})
|
69
|
+
}
|
70
|
+
|
71
|
+
updateAddButtonState() {
|
72
|
+
const addButton = this.addButtonTarget
|
73
|
+
if (this.pairTargets.length >= this.limitValue) {
|
74
|
+
addButton.disabled = true
|
75
|
+
addButton.classList.add('opacity-50', 'cursor-not-allowed')
|
76
|
+
} else {
|
77
|
+
addButton.disabled = false
|
78
|
+
addButton.classList.remove('opacity-50', 'cursor-not-allowed')
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
// Serialize the current key-value pairs to JSON
|
83
|
+
toJSON() {
|
84
|
+
const pairs = {}
|
85
|
+
this.pairTargets.forEach(pair => {
|
86
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
87
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
88
|
+
|
89
|
+
if (keyInput && valueInput && keyInput.value.trim()) {
|
90
|
+
pairs[keyInput.value.trim()] = valueInput.value
|
91
|
+
}
|
92
|
+
})
|
93
|
+
return JSON.stringify(pairs)
|
94
|
+
}
|
95
|
+
|
96
|
+
// Get the current key-value pairs as an object
|
97
|
+
toObject() {
|
98
|
+
const pairs = {}
|
99
|
+
this.pairTargets.forEach(pair => {
|
100
|
+
const keyInput = pair.querySelector('[data-key-value-store-target="keyInput"]')
|
101
|
+
const valueInput = pair.querySelector('[data-key-value-store-target="valueInput"]')
|
102
|
+
|
103
|
+
if (keyInput && valueInput && keyInput.value.trim()) {
|
104
|
+
pairs[keyInput.value.trim()] = valueInput.value
|
105
|
+
}
|
106
|
+
})
|
107
|
+
return pairs
|
108
|
+
}
|
109
|
+
}
|
@@ -19,6 +19,7 @@ import AttachmentPreviewContainerController from "./attachment_preview_container
|
|
19
19
|
import SidebarController from "./sidebar_controller.js"
|
20
20
|
import PasswordVisibilityController from "./password_visibility_controller.js"
|
21
21
|
import RemoteModalController from "./remote_modal_controller.js"
|
22
|
+
import KeyValueStoreController from "./key_value_st\ore_controller.js"
|
22
23
|
|
23
24
|
export default function (application) {
|
24
25
|
// Register controllers here
|
@@ -42,4 +43,5 @@ export default function (application) {
|
|
42
43
|
application.register("attachment-preview", AttachmentPreviewController)
|
43
44
|
application.register("attachment-preview-container", AttachmentPreviewContainerController)
|
44
45
|
application.register("remote-modal", RemoteModalController)
|
46
|
+
application.register("key-value-store", KeyValueStoreController)
|
45
47
|
}
|
data/src/js/plutonium.js
CHANGED
data/src/js/turbo/index.js
CHANGED
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.25.
|
4
|
+
version: 0.25.2
|
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-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -418,6 +418,7 @@ files:
|
|
418
418
|
- app/assets/plutonium.min.js
|
419
419
|
- app/assets/plutonium.min.js.map
|
420
420
|
- app/assets/plutonium.png
|
421
|
+
- app/javascript/controllers/key_value_store_controller.js
|
421
422
|
- app/views/layouts/resource.html.erb
|
422
423
|
- app/views/layouts/rodauth.html.erb
|
423
424
|
- app/views/plutonium/_flash.html.erb
|
@@ -839,6 +840,7 @@ files:
|
|
839
840
|
- lib/plutonium/ui/form/components/easymde.rb
|
840
841
|
- lib/plutonium/ui/form/components/flatpickr.rb
|
841
842
|
- lib/plutonium/ui/form/components/intl_tel_input.rb
|
843
|
+
- lib/plutonium/ui/form/components/key_value_store.rb
|
842
844
|
- lib/plutonium/ui/form/components/secure_association.rb
|
843
845
|
- lib/plutonium/ui/form/components/secure_polymorphic_association.rb
|
844
846
|
- lib/plutonium/ui/form/components/uppy.rb
|
@@ -908,6 +910,7 @@ files:
|
|
908
910
|
- src/js/controllers/form_controller.js
|
909
911
|
- src/js/controllers/frame_navigator_controller.js
|
910
912
|
- src/js/controllers/intl_tel_input_controller.js
|
913
|
+
- src/js/controllers/key_value_store_controller.js
|
911
914
|
- src/js/controllers/nested_resource_form_fields_controller.js
|
912
915
|
- src/js/controllers/password_visibility_controller.js
|
913
916
|
- src/js/controllers/register_controllers.js
|