rails_admin_next 1.0.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 (184) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +46 -0
  3. data/LICENSE.md +21 -0
  4. data/README.md +317 -0
  5. data/Rakefile +25 -0
  6. data/app/assets/javascripts/rails_admin/custom/ui.js +1 -0
  7. data/app/assets/stylesheets/rails_admin/framework.css +2375 -0
  8. data/app/assets/stylesheets/rails_admin/skin.css +472 -0
  9. data/app/assets/stylesheets/rails_admin/tokens.css +281 -0
  10. data/app/assets/stylesheets/rails_admin.css +47 -0
  11. data/app/controllers/rails_admin_next/application_controller.rb +98 -0
  12. data/app/controllers/rails_admin_next/main_controller.rb +185 -0
  13. data/app/helpers/rails_admin_next/application_helper.rb +254 -0
  14. data/app/helpers/rails_admin_next/form_builder.rb +173 -0
  15. data/app/helpers/rails_admin_next/main_helper.rb +63 -0
  16. data/app/views/layouts/rails_admin_next/_head.html.erb +29 -0
  17. data/app/views/layouts/rails_admin_next/_navigation.html.erb +18 -0
  18. data/app/views/layouts/rails_admin_next/_secondary_navigation.html.erb +27 -0
  19. data/app/views/layouts/rails_admin_next/_sidebar_navigation.html.erb +6 -0
  20. data/app/views/layouts/rails_admin_next/application.html.erb +42 -0
  21. data/app/views/layouts/rails_admin_next/content.html.erb +22 -0
  22. data/app/views/layouts/rails_admin_next/modal.js.erb +7 -0
  23. data/app/views/rails_admin_next/main/_dashboard_history.html.erb +46 -0
  24. data/app/views/rails_admin_next/main/_delete_notice.html.erb +36 -0
  25. data/app/views/rails_admin_next/main/_form_action_text.html.erb +2 -0
  26. data/app/views/rails_admin_next/main/_form_boolean.html.erb +15 -0
  27. data/app/views/rails_admin_next/main/_form_colorpicker.html.erb +6 -0
  28. data/app/views/rails_admin_next/main/_form_datetime.html.erb +14 -0
  29. data/app/views/rails_admin_next/main/_form_enumeration.html.erb +26 -0
  30. data/app/views/rails_admin_next/main/_form_field.html.erb +2 -0
  31. data/app/views/rails_admin_next/main/_form_file_upload.html.erb +18 -0
  32. data/app/views/rails_admin_next/main/_form_filtering_multiselect.html.erb +20 -0
  33. data/app/views/rails_admin_next/main/_form_filtering_select.html.erb +22 -0
  34. data/app/views/rails_admin_next/main/_form_multiple_file_upload.html.erb +21 -0
  35. data/app/views/rails_admin_next/main/_form_nested_many.html.erb +20 -0
  36. data/app/views/rails_admin_next/main/_form_nested_one.html.erb +19 -0
  37. data/app/views/rails_admin_next/main/_form_polymorphic_association.html.erb +24 -0
  38. data/app/views/rails_admin_next/main/_form_text.html.erb +2 -0
  39. data/app/views/rails_admin_next/main/_pagination.html.erb +54 -0
  40. data/app/views/rails_admin_next/main/_submit_buttons.html.erb +26 -0
  41. data/app/views/rails_admin_next/main/bulk_delete.html.erb +19 -0
  42. data/app/views/rails_admin_next/main/dashboard.html.erb +64 -0
  43. data/app/views/rails_admin_next/main/delete.html.erb +21 -0
  44. data/app/views/rails_admin_next/main/edit.html.erb +3 -0
  45. data/app/views/rails_admin_next/main/export.html.erb +149 -0
  46. data/app/views/rails_admin_next/main/history.html.erb +80 -0
  47. data/app/views/rails_admin_next/main/index.html.erb +186 -0
  48. data/app/views/rails_admin_next/main/new.html.erb +3 -0
  49. data/app/views/rails_admin_next/main/show.html.erb +30 -0
  50. data/config/brakeman.ignore +145 -0
  51. data/config/importmap.rails_admin.rb +29 -0
  52. data/config/initializers/active_record_extensions.rb +21 -0
  53. data/config/locales/rails_admin_next.en.yml +158 -0
  54. data/config/routes.rb +14 -0
  55. data/lib/generators/rails_admin_next/install_generator.rb +39 -0
  56. data/lib/generators/rails_admin_next/templates/initializer.erb +40 -0
  57. data/lib/generators/rails_admin_next/utils.rb +21 -0
  58. data/lib/rails_admin_next/abstract_model.rb +266 -0
  59. data/lib/rails_admin_next/adapters/active_record/association.rb +130 -0
  60. data/lib/rails_admin_next/adapters/active_record/object_extension.rb +13 -0
  61. data/lib/rails_admin_next/adapters/active_record/property.rb +58 -0
  62. data/lib/rails_admin_next/adapters/active_record.rb +371 -0
  63. data/lib/rails_admin_next/config/actions/base.rb +160 -0
  64. data/lib/rails_admin_next/config/actions/bulk_delete.rb +67 -0
  65. data/lib/rails_admin_next/config/actions/dashboard.rb +62 -0
  66. data/lib/rails_admin_next/config/actions/delete.rb +58 -0
  67. data/lib/rails_admin_next/config/actions/edit.rb +56 -0
  68. data/lib/rails_admin_next/config/actions/export.rb +47 -0
  69. data/lib/rails_admin_next/config/actions/history_index.rb +40 -0
  70. data/lib/rails_admin_next/config/actions/history_show.rb +40 -0
  71. data/lib/rails_admin_next/config/actions/index.rb +108 -0
  72. data/lib/rails_admin_next/config/actions/new.rb +69 -0
  73. data/lib/rails_admin_next/config/actions/show.rb +36 -0
  74. data/lib/rails_admin_next/config/actions/show_in_app.rb +37 -0
  75. data/lib/rails_admin_next/config/actions.rb +122 -0
  76. data/lib/rails_admin_next/config/configurable.rb +107 -0
  77. data/lib/rails_admin_next/config/const_load_suppressor.rb +78 -0
  78. data/lib/rails_admin_next/config/fields/association.rb +169 -0
  79. data/lib/rails_admin_next/config/fields/base.rb +391 -0
  80. data/lib/rails_admin_next/config/fields/collection_association.rb +90 -0
  81. data/lib/rails_admin_next/config/fields/factories/action_text.rb +14 -0
  82. data/lib/rails_admin_next/config/fields/factories/active_storage.rb +34 -0
  83. data/lib/rails_admin_next/config/fields/factories/association.rb +32 -0
  84. data/lib/rails_admin_next/config/fields/factories/encrypted_password.rb +28 -0
  85. data/lib/rails_admin_next/config/fields/factories/enum.rb +23 -0
  86. data/lib/rails_admin_next/config/fields/factories/password.rb +19 -0
  87. data/lib/rails_admin_next/config/fields/group.rb +77 -0
  88. data/lib/rails_admin_next/config/fields/singular_association.rb +62 -0
  89. data/lib/rails_admin_next/config/fields/types/action_text.rb +20 -0
  90. data/lib/rails_admin_next/config/fields/types/active_record_enum.rb +62 -0
  91. data/lib/rails_admin_next/config/fields/types/active_storage.rb +70 -0
  92. data/lib/rails_admin_next/config/fields/types/all.rb +32 -0
  93. data/lib/rails_admin_next/config/fields/types/belongs_to_association.rb +48 -0
  94. data/lib/rails_admin_next/config/fields/types/boolean.rb +70 -0
  95. data/lib/rails_admin_next/config/fields/types/citext.rb +15 -0
  96. data/lib/rails_admin_next/config/fields/types/color.rb +43 -0
  97. data/lib/rails_admin_next/config/fields/types/date.rb +38 -0
  98. data/lib/rails_admin_next/config/fields/types/datetime.rb +79 -0
  99. data/lib/rails_admin_next/config/fields/types/decimal.rb +23 -0
  100. data/lib/rails_admin_next/config/fields/types/enum.rb +52 -0
  101. data/lib/rails_admin_next/config/fields/types/file_upload.rb +87 -0
  102. data/lib/rails_admin_next/config/fields/types/float.rb +23 -0
  103. data/lib/rails_admin_next/config/fields/types/has_and_belongs_to_many_association.rb +16 -0
  104. data/lib/rails_admin_next/config/fields/types/has_many_association.rb +16 -0
  105. data/lib/rails_admin_next/config/fields/types/has_one_association.rb +35 -0
  106. data/lib/rails_admin_next/config/fields/types/hidden.rb +31 -0
  107. data/lib/rails_admin_next/config/fields/types/inet.rb +15 -0
  108. data/lib/rails_admin_next/config/fields/types/integer.rb +20 -0
  109. data/lib/rails_admin_next/config/fields/types/json.rb +37 -0
  110. data/lib/rails_admin_next/config/fields/types/multiple_active_storage.rb +82 -0
  111. data/lib/rails_admin_next/config/fields/types/multiple_file_upload.rb +134 -0
  112. data/lib/rails_admin_next/config/fields/types/numeric.rb +24 -0
  113. data/lib/rails_admin_next/config/fields/types/password.rb +45 -0
  114. data/lib/rails_admin_next/config/fields/types/polymorphic_association.rb +118 -0
  115. data/lib/rails_admin_next/config/fields/types/serialized.rb +28 -0
  116. data/lib/rails_admin_next/config/fields/types/string.rb +50 -0
  117. data/lib/rails_admin_next/config/fields/types/string_like.rb +25 -0
  118. data/lib/rails_admin_next/config/fields/types/text.rb +28 -0
  119. data/lib/rails_admin_next/config/fields/types/time.rb +39 -0
  120. data/lib/rails_admin_next/config/fields/types/timestamp.rb +16 -0
  121. data/lib/rails_admin_next/config/fields/types/uuid.rb +15 -0
  122. data/lib/rails_admin_next/config/fields/types.rb +29 -0
  123. data/lib/rails_admin_next/config/fields.rb +88 -0
  124. data/lib/rails_admin_next/config/groupable.rb +20 -0
  125. data/lib/rails_admin_next/config/has_description.rb +14 -0
  126. data/lib/rails_admin_next/config/has_fields.rb +144 -0
  127. data/lib/rails_admin_next/config/has_groups.rb +27 -0
  128. data/lib/rails_admin_next/config/hideable.rb +30 -0
  129. data/lib/rails_admin_next/config/inspectable.rb +39 -0
  130. data/lib/rails_admin_next/config/lazy_model.rb +74 -0
  131. data/lib/rails_admin_next/config/model.rb +126 -0
  132. data/lib/rails_admin_next/config/proxyable/proxy.rb +42 -0
  133. data/lib/rails_admin_next/config/proxyable.rb +26 -0
  134. data/lib/rails_admin_next/config/sections/base.rb +36 -0
  135. data/lib/rails_admin_next/config/sections/create.rb +13 -0
  136. data/lib/rails_admin_next/config/sections/edit.rb +13 -0
  137. data/lib/rails_admin_next/config/sections/export.rb +13 -0
  138. data/lib/rails_admin_next/config/sections/list.rb +63 -0
  139. data/lib/rails_admin_next/config/sections/modal.rb +12 -0
  140. data/lib/rails_admin_next/config/sections/nested.rb +12 -0
  141. data/lib/rails_admin_next/config/sections/show.rb +12 -0
  142. data/lib/rails_admin_next/config/sections/update.rb +12 -0
  143. data/lib/rails_admin_next/config/sections.rb +39 -0
  144. data/lib/rails_admin_next/config.rb +436 -0
  145. data/lib/rails_admin_next/engine.rb +91 -0
  146. data/lib/rails_admin_next/extension.rb +35 -0
  147. data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +90 -0
  148. data/lib/rails_admin_next/extensions/cancancan.rb +5 -0
  149. data/lib/rails_admin_next/extensions/controller_extension.rb +8 -0
  150. data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +192 -0
  151. data/lib/rails_admin_next/extensions/paper_trail.rb +5 -0
  152. data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +72 -0
  153. data/lib/rails_admin_next/extensions/pundit.rb +5 -0
  154. data/lib/rails_admin_next/extensions/url_for_extension.rb +15 -0
  155. data/lib/rails_admin_next/icons.rb +143 -0
  156. data/lib/rails_admin_next/support/composite_keys_serializer.rb +15 -0
  157. data/lib/rails_admin_next/support/csv_converter.rb +100 -0
  158. data/lib/rails_admin_next/support/hash_helper.rb +31 -0
  159. data/lib/rails_admin_next/support/paginated_collection.rb +79 -0
  160. data/lib/rails_admin_next/version.rb +17 -0
  161. data/lib/rails_admin_next.rb +72 -0
  162. data/lib/tasks/rails_admin.rake +40 -0
  163. data/src/rails_admin/application.js +15 -0
  164. data/src/rails_admin/base.js +12 -0
  165. data/src/rails_admin/controllers/alert_controller.js +28 -0
  166. data/src/rails_admin/controllers/collapse_controller.js +16 -0
  167. data/src/rails_admin/controllers/dropdown_controller.js +110 -0
  168. data/src/rails_admin/controllers/feedback_controller.js +73 -0
  169. data/src/rails_admin/controllers/filter_box_controller.js +346 -0
  170. data/src/rails_admin/controllers/filtering_multiselect_controller.js +386 -0
  171. data/src/rails_admin/controllers/filtering_select_controller.js +509 -0
  172. data/src/rails_admin/controllers/index.js +31 -0
  173. data/src/rails_admin/controllers/modal_controller.js +142 -0
  174. data/src/rails_admin/controllers/nested_form_controller.js +117 -0
  175. data/src/rails_admin/controllers/polymorphic_controller.js +26 -0
  176. data/src/rails_admin/controllers/remote_form_controller.js +173 -0
  177. data/src/rails_admin/controllers/sidescroll_controller.js +23 -0
  178. data/src/rails_admin/dom-ready.js +20 -0
  179. data/src/rails_admin/dom.js +19 -0
  180. data/src/rails_admin/i18n.js +20 -0
  181. data/src/rails_admin/icons.js +64 -0
  182. data/src/rails_admin/interactions.js +221 -0
  183. data/src/rails_admin/select_query.js +65 -0
  184. metadata +346 -0
