mensa 0.2.4 → 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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +6 -2
  3. data/.devcontainer/compose.yaml +1 -1
  4. data/.devcontainer/devcontainer.json +31 -29
  5. data/.devcontainer/postCreate.sh +8 -0
  6. data/.devcontainer/postStart.sh +9 -0
  7. data/.gitignore +3 -1
  8. data/.zed/tasks.json +12 -0
  9. data/CHANGELOG.md +6 -0
  10. data/Gemfile.lock +155 -153
  11. data/Procfile +1 -1
  12. data/README.md +95 -60
  13. data/app/assets/stylesheets/mensa/application.css +14 -11
  14. data/app/components/mensa/add_filter/component.css +110 -5
  15. data/app/components/mensa/add_filter/component.html.slim +10 -12
  16. data/app/components/mensa/add_filter/component.rb +8 -2
  17. data/app/components/mensa/add_filter/component_controller.js +697 -85
  18. data/app/components/mensa/cell/component.css +9 -0
  19. data/app/components/mensa/column_customizer/component.css +40 -0
  20. data/app/components/mensa/column_customizer/component.html.slim +14 -0
  21. data/app/components/mensa/column_customizer/component.rb +13 -0
  22. data/app/components/mensa/column_customizer/component_controller.js +383 -0
  23. data/app/components/mensa/control_bar/component.css +127 -4
  24. data/app/components/mensa/control_bar/component.html.slim +41 -14
  25. data/app/components/mensa/control_bar/component.rb +2 -6
  26. data/app/components/mensa/empty_state/component.css +20 -0
  27. data/app/components/mensa/empty_state/component.html.slim +7 -0
  28. data/app/components/mensa/empty_state/component.rb +18 -0
  29. data/app/components/mensa/filter_pill/component.css +23 -0
  30. data/app/components/mensa/filter_pill/component.html.slim +9 -0
  31. data/app/components/mensa/filter_pill/component.rb +24 -0
  32. data/app/components/mensa/filter_pill/component_controller.js +52 -0
  33. data/app/components/mensa/filter_pill_list/component.css +63 -0
  34. data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
  35. data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
  36. data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
  37. data/app/components/mensa/header/component.css +41 -43
  38. data/app/components/mensa/header/component.html.slim +7 -7
  39. data/app/components/mensa/header/component.rb +1 -1
  40. data/app/components/mensa/row_action/component.html.slim +2 -2
  41. data/app/components/mensa/row_action/component.rb +1 -1
  42. data/app/components/mensa/search/component.css +68 -9
  43. data/app/components/mensa/search/component.html.slim +19 -15
  44. data/app/components/mensa/search/component.rb +1 -1
  45. data/app/components/mensa/search/component_controller.js +39 -49
  46. data/app/components/mensa/selection/component_controller.js +147 -0
  47. data/app/components/mensa/table/component.css +28 -0
  48. data/app/components/mensa/table/component.html.slim +9 -6
  49. data/app/components/mensa/table/component.rb +1 -0
  50. data/app/components/mensa/table/component_controller.js +524 -76
  51. data/app/components/mensa/table_row/component.css +6 -0
  52. data/app/components/mensa/table_row/component.html.slim +8 -3
  53. data/app/components/mensa/table_row/component.rb +1 -1
  54. data/app/components/mensa/view/component.css +97 -29
  55. data/app/components/mensa/view/component.html.slim +23 -10
  56. data/app/components/mensa/view/component.rb +5 -0
  57. data/app/components/mensa/views/component.css +106 -13
  58. data/app/components/mensa/views/component.html.slim +51 -17
  59. data/app/components/mensa/views/component_controller.js +245 -20
  60. data/app/controllers/mensa/application_controller.rb +1 -1
  61. data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
  62. data/app/controllers/mensa/tables/exports_controller.rb +96 -0
  63. data/app/controllers/mensa/tables/filters_controller.rb +6 -2
  64. data/app/controllers/mensa/tables/views_controller.rb +108 -0
  65. data/app/controllers/mensa/tables_controller.rb +5 -14
  66. data/app/helpers/mensa/application_helper.rb +4 -1
  67. data/app/javascript/mensa/application.js +2 -2
  68. data/app/javascript/mensa/controllers/application_controller.js +5 -21
  69. data/app/javascript/mensa/controllers/index.js +16 -7
  70. data/app/jobs/mensa/export_job.rb +77 -85
  71. data/app/models/mensa/export.rb +93 -0
  72. data/app/tables/mensa/action.rb +3 -1
  73. data/app/tables/mensa/base.rb +103 -17
  74. data/app/tables/mensa/batch_action.rb +27 -0
  75. data/app/tables/mensa/cell.rb +21 -6
  76. data/app/tables/mensa/column.rb +30 -25
  77. data/app/tables/mensa/config/action_dsl.rb +1 -1
  78. data/app/tables/mensa/config/batch_dsl.rb +13 -0
  79. data/app/tables/mensa/config/column_dsl.rb +1 -0
  80. data/app/tables/mensa/config/dsl_logic.rb +8 -4
  81. data/app/tables/mensa/config/filter_dsl.rb +4 -1
  82. data/app/tables/mensa/config/render_dsl.rb +1 -1
  83. data/app/tables/mensa/config/table_dsl.rb +14 -4
  84. data/app/tables/mensa/config/view_dsl.rb +2 -0
  85. data/app/tables/mensa/config_readers.rb +34 -3
  86. data/app/tables/mensa/filter.rb +94 -14
  87. data/app/tables/mensa/row.rb +1 -1
  88. data/app/tables/mensa/scope.rb +25 -13
  89. data/app/views/mensa/exports/_badge.html.slim +5 -0
  90. data/app/views/mensa/exports/_dialog.html.slim +42 -0
  91. data/app/views/mensa/exports/_list.html.slim +29 -0
  92. data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
  93. data/app/views/mensa/tables/show.html.slim +2 -0
  94. data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
  95. data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
  96. data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
  97. data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
  98. data/bin/setup +1 -1
  99. data/config/locales/en.yml +45 -1
  100. data/config/locales/nl.yml +46 -1
  101. data/config/routes.rb +7 -0
  102. data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
  103. data/docs/columns.png +0 -0
  104. data/docs/export.png +0 -0
  105. data/docs/filters.png +0 -0
  106. data/docs/table.png +0 -0
  107. data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
  108. data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
  109. data/lib/mensa/configuration.rb +35 -15
  110. data/lib/mensa/engine.rb +15 -10
  111. data/lib/mensa/version.rb +1 -1
  112. data/lib/mensa.rb +2 -2
  113. data/lib/tasks/mensa_tasks.rake +1 -1
  114. data/mensa.gemspec +3 -2
  115. data/mise.toml +8 -0
  116. data/package-lock.json +0 -7
  117. metadata +60 -15
  118. data/app/components/mensa/filter/component_controller.js +0 -12
  119. data/app/components/mensa/filter_list/component.css +0 -14
  120. data/app/components/mensa/filter_list/component.html.slim +0 -14
  121. data/app/components/mensa/filter_list/component_controller.js +0 -14
  122. /data/{rubocop.yml → .rubocop.yml} +0 -0
