mensa 0.2.5 → 0.2.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/.devcontainer/Dockerfile +6 -2
- data/.devcontainer/compose.yaml +1 -1
- data/.devcontainer/devcontainer.json +31 -29
- data/.devcontainer/postCreate.sh +8 -0
- data/.devcontainer/postStart.sh +9 -0
- data/.gitignore +3 -1
- data/.zed/tasks.json +12 -0
- data/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +85 -60
- data/app/assets/stylesheets/mensa/application.css +14 -11
- data/app/components/mensa/add_filter/component.css +110 -5
- data/app/components/mensa/add_filter/component.html.slim +10 -12
- data/app/components/mensa/add_filter/component.rb +7 -1
- data/app/components/mensa/add_filter/component_controller.js +697 -83
- data/app/components/mensa/cell/component.css +9 -0
- data/app/components/mensa/column_customizer/component.css +40 -0
- data/app/components/mensa/column_customizer/component.html.slim +14 -0
- data/app/components/mensa/column_customizer/component.rb +13 -0
- data/app/components/mensa/column_customizer/component_controller.js +383 -0
- data/app/components/mensa/control_bar/component.css +127 -4
- data/app/components/mensa/control_bar/component.html.slim +41 -14
- data/app/components/mensa/control_bar/component.rb +0 -4
- data/app/components/mensa/empty_state/component.css +20 -0
- data/app/components/mensa/empty_state/component.html.slim +7 -0
- data/app/components/mensa/empty_state/component.rb +18 -0
- data/app/components/mensa/filter_pill/component.css +23 -0
- data/app/components/mensa/filter_pill/component.html.slim +9 -6
- data/app/components/mensa/filter_pill/component.rb +9 -0
- data/app/components/mensa/filter_pill/component_controller.js +50 -10
- data/app/components/mensa/filter_pill_list/component.css +58 -9
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -8
- data/app/components/mensa/filter_pill_list/component_controller.js +747 -48
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/row_action/component.html.slim +2 -2
- data/app/components/mensa/search/component.css +68 -9
- data/app/components/mensa/search/component.html.slim +19 -15
- data/app/components/mensa/search/component_controller.js +39 -49
- data/app/components/mensa/selection/component_controller.js +147 -0
- data/app/components/mensa/table/component.css +28 -0
- data/app/components/mensa/table/component.html.slim +9 -6
- data/app/components/mensa/table/component.rb +1 -0
- data/app/components/mensa/table/component_controller.js +524 -88
- data/app/components/mensa/table_row/component.css +6 -0
- data/app/components/mensa/table_row/component.html.slim +8 -3
- data/app/components/mensa/view/component.css +97 -29
- data/app/components/mensa/view/component.html.slim +23 -10
- data/app/components/mensa/view/component.rb +5 -0
- data/app/components/mensa/views/component.css +106 -13
- data/app/components/mensa/views/component.html.slim +51 -17
- data/app/components/mensa/views/component_controller.js +245 -20
- data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
- data/app/controllers/mensa/tables/exports_controller.rb +96 -0
- data/app/controllers/mensa/tables/filters_controller.rb +4 -1
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +3 -6
- data/app/helpers/mensa/application_helper.rb +4 -0
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/index.js +13 -4
- data/app/jobs/mensa/export_job.rb +77 -84
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/base.rb +103 -12
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +15 -0
- data/app/tables/mensa/column.rb +15 -2
- data/app/tables/mensa/config/batch_dsl.rb +13 -0
- data/app/tables/mensa/config/column_dsl.rb +1 -0
- data/app/tables/mensa/config/filter_dsl.rb +4 -1
- data/app/tables/mensa/config/render_dsl.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +12 -5
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +20 -1
- data/app/tables/mensa/filter.rb +86 -3
- data/app/tables/mensa/scope.rb +24 -12
- data/app/views/mensa/exports/_badge.html.slim +5 -0
- data/app/views/mensa/exports/_dialog.html.slim +42 -0
- data/app/views/mensa/exports/_list.html.slim +29 -0
- data/app/views/mensa/tables/filters/show.turbo_stream.slim +35 -8
- data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
- data/config/locales/en.yml +44 -0
- data/config/locales/nl.yml +45 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
- data/docs/columns.png +0 -0
- data/docs/export.png +0 -0
- data/docs/filters.png +0 -0
- data/docs/table.png +0 -0
- data/lib/mensa/configuration.rb +33 -12
- data/lib/mensa/engine.rb +7 -2
- data/lib/mensa/version.rb +1 -1
- data/mensa.gemspec +2 -1
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +50 -8
|
@@ -1,92 +1,528 @@
|
|
|
1
1
|
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
-
import { get } from "@rails/request.js";
|
|
2
|
+
import { get, post, patch } from "@rails/request.js";
|
|
3
3
|
|
|
4
4
|
export default class TableComponentController extends ApplicationController {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
5
|
+
static targets = [
|
|
6
|
+
"controlBar",
|
|
7
|
+
"filterList",
|
|
8
|
+
"views",
|
|
9
|
+
"view",
|
|
10
|
+
"turboFrame",
|
|
11
|
+
"saveViewDialog",
|
|
12
|
+
"saveViewName",
|
|
13
|
+
"saveViewDescription",
|
|
14
|
+
"exportDialog",
|
|
15
|
+
"exportIcon",
|
|
16
|
+
"saveResetButtons",
|
|
17
|
+
"saveDropdown",
|
|
18
|
+
"saveSimple",
|
|
19
|
+
"saveSplit",
|
|
20
|
+
"eyeButton",
|
|
21
|
+
];
|
|
22
|
+
static outlets = ["mensa-filter-pill-list", "mensa-column-customizer"];
|
|
23
|
+
static values = {
|
|
24
|
+
supportsViews: Boolean,
|
|
25
|
+
tableUrl: String,
|
|
26
|
+
saveViewUrl: String,
|
|
27
|
+
viewsUrl: String,
|
|
28
|
+
exportsUrl: String,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
connect() {
|
|
32
|
+
super.connect();
|
|
33
|
+
|
|
34
|
+
this.frameLoadFallback = setTimeout(() => this.loadFrame(), 100);
|
|
35
|
+
|
|
36
|
+
if (this.hasTurboFrameTarget) {
|
|
37
|
+
this.captureNavigationHandler = () => this.captureNavigation();
|
|
38
|
+
this.turboFrameTarget.addEventListener(
|
|
39
|
+
"turbo:frame-load",
|
|
40
|
+
this.captureNavigationHandler,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Close save dropdown when clicking outside
|
|
45
|
+
this._saveDropdownOutsideHandler = (e) => {
|
|
46
|
+
if (this.hasSaveDropdownTarget && !this.saveDropdownTarget.classList.contains("hidden")) {
|
|
47
|
+
const saveArea = this.saveDropdownTarget.closest(".relative");
|
|
48
|
+
if (saveArea && !saveArea.contains(e.target)) {
|
|
49
|
+
this.saveDropdownTarget.classList.add("hidden");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("click", this._saveDropdownOutsideHandler);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
disconnect() {
|
|
57
|
+
if (this.frameLoadFallback) {
|
|
58
|
+
clearTimeout(this.frameLoadFallback);
|
|
59
|
+
this.frameLoadFallback = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.hasTurboFrameTarget && this.captureNavigationHandler) {
|
|
63
|
+
this.turboFrameTarget.removeEventListener(
|
|
64
|
+
"turbo:frame-load",
|
|
65
|
+
this.captureNavigationHandler,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.removeEventListener("click", this._saveDropdownOutsideHandler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
captureNavigation() {
|
|
73
|
+
if (!this.hasMensaFilterPillListOutlet) return;
|
|
74
|
+
if (!this.hasTurboFrameTarget) return;
|
|
75
|
+
const src = this.turboFrameTarget.getAttribute("src");
|
|
76
|
+
if (!src) return;
|
|
77
|
+
this.mensaFilterPillListOutlet.captureNavigation(src);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
loadFrame() {
|
|
81
|
+
if (this.frameLoadHandled) return;
|
|
82
|
+
this.frameLoadHandled = true;
|
|
83
|
+
|
|
84
|
+
if (this.frameLoadFallback) {
|
|
85
|
+
clearTimeout(this.frameLoadFallback);
|
|
86
|
+
this.frameLoadFallback = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!this.hasTurboFrameTarget) return;
|
|
90
|
+
|
|
91
|
+
// When the filter-pill-list outlet is available, build the frame URL from the
|
|
92
|
+
// full saved state (filters, view, order, …) so that captureNavigation doesn't
|
|
93
|
+
// call persistView("") and wipe the active view from localStorage.
|
|
94
|
+
// This happens on back-navigation: the outlet is connected before loadFrame is
|
|
95
|
+
// called, but tableUrlValue was computed from the initial page URL which never
|
|
96
|
+
// carries table_view_id.
|
|
97
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
98
|
+
const outlet = this.mensaFilterPillListOutlet;
|
|
99
|
+
const state = {
|
|
100
|
+
filters: outlet.loadFilters(),
|
|
101
|
+
query: outlet.loadQuery(),
|
|
102
|
+
view: outlet.loadView(),
|
|
103
|
+
order: outlet.loadOrder(),
|
|
104
|
+
page: outlet.loadPage(),
|
|
105
|
+
};
|
|
106
|
+
this.turboFrameTarget.setAttribute("src", outlet.buildUrl(state).toString());
|
|
107
|
+
} else if (this.hasTableUrlValue) {
|
|
108
|
+
this.turboFrameTarget.setAttribute("src", this.tableUrlValue);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Save/Reset button visibility ---
|
|
113
|
+
|
|
114
|
+
showSaveReset() {
|
|
115
|
+
if (this.hasSaveResetButtonsTarget) {
|
|
116
|
+
this.saveResetButtonsTarget.classList.remove("hidden");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
hideSaveReset() {
|
|
121
|
+
if (this.hasSaveResetButtonsTarget) {
|
|
122
|
+
this.saveResetButtonsTarget.classList.add("hidden");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Called whenever a filter or search changes so the save/reset buttons appear.
|
|
127
|
+
notifyUnsavedState() {
|
|
128
|
+
this._updateSaveButtonMode();
|
|
129
|
+
this.showSaveReset();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Cancel / Reset ---
|
|
133
|
+
|
|
134
|
+
// Resets all filters, search, and column customisation to the active view's clean state.
|
|
135
|
+
cancelFiltersAndSearch(event) {
|
|
136
|
+
if (event) event.preventDefault();
|
|
137
|
+
this.hideSaveReset();
|
|
138
|
+
if (this.hasSaveDropdownTarget) this.saveDropdownTarget.classList.add("hidden");
|
|
139
|
+
|
|
140
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
141
|
+
this.mensaFilterPillListOutlet.clearFiltersAndSearch();
|
|
142
|
+
}
|
|
143
|
+
if (this.hasMensaColumnCustomizerOutlet) {
|
|
144
|
+
this.mensaColumnCustomizerOutlet.resetToDefault();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Save ---
|
|
149
|
+
|
|
150
|
+
toggleSaveDropdown(event) {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
if (!this.hasSaveDropdownTarget) return;
|
|
154
|
+
const isHidden = this.saveDropdownTarget.classList.contains("hidden");
|
|
155
|
+
this.saveDropdownTarget.classList.toggle("hidden");
|
|
156
|
+
if (isHidden) {
|
|
157
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
158
|
+
this.saveDropdownTarget.style.top = `${rect.bottom + 4}px`;
|
|
159
|
+
this.saveDropdownTarget.style.right = `${window.innerWidth - rect.right}px`;
|
|
160
|
+
this.saveDropdownTarget.style.left = "auto";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// "Save as new view" — always opens the name dialog
|
|
165
|
+
saveAsNewView(event) {
|
|
166
|
+
if (event) event.preventDefault();
|
|
167
|
+
if (this.hasSaveDropdownTarget) this.saveDropdownTarget.classList.add("hidden");
|
|
168
|
+
this._openSaveDialog();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// "Update view" — updates the currently selected user-owned view in place
|
|
172
|
+
async updateCurrentViewAction(event) {
|
|
173
|
+
if (event) event.preventDefault();
|
|
174
|
+
if (this.hasSaveDropdownTarget) this.saveDropdownTarget.classList.add("hidden");
|
|
175
|
+
|
|
176
|
+
const viewId = this._selectedUserViewId();
|
|
177
|
+
if (!viewId) {
|
|
178
|
+
this._openSaveDialog();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await this._updateCurrentView(viewId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Legacy: called from the old "Save" button. Routes to update-in-place for
|
|
185
|
+
// user views, otherwise opens the dialog.
|
|
186
|
+
saveFiltersAndSearch(event) {
|
|
187
|
+
if (event) event.preventDefault();
|
|
188
|
+
const viewId = this._selectedUserViewId();
|
|
189
|
+
if (viewId) {
|
|
190
|
+
this._updateCurrentView(viewId);
|
|
191
|
+
} else {
|
|
192
|
+
this._openSaveDialog();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async _updateCurrentView(viewId) {
|
|
197
|
+
const state = this.currentViewState();
|
|
198
|
+
this.hideSaveReset();
|
|
199
|
+
|
|
200
|
+
const response = await patch(`${this.saveViewUrlValue}/${viewId}`, {
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
query: state.query,
|
|
203
|
+
filters: state.filters,
|
|
204
|
+
order: state.order,
|
|
205
|
+
column_order: state.column_order,
|
|
206
|
+
hidden_columns: state.hidden_columns,
|
|
207
|
+
turbo_frame_id: this.hasTurboFrameTarget ? this.turboFrameTarget.id : null,
|
|
208
|
+
}),
|
|
209
|
+
contentType: "application/json",
|
|
210
|
+
responseKind: "turbo-stream",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (response.ok && this.hasMensaFilterPillListOutlet) {
|
|
214
|
+
this.mensaFilterPillListOutlet.clearPersistedDirtyState();
|
|
215
|
+
this.mensaFilterPillListOutlet.persistView(viewId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
cancelSaveView(event) {
|
|
220
|
+
if (event) event.preventDefault();
|
|
221
|
+
this.closeSaveViewDialog();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
saveViewDialogBackdrop(event) {
|
|
225
|
+
if (event.target === this.saveViewDialogTarget) {
|
|
226
|
+
this.closeSaveViewDialog();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
closeSaveViewDialog() {
|
|
231
|
+
if (!this.hasSaveViewDialogTarget) return;
|
|
232
|
+
if (typeof this.saveViewDialogTarget.close === "function") {
|
|
233
|
+
this.saveViewDialogTarget.close();
|
|
234
|
+
} else {
|
|
235
|
+
this.saveViewDialogTarget.removeAttribute("open");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async confirmSaveView(event) {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
|
|
242
|
+
const name = this.hasSaveViewNameTarget ? this.saveViewNameTarget.value.trim() : "";
|
|
243
|
+
if (!name) {
|
|
244
|
+
if (this.hasSaveViewNameTarget) this.saveViewNameTarget.reportValidity();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const description = this.hasSaveViewDescriptionTarget
|
|
249
|
+
? this.saveViewDescriptionTarget.value.trim()
|
|
250
|
+
: "";
|
|
251
|
+
|
|
252
|
+
const state = this.currentViewState();
|
|
253
|
+
|
|
254
|
+
const response = await post(this.saveViewUrlValue, {
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
name,
|
|
257
|
+
description,
|
|
258
|
+
query: state.query,
|
|
259
|
+
filters: state.filters,
|
|
260
|
+
order: state.order,
|
|
261
|
+
column_order: state.column_order,
|
|
262
|
+
hidden_columns: state.hidden_columns,
|
|
263
|
+
turbo_frame_id: this.hasTurboFrameTarget ? this.turboFrameTarget.id : null,
|
|
264
|
+
}),
|
|
265
|
+
contentType: "application/json",
|
|
266
|
+
responseKind: "turbo-stream",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
this.closeSaveViewDialog();
|
|
271
|
+
this.hideSaveReset();
|
|
272
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
273
|
+
this.mensaFilterPillListOutlet.clearPersistedDirtyState();
|
|
274
|
+
}
|
|
275
|
+
// After Turbo processes the stream the views component re-renders with
|
|
276
|
+
// the new view selected — read its ID from the DOM and persist it.
|
|
277
|
+
setTimeout(() => this._persistSelectedViewId(), 0);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
currentViewState() {
|
|
282
|
+
if (!this.hasMensaFilterPillListOutlet) {
|
|
283
|
+
return { filters: {}, query: "", order: {} };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const outlet = this.mensaFilterPillListOutlet;
|
|
287
|
+
const input = outlet.searchInputElement();
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
filters: outlet.collectFilters(),
|
|
291
|
+
query: input ? input.value : outlet.loadQuery(),
|
|
292
|
+
order: outlet.loadOrder(),
|
|
293
|
+
column_order: outlet.loadColumnOrder(),
|
|
294
|
+
hidden_columns: outlet.loadHiddenColumns(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
viewTargetConnected() {}
|
|
299
|
+
|
|
300
|
+
filterListTargetConnected(element) {
|
|
301
|
+
// The filter bar is always visible in the new design — nothing to toggle.
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Called when the filter-pill-list outlet connects (including after turbo-stream re-renders).
|
|
305
|
+
// Re-applied on every connect so late-arriving pills (from restoreState) get the right state.
|
|
306
|
+
mensaFilterPillListOutletConnected() {
|
|
307
|
+
const hasViewFilters = this._hasViewFilterPills();
|
|
308
|
+
if (hasViewFilters) {
|
|
309
|
+
const visible = this._loadViewFiltersVisible();
|
|
310
|
+
this.element.classList.toggle("mensa-table--view-filters-hidden", !visible);
|
|
311
|
+
} else {
|
|
312
|
+
this.element.classList.remove("mensa-table--view-filters-hidden");
|
|
313
|
+
}
|
|
314
|
+
this._updateEyeButton();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Called from the filter-pill-list controller when the user selects a different view.
|
|
318
|
+
// Resets the eye preference to hidden so the new view's filters start collapsed.
|
|
319
|
+
// Does NOT call _updateEyeButton() — the old DOM is still present here and would cause
|
|
320
|
+
// _updateEyeButton to remove the class we just added. The proper update happens in
|
|
321
|
+
// mensaFilterPillListOutletConnected after the turbo-stream re-render.
|
|
322
|
+
viewChanged() {
|
|
323
|
+
this._saveViewFiltersVisible(false);
|
|
324
|
+
this.element.classList.add("mensa-table--view-filters-hidden");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Eye toggle (view-origin filter pills) ---
|
|
328
|
+
|
|
329
|
+
toggleViewFilters(event) {
|
|
330
|
+
if (event) event.preventDefault();
|
|
331
|
+
const nowHidden = this.element.classList.toggle("mensa-table--view-filters-hidden");
|
|
332
|
+
this._saveViewFiltersVisible(!nowHidden);
|
|
333
|
+
this._updateEyeButton();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_hasViewFilterPills() {
|
|
337
|
+
return this.element.querySelectorAll('[data-view-filter="true"]').length > 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Only updates button visibility and icon — never touches the hidden class.
|
|
341
|
+
// Class management is the sole responsibility of mensaFilterPillListOutletConnected
|
|
342
|
+
// and toggleViewFilters.
|
|
343
|
+
_updateEyeButton() {
|
|
344
|
+
const hasViewFilters = this._hasViewFilterPills();
|
|
345
|
+
|
|
346
|
+
if (!this.hasEyeButtonTarget) return;
|
|
347
|
+
|
|
348
|
+
this.eyeButtonTarget.classList.toggle("hidden", !hasViewFilters);
|
|
349
|
+
|
|
350
|
+
if (hasViewFilters) {
|
|
351
|
+
const hidden = this.element.classList.contains("mensa-table--view-filters-hidden");
|
|
352
|
+
// Replace innerHTML so FontAwesome's MutationObserver re-processes the new <i>
|
|
353
|
+
this.eyeButtonTarget.innerHTML = hidden
|
|
354
|
+
? '<i class="fa-solid fa-eye"></i>'
|
|
355
|
+
: '<i class="fa-solid fa-eye-slash"></i>';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- View filter visibility persistence ---
|
|
360
|
+
|
|
361
|
+
_viewFiltersStorageKey() {
|
|
362
|
+
return `mensa:view-filters-visible:${this._tableName()}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_tableName() {
|
|
366
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
367
|
+
return this.mensaFilterPillListOutlet.tableNameValue;
|
|
368
|
+
}
|
|
369
|
+
const el = this.element.querySelector("[data-mensa-filter-pill-list-table-name-value]");
|
|
370
|
+
return el?.dataset?.mensaFilterPillListTableNameValue || "";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_loadViewFiltersVisible() {
|
|
374
|
+
try {
|
|
375
|
+
return window.localStorage.getItem(this._viewFiltersStorageKey()) === "true";
|
|
376
|
+
} catch (e) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_saveViewFiltersVisible(visible) {
|
|
382
|
+
try {
|
|
383
|
+
if (visible) {
|
|
384
|
+
window.localStorage.setItem(this._viewFiltersStorageKey(), "true");
|
|
385
|
+
} else {
|
|
386
|
+
window.localStorage.removeItem(this._viewFiltersStorageKey());
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Export ---
|
|
392
|
+
|
|
393
|
+
export(event) {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
if (!this.hasExportDialogTarget) return;
|
|
396
|
+
|
|
397
|
+
if (this.hasExportsUrlValue && this.exportsUrlValue) {
|
|
398
|
+
get(this.exportsUrlValue, { responseKind: "turbo-stream" }).finally(
|
|
399
|
+
() => this.openExportDialog(),
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
this.openExportDialog();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
openExportDialog() {
|
|
407
|
+
if (!this.hasExportDialogTarget) return;
|
|
408
|
+
if (typeof this.exportDialogTarget.showModal === "function") {
|
|
409
|
+
this.exportDialogTarget.showModal();
|
|
410
|
+
} else {
|
|
411
|
+
this.exportDialogTarget.setAttribute("open", "");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cancelExport(event) {
|
|
416
|
+
if (event) event.preventDefault();
|
|
417
|
+
this.closeExportDialog();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
exportDialogBackdrop(event) {
|
|
421
|
+
if (event.target === this.exportDialogTarget) {
|
|
422
|
+
this.closeExportDialog();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
closeExportDialog() {
|
|
427
|
+
if (!this.hasExportDialogTarget) return;
|
|
428
|
+
if (typeof this.exportDialogTarget.close === "function") {
|
|
429
|
+
this.exportDialogTarget.close();
|
|
430
|
+
} else {
|
|
431
|
+
this.exportDialogTarget.removeAttribute("open");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
confirmExport(event) {
|
|
436
|
+
event.preventDefault();
|
|
437
|
+
if (!this.hasExportsUrlValue) return;
|
|
438
|
+
|
|
439
|
+
const dialog = this.exportDialogTarget;
|
|
440
|
+
const scope = dialog.querySelector('input[name="scope"]:checked')?.value || "all";
|
|
441
|
+
const exportFormat = dialog.querySelector('input[name="export_format"]:checked')?.value || "csv_excel";
|
|
442
|
+
|
|
443
|
+
const state = this.currentViewState();
|
|
444
|
+
const nav = this.hasMensaFilterPillListOutlet
|
|
445
|
+
? this.mensaFilterPillListOutlet.currentRequestState()
|
|
446
|
+
: { page: "", query: "", view: "" };
|
|
447
|
+
const view = this.hasMensaFilterPillListOutlet
|
|
448
|
+
? this.mensaFilterPillListOutlet.loadView() || nav.view
|
|
449
|
+
: "";
|
|
450
|
+
|
|
451
|
+
post(this.exportsUrlValue, {
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
scope,
|
|
454
|
+
export_format: exportFormat,
|
|
455
|
+
table_view_id: view,
|
|
456
|
+
page: nav.page,
|
|
457
|
+
query: state.query || nav.query,
|
|
458
|
+
filters: state.filters,
|
|
459
|
+
order: state.order,
|
|
460
|
+
}),
|
|
461
|
+
contentType: "application/json",
|
|
462
|
+
responseKind: "turbo-stream",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
get ourUrl() {
|
|
467
|
+
if (this.hasTurboFrameTarget && this.turboFrameTarget.getAttribute("src")) {
|
|
468
|
+
return new URL(this.turboFrameTarget.getAttribute("src"));
|
|
469
|
+
}
|
|
470
|
+
if (this.hasTableUrlValue && this.tableUrlValue) {
|
|
471
|
+
return new URL(this.tableUrlValue);
|
|
472
|
+
}
|
|
473
|
+
return new URL(window.location.href);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// --- Private ---
|
|
477
|
+
|
|
478
|
+
// Reads the currently-selected view ID from the DOM (after Turbo re-renders
|
|
479
|
+
// the views component) and persists it to localStorage.
|
|
480
|
+
_persistSelectedViewId() {
|
|
481
|
+
if (!this.hasMensaFilterPillListOutlet) return;
|
|
482
|
+
const checked = Array.from(
|
|
483
|
+
this.element.querySelectorAll('[data-mensa-views-target="view"]'),
|
|
484
|
+
).find((el) => {
|
|
485
|
+
const check = el.querySelector(".mensa-table__views__option-check");
|
|
486
|
+
return check && !check.classList.contains("invisible");
|
|
487
|
+
});
|
|
488
|
+
const viewId = checked?.dataset.viewId || "";
|
|
489
|
+
this.mensaFilterPillListOutlet.persistView(viewId);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Show the plain "Save" button for system/default views, and the dropdown
|
|
493
|
+
// "Save ▾" button for user-created views.
|
|
494
|
+
_updateSaveButtonMode() {
|
|
495
|
+
if (!this.hasSaveSimpleTarget && !this.hasSaveSplitTarget) return;
|
|
496
|
+
const isUserView = !!this._selectedUserViewId();
|
|
497
|
+
this.saveSimpleTargets.forEach((t) => t.classList.toggle("hidden", isUserView));
|
|
498
|
+
this.saveSplitTargets.forEach((t) => t.classList.toggle("hidden", !isUserView));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_selectedUserViewId() {
|
|
502
|
+
const selectedViewEl = this.element.querySelector(
|
|
503
|
+
'[data-mensa-views-target="view"]',
|
|
504
|
+
);
|
|
505
|
+
// Find the one whose check is visible
|
|
506
|
+
const checked = Array.from(
|
|
507
|
+
this.element.querySelectorAll('[data-mensa-views-target="view"]'),
|
|
508
|
+
).find((el) => {
|
|
509
|
+
const check = el.querySelector(".mensa-table__views__option-check");
|
|
510
|
+
return check && !check.classList.contains("invisible");
|
|
511
|
+
});
|
|
512
|
+
const viewId = checked?.getAttribute("data-view-id") || "";
|
|
513
|
+
// UUID-based IDs are user-created views
|
|
514
|
+
return viewId && /[a-f0-9-]{32}$/.test(viewId) ? viewId : null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_openSaveDialog() {
|
|
518
|
+
if (!this.hasSaveViewDialogTarget) return;
|
|
519
|
+
if (this.hasSaveViewNameTarget) this.saveViewNameTarget.value = "";
|
|
520
|
+
if (this.hasSaveViewDescriptionTarget) this.saveViewDescriptionTarget.value = "";
|
|
521
|
+
if (typeof this.saveViewDialogTarget.showModal === "function") {
|
|
522
|
+
this.saveViewDialogTarget.showModal();
|
|
523
|
+
} else {
|
|
524
|
+
this.saveViewDialogTarget.setAttribute("open", "");
|
|
525
|
+
}
|
|
526
|
+
if (this.hasSaveViewNameTarget) this.saveViewNameTarget.focus();
|
|
527
|
+
}
|
|
92
528
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
tr *row.link_attributes
|
|
2
|
+
- if table. batch_actions?
|
|
3
|
+
td.mensa-table__checkbox-col data-action="click->mensa-selection#stopPropagation"
|
|
4
|
+
input.mensa-table__select-all type="checkbox" value=row.record.id data-action="change->mensa-selection#toggleRow" data-mensa-selection-target="rowCheckbox"
|
|
5
|
+
- if table.actions? && Mensa.config.row_actions_position == :front
|
|
6
|
+
td.actions
|
|
7
|
+
= render(Mensa::RowAction::Component.with_collection(table.actions, row: row, table: table))
|
|
2
8
|
= render(Mensa::Cell::Component.with_collection(table.display_columns, row: row))
|
|
3
|
-
- if table.actions?
|
|
4
|
-
td.
|
|
9
|
+
- if table.actions? && Mensa.config.row_actions_position == :back
|
|
10
|
+
td.actions
|
|
5
11
|
= render(Mensa::RowAction::Component.with_collection(table.actions, row: row, table: table))
|
|
6
|
-
|
|