mensa 0.3.4 → 0.5.0
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/.zed/tasks.json +1 -1
- data/Gemfile.lock +1 -4
- data/README.md +26 -14
- data/app/components/mensa/add_filter/component.html.erb +1 -1
- data/app/components/mensa/column_customizer/component_controller.js +1 -1
- data/app/components/mensa/control_bar/component.css +23 -3
- data/app/components/mensa/control_bar/component.html.erb +5 -5
- data/app/components/mensa/empty_state/component.rb +1 -1
- data/app/components/mensa/filter_pill/component.css +17 -3
- data/app/components/mensa/table/component.html.erb +1 -1
- data/app/components/mensa/table/component.rb +3 -3
- data/app/components/mensa/table/component_controller.js +97 -28
- data/app/components/mensa/view/component.html.erb +1 -1
- data/app/components/mensa/view/component.rb +1 -1
- data/app/controllers/mensa/tables/exports_controller.rb +25 -5
- data/app/controllers/mensa/tables_controller.rb +7 -9
- data/app/helpers/mensa/application_helper.rb +2 -2
- data/app/jobs/mensa/export_job.rb +4 -0
- data/app/jobs/mensa/recurring_exports_job.rb +17 -0
- data/app/models/mensa/export.rb +54 -2
- data/app/tables/mensa/base.rb +52 -12
- data/app/tables/mensa/column.rb +16 -6
- data/app/tables/mensa/config/dsl_logic.rb +2 -2
- data/app/tables/mensa/config/table_dsl.rb +2 -0
- data/app/tables/mensa/filter.rb +8 -1
- data/app/tables/mensa/scope.rb +14 -13
- data/app/tables/mensa/search.rb +70 -0
- data/app/views/mensa/exports/_dialog.html.erb +23 -0
- data/app/views/mensa/exports/_list.html.erb +13 -0
- data/app/views/mensa/tables/show.html.erb +0 -2
- data/app/views/mensa/tables/standard_error.html.erb +9 -0
- data/config/locales/en.yml +22 -0
- data/config/locales/nl.yml +21 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20260612110000_add_repeat_to_mensa_exports.rb +8 -0
- data/lib/mensa/configuration.rb +6 -7
- data/lib/mensa/engine.rb +0 -1
- data/lib/mensa/version.rb +1 -1
- data/lib/tasks/mensa_tasks.rake +7 -0
- data/mensa.gemspec +0 -1
- metadata +6 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ad9a06fc13439d733db95cc8d3c0142247291057d8a5c84fce0f090f8c96a63
|
|
4
|
+
data.tar.gz: fef0dcdcaf8b21e038af8baa90f3509016372f30998a7bdd0cb94c3050a19444
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e86a46aab67adf045e5939e9d307732e7752ccc50f3bae26b67dc9176ecb00508405b9b8c9c146fd7f0eb88a71f2aeba7f9f49fc943e3b8bb6910cbd1858e2d9
|
|
7
|
+
data.tar.gz: 89e4d11db3e400215fdbce8cd5b031577a8c254cb92fc37f99c7ebbb726609c7d5e70c7bfb143a529f7ae6e9d5290d1bf4df314b6ca4e65384ebb20198f038dd
|
data/.zed/tasks.json
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
mensa (0.
|
|
4
|
+
mensa (0.4.0)
|
|
5
5
|
csv
|
|
6
6
|
importmap-rails
|
|
7
7
|
pagy (>= 43)
|
|
@@ -10,7 +10,6 @@ PATH
|
|
|
10
10
|
rubyzip (>= 1.2.2)
|
|
11
11
|
stimulus-rails
|
|
12
12
|
tailwindcss-rails (~> 3.3)
|
|
13
|
-
textacular (>= 5)
|
|
14
13
|
turbo-rails
|
|
15
14
|
view_component (~> 3.11)
|
|
16
15
|
|
|
@@ -337,8 +336,6 @@ GEM
|
|
|
337
336
|
tailwindcss-ruby (3.4.19-arm64-darwin)
|
|
338
337
|
tailwindcss-ruby (3.4.19-x86_64-darwin)
|
|
339
338
|
tailwindcss-ruby (3.4.19-x86_64-linux)
|
|
340
|
-
textacular (5.7.0)
|
|
341
|
-
activerecord (>= 5.0)
|
|
342
339
|
thor (1.5.0)
|
|
343
340
|
timeout (0.6.1)
|
|
344
341
|
tsort (0.2.0)
|
data/README.md
CHANGED
|
@@ -9,21 +9,19 @@ Due to search, it only works with postgresql at the moment.
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
Features:
|
|
12
|
-
|
|
13
|
-
- [x]
|
|
14
|
-
- [x]
|
|
15
|
-
- [x]
|
|
16
|
-
- [x] filtering of multiple columns
|
|
12
|
+
- [x] Very fast
|
|
13
|
+
- [x] Row-links
|
|
14
|
+
- [x] Sorting
|
|
15
|
+
- [x] Filtering of multiple columns
|
|
17
16
|
- [X] Hide filter icon in case there are no filters
|
|
18
|
-
- [X]
|
|
19
|
-
- [X]
|
|
20
|
-
- [X]
|
|
21
|
-
- [X]
|
|
22
|
-
- [x]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- [ ] Search only works on table text columns
|
|
17
|
+
- [X] Column ordering
|
|
18
|
+
- [X] Editing of existing filters
|
|
19
|
+
- [X] View selection and exports per view
|
|
20
|
+
- [X] Multiple selection of rows and batch processing
|
|
21
|
+
- [x] Tables without headers (and without most of the above)
|
|
22
|
+
- [X] Search works on all table columns
|
|
23
|
+
- [X] Exports can be scheduled to run recurring (daily/weekly/monthly/quarterly/bi-yearly/yearly)
|
|
24
|
+
You will have to bring your own mailer, see configuration for details.
|
|
27
25
|
|
|
28
26
|
Nice to haves:
|
|
29
27
|
|
|
@@ -191,6 +189,20 @@ Exporting is built into the table's control bar. Clicking the export button open
|
|
|
191
189
|
a dialog that lists the user's previous downloads and lets them request a new
|
|
192
190
|
export (scope and CSV format).
|
|
193
191
|
|
|
192
|
+
#### Repeating exports
|
|
193
|
+
|
|
194
|
+
The user can choose to export a table on a regular basis (daily, weekly, monthly, quarterly, bi-yearly, yearly).
|
|
195
|
+
|
|
196
|
+
When the user selects a repeating export, the table will be exported automatically on the specified schedule.
|
|
197
|
+
|
|
198
|
+
For this to work you need to have a cron job which runs daily.
|
|
199
|
+
When using `sidekiq-cron` or `goodjob` the `RecurringExportsJob` needs to be scheduled to run daily.
|
|
200
|
+
|
|
201
|
+
If you're just using cron, you can add the following to your crontab:
|
|
202
|
+
```
|
|
203
|
+
0 0 * * * rails runner "Mensa::RecurringExportsJob.perform_later"
|
|
204
|
+
```
|
|
205
|
+
|
|
194
206
|
## Contributing
|
|
195
207
|
|
|
196
208
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
data-mensa-add-filter-operator-labels-value="<%= operator_labels.to_json %>">
|
|
5
5
|
<button class="mensa-table__add_filter__trigger"
|
|
6
6
|
type="button"
|
|
7
|
-
title="<%= t("mensa.add_filter.add"
|
|
7
|
+
title="<%= t("mensa.add_filter.add") %>"
|
|
8
8
|
data-action="mensa-add-filter#openAllColumns">
|
|
9
9
|
<i class="fa-solid fa-circle-plus"></i>
|
|
10
10
|
</button>
|
|
@@ -273,7 +273,7 @@ export default class ColumnCustomizerController extends ApplicationController {
|
|
|
273
273
|
if (!urlStr) return false;
|
|
274
274
|
|
|
275
275
|
try {
|
|
276
|
-
const url = new URL(urlStr);
|
|
276
|
+
const url = new URL(urlStr, window.location.origin);
|
|
277
277
|
|
|
278
278
|
const toDelete = [];
|
|
279
279
|
url.searchParams.forEach((_, key) => {
|
|
@@ -62,19 +62,31 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
&__item {
|
|
65
|
-
@apply flex shrink-0 items-center
|
|
65
|
+
@apply flex shrink-0 items-center gap-3 px-3 py-2.5;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&__delete-form {
|
|
69
|
+
@apply shrink-0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__delete {
|
|
73
|
+
@apply inline-flex h-9 w-9 items-center justify-center rounded-md text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-red-600 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-red-500/30 transition-colors cursor-pointer;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
&__item-info {
|
|
69
|
-
@apply flex flex-col min-w-0;
|
|
77
|
+
@apply flex flex-col min-w-0 flex-1;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
&__item-name {
|
|
73
81
|
@apply text-sm font-medium text-gray-900 dark:text-gray-100 truncate;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
&__item-repeat {
|
|
85
|
+
@apply text-xs font-normal text-gray-500 dark:text-gray-400;
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
&__item-meta {
|
|
77
|
-
@apply text-xs text-gray-
|
|
89
|
+
@apply text-xs font-normal text-gray-400 dark:text-gray-500;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
&__download {
|
|
@@ -109,6 +121,14 @@
|
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
&__repeat-options {
|
|
125
|
+
@apply pl-6;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&__select {
|
|
129
|
+
@apply w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-opacity-20 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100;
|
|
130
|
+
}
|
|
131
|
+
|
|
112
132
|
&__actions {
|
|
113
133
|
@apply flex justify-end gap-2 pt-1;
|
|
114
134
|
}
|
|
@@ -11,28 +11,28 @@
|
|
|
11
11
|
<% if table.current_user && table.supports_custom_views? %>
|
|
12
12
|
<div class="relative">
|
|
13
13
|
<button class="mensa-table__control_bar__button hidden" type="button" data-mensa-table-target="saveSimple" data-action="mensa-table#saveAsNewView">
|
|
14
|
-
<%= t(".save"
|
|
14
|
+
<%= t("mensa.save") %>
|
|
15
15
|
</button>
|
|
16
16
|
<button class="mensa-table__control_bar__button hidden" type="button" data-mensa-table-target="saveSplit" data-action="mensa-table#toggleSaveDropdown">
|
|
17
|
-
<%= t(".save"
|
|
17
|
+
<%= t("mensa.save") %>
|
|
18
18
|
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
19
19
|
</button>
|
|
20
20
|
<ul class="mensa-table__control_bar__save-dropdown hidden" data-mensa-table-target="saveDropdown">
|
|
21
21
|
<li>
|
|
22
22
|
<button class="mensa-table__control_bar__save-dropdown-item" type="button" data-action="mensa-table#updateCurrentViewAction">
|
|
23
|
-
<%= t(".update_view"
|
|
23
|
+
<%= t("mensa.update_view") %>
|
|
24
24
|
</button>
|
|
25
25
|
</li>
|
|
26
26
|
<li>
|
|
27
27
|
<button class="mensa-table__control_bar__save-dropdown-item" type="button" data-action="mensa-table#saveAsNewView">
|
|
28
|
-
<%= t(".save_as_new_view"
|
|
28
|
+
<%= t("mensa.save_as_new_view") %>
|
|
29
29
|
</button>
|
|
30
30
|
</li>
|
|
31
31
|
</ul>
|
|
32
32
|
</div>
|
|
33
33
|
<% else %>
|
|
34
34
|
<button class="mensa-table__control_bar__button" type="button" data-action="mensa-table#saveAsNewView">
|
|
35
|
-
<%= t(".save"
|
|
35
|
+
<%= t("mensa.save") %>
|
|
36
36
|
</button>
|
|
37
37
|
<% end %>
|
|
38
38
|
</div>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
.mensa-filter-pill {
|
|
2
|
-
@apply inline-flex items-stretch text-xs;
|
|
2
|
+
@apply inline-flex items-stretch text-xs rounded-md overflow-hidden;
|
|
3
3
|
|
|
4
4
|
&__chip {
|
|
5
|
-
@apply flex items-center gap-1 pl-2 pr-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-
|
|
5
|
+
@apply flex items-center gap-1 pl-2 pr-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-md cursor-pointer transition-all;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
&:hover &__chip,
|
|
9
|
+
&:focus-within &__chip {
|
|
10
|
+
@apply bg-gray-200 dark:bg-gray-600 rounded-r-none;
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
&__column {
|
|
@@ -18,6 +23,15 @@
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
&__remove {
|
|
21
|
-
@apply inline-flex items-center justify-center px-
|
|
26
|
+
@apply inline-flex items-center justify-center w-0 px-0 text-gray-400 bg-gray-100 dark:bg-gray-700 cursor-pointer overflow-hidden opacity-0 -translate-x-1 pointer-events-none transition-all duration-200;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&:hover &__remove,
|
|
30
|
+
&:focus-within &__remove {
|
|
31
|
+
@apply w-7 px-1.5 opacity-100 translate-x-0 pointer-events-auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__remove:hover {
|
|
35
|
+
@apply text-gray-700 bg-gray-200 dark:bg-gray-600 dark:text-gray-200;
|
|
22
36
|
}
|
|
23
37
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div class="mensa-table"
|
|
2
2
|
id="table-<%= table.table_id %>"
|
|
3
3
|
data-mensa-table-supports-views-value="<%= table.supports_views? %>"
|
|
4
|
-
data-mensa-table-table-url-value="<%=
|
|
4
|
+
data-mensa-table-table-url-value="<%= table.path(turbo_frame_id: table.table_id, user_params: params) %>"
|
|
5
5
|
data-mensa-table-save-view-url-value="<%= helpers.mensa.table_views_path(table.name) %>"
|
|
6
6
|
data-mensa-table-views-url-value="<%= helpers.mensa.table_views_path(table.name) %>"
|
|
7
7
|
data-mensa-table-exports-url-value="<%= helpers.mensa.table_exports_path(table.name) %>"
|
|
@@ -8,11 +8,11 @@ module Mensa
|
|
|
8
8
|
attr_reader :table
|
|
9
9
|
attr_reader :params
|
|
10
10
|
|
|
11
|
-
def initialize(table_name,
|
|
12
|
-
@
|
|
11
|
+
def initialize(table_name, params: {}, **options)
|
|
12
|
+
@params = params
|
|
13
|
+
@table = Mensa.for_name(table_name, {params: params})
|
|
13
14
|
@table.original_view_context = options[:original_view_context]
|
|
14
15
|
@table.component = self
|
|
15
|
-
@params = options[:params] || {}
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -43,7 +43,10 @@ export default class TableComponentController extends ApplicationController {
|
|
|
43
43
|
|
|
44
44
|
// Close save dropdown when clicking outside
|
|
45
45
|
this._saveDropdownOutsideHandler = (e) => {
|
|
46
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
this.hasSaveDropdownTarget &&
|
|
48
|
+
!this.saveDropdownTarget.classList.contains("hidden")
|
|
49
|
+
) {
|
|
47
50
|
const saveArea = this.saveDropdownTarget.closest(".relative");
|
|
48
51
|
if (saveArea && !saveArea.contains(e.target)) {
|
|
49
52
|
this.saveDropdownTarget.classList.add("hidden");
|
|
@@ -98,12 +101,15 @@ export default class TableComponentController extends ApplicationController {
|
|
|
98
101
|
const outlet = this.mensaFilterPillListOutlet;
|
|
99
102
|
const state = {
|
|
100
103
|
filters: outlet.loadFilters(),
|
|
101
|
-
query:
|
|
102
|
-
view:
|
|
103
|
-
order:
|
|
104
|
-
page:
|
|
104
|
+
query: outlet.loadQuery(),
|
|
105
|
+
view: outlet.loadView(),
|
|
106
|
+
order: outlet.loadOrder(),
|
|
107
|
+
page: outlet.loadPage(),
|
|
105
108
|
};
|
|
106
|
-
this.turboFrameTarget.setAttribute(
|
|
109
|
+
this.turboFrameTarget.setAttribute(
|
|
110
|
+
"src",
|
|
111
|
+
outlet.buildUrl(state).toString(),
|
|
112
|
+
);
|
|
107
113
|
} else if (this.hasTableUrlValue) {
|
|
108
114
|
this.turboFrameTarget.setAttribute("src", this.tableUrlValue);
|
|
109
115
|
}
|
|
@@ -135,7 +141,8 @@ export default class TableComponentController extends ApplicationController {
|
|
|
135
141
|
cancelFiltersAndSearch(event) {
|
|
136
142
|
if (event) event.preventDefault();
|
|
137
143
|
this.hideSaveReset();
|
|
138
|
-
if (this.hasSaveDropdownTarget)
|
|
144
|
+
if (this.hasSaveDropdownTarget)
|
|
145
|
+
this.saveDropdownTarget.classList.add("hidden");
|
|
139
146
|
|
|
140
147
|
if (this.hasMensaFilterPillListOutlet) {
|
|
141
148
|
this.mensaFilterPillListOutlet.clearFiltersAndSearch();
|
|
@@ -164,14 +171,16 @@ export default class TableComponentController extends ApplicationController {
|
|
|
164
171
|
// "Save as new view" — always opens the name dialog
|
|
165
172
|
saveAsNewView(event) {
|
|
166
173
|
if (event) event.preventDefault();
|
|
167
|
-
if (this.hasSaveDropdownTarget)
|
|
174
|
+
if (this.hasSaveDropdownTarget)
|
|
175
|
+
this.saveDropdownTarget.classList.add("hidden");
|
|
168
176
|
this._openSaveDialog();
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
// "Update view" — updates the currently selected user-owned view in place
|
|
172
180
|
async updateCurrentViewAction(event) {
|
|
173
181
|
if (event) event.preventDefault();
|
|
174
|
-
if (this.hasSaveDropdownTarget)
|
|
182
|
+
if (this.hasSaveDropdownTarget)
|
|
183
|
+
this.saveDropdownTarget.classList.add("hidden");
|
|
175
184
|
|
|
176
185
|
const viewId = this._selectedUserViewId();
|
|
177
186
|
if (!viewId) {
|
|
@@ -204,7 +213,9 @@ export default class TableComponentController extends ApplicationController {
|
|
|
204
213
|
order: state.order,
|
|
205
214
|
column_order: state.column_order,
|
|
206
215
|
hidden_columns: state.hidden_columns,
|
|
207
|
-
turbo_frame_id: this.hasTurboFrameTarget
|
|
216
|
+
turbo_frame_id: this.hasTurboFrameTarget
|
|
217
|
+
? this.turboFrameTarget.id
|
|
218
|
+
: null,
|
|
208
219
|
}),
|
|
209
220
|
contentType: "application/json",
|
|
210
221
|
responseKind: "turbo-stream",
|
|
@@ -239,9 +250,12 @@ export default class TableComponentController extends ApplicationController {
|
|
|
239
250
|
async confirmSaveView(event) {
|
|
240
251
|
event.preventDefault();
|
|
241
252
|
|
|
242
|
-
const name = this.hasSaveViewNameTarget
|
|
253
|
+
const name = this.hasSaveViewNameTarget
|
|
254
|
+
? this.saveViewNameTarget.value.trim()
|
|
255
|
+
: "";
|
|
243
256
|
if (!name) {
|
|
244
|
-
if (this.hasSaveViewNameTarget)
|
|
257
|
+
if (this.hasSaveViewNameTarget)
|
|
258
|
+
this.saveViewNameTarget.reportValidity();
|
|
245
259
|
return;
|
|
246
260
|
}
|
|
247
261
|
|
|
@@ -260,7 +274,9 @@ export default class TableComponentController extends ApplicationController {
|
|
|
260
274
|
order: state.order,
|
|
261
275
|
column_order: state.column_order,
|
|
262
276
|
hidden_columns: state.hidden_columns,
|
|
263
|
-
turbo_frame_id: this.hasTurboFrameTarget
|
|
277
|
+
turbo_frame_id: this.hasTurboFrameTarget
|
|
278
|
+
? this.turboFrameTarget.id
|
|
279
|
+
: null,
|
|
264
280
|
}),
|
|
265
281
|
contentType: "application/json",
|
|
266
282
|
responseKind: "turbo-stream",
|
|
@@ -307,7 +323,10 @@ export default class TableComponentController extends ApplicationController {
|
|
|
307
323
|
const hasViewFilters = this._hasViewFilterPills();
|
|
308
324
|
if (hasViewFilters) {
|
|
309
325
|
const visible = this._loadViewFiltersVisible();
|
|
310
|
-
this.element.classList.toggle(
|
|
326
|
+
this.element.classList.toggle(
|
|
327
|
+
"mensa-table--view-filters-hidden",
|
|
328
|
+
!visible,
|
|
329
|
+
);
|
|
311
330
|
} else {
|
|
312
331
|
this.element.classList.remove("mensa-table--view-filters-hidden");
|
|
313
332
|
}
|
|
@@ -328,13 +347,18 @@ export default class TableComponentController extends ApplicationController {
|
|
|
328
347
|
|
|
329
348
|
toggleViewFilters(event) {
|
|
330
349
|
if (event) event.preventDefault();
|
|
331
|
-
const nowHidden = this.element.classList.toggle(
|
|
350
|
+
const nowHidden = this.element.classList.toggle(
|
|
351
|
+
"mensa-table--view-filters-hidden",
|
|
352
|
+
);
|
|
332
353
|
this._saveViewFiltersVisible(!nowHidden);
|
|
333
354
|
this._updateEyeButton();
|
|
334
355
|
}
|
|
335
356
|
|
|
336
357
|
_hasViewFilterPills() {
|
|
337
|
-
return
|
|
358
|
+
return (
|
|
359
|
+
this.element.querySelectorAll('[data-view-filter="true"]').length >
|
|
360
|
+
0
|
|
361
|
+
);
|
|
338
362
|
}
|
|
339
363
|
|
|
340
364
|
// Only updates button visibility and icon — never touches the hidden class.
|
|
@@ -348,7 +372,9 @@ export default class TableComponentController extends ApplicationController {
|
|
|
348
372
|
this.eyeButtonTarget.classList.toggle("hidden", !hasViewFilters);
|
|
349
373
|
|
|
350
374
|
if (hasViewFilters) {
|
|
351
|
-
const hidden = this.element.classList.contains(
|
|
375
|
+
const hidden = this.element.classList.contains(
|
|
376
|
+
"mensa-table--view-filters-hidden",
|
|
377
|
+
);
|
|
352
378
|
// Replace innerHTML so FontAwesome's MutationObserver re-processes the new <i>
|
|
353
379
|
this.eyeButtonTarget.innerHTML = hidden
|
|
354
380
|
? '<i class="fa-solid fa-eye"></i>'
|
|
@@ -366,13 +392,18 @@ export default class TableComponentController extends ApplicationController {
|
|
|
366
392
|
if (this.hasMensaFilterPillListOutlet) {
|
|
367
393
|
return this.mensaFilterPillListOutlet.tableNameValue;
|
|
368
394
|
}
|
|
369
|
-
const el = this.element.querySelector(
|
|
395
|
+
const el = this.element.querySelector(
|
|
396
|
+
"[data-mensa-filter-pill-list-table-name-value]",
|
|
397
|
+
);
|
|
370
398
|
return el?.dataset?.mensaFilterPillListTableNameValue || "";
|
|
371
399
|
}
|
|
372
400
|
|
|
373
401
|
_loadViewFiltersVisible() {
|
|
374
402
|
try {
|
|
375
|
-
return
|
|
403
|
+
return (
|
|
404
|
+
window.localStorage.getItem(this._viewFiltersStorageKey()) ===
|
|
405
|
+
"true"
|
|
406
|
+
);
|
|
376
407
|
} catch (e) {
|
|
377
408
|
return false;
|
|
378
409
|
}
|
|
@@ -381,7 +412,10 @@ export default class TableComponentController extends ApplicationController {
|
|
|
381
412
|
_saveViewFiltersVisible(visible) {
|
|
382
413
|
try {
|
|
383
414
|
if (visible) {
|
|
384
|
-
window.localStorage.setItem(
|
|
415
|
+
window.localStorage.setItem(
|
|
416
|
+
this._viewFiltersStorageKey(),
|
|
417
|
+
"true",
|
|
418
|
+
);
|
|
385
419
|
} else {
|
|
386
420
|
window.localStorage.removeItem(this._viewFiltersStorageKey());
|
|
387
421
|
}
|
|
@@ -432,13 +466,36 @@ export default class TableComponentController extends ApplicationController {
|
|
|
432
466
|
}
|
|
433
467
|
}
|
|
434
468
|
|
|
469
|
+
toggleExportRepeat() {
|
|
470
|
+
const dialog = this.exportDialogTarget;
|
|
471
|
+
const options = dialog.querySelector(
|
|
472
|
+
"[data-mensa-table-repeat-options]",
|
|
473
|
+
);
|
|
474
|
+
if (!options) return;
|
|
475
|
+
|
|
476
|
+
const mode = dialog.querySelector(
|
|
477
|
+
'input[name="repeat_mode"]:checked',
|
|
478
|
+
)?.value;
|
|
479
|
+
options.hidden = mode !== "repeating";
|
|
480
|
+
}
|
|
481
|
+
|
|
435
482
|
confirmExport(event) {
|
|
436
483
|
event.preventDefault();
|
|
437
484
|
if (!this.hasExportsUrlValue) return;
|
|
438
485
|
|
|
439
486
|
const dialog = this.exportDialogTarget;
|
|
440
|
-
const scope =
|
|
441
|
-
|
|
487
|
+
const scope =
|
|
488
|
+
dialog.querySelector('input[name="scope"]:checked')?.value || "all";
|
|
489
|
+
const exportFormat =
|
|
490
|
+
dialog.querySelector('input[name="export_format"]:checked')
|
|
491
|
+
?.value || "csv_excel";
|
|
492
|
+
const repeatMode = dialog.querySelector(
|
|
493
|
+
'input[name="repeat_mode"]:checked',
|
|
494
|
+
)?.value;
|
|
495
|
+
const repeat =
|
|
496
|
+
repeatMode === "repeating"
|
|
497
|
+
? dialog.querySelector('select[name="repeat"]')?.value || ""
|
|
498
|
+
: "";
|
|
442
499
|
|
|
443
500
|
const state = this.currentViewState();
|
|
444
501
|
const nav = this.hasMensaFilterPillListOutlet
|
|
@@ -452,6 +509,7 @@ export default class TableComponentController extends ApplicationController {
|
|
|
452
509
|
body: JSON.stringify({
|
|
453
510
|
scope,
|
|
454
511
|
export_format: exportFormat,
|
|
512
|
+
repeat,
|
|
455
513
|
table_view_id: view,
|
|
456
514
|
page: nav.page,
|
|
457
515
|
query: state.query || nav.query,
|
|
@@ -464,11 +522,17 @@ export default class TableComponentController extends ApplicationController {
|
|
|
464
522
|
}
|
|
465
523
|
|
|
466
524
|
get ourUrl() {
|
|
467
|
-
if (
|
|
468
|
-
|
|
525
|
+
if (
|
|
526
|
+
this.hasTurboFrameTarget &&
|
|
527
|
+
this.turboFrameTarget.getAttribute("src")
|
|
528
|
+
) {
|
|
529
|
+
return new URL(
|
|
530
|
+
this.turboFrameTarget.getAttribute("src"),
|
|
531
|
+
window.location.origin,
|
|
532
|
+
);
|
|
469
533
|
}
|
|
470
534
|
if (this.hasTableUrlValue && this.tableUrlValue) {
|
|
471
|
-
return new URL(this.tableUrlValue);
|
|
535
|
+
return new URL(this.tableUrlValue, window.location.origin);
|
|
472
536
|
}
|
|
473
537
|
return new URL(window.location.href);
|
|
474
538
|
}
|
|
@@ -494,8 +558,12 @@ export default class TableComponentController extends ApplicationController {
|
|
|
494
558
|
_updateSaveButtonMode() {
|
|
495
559
|
if (!this.hasSaveSimpleTarget && !this.hasSaveSplitTarget) return;
|
|
496
560
|
const isUserView = !!this._selectedUserViewId();
|
|
497
|
-
this.saveSimpleTargets.forEach((t) =>
|
|
498
|
-
|
|
561
|
+
this.saveSimpleTargets.forEach((t) =>
|
|
562
|
+
t.classList.toggle("hidden", isUserView),
|
|
563
|
+
);
|
|
564
|
+
this.saveSplitTargets.forEach((t) =>
|
|
565
|
+
t.classList.toggle("hidden", !isUserView),
|
|
566
|
+
);
|
|
499
567
|
}
|
|
500
568
|
|
|
501
569
|
_selectedUserViewId() {
|
|
@@ -517,7 +585,8 @@ export default class TableComponentController extends ApplicationController {
|
|
|
517
585
|
_openSaveDialog() {
|
|
518
586
|
if (!this.hasSaveViewDialogTarget) return;
|
|
519
587
|
if (this.hasSaveViewNameTarget) this.saveViewNameTarget.value = "";
|
|
520
|
-
if (this.hasSaveViewDescriptionTarget)
|
|
588
|
+
if (this.hasSaveViewDescriptionTarget)
|
|
589
|
+
this.saveViewDescriptionTarget.value = "";
|
|
521
590
|
if (typeof this.saveViewDialogTarget.showModal === "function") {
|
|
522
591
|
this.saveViewDialogTarget.showModal();
|
|
523
592
|
} else {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</th>
|
|
28
28
|
<% end %>
|
|
29
29
|
<% if table.actions? && Mensa.config.row_actions_position == :front %>
|
|
30
|
-
<th
|
|
30
|
+
<th><%= t("mensa.actions") %></th>
|
|
31
31
|
<% end %>
|
|
32
32
|
<%= render(Mensa::Header::Component.with_collection(table.display_columns, table: table)) %>
|
|
33
33
|
<% if table.actions? && Mensa.config.row_actions_position == :back %>
|
|
@@ -23,6 +23,7 @@ module Mensa
|
|
|
23
23
|
user: current_mensa_user,
|
|
24
24
|
format: params[:export_format].to_s.presence_in(Mensa::Export::FORMATS) || "csv_excel",
|
|
25
25
|
scope: params[:scope].to_s.presence_in(Mensa::Export::SCOPES) || "all",
|
|
26
|
+
repeat: params[:repeat].to_s.presence_in(Mensa::Export::REPEATS) || "",
|
|
26
27
|
config: params.permit(:query, :page, order: {}, filters: {}).to_h,
|
|
27
28
|
status: "pending"
|
|
28
29
|
)
|
|
@@ -42,6 +43,20 @@ module Mensa
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
# Deletes an export from the current user's download list. The attached
|
|
47
|
+
# asset is purged automatically when the record is destroyed.
|
|
48
|
+
def destroy
|
|
49
|
+
export = exports.find(params[:id])
|
|
50
|
+
export.destroy
|
|
51
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
52
|
+
|
|
53
|
+
respond_to do |format|
|
|
54
|
+
format.turbo_stream { render turbo_stream: [list_stream, badge_stream] }
|
|
55
|
+
format.json { head :no_content }
|
|
56
|
+
format.html { redirect_back fallback_location: mensa.table_exports_path(params[:table_id]) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
45
60
|
# Streams the generated CSV and then removes the export, purging the
|
|
46
61
|
# attached asset. Downloads are single-use: routing them through the
|
|
47
62
|
# controller (instead of a direct Active Storage link) gives us a hook to
|
|
@@ -56,12 +71,17 @@ module Mensa
|
|
|
56
71
|
|
|
57
72
|
send_data data, filename: filename, type: content_type, disposition: "attachment"
|
|
58
73
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
74
|
+
# One-off exports are single-use and are deleted after download.
|
|
75
|
+
# Repeating exports stay in place and can be removed explicitly via the
|
|
76
|
+
# trash action.
|
|
62
77
|
begin
|
|
63
|
-
export.
|
|
64
|
-
|
|
78
|
+
if export.repeating?
|
|
79
|
+
export.asset.purge_later
|
|
80
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
81
|
+
else
|
|
82
|
+
export.destroy
|
|
83
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
84
|
+
end
|
|
65
85
|
rescue => e
|
|
66
86
|
Mensa.config.logger&.warn("Mensa::Export cleanup failed for #{export.id}: #{e.class}: #{e.message}")
|
|
67
87
|
end
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
module Mensa
|
|
2
2
|
class TablesController < ApplicationController
|
|
3
3
|
def show
|
|
4
|
-
|
|
4
|
+
config = params.permit(:format, :query, :id, :page, :table_view_id, :turbo_frame_id, order: {}, column_order: [], hidden_columns: [], params: {}).to_h
|
|
5
|
+
config[:filters] = params[:filters]&.to_unsafe_h || {}
|
|
6
|
+
config[:params] = params[:params]&.to_unsafe_h || {}
|
|
5
7
|
|
|
6
|
-
config = {}
|
|
7
8
|
if params[:table_view_id]
|
|
9
|
+
view_lookup_table = Mensa.for_name(params[:id], config)
|
|
8
10
|
@view = Mensa::TableView.find_by(table_name: params[:id], id: params[:table_view_id])
|
|
9
|
-
@view ||=
|
|
10
|
-
config = @view&.config&.deep_transform_keys(&:to_sym)
|
|
11
|
+
@view ||= view_lookup_table.system_views.find { |v| v.id == params[:table_view_id].to_sym }
|
|
12
|
+
config = (@view&.config&.deep_transform_keys(&:to_sym) || {}).merge(config)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
config = config.merge(params.permit!.to_h)
|
|
14
|
-
config = config.merge(params.permit(:format, :query, :id, :page, :table_view_id, :turbo_frame_id, order: {}, column_order: [], hidden_columns: []).to_h)
|
|
15
|
-
config[:filters] = params[:filters]&.to_unsafe_h || config[:filters] || {}
|
|
16
|
-
|
|
17
15
|
@table = Mensa.for_name(params[:id], config)
|
|
18
16
|
@table.request = request
|
|
19
17
|
@table.table_view = @view
|
|
20
18
|
@table.original_view_context = helpers
|
|
21
19
|
|
|
22
20
|
respond_to do |format|
|
|
23
|
-
format.turbo_stream # Used for
|
|
21
|
+
format.turbo_stream # Used for filtering
|
|
24
22
|
format.html
|
|
25
23
|
end
|
|
26
24
|
end
|