@@ -0,0 +1,346 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import I18n from "rails_admin/i18n";
3
+ import { icon } from "rails_admin/icons";
4
+ import { el } from "rails_admin/dom";
5
+
6
+ function option(value, text, attrs = {}) {
7
+ const node = el("option", { value, ...attrs });
8
+ if (text != null) node.textContent = text;
9
+ return node;
10
+ }
11
+
12
+ // Attached to #filters_box, it renders the pre-existing filters (from the `f`
13
+ // query params, serialized into data-options) on connect, and — via
14
+ // document-level delegation (the "Add filter" menu lives in the navbar, far
15
+ // from #filters_box) — appends/removes filter rows and toggles enum
16
+ // multi-select mode. Visibility is not mirrored here: the hr.filters_box divider
17
+ // and each row's additional value fieldsets are derived by :has() CSS (skin.css)
18
+ // from the rows and their operator <select> state.
19
+ export default class extends Controller {
20
+ connect() {
21
+ this.box = this.element;
22
+
23
+ this.onAdd = (e) => {
24
+ const link = e.target.closest("#filters a");
25
+ if (!link) return;
26
+ e.preventDefault();
27
+ this.#append({
28
+ index: Date.now().toString().slice(6, 11),
29
+ ...JSON.parse(link.dataset.options),
30
+ });
31
+ };
32
+ this.onDelete = (e) => {
33
+ const del = e.target.closest("#filters_box .delete");
34
+ if (!del) return;
35
+ e.preventDefault();
36
+ del.closest(".filter")?.remove();
37
+ };
38
+ this.onSwitchSelect = (e) => {
39
+ const sw = e.target.closest("#filters_box .switch-select");
40
+ if (!sw) return;
41
+ e.preventDefault();
42
+ this.#switchSelect(sw);
43
+ };
44
+
45
+ document.addEventListener("click", this.onAdd);
46
+ document.addEventListener("click", this.onDelete);
47
+ document.addEventListener("click", this.onSwitchSelect);
48
+
49
+ this.#renderInitial();
50
+ }
51
+
52
+ disconnect() {
53
+ document.removeEventListener("click", this.onAdd);
54
+ document.removeEventListener("click", this.onDelete);
55
+ document.removeEventListener("click", this.onSwitchSelect);
56
+ // Clear so a browser back/forward restore doesn't double-render the filters.
57
+ this.box.replaceChildren();
58
+ }
59
+
60
+ #renderInitial() {
61
+ this.box.replaceChildren();
62
+ let initial = [];
63
+ try {
64
+ initial = JSON.parse(this.box.dataset.options || "[]");
65
+ } catch {
66
+ initial = [];
67
+ }
68
+ initial.forEach((options) => this.#append(options));
69
+ }
70
+
71
+ #append(options) {
72
+ const fieldLabel = options.label;
73
+ const fieldName = options.name;
74
+ const fieldType = options.type;
75
+ const fieldValue = options.value || "";
76
+ const fieldOperator = options.operator;
77
+ const operators = options.operators || [];
78
+ const index = options.index;
79
+ const valueName = `f[${fieldName}][${index}][v]`;
80
+ const operatorName = `f[${fieldName}][${index}][o]`;
81
+
82
+ let control = null;
83
+ let additional = null;
84
+
85
+ if (operators.length > 0) {
86
+ control = el("select", {
87
+ class: "form-control form-select form-select-sm",
88
+ name: operatorName,
89
+ });
90
+ operators.forEach((operator) => {
91
+ const element = this.#buildOperator(operator, options);
92
+ if (!element) return;
93
+ if (element.getAttribute("value") === fieldOperator)
94
+ element.selected = true;
95
+ control.appendChild(element);
96
+ });
97
+ if (control.querySelector("[data-additional-fieldset]")) {
98
+ control.classList.add("switch-additional-fieldsets");
99
+ }
100
+ }
101
+
102
+ switch (fieldType) {
103
+ case "boolean":
104
+ if (control) {
105
+ control.setAttribute("name", valueName);
106
+ control.querySelectorAll("option").forEach((o) => {
107
+ if (o.getAttribute("value") === fieldValue) o.selected = true;
108
+ });
109
+ }
110
+ break;
111
+ case "date":
112
+ case "datetime":
113
+ case "timestamp":
114
+ case "time": {
115
+ // Native HTML5 inputs accept/emit ISO only: date → %Y-%m-%d,
116
+ // time → %H:%M:%S (step=1 keeps seconds), datetime/timestamp → %Y-%m-%dT%H:%M:%S.
117
+ const nativeType =
118
+ fieldType === "date"
119
+ ? "date"
120
+ : fieldType === "time"
121
+ ? "time"
122
+ : "datetime-local";
123
+ additional = [undefined, "-∞", "∞"].map((placeholder, i) => {
124
+ const input = el("input", {
125
+ class: `input-sm form-control form-control-sm ${
126
+ fieldType === "date" ? "date" : "datetime"
127
+ }`,
128
+ type: nativeType,
129
+ step: nativeType === "date" ? null : 1,
130
+ name: `${valueName}[]`,
131
+ value: (fieldValue[i] ?? "") || "",
132
+ placeholder,
133
+ });
134
+ return el(
135
+ "span",
136
+ { class: `additional-fieldset ${i === 0 ? "default" : "between"}` },
137
+ input
138
+ );
139
+ });
140
+ break;
141
+ }
142
+ case "enum":
143
+ if (control) {
144
+ const multiple = Array.isArray(fieldValue);
145
+ control.setAttribute("name", multiple ? `${valueName}[]` : valueName);
146
+ if (multiple) control.setAttribute("multiple", "multiple");
147
+ control.querySelectorAll("option").forEach((o) => {
148
+ const value = o.getAttribute("value");
149
+ if (multiple ? fieldValue.includes(value) : value === fieldValue)
150
+ o.selected = true;
151
+ });
152
+ if (multiple) this.#setValuelessOptionsHidden(control, true);
153
+ const toggle = el(
154
+ "a",
155
+ { href: "#", class: "switch-select" },
156
+ icon(multiple ? "minus" : "plus")
157
+ );
158
+ control = [control, toggle];
159
+ }
160
+ break;
161
+ case "citext":
162
+ case "string":
163
+ case "text":
164
+ case "belongs_to_association":
165
+ case "has_one_association":
166
+ additional = el("input", {
167
+ class: "additional-fieldset form-control form-control-sm",
168
+ type: "text",
169
+ name: valueName,
170
+ value: fieldValue,
171
+ });
172
+ break;
173
+ case "integer":
174
+ case "decimal":
175
+ case "float": {
176
+ const main = el("input", {
177
+ class: "additional-fieldset default form-control form-control-sm",
178
+ type: fieldType,
179
+ name: `${valueName}[]`,
180
+ value: (fieldValue[0] ?? "") || "",
181
+ });
182
+ const lower = el("input", {
183
+ placeholder: "-∞",
184
+ class: "additional-fieldset between form-control form-control-sm",
185
+ type: fieldType,
186
+ name: `${valueName}[]`,
187
+ value: (fieldValue[1] ?? "") || "",
188
+ });
189
+ const upper = el("input", {
190
+ placeholder: "∞",
191
+ class: "additional-fieldset between form-control form-control-sm",
192
+ type: fieldType,
193
+ name: `${valueName}[]`,
194
+ value: (fieldValue[2] ?? "") || "",
195
+ });
196
+ additional = [main, lower, upper];
197
+ break;
198
+ }
199
+ default:
200
+ control = el("input", {
201
+ type: "text",
202
+ class: "form-control form-control-sm",
203
+ name: valueName,
204
+ value: fieldValue,
205
+ });
206
+ break;
207
+ }
208
+
209
+ const containerId = `${fieldName}-${index}-filter-container`;
210
+ document.getElementById(containerId)?.remove();
211
+
212
+ const deleteButton = el(
213
+ "button",
214
+ { type: "button", class: "btn btn-info btn-sm delete" },
215
+ icon("trash"),
216
+ document.createTextNode(fieldLabel)
217
+ );
218
+
219
+ const content = el(
220
+ "div",
221
+ { id: containerId, class: "filter d-inline-block my-1" },
222
+ deleteButton
223
+ );
224
+ content.append(" ");
225
+ [].concat(control).forEach((node) => node && content.append(node));
226
+ content.append(" ");
227
+ [].concat(additional).forEach((node) => node && content.append(node));
228
+
229
+ this.box.appendChild(content);
230
+ }
231
+
232
+ #buildOperator(operator, options) {
233
+ if (operator instanceof Object) {
234
+ const element = option(undefined, operator.label);
235
+ const { label, ...attrs } = operator;
236
+ for (const key in attrs) element.setAttribute(key, attrs[key]);
237
+ return element;
238
+ }
239
+ switch (operator) {
240
+ case "_discard":
241
+ return option("_discard", "...");
242
+ case "_separator":
243
+ return option(null, "---------", { disabled: "disabled" });
244
+ case "_present":
245
+ return option("_present", I18n.t("is_present"));
246
+ case "_blank":
247
+ return option("_blank", I18n.t("is_blank"));
248
+ case "_not_null":
249
+ return option("_not_null", I18n.t("is_present"));
250
+ case "_null":
251
+ return option("_null", I18n.t("is_blank"));
252
+ case "true":
253
+ return option("true", I18n.t("true"));
254
+ case "false":
255
+ return option("false", I18n.t("false"));
256
+ case "today":
257
+ return option("today", I18n.t("today"));
258
+ case "yesterday":
259
+ return option("yesterday", I18n.t("yesterday"));
260
+ case "this_week":
261
+ return option("this_week", I18n.t("this_week"));
262
+ case "last_week":
263
+ return option("last_week", I18n.t("last_week"));
264
+ case "like":
265
+ return option("like", I18n.t("contains"), {
266
+ "data-additional-fieldset": "additional-fieldset",
267
+ });
268
+ case "not_like":
269
+ return option("not_like", I18n.t("does_not_contain"), {
270
+ "data-additional-fieldset": "additional-fieldset",
271
+ });
272
+ case "is":
273
+ return option("is", I18n.t("is_exactly"), {
274
+ "data-additional-fieldset": "additional-fieldset",
275
+ });
276
+ case "starts_with":
277
+ return option("starts_with", I18n.t("starts_with"), {
278
+ "data-additional-fieldset": "additional-fieldset",
279
+ });
280
+ case "ends_with":
281
+ return option("ends_with", I18n.t("ends_with"), {
282
+ "data-additional-fieldset": "additional-fieldset",
283
+ });
284
+ case "default": {
285
+ let label;
286
+ switch (options.type) {
287
+ case "date":
288
+ case "datetime":
289
+ case "timestamp":
290
+ label = I18n.t("date");
291
+ break;
292
+ case "time":
293
+ label = I18n.t("time");
294
+ break;
295
+ case "integer":
296
+ case "decimal":
297
+ case "float":
298
+ label = I18n.t("number");
299
+ break;
300
+ }
301
+ return option("default", label, {
302
+ "data-additional-fieldset": "default",
303
+ });
304
+ }
305
+ case "between":
306
+ return option("between", I18n.t("between_and_"), {
307
+ "data-additional-fieldset": "between",
308
+ });
309
+ default:
310
+ return null;
311
+ }
312
+ }
313
+
314
+ #switchSelect(toggle) {
315
+ const select =
316
+ toggle.parentElement.querySelector("select") ||
317
+ toggle.previousElementSibling;
318
+ const multiple = !select.hasAttribute("multiple");
319
+ if (multiple) {
320
+ select.setAttribute("multiple", "multiple");
321
+ select.setAttribute("name", `${select.getAttribute("name")}[]`);
322
+ } else {
323
+ select.removeAttribute("multiple");
324
+ select.setAttribute(
325
+ "name",
326
+ select.getAttribute("name").replace(/\[\]$/, "")
327
+ );
328
+ }
329
+ this.#setValuelessOptionsHidden(select, multiple);
330
+ toggle.querySelector("svg")?.replaceWith(icon(multiple ? "minus" : "plus"));
331
+ }
332
+
333
+ // Multi-select mode lists enum values only, so the "..."/presence operator
334
+ // entries and the disabled separator hide. WebKit ignores display styling on
335
+ // <option>, so hide with the `hidden` attribute and additionally disable the
336
+ // value-carrying entries so they cannot be picked; the separator's own
337
+ // disabled state is left alone (it doubles as its selector hook).
338
+ #setValuelessOptionsHidden(select, hidden) {
339
+ select
340
+ .querySelectorAll("option[value^=_], option[disabled]")
341
+ .forEach((o) => {
342
+ o.hidden = hidden;
343
+ if (o.getAttribute("value")) o.disabled = hidden;
344
+ });
345
+ }
346
+ }