@@ -1,80 +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
- static outlets = ["mensa-filter"]
6
-
7
- static targets = [
8
- "controlBar", // Bar with buttons
9
- "condenseExpandIcon", // Icon
10
- "filterList", // Tabs or list of filters
11
- "views", // Tabs or list of views
12
- "viewButtons", // Cancel and save buttons for views
13
- "search", // Search bar
14
- "view", // View contains table element
15
- "turboFrame", // The turbo-frame
16
- ];
17
- static values = {
18
- supportsViews: Boolean,
19
- };
20
-
21
- connect() {
22
- super.connect();
23
- }
24
-
25
- openFiltersAndSearch(event) {
26
- event.preventDefault();
27
-
28
- if (this.supportsViewsValue) {
29
- this.viewButtonsTarget.classList.remove("hidden");
30
- this.searchTarget.classList.remove("hidden");
31
- this.viewsTarget.classList.add("hidden");
32
- this.filterListTarget.classList.remove("hidden");
33
- } else {
34
- this.controlBarTarget.classList.add("hidden");
35
- this.viewButtonsTarget.classList.remove("hidden");
36
- this.filterListTarget.classList.remove("hidden");
37
- }
38
- }
39
-
40
- cancelFiltersAndSearch(event) {
41
- event.preventDefault();
42
-
43
- if (this.supportsViewsValue) {
44
- this.searchTarget.classList.add("hidden");
45
- this.viewButtonsTarget.classList.add("hidden");
46
- this.filterListTarget.classList.add("hidden");
47
- this.viewsTarget.classList.remove("hidden");
48
- } else {
49
- this.controlBarTarget.classList.remove("hidden");
50
- this.viewButtonsTarget.classList.add("hidden");
51
- this.filterListTarget.classList.add("hidden");
52
- }
53
- }
54
-
55
- saveFiltersAndSearch(event) {
56
- event.preventDefault();
57
- }
58
-
59
- condenseExpand(event) {
60
- event.preventDefault();
61
-
62
- if (this.viewTarget.classList.contains("mensa-table__condensed")) {
63
- this.viewTarget.classList.remove("mensa-table__condensed");
64
- this.condenseExpandIconTarget.classList.add("fa-compress");
65
- this.condenseExpandIconTarget.classList.remove("fa-expand");
66
- } else {
67
- this.viewTarget.classList.add("mensa-table__condensed");
68
- this.condenseExpandIconTarget.classList.remove("fa-compress");
69
- this.condenseExpandIconTarget.classList.add("fa-expand");
70
- }
71
- }
72
-
73
- export(event) {
74
- event.preventDefault();
75
-
76
- let url = this.ourUrl;
77
- url.pathname += ".xlsx";
78
- get(url, {}).then(() => { });
79
- }
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
+ }
80
528
  }
@@ -0,0 +1,6 @@
1
+ .mensa-table {
2
+ tbody {
3
+ td {
4
+ }
5
+ }
6
+ }
@@ -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.flex.space-x-2
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
- | &nbsp;
@@ -16,4 +16,4 @@ module Mensa
16
16
  end
17
17
  end
18
18
  end
19
- end
19
+ end