flexi_admin 0.0.5 → 0.0.6
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/README.md +254 -18
- data/lib/flexi_admin/components/helpers/action_helper.rb +21 -0
- data/lib/flexi_admin/components/helpers/frontend_helper.rb +23 -0
- data/lib/flexi_admin/components/helpers/link_helper.rb +35 -2
- data/lib/flexi_admin/components/helpers/resource_helper.rb +79 -14
- data/lib/flexi_admin/components/helpers/url_helper.rb +15 -0
- data/lib/flexi_admin/components/nav/breadcrumbs_component.html.slim +13 -0
- data/lib/flexi_admin/components/nav/breadcrumbs_component.rb +86 -0
- data/lib/flexi_admin/components/resource/autocomplete_component.html.slim +19 -7
- data/lib/flexi_admin/components/resource/autocomplete_component.rb +41 -11
- data/lib/flexi_admin/components/resource/button_select_component.html.slim +2 -2
- data/lib/flexi_admin/components/resource/button_select_component.rb +8 -1
- data/lib/flexi_admin/components/resource/filter_component.html.slim +47 -0
- data/lib/flexi_admin/components/resource/filter_component.rb +51 -0
- data/lib/flexi_admin/components/resource/form_component.rb +16 -1
- data/lib/flexi_admin/components/resource/form_element_component.rb +1 -1
- data/lib/flexi_admin/components/resource/form_mixin.rb +186 -42
- data/lib/flexi_admin/components/resource/link_action_component.html.slim +2 -2
- data/lib/flexi_admin/components/resource/link_action_component.rb +3 -2
- data/lib/flexi_admin/components/resource/view_component.html.slim +14 -4
- data/lib/flexi_admin/components/resource/view_component.rb +11 -2
- data/lib/flexi_admin/components/resources/bulk_action/button_component.rb +8 -1
- data/lib/flexi_admin/components/resources/bulk_action/modal_component.html.slim +21 -15
- data/lib/flexi_admin/components/resources/bulk_action/modal_component.rb +16 -8
- data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.html.slim +16 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.rb +78 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.html.slim +15 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.rb +33 -0
- data/lib/flexi_admin/components/resources/grid_view/card_component.html.slim +4 -1
- data/lib/flexi_admin/components/resources/grid_view/card_component.rb +4 -1
- data/lib/flexi_admin/components/resources/grid_view/grid_component.html.slim +3 -1
- data/lib/flexi_admin/components/resources/grid_view/grid_component.rb +4 -2
- data/lib/flexi_admin/components/resources/grid_view_component.rb +7 -2
- data/lib/flexi_admin/components/resources/index_page_component.html.slim +5 -4
- data/lib/flexi_admin/components/resources/index_page_component.rb +1 -0
- data/lib/flexi_admin/components/resources/list_view/table_component.html.slim +1 -1
- data/lib/flexi_admin/components/resources/list_view_component.rb +2 -1
- data/lib/flexi_admin/components/resources/pagination_component.html.slim +51 -39
- data/lib/flexi_admin/components/resources/pagination_component.rb +52 -47
- data/lib/flexi_admin/components/resources/switch_view_component.html.slim +2 -2
- data/lib/flexi_admin/components/resources/view_component.html.slim +15 -6
- data/lib/flexi_admin/components/shared/medium_component.html.slim +1 -1
- data/lib/flexi_admin/components/shared/table/header_item_component.html.slim +1 -1
- data/lib/flexi_admin/components/shared/table/header_item_component.rb +59 -61
- data/lib/flexi_admin/components/shared/tabs_component.html.slim +26 -0
- data/lib/flexi_admin/components/shared/tabs_component.rb +68 -0
- data/lib/flexi_admin/components.rb +11 -3
- data/lib/flexi_admin/config.rb +21 -3
- data/lib/flexi_admin/controllers/modals_controller.rb +4 -2
- data/lib/flexi_admin/controllers/resources_controller.rb +246 -51
- data/lib/flexi_admin/engine.rb +4 -0
- data/lib/flexi_admin/helpers/application_helper.rb +3 -0
- data/lib/flexi_admin/javascript/controllers/add_row_controller.js +118 -0
- data/lib/flexi_admin/javascript/controllers/autocomplete_controller.js +17 -1
- data/lib/flexi_admin/javascript/controllers/bulk_action_controller.js +192 -33
- data/lib/flexi_admin/javascript/controllers/copy_to_clipboard_controller.js +16 -0
- data/lib/flexi_admin/javascript/controllers/datalist_controller.js +1 -1
- data/lib/flexi_admin/javascript/controllers/delete_controller.js +40 -0
- data/lib/flexi_admin/javascript/controllers/expandable_field_controller.js +15 -0
- data/lib/flexi_admin/javascript/controllers/filter_auto_submit_controller.js +93 -0
- data/lib/flexi_admin/javascript/controllers/index.js +18 -0
- data/lib/flexi_admin/javascript/controllers/pagination_controller.js +8 -0
- data/lib/flexi_admin/javascript/controllers/tabs_controller.js +24 -0
- data/lib/flexi_admin/models/concerns/application_resource.rb +6 -0
- data/lib/flexi_admin/models/concerns/parentable.rb +3 -0
- data/lib/flexi_admin/models/context_params.rb +35 -10
- data/lib/flexi_admin/models/resources/context.rb +4 -0
- data/lib/flexi_admin/routes.rb +7 -1
- data/lib/flexi_admin/services/create_resource.rb +6 -6
- data/lib/flexi_admin/version.rb +1 -1
- data/lib/flexi_admin/views/shared/_reload_frame.slim +19 -0
- data/lib/flexi_admin.rb +0 -3
- metadata +171 -10
|
@@ -7,16 +7,27 @@ export default class extends Controller {
|
|
|
7
7
|
scope: String,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
static targets = ["counter", "defaultText", "selectionText"];
|
|
11
|
+
|
|
10
12
|
connect() {
|
|
11
|
-
this.
|
|
13
|
+
this._loadFromStorage();
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// Bind the handler so we can properly remove it in disconnect
|
|
16
|
+
this._boundModalOpened = this._modalOpened.bind(this);
|
|
17
|
+
document.addEventListener("bulk-action-modal-opened", this._boundModalOpened);
|
|
18
|
+
|
|
19
|
+
// Track shiftKey from click events (change events don't carry modifier keys)
|
|
20
|
+
this._boundTrackShift = (e) => { this._shiftDown = e.shiftKey; };
|
|
21
|
+
this.element.addEventListener("click", this._boundTrackShift, true);
|
|
22
|
+
|
|
23
|
+
// Restore checkbox states based on stored selections
|
|
24
|
+
this._restoreCheckboxStates();
|
|
25
|
+
this._updateSelectionUI();
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
disconnect() {
|
|
19
|
-
document.removeEventListener("bulk-action-modal-opened", this.
|
|
29
|
+
document.removeEventListener("bulk-action-modal-opened", this._boundModalOpened);
|
|
30
|
+
this.element.removeEventListener("click", this._boundTrackShift, true);
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
async requestModal(event) {
|
|
@@ -30,21 +41,41 @@ export default class extends Controller {
|
|
|
30
41
|
const modal = this._withModal();
|
|
31
42
|
const form = modal.querySelector("form");
|
|
32
43
|
form.requestSubmit();
|
|
44
|
+
this._unselectAll();
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
toggle(event) {
|
|
36
|
-
const
|
|
48
|
+
const checkbox = event.target;
|
|
49
|
+
const checkboxes = this._allCheckboxes();
|
|
37
50
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
this.
|
|
51
|
+
if (this._shiftDown && this._lastClickedCheckbox) {
|
|
52
|
+
const currentIndex = checkboxes.indexOf(checkbox);
|
|
53
|
+
const lastIndex = checkboxes.indexOf(this._lastClickedCheckbox);
|
|
54
|
+
|
|
55
|
+
if (currentIndex !== -1 && lastIndex !== -1) {
|
|
56
|
+
const start = Math.min(currentIndex, lastIndex);
|
|
57
|
+
const end = Math.max(currentIndex, lastIndex);
|
|
58
|
+
|
|
59
|
+
for (let i = start; i <= end; i++) {
|
|
60
|
+
checkboxes[i].checked = true;
|
|
61
|
+
if (!this.selectedIds.includes(checkboxes[i].value)) {
|
|
62
|
+
this.selectedIds.push(checkboxes[i].value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
41
66
|
} else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
|
|
67
|
+
const id = checkbox.value;
|
|
68
|
+
if (checkbox.checked) {
|
|
69
|
+
this.selectedIds.push(id);
|
|
70
|
+
} else {
|
|
71
|
+
this.selectedIds = this.selectedIds.filter(
|
|
72
|
+
(selectedId) => selectedId !== id
|
|
73
|
+
);
|
|
74
|
+
}
|
|
46
75
|
}
|
|
47
76
|
|
|
77
|
+
this._lastClickedCheckbox = checkbox;
|
|
78
|
+
|
|
48
79
|
if (this.selectedIds.length > 0) {
|
|
49
80
|
this._enableActions();
|
|
50
81
|
} else {
|
|
@@ -52,7 +83,8 @@ export default class extends Controller {
|
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
this._persist();
|
|
55
|
-
|
|
86
|
+
this._saveToStorage();
|
|
87
|
+
this._updateSelectionUI();
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
toggleAll(event) {
|
|
@@ -63,6 +95,26 @@ export default class extends Controller {
|
|
|
63
95
|
}
|
|
64
96
|
}
|
|
65
97
|
|
|
98
|
+
navigateWithSelectedIds(event) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
|
|
101
|
+
if (this.selectedIds.length === 0) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const target = event.target || event.currentTarget;
|
|
106
|
+
const baseUrl = target.dataset.baseUrl;
|
|
107
|
+
const paramName = target.dataset.paramName || 'selected_ids[]';
|
|
108
|
+
const params = new URLSearchParams();
|
|
109
|
+
|
|
110
|
+
this.selectedIds.forEach(id => {
|
|
111
|
+
params.append(paramName, id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
115
|
+
window.Turbo.visit(url);
|
|
116
|
+
}
|
|
117
|
+
|
|
66
118
|
_modalOpened(event) {
|
|
67
119
|
if (event.detail.scope !== this.scopeValue) {
|
|
68
120
|
return;
|
|
@@ -70,20 +122,50 @@ export default class extends Controller {
|
|
|
70
122
|
|
|
71
123
|
const modal = this._withModal();
|
|
72
124
|
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
// If selectedIds are provided in the event (from row action), use them
|
|
126
|
+
// Otherwise, use the controller's selectedIds (from bulk checkbox selection)
|
|
127
|
+
const idsToUse = event.detail.selectedIds && event.detail.selectedIds.length > 0
|
|
128
|
+
? event.detail.selectedIds
|
|
129
|
+
: this.selectedIds;
|
|
130
|
+
|
|
131
|
+
this._populateCountElements(modal, idsToUse.length);
|
|
132
|
+
this._populateIds(modal, idsToUse);
|
|
75
133
|
this._addProcessor(modal, event.detail.modalId);
|
|
134
|
+
this._addScope(modal, event.detail.scope);
|
|
76
135
|
}
|
|
77
136
|
|
|
78
|
-
_populateCountElements(modal) {
|
|
137
|
+
_populateCountElements(modal, count = null) {
|
|
138
|
+
const countToUse = count !== null ? count : this.selectedIds.length;
|
|
79
139
|
const countElements = modal.querySelectorAll("span.count");
|
|
80
140
|
countElements.forEach((countElement) => {
|
|
81
|
-
countElement.textContent =
|
|
141
|
+
countElement.textContent = countToUse;
|
|
82
142
|
});
|
|
83
143
|
}
|
|
84
144
|
|
|
145
|
+
_addScope(modal, scope) {
|
|
146
|
+
const form = modal.querySelector("form");
|
|
147
|
+
|
|
148
|
+
const existingInput = form.querySelector('input[name="fa_scope"]');
|
|
149
|
+
if (existingInput) {
|
|
150
|
+
existingInput.remove();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const hiddenInput = document.createElement("input");
|
|
154
|
+
hiddenInput.type = "hidden";
|
|
155
|
+
hiddenInput.name = "fa_scope";
|
|
156
|
+
hiddenInput.value = scope;
|
|
157
|
+
form.appendChild(hiddenInput);
|
|
158
|
+
}
|
|
159
|
+
|
|
85
160
|
_addProcessor(modal, processor) {
|
|
86
161
|
const form = modal.querySelector("form");
|
|
162
|
+
|
|
163
|
+
// Remove any existing processor input to prevent duplicates when called multiple times
|
|
164
|
+
const existingProcessorInput = form.querySelector('input[name="processor"]');
|
|
165
|
+
if (existingProcessorInput) {
|
|
166
|
+
existingProcessorInput.remove();
|
|
167
|
+
}
|
|
168
|
+
|
|
87
169
|
const hiddenInput = document.createElement("input");
|
|
88
170
|
hiddenInput.type = "hidden";
|
|
89
171
|
hiddenInput.name = "processor";
|
|
@@ -91,12 +173,20 @@ export default class extends Controller {
|
|
|
91
173
|
form.appendChild(hiddenInput);
|
|
92
174
|
}
|
|
93
175
|
|
|
94
|
-
_populateIds(modal) {
|
|
176
|
+
_populateIds(modal, ids = null) {
|
|
177
|
+
const idsToUse = ids !== null ? ids : this.selectedIds;
|
|
95
178
|
const form = modal.querySelector("form");
|
|
179
|
+
|
|
180
|
+
// Remove any existing ids input to prevent duplicates when called multiple times
|
|
181
|
+
const existingIdsInput = form.querySelector('input[name="ids"]');
|
|
182
|
+
if (existingIdsInput) {
|
|
183
|
+
existingIdsInput.remove();
|
|
184
|
+
}
|
|
185
|
+
|
|
96
186
|
const hiddenInput = document.createElement("input");
|
|
97
187
|
hiddenInput.type = "hidden";
|
|
98
188
|
hiddenInput.name = "ids";
|
|
99
|
-
hiddenInput.value = JSON.stringify(
|
|
189
|
+
hiddenInput.value = JSON.stringify(idsToUse);
|
|
100
190
|
form.appendChild(hiddenInput);
|
|
101
191
|
}
|
|
102
192
|
|
|
@@ -105,39 +195,39 @@ export default class extends Controller {
|
|
|
105
195
|
}
|
|
106
196
|
|
|
107
197
|
_selectAll() {
|
|
108
|
-
|
|
109
|
-
const checkboxes = document.querySelectorAll(
|
|
110
|
-
`.bulk-action-checkbox > input[name="${this.scopeValue}"]`
|
|
111
|
-
);
|
|
198
|
+
const checkboxes = this._allCheckboxes();
|
|
112
199
|
|
|
113
|
-
this.selectedIds =
|
|
200
|
+
this.selectedIds = checkboxes.map((checkbox) => checkbox.value);
|
|
114
201
|
|
|
115
|
-
|
|
202
|
+
checkboxes.forEach((checkbox) => {
|
|
116
203
|
checkbox.checked = true;
|
|
117
204
|
});
|
|
118
205
|
|
|
119
206
|
this._persist();
|
|
207
|
+
this._saveToStorage();
|
|
208
|
+
this._updateSelectionUI();
|
|
120
209
|
this._enableActions();
|
|
121
|
-
// console.log(`${this.scopeValue} select all`, this.selectedIds.length);
|
|
122
210
|
}
|
|
123
211
|
|
|
124
212
|
_unselectAll() {
|
|
125
213
|
this.selectedIds = [];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
Array.from(checkboxes).forEach((checkbox) => {
|
|
214
|
+
this._lastClickedCheckbox = null;
|
|
215
|
+
const checkboxes = this._allCheckboxes();
|
|
216
|
+
checkboxes.forEach((checkbox) => {
|
|
130
217
|
checkbox.checked = false;
|
|
131
218
|
});
|
|
132
219
|
|
|
220
|
+
const selectAllCheckbox = document.querySelector('#checkbox-all');
|
|
221
|
+
if (selectAllCheckbox) selectAllCheckbox.checked = false;
|
|
222
|
+
|
|
133
223
|
this._persist();
|
|
224
|
+
this._saveToStorage();
|
|
225
|
+
this._updateSelectionUI();
|
|
134
226
|
this._disableActions();
|
|
135
|
-
// console.log(`${this.scopeValue} unselect all`, this.selectedIds.length);
|
|
136
227
|
}
|
|
137
228
|
|
|
138
229
|
_persist() {
|
|
139
230
|
this.element.dataset.ids = JSON.stringify(this.selectedIds);
|
|
140
|
-
// console.log("persist", this.element.dataset.ids);
|
|
141
231
|
}
|
|
142
232
|
|
|
143
233
|
_enableActions() {
|
|
@@ -155,4 +245,73 @@ export default class extends Controller {
|
|
|
155
245
|
item.classList.add("disabled");
|
|
156
246
|
});
|
|
157
247
|
}
|
|
248
|
+
|
|
249
|
+
clearSelection(event) {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
this._unselectAll();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_storageKey() {
|
|
255
|
+
return `bulk_action_selection_${this.scopeValue}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_loadFromStorage() {
|
|
259
|
+
const stored = sessionStorage.getItem(this._storageKey());
|
|
260
|
+
if (stored) {
|
|
261
|
+
try {
|
|
262
|
+
this.selectedIds = JSON.parse(stored);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
this.selectedIds = [];
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
this.selectedIds = [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_saveToStorage() {
|
|
272
|
+
sessionStorage.setItem(this._storageKey(), JSON.stringify(this.selectedIds));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_restoreCheckboxStates() {
|
|
276
|
+
if (this.selectedIds.length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const checkboxes = this._allCheckboxes();
|
|
281
|
+
checkboxes.forEach((checkbox) => {
|
|
282
|
+
if (this.selectedIds.includes(checkbox.value)) {
|
|
283
|
+
checkbox.checked = true;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (this.selectedIds.length > 0) {
|
|
288
|
+
this._enableActions();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_allCheckboxes() {
|
|
293
|
+
return Array.from(document.querySelectorAll(
|
|
294
|
+
`.bulk-action-checkbox > input[name="${this.scopeValue}"]:not(#checkbox-all)`
|
|
295
|
+
));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_updateSelectionUI() {
|
|
299
|
+
const count = this.selectedIds.length;
|
|
300
|
+
|
|
301
|
+
// Update counter value
|
|
302
|
+
if (this.hasCounterTarget) {
|
|
303
|
+
this.counterTarget.textContent = count;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Toggle between default text and selection text
|
|
307
|
+
if (this.hasDefaultTextTarget && this.hasSelectionTextTarget) {
|
|
308
|
+
if (count > 0) {
|
|
309
|
+
this.defaultTextTarget.style.display = 'none';
|
|
310
|
+
this.selectionTextTarget.style.display = 'inline';
|
|
311
|
+
} else {
|
|
312
|
+
this.defaultTextTarget.style.display = 'inline';
|
|
313
|
+
this.selectionTextTarget.style.display = 'none';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
158
317
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
copy() {
|
|
5
|
+
const input = this.element.querySelector("input, textarea")
|
|
6
|
+
if (!input) return
|
|
7
|
+
|
|
8
|
+
navigator.clipboard.writeText(input.value).then(() => {
|
|
9
|
+
const icon = this.element.querySelector(".copy-field-btn i")
|
|
10
|
+
if (!icon) return
|
|
11
|
+
|
|
12
|
+
icon.classList.replace("bi-clipboard", "bi-check")
|
|
13
|
+
setTimeout(() => icon.classList.replace("bi-check", "bi-clipboard"), 1500)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import { CSRFToken } from "../utils";
|
|
3
|
+
|
|
4
|
+
// Connects to data-controller="delete"
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
resourcePath: String,
|
|
8
|
+
confirmMessage: String,
|
|
9
|
+
disabled: Boolean,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
delete(event) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
|
|
15
|
+
if (this.disabledValue) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const confirmMessage = this.confirmMessageValue || "Are you sure you want to delete this item?";
|
|
20
|
+
|
|
21
|
+
if (!confirm(confirmMessage)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fetch(this.resourcePathValue, {
|
|
26
|
+
method: "DELETE",
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: "text/vnd.turbo-stream.html",
|
|
29
|
+
"X-CSRF-Token": CSRFToken(),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
.then((response) => response.text())
|
|
33
|
+
.then((html) => {
|
|
34
|
+
Turbo.renderStreamMessage(html);
|
|
35
|
+
})
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
console.error("Error deleting resource:", error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
connect() {
|
|
5
|
+
this.resize();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
resize() {
|
|
9
|
+
this.element.style.height = "auto";
|
|
10
|
+
const minHeight =
|
|
11
|
+
parseFloat(getComputedStyle(this.element).lineHeight) || 36;
|
|
12
|
+
this.element.style.height =
|
|
13
|
+
Math.max(this.element.scrollHeight, minHeight) + "px";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="filter-auto-submit"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
initialize() {
|
|
6
|
+
this.initialValues = new Map();
|
|
7
|
+
this.storageKey = 'filterAutoSubmit_openDropdowns';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.getInputFields().forEach((field) => {
|
|
12
|
+
this.initialValues.set(field, field.value);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
this.reopenDropdowns();
|
|
16
|
+
this.autofocusSearchField();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
autofocusSearchField() {
|
|
20
|
+
const field = this.element.querySelector('input[type="text"], input[type="search"]');
|
|
21
|
+
if (field) field.focus();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reopenDropdowns() {
|
|
25
|
+
const openDropdownIds = this.getOpenDropdownIds();
|
|
26
|
+
if (openDropdownIds.length > 0) {
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
openDropdownIds.forEach((dropdownId) => {
|
|
29
|
+
const button = this.element.querySelector(`#${dropdownId}`);
|
|
30
|
+
if (button) {
|
|
31
|
+
const dropdown = new bootstrap.Dropdown(button);
|
|
32
|
+
dropdown.show();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
sessionStorage.removeItem(this.storageKey);
|
|
36
|
+
}, 100);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getOpenDropdownIds() {
|
|
41
|
+
try {
|
|
42
|
+
const stored = sessionStorage.getItem(this.storageKey);
|
|
43
|
+
return stored ? JSON.parse(stored) : [];
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
storeOpenDropdownIds(ids) {
|
|
50
|
+
try {
|
|
51
|
+
sessionStorage.setItem(this.storageKey, JSON.stringify(ids));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handleDropdownChange(event) {
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
|
|
59
|
+
const openDropdownIds = [];
|
|
60
|
+
this.element.querySelectorAll('.dropdown-menu.show').forEach((menu) => {
|
|
61
|
+
const button = menu.previousElementSibling;
|
|
62
|
+
if (button && button.id) {
|
|
63
|
+
openDropdownIds.push(button.id);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.storeOpenDropdownIds(openDropdownIds);
|
|
68
|
+
|
|
69
|
+
this.element.requestSubmit();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
submitOnBlur(event) {
|
|
73
|
+
let field = null;
|
|
74
|
+
|
|
75
|
+
if (event.target.matches('input, textarea')) {
|
|
76
|
+
field = event.target;
|
|
77
|
+
} else {
|
|
78
|
+
field = event.target.querySelector('input, textarea');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!field) return;
|
|
82
|
+
|
|
83
|
+
if (this.initialValues.get(field) !== field.value) {
|
|
84
|
+
this.element.requestSubmit();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getInputFields() {
|
|
89
|
+
return Array.from(
|
|
90
|
+
this.element.querySelectorAll('input[type="text"], input[type="search"], textarea')
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -16,6 +16,12 @@ application.register("button-select", ButtonSelectController);
|
|
|
16
16
|
import DatalistController from "./datalist_controller";
|
|
17
17
|
application.register("datalist", DatalistController);
|
|
18
18
|
|
|
19
|
+
import DeleteController from "./delete_controller";
|
|
20
|
+
application.register("delete", DeleteController);
|
|
21
|
+
|
|
22
|
+
import FilterAutoSubmitController from "./filter_auto_submit_controller";
|
|
23
|
+
application.register("filter-auto-submit", FilterAutoSubmitController);
|
|
24
|
+
|
|
19
25
|
import FormController from "./form_controller";
|
|
20
26
|
application.register("form", FormController);
|
|
21
27
|
|
|
@@ -42,3 +48,15 @@ application.register("sorting", SortingController);
|
|
|
42
48
|
|
|
43
49
|
import FloatingTocController from "./floating_toc_controller";
|
|
44
50
|
application.register("floating-toc", FloatingTocController);
|
|
51
|
+
|
|
52
|
+
import AddRowController from "./add_row_controller";
|
|
53
|
+
application.register("add-row", AddRowController);
|
|
54
|
+
|
|
55
|
+
import CopyToClipboardController from "./copy_to_clipboard_controller";
|
|
56
|
+
application.register("copy-to-clipboard", CopyToClipboardController);
|
|
57
|
+
|
|
58
|
+
import ExpandableFieldController from "./expandable_field_controller";
|
|
59
|
+
application.register("expandable-field", ExpandableFieldController);
|
|
60
|
+
|
|
61
|
+
import TabsController from "./tabs_controller";
|
|
62
|
+
application.register("tabs", TabsController);
|
|
@@ -10,4 +10,12 @@ export default class extends Controller {
|
|
|
10
10
|
|
|
11
11
|
fetchTurboContent(event, path);
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
changePerPage(event) {
|
|
15
|
+
const newPerPage = event.target.value;
|
|
16
|
+
const pathTemplate = event.target.dataset.perPagePath;
|
|
17
|
+
const path = pathTemplate.replace("__PER_PAGE__", newPerPage);
|
|
18
|
+
|
|
19
|
+
fetchTurboContent(event, path);
|
|
20
|
+
}
|
|
13
21
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Keeps the open tab of a TabsComponent in the URL hash (#<key>), so a reload or a
|
|
4
|
+
// shared link reopens the same tab instead of falling back to the first one.
|
|
5
|
+
//
|
|
6
|
+
// Connects to data-controller="tabs". Each tab button carries
|
|
7
|
+
// data-tabs-key-param="<key>" and data-action="shown.bs.tab->tabs#remember".
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
connect() {
|
|
10
|
+
const key = decodeURIComponent(window.location.hash.replace("#", ""));
|
|
11
|
+
if (!key) return;
|
|
12
|
+
|
|
13
|
+
const button = Array.from(this.element.querySelectorAll("[data-tabs-key-param]")).find(
|
|
14
|
+
(candidate) => candidate.dataset.tabsKeyParam === key
|
|
15
|
+
);
|
|
16
|
+
// Clicking goes through Bootstrap's own data-api, so the controller needs no
|
|
17
|
+
// reference to the `bootstrap` global.
|
|
18
|
+
if (button && !button.classList.contains("active")) button.click();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
remember({ params: { key } }) {
|
|
22
|
+
history.replaceState(history.state, "", `#${key}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -4,6 +4,12 @@ module FlexiAdmin::Models::Concerns::ApplicationResource
|
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
include FlexiAdmin::Models::Concerns::Parentable
|
|
6
6
|
|
|
7
|
+
class_methods do
|
|
8
|
+
def fa_allowed_scopes
|
|
9
|
+
[]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
def identifier
|
|
8
14
|
"#{self.class.name.underscore}_#{id}"
|
|
9
15
|
end
|
|
@@ -6,6 +6,9 @@ module FlexiAdmin::Models::Concerns::Parentable
|
|
|
6
6
|
included do
|
|
7
7
|
scope :with_parent, lambda { |parent_instance|
|
|
8
8
|
return all if parent_instance.blank?
|
|
9
|
+
if !parent_instance.class.ancestors.include?(ActiveRecord::Base)
|
|
10
|
+
raise ArgumentError, "Parent instance must be a valid model instance, was #{parent_instance.class.name}"
|
|
11
|
+
end
|
|
9
12
|
|
|
10
13
|
plural_name = parent_instance.class.model_name.plural
|
|
11
14
|
|
|
@@ -14,7 +14,10 @@ module FlexiAdmin::Models
|
|
|
14
14
|
view: 'fa_view',
|
|
15
15
|
ac_action: 'ac_action', # autocomplete action
|
|
16
16
|
ac_fields: { 'ac_fields': [] }, # autocomplete result fields
|
|
17
|
-
ac_path: 'ac_path' # autocomplete path for opening resource in new window
|
|
17
|
+
ac_path: 'ac_path', # autocomplete path for opening resource in new window
|
|
18
|
+
reload_frame: 'fa_reload_frame', # turbo-frame ID to reload after bulk actions
|
|
19
|
+
reload: 'fa_reload', # :page forces full page reload after bulk actions
|
|
20
|
+
custom_scope: 'fa_custom_scope', # named model scope for autocomplete filtering
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
def self.permitted_params_keys
|
|
@@ -31,12 +34,19 @@ module FlexiAdmin::Models
|
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
|
|
34
|
-
attr_reader :params
|
|
37
|
+
attr_reader :params, :request_params
|
|
35
38
|
|
|
36
39
|
def initialize(params)
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
params_hash = params.to_h.with_indifferent_access
|
|
41
|
+
|
|
42
|
+
@params = params_hash.each_with_object({}) do |(key, value), acc|
|
|
43
|
+
if self.class.params_map.invert[key.to_s]
|
|
44
|
+
acc[self.class.params_map.invert[key.to_s]] = value
|
|
45
|
+
end
|
|
39
46
|
end.with_indifferent_access
|
|
47
|
+
|
|
48
|
+
excluded_keys = FlexiAdmin::Controllers::ResourcesController::DEFAULT_CONTROLLER_PARAMS_KEYS
|
|
49
|
+
@request_params = params_hash.except(*self.class.params_map.values, *excluded_keys).with_indifferent_access
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
def [](key)
|
|
@@ -54,7 +64,9 @@ module FlexiAdmin::Models
|
|
|
54
64
|
|
|
55
65
|
new_params = new_params.transform_keys { |key| self.class.params_map[key.to_sym] || key.to_sym }
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
combined_params = new_params.merge(@request_params)
|
|
68
|
+
|
|
69
|
+
self.class.new(combined_params)
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def with_parent(parent_instance)
|
|
@@ -71,12 +83,17 @@ module FlexiAdmin::Models
|
|
|
71
83
|
def except(*keys)
|
|
72
84
|
new_params = @params.dup.except(*keys)
|
|
73
85
|
new_params = new_params.transform_keys { |key| self.class.params_map.invert[key.to_s] }
|
|
74
|
-
|
|
86
|
+
|
|
87
|
+
combined_params = new_params.merge(@request_params)
|
|
88
|
+
|
|
89
|
+
self.class.new(combined_params)
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
def to_params
|
|
78
|
-
self.class.params_map.map { |k, v| [v, params[k]] }
|
|
93
|
+
context_params = self.class.params_map.map { |k, v| [v, params[k]] }
|
|
79
94
|
.reject { |_k, v| v.blank? }.to_h
|
|
95
|
+
|
|
96
|
+
context_params.merge(@request_params)
|
|
80
97
|
end
|
|
81
98
|
|
|
82
99
|
def to_path(path)
|
|
@@ -88,7 +105,7 @@ module FlexiAdmin::Models
|
|
|
88
105
|
end
|
|
89
106
|
|
|
90
107
|
def per_page
|
|
91
|
-
params[:per_page]
|
|
108
|
+
params[:per_page]
|
|
92
109
|
end
|
|
93
110
|
|
|
94
111
|
def page
|
|
@@ -113,7 +130,7 @@ module FlexiAdmin::Models
|
|
|
113
130
|
end
|
|
114
131
|
|
|
115
132
|
def pagination
|
|
116
|
-
{ page
|
|
133
|
+
{ page: page, per_page: per_page }
|
|
117
134
|
end
|
|
118
135
|
|
|
119
136
|
def sort
|
|
@@ -123,5 +140,13 @@ module FlexiAdmin::Models
|
|
|
123
140
|
def order
|
|
124
141
|
params[:order]
|
|
125
142
|
end
|
|
143
|
+
|
|
144
|
+
def reload_frame
|
|
145
|
+
params[:reload_frame]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def reload
|
|
149
|
+
params[:reload]&.to_sym
|
|
150
|
+
end
|
|
126
151
|
end
|
|
127
|
-
end
|
|
152
|
+
end
|