mensa 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +6 -2
- data/.devcontainer/compose.yaml +1 -1
- data/.devcontainer/devcontainer.json +31 -29
- data/.devcontainer/postCreate.sh +8 -0
- data/.devcontainer/postStart.sh +9 -0
- data/.gitignore +3 -1
- data/.zed/tasks.json +12 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +155 -153
- data/Procfile +1 -1
- data/README.md +95 -60
- data/app/assets/stylesheets/mensa/application.css +14 -11
- data/app/components/mensa/add_filter/component.css +110 -5
- data/app/components/mensa/add_filter/component.html.slim +10 -12
- data/app/components/mensa/add_filter/component.rb +8 -2
- data/app/components/mensa/add_filter/component_controller.js +697 -85
- data/app/components/mensa/cell/component.css +9 -0
- data/app/components/mensa/column_customizer/component.css +40 -0
- data/app/components/mensa/column_customizer/component.html.slim +14 -0
- data/app/components/mensa/column_customizer/component.rb +13 -0
- data/app/components/mensa/column_customizer/component_controller.js +383 -0
- data/app/components/mensa/control_bar/component.css +127 -4
- data/app/components/mensa/control_bar/component.html.slim +41 -14
- data/app/components/mensa/control_bar/component.rb +2 -6
- data/app/components/mensa/empty_state/component.css +20 -0
- data/app/components/mensa/empty_state/component.html.slim +7 -0
- data/app/components/mensa/empty_state/component.rb +18 -0
- data/app/components/mensa/filter_pill/component.css +23 -0
- data/app/components/mensa/filter_pill/component.html.slim +9 -0
- data/app/components/mensa/filter_pill/component.rb +24 -0
- data/app/components/mensa/filter_pill/component_controller.js +52 -0
- data/app/components/mensa/filter_pill_list/component.css +63 -0
- data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
- data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
- data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
- data/app/components/mensa/header/component.css +41 -43
- data/app/components/mensa/header/component.html.slim +7 -7
- data/app/components/mensa/header/component.rb +1 -1
- data/app/components/mensa/row_action/component.html.slim +2 -2
- data/app/components/mensa/row_action/component.rb +1 -1
- data/app/components/mensa/search/component.css +68 -9
- data/app/components/mensa/search/component.html.slim +19 -15
- data/app/components/mensa/search/component.rb +1 -1
- data/app/components/mensa/search/component_controller.js +39 -49
- data/app/components/mensa/selection/component_controller.js +147 -0
- data/app/components/mensa/table/component.css +28 -0
- data/app/components/mensa/table/component.html.slim +9 -6
- data/app/components/mensa/table/component.rb +1 -0
- data/app/components/mensa/table/component_controller.js +524 -76
- data/app/components/mensa/table_row/component.css +6 -0
- data/app/components/mensa/table_row/component.html.slim +8 -3
- data/app/components/mensa/table_row/component.rb +1 -1
- data/app/components/mensa/view/component.css +97 -29
- data/app/components/mensa/view/component.html.slim +23 -10
- data/app/components/mensa/view/component.rb +5 -0
- data/app/components/mensa/views/component.css +106 -13
- data/app/components/mensa/views/component.html.slim +51 -17
- data/app/components/mensa/views/component_controller.js +245 -20
- data/app/controllers/mensa/application_controller.rb +1 -1
- data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
- data/app/controllers/mensa/tables/exports_controller.rb +96 -0
- data/app/controllers/mensa/tables/filters_controller.rb +6 -2
- data/app/controllers/mensa/tables/views_controller.rb +108 -0
- data/app/controllers/mensa/tables_controller.rb +5 -14
- data/app/helpers/mensa/application_helper.rb +4 -1
- data/app/javascript/mensa/application.js +2 -2
- data/app/javascript/mensa/controllers/application_controller.js +5 -21
- data/app/javascript/mensa/controllers/index.js +16 -7
- data/app/jobs/mensa/export_job.rb +77 -85
- data/app/models/mensa/export.rb +93 -0
- data/app/tables/mensa/action.rb +3 -1
- data/app/tables/mensa/base.rb +103 -17
- data/app/tables/mensa/batch_action.rb +27 -0
- data/app/tables/mensa/cell.rb +21 -6
- data/app/tables/mensa/column.rb +30 -25
- data/app/tables/mensa/config/action_dsl.rb +1 -1
- data/app/tables/mensa/config/batch_dsl.rb +13 -0
- data/app/tables/mensa/config/column_dsl.rb +1 -0
- data/app/tables/mensa/config/dsl_logic.rb +8 -4
- data/app/tables/mensa/config/filter_dsl.rb +4 -1
- data/app/tables/mensa/config/render_dsl.rb +1 -1
- data/app/tables/mensa/config/table_dsl.rb +14 -4
- data/app/tables/mensa/config/view_dsl.rb +2 -0
- data/app/tables/mensa/config_readers.rb +34 -3
- data/app/tables/mensa/filter.rb +94 -14
- data/app/tables/mensa/row.rb +1 -1
- data/app/tables/mensa/scope.rb +25 -13
- data/app/views/mensa/exports/_badge.html.slim +5 -0
- data/app/views/mensa/exports/_dialog.html.slim +42 -0
- data/app/views/mensa/exports/_list.html.slim +29 -0
- data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
- data/app/views/mensa/tables/show.html.slim +2 -0
- data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
- data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
- data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
- data/bin/setup +1 -1
- data/config/locales/en.yml +45 -1
- data/config/locales/nl.yml +46 -1
- data/config/routes.rb +7 -0
- data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
- data/docs/columns.png +0 -0
- data/docs/export.png +0 -0
- data/docs/filters.png +0 -0
- data/docs/table.png +0 -0
- data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
- data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
- data/lib/mensa/configuration.rb +35 -15
- data/lib/mensa/engine.rb +15 -10
- data/lib/mensa/version.rb +1 -1
- data/lib/mensa.rb +2 -2
- data/lib/tasks/mensa_tasks.rake +1 -1
- data/mensa.gemspec +3 -2
- data/mise.toml +8 -0
- data/package-lock.json +0 -7
- metadata +60 -15
- data/app/components/mensa/filter/component_controller.js +0 -12
- data/app/components/mensa/filter_list/component.css +0 -14
- data/app/components/mensa/filter_list/component.html.slim +0 -14
- data/app/components/mensa/filter_list/component_controller.js +0 -14
- /data/{rubocop.yml → .rubocop.yml} +0 -0
|
@@ -1,46 +1,114 @@
|
|
|
1
|
-
@import
|
|
1
|
+
@import "paging.css";
|
|
2
2
|
|
|
3
|
-
.mensa-
|
|
4
|
-
|
|
3
|
+
.mensa-batch-bar {
|
|
4
|
+
/* Sits on top of the thead row. Positioned relative to the .overflow-y-auto.relative
|
|
5
|
+
wrapper, which starts flush with the top of the table. top/left/right cover the
|
|
6
|
+
full width without any JS width-setting. py-2 + h-4 content mirrors the exact
|
|
7
|
+
height of the header row (same py-2 padding + h-4 container as thead th). */
|
|
8
|
+
position: absolute;
|
|
9
|
+
top: 0;
|
|
10
|
+
left: 0;
|
|
11
|
+
z-index: 10;
|
|
12
|
+
@apply pt-3 pb-2 bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600;
|
|
13
|
+
|
|
14
|
+
&__content {
|
|
15
|
+
/* h-4 matches the .container height used inside header th cells.
|
|
16
|
+
pl-3 aligns the checkbox with the checkbox column (w-8, pl-4). */
|
|
17
|
+
@apply h-4 flex items-center gap-2 pl-4;
|
|
18
|
+
}
|
|
5
19
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
&__count {
|
|
21
|
+
@apply text-xs font-medium text-gray-700 dark:text-gray-200 mr-1;
|
|
22
|
+
}
|
|
9
23
|
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
&__button {
|
|
25
|
+
@apply inline-flex items-center gap-1 rounded px-3 py-1 text-xs font-medium text-gray-700 dark:text-gray-200 ring-1 ring-inset ring-gray-300 dark:ring-gray-500 bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none transition-colors cursor-pointer leading-none;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
.mensa-table {
|
|
30
|
+
@apply bg-white dark:bg-gray-500 dark:border-gray-700;
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
.mensa-table__checkbox-col {
|
|
33
|
+
@apply w-8;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
|
-
|
|
36
|
+
.mensa-table__select-all {
|
|
37
|
+
@apply h-4 w-4 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 cursor-pointer;
|
|
38
|
+
appearance: none;
|
|
39
|
+
|
|
40
|
+
&:hover {
|
|
41
|
+
@apply border-gray-500 dark:border-gray-400;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&:checked {
|
|
45
|
+
&:hover {
|
|
46
|
+
@apply bg-gray-700 border-gray-700 dark:bg-gray-200 dark:border-gray-200;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&:indeterminate {
|
|
51
|
+
&:hover {
|
|
52
|
+
@apply bg-gray-700 border-gray-700 dark:bg-gray-200 dark:border-gray-200;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
20
55
|
|
|
21
|
-
|
|
56
|
+
&:checked {
|
|
57
|
+
@apply bg-gray-900 border-gray-900 dark:bg-white dark:border-white;
|
|
58
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='white'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
|
|
59
|
+
background-size: 100% 100%;
|
|
60
|
+
background-repeat: no-repeat;
|
|
61
|
+
background-position: center;
|
|
62
|
+
}
|
|
22
63
|
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
&:indeterminate {
|
|
65
|
+
@apply bg-gray-900 border-gray-900 dark:bg-white dark:border-white;
|
|
66
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");
|
|
67
|
+
background-size: 100% 100%;
|
|
68
|
+
background-repeat: no-repeat;
|
|
69
|
+
background-position: center;
|
|
70
|
+
}
|
|
25
71
|
|
|
26
|
-
&:
|
|
27
|
-
|
|
72
|
+
&:focus {
|
|
73
|
+
@apply outline-none ring-2 ring-offset-1 ring-gray-700 dark:ring-gray-300;
|
|
28
74
|
}
|
|
29
|
-
|
|
75
|
+
}
|
|
30
76
|
|
|
31
|
-
|
|
32
|
-
@apply
|
|
33
|
-
}
|
|
77
|
+
.badge {
|
|
78
|
+
@apply bg-primary-100 text-primary-600 hidden ml-3 rounded-full text-xs font-medium md:inline-block py-0.5 px-2.5;
|
|
34
79
|
}
|
|
35
|
-
}
|
|
36
80
|
|
|
37
|
-
&__condensed {
|
|
38
81
|
table {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
@apply min-w-full divide-y divide-gray-50 dark:divide-gray-800 border-0;
|
|
83
|
+
|
|
84
|
+
thead {
|
|
85
|
+
@apply bg-gray-100 top-0 dark:bg-gray-700 dark:font-medium dark:text-gray-400 dark:lowercase;
|
|
86
|
+
|
|
87
|
+
/* tr and th moved to header */
|
|
88
|
+
|
|
89
|
+
.mensa-table__checkbox-col {
|
|
90
|
+
@apply pr-2;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
tbody {
|
|
95
|
+
@apply divide-y divide-gray-200 dark:divide-gray-600;
|
|
96
|
+
|
|
97
|
+
tr {
|
|
98
|
+
@apply cursor-pointer bg-white dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
td {
|
|
102
|
+
@apply px-2 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400;
|
|
103
|
+
|
|
104
|
+
&:first-child {
|
|
105
|
+
@apply pl-4;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&:last-child {
|
|
109
|
+
@apply pr-4;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
42
112
|
}
|
|
43
|
-
}
|
|
44
113
|
}
|
|
45
|
-
|
|
46
|
-
}
|
|
114
|
+
}
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
.overflow-y-auto
|
|
1
|
+
.overflow-y-auto.relative data-mensa-table-target="view" data-controller="mensa-selection" data-mensa-selection-batch-url-value=(table.batch_actions? ? helpers.mensa.table_batch_actions_path(table.name) : "")
|
|
2
|
+
- if table.batch_actions?
|
|
3
|
+
.mensa-batch-bar.hidden data-mensa-selection-target="batchBar"
|
|
4
|
+
.mensa-batch-bar__content
|
|
5
|
+
input.mensa-table__select-all type="checkbox" data-action="click->mensa-selection#deselectAll" data-mensa-selection-target="batchAllCheckbox"
|
|
6
|
+
span.mensa-batch-bar__count data-mensa-selection-target="selectedCount"
|
|
7
|
+
- table.batch_actions.each do |batch_action|
|
|
8
|
+
button.mensa-batch-bar__button type="button" data-action="click->mensa-selection#executeBatch" data-mensa-selection-batch-action-param=batch_action.name.to_s
|
|
9
|
+
= batch_action.title
|
|
2
10
|
table.w-full
|
|
3
11
|
- if table.show_header?
|
|
4
12
|
thead
|
|
5
13
|
tr
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
- if table. batch_actions?
|
|
15
|
+
th.mensa-table__checkbox-col
|
|
16
|
+
.container
|
|
17
|
+
input.mensa-table__select-all type="checkbox" data-action="change->mensa-selection#toggleAll" data-mensa-selection-target="headerCheckbox"
|
|
18
|
+
- if table.actions? && Mensa.config.row_actions_position == :front
|
|
19
|
+
th Actions
|
|
20
|
+
= render(Mensa::Header::Component.with_collection(table.display_columns, table: table))
|
|
21
|
+
- if table.actions? && Mensa.config.row_actions_position == :back
|
|
22
|
+
th Actions
|
|
10
23
|
tbody
|
|
11
24
|
= render(Mensa::TableRow::Component.with_collection(table.rows, table: table))
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
- if table.pagy_details&.count == 0
|
|
26
|
+
= render Mensa::EmptyState::Component.new(table: table)
|
|
27
|
+
- elsif table.pagy_details&.last > 1
|
|
14
28
|
.paging
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
== table.pagy_details.series_nav(anchor_string: 'data-turbo-frame="_self"')
|
|
29
|
+
span = t("mensa.paging.info", model: model_name_plural, from: table.pagy_details.from, to: table.pagy_details.to, count: table.pagy_details.count)
|
|
30
|
+
== table.pagy_details.series_nav(anchor_string: 'data-turbo-frame="_self"')
|
|
@@ -1,22 +1,115 @@
|
|
|
1
1
|
.mensa-table {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
&__views {
|
|
3
|
+
@apply relative flex-none;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
&__trigger {
|
|
6
|
+
@apply h-full flex items-center gap-1.5 px-3 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 whitespace-nowrap cursor-pointer transition-colors;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
&__trigger-icon {
|
|
10
|
+
@apply text-gray-500 dark:text-gray-400 text-xs;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&__dropdown {
|
|
14
|
+
@apply absolute left-0 top-full z-20 mt-1 min-w-[12rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&__option {
|
|
18
|
+
@apply flex items-center justify-between px-1 hover:bg-gray-50 dark:hover:bg-gray-700;
|
|
19
|
+
|
|
20
|
+
&-btn {
|
|
21
|
+
@apply flex items-center gap-2 flex-1 px-2 py-1.5 text-sm text-left text-gray-700 dark:text-gray-200 cursor-pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&-check {
|
|
25
|
+
@apply text-primary-600 text-xs w-4;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&-menu {
|
|
29
|
+
@apply p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 rounded cursor-pointer opacity-0 transition-opacity;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:hover &-menu {
|
|
33
|
+
@apply opacity-100;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&-system {
|
|
37
|
+
@apply p-1.5 text-gray-400 opacity-0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:hover &-system {
|
|
41
|
+
@apply p-1.5 text-gray-300 opacity-100;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&__submenu {
|
|
46
|
+
@apply absolute z-30 min-w-[11rem] rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 py-1;
|
|
47
|
+
|
|
48
|
+
&-item {
|
|
49
|
+
@apply flex items-center gap-2.5 w-full px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer;
|
|
50
|
+
|
|
51
|
+
i {
|
|
52
|
+
@apply w-4 text-gray-400;
|
|
53
|
+
}
|
|
8
54
|
|
|
9
|
-
|
|
10
|
-
|
|
55
|
+
&--danger {
|
|
56
|
+
@apply text-red-600 dark:text-red-400;
|
|
57
|
+
|
|
58
|
+
i {
|
|
59
|
+
@apply text-red-500;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&__rename-dialog {
|
|
66
|
+
@apply w-80 max-w-[90vw] rounded-xl border-0 p-5 bg-white dark:bg-gray-800 shadow-2xl ring-1 ring-black/5 dark:ring-white/10;
|
|
67
|
+
|
|
68
|
+
&::backdrop {
|
|
69
|
+
@apply bg-gray-900/40;
|
|
70
|
+
backdrop-filter: blur(2px);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&__rename-form {
|
|
75
|
+
@apply flex flex-col gap-4;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&__rename-title {
|
|
79
|
+
@apply text-base font-semibold text-gray-900 dark:text-gray-100;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&__rename-label {
|
|
83
|
+
@apply flex flex-col gap-1.5 text-sm font-medium text-gray-700 dark:text-gray-300;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
input&__rename-input {
|
|
87
|
+
@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;
|
|
88
|
+
border-radius: 0.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&__rename-actions {
|
|
92
|
+
@apply flex justify-end gap-2;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&__rename-btn {
|
|
96
|
+
@apply rounded-lg px-3 py-1.5 text-sm font-semibold shadow-sm focus:outline-none transition-colors;
|
|
97
|
+
|
|
98
|
+
&--secondary {
|
|
99
|
+
@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;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
&--primary {
|
|
103
|
+
@apply bg-primary-600 text-white hover:bg-primary-500;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
11
106
|
}
|
|
12
|
-
}
|
|
13
107
|
}
|
|
14
108
|
|
|
15
|
-
.tippy-box[data-theme~=
|
|
16
|
-
|
|
109
|
+
.tippy-box[data-theme~="mensa"] {
|
|
110
|
+
@apply bg-gray-200 text-gray-800 dark:text-gray-400;
|
|
17
111
|
}
|
|
18
112
|
|
|
19
|
-
.tippy-box[data-theme~=
|
|
20
|
-
|
|
113
|
+
.tippy-box[data-theme~="mensa"] .tippy-arrow {
|
|
114
|
+
@apply text-gray-200;
|
|
21
115
|
}
|
|
22
|
-
|
|
@@ -1,18 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
| Select a tab
|
|
5
|
-
select#tabs.block.w-full.rounded-md.border-gray-300.focus:border-indigo-500.focus:ring-indigo-500[name="tabs"]
|
|
6
|
-
- table.all_views.each.with_index do |view, index|
|
|
7
|
-
option = view.name
|
|
8
|
-
.hidden.sm:block
|
|
9
|
-
nav.flex.justify-between[aria-label="Tabs"]
|
|
10
|
-
.flex.space-x-2.overflow-x-auto.whitespace-nowrap.scroll-p-0
|
|
11
|
-
- table.all_views.each.with_index do |view, index|
|
|
12
|
-
= link_to(table.path(table_view_id: view.id, turbo_frame_id: table.table_id), "data-turbo-frame": table.table_id, class: "view #{(view.id == table.table_view&.id || view.id == :all && table.table_view.blank?) ? 'selected' : ''}", data: {"mensa-views-target": "view", action: "mensa-views#select", "tippy-content": view.description}) do
|
|
13
|
-
= view.name
|
|
1
|
+
- current_view = table.all_views.find { |v| v.id == table.table_view&.id } || table.all_views.first
|
|
2
|
+
- current_view_name = current_view&.name || t(".all", default: "All")
|
|
3
|
+
- current_view_id = current_view&.id || ""
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
=
|
|
5
|
+
.mensa-table__views id="mensa-views-#{table.table_id}" data-mensa-table-target="views" data-controller="mensa-views" data-mensa-views-mensa-filter-pill-list-outlet="#mensa-filter-pill-list-#{table.table_id}" data-mensa-views-table-id-value=table.table_id data-mensa-views-views-url-value=helpers.mensa.table_views_path(table.name)
|
|
6
|
+
button.mensa-table__views__trigger type="button" data-action="mensa-views#toggleDropdown" data-mensa-views-target="trigger" aria-haspopup="listbox"
|
|
7
|
+
span.mensa-table__views__trigger-label data-mensa-views-target="triggerLabel"
|
|
8
|
+
= current_view_name
|
|
9
|
+
i.fa-solid.fa-sort.mensa-table__views__trigger-icon
|
|
10
|
+
|
|
11
|
+
.mensa-table__views__dropdown.hidden data-mensa-views-target="dropdown"
|
|
12
|
+
ul role="listbox"
|
|
13
|
+
- table.all_views.each do |view|
|
|
14
|
+
- is_selected = view.id == table.table_view&.id || (view.id == :default && table.table_view.blank?)
|
|
15
|
+
- is_user_view = view.id.is_a?(String) && view.id.match?(/\A[0-9a-f-]{32,}\z/i)
|
|
16
|
+
li.mensa-table__views__option[role="option" data-view-id=view.id data-view-name=view.name data-mensa-views-target="view"]
|
|
17
|
+
button.mensa-table__views__option-btn[type="button" data-action="mensa-views#select" data-view-id=view.id]
|
|
18
|
+
i.mensa-table__views__option-check.fa-solid.fa-check class=("invisible" unless is_selected)
|
|
19
|
+
span = view.name
|
|
20
|
+
- if is_user_view
|
|
21
|
+
button.mensa-table__views__option-menu[type="button" data-action="mensa-views#toggleSubmenu" data-view-id=view.id title="View options"]
|
|
22
|
+
i.fa-solid.fa-ellipsis
|
|
23
|
+
- else
|
|
24
|
+
.mensa-table__views__option-system
|
|
25
|
+
i.fa-solid.fa-ban
|
|
26
|
+
|
|
27
|
+
/ Per-view submenu (shown via JS positioning)
|
|
28
|
+
.mensa-table__views__submenu.hidden data-mensa-views-target="submenu"
|
|
29
|
+
button.mensa-table__views__submenu-item[type="button" data-action="mensa-views#renameView"]
|
|
30
|
+
i.fa-solid.fa-pencil
|
|
31
|
+
= t(".rename_view", default: "Rename view")
|
|
32
|
+
button.mensa-table__views__submenu-item[type="button" data-action="mensa-views#duplicateView"]
|
|
33
|
+
i.fa-solid.fa-copy
|
|
34
|
+
= t(".duplicate_view", default: "Duplicate view")
|
|
35
|
+
button.mensa-table__views__submenu-item.mensa-table__views__submenu-item--danger[type="button" data-action="mensa-views#deleteView"]
|
|
36
|
+
i.fa-solid.fa-trash
|
|
37
|
+
= t(".delete_view", default: "Delete view")
|
|
38
|
+
|
|
39
|
+
/ Rename dialog
|
|
40
|
+
dialog.mensa-table__views__rename-dialog data-mensa-views-target="renameDialog" data-action="click->mensa-views#renameDialogBackdrop"
|
|
41
|
+
form.mensa-table__views__rename-form data-action="submit->mensa-views#confirmRename"
|
|
42
|
+
input type="hidden" data-mensa-views-target="renameViewId"
|
|
43
|
+
h3.mensa-table__views__rename-title
|
|
44
|
+
= t(".rename_view", default: "Rename view")
|
|
45
|
+
label.mensa-table__views__rename-label
|
|
46
|
+
= t(".view_name", default: "Name")
|
|
47
|
+
input.mensa-table__views__rename-input[type="text" required=true placeholder=t(".view_name_placeholder", default: "View name") data-mensa-views-target="renameInput"]
|
|
48
|
+
.mensa-table__views__rename-actions
|
|
49
|
+
button.mensa-table__views__rename-btn.mensa-table__views__rename-btn--secondary[type="button" data-action="mensa-views#cancelRename"]
|
|
50
|
+
= t(".cancel", default: "Cancel")
|
|
51
|
+
button.mensa-table__views__rename-btn.mensa-table__views__rename-btn--primary[type="submit"]
|
|
52
|
+
= t(".rename", default: "Rename")
|
|
@@ -1,22 +1,247 @@
|
|
|
1
|
-
import ApplicationController from
|
|
2
|
-
import
|
|
1
|
+
import ApplicationController from "mensa/controllers/application_controller";
|
|
2
|
+
import { patch, post, destroy } from "@rails/request.js";
|
|
3
3
|
|
|
4
4
|
export default class ViewsComponentController extends ApplicationController {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
static targets = ["trigger", "triggerLabel", "dropdown", "view", "submenu", "renameDialog", "renameInput", "renameViewId"];
|
|
6
|
+
static outlets = ["mensa-filter-pill-list"];
|
|
7
|
+
static values = {
|
|
8
|
+
tableId: String,
|
|
9
|
+
viewsUrl: String,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this._activeSubmenuViewId = null;
|
|
14
|
+
this._outsideClickHandler = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this._unbindOutsideClick();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toggleDropdown(event) {
|
|
22
|
+
event.stopPropagation();
|
|
23
|
+
const isOpen = !this.dropdownTarget.classList.contains("hidden");
|
|
24
|
+
if (isOpen) {
|
|
25
|
+
this._closeDropdown();
|
|
26
|
+
} else {
|
|
27
|
+
this._openDropdown();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
select(event) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
event.stopPropagation();
|
|
34
|
+
|
|
35
|
+
const selected = event.currentTarget;
|
|
36
|
+
const viewId = selected.dataset.viewId || "";
|
|
37
|
+
|
|
38
|
+
this.viewTargets.forEach((el) => {
|
|
39
|
+
const btn = el.querySelector("[data-action='mensa-views#select']");
|
|
40
|
+
const check = el.querySelector(".mensa-table__views__option-check");
|
|
41
|
+
const isThis = el.dataset.viewId === viewId;
|
|
42
|
+
check?.classList.toggle("invisible", !isThis);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const viewName = event.currentTarget.querySelector("span")?.textContent?.trim() || "";
|
|
46
|
+
if (this.hasTriggerLabelTarget) {
|
|
47
|
+
this.triggerLabelTarget.textContent = viewName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this._closeDropdown();
|
|
51
|
+
|
|
52
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
53
|
+
this.mensaFilterPillListOutlet.viewSelected(viewId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toggleSubmenu(event) {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
|
|
61
|
+
const viewId = event.currentTarget.dataset.viewId;
|
|
62
|
+
const optionEl = event.currentTarget.closest("[data-mensa-views-target='view']");
|
|
63
|
+
|
|
64
|
+
if (this._activeSubmenuViewId === viewId && !this.submenuTarget.classList.contains("hidden")) {
|
|
65
|
+
this._closeSubmenu();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this._activeSubmenuViewId = viewId;
|
|
70
|
+
this.submenuTarget.dataset.viewId = viewId;
|
|
71
|
+
|
|
72
|
+
// Position the submenu next to the clicked button
|
|
73
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
74
|
+
const dropdownRect = this.dropdownTarget.getBoundingClientRect();
|
|
75
|
+
this.submenuTarget.style.top = `${rect.top - dropdownRect.top}px`;
|
|
76
|
+
this.submenuTarget.style.left = `${dropdownRect.width}px`;
|
|
77
|
+
|
|
78
|
+
this.submenuTarget.classList.remove("hidden");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
renameView(event) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const viewId = this.submenuTarget.dataset.viewId;
|
|
84
|
+
const viewEl = this.viewTargets.find((el) => el.dataset.viewId === viewId);
|
|
85
|
+
const viewName = viewEl?.dataset.viewName || "";
|
|
86
|
+
|
|
87
|
+
this._closeSubmenu();
|
|
88
|
+
this._closeDropdown();
|
|
89
|
+
|
|
90
|
+
if (this.hasRenameDialogTarget) {
|
|
91
|
+
this.renameViewIdTarget.value = viewId;
|
|
92
|
+
this.renameInputTarget.value = viewName;
|
|
93
|
+
if (typeof this.renameDialogTarget.showModal === "function") {
|
|
94
|
+
this.renameDialogTarget.showModal();
|
|
95
|
+
} else {
|
|
96
|
+
this.renameDialogTarget.setAttribute("open", "");
|
|
97
|
+
}
|
|
98
|
+
this.renameInputTarget.select();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
cancelRename(event) {
|
|
103
|
+
if (event) event.preventDefault();
|
|
104
|
+
this._closeRenameDialog();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
renameDialogBackdrop(event) {
|
|
108
|
+
if (event.target === this.renameDialogTarget) {
|
|
109
|
+
this._closeRenameDialog();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async confirmRename(event) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
const viewId = this.renameViewIdTarget.value;
|
|
116
|
+
const newName = this.renameInputTarget.value.trim();
|
|
117
|
+
if (!newName) {
|
|
118
|
+
this.renameInputTarget.reportValidity();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const turboFrameId = this._turboFrameId();
|
|
123
|
+
const response = await patch(`${this.viewsUrlValue}/${viewId}`, {
|
|
124
|
+
body: JSON.stringify({ name: newName, turbo_frame_id: turboFrameId }),
|
|
125
|
+
contentType: "application/json",
|
|
126
|
+
responseKind: "turbo-stream",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
this._closeRenameDialog();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async duplicateView(event) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const viewId = this.submenuTarget.dataset.viewId;
|
|
137
|
+
const viewEl = this.viewTargets.find((el) => el.dataset.viewId === viewId);
|
|
138
|
+
const viewName = viewEl?.dataset.viewName || "";
|
|
139
|
+
|
|
140
|
+
this._closeSubmenu();
|
|
141
|
+
this._closeDropdown();
|
|
142
|
+
|
|
143
|
+
// Read the current state from the filter pill list outlet
|
|
144
|
+
let state = { filters: {}, query: "", order: {} };
|
|
145
|
+
if (this.hasMensaFilterPillListOutlet) {
|
|
146
|
+
const outlet = this.mensaFilterPillListOutlet;
|
|
147
|
+
state = {
|
|
148
|
+
filters: outlet.collectFilters(),
|
|
149
|
+
query: outlet.loadQuery(),
|
|
150
|
+
order: outlet.loadOrder(),
|
|
151
|
+
column_order: outlet.loadColumnOrder(),
|
|
152
|
+
hidden_columns: outlet.loadHiddenColumns(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const turboFrameId = this._turboFrameId();
|
|
157
|
+
await post(this.viewsUrlValue, {
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
name: `${viewName} (copy)`,
|
|
160
|
+
...state,
|
|
161
|
+
turbo_frame_id: turboFrameId,
|
|
162
|
+
}),
|
|
163
|
+
contentType: "application/json",
|
|
164
|
+
responseKind: "turbo-stream",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async deleteView(event) {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
const viewId = this.submenuTarget.dataset.viewId;
|
|
171
|
+
const viewEl = this.viewTargets.find((el) => el.dataset.viewId === viewId);
|
|
172
|
+
const viewName = viewEl?.dataset.viewName || "this view";
|
|
173
|
+
|
|
174
|
+
this._closeSubmenu();
|
|
175
|
+
this._closeDropdown();
|
|
176
|
+
|
|
177
|
+
if (!confirm(`Delete "${viewName}"?`)) return;
|
|
178
|
+
|
|
179
|
+
const turboFrameId = this._turboFrameId();
|
|
180
|
+
await destroy(`${this.viewsUrlValue}/${viewId}`, {
|
|
181
|
+
body: JSON.stringify({ turbo_frame_id: turboFrameId }),
|
|
182
|
+
contentType: "application/json",
|
|
183
|
+
responseKind: "turbo-stream",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Called from outside (e.g. after a view is saved/updated) to update the trigger label
|
|
188
|
+
updateSelectedView(viewId, viewName) {
|
|
189
|
+
this.viewTargets.forEach((el) => {
|
|
190
|
+
const check = el.querySelector(".mensa-table__views__option-check");
|
|
191
|
+
check?.classList.toggle("invisible", el.dataset.viewId !== viewId);
|
|
192
|
+
});
|
|
193
|
+
if (this.hasTriggerLabelTarget && viewName) {
|
|
194
|
+
this.triggerLabelTarget.textContent = viewName;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- private ---
|
|
199
|
+
|
|
200
|
+
_openDropdown() {
|
|
201
|
+
this.dropdownTarget.classList.remove("hidden");
|
|
202
|
+
this._bindOutsideClick();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_closeDropdown() {
|
|
206
|
+
this.dropdownTarget.classList.add("hidden");
|
|
207
|
+
this._closeSubmenu();
|
|
208
|
+
this._unbindOutsideClick();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_closeSubmenu() {
|
|
212
|
+
this.submenuTarget.classList.add("hidden");
|
|
213
|
+
this._activeSubmenuViewId = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_closeRenameDialog() {
|
|
217
|
+
if (!this.hasRenameDialogTarget) return;
|
|
218
|
+
if (typeof this.renameDialogTarget.close === "function") {
|
|
219
|
+
this.renameDialogTarget.close();
|
|
220
|
+
} else {
|
|
221
|
+
this.renameDialogTarget.removeAttribute("open");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_turboFrameId() {
|
|
226
|
+
const root = this.element.closest(".mensa-table");
|
|
227
|
+
const frame = root?.querySelector("turbo-frame");
|
|
228
|
+
return frame?.id || this.tableIdValue || "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_bindOutsideClick() {
|
|
232
|
+
this._unbindOutsideClick();
|
|
233
|
+
this._outsideClickHandler = (e) => {
|
|
234
|
+
if (!this.element.contains(e.target)) {
|
|
235
|
+
this._closeDropdown();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
setTimeout(() => document.addEventListener("click", this._outsideClickHandler), 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_unbindOutsideClick() {
|
|
242
|
+
if (this._outsideClickHandler) {
|
|
243
|
+
document.removeEventListener("click", this._outsideClickHandler);
|
|
244
|
+
this._outsideClickHandler = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|