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.
- 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/CHANGELOG.md +6 -0
- data/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +95 -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 +8 -2
- data/app/components/mensa/add_filter/component_controller.js +697 -85
- 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 +2 -6
- 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 -0
- data/app/components/mensa/filter_pill/component.rb +24 -0
- data/app/components/mensa/filter_pill/component_controller.js +52 -0
- data/app/components/mensa/filter_pill_list/component.css +63 -0
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
- data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
- data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/header/component.rb +1 -1
- data/app/components/mensa/row_action/component.html.slim +2 -2
- data/app/components/mensa/row_action/component.rb +1 -1
- 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.rb +1 -1
- 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 -76
- 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/table_row/component.rb +1 -1
- 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/application_controller.rb +1 -1
- 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 +6 -2
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +5 -14
- data/app/helpers/mensa/application_helper.rb +4 -1
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/application_controller.js +5 -21
- data/app/javascript/mensa/controllers/index.js +16 -7
- data/app/jobs/mensa/export_job.rb +77 -85
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/action.rb +3 -1
- data/app/tables/mensa/base.rb +103 -17
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +21 -6
- data/app/tables/mensa/column.rb +30 -25
- data/app/tables/mensa/config/action_dsl.rb +1 -1
- 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/dsl_logic.rb +8 -4
- 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 +14 -4
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +34 -3
- data/app/tables/mensa/filter.rb +94 -14
- data/app/tables/mensa/row.rb +1 -1
- data/app/tables/mensa/scope.rb +25 -13
- 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 +34 -6
- data/app/views/mensa/tables/show.html.slim +2 -0
- data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
- 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/bin/setup +1 -1
- data/config/locales/en.yml +45 -1
- data/config/locales/nl.yml +46 -1
- 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/generators/mensa/tailwind_config_generator.rb +3 -3
- data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
- data/lib/mensa/configuration.rb +35 -15
- data/lib/mensa/engine.rb +15 -10
- data/lib/mensa/version.rb +1 -1
- data/lib/mensa.rb +2 -2
- data/lib/tasks/mensa_tasks.rake +1 -1
- data/mensa.gemspec +3 -2
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +60 -15
- data/app/components/mensa/filter/component_controller.js +0 -12
- data/app/components/mensa/filter_list/component.css +0 -14
- data/app/components/mensa/filter_list/component.html.slim +0 -14
- data/app/components/mensa/filter_list/component_controller.js +0 -14
- /data/{rubocop.yml → .rubocop.yml} +0 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
+
import { get } from "@rails/request.js";
|
|
3
|
+
|
|
4
|
+
export default class FilterPillListComponentController extends ApplicationController {
|
|
5
|
+
static outlets = ["mensa-table", "mensa-filter-pill", "mensa-add-filter"];
|
|
6
|
+
static targets = ["searchInput", "resetSearchButton"];
|
|
7
|
+
|
|
8
|
+
static values = {
|
|
9
|
+
supportsViews: Boolean,
|
|
10
|
+
tableName: String,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
super.connect();
|
|
15
|
+
this._monitorResetButton();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The mensa-table outlet provides `ourUrl`, which we need both to apply and
|
|
19
|
+
// to restore state. Outlets connect asynchronously, so we trigger the restore
|
|
20
|
+
// from the outlet-connected callback to be sure it's available.
|
|
21
|
+
mensaTableOutletConnected() {
|
|
22
|
+
this.restoreState();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// --- Search actions (formerly in search component controller) ---
|
|
26
|
+
|
|
27
|
+
searchFocused(event) {
|
|
28
|
+
if (this._supportsColumnFilters()) {
|
|
29
|
+
// Don't open the column list while the value popover is already showing —
|
|
30
|
+
// focusing the search input for keyboard nav must not clobber the popover.
|
|
31
|
+
if (this.mensaAddFilterOutlet.isValuePopoverOpen) return;
|
|
32
|
+
this.mensaAddFilterOutlet.filterColumns("");
|
|
33
|
+
this.mensaAddFilterOutlet.showList(this.searchInputTarget);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
monitorSearch(event) {
|
|
38
|
+
const value = this.hasSearchInputTarget
|
|
39
|
+
? this.searchInputTarget.value
|
|
40
|
+
: "";
|
|
41
|
+
this._monitorResetButton();
|
|
42
|
+
|
|
43
|
+
if (this._supportsColumnFilters()) {
|
|
44
|
+
if (this.mensaAddFilterOutlet.isValuePopoverOpen) {
|
|
45
|
+
this.mensaAddFilterOutlet.filterValues(value);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.mensaAddFilterOutlet.filterColumns(value);
|
|
49
|
+
if (value.length > 0) {
|
|
50
|
+
if (this.mensaAddFilterOutlet.visibleColumnCount > 0) {
|
|
51
|
+
this.mensaAddFilterOutlet.showList(this.searchInputTarget);
|
|
52
|
+
} else {
|
|
53
|
+
// No matching columns — hide column list so Enter does a text search
|
|
54
|
+
this.mensaAddFilterOutlet.hideList();
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
this.mensaAddFilterOutlet.showList(this.searchInputTarget);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resetSearch(event) {
|
|
63
|
+
if (event) event.preventDefault();
|
|
64
|
+
|
|
65
|
+
if (this.hasSearchInputTarget) {
|
|
66
|
+
this.searchInputTarget.value = "";
|
|
67
|
+
this.searchInputTarget.focus();
|
|
68
|
+
}
|
|
69
|
+
if (this.hasResetSearchButtonTarget) {
|
|
70
|
+
this.resetSearchButtonTarget.classList.add("hidden");
|
|
71
|
+
}
|
|
72
|
+
if (this._supportsColumnFilters()) {
|
|
73
|
+
this.mensaAddFilterOutlet.hideList();
|
|
74
|
+
this.mensaAddFilterOutlet.closeValuePopover?.();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.setQuery("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
search(event) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
|
|
83
|
+
// If the value popover is open, Enter confirms the highlighted/selected item
|
|
84
|
+
if (
|
|
85
|
+
this._supportsColumnFilters() &&
|
|
86
|
+
this.mensaAddFilterOutlet.isValuePopoverOpen
|
|
87
|
+
) {
|
|
88
|
+
this.mensaAddFilterOutlet.confirmHighlightedValue();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If the column autocomplete has visible options and one is selected via
|
|
93
|
+
// keyboard, let the add-filter controller handle it
|
|
94
|
+
if (
|
|
95
|
+
this._supportsColumnFilters() &&
|
|
96
|
+
this.mensaAddFilterOutlet.hasHighlightedColumn
|
|
97
|
+
) {
|
|
98
|
+
this.mensaAddFilterOutlet.confirmHighlightedColumn();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hide column dropdown before searching
|
|
103
|
+
if (this._supportsColumnFilters()) {
|
|
104
|
+
this.mensaAddFilterOutlet.hideList();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const query = this.hasSearchInputTarget
|
|
108
|
+
? this.searchInputTarget.value
|
|
109
|
+
: "";
|
|
110
|
+
if (query.length > 0 && query.length < 3) return;
|
|
111
|
+
|
|
112
|
+
this.setQuery(query);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
navigateDown(event) {
|
|
116
|
+
if (!this._supportsColumnFilters()) return;
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
if (this.mensaAddFilterOutlet.isValuePopoverOpen) {
|
|
119
|
+
this.mensaAddFilterOutlet.highlightNextValue();
|
|
120
|
+
} else {
|
|
121
|
+
this.mensaAddFilterOutlet.highlightNext();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
navigateUp(event) {
|
|
126
|
+
if (!this._supportsColumnFilters()) return;
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
if (this.mensaAddFilterOutlet.isValuePopoverOpen) {
|
|
129
|
+
this.mensaAddFilterOutlet.highlightPrevValue();
|
|
130
|
+
} else {
|
|
131
|
+
this.mensaAddFilterOutlet.highlightPrev();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Filter state management ---
|
|
136
|
+
|
|
137
|
+
// Called when an existing filter pill is clicked.
|
|
138
|
+
editFilter(columnName, value, operator, anchor) {
|
|
139
|
+
if (!this.hasMensaAddFilterOutlet) return;
|
|
140
|
+
this.mensaAddFilterOutlet.editColumn(
|
|
141
|
+
columnName,
|
|
142
|
+
value,
|
|
143
|
+
operator,
|
|
144
|
+
anchor,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Called when a filter is added/changed.
|
|
149
|
+
refreshFilters() {
|
|
150
|
+
this._notifyUnsavedState();
|
|
151
|
+
return this.applyState({
|
|
152
|
+
filters: this.collectFilters(),
|
|
153
|
+
query: this.loadQuery(),
|
|
154
|
+
view: this.loadView(),
|
|
155
|
+
order: this.loadOrder(),
|
|
156
|
+
page: "",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Called by the search controller when the query is submitted or reset.
|
|
161
|
+
setQuery(query) {
|
|
162
|
+
if (query && query.length > 0) this._notifyUnsavedState();
|
|
163
|
+
this.applyState({
|
|
164
|
+
filters: this.collectFilters(),
|
|
165
|
+
query,
|
|
166
|
+
view: this.loadView(),
|
|
167
|
+
order: this.loadOrder(),
|
|
168
|
+
page: "",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Called by the views controller when a view tab is selected.
|
|
173
|
+
viewSelected(view) {
|
|
174
|
+
const state = {
|
|
175
|
+
filters: {},
|
|
176
|
+
query: "",
|
|
177
|
+
view,
|
|
178
|
+
order: this.loadOrder(),
|
|
179
|
+
page: "",
|
|
180
|
+
};
|
|
181
|
+
this.persistState(state);
|
|
182
|
+
this.setSearchField("");
|
|
183
|
+
this.updateSearchPlaceholder();
|
|
184
|
+
if (this.hasMensaTableOutlet) {
|
|
185
|
+
this.mensaTableOutlet.viewChanged();
|
|
186
|
+
}
|
|
187
|
+
this.requestState(state);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Called by the table controller after a turbo-frame navigation.
|
|
191
|
+
captureNavigation(src) {
|
|
192
|
+
let url;
|
|
193
|
+
try {
|
|
194
|
+
url = new URL(src, window.location.origin);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.lastTableUrl = url.toString();
|
|
200
|
+
this.persistPage(url.searchParams.get("page") || "");
|
|
201
|
+
// Only update the stored view when the param is explicitly present.
|
|
202
|
+
// An absent table_view_id (e.g. the plain tableUrlValue on back-navigation)
|
|
203
|
+
// must not erase a view the user deliberately selected.
|
|
204
|
+
if (url.searchParams.has("table_view_id")) {
|
|
205
|
+
this.persistView(url.searchParams.get("table_view_id") || "");
|
|
206
|
+
}
|
|
207
|
+
const order = this.parseOrderParams(url.searchParams);
|
|
208
|
+
this.persistOrder(order);
|
|
209
|
+
|
|
210
|
+
if (Object.keys(order).length > 0) {
|
|
211
|
+
this._notifyUnsavedState();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
currentRequestState() {
|
|
216
|
+
let url;
|
|
217
|
+
try {
|
|
218
|
+
url = this.lastTableUrl
|
|
219
|
+
? new URL(this.lastTableUrl, window.location.origin)
|
|
220
|
+
: this.mensaTableOutlet.ourUrl;
|
|
221
|
+
} catch (e) {
|
|
222
|
+
url = this.mensaTableOutlet.ourUrl;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const params = url.searchParams;
|
|
226
|
+
const filters = {};
|
|
227
|
+
const order = {};
|
|
228
|
+
params.forEach((value, key) => {
|
|
229
|
+
// Multi-select: filters[col][value][]
|
|
230
|
+
const multiMatch = key.match(/^filters\[(.+?)\]\[value\]\[\]$/);
|
|
231
|
+
if (multiMatch) {
|
|
232
|
+
const column = multiMatch[1];
|
|
233
|
+
const f = (filters[column] = filters[column] || {});
|
|
234
|
+
f.value = Array.isArray(f.value)
|
|
235
|
+
? [...f.value, value]
|
|
236
|
+
: [value];
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const filterMatch = key.match(
|
|
240
|
+
/^filters\[(.+?)\]\[(value|operator)\]$/,
|
|
241
|
+
);
|
|
242
|
+
if (filterMatch) {
|
|
243
|
+
const column = filterMatch[1];
|
|
244
|
+
(filters[column] = filters[column] || {})[filterMatch[2]] =
|
|
245
|
+
value;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const orderMatch = key.match(/^order\[(.+)\]$/);
|
|
249
|
+
if (orderMatch) order[orderMatch[1]] = value;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
filters,
|
|
254
|
+
query: params.get("query") || "",
|
|
255
|
+
view: params.get("table_view_id") || "",
|
|
256
|
+
page: params.get("page") || "",
|
|
257
|
+
order,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clearFiltersAndSearch() {
|
|
262
|
+
const view = this.loadView();
|
|
263
|
+
// Wipe all dirty-state entries; keep only the active view.
|
|
264
|
+
this.clearPersistedDirtyState();
|
|
265
|
+
this.persistView(view);
|
|
266
|
+
this.setSearchField("");
|
|
267
|
+
// buildUrl reads column_order/hidden_columns from localStorage — now empty,
|
|
268
|
+
// so the server receives a completely clean request for the current view.
|
|
269
|
+
this.requestState({
|
|
270
|
+
filters: {},
|
|
271
|
+
query: "",
|
|
272
|
+
view,
|
|
273
|
+
order: {},
|
|
274
|
+
page: "",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
restoreState() {
|
|
279
|
+
const table = this.mensaTableOutlet;
|
|
280
|
+
const filters = this.loadFilters();
|
|
281
|
+
const query = this.loadQuery();
|
|
282
|
+
const view = this.loadView();
|
|
283
|
+
const page = this.loadPage();
|
|
284
|
+
const order = this.loadOrder();
|
|
285
|
+
const columnOrder = this.loadColumnOrder();
|
|
286
|
+
const hiddenColumns = this.loadHiddenColumns();
|
|
287
|
+
|
|
288
|
+
// Show save/reset if any persistent state exists (order, column layout, filters, search)
|
|
289
|
+
if (
|
|
290
|
+
Object.keys(filters).length > 0 ||
|
|
291
|
+
query.length > 0 ||
|
|
292
|
+
Object.keys(order).length > 0 ||
|
|
293
|
+
columnOrder.length > 0 ||
|
|
294
|
+
hiddenColumns.length > 0
|
|
295
|
+
) {
|
|
296
|
+
this._notifyUnsavedState();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const hasFilterOrSearch =
|
|
300
|
+
Object.keys(filters).length > 0 || query.length > 0;
|
|
301
|
+
const hasState =
|
|
302
|
+
hasFilterOrSearch ||
|
|
303
|
+
view.length > 0 ||
|
|
304
|
+
page.length > 0 ||
|
|
305
|
+
Object.keys(order).length > 0 ||
|
|
306
|
+
columnOrder.length > 0;
|
|
307
|
+
|
|
308
|
+
const alreadyRendered = Object.keys(this.renderedFilters()).length > 0;
|
|
309
|
+
|
|
310
|
+
if (!hasState || alreadyRendered || table.frameLoadHandled) {
|
|
311
|
+
if (typeof table.loadFrame === "function") {
|
|
312
|
+
table.loadFrame();
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
table.frameLoadHandled = true;
|
|
318
|
+
|
|
319
|
+
this.setSearchField(query);
|
|
320
|
+
this.setViewHighlight(view);
|
|
321
|
+
|
|
322
|
+
const state = { filters, query, view, page, order };
|
|
323
|
+
|
|
324
|
+
// No need to show/hide the filter bar — it's always visible.
|
|
325
|
+
// Just apply state and load the frame.
|
|
326
|
+
this.applyState(state);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
applyState(state) {
|
|
330
|
+
return this.persistAndRequest(state);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
persistAndRequest(state) {
|
|
334
|
+
this.persistState(state);
|
|
335
|
+
return this.requestState(state);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
requestState(state) {
|
|
339
|
+
const url = this.buildUrl(state);
|
|
340
|
+
this.lastTableUrl = url.toString();
|
|
341
|
+
return get(url, { responseKind: "turbo-stream" });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
buildUrl(state) {
|
|
345
|
+
const url = this.mensaTableOutlet.ourUrl;
|
|
346
|
+
|
|
347
|
+
this.removeFilterParams(url);
|
|
348
|
+
this.removeOrderParams(url);
|
|
349
|
+
this.removeColumnParams(url);
|
|
350
|
+
url.searchParams.delete("query");
|
|
351
|
+
url.searchParams.delete("page");
|
|
352
|
+
url.searchParams.delete("table_view_id");
|
|
353
|
+
|
|
354
|
+
Object.entries(state.filters || {}).forEach(([columnName, filter]) => {
|
|
355
|
+
if (Object.prototype.hasOwnProperty.call(filter, "value")) {
|
|
356
|
+
if (Array.isArray(filter.value)) {
|
|
357
|
+
filter.value.forEach((v) =>
|
|
358
|
+
url.searchParams.append(
|
|
359
|
+
`filters[${columnName}][value][]`,
|
|
360
|
+
v,
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
url.searchParams.append(
|
|
365
|
+
`filters[${columnName}][value]`,
|
|
366
|
+
filter.value ?? "",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
url.searchParams.append(
|
|
371
|
+
`filters[${columnName}][operator]`,
|
|
372
|
+
filter.operator,
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
Object.entries(state.order || {}).forEach(([columnName, direction]) => {
|
|
377
|
+
url.searchParams.set(`order[${columnName}]`, direction);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (state.query) url.searchParams.set("query", state.query);
|
|
381
|
+
if (state.view) url.searchParams.set("table_view_id", state.view);
|
|
382
|
+
if (state.page) url.searchParams.set("page", state.page);
|
|
383
|
+
|
|
384
|
+
this.loadColumnOrder().forEach((col) =>
|
|
385
|
+
url.searchParams.append("column_order[]", col),
|
|
386
|
+
);
|
|
387
|
+
this.loadHiddenColumns().forEach((col) =>
|
|
388
|
+
url.searchParams.append("hidden_columns[]", col),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return url;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
removeFilterParams(url) {
|
|
395
|
+
const keys = new Set();
|
|
396
|
+
url.searchParams.forEach((_v, key) => {
|
|
397
|
+
if (key.startsWith("filters[")) keys.add(key);
|
|
398
|
+
});
|
|
399
|
+
keys.forEach((key) => url.searchParams.delete(key));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
removeColumnParams(url) {
|
|
403
|
+
const keys = [];
|
|
404
|
+
url.searchParams.forEach((_v, key) => {
|
|
405
|
+
if (
|
|
406
|
+
key.startsWith("column_order") ||
|
|
407
|
+
key.startsWith("hidden_columns")
|
|
408
|
+
)
|
|
409
|
+
keys.push(key);
|
|
410
|
+
});
|
|
411
|
+
keys.forEach((key) => url.searchParams.delete(key));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
removeOrderParams(url) {
|
|
415
|
+
const keys = [];
|
|
416
|
+
url.searchParams.forEach((_v, key) => {
|
|
417
|
+
if (key.startsWith("order[")) keys.push(key);
|
|
418
|
+
});
|
|
419
|
+
keys.forEach((key) => url.searchParams.delete(key));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
parseOrderParams(searchParams) {
|
|
423
|
+
const order = {};
|
|
424
|
+
searchParams.forEach((value, key) => {
|
|
425
|
+
const match = key.match(/^order\[(.+)\]$/);
|
|
426
|
+
if (match) order[match[1]] = value;
|
|
427
|
+
});
|
|
428
|
+
return order;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
collectFilters() {
|
|
432
|
+
const filters = this.renderedFilters();
|
|
433
|
+
|
|
434
|
+
if (
|
|
435
|
+
this.hasMensaAddFilterOutlet &&
|
|
436
|
+
this.mensaAddFilterOutlet.selectedFilterColumn
|
|
437
|
+
) {
|
|
438
|
+
const isMultiple = this.mensaAddFilterOutlet.isMultipleMode;
|
|
439
|
+
const filter = {
|
|
440
|
+
operator: this.mensaAddFilterOutlet.operator,
|
|
441
|
+
};
|
|
442
|
+
if (this.mensaAddFilterOutlet.operatorRequiresValue()) {
|
|
443
|
+
filter.value = isMultiple
|
|
444
|
+
? this.mensaAddFilterOutlet.selectedValues
|
|
445
|
+
: this.mensaAddFilterOutlet.hasValueTarget
|
|
446
|
+
? this.mensaAddFilterOutlet.valueTarget.value
|
|
447
|
+
: "";
|
|
448
|
+
}
|
|
449
|
+
filters[this.mensaAddFilterOutlet.selectedFilterColumn] = filter;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return filters;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
renderedFilters() {
|
|
456
|
+
const filters = {};
|
|
457
|
+
|
|
458
|
+
this.element
|
|
459
|
+
.querySelectorAll('[data-controller~="mensa-filter-pill"]')
|
|
460
|
+
.forEach((pill) => {
|
|
461
|
+
const columnName = pill.getAttribute(
|
|
462
|
+
"data-mensa-filter-pill-column-name-value",
|
|
463
|
+
);
|
|
464
|
+
if (!columnName) return;
|
|
465
|
+
|
|
466
|
+
const operator = pill.getAttribute(
|
|
467
|
+
"data-mensa-filter-pill-operator-value",
|
|
468
|
+
);
|
|
469
|
+
const operatorWithoutValue =
|
|
470
|
+
pill.getAttribute(
|
|
471
|
+
"data-mensa-filter-pill-operator-without-value-value",
|
|
472
|
+
) === "true";
|
|
473
|
+
const rawValue = pill.getAttribute(
|
|
474
|
+
"data-mensa-filter-pill-value-value",
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (operatorWithoutValue) {
|
|
478
|
+
filters[columnName] = { operator };
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let value;
|
|
483
|
+
try {
|
|
484
|
+
value = JSON.parse(rawValue);
|
|
485
|
+
} catch {
|
|
486
|
+
value = rawValue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
filters[columnName] = {
|
|
490
|
+
value,
|
|
491
|
+
operator,
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return filters;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
setSearchField(query) {
|
|
499
|
+
if (this.hasSearchInputTarget) {
|
|
500
|
+
this.searchInputTarget.value = query || "";
|
|
501
|
+
}
|
|
502
|
+
this._monitorResetButton();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
setViewHighlight(view) {
|
|
506
|
+
if (!view) return;
|
|
507
|
+
|
|
508
|
+
const root = this.element.closest(".mensa-table");
|
|
509
|
+
if (!root) return;
|
|
510
|
+
|
|
511
|
+
// Update the views dropdown option checks
|
|
512
|
+
root.querySelectorAll('[data-mensa-views-target="view"]').forEach(
|
|
513
|
+
(el) => {
|
|
514
|
+
const linkView = el.getAttribute("data-view-id") || "";
|
|
515
|
+
const check = el.querySelector(
|
|
516
|
+
".mensa-table__views__option-check",
|
|
517
|
+
);
|
|
518
|
+
if (check) {
|
|
519
|
+
check.classList.toggle(
|
|
520
|
+
"invisible",
|
|
521
|
+
view !== "" && linkView !== view,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Update the trigger label
|
|
528
|
+
const triggerLabel = root.querySelector(
|
|
529
|
+
'[data-mensa-views-target="triggerLabel"]',
|
|
530
|
+
);
|
|
531
|
+
if (triggerLabel && view) {
|
|
532
|
+
const viewEl = root.querySelector(
|
|
533
|
+
`[data-mensa-views-target="view"][data-view-id="${view}"]`,
|
|
534
|
+
);
|
|
535
|
+
if (viewEl) {
|
|
536
|
+
const name =
|
|
537
|
+
viewEl.dataset.viewName ||
|
|
538
|
+
viewEl.querySelector("span")?.textContent?.trim();
|
|
539
|
+
if (name) triggerLabel.textContent = name;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.updateSearchPlaceholder();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
updateSearchPlaceholder() {}
|
|
547
|
+
|
|
548
|
+
_supportsColumnFilters() {
|
|
549
|
+
return (
|
|
550
|
+
this.hasMensaAddFilterOutlet &&
|
|
551
|
+
this.mensaAddFilterOutlet.filterListItemTargets.length > 0
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Clears all dirty-state localStorage keys so the view is clean after save.
|
|
556
|
+
clearPersistedDirtyState() {
|
|
557
|
+
this.writeStorage(this.filtersStorageKey, null);
|
|
558
|
+
this.writeStorage(this.searchStorageKey, null);
|
|
559
|
+
this.writeStorage(this.orderStorageKey, null);
|
|
560
|
+
this.writeStorage(`mensa:column_order:${this.tableNameValue}`, null);
|
|
561
|
+
this.writeStorage(`mensa:hidden_columns:${this.tableNameValue}`, null);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Keep these for backward-compatibility with other controllers that look up
|
|
565
|
+
// the search input via helper methods.
|
|
566
|
+
searchInputElement() {
|
|
567
|
+
return this.hasSearchInputTarget ? this.searchInputTarget : null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
resetSearchButtonElement() {
|
|
571
|
+
return this.hasResetSearchButtonTarget
|
|
572
|
+
? this.resetSearchButtonTarget
|
|
573
|
+
: null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --- Persistence ---
|
|
577
|
+
|
|
578
|
+
persistState(state) {
|
|
579
|
+
this.persistFilters(state.filters || {});
|
|
580
|
+
this.persistQuery(state.query || "");
|
|
581
|
+
this.persistView(state.view || "");
|
|
582
|
+
this.persistPage(state.page || "");
|
|
583
|
+
this.persistOrder(state.order || {});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
persistFilters(filters) {
|
|
587
|
+
this.writeStorage(
|
|
588
|
+
this.filtersStorageKey,
|
|
589
|
+
Object.keys(filters).length > 0 ? JSON.stringify(filters) : null,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
loadFilters() {
|
|
594
|
+
const raw = this.readStorage(this.filtersStorageKey);
|
|
595
|
+
if (!raw) return {};
|
|
596
|
+
try {
|
|
597
|
+
return JSON.parse(raw);
|
|
598
|
+
} catch (e) {
|
|
599
|
+
return {};
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
persistQuery(query) {
|
|
604
|
+
this.writeStorage(
|
|
605
|
+
this.searchStorageKey,
|
|
606
|
+
query && query.length > 0 ? query : null,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
loadQuery() {
|
|
611
|
+
return this.readStorage(this.searchStorageKey) || "";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
persistView(view) {
|
|
615
|
+
this.writeStorage(
|
|
616
|
+
this.viewStorageKey,
|
|
617
|
+
view && view.length > 0 ? view : null,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
loadView() {
|
|
622
|
+
return this.readStorage(this.viewStorageKey) || "";
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
persistPage(page) {
|
|
626
|
+
this.writeStorage(
|
|
627
|
+
this.pageStorageKey,
|
|
628
|
+
page && page !== "1" ? page : null,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
loadPage() {
|
|
633
|
+
return this.readStorage(this.pageStorageKey) || "";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
persistOrder(order) {
|
|
637
|
+
this.writeStorage(
|
|
638
|
+
this.orderStorageKey,
|
|
639
|
+
order && Object.keys(order).length > 0
|
|
640
|
+
? JSON.stringify(order)
|
|
641
|
+
: null,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
loadOrder() {
|
|
646
|
+
const raw = this.readStorage(this.orderStorageKey);
|
|
647
|
+
if (!raw) return {};
|
|
648
|
+
try {
|
|
649
|
+
return JSON.parse(raw);
|
|
650
|
+
} catch (e) {
|
|
651
|
+
return {};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
writeStorage(key, value) {
|
|
656
|
+
if (!this.hasStorage) return;
|
|
657
|
+
try {
|
|
658
|
+
if (value === null) {
|
|
659
|
+
window.localStorage.removeItem(key);
|
|
660
|
+
} else {
|
|
661
|
+
window.localStorage.setItem(key, value);
|
|
662
|
+
}
|
|
663
|
+
} catch (e) {}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
readStorage(key) {
|
|
667
|
+
if (!this.hasStorage) return null;
|
|
668
|
+
try {
|
|
669
|
+
return window.localStorage.getItem(key);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
get hasStorage() {
|
|
676
|
+
try {
|
|
677
|
+
return typeof window !== "undefined" && !!window.localStorage;
|
|
678
|
+
} catch (e) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
loadColumnOrder() {
|
|
684
|
+
try {
|
|
685
|
+
return (
|
|
686
|
+
JSON.parse(
|
|
687
|
+
this.readStorage(
|
|
688
|
+
`mensa:column_order:${this.tableNameValue}`,
|
|
689
|
+
),
|
|
690
|
+
) || []
|
|
691
|
+
);
|
|
692
|
+
} catch (e) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
loadHiddenColumns() {
|
|
698
|
+
try {
|
|
699
|
+
return (
|
|
700
|
+
JSON.parse(
|
|
701
|
+
this.readStorage(
|
|
702
|
+
`mensa:hidden_columns:${this.tableNameValue}`,
|
|
703
|
+
),
|
|
704
|
+
) || []
|
|
705
|
+
);
|
|
706
|
+
} catch (e) {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
get filtersStorageKey() {
|
|
712
|
+
return `mensa:filters:${this.tableNameValue}`;
|
|
713
|
+
}
|
|
714
|
+
get searchStorageKey() {
|
|
715
|
+
return `mensa:search:${this.tableNameValue}`;
|
|
716
|
+
}
|
|
717
|
+
get viewStorageKey() {
|
|
718
|
+
return `mensa:view:${this.tableNameValue}`;
|
|
719
|
+
}
|
|
720
|
+
get pageStorageKey() {
|
|
721
|
+
return `mensa:page:${this.tableNameValue}`;
|
|
722
|
+
}
|
|
723
|
+
get orderStorageKey() {
|
|
724
|
+
return `mensa:order:${this.tableNameValue}`;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
get ourUrl() {
|
|
728
|
+
return this.mensaTableOutlet.ourUrl;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// --- Private ---
|
|
732
|
+
|
|
733
|
+
_monitorResetButton() {
|
|
734
|
+
if (!this.hasSearchInputTarget) return;
|
|
735
|
+
const hasValue = this.searchInputTarget.value.length > 0;
|
|
736
|
+
if (this.hasResetSearchButtonTarget) {
|
|
737
|
+
this.resetSearchButtonTarget.classList.toggle("hidden", !hasValue);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
_notifyUnsavedState() {
|
|
742
|
+
if (
|
|
743
|
+
this.hasMensaTableOutlet &&
|
|
744
|
+
typeof this.mensaTableOutlet.notifyUnsavedState === "function"
|
|
745
|
+
) {
|
|
746
|
+
this.mensaTableOutlet.notifyUnsavedState();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|