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,40 @@
|
|
|
1
|
+
.mensa-table {
|
|
2
|
+
&__column_customizer {
|
|
3
|
+
&__popover {
|
|
4
|
+
@apply fixed z-50 w-64 rounded-xl bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 py-2;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
&__heading {
|
|
8
|
+
@apply px-4 pt-2 pb-1 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&__row {
|
|
12
|
+
@apply flex items-center gap-3 px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-grab select-none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&__handle {
|
|
16
|
+
@apply text-gray-300 dark:text-gray-600 text-xs flex-none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&__name {
|
|
20
|
+
@apply flex-1 text-sm text-gray-700 dark:text-gray-200 truncate;
|
|
21
|
+
|
|
22
|
+
&--hidden {
|
|
23
|
+
@apply text-gray-400 dark:text-gray-500;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&__visibility {
|
|
28
|
+
@apply flex-none text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 cursor-pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Show the correct eye icon based on the row's data-visible attribute.
|
|
32
|
+
Works with both FA CSS (i elements) and FA SVG mode (svg elements). */
|
|
33
|
+
&__row[data-visible="true"] .fa-eye-slash {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
&__row[data-visible="false"] .fa-eye {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.relative data-controller="mensa-column-customizer" data-mensa-column-customizer-table-name-value=table.name data-mensa-column-customizer-turbo-frame-id-value=table.table_id data-mensa-column-customizer-mensa-table-outlet="#table-#{table.table_id}"
|
|
2
|
+
button.mensa-table__control_bar__button type="button" data-action="click->mensa-column-customizer#toggle"
|
|
3
|
+
i class=Mensa.config.icons[:control_bar_edit]
|
|
4
|
+
.mensa-table__column_customizer__popover.hidden data-mensa-column-customizer-target="popover"
|
|
5
|
+
p.mensa-table__column_customizer__heading Columns
|
|
6
|
+
ul
|
|
7
|
+
- table.columns.reject(&:internal?).each do |column|
|
|
8
|
+
li.mensa-table__column_customizer__row[data-mensa-column-customizer-target="columnRow" data-column-name=column.name.to_s data-visible=column.visible?.to_s draggable="true" data-action="dragstart->mensa-column-customizer#dragStart dragover->mensa-column-customizer#dragOver drop->mensa-column-customizer#drop dragend->mensa-column-customizer#dragEnd"]
|
|
9
|
+
i.mensa-table__column_customizer__handle.fa-solid.fa-grip-vertical
|
|
10
|
+
span.mensa-table__column_customizer__name[class=(!column.visible? ? "mensa-table__column_customizer__name--hidden" : "")]
|
|
11
|
+
= column.human_name
|
|
12
|
+
button.mensa-table__column_customizer__visibility type="button" data-action="click->mensa-column-customizer#toggleVisibility"
|
|
13
|
+
i.fa-solid.fa-eye
|
|
14
|
+
i.fa-solid.fa-eye-slash
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
+
|
|
3
|
+
export default class ColumnCustomizerController extends ApplicationController {
|
|
4
|
+
static outlets = ["mensa-table"];
|
|
5
|
+
static targets = ["popover", "columnRow"];
|
|
6
|
+
static values = { turboFrameId: String, tableName: String };
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
super.connect();
|
|
10
|
+
this._dragSource = null;
|
|
11
|
+
this._dragGhost = null;
|
|
12
|
+
this._outsideClickHandler = null;
|
|
13
|
+
|
|
14
|
+
// Capture the server-rendered order before localStorage reorders the DOM.
|
|
15
|
+
this._defaultColumnOrder = this.columnRowTargets.map(
|
|
16
|
+
(r) => r.dataset.columnName,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// 1. Update the DOM immediately (reorder rows, flip data-visible).
|
|
20
|
+
// CSS [data-visible] rules handle icon display — no JS icon work needed.
|
|
21
|
+
const hasState = this._applyLocalState();
|
|
22
|
+
|
|
23
|
+
// 2. Bake the column params into the mensa-table element's tableUrlValue
|
|
24
|
+
// right now, before any outlet connects. filter_pill_list.restoreState()
|
|
25
|
+
// reads ourUrl → tableUrlValue to build the initial frame URL, so by
|
|
26
|
+
// injecting here we get column_order[] into that single first request
|
|
27
|
+
// without needing a separate second frame load.
|
|
28
|
+
if (hasState) {
|
|
29
|
+
this._pendingStorageApply = !this._injectColumnParamsIntoTableUrl();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
disconnect() {
|
|
34
|
+
this._unbindOutsideClick();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback: only reached when _injectColumnParamsIntoTableUrl couldn't find
|
|
38
|
+
// the mensa-table element during connect() (unusual page structures).
|
|
39
|
+
mensaTableOutletConnected() {
|
|
40
|
+
if (this._pendingStorageApply) {
|
|
41
|
+
this._pendingStorageApply = false;
|
|
42
|
+
this._applyChanges();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toggle() {
|
|
47
|
+
if (this.popoverTarget.classList.contains("hidden")) {
|
|
48
|
+
this.popoverTarget.classList.remove("hidden");
|
|
49
|
+
this._positionPopover();
|
|
50
|
+
this._bindOutsideClick();
|
|
51
|
+
} else {
|
|
52
|
+
this.popoverTarget.classList.add("hidden");
|
|
53
|
+
this._unbindOutsideClick();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_positionPopover() {
|
|
58
|
+
const btn = this.element.querySelector("button");
|
|
59
|
+
if (!btn) return;
|
|
60
|
+
const rect = btn.getBoundingClientRect();
|
|
61
|
+
this.popoverTarget.style.top = `${rect.bottom + 4}px`;
|
|
62
|
+
this.popoverTarget.style.right = `${window.innerWidth - rect.right}px`;
|
|
63
|
+
this.popoverTarget.style.left = "auto";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toggleVisibility(event) {
|
|
67
|
+
event.stopPropagation();
|
|
68
|
+
const row = event.currentTarget.closest("[data-column-name]");
|
|
69
|
+
const nowVisible = row.dataset.visible !== "true";
|
|
70
|
+
|
|
71
|
+
// Flip the data-visible attribute — CSS handles which icon is shown.
|
|
72
|
+
row.dataset.visible = nowVisible.toString();
|
|
73
|
+
|
|
74
|
+
const nameEl = row.querySelector(
|
|
75
|
+
".mensa-table__column_customizer__name",
|
|
76
|
+
);
|
|
77
|
+
if (nameEl) {
|
|
78
|
+
nameEl.classList.toggle(
|
|
79
|
+
"mensa-table__column_customizer__name--hidden",
|
|
80
|
+
!nowVisible,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this._persistAndApply();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dragStart(event) {
|
|
88
|
+
this._dragSource = event.currentTarget;
|
|
89
|
+
event.dataTransfer.effectAllowed = "move";
|
|
90
|
+
event.dataTransfer.setData("text/plain", ""); // required for Firefox
|
|
91
|
+
|
|
92
|
+
// Build a clean ghost so the screenshot doesn't bleed through the
|
|
93
|
+
// popover and show the underlying table row.
|
|
94
|
+
const src = event.currentTarget;
|
|
95
|
+
const ghost = src.cloneNode(true);
|
|
96
|
+
Object.assign(ghost.style, {
|
|
97
|
+
position: "absolute",
|
|
98
|
+
top: "-1000px",
|
|
99
|
+
left: "0",
|
|
100
|
+
width: `${src.offsetWidth}px`,
|
|
101
|
+
background: "white",
|
|
102
|
+
borderRadius: "8px",
|
|
103
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
104
|
+
pointerEvents: "none",
|
|
105
|
+
});
|
|
106
|
+
document.body.appendChild(ghost);
|
|
107
|
+
event.dataTransfer.setDragImage(
|
|
108
|
+
ghost,
|
|
109
|
+
src.offsetWidth / 2,
|
|
110
|
+
src.offsetHeight / 2,
|
|
111
|
+
);
|
|
112
|
+
this._dragGhost = ghost;
|
|
113
|
+
|
|
114
|
+
setTimeout(() => this._dragSource?.classList.add("opacity-40"), 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
dragOver(event) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
event.dataTransfer.dropEffect = "move";
|
|
120
|
+
const target = event.currentTarget;
|
|
121
|
+
if (!this._dragSource || target === this._dragSource) return;
|
|
122
|
+
const rect = target.getBoundingClientRect();
|
|
123
|
+
const midY = rect.top + rect.height / 2;
|
|
124
|
+
if (event.clientY < midY) {
|
|
125
|
+
target.parentNode.insertBefore(this._dragSource, target);
|
|
126
|
+
} else {
|
|
127
|
+
target.parentNode.insertBefore(
|
|
128
|
+
this._dragSource,
|
|
129
|
+
target.nextSibling,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
drop(event) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dragEnd(event) {
|
|
139
|
+
this._dragSource?.classList.remove("opacity-40");
|
|
140
|
+
this._dragSource = null;
|
|
141
|
+
if (this._dragGhost) {
|
|
142
|
+
this._dragGhost.remove();
|
|
143
|
+
this._dragGhost = null;
|
|
144
|
+
}
|
|
145
|
+
this._persistAndApply();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Resets the popover DOM to the original server-rendered order and makes all
|
|
149
|
+
// columns visible. Called by the table controller after a view reset.
|
|
150
|
+
resetToDefault() {
|
|
151
|
+
if (this._defaultColumnOrder) {
|
|
152
|
+
this._reorderRows(this._defaultColumnOrder);
|
|
153
|
+
}
|
|
154
|
+
this.columnRowTargets.forEach((row) => {
|
|
155
|
+
row.dataset.visible = "true";
|
|
156
|
+
const nameEl = row.querySelector(
|
|
157
|
+
".mensa-table__column_customizer__name",
|
|
158
|
+
);
|
|
159
|
+
if (nameEl)
|
|
160
|
+
nameEl.classList.remove(
|
|
161
|
+
"mensa-table__column_customizer__name--hidden",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- private ---
|
|
167
|
+
|
|
168
|
+
_persistAndApply() {
|
|
169
|
+
this._persistToStorage();
|
|
170
|
+
this._applyChanges();
|
|
171
|
+
if (this.hasMensaTableOutlet) {
|
|
172
|
+
this.mensaTableOutlet.notifyUnsavedState();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_applyChanges() {
|
|
177
|
+
if (!this.hasMensaTableOutlet) return;
|
|
178
|
+
|
|
179
|
+
const url = this.mensaTableOutlet.ourUrl;
|
|
180
|
+
this._preserveActiveView(url);
|
|
181
|
+
|
|
182
|
+
// Strip stale column customizer params.
|
|
183
|
+
const toDelete = [];
|
|
184
|
+
url.searchParams.forEach((_, key) => {
|
|
185
|
+
if (
|
|
186
|
+
key.startsWith("column_order") ||
|
|
187
|
+
key.startsWith("hidden_columns")
|
|
188
|
+
) {
|
|
189
|
+
toDelete.push(key);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
toDelete.forEach((k) => url.searchParams.delete(k));
|
|
193
|
+
|
|
194
|
+
// Write current order and visibility from the DOM.
|
|
195
|
+
this.columnRowTargets.forEach((row) => {
|
|
196
|
+
url.searchParams.append("column_order[]", row.dataset.columnName);
|
|
197
|
+
if (row.dataset.visible === "false") {
|
|
198
|
+
url.searchParams.append(
|
|
199
|
+
"hidden_columns[]",
|
|
200
|
+
row.dataset.columnName,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Navigate the frame directly so its src stays in sync with the column
|
|
206
|
+
// params. The column customizer lives outside the frame, so the popover
|
|
207
|
+
// remains open. Subsequent filter/sort requests will inherit the updated
|
|
208
|
+
// src (including column_order[] and hidden_columns[]) via ourUrl.
|
|
209
|
+
const frame = document.getElementById(this.turboFrameIdValue);
|
|
210
|
+
if (frame) frame.setAttribute("src", url.toString());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Update localStorage with the current DOM state.
|
|
214
|
+
_persistToStorage() {
|
|
215
|
+
const order = this.columnRowTargets.map((r) => r.dataset.columnName);
|
|
216
|
+
const hidden = this.columnRowTargets
|
|
217
|
+
.filter((r) => r.dataset.visible === "false")
|
|
218
|
+
.map((r) => r.dataset.columnName);
|
|
219
|
+
|
|
220
|
+
this._writeStorage(
|
|
221
|
+
this._columnOrderKey,
|
|
222
|
+
order.length ? JSON.stringify(order) : null,
|
|
223
|
+
);
|
|
224
|
+
this._writeStorage(
|
|
225
|
+
this._hiddenColumnsKey,
|
|
226
|
+
hidden.length ? JSON.stringify(hidden) : null,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Apply persisted column state to the DOM only (no server call).
|
|
231
|
+
// Called during connect() before outlets are available.
|
|
232
|
+
// Returns true if any state was restored (signals that a server call is needed).
|
|
233
|
+
_applyLocalState() {
|
|
234
|
+
const rawOrder = this._readStorage(this._columnOrderKey);
|
|
235
|
+
const rawHidden = this._readStorage(this._hiddenColumnsKey);
|
|
236
|
+
if (!rawOrder && !rawHidden) return false;
|
|
237
|
+
|
|
238
|
+
const order = rawOrder ? JSON.parse(rawOrder) : null;
|
|
239
|
+
const hidden = rawHidden ? JSON.parse(rawHidden) : [];
|
|
240
|
+
|
|
241
|
+
if (order) this._reorderRows(order);
|
|
242
|
+
|
|
243
|
+
// Flip data-visible on each row. The CSS [data-visible] rules handle
|
|
244
|
+
// showing/hiding the correct eye icon — no querySelector("i") needed.
|
|
245
|
+
this.columnRowTargets.forEach((row) => {
|
|
246
|
+
const isHidden = hidden.includes(row.dataset.columnName);
|
|
247
|
+
row.dataset.visible = (!isHidden).toString();
|
|
248
|
+
const nameEl = row.querySelector(
|
|
249
|
+
".mensa-table__column_customizer__name",
|
|
250
|
+
);
|
|
251
|
+
if (nameEl) {
|
|
252
|
+
nameEl.classList.toggle(
|
|
253
|
+
"mensa-table__column_customizer__name--hidden",
|
|
254
|
+
isHidden,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Writes column_order[] and hidden_columns[] into the mensa-table element's
|
|
263
|
+
// data-mensa-table-table-url-value attribute so that ourUrl() (used by
|
|
264
|
+
// filter_pill_list.restoreState) carries the column params in the very first
|
|
265
|
+
// frame request. Returns true on success.
|
|
266
|
+
_injectColumnParamsIntoTableUrl() {
|
|
267
|
+
const tableEl = document.getElementById(
|
|
268
|
+
`table-${this.turboFrameIdValue}`,
|
|
269
|
+
);
|
|
270
|
+
if (!tableEl) return false;
|
|
271
|
+
|
|
272
|
+
const urlStr = tableEl.dataset.mensaTableTableUrlValue;
|
|
273
|
+
if (!urlStr) return false;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const url = new URL(urlStr);
|
|
277
|
+
|
|
278
|
+
const toDelete = [];
|
|
279
|
+
url.searchParams.forEach((_, key) => {
|
|
280
|
+
if (
|
|
281
|
+
key.startsWith("column_order") ||
|
|
282
|
+
key.startsWith("hidden_columns")
|
|
283
|
+
) {
|
|
284
|
+
toDelete.push(key);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
toDelete.forEach((k) => url.searchParams.delete(k));
|
|
288
|
+
|
|
289
|
+
this.columnRowTargets.forEach((row) => {
|
|
290
|
+
url.searchParams.append(
|
|
291
|
+
"column_order[]",
|
|
292
|
+
row.dataset.columnName,
|
|
293
|
+
);
|
|
294
|
+
if (row.dataset.visible === "false") {
|
|
295
|
+
url.searchParams.append(
|
|
296
|
+
"hidden_columns[]",
|
|
297
|
+
row.dataset.columnName,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
this._preserveActiveView(url);
|
|
303
|
+
tableEl.dataset.mensaTableTableUrlValue = url.toString();
|
|
304
|
+
return true;
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_reorderRows(order) {
|
|
311
|
+
if (!this.hasColumnRowTarget) return;
|
|
312
|
+
const list = this.columnRowTargets[0].parentNode;
|
|
313
|
+
const rowMap = {};
|
|
314
|
+
this.columnRowTargets.forEach((row) => {
|
|
315
|
+
rowMap[row.dataset.columnName] = row;
|
|
316
|
+
});
|
|
317
|
+
order.forEach((name) => {
|
|
318
|
+
if (rowMap[name]) list.appendChild(rowMap[name]);
|
|
319
|
+
});
|
|
320
|
+
// Append any rows not present in the saved order at the end.
|
|
321
|
+
this.columnRowTargets.forEach((row) => {
|
|
322
|
+
if (!order.includes(row.dataset.columnName)) {
|
|
323
|
+
list.appendChild(row);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_preserveActiveView(url) {
|
|
329
|
+
const view =
|
|
330
|
+
this.mensaTableOutlet?.mensaFilterPillListOutlet?.loadView?.() ||
|
|
331
|
+
"";
|
|
332
|
+
if (view) url.searchParams.set("table_view_id", view);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
get _columnOrderKey() {
|
|
336
|
+
return `mensa:column_order:${this.tableNameValue}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get _hiddenColumnsKey() {
|
|
340
|
+
return `mensa:hidden_columns:${this.tableNameValue}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_writeStorage(key, value) {
|
|
344
|
+
try {
|
|
345
|
+
if (value === null) {
|
|
346
|
+
window.localStorage.removeItem(key);
|
|
347
|
+
} else {
|
|
348
|
+
window.localStorage.setItem(key, value);
|
|
349
|
+
}
|
|
350
|
+
} catch (e) {
|
|
351
|
+
// localStorage unavailable (private mode / disabled) — ignore.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_readStorage(key) {
|
|
356
|
+
try {
|
|
357
|
+
return window.localStorage.getItem(key);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_bindOutsideClick() {
|
|
364
|
+
this._unbindOutsideClick();
|
|
365
|
+
this._outsideClickHandler = (event) => {
|
|
366
|
+
if (!this.element.contains(event.target)) {
|
|
367
|
+
this.popoverTarget.classList.add("hidden");
|
|
368
|
+
this._unbindOutsideClick();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
// Defer so the opening click doesn't immediately close the popover.
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
document.addEventListener("click", this._outsideClickHandler);
|
|
374
|
+
}, 0);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_unbindOutsideClick() {
|
|
378
|
+
if (this._outsideClickHandler) {
|
|
379
|
+
document.removeEventListener("click", this._outsideClickHandler);
|
|
380
|
+
this._outsideClickHandler = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -1,5 +1,128 @@
|
|
|
1
1
|
.mensa-table {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
&__control_bar {
|
|
3
|
+
&__button {
|
|
4
|
+
@apply h-[26px] inline-flex items-center justify-center gap-1 cursor-pointer rounded-md px-1.5 text-sm text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition-colors;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
&__badge {
|
|
8
|
+
@apply absolute -top-1.5 -right-1.5 inline-flex items-center justify-center min-w-[1.125rem] h-[1.125rem] px-1 rounded-full bg-primary-600 text-white text-[0.625rem] font-semibold leading-none ring-2 ring-white dark:ring-gray-800;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&__save-dropdown {
|
|
12
|
+
@apply fixed z-50 min-w-[10rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1 px-2;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&__save-dropdown-item {
|
|
16
|
+
@apply w-full flex items-center px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer text-left;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&__save-reset {
|
|
21
|
+
@apply flex items-center gap-1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__export-dialog {
|
|
25
|
+
@apply w-[34rem] max-w-[92vw] max-h-[85vh] rounded-2xl border-0 p-0 bg-white dark:bg-gray-800 shadow-2xl ring-1 ring-black/5 dark:ring-white/10 text-gray-900 dark:text-gray-100;
|
|
26
|
+
|
|
27
|
+
&::backdrop {
|
|
28
|
+
@apply bg-gray-900/40;
|
|
29
|
+
backdrop-filter: blur(2px);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&__panel {
|
|
33
|
+
@apply flex flex-col max-h-[85vh] overflow-hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__header {
|
|
37
|
+
@apply flex items-center justify-between gap-4 px-6 py-4 border-b border-gray-200 dark:border-gray-700;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&__title {
|
|
41
|
+
@apply text-lg font-semibold leading-6 text-gray-900 dark:text-gray-100;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&__close {
|
|
45
|
+
@apply cursor-pointer text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 focus:outline-none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__body {
|
|
49
|
+
@apply flex flex-col gap-5 p-6 min-h-0 overflow-y-auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&__section-title {
|
|
53
|
+
@apply text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__empty {
|
|
57
|
+
@apply flex h-44 items-center justify-center rounded-lg ring-1 ring-gray-200 dark:ring-gray-700 px-3 text-center text-sm text-gray-500 dark:text-gray-400;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&__list {
|
|
61
|
+
@apply flex flex-col divide-y divide-gray-100 dark:divide-gray-700 rounded-lg ring-1 ring-gray-200 dark:ring-gray-700 h-44 overflow-x-hidden overflow-y-auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&__item {
|
|
65
|
+
@apply flex shrink-0 items-center justify-between gap-3 px-3 py-2.5;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&__item-info {
|
|
69
|
+
@apply flex flex-col min-w-0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__item-name {
|
|
73
|
+
@apply text-sm font-medium text-gray-900 dark:text-gray-100 truncate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&__item-meta {
|
|
77
|
+
@apply text-xs text-gray-500 dark:text-gray-400;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&__download {
|
|
81
|
+
@apply inline-flex items-center gap-1.5 rounded-md bg-white dark:bg-gray-700 px-2.5 py-1.5 text-sm font-medium text-primary-600 dark:text-primary-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&__status {
|
|
85
|
+
@apply inline-flex items-center gap-1.5 text-sm;
|
|
86
|
+
|
|
87
|
+
&--pending {
|
|
88
|
+
@apply text-gray-500 dark:text-gray-400;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&--failed {
|
|
92
|
+
@apply text-red-600 dark:text-red-400 font-medium;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&__form {
|
|
97
|
+
@apply flex flex-col gap-5 border-t border-gray-200 dark:border-gray-700 pt-5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&__fieldset {
|
|
101
|
+
@apply flex flex-col gap-2 border-0 p-0 m-0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&__option {
|
|
105
|
+
@apply flex items-start gap-2.5 text-sm text-gray-700 dark:text-gray-300 cursor-pointer;
|
|
106
|
+
|
|
107
|
+
input {
|
|
108
|
+
@apply mt-0.5 text-primary-600 focus:ring-primary-500;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__actions {
|
|
113
|
+
@apply flex justify-end gap-2 pt-1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&__button {
|
|
117
|
+
@apply inline-flex items-center gap-1.5 rounded-lg px-3.5 py-2 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors;
|
|
118
|
+
|
|
119
|
+
&--secondary {
|
|
120
|
+
@apply bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:ring-gray-400;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&--primary {
|
|
124
|
+
@apply bg-primary-600 text-white hover:bg-primary-500 focus:ring-primary-500;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
.mensa-table__control_bar data-mensa-table-target="controlBar"
|
|
2
|
-
.flex.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
button.
|
|
10
|
-
i
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
.flex.items-center.gap-1.py-0
|
|
3
|
+
/ Column customizer (always shown first when available)
|
|
4
|
+
- if view_columns_ordering?
|
|
5
|
+
= render Mensa::ColumnCustomizer::Component.new(table: table)
|
|
6
|
+
|
|
7
|
+
/ Save/Reset buttons — shown when there are unsaved filters or search
|
|
8
|
+
.mensa-table__save-reset.hidden data-mensa-table-target="saveResetButtons"
|
|
9
|
+
button.mensa-table__control_bar__button[type="button" title=t('.reset', default: 'Reset') data-action="mensa-table#cancelFiltersAndSearch"]
|
|
10
|
+
i.fa-solid.fa-rotate-left
|
|
11
|
+
- if table.current_user && table.supports_custom_views?
|
|
12
|
+
.relative
|
|
13
|
+
/ Simple save — shown when the default/system view is active
|
|
14
|
+
button.mensa-table__control_bar__button.hidden[type="button" data-mensa-table-target="saveSimple" data-action="mensa-table#saveAsNewView"]
|
|
15
|
+
= t('.save', default: 'Save')
|
|
16
|
+
/ Save with dropdown — shown when a user-created view is active
|
|
17
|
+
button.mensa-table__control_bar__button.hidden[type="button" data-mensa-table-target="saveSplit" data-action="mensa-table#toggleSaveDropdown"]
|
|
18
|
+
= t('.save', default: 'Save')
|
|
19
|
+
i.fa-solid.fa-chevron-down.text-xs
|
|
20
|
+
ul.mensa-table__control_bar__save-dropdown.hidden data-mensa-table-target="saveDropdown"
|
|
21
|
+
li
|
|
22
|
+
button.mensa-table__control_bar__save-dropdown-item[type="button" data-action="mensa-table#updateCurrentViewAction"]
|
|
23
|
+
= t('.update_view', default: 'Update view')
|
|
24
|
+
li
|
|
25
|
+
button.mensa-table__control_bar__save-dropdown-item[type="button" data-action="mensa-table#saveAsNewView"]
|
|
26
|
+
= t('.save_as_new_view', default: 'Save as new view')
|
|
27
|
+
- else
|
|
28
|
+
button.mensa-table__control_bar__button[type="button" data-action="mensa-table#saveAsNewView"]
|
|
29
|
+
= t('.save', default: 'Save')
|
|
30
|
+
|
|
31
|
+
/ Eye icon — toggles visibility of view-origin filter pills (shown by JS when needed)
|
|
32
|
+
button.mensa-table__control_bar__button.hidden[type="button" data-mensa-table-target="eyeButton" data-action="click->mensa-table#toggleViewFilters"]
|
|
33
|
+
i.fa-solid.fa-eye
|
|
34
|
+
|
|
35
|
+
/ Export
|
|
36
|
+
- if table.exportable?
|
|
37
|
+
button.mensa-table__control_bar__button.relative[type="button" data-action="mensa-table#export"]
|
|
16
38
|
i data-mensa-table-target="exportIcon" class=Mensa.config.icons[:control_bar_export]
|
|
39
|
+
= helpers.render partial: "mensa/exports/badge", locals: {table_name: table.name, user: table.current_user}
|
|
40
|
+
|
|
41
|
+
- if table.exportable?
|
|
42
|
+
= helpers.turbo_stream_from Mensa::Export.stream_name(table.name, table.current_user)
|
|
43
|
+
= helpers.render partial: "mensa/exports/dialog", locals: {table: table}
|
|
@@ -9,12 +9,8 @@ module Mensa
|
|
|
9
9
|
@table = table
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
table.
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def view_condensed_toggle?
|
|
17
|
-
table.view_condensed_toggle?
|
|
12
|
+
def view_columns_ordering?
|
|
13
|
+
table.view_columns_ordering?
|
|
18
14
|
end
|
|
19
15
|
end
|
|
20
16
|
end
|