mensa 0.2.5 → 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/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +85 -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 +7 -1
- data/app/components/mensa/add_filter/component_controller.js +697 -83
- 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 +0 -4
- 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 -6
- data/app/components/mensa/filter_pill/component.rb +9 -0
- data/app/components/mensa/filter_pill/component_controller.js +50 -10
- data/app/components/mensa/filter_pill_list/component.css +58 -9
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -8
- data/app/components/mensa/filter_pill_list/component_controller.js +747 -48
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/row_action/component.html.slim +2 -2
- 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_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 -88
- 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/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/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 +4 -1
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +3 -6
- data/app/helpers/mensa/application_helper.rb +4 -0
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/index.js +13 -4
- data/app/jobs/mensa/export_job.rb +77 -84
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/base.rb +103 -12
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +15 -0
- data/app/tables/mensa/column.rb +15 -2
- 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/filter_dsl.rb +4 -1
- data/app/tables/mensa/config/render_dsl.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +12 -5
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +20 -1
- data/app/tables/mensa/filter.rb +86 -3
- data/app/tables/mensa/scope.rb +24 -12
- 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 +35 -8
- 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/config/locales/en.yml +44 -0
- data/config/locales/nl.yml +45 -0
- 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/mensa/configuration.rb +33 -12
- data/lib/mensa/engine.rb +7 -2
- data/lib/mensa/version.rb +1 -1
- data/mensa.gemspec +2 -1
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +50 -8
|
@@ -1,54 +1,52 @@
|
|
|
1
1
|
.mensa-table {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
thead {
|
|
3
|
+
tr {
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
th {
|
|
7
|
+
@apply top-0 z-10 py-2 px-2 align-bottom text-left text-sm font-medium text-gray-500 border-b border-gray-200 dark:border-gray-600 whitespace-nowrap;
|
|
8
|
+
|
|
9
|
+
&:first-child {
|
|
10
|
+
@apply pl-4;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&:last-child {
|
|
14
|
+
@apply pr-4;
|
|
15
|
+
}
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
.container {
|
|
18
|
+
@apply h-4 flex items-center;
|
|
19
|
+
}
|
|
9
20
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
.title {
|
|
22
|
+
@apply w-full;
|
|
23
|
+
}
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
/* For sortable columns the link takes over the cell's padding so
|
|
26
|
+
the entire th area — including what was padding — is clickable. */
|
|
27
|
+
&:has(.order) {
|
|
28
|
+
@apply p-0;
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
.order {
|
|
31
|
+
@apply flex items-center gap-1 w-full py-2 px-2;
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
&:first-child .order {
|
|
35
|
+
@apply pl-4;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:last-child .order {
|
|
39
|
+
@apply pr-4;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
26
42
|
}
|
|
27
|
-
}
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/*}*/
|
|
45
|
+
&__column_edit {
|
|
46
|
+
thead {
|
|
47
|
+
th {
|
|
48
|
+
@apply cursor-grab;
|
|
49
|
+
}
|
|
36
50
|
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
&__condensed {
|
|
41
|
-
thead {
|
|
42
|
-
th {
|
|
43
|
-
@apply whitespace-nowrap py-2 pl-4 pr-3 px-0;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
&__column_edit {
|
|
48
|
-
thead {
|
|
49
|
-
th {
|
|
50
|
-
@apply cursor-grab;
|
|
51
|
-
}
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
}
|
|
52
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
th
|
|
2
|
-
.
|
|
3
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
- if column.sortable?
|
|
3
|
+
= link_to table.path(order: {column.name => column.next_sort_direction}, turbo_frame_id: table.table_id), "data-turbo-frame": "_self", class: "order cursor-pointer"
|
|
4
|
+
span = column.human_name
|
|
5
|
+
i class=Mensa.config.icons["order_indicator#{column.sort_direction.to_s.present? ? "_#{column.sort_direction.to_s}" : ""}".to_sym]
|
|
6
|
+
- else
|
|
7
|
+
.container
|
|
8
|
+
.title = column.human_name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
- if action.show.call(row.record)
|
|
2
|
-
a href=(action.link ? table.original_view_context.instance_exec(row.record, &action.link) : '') title=action.title *action.link_attributes
|
|
2
|
+
a.action href=(action.link ? table.original_view_context.instance_exec(row.record, &action.link) : '') title=action.title *action.link_attributes
|
|
3
3
|
- if action.icon
|
|
4
4
|
i.fa class=action.icon
|
|
5
5
|
- else
|
|
6
|
-
= name
|
|
6
|
+
= name
|
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
.mensa-table {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
&__search {
|
|
3
|
+
@apply dark:bg-gray-800 dark:border-gray-800 border-b p-2;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
&__button {
|
|
6
|
+
@apply cursor-pointer w-6 h-full text-gray-400;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
input&__input {
|
|
10
|
+
@apply pl-2 h-6 w-full border-0 bg-transparent pr-4 text-gray-600 dark:text-gray-400 placeholder:text-gray-400 focus:ring-0 text-sm;
|
|
11
|
+
}
|
|
7
12
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
|
|
14
|
+
&__save-view-dialog {
|
|
15
|
+
@apply w-[26rem] max-w-[90vw] 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;
|
|
16
|
+
|
|
17
|
+
&::backdrop {
|
|
18
|
+
@apply bg-gray-900/40;
|
|
19
|
+
backdrop-filter: blur(2px);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&__form {
|
|
23
|
+
@apply flex flex-col gap-5 p-6;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&__header {
|
|
27
|
+
@apply flex flex-col gap-1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&__title {
|
|
31
|
+
@apply text-lg font-semibold leading-6 text-gray-900 dark:text-gray-100;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__subtitle {
|
|
35
|
+
@apply text-sm text-gray-500 dark:text-gray-400;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&__field {
|
|
39
|
+
@apply flex flex-col gap-1.5;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&__label {
|
|
43
|
+
@apply text-sm font-medium text-gray-700 dark:text-gray-300;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
input&__input,
|
|
47
|
+
textarea&__textarea {
|
|
48
|
+
@apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 shadow-sm placeholder:text-gray-400 focus:border-primary-500 focus:ring-2 focus:ring-primary-500 focus:outline-none;
|
|
49
|
+
border-radius: 0.5rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
textarea&__textarea {
|
|
53
|
+
@apply resize-y min-h-[5rem];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__actions {
|
|
57
|
+
@apply flex justify-end gap-2 pt-1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&__button {
|
|
61
|
+
@apply 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;
|
|
62
|
+
|
|
63
|
+
&--secondary {
|
|
64
|
+
@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;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&--primary {
|
|
68
|
+
@apply bg-primary-600 text-white hover:bg-primary-500 focus:ring-primary-500;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
11
71
|
}
|
|
12
|
-
|
|
13
|
-
}
|
|
72
|
+
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
.
|
|
3
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
- if table.current_user
|
|
2
|
+
dialog.mensa-table__save-view-dialog data-mensa-table-target="saveViewDialog" data-action="click->mensa-table#saveViewDialogBackdrop"
|
|
3
|
+
form.mensa-table__save-view-dialog__form data-action="submit->mensa-table#confirmSaveView"
|
|
4
|
+
header.mensa-table__save-view-dialog__header
|
|
5
|
+
h2.mensa-table__save-view-dialog__title
|
|
6
|
+
= t('.save_view_title', default: 'Save view')
|
|
7
|
+
p.mensa-table__save-view-dialog__subtitle
|
|
8
|
+
= t('.save_view_subtitle', default: 'Save the current filters, ordering and search as a reusable view.')
|
|
9
|
+
label.mensa-table__save-view-dialog__field
|
|
10
|
+
span.mensa-table__save-view-dialog__label
|
|
11
|
+
= t('.view_name', default: 'Name')
|
|
12
|
+
input.mensa-table__save-view-dialog__input type="text" required=true placeholder=t('.view_name_placeholder', default: 'e.g. Active customers') data-mensa-table-target="saveViewName"
|
|
13
|
+
label.mensa-table__save-view-dialog__field
|
|
14
|
+
span.mensa-table__save-view-dialog__label
|
|
15
|
+
= t('.view_description', default: 'Description')
|
|
16
|
+
textarea.mensa-table__save-view-dialog__textarea rows="3" placeholder=t('.view_description_placeholder', default: 'Optional notes about this view') data-mensa-table-target="saveViewDescription"
|
|
17
|
+
.mensa-table__save-view-dialog__actions
|
|
18
|
+
button.mensa-table__save-view-dialog__button.mensa-table__save-view-dialog__button--secondary type="button" data-action="mensa-table#cancelSaveView"
|
|
12
19
|
= t('.cancel')
|
|
13
|
-
button
|
|
20
|
+
button.mensa-table__save-view-dialog__button.mensa-table__save-view-dialog__button--primary type="submit"
|
|
14
21
|
= t('.save')
|
|
15
|
-
- unless table.supports_views?
|
|
16
|
-
= render Mensa::ControlBar::Component.new(table: table)
|
|
17
|
-
|
|
@@ -1,64 +1,54 @@
|
|
|
1
|
-
import ApplicationController from "mensa/controllers/application_controller"
|
|
2
|
-
|
|
3
|
-
import { get } from "@rails/request.js"
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
4
2
|
|
|
5
3
|
export default class SearchComponentController extends ApplicationController {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
connect() {
|
|
9
|
-
super.connect()
|
|
10
|
-
this.monitorSearch()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
monitorSearch(event) {
|
|
14
|
-
event && event.preventDefault()
|
|
4
|
+
static targets = ["resetSearchButton", "searchInput"];
|
|
5
|
+
static outlets = ["mensa-filter-pill-list"];
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} else {
|
|
20
|
-
this.resetSearchButtonTarget.classList.add("hidden")
|
|
7
|
+
connect() {
|
|
8
|
+
super.connect();
|
|
9
|
+
this.monitorSearch();
|
|
21
10
|
}
|
|
22
|
-
}
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
monitorSearch(event) {
|
|
13
|
+
event && event.preventDefault();
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
if (this.searchInputTarget.value.length >= 1) {
|
|
16
|
+
this.resetSearchButtonTarget.classList.remove("hidden");
|
|
17
|
+
this.searchInputTarget.focus();
|
|
18
|
+
} else {
|
|
19
|
+
this.resetSearchButtonTarget.classList.add("hidden");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
38
|
-
}
|
|
23
|
+
resetSearch(event) {
|
|
24
|
+
event.preventDefault();
|
|
39
25
|
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
this.searchInputTarget.value = "";
|
|
27
|
+
this.searchInputTarget.focus();
|
|
28
|
+
this.resetSearchButtonTarget.classList.add("hidden");
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
// Re-request the table without a query (keeping any active filters) and
|
|
31
|
+
// forget the persisted query. The filter pill list owns persistence and the
|
|
32
|
+
// request so filters and search stay composed in a single call.
|
|
33
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
34
|
+
this.mensaFilterPillListOutlet.setQuery("");
|
|
35
|
+
}
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (url.searchParams.get("query") === this.query) {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
38
|
+
search(event) {
|
|
39
|
+
event.preventDefault();
|
|
52
40
|
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
if (this.query.length < 3) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
// Persist the query and re-request the table, keeping any active filters.
|
|
46
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
47
|
+
this.mensaFilterPillListOutlet.setQuery(this.query);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
60
50
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
get query() {
|
|
52
|
+
return this.searchInputTarget.value;
|
|
53
|
+
}
|
|
64
54
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
+
|
|
3
|
+
// Manages row-level checkbox selection and the batch-action bar.
|
|
4
|
+
//
|
|
5
|
+
// The controller lives on the .overflow-y-auto.relative wrapper element.
|
|
6
|
+
//
|
|
7
|
+
// The batch bar is a div.mensa-batch-bar that is a sibling of the <table>,
|
|
8
|
+
// absolutely positioned at top:0/left:0 inside that wrapper. Its width is
|
|
9
|
+
// set to the <table>'s offsetWidth so it covers the full table — including
|
|
10
|
+
// any overflow that causes horizontal scrolling.
|
|
11
|
+
export default class SelectionController extends ApplicationController {
|
|
12
|
+
static targets = [
|
|
13
|
+
"headerCheckbox", // select-all checkbox in the header <th>
|
|
14
|
+
"rowCheckbox", // per-row checkboxes in <tbody>
|
|
15
|
+
"batchBar", // absolutely-positioned div overlaying the header row
|
|
16
|
+
"selectedCount", // span inside the batch bar showing "N selected"
|
|
17
|
+
"batchAllCheckbox", // checkbox inside the batch bar (clicking deselects all)
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
static values = {
|
|
21
|
+
batchUrl: String, // POST endpoint for batch actions
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
connect() {
|
|
25
|
+
super.connect();
|
|
26
|
+
this.resizeHandler = () => this.syncBatchBarWidth();
|
|
27
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
disconnect() {
|
|
31
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sets the batch bar's width to match the <table> element so it covers
|
|
35
|
+
// the full scrollable width, not just the visible viewport portion.
|
|
36
|
+
syncBatchBarWidth() {
|
|
37
|
+
if (!this.hasBatchBarTarget) return;
|
|
38
|
+
const table = this.element.querySelector("table");
|
|
39
|
+
if (!table) return;
|
|
40
|
+
this.batchBarTarget.style.width = `${table.offsetWidth}px`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Called when the select-all checkbox in the header changes.
|
|
44
|
+
toggleAll(event) {
|
|
45
|
+
const checked = event.target.checked;
|
|
46
|
+
this.rowCheckboxTargets.forEach((cb) => {
|
|
47
|
+
cb.checked = checked;
|
|
48
|
+
});
|
|
49
|
+
this.updateSelectionState();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Called when an individual row checkbox changes.
|
|
53
|
+
toggleRow() {
|
|
54
|
+
this.updateSelectionState();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Called when the batch bar's checkbox is clicked — always deselects all.
|
|
58
|
+
deselectAll() {
|
|
59
|
+
this.rowCheckboxTargets.forEach((cb) => (cb.checked = false));
|
|
60
|
+
this.updateSelectionState();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Stops click events from bubbling up to the row's satis-link handler,
|
|
64
|
+
// preventing accidental navigation when the user clicks a checkbox cell.
|
|
65
|
+
stopPropagation(event) {
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Builds and submits a hidden form for the chosen batch action.
|
|
70
|
+
// Reads selected record IDs from the checked row checkboxes.
|
|
71
|
+
executeBatch(event) {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
|
|
74
|
+
const actionName = event.params.batchAction;
|
|
75
|
+
const selectedIds = this.rowCheckboxTargets
|
|
76
|
+
.filter((cb) => cb.checked)
|
|
77
|
+
.map((cb) => cb.value);
|
|
78
|
+
|
|
79
|
+
if (selectedIds.length === 0) return;
|
|
80
|
+
if (!this.batchUrlValue) return;
|
|
81
|
+
|
|
82
|
+
const form = document.createElement("form");
|
|
83
|
+
form.method = "post";
|
|
84
|
+
form.action = this.batchUrlValue;
|
|
85
|
+
|
|
86
|
+
// Rails CSRF token
|
|
87
|
+
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
88
|
+
if (csrfMeta) {
|
|
89
|
+
const csrfInput = document.createElement("input");
|
|
90
|
+
csrfInput.type = "hidden";
|
|
91
|
+
csrfInput.name = "authenticity_token";
|
|
92
|
+
csrfInput.value = csrfMeta.getAttribute("content");
|
|
93
|
+
form.appendChild(csrfInput);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Batch action name
|
|
97
|
+
const actionInput = document.createElement("input");
|
|
98
|
+
actionInput.type = "hidden";
|
|
99
|
+
actionInput.name = "batch_action_name";
|
|
100
|
+
actionInput.value = actionName;
|
|
101
|
+
form.appendChild(actionInput);
|
|
102
|
+
|
|
103
|
+
// One hidden input per selected record ID
|
|
104
|
+
selectedIds.forEach((id) => {
|
|
105
|
+
const input = document.createElement("input");
|
|
106
|
+
input.type = "hidden";
|
|
107
|
+
input.name = "record_ids[]";
|
|
108
|
+
input.value = id;
|
|
109
|
+
form.appendChild(input);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
document.body.appendChild(form);
|
|
113
|
+
form.submit();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Syncs the header checkbox state and shows/hides the batch bar overlay.
|
|
117
|
+
updateSelectionState() {
|
|
118
|
+
const all = this.rowCheckboxTargets;
|
|
119
|
+
const checked = all.filter((cb) => cb.checked);
|
|
120
|
+
const hasSelection = checked.length > 0;
|
|
121
|
+
const allSelected = all.length > 0 && checked.length === all.length;
|
|
122
|
+
const someSelected = checked.length > 0 && checked.length < all.length;
|
|
123
|
+
|
|
124
|
+
// Keep the header select-all checkbox in sync
|
|
125
|
+
if (this.hasHeaderCheckboxTarget) {
|
|
126
|
+
this.headerCheckboxTarget.checked = allSelected;
|
|
127
|
+
this.headerCheckboxTarget.indeterminate = someSelected;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Mirror the state on the batch bar's deselect-all checkbox
|
|
131
|
+
if (this.hasBatchAllCheckboxTarget) {
|
|
132
|
+
this.batchAllCheckboxTarget.checked = allSelected;
|
|
133
|
+
this.batchAllCheckboxTarget.indeterminate = someSelected;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Show / hide the batch bar overlay
|
|
137
|
+
if (this.hasBatchBarTarget) {
|
|
138
|
+
if (hasSelection) this.syncBatchBarWidth();
|
|
139
|
+
this.batchBarTarget.classList.toggle("hidden", !hasSelection);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update "N selected" label
|
|
143
|
+
if (this.hasSelectedCountTarget) {
|
|
144
|
+
this.selectedCountTarget.textContent = `${checked.length} selected`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Hide view-origin filter pills when the eye toggle is in hidden state */
|
|
2
|
+
.mensa-table--view-filters-hidden [data-view-filter="true"] {
|
|
3
|
+
display: none;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* Hide the + trigger when view filters are collapsed AND no user-added pills exist.
|
|
7
|
+
Using :not(:has(...)) keeps it visible whenever the user has their own filters.
|
|
8
|
+
The controller and its column-list ul stay in the DOM so the search-input focus
|
|
9
|
+
handler can still open the column list. */
|
|
10
|
+
.mensa-table--view-filters-hidden
|
|
11
|
+
.mensa-table__search-bar__pills-area:not(:has(.mensa-filter-pill:not([data-view-filter="true"])))
|
|
12
|
+
.mensa-table__add_filter__trigger {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.mensa-table {
|
|
17
|
+
select {
|
|
18
|
+
@apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 pl-3 pr-10 text-gray-900 dark:text-gray-100 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 sm:text-sm;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&__toolbar {
|
|
22
|
+
@apply flex items-center gap-2 px-2 py-1 border-b border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&__search-container {
|
|
26
|
+
@apply flex flex-1 min-w-0 items-stretch h-8 rounded-md bg-white dark:bg-gray-800;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
.mensa-table id="table-#{table.table_id}" data-mensa-table-
|
|
1
|
+
.mensa-table id="table-#{table.table_id}" data-mensa-table-supports-views-value="#{table.supports_views?}" data-mensa-table-table-url-value=helpers.mensa.table_url(table.name, {turbo_frame_id: table.table_id}.merge(params)) data-mensa-table-save-view-url-value=helpers.mensa.table_views_path(table.name) data-mensa-table-views-url-value=helpers.mensa.table_views_path(table.name) data-mensa-table-exports-url-value=helpers.mensa.table_exports_path(table.name) data-controller="mensa-table" data-mensa-table-mensa-filter-pill-outlet="[data-controller='mensa-filter-pill']" data-mensa-table-mensa-filter-pill-list-outlet="#mensa-filter-pill-list-#{table.table_id}" data-mensa-table-mensa-column-customizer-outlet="[data-controller='mensa-column-customizer']"
|
|
2
|
+
.mensa-table__toolbar
|
|
3
|
+
.mensa-table__search-container
|
|
4
|
+
- if table.supports_views? && table.show_header?
|
|
5
|
+
= render Mensa::Views::Component.new(table: table)
|
|
6
|
+
div.flex.flex-1.min-w-0 id="filters-#{table.table_id}"
|
|
7
|
+
= render Mensa::FilterPillList::Component.new(table: table)
|
|
8
|
+
= render Mensa::ControlBar::Component.new(table: table)
|
|
2
9
|
= render Mensa::Search::Component.new(table: table)
|
|
3
|
-
|
|
4
|
-
= render Mensa::FilterPillList::Component.new(table: table)
|
|
5
|
-
- if table.supports_views? && table.show_header?
|
|
6
|
-
= render Mensa::Views::Component.new(table: table)
|
|
7
|
-
turbo-frame id=table.table_id src=helpers.mensa.table_url(table.name, {turbo_frame_id: table.table_id}.merge(params)) target="_top" loading="lazy" data-mensa-table-target="turboFrame" data-turbo-permanent=""
|
|
10
|
+
turbo-frame id=table.table_id target="_top" data-mensa-table-target="turboFrame" data-turbo-permanent=""
|