admin_suite 0.2.3 → 0.2.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac7ab643c69ac46cf31a6b48734cd58cae60233e8284a738147b419e4982fde3
|
|
4
|
+
data.tar.gz: b37617cd8e1f00fef844f95ddb81800f12990d48940c1a7d14cf47de1fd7e64e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d845685a1eabba91a371b975f3599bdf282a3940af720a9feb545751e044544a0aa8818611e742ebb37728fe86c5af41bb511462a211e9fcdc8e286649a67a05
|
|
7
|
+
data.tar.gz: dcc33dd1079fe5da4357445b939742d7cada340f858deabc45bdc3531dad59fe46a7c77c9ba33d9df5b0bc3ffe9f9c7b163a46b4484c3c99c5825e38f947acf9
|
|
@@ -145,23 +145,20 @@ module AdminSuite
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
def set_resource
|
|
148
|
-
|
|
149
|
-
rescue ActiveRecord::RecordNotFound
|
|
150
|
-
# Support "friendly" params (e.g. slugged records) without requiring host apps
|
|
151
|
-
# to change their model primary keys.
|
|
148
|
+
klass = resource_class
|
|
152
149
|
id = params[:id].to_s
|
|
153
|
-
columns =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
150
|
+
columns = klass.column_names
|
|
151
|
+
|
|
152
|
+
# Prevent ActiveRecord from coercing UUID-ish params like "2ce3-..."
|
|
153
|
+
# into integer ids (e.g., 2) for integer primary keys.
|
|
154
|
+
if non_numeric_id_for_numeric_primary_key?(klass, id)
|
|
155
|
+
@resource = find_friendly_resource!(klass, id, columns)
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@resource = klass.find(id)
|
|
160
|
+
rescue ActiveRecord::RecordNotFound
|
|
161
|
+
@resource = find_friendly_resource!(klass, id, columns)
|
|
165
162
|
end
|
|
166
163
|
|
|
167
164
|
def resource
|
|
@@ -244,5 +241,33 @@ module AdminSuite
|
|
|
244
241
|
def collection_url
|
|
245
242
|
resources_path(portal: current_portal, resource_name: resource_name)
|
|
246
243
|
end
|
|
244
|
+
|
|
245
|
+
def find_friendly_resource!(klass, id, columns = klass.column_names)
|
|
246
|
+
if columns.include?("slug")
|
|
247
|
+
record = klass.find_by(slug: id)
|
|
248
|
+
return record if record
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if columns.include?("uuid")
|
|
252
|
+
record = klass.find_by(uuid: id)
|
|
253
|
+
return record if record
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if columns.include?("token")
|
|
257
|
+
record = klass.find_by(token: id)
|
|
258
|
+
return record if record
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
raise ActiveRecord::RecordNotFound
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def non_numeric_id_for_numeric_primary_key?(klass, id)
|
|
265
|
+
primary_key = klass.primary_key.to_s
|
|
266
|
+
return false if primary_key.blank?
|
|
267
|
+
|
|
268
|
+
pk_type = klass.columns_hash[primary_key]&.type
|
|
269
|
+
numeric_pk = %i[integer bigint].include?(pk_type)
|
|
270
|
+
numeric_pk && id !~ /\A\d+\z/
|
|
271
|
+
end
|
|
247
272
|
end
|
|
248
273
|
end
|
|
@@ -1079,6 +1079,16 @@ module AdminSuite
|
|
|
1079
1079
|
current_label = if current_value.present? && collection.is_a?(Array)
|
|
1080
1080
|
match = collection.find { |opt| opt.is_a?(Array) ? opt[1].to_s == current_value.to_s : opt.to_s == current_value.to_s }
|
|
1081
1081
|
match.is_a?(Array) ? match[0] : match.to_s
|
|
1082
|
+
elsif current_value.present? && collection.is_a?(String)
|
|
1083
|
+
association_name = field.name.to_s.sub(/_id\z/, "")
|
|
1084
|
+
assoc = resource.public_send(association_name) if resource.respond_to?(association_name)
|
|
1085
|
+
if assoc.respond_to?(:name) && assoc.name.present?
|
|
1086
|
+
assoc.name
|
|
1087
|
+
elsif assoc.respond_to?(:title) && assoc.title.present?
|
|
1088
|
+
assoc.title
|
|
1089
|
+
else
|
|
1090
|
+
current_value
|
|
1091
|
+
end
|
|
1082
1092
|
else
|
|
1083
1093
|
current_value
|
|
1084
1094
|
end
|
|
@@ -1088,7 +1098,8 @@ module AdminSuite
|
|
|
1088
1098
|
controller: "admin-suite--searchable-select",
|
|
1089
1099
|
"admin-suite--searchable-select-options-value": options_json,
|
|
1090
1100
|
"admin-suite--searchable-select-creatable-value": field.create_url.present?,
|
|
1091
|
-
"admin-suite--searchable-select-search-url-value": collection.is_a?(String) ? collection : ""
|
|
1101
|
+
"admin-suite--searchable-select-search-url-value": collection.is_a?(String) ? collection : "",
|
|
1102
|
+
"admin-suite--searchable-select-create-url-value": field.create_url.to_s
|
|
1092
1103
|
},
|
|
1093
1104
|
class: "relative") do
|
|
1094
1105
|
concat(hidden_field_tag("#{param_key}[#{field.name}]", current_value, data: { "admin-suite--searchable-select-target": "input" }))
|
|
@@ -11,6 +11,7 @@ export default class extends Controller {
|
|
|
11
11
|
options: { type: Array, default: [] },
|
|
12
12
|
creatable: { type: Boolean, default: false },
|
|
13
13
|
searchUrl: { type: String, default: "" },
|
|
14
|
+
createUrl: { type: String, default: "" },
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
connect() {
|
|
@@ -55,16 +56,8 @@ export default class extends Controller {
|
|
|
55
56
|
opt.label.toLowerCase().includes(query),
|
|
56
57
|
)
|
|
57
58
|
|
|
58
|
-
if (
|
|
59
|
-
this.
|
|
60
|
-
query &&
|
|
61
|
-
!this.filteredOptions.some((o) => o.value === query)
|
|
62
|
-
) {
|
|
63
|
-
this.filteredOptions.push({
|
|
64
|
-
value: query,
|
|
65
|
-
label: `Create "${query}"`,
|
|
66
|
-
isNew: true,
|
|
67
|
-
})
|
|
59
|
+
if (this.creatableValue && query && !this.hasExactOptionMatch(query)) {
|
|
60
|
+
this.filteredOptions.push(this.buildCreateOption(query))
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
this.renderDropdown()
|
|
@@ -81,19 +74,11 @@ export default class extends Controller {
|
|
|
81
74
|
const data = await response.json()
|
|
82
75
|
this.filteredOptions = data.map((item) => ({
|
|
83
76
|
value: item.id || item.value,
|
|
84
|
-
label: item.name || item.label,
|
|
77
|
+
label: item.name || item.title || item.label,
|
|
85
78
|
}))
|
|
86
79
|
|
|
87
|
-
if (
|
|
88
|
-
this.
|
|
89
|
-
query &&
|
|
90
|
-
!this.filteredOptions.some((o) => o.value === query)
|
|
91
|
-
) {
|
|
92
|
-
this.filteredOptions.push({
|
|
93
|
-
value: query,
|
|
94
|
-
label: `Create "${query}"`,
|
|
95
|
-
isNew: true,
|
|
96
|
-
})
|
|
80
|
+
if (this.creatableValue && query && !this.hasExactOptionMatch(query)) {
|
|
81
|
+
this.filteredOptions.push(this.buildCreateOption(query))
|
|
97
82
|
}
|
|
98
83
|
|
|
99
84
|
this.renderDropdown()
|
|
@@ -117,7 +102,9 @@ export default class extends Controller {
|
|
|
117
102
|
class="block w-full text-left px-3 py-2 text-sm hover:bg-slate-100 ${index === this.selectedIndex ? "bg-slate-100" : ""} ${opt.isNew ? "text-indigo-600 font-medium" : "text-slate-700"}"
|
|
118
103
|
data-action="click->admin-suite--searchable-select#select"
|
|
119
104
|
data-value="${opt.value}"
|
|
120
|
-
data-label="${opt.label}"
|
|
105
|
+
data-label="${opt.label}"
|
|
106
|
+
data-create-label="${opt.createLabel || ""}"
|
|
107
|
+
data-is-new="${opt.isNew ? "true" : "false"}">
|
|
121
108
|
${opt.label}
|
|
122
109
|
</button>
|
|
123
110
|
`,
|
|
@@ -125,18 +112,12 @@ export default class extends Controller {
|
|
|
125
112
|
.join("")
|
|
126
113
|
}
|
|
127
114
|
|
|
128
|
-
select(event) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
.replace(/^Create "/, "")
|
|
132
|
-
.replace(/"$/, "")
|
|
133
|
-
|
|
134
|
-
this.inputTarget.value = value
|
|
135
|
-
this.searchTarget.value = label
|
|
136
|
-
this.close()
|
|
115
|
+
async select(event) {
|
|
116
|
+
const option = this.optionFromElement(event.currentTarget)
|
|
117
|
+
await this.applyOption(option)
|
|
137
118
|
}
|
|
138
119
|
|
|
139
|
-
keydown(event) {
|
|
120
|
+
async keydown(event) {
|
|
140
121
|
switch (event.key) {
|
|
141
122
|
case "ArrowDown":
|
|
142
123
|
event.preventDefault()
|
|
@@ -154,12 +135,8 @@ export default class extends Controller {
|
|
|
154
135
|
case "Enter":
|
|
155
136
|
event.preventDefault()
|
|
156
137
|
if (this.selectedIndex >= 0 && this.filteredOptions[this.selectedIndex]) {
|
|
157
|
-
const
|
|
158
|
-
this.
|
|
159
|
-
this.searchTarget.value = opt.label
|
|
160
|
-
.replace(/^Create "/, "")
|
|
161
|
-
.replace(/"$/, "")
|
|
162
|
-
this.close()
|
|
138
|
+
const option = this.filteredOptions[this.selectedIndex]
|
|
139
|
+
await this.applyOption(option)
|
|
163
140
|
}
|
|
164
141
|
break
|
|
165
142
|
case "Escape":
|
|
@@ -167,5 +144,111 @@ export default class extends Controller {
|
|
|
167
144
|
break
|
|
168
145
|
}
|
|
169
146
|
}
|
|
147
|
+
|
|
148
|
+
hasExactOptionMatch(query) {
|
|
149
|
+
return this.filteredOptions.some((opt) => {
|
|
150
|
+
const value = String(opt.value || "").toLowerCase().trim()
|
|
151
|
+
const label = String(opt.label || "").toLowerCase().trim()
|
|
152
|
+
return value === query || label === query
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
buildCreateOption(query) {
|
|
157
|
+
return {
|
|
158
|
+
value: query,
|
|
159
|
+
label: `Create "${query}"`,
|
|
160
|
+
createLabel: query,
|
|
161
|
+
isNew: true,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
optionFromElement(element) {
|
|
166
|
+
const isNew = element.dataset.isNew === "true"
|
|
167
|
+
return {
|
|
168
|
+
value: element.dataset.value,
|
|
169
|
+
label: isNew
|
|
170
|
+
? element.dataset.createLabel || element.dataset.label
|
|
171
|
+
: element.dataset.label,
|
|
172
|
+
isNew,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async applyOption(option) {
|
|
177
|
+
if (!option) return
|
|
178
|
+
|
|
179
|
+
let value = option.value
|
|
180
|
+
let label = option.label || ""
|
|
181
|
+
|
|
182
|
+
if (option.isNew) {
|
|
183
|
+
if (!this.createUrlValue) {
|
|
184
|
+
this.showInlineError("Create URL is not configured for this field.")
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const created = await this.createOption(label)
|
|
189
|
+
if (!created) return
|
|
190
|
+
|
|
191
|
+
value = created.id
|
|
192
|
+
label = created.name || created.title || label
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.inputTarget.value = value
|
|
196
|
+
this.searchTarget.value = label
|
|
197
|
+
this.close()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async createOption(label) {
|
|
201
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
202
|
+
this.searchTarget.disabled = true
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetch(this.createUrlValue, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
Accept: "application/json",
|
|
210
|
+
...(csrfToken ? { "X-CSRF-Token": csrfToken } : {}),
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
name: label,
|
|
214
|
+
title: label,
|
|
215
|
+
}),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const data = await response.json().catch(() => ({}))
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const errors = Array.isArray(data?.errors) ? data.errors.join(", ") : null
|
|
221
|
+
this.showInlineError(errors || "Could not create this option.")
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!data?.id) {
|
|
226
|
+
this.showInlineError("Create endpoint returned no id.")
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return data
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error("Create failed:", error)
|
|
233
|
+
this.showInlineError("Could not create this option.")
|
|
234
|
+
return null
|
|
235
|
+
} finally {
|
|
236
|
+
this.searchTarget.disabled = false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
showInlineError(message) {
|
|
241
|
+
this.dropdownTarget.innerHTML = `
|
|
242
|
+
<div class="px-3 py-2 text-sm text-red-600">${this.escapeHtml(message)}</div>
|
|
243
|
+
`
|
|
244
|
+
this.dropdownTarget.classList.remove("hidden")
|
|
245
|
+
this.isOpen = true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
escapeHtml(value) {
|
|
249
|
+
const div = document.createElement("div")
|
|
250
|
+
div.textContent = value
|
|
251
|
+
return div.innerHTML
|
|
252
|
+
}
|
|
170
253
|
}
|
|
171
254
|
|
data/lib/admin_suite/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: admin_suite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TechWright Labs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|