mensa 0.6.11 → 0.6.12
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/.shipkit.yml +2 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +8 -2
- data/README.md +32 -0
- data/app/components/mensa/column_customizer/component.html.erb +1 -0
- data/app/components/mensa/column_customizer/component_controller.js +8 -3
- data/app/components/mensa/control_bar/component.html.erb +2 -2
- data/app/components/mensa/filter_pill_list/component.html.erb +1 -0
- data/app/components/mensa/filter_pill_list/component_controller.js +15 -9
- data/app/components/mensa/table/component.html.erb +3 -3
- data/app/components/mensa/table/component_controller.js +9 -5
- data/app/components/mensa/view/component.html.erb +1 -1
- data/app/components/mensa/views/component.html.erb +1 -1
- data/app/components/mensa/views/component_controller.js +2 -2
- data/app/controllers/mensa/tables/batch_actions_controller.rb +1 -1
- data/app/controllers/mensa/tables/exports_controller.rb +31 -12
- data/app/controllers/mensa/tables/filters_controller.rb +2 -2
- data/app/controllers/mensa/tables/views_controller.rb +8 -3
- data/app/javascript/mensa/controllers/application_controller.js +8 -0
- data/app/jobs/mensa/export_job.rb +5 -5
- data/app/models/mensa/export.rb +42 -25
- data/app/tables/mensa/base.rb +21 -1
- data/app/tables/mensa/column.rb +1 -1
- data/app/tables/mensa/scope.rb +21 -2
- data/app/tables/mensa/table_params.rb +44 -0
- data/app/views/mensa/exports/_badge.html.erb +3 -2
- data/app/views/mensa/exports/_dialog.html.erb +2 -2
- data/app/views/mensa/exports/_list.html.erb +1 -1
- data/lib/mensa/configuration.rb +24 -1
- data/lib/mensa/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72a11ca0b316e616b3705f54523e4fe6f6ae408f7e767f9a130db02d1afc088f
|
|
4
|
+
data.tar.gz: ae13275d6ead1adece9e27ea3f82143c3dc88bb1aa7f443ad8aaf8dca251eff1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a5f0d0e105762e8542af697c8bc1af5816ac2d628b9ba1e22bab262828ddf47ffbfe5e8f8a9d03ca78990caa8cf6b313748199eaa2f2eae0658cf6a2ad0c8f0
|
|
7
|
+
data.tar.gz: 8ba1ab12c0c48b03f4b425cc933614bbc5880fed150fbf70f2e32e39dd90307eca0a530c2cf43e81450708ba094fcab9dfe6dd81258b2cb77a72818f4f48fed9
|
data/.shipkit.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.12
|
|
4
|
+
|
|
5
|
+
- Tables built with `params` (`table :name, params: {...}`) now work in filters,
|
|
6
|
+
views, batch actions and exports. The params are sent to these endpoints as
|
|
7
|
+
`params[...]` query parameters and stored on `Mensa::Export#config`, so
|
|
8
|
+
`Mensa::ExportJob` (and recurring exports) rebuild the table with them.
|
|
9
|
+
Host apps can remove their FiltersController, ExportsController and
|
|
10
|
+
Table::Component patches for this.
|
|
11
|
+
- The export list, badge and Turbo stream are scoped per table params, so
|
|
12
|
+
exports of e.g. two different queries on the same table are no longer mixed.
|
|
13
|
+
- Browser state (filters, search, order, column order, hidden columns) is
|
|
14
|
+
stored per table params. Unknown column names in `column_order`,
|
|
15
|
+
`hidden_columns`, `order` and `filters` are ignored instead of raising.
|
|
16
|
+
- The export completed callback is now `export_completed`, as documented in the
|
|
17
|
+
generated initializer. The old `export_complete` name still works. Unknown
|
|
18
|
+
callback names are logged as a warning.
|
|
19
|
+
- `selected_scope` only selects `id` when the relation has an `id` column.
|
|
20
|
+
|
|
3
21
|
## 0.2.4
|
|
4
22
|
|
|
5
23
|
- Renames the data column on table_views to config
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
mensa (0.6.
|
|
4
|
+
mensa (0.6.11)
|
|
5
5
|
csv
|
|
6
6
|
importmap-rails
|
|
7
7
|
pagy (>= 43)
|
|
@@ -366,6 +366,11 @@ GEM
|
|
|
366
366
|
yaml (0.4.0)
|
|
367
367
|
zeitwerk (2.8.2)
|
|
368
368
|
|
|
369
|
+
GEM
|
|
370
|
+
remote: https://rubygems.pkg.github.com/entdec/
|
|
371
|
+
specs:
|
|
372
|
+
shipkit (0.1.2)
|
|
373
|
+
|
|
369
374
|
PLATFORMS
|
|
370
375
|
aarch64-linux-gnu
|
|
371
376
|
aarch64-linux-musl
|
|
@@ -391,8 +396,9 @@ DEPENDENCIES
|
|
|
391
396
|
ruby-lsp
|
|
392
397
|
ruby-lsp-rails
|
|
393
398
|
selenium-webdriver (~> 4.17)
|
|
399
|
+
shipkit!
|
|
394
400
|
sprockets-rails
|
|
395
401
|
standard
|
|
396
402
|
|
|
397
403
|
BUNDLED WITH
|
|
398
|
-
2.6.
|
|
404
|
+
2.6.9
|
data/README.md
CHANGED
|
@@ -186,12 +186,44 @@ Next you can run the generator to generate a table:
|
|
|
186
186
|
$ rails g mensa:table:generate <model_name>
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### Table params
|
|
190
|
+
|
|
191
|
+
A table can be built with extra params, for example when its scope or columns
|
|
192
|
+
depend on a record:
|
|
193
|
+
|
|
194
|
+
```erb
|
|
195
|
+
<%= table :query_results, params: {query_id: @query.id} %>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
They are available as `params` inside the table (e.g. `params[:query_id]`) and
|
|
199
|
+
are sent along whenever Mensa rebuilds the table: paging, filters, views, batch
|
|
200
|
+
actions and exports (including recurring exports, which store them on the
|
|
201
|
+
`Mensa::Export`). Exports and the state the browser remembers (filters, order,
|
|
202
|
+
columns) are kept per table *and* params.
|
|
203
|
+
|
|
189
204
|
### Exports
|
|
190
205
|
|
|
191
206
|
Exporting is built into the table's control bar. Clicking the export button opens
|
|
192
207
|
a dialog that lists the user's previous downloads and lets them request a new
|
|
193
208
|
export (scope and CSV format).
|
|
194
209
|
|
|
210
|
+
#### Export callbacks
|
|
211
|
+
|
|
212
|
+
Configure callbacks in your Mensa initializer to act on exports, for example to
|
|
213
|
+
email the user once their download is ready:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Mensa.setup do |config|
|
|
217
|
+
config.callbacks = {
|
|
218
|
+
export_started: ->(export) {},
|
|
219
|
+
export_completed: ->(export) { ExportMailer.with(export: export).ready.deliver_later }
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`export_completed` was called `export_complete` before 0.6.12; that name still
|
|
225
|
+
works. Unknown callback names are logged as a warning.
|
|
226
|
+
|
|
195
227
|
#### Repeating exports
|
|
196
228
|
|
|
197
229
|
The user can choose to export a table on a regular basis (daily, weekly, monthly, quarterly, bi-yearly, yearly).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<div class="relative"
|
|
2
2
|
data-controller="mensa-column-customizer"
|
|
3
3
|
data-mensa-column-customizer-table-name-value="<%= table.name %>"
|
|
4
|
+
data-mensa-column-customizer-storage-key-value="<%= table.storage_key %>"
|
|
4
5
|
data-mensa-column-customizer-turbo-frame-id-value="<%= table.table_id %>"
|
|
5
6
|
data-mensa-column-customizer-mensa-table-outlet="#table-<%= table.table_id %>">
|
|
6
7
|
<button class="mensa-table__control_bar__button" type="button" data-action="click->mensa-column-customizer#toggle">
|
|
@@ -3,7 +3,7 @@ import ApplicationController from "mensa/controllers/application_controller";
|
|
|
3
3
|
export default class ColumnCustomizerController extends ApplicationController {
|
|
4
4
|
static outlets = ["mensa-table"];
|
|
5
5
|
static targets = ["popover", "columnRow"];
|
|
6
|
-
static values = { turboFrameId: String, tableName: String };
|
|
6
|
+
static values = { turboFrameId: String, tableName: String, storageKey: String };
|
|
7
7
|
|
|
8
8
|
connect() {
|
|
9
9
|
super.connect();
|
|
@@ -340,12 +340,17 @@ export default class ColumnCustomizerController extends ApplicationController {
|
|
|
340
340
|
if (view) url.searchParams.set("table_view_id", view);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
// Tables built with different params keep separate browser state.
|
|
344
|
+
get storageName() {
|
|
345
|
+
return this.storageKeyValue || this.tableNameValue;
|
|
346
|
+
}
|
|
347
|
+
|
|
343
348
|
get _columnOrderKey() {
|
|
344
|
-
return `mensa:column_order:${this.
|
|
349
|
+
return `mensa:column_order:${this.storageName}`;
|
|
345
350
|
}
|
|
346
351
|
|
|
347
352
|
get _hiddenColumnsKey() {
|
|
348
|
-
return `mensa:hidden_columns:${this.
|
|
353
|
+
return `mensa:hidden_columns:${this.storageName}`;
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
_writeStorage(key, value) {
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
<% if table.exportable? %>
|
|
46
46
|
<button class="mensa-table__control_bar__button relative" type="button" data-action="mensa-table#export">
|
|
47
47
|
<i data-mensa-table-target="exportIcon" class="<%= Mensa.config.icons[:control_bar_export] %>"></i>
|
|
48
|
-
<%= helpers.render partial: "mensa/exports/badge", locals: {table_name: table.name, user: table.current_user} %>
|
|
48
|
+
<%= helpers.render partial: "mensa/exports/badge", locals: {table_name: table.name, user: table.current_user, table_params: table.params} %>
|
|
49
49
|
</button>
|
|
50
50
|
<% end %>
|
|
51
51
|
</div>
|
|
52
52
|
|
|
53
53
|
<% if table.exportable? %>
|
|
54
|
-
<%= helpers.turbo_stream_from Mensa::Export.stream_name(table.name, table.current_user) %>
|
|
54
|
+
<%= helpers.turbo_stream_from Mensa::Export.stream_name(table.name, table.current_user, params: table.params) %>
|
|
55
55
|
<%= helpers.render partial: "mensa/exports/dialog", locals: {table: table} %>
|
|
56
56
|
<% end %>
|
|
57
57
|
</div>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
data-controller="mensa-filter-pill-list"
|
|
5
5
|
data-mensa-table-target="filterList"
|
|
6
6
|
data-mensa-filter-pill-list-table-name-value="<%= table.name %>"
|
|
7
|
+
data-mensa-filter-pill-list-storage-key-value="<%= table.storage_key %>"
|
|
7
8
|
data-mensa-filter-pill-list-mensa-table-outlet=".mensa-table#table-<%= table.table_id %>"
|
|
8
9
|
data-mensa-filter-pill-list-mensa-filter-pill-outlet=".mensa-filter-pill"
|
|
9
10
|
data-mensa-filter-pill-list-mensa-add-filter-outlet="[data-controller=mensa-add-filter]">
|
|
@@ -8,6 +8,7 @@ export default class FilterPillListComponentController extends ApplicationContro
|
|
|
8
8
|
static values = {
|
|
9
9
|
supportsViews: Boolean,
|
|
10
10
|
tableName: String,
|
|
11
|
+
storageKey: String,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
connect() {
|
|
@@ -567,8 +568,8 @@ export default class FilterPillListComponentController extends ApplicationContro
|
|
|
567
568
|
this.writeStorage(this.filtersStorageKey, null);
|
|
568
569
|
this.writeStorage(this.searchStorageKey, null);
|
|
569
570
|
this.writeStorage(this.orderStorageKey, null);
|
|
570
|
-
this.writeStorage(`mensa:column_order:${this.
|
|
571
|
-
this.writeStorage(`mensa:hidden_columns:${this.
|
|
571
|
+
this.writeStorage(`mensa:column_order:${this.storageName}`, null);
|
|
572
|
+
this.writeStorage(`mensa:hidden_columns:${this.storageName}`, null);
|
|
572
573
|
}
|
|
573
574
|
|
|
574
575
|
// Keep these for backward-compatibility with other controllers that look up
|
|
@@ -698,7 +699,7 @@ export default class FilterPillListComponentController extends ApplicationContro
|
|
|
698
699
|
return (
|
|
699
700
|
JSON.parse(
|
|
700
701
|
this.readStorage(
|
|
701
|
-
`mensa:column_order:${this.
|
|
702
|
+
`mensa:column_order:${this.storageName}`,
|
|
702
703
|
),
|
|
703
704
|
) || []
|
|
704
705
|
);
|
|
@@ -712,7 +713,7 @@ export default class FilterPillListComponentController extends ApplicationContro
|
|
|
712
713
|
return (
|
|
713
714
|
JSON.parse(
|
|
714
715
|
this.readStorage(
|
|
715
|
-
`mensa:hidden_columns:${this.
|
|
716
|
+
`mensa:hidden_columns:${this.storageName}`,
|
|
716
717
|
),
|
|
717
718
|
) || []
|
|
718
719
|
);
|
|
@@ -721,20 +722,25 @@ export default class FilterPillListComponentController extends ApplicationContro
|
|
|
721
722
|
}
|
|
722
723
|
}
|
|
723
724
|
|
|
725
|
+
// Tables built with different params keep separate browser state.
|
|
726
|
+
get storageName() {
|
|
727
|
+
return this.storageKeyValue || this.tableNameValue;
|
|
728
|
+
}
|
|
729
|
+
|
|
724
730
|
get filtersStorageKey() {
|
|
725
|
-
return `mensa:filters:${this.
|
|
731
|
+
return `mensa:filters:${this.storageName}`;
|
|
726
732
|
}
|
|
727
733
|
get searchStorageKey() {
|
|
728
|
-
return `mensa:search:${this.
|
|
734
|
+
return `mensa:search:${this.storageName}`;
|
|
729
735
|
}
|
|
730
736
|
get viewStorageKey() {
|
|
731
|
-
return `mensa:view:${this.
|
|
737
|
+
return `mensa:view:${this.storageName}`;
|
|
732
738
|
}
|
|
733
739
|
get pageStorageKey() {
|
|
734
|
-
return `mensa:page:${this.
|
|
740
|
+
return `mensa:page:${this.storageName}`;
|
|
735
741
|
}
|
|
736
742
|
get orderStorageKey() {
|
|
737
|
-
return `mensa:order:${this.
|
|
743
|
+
return `mensa:order:${this.storageName}`;
|
|
738
744
|
}
|
|
739
745
|
|
|
740
746
|
get ourUrl() {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
id="table-<%= table.table_id %>"
|
|
3
3
|
data-mensa-table-supports-views-value="<%= table.views? %>"
|
|
4
4
|
data-mensa-table-table-url-value="<%= table.path(turbo_frame_id: table.table_id, user_params: params) %>"
|
|
5
|
-
data-mensa-table-save-view-url-value="<%= helpers.mensa.table_views_path(table.name) %>"
|
|
6
|
-
data-mensa-table-views-url-value="<%= helpers.mensa.table_views_path(table.name) %>"
|
|
7
|
-
data-mensa-table-exports-url-value="<%= helpers.mensa.table_exports_path(table.name) %>"
|
|
5
|
+
data-mensa-table-save-view-url-value="<%= table.path_with_params(helpers.mensa.table_views_path(table.name)) %>"
|
|
6
|
+
data-mensa-table-views-url-value="<%= table.path_with_params(helpers.mensa.table_views_path(table.name)) %>"
|
|
7
|
+
data-mensa-table-exports-url-value="<%= table.path_with_params(helpers.mensa.table_exports_path(table.name)) %>"
|
|
8
8
|
data-controller="mensa-table"
|
|
9
9
|
data-action="click@document->mensa-table#saveDropdownOutsideHandler"
|
|
10
10
|
data-mensa-table-mensa-filter-pill-outlet="[data-controller='mensa-filter-pill']"
|
|
@@ -255,7 +255,7 @@ export default class TableComponentController extends ApplicationController {
|
|
|
255
255
|
const state = this.currentViewState();
|
|
256
256
|
this.hideSaveReset();
|
|
257
257
|
|
|
258
|
-
const response = await patch(
|
|
258
|
+
const response = await patch(this.memberUrl(this.saveViewUrlValue, viewId), {
|
|
259
259
|
body: JSON.stringify({
|
|
260
260
|
query: state.query,
|
|
261
261
|
filters: state.filters,
|
|
@@ -439,17 +439,21 @@ export default class TableComponentController extends ApplicationController {
|
|
|
439
439
|
// --- View filter visibility persistence ---
|
|
440
440
|
|
|
441
441
|
_viewFiltersStorageKey() {
|
|
442
|
-
return `mensa:view-filters-visible:${this.
|
|
442
|
+
return `mensa:view-filters-visible:${this._storageName()}`;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
_storageName() {
|
|
446
446
|
if (this.hasMensaFilterPillListOutlet) {
|
|
447
|
-
return this.mensaFilterPillListOutlet.
|
|
447
|
+
return this.mensaFilterPillListOutlet.storageName;
|
|
448
448
|
}
|
|
449
449
|
const el = this.element.querySelector(
|
|
450
450
|
"[data-mensa-filter-pill-list-table-name-value]",
|
|
451
451
|
);
|
|
452
|
-
return
|
|
452
|
+
return (
|
|
453
|
+
el?.dataset?.mensaFilterPillListStorageKeyValue ||
|
|
454
|
+
el?.dataset?.mensaFilterPillListTableNameValue ||
|
|
455
|
+
""
|
|
456
|
+
);
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
_loadViewFiltersVisible() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="overflow-y-auto relative"
|
|
3
3
|
data-mensa-table-target="scrollContainer"
|
|
4
4
|
data-controller="mensa-selection"
|
|
5
|
-
data-mensa-selection-batch-url-value="<%= table.batch_actions? ? helpers.mensa.table_batch_actions_path(table.name) : "" %>">
|
|
5
|
+
data-mensa-selection-batch-url-value="<%= table.batch_actions? ? table.path_with_params(helpers.mensa.table_batch_actions_path(table.name)) : "" %>">
|
|
6
6
|
<% if table.batch_actions? %>
|
|
7
7
|
<div class="mensa-batch-bar hidden" data-mensa-selection-target="batchBar">
|
|
8
8
|
<div class="mensa-batch-bar__content">
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
data-controller="mensa-views"
|
|
9
9
|
data-mensa-views-mensa-filter-pill-list-outlet="#mensa-filter-pill-list-<%= table.table_id %>"
|
|
10
10
|
data-mensa-views-table-id-value="<%= table.table_id %>"
|
|
11
|
-
data-mensa-views-views-url-value="<%= helpers.mensa.table_views_path(table.name) %>">
|
|
11
|
+
data-mensa-views-views-url-value="<%= table.path_with_params(helpers.mensa.table_views_path(table.name)) %>">
|
|
12
12
|
<button class="mensa-table__views__trigger" type="button" data-action="mensa-views#toggleDropdown" data-mensa-views-target="trigger" aria-haspopup="listbox">
|
|
13
13
|
<span class="mensa-table__views__trigger-label" data-mensa-views-target="triggerLabel"><%= current_view_name %></span>
|
|
14
14
|
<i class="<%= Mensa.config.icons[:views_trigger] %> mensa-table__views__trigger-icon"></i>
|
|
@@ -120,7 +120,7 @@ export default class ViewsComponentController extends ApplicationController {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const turboFrameId = this._turboFrameId();
|
|
123
|
-
const response = await patch(
|
|
123
|
+
const response = await patch(this.memberUrl(this.viewsUrlValue, viewId), {
|
|
124
124
|
body: JSON.stringify({ name: newName, turbo_frame_id: turboFrameId }),
|
|
125
125
|
contentType: "application/json",
|
|
126
126
|
responseKind: "turbo-stream",
|
|
@@ -177,7 +177,7 @@ export default class ViewsComponentController extends ApplicationController {
|
|
|
177
177
|
if (!confirm(`Delete "${viewName}"?`)) return;
|
|
178
178
|
|
|
179
179
|
const turboFrameId = this._turboFrameId();
|
|
180
|
-
await destroy(
|
|
180
|
+
await destroy(this.memberUrl(this.viewsUrlValue, viewId), {
|
|
181
181
|
body: JSON.stringify({ turbo_frame_id: turboFrameId }),
|
|
182
182
|
contentType: "application/json",
|
|
183
183
|
responseKind: "turbo-stream",
|
|
@@ -8,7 +8,7 @@ module Mensa
|
|
|
8
8
|
batch_action_name = params[:batch_action_name]&.to_sym
|
|
9
9
|
record_ids = Array(params[:record_ids])
|
|
10
10
|
|
|
11
|
-
table = Mensa.for_name(table_name)
|
|
11
|
+
table = Mensa.for_name(table_name, params: Mensa::TableParams.from_request(params))
|
|
12
12
|
table.original_view_context = helpers
|
|
13
13
|
|
|
14
14
|
batch_action = table.batch_actions.find { |a| a.name == batch_action_name }
|
|
@@ -4,6 +4,10 @@ module Mensa
|
|
|
4
4
|
# requests. Generating the CSV happens asynchronously in Mensa::ExportJob;
|
|
5
5
|
# both the export button badge and the downloads list are refreshed via
|
|
6
6
|
# Turbo streams once the job completes.
|
|
7
|
+
#
|
|
8
|
+
# Tables built with extra params send them along as params[...] in the
|
|
9
|
+
# exports URL. They are stored on the export so Mensa::ExportJob can
|
|
10
|
+
# rebuild the table, and they scope the downloads list and badge.
|
|
7
11
|
class ExportsController < ::ApplicationController
|
|
8
12
|
# Returns the current downloads list for the table, used to refresh the
|
|
9
13
|
# contents of the export dialog when it is opened.
|
|
@@ -46,9 +50,10 @@ module Mensa
|
|
|
46
50
|
# Deletes an export from the current user's download list. The attached
|
|
47
51
|
# asset is purged automatically when the record is destroyed.
|
|
48
52
|
def destroy
|
|
49
|
-
export =
|
|
53
|
+
export = user_exports.find(params[:id])
|
|
54
|
+
@table_params = export.table_params
|
|
50
55
|
export.destroy
|
|
51
|
-
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
56
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user, params: export.table_params)
|
|
52
57
|
|
|
53
58
|
respond_to do |format|
|
|
54
59
|
format.turbo_stream { render turbo_stream: [list_stream, badge_stream] }
|
|
@@ -62,7 +67,7 @@ module Mensa
|
|
|
62
67
|
# controller (instead of a direct Active Storage link) gives us a hook to
|
|
63
68
|
# delete the Mensa::Export record and free the stored file afterwards.
|
|
64
69
|
def download
|
|
65
|
-
export =
|
|
70
|
+
export = user_exports.find(params[:id])
|
|
66
71
|
return head :not_found unless export.downloadable?
|
|
67
72
|
|
|
68
73
|
data = export.asset.download
|
|
@@ -77,11 +82,10 @@ module Mensa
|
|
|
77
82
|
begin
|
|
78
83
|
if export.repeating?
|
|
79
84
|
export.asset.purge_later
|
|
80
|
-
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
81
85
|
else
|
|
82
86
|
export.destroy
|
|
83
|
-
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
84
87
|
end
|
|
88
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user, params: export.table_params)
|
|
85
89
|
rescue => e
|
|
86
90
|
Mensa.config.logger&.warn("Mensa::Export cleanup failed for #{export.id}: #{e.class}: #{e.message}")
|
|
87
91
|
end
|
|
@@ -89,35 +93,50 @@ module Mensa
|
|
|
89
93
|
|
|
90
94
|
private
|
|
91
95
|
|
|
96
|
+
# All of the current user's exports of this table, regardless of
|
|
97
|
+
# params. Download and delete links don't carry the params.
|
|
98
|
+
def user_exports
|
|
99
|
+
Mensa::Export.where(table_name: params[:table_id]).for_user(current_mensa_user)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The exports listed for the table as built with the current params.
|
|
92
103
|
def exports
|
|
93
|
-
@exports ||= Mensa::Export.for_table(params[:table_id]).for_user(current_mensa_user).recent
|
|
104
|
+
@exports ||= Mensa::Export.for_table(params[:table_id], params: table_params).for_user(current_mensa_user).recent
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def table_params
|
|
108
|
+
@table_params ||= Mensa::TableParams.from_request(params)
|
|
94
109
|
end
|
|
95
110
|
|
|
96
111
|
def list_stream
|
|
97
|
-
turbo_stream.replace(Mensa::Export.list_dom_id(params[:table_id], current_mensa_user),
|
|
112
|
+
turbo_stream.replace(Mensa::Export.list_dom_id(params[:table_id], current_mensa_user, params: table_params),
|
|
98
113
|
partial: "mensa/exports/list", locals: list_locals)
|
|
99
114
|
end
|
|
100
115
|
|
|
101
116
|
def badge_stream
|
|
102
|
-
turbo_stream.replace(Mensa::Export.badge_dom_id(params[:table_id], current_mensa_user),
|
|
117
|
+
turbo_stream.replace(Mensa::Export.badge_dom_id(params[:table_id], current_mensa_user, params: table_params),
|
|
103
118
|
partial: "mensa/exports/badge",
|
|
104
|
-
locals: {table_name: params[:table_id], user: current_mensa_user})
|
|
119
|
+
locals: {table_name: params[:table_id], user: current_mensa_user, table_params: table_params})
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def list_locals
|
|
108
|
-
{table_name: params[:table_id], user: current_mensa_user, exports: exports}
|
|
123
|
+
{table_name: params[:table_id], user: current_mensa_user, table_params: table_params, exports: exports}
|
|
109
124
|
end
|
|
110
125
|
|
|
111
126
|
def export_config
|
|
112
|
-
params.permit(
|
|
127
|
+
config = params.permit(
|
|
113
128
|
:query,
|
|
114
129
|
:page,
|
|
115
130
|
:table_view_id,
|
|
116
131
|
order: {},
|
|
117
132
|
filters: {},
|
|
118
133
|
column_order: [],
|
|
119
|
-
hidden_columns: []
|
|
134
|
+
hidden_columns: [],
|
|
135
|
+
params: {}
|
|
120
136
|
).to_h
|
|
137
|
+
config.delete(:params)
|
|
138
|
+
config[:params] = table_params if table_params.present?
|
|
139
|
+
config
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
def current_mensa_user
|
|
@@ -10,13 +10,13 @@ module Mensa
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def index
|
|
13
|
-
config = {}.merge(params.permit(:id, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}).to_h)
|
|
13
|
+
config = {}.merge(params.permit(:id, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}, params: {}).to_h)
|
|
14
14
|
@table = Mensa.for_name(params[:table_id], config)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Returns the filter information on the column-name
|
|
18
18
|
def show
|
|
19
|
-
config = {}.merge(params.permit(:id, :value, :operator, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}).to_h)
|
|
19
|
+
config = {}.merge(params.permit(:id, :value, :operator, :page, :table_id, :target, :table_view_id, :turbo_frame_id, order: {}, filters: {}, params: {}).to_h)
|
|
20
20
|
@table = Mensa.for_name(params[:table_id], config)
|
|
21
21
|
@table.original_view_context = helpers
|
|
22
22
|
@column = @table.column(params[:id])
|
|
@@ -24,7 +24,7 @@ module Mensa
|
|
|
24
24
|
# generated element IDs match what is already in the DOM.
|
|
25
25
|
table_config = view.config
|
|
26
26
|
.deep_transform_keys(&:to_sym)
|
|
27
|
-
.merge(turbo_frame_id: params[:turbo_frame_id])
|
|
27
|
+
.merge(turbo_frame_id: params[:turbo_frame_id], params: table_params)
|
|
28
28
|
|
|
29
29
|
@table = Mensa.for_name(params[:table_id], table_config)
|
|
30
30
|
@table.request = request
|
|
@@ -56,7 +56,7 @@ module Mensa
|
|
|
56
56
|
format.turbo_stream do
|
|
57
57
|
table_config = view.config
|
|
58
58
|
.deep_transform_keys(&:to_sym)
|
|
59
|
-
.merge(turbo_frame_id: params[:turbo_frame_id])
|
|
59
|
+
.merge(turbo_frame_id: params[:turbo_frame_id], params: table_params)
|
|
60
60
|
|
|
61
61
|
@table = Mensa.for_name(params[:table_id], table_config)
|
|
62
62
|
@table.request = request
|
|
@@ -81,7 +81,7 @@ module Mensa
|
|
|
81
81
|
|
|
82
82
|
respond_to do |format|
|
|
83
83
|
format.turbo_stream do
|
|
84
|
-
@table = Mensa.for_name(params[:table_id], {turbo_frame_id: params[:turbo_frame_id]})
|
|
84
|
+
@table = Mensa.for_name(params[:table_id], {turbo_frame_id: params[:turbo_frame_id], params: table_params})
|
|
85
85
|
@table.request = request
|
|
86
86
|
@table.original_view_context = helpers
|
|
87
87
|
end
|
|
@@ -100,6 +100,11 @@ module Mensa
|
|
|
100
100
|
config
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# The params the table was built with, sent along in the views URL.
|
|
104
|
+
def table_params
|
|
105
|
+
Mensa::TableParams.from_request(params)
|
|
106
|
+
end
|
|
107
|
+
|
|
103
108
|
def current_mensa_user
|
|
104
109
|
Current.user if defined?(Current) && Current.respond_to?(:user)
|
|
105
110
|
end
|
|
@@ -6,6 +6,14 @@ export default class ApplicationController extends Controller {
|
|
|
6
6
|
this.element[this.identifier] = this
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// Appends a member id to a collection URL while keeping its query string
|
|
10
|
+
// (e.g. the table's params[...]) intact.
|
|
11
|
+
memberUrl(collectionUrl, id) {
|
|
12
|
+
const url = new URL(collectionUrl, window.location.origin)
|
|
13
|
+
url.pathname += `/${encodeURIComponent(id)}`
|
|
14
|
+
return url.toString()
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
getController(element, identifier) {
|
|
10
18
|
return this.application.getControllerForElementAndIdentifier(element, identifier)
|
|
11
19
|
}
|
|
@@ -13,7 +13,7 @@ module Mensa
|
|
|
13
13
|
return unless export
|
|
14
14
|
|
|
15
15
|
export.update(status: "processing")
|
|
16
|
-
Mensa.config.
|
|
16
|
+
Mensa.config.run_callback(:export_started, export)
|
|
17
17
|
|
|
18
18
|
table = build_table(export)
|
|
19
19
|
unless table.exportable?
|
|
@@ -27,7 +27,7 @@ module Mensa
|
|
|
27
27
|
export.asset.attach(io: tempfile, filename: filename, content_type: content_type)
|
|
28
28
|
finalize(export, status: "completed", filename: filename)
|
|
29
29
|
|
|
30
|
-
Mensa.config.
|
|
30
|
+
Mensa.config.run_callback(:export_completed, export)
|
|
31
31
|
rescue => e
|
|
32
32
|
Mensa.config.logger&.error("Mensa::ExportJob failed for export #{export&.id}: #{e.class}: #{e.message}")
|
|
33
33
|
finalize(export, status: "failed") if export
|
|
@@ -40,8 +40,8 @@ module Mensa
|
|
|
40
40
|
|
|
41
41
|
# Rebuilds the table the export was requested for, layering the view
|
|
42
42
|
# configuration (if any) underneath the captured request configuration
|
|
43
|
-
# (filters, query, ordering, page) so the generated data matches
|
|
44
|
-
# user saw when they requested the export.
|
|
43
|
+
# (params, filters, query, ordering, page) so the generated data matches
|
|
44
|
+
# what the user saw when they requested the export.
|
|
45
45
|
def build_table(export)
|
|
46
46
|
config = {}
|
|
47
47
|
|
|
@@ -131,7 +131,7 @@ module Mensa
|
|
|
131
131
|
export.update(attributes)
|
|
132
132
|
# Refresh the export button badge (download count) and the downloads list
|
|
133
133
|
# inside the export dialog for everyone viewing this table.
|
|
134
|
-
Mensa::Export.broadcast_refresh(export.table_name, export.user)
|
|
134
|
+
Mensa::Export.broadcast_refresh(export.table_name, export.user, params: export.table_params)
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
data/app/models/mensa/export.rb
CHANGED
|
@@ -16,7 +16,19 @@ module Mensa
|
|
|
16
16
|
validates :status, inclusion: {in: STATUSES}
|
|
17
17
|
validates :repeat, inclusion: {in: REPEATS}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Exports of a table built with the given params (see Mensa::TableParams).
|
|
20
|
+
# Exports of the same table with different params are kept apart.
|
|
21
|
+
scope :for_table, ->(table_name, params: nil) {
|
|
22
|
+
relation = where(table_name: table_name.to_s)
|
|
23
|
+
table_params = Mensa::TableParams.normalize(params)
|
|
24
|
+
config_params = "#{quoted_table_name}.config -> 'params'"
|
|
25
|
+
|
|
26
|
+
if table_params.blank?
|
|
27
|
+
relation.where("COALESCE(#{config_params}, '{}'::jsonb) = '{}'::jsonb")
|
|
28
|
+
else
|
|
29
|
+
relation.where("#{config_params} = ?::jsonb", table_params.to_json)
|
|
30
|
+
end
|
|
31
|
+
}
|
|
20
32
|
scope :for_user, ->(user) { where(user_id: user.respond_to?(:id) ? user&.id : user) }
|
|
21
33
|
scope :completed, -> { where(status: "completed") }
|
|
22
34
|
scope :recent, -> { order(created_at: :desc) }
|
|
@@ -39,6 +51,11 @@ module Mensa
|
|
|
39
51
|
status == "pending"
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
# The params the table was built with when this export was requested.
|
|
55
|
+
def table_params
|
|
56
|
+
(config || {}).stringify_keys["params"].presence || {}
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
def next_repeat_run_at(from: nil)
|
|
43
60
|
return if repeat.blank?
|
|
44
61
|
|
|
@@ -92,51 +109,51 @@ module Mensa
|
|
|
92
109
|
completed? && asset.attached?
|
|
93
110
|
end
|
|
94
111
|
|
|
95
|
-
# Number of currently-downloadable exports for a table/user
|
|
96
|
-
# This is the number rendered in the export button badge.
|
|
97
|
-
def self.completed_count(table_name, user)
|
|
98
|
-
for_table(table_name).for_user(user).with_downloadable_asset.count
|
|
112
|
+
# Number of currently-downloadable exports for a table/user/params
|
|
113
|
+
# combination. This is the number rendered in the export button badge.
|
|
114
|
+
def self.completed_count(table_name, user, params: nil)
|
|
115
|
+
for_table(table_name, params: params).for_user(user).with_downloadable_asset.count
|
|
99
116
|
end
|
|
100
117
|
|
|
101
|
-
# A stable, page-independent key identifying the exports of a
|
|
102
|
-
# combination, used for Turbo stream names and DOM ids
|
|
103
|
-
# can target them after completion.
|
|
104
|
-
def self.token(table_name, user)
|
|
118
|
+
# A stable, page-independent key identifying the exports of a
|
|
119
|
+
# table/user/params combination, used for Turbo stream names and DOM ids
|
|
120
|
+
# so background jobs can target them after completion.
|
|
121
|
+
def self.token(table_name, user, params: nil)
|
|
105
122
|
user_key = user.respond_to?(:id) ? user&.id : user
|
|
106
|
-
[table_name.to_s, user_key || "anonymous"].join("-").parameterize
|
|
123
|
+
[table_name.to_s, user_key || "anonymous", Mensa::TableParams.digest(params)].compact.join("-").parameterize
|
|
107
124
|
end
|
|
108
125
|
|
|
109
|
-
def self.stream_name(table_name, user)
|
|
110
|
-
"mensa-exports-#{token(table_name, user)}"
|
|
126
|
+
def self.stream_name(table_name, user, params: nil)
|
|
127
|
+
"mensa-exports-#{token(table_name, user, params: params)}"
|
|
111
128
|
end
|
|
112
129
|
|
|
113
|
-
def self.badge_dom_id(table_name, user)
|
|
114
|
-
"mensa-export-badge-#{token(table_name, user)}"
|
|
130
|
+
def self.badge_dom_id(table_name, user, params: nil)
|
|
131
|
+
"mensa-export-badge-#{token(table_name, user, params: params)}"
|
|
115
132
|
end
|
|
116
133
|
|
|
117
|
-
def self.list_dom_id(table_name, user)
|
|
118
|
-
"mensa-export-list-#{token(table_name, user)}"
|
|
134
|
+
def self.list_dom_id(table_name, user, params: nil)
|
|
135
|
+
"mensa-export-list-#{token(table_name, user, params: params)}"
|
|
119
136
|
end
|
|
120
137
|
|
|
121
138
|
# Re-renders the export button badge and downloads list for everyone
|
|
122
|
-
# subscribed to this table/user
|
|
123
|
-
# Action Cable backend (or other broadcast failure) must never
|
|
124
|
-
# caller (job completion, download cleanup, ...).
|
|
125
|
-
def self.broadcast_refresh(table_name, user)
|
|
126
|
-
stream = stream_name(table_name, user)
|
|
139
|
+
# subscribed to this table/user/params export stream. Best-effort: a
|
|
140
|
+
# missing Action Cable backend (or other broadcast failure) must never
|
|
141
|
+
# break the caller (job completion, download cleanup, ...).
|
|
142
|
+
def self.broadcast_refresh(table_name, user, params: nil)
|
|
143
|
+
stream = stream_name(table_name, user, params: params)
|
|
127
144
|
|
|
128
145
|
Turbo::StreamsChannel.broadcast_replace_to(
|
|
129
146
|
stream,
|
|
130
|
-
target: badge_dom_id(table_name, user),
|
|
147
|
+
target: badge_dom_id(table_name, user, params: params),
|
|
131
148
|
partial: "mensa/exports/badge",
|
|
132
|
-
locals: {table_name: table_name, user: user}
|
|
149
|
+
locals: {table_name: table_name, user: user, table_params: params}
|
|
133
150
|
)
|
|
134
151
|
|
|
135
152
|
Turbo::StreamsChannel.broadcast_replace_to(
|
|
136
153
|
stream,
|
|
137
|
-
target: list_dom_id(table_name, user),
|
|
154
|
+
target: list_dom_id(table_name, user, params: params),
|
|
138
155
|
partial: "mensa/exports/list",
|
|
139
|
-
locals: {table_name: table_name, user: user, exports: for_table(table_name).for_user(user).recent}
|
|
156
|
+
locals: {table_name: table_name, user: user, table_params: params, exports: for_table(table_name, params: params).for_user(user).recent}
|
|
140
157
|
)
|
|
141
158
|
rescue => e
|
|
142
159
|
Mensa.config.logger&.warn("Mensa::Export broadcast failed: #{e.class}: #{e.message}")
|
data/app/tables/mensa/base.rb
CHANGED
|
@@ -25,9 +25,11 @@ module Mensa
|
|
|
25
25
|
@params = (@config[:params].presence || {}).deep_symbolize_keys
|
|
26
26
|
@config[:params] = @params
|
|
27
27
|
|
|
28
|
+
# Hidden columns can come from browser state saved for different params,
|
|
29
|
+
# so skip names that aren't columns of this table.
|
|
28
30
|
current_hidden_columns&.each do |column_name|
|
|
29
31
|
c = columns.find { |c| c.name == column_name.to_sym }
|
|
30
|
-
c.config[:visible] = false
|
|
32
|
+
c.config[:visible] = false if c
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
@@ -40,6 +42,12 @@ module Mensa
|
|
|
40
42
|
# are absent from any URL-supplied column_order. Always append them so
|
|
41
43
|
# that columns and selected_scope include their attributes.
|
|
42
44
|
all_keys = (config[:columns]&.keys || []).map(&:to_sym)
|
|
45
|
+
# A column_order can come from browser state saved for different params
|
|
46
|
+
# (with different columns), so drop names that aren't columns here.
|
|
47
|
+
if all_keys.any?
|
|
48
|
+
order &= all_keys
|
|
49
|
+
order = all_keys if order.empty?
|
|
50
|
+
end
|
|
43
51
|
internal_keys = all_keys.select { |key| config.dig(:columns, key, :internal) }
|
|
44
52
|
(order | internal_keys)
|
|
45
53
|
end
|
|
@@ -140,6 +148,18 @@ module Mensa
|
|
|
140
148
|
query.present? ? "#{path}?#{query}" : path
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
# Appends this table's params to a Mensa endpoint path, so the endpoint
|
|
152
|
+
# can rebuild the table (see Mensa::TableParams).
|
|
153
|
+
def path_with_params(path)
|
|
154
|
+
Mensa::TableParams.append_to(path, params)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Key under which the browser persists this table's state (filters, order,
|
|
158
|
+
# column order, ...). Tables built with different params get separate state.
|
|
159
|
+
def storage_key
|
|
160
|
+
[name, Mensa::TableParams.digest(params)].compact.join(":")
|
|
161
|
+
end
|
|
162
|
+
|
|
143
163
|
def all_views
|
|
144
164
|
views = system_views
|
|
145
165
|
views += TableView.where(table_name: name).where(user: [nil, current_user])
|
data/app/tables/mensa/column.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Mensa
|
|
|
74
74
|
@attribute_for_condition = if config[:attribute].present?
|
|
75
75
|
Arel.sql(raw_attribute)
|
|
76
76
|
elsif table.model.column_names.include? name.to_s
|
|
77
|
-
Arel.sql("
|
|
77
|
+
Arel.sql("#{table.model.connection.quote_table_name(table.model.table_name)}.#{table.model.connection.quote_column_name(name)}")
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
data/app/tables/mensa/scope.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Mensa
|
|
|
47
47
|
ensure_internal_columns_for_joined_associations
|
|
48
48
|
|
|
49
49
|
@selected_scope = ordered_scope
|
|
50
|
-
@selected_scope = @selected_scope.select([:id] + columns.filter_map(&:attribute))
|
|
50
|
+
@selected_scope = @selected_scope.select((id_column? ? [:id] : []) + columns.filter_map(&:attribute))
|
|
51
51
|
|
|
52
52
|
@selected_scope
|
|
53
53
|
end
|
|
@@ -76,6 +76,16 @@ module Mensa
|
|
|
76
76
|
|
|
77
77
|
private
|
|
78
78
|
|
|
79
|
+
# Relations without an id column (e.g. a read-only model backed by a
|
|
80
|
+
# subquery) can't select it.
|
|
81
|
+
def id_column?
|
|
82
|
+
relation = ordered_scope
|
|
83
|
+
klass = relation.respond_to?(:klass) ? relation.klass : model
|
|
84
|
+
klass.column_names.include?("id")
|
|
85
|
+
rescue ActiveRecord::ActiveRecordError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
79
89
|
def adjacent_record(record, step:)
|
|
80
90
|
records = ordered_scope.to_a
|
|
81
91
|
index = records.find_index { |candidate| same_record?(candidate, record) }
|
|
@@ -110,11 +120,20 @@ module Mensa
|
|
|
110
120
|
result = current_order_provided? ? (current_order || {}) : (config[:order] || {})
|
|
111
121
|
result = result.symbolize_keys.compact_blank.transform_values(&:to_sym)
|
|
112
122
|
result.filter_map { |k, v|
|
|
113
|
-
|
|
123
|
+
col = column(k)
|
|
124
|
+
# Skip orders on unknown names, e.g. from browser state saved for
|
|
125
|
+
# different params.
|
|
126
|
+
next unless col || model_attribute?(k)
|
|
127
|
+
|
|
128
|
+
attribute = col&.attribute_for_condition || k
|
|
114
129
|
"#{attribute} #{v} NULLS LAST"
|
|
115
130
|
}.join(", ")
|
|
116
131
|
end
|
|
117
132
|
|
|
133
|
+
def model_attribute?(name)
|
|
134
|
+
model.respond_to?(:column_names) && model.column_names.include?(name.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
118
137
|
# Builds an order hash for URL generation. Merges current order with overrides;
|
|
119
138
|
# nil values become "" so they appear in the URL as order[col]= (which tells
|
|
120
139
|
# the server the user explicitly cleared that column's sort direction).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Mensa
|
|
4
|
+
# Helpers for the extra +params+ a table can be built with
|
|
5
|
+
# (`Mensa::Table::Component.new("name", params: {...})`). They travel to
|
|
6
|
+
# Mensa's endpoints as `params[...]` query parameters, the same way
|
|
7
|
+
# Mensa::Base#path sends them, so normalizing through that query-string
|
|
8
|
+
# round trip gives exactly the hash the controllers receive.
|
|
9
|
+
module TableParams
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Returns the params as a string-keyed hash with string values, or {} when
|
|
13
|
+
# blank. Accepts plain hashes and ActionController::Parameters.
|
|
14
|
+
def normalize(params)
|
|
15
|
+
params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
|
|
16
|
+
return {} if params.blank?
|
|
17
|
+
|
|
18
|
+
Rack::Utils.parse_nested_query({params: params}.to_query)["params"] || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The permitted `params` sent along with a request to a Mensa endpoint.
|
|
22
|
+
def from_request(request_params)
|
|
23
|
+
normalize(request_params.slice(:params).permit(params: {}).to_h[:params])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Query string (`params[key]=value`) to append to Mensa endpoint URLs.
|
|
27
|
+
def to_query(params)
|
|
28
|
+
normalized = normalize(params)
|
|
29
|
+
normalized.present? ? {params: normalized}.to_query : ""
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Appends the params to +path+ as `params[...]` query parameters.
|
|
33
|
+
def append_to(path, params)
|
|
34
|
+
query = to_query(params)
|
|
35
|
+
query.present? ? "#{path}?#{query}" : path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Short, stable digest identifying a set of params, or nil when blank.
|
|
39
|
+
def digest(params)
|
|
40
|
+
query = to_query(params)
|
|
41
|
+
Digest::SHA256.hexdigest(query)[0, 12] if query.present?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
1
|
+
<% table_params = local_assigns[:table_params] %>
|
|
2
|
+
<% count = Mensa::Export.completed_count(table_name, user, params: table_params) %>
|
|
3
|
+
<span id="<%= Mensa::Export.badge_dom_id(table_name, user, params: table_params) %>"
|
|
3
4
|
class="mensa-table__control_bar__badge <%= 'hidden' unless count.positive? %>"
|
|
4
5
|
aria-hidden="<%= count.positive? ? 'false' : 'true' %>">
|
|
5
6
|
<%= count %>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% user = table.current_user %>
|
|
2
|
-
<% exports = Mensa::Export.for_table(table.name).for_user(user).recent %>
|
|
2
|
+
<% exports = Mensa::Export.for_table(table.name, params: table.params).for_user(user).recent %>
|
|
3
3
|
<dialog class="mensa-table__export-dialog" data-mensa-table-target="exportDialog" data-action="click->mensa-table#exportDialogBackdrop">
|
|
4
4
|
<div class="mensa-table__export-dialog__panel">
|
|
5
5
|
<header class="mensa-table__export-dialog__header">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</header>
|
|
11
11
|
|
|
12
12
|
<div class="mensa-table__export-dialog__body">
|
|
13
|
-
<%= render partial: "mensa/exports/list", locals: {table_name: table.name, user: user, exports: exports} %>
|
|
13
|
+
<%= render partial: "mensa/exports/list", locals: {table_name: table.name, user: user, table_params: table.params, exports: exports} %>
|
|
14
14
|
|
|
15
15
|
<form class="mensa-table__export-dialog__form" data-action="submit->mensa-table#confirmExport">
|
|
16
16
|
<fieldset class="mensa-table__export-dialog__fieldset">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div id="<%= Mensa::Export.list_dom_id(table_name, user) %>" class="mensa-table__export-dialog__downloads">
|
|
1
|
+
<div id="<%= Mensa::Export.list_dom_id(table_name, user, params: local_assigns[:table_params]) %>" class="mensa-table__export-dialog__downloads">
|
|
2
2
|
<h3 class="mensa-table__export-dialog__section-title"><%= t("mensa.exports.available_downloads", default: "Available downloads") %></h3>
|
|
3
3
|
<% if exports.blank? %>
|
|
4
4
|
<p class="mensa-table__export-dialog__empty"><%= t("mensa.exports.empty", default: "You have no downloads yet. Create one below.") %></p>
|
data/lib/mensa/configuration.rb
CHANGED
|
@@ -120,15 +120,20 @@ module Mensa
|
|
|
120
120
|
end,
|
|
121
121
|
# Called with the Mensa::Export once the CSV has been generated and
|
|
122
122
|
# attached (export.asset). Use this to e.g. notify or email the user.
|
|
123
|
+
# (Before 0.6.12 this was called export_complete; that name still works.)
|
|
123
124
|
#
|
|
124
125
|
# UserMailer.with(
|
|
125
126
|
# user: User.find(export.user_id),
|
|
126
127
|
# export: export,
|
|
127
128
|
# ).export_email.deliver_later
|
|
128
|
-
|
|
129
|
+
export_completed: lambda do |export|
|
|
129
130
|
end
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
CALLBACKS = %i[export_started export_completed].freeze
|
|
134
|
+
# Older names that are still called alongside the current one.
|
|
135
|
+
CALLBACK_ALIASES = {export_completed: %i[export_complete]}.freeze
|
|
136
|
+
|
|
132
137
|
# :front or :back (default)
|
|
133
138
|
option :row_actions_position, default: :back
|
|
134
139
|
# It's either :basic or :fuzzy, for fuzzy search you need to have `pg_trgm` extension installed
|
|
@@ -137,6 +142,24 @@ module Mensa
|
|
|
137
142
|
def initialize
|
|
138
143
|
set_defaults!
|
|
139
144
|
end
|
|
145
|
+
|
|
146
|
+
def callbacks=(callbacks)
|
|
147
|
+
known = CALLBACKS + CALLBACK_ALIASES.values.flatten
|
|
148
|
+
unknown = callbacks.to_h.keys.map(&:to_sym) - known
|
|
149
|
+
if unknown.any?
|
|
150
|
+
logger&.warn("Mensa: ignoring unknown callback(s) #{unknown.join(", ")}; known callbacks are #{CALLBACKS.join(", ")}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@callbacks = callbacks
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Calls the callback registered under +name+ (or one of its older names).
|
|
157
|
+
def run_callback(name, *args)
|
|
158
|
+
[name, *CALLBACK_ALIASES.fetch(name, [])].each do |key|
|
|
159
|
+
callback = callbacks && (callbacks[key] || callbacks[key.to_s])
|
|
160
|
+
callback&.call(*args)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
140
163
|
end
|
|
141
164
|
|
|
142
165
|
module Configurable
|
data/lib/mensa/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mensa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom de Grunt
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -168,6 +168,7 @@ files:
|
|
|
168
168
|
- ".gitignore"
|
|
169
169
|
- ".prettierrc.json"
|
|
170
170
|
- ".rubocop.yml"
|
|
171
|
+
- ".shipkit.yml"
|
|
171
172
|
- ".vscode/settings.json"
|
|
172
173
|
- ".vscode/tasks.json"
|
|
173
174
|
- ".zed/tasks.json"
|
|
@@ -277,6 +278,7 @@ files:
|
|
|
277
278
|
- app/tables/mensa/scope.rb
|
|
278
279
|
- app/tables/mensa/search.rb
|
|
279
280
|
- app/tables/mensa/system_view.rb
|
|
281
|
+
- app/tables/mensa/table_params.rb
|
|
280
282
|
- app/views/mensa/exports/_badge.html.erb
|
|
281
283
|
- app/views/mensa/exports/_dialog.html.erb
|
|
282
284
|
- app/views/mensa/exports/_list.html.erb
|