mensa 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +6 -2
  3. data/.devcontainer/compose.yaml +1 -1
  4. data/.devcontainer/devcontainer.json +31 -29
  5. data/.devcontainer/postCreate.sh +8 -0
  6. data/.devcontainer/postStart.sh +9 -0
  7. data/.gitignore +3 -1
  8. data/.zed/tasks.json +12 -0
  9. data/Gemfile.lock +155 -153
  10. data/Procfile +1 -1
  11. data/README.md +85 -60
  12. data/app/assets/stylesheets/mensa/application.css +14 -11
  13. data/app/components/mensa/add_filter/component.css +110 -5
  14. data/app/components/mensa/add_filter/component.html.slim +10 -12
  15. data/app/components/mensa/add_filter/component.rb +7 -1
  16. data/app/components/mensa/add_filter/component_controller.js +697 -83
  17. data/app/components/mensa/cell/component.css +9 -0
  18. data/app/components/mensa/column_customizer/component.css +40 -0
  19. data/app/components/mensa/column_customizer/component.html.slim +14 -0
  20. data/app/components/mensa/column_customizer/component.rb +13 -0
  21. data/app/components/mensa/column_customizer/component_controller.js +383 -0
  22. data/app/components/mensa/control_bar/component.css +127 -4
  23. data/app/components/mensa/control_bar/component.html.slim +41 -14
  24. data/app/components/mensa/control_bar/component.rb +0 -4
  25. data/app/components/mensa/empty_state/component.css +20 -0
  26. data/app/components/mensa/empty_state/component.html.slim +7 -0
  27. data/app/components/mensa/empty_state/component.rb +18 -0
  28. data/app/components/mensa/filter_pill/component.css +23 -0
  29. data/app/components/mensa/filter_pill/component.html.slim +9 -6
  30. data/app/components/mensa/filter_pill/component.rb +9 -0
  31. data/app/components/mensa/filter_pill/component_controller.js +50 -10
  32. data/app/components/mensa/filter_pill_list/component.css +58 -9
  33. data/app/components/mensa/filter_pill_list/component.html.slim +11 -8
  34. data/app/components/mensa/filter_pill_list/component_controller.js +747 -48
  35. data/app/components/mensa/header/component.css +41 -43
  36. data/app/components/mensa/header/component.html.slim +7 -7
  37. data/app/components/mensa/row_action/component.html.slim +2 -2
  38. data/app/components/mensa/search/component.css +68 -9
  39. data/app/components/mensa/search/component.html.slim +19 -15
  40. data/app/components/mensa/search/component_controller.js +39 -49
  41. data/app/components/mensa/selection/component_controller.js +147 -0
  42. data/app/components/mensa/table/component.css +28 -0
  43. data/app/components/mensa/table/component.html.slim +9 -6
  44. data/app/components/mensa/table/component.rb +1 -0
  45. data/app/components/mensa/table/component_controller.js +524 -88
  46. data/app/components/mensa/table_row/component.css +6 -0
  47. data/app/components/mensa/table_row/component.html.slim +8 -3
  48. data/app/components/mensa/view/component.css +97 -29
  49. data/app/components/mensa/view/component.html.slim +23 -10
  50. data/app/components/mensa/view/component.rb +5 -0
  51. data/app/components/mensa/views/component.css +106 -13
  52. data/app/components/mensa/views/component.html.slim +51 -17
  53. data/app/components/mensa/views/component_controller.js +245 -20
  54. data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
  55. data/app/controllers/mensa/tables/exports_controller.rb +96 -0
  56. data/app/controllers/mensa/tables/filters_controller.rb +4 -1
  57. data/app/controllers/mensa/tables/views_controller.rb +108 -0
  58. data/app/controllers/mensa/tables_controller.rb +3 -6
  59. data/app/helpers/mensa/application_helper.rb +4 -0
  60. data/app/javascript/mensa/application.js +2 -2
  61. data/app/javascript/mensa/controllers/index.js +13 -4
  62. data/app/jobs/mensa/export_job.rb +77 -84
  63. data/app/models/mensa/export.rb +93 -0
  64. data/app/tables/mensa/base.rb +103 -12
  65. data/app/tables/mensa/batch_action.rb +27 -0
  66. data/app/tables/mensa/cell.rb +15 -0
  67. data/app/tables/mensa/column.rb +15 -2
  68. data/app/tables/mensa/config/batch_dsl.rb +13 -0
  69. data/app/tables/mensa/config/column_dsl.rb +1 -0
  70. data/app/tables/mensa/config/filter_dsl.rb +4 -1
  71. data/app/tables/mensa/config/render_dsl.rb +1 -1
  72. data/app/tables/mensa/config/table_dsl.rb +12 -5
  73. data/app/tables/mensa/config/view_dsl.rb +2 -0
  74. data/app/tables/mensa/config_readers.rb +20 -1
  75. data/app/tables/mensa/filter.rb +86 -3
  76. data/app/tables/mensa/scope.rb +24 -12
  77. data/app/views/mensa/exports/_badge.html.slim +5 -0
  78. data/app/views/mensa/exports/_dialog.html.slim +42 -0
  79. data/app/views/mensa/exports/_list.html.slim +29 -0
  80. data/app/views/mensa/tables/filters/show.turbo_stream.slim +35 -8
  81. data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
  82. data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
  83. data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
  84. data/config/locales/en.yml +44 -0
  85. data/config/locales/nl.yml +45 -0
  86. data/config/routes.rb +7 -0
  87. data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
  88. data/docs/columns.png +0 -0
  89. data/docs/export.png +0 -0
  90. data/docs/filters.png +0 -0
  91. data/docs/table.png +0 -0
  92. data/lib/mensa/configuration.rb +33 -12
  93. data/lib/mensa/engine.rb +7 -2
  94. data/lib/mensa/version.rb +1 -1
  95. data/mensa.gemspec +2 -1
  96. data/mise.toml +8 -0
  97. data/package-lock.json +0 -7
  98. metadata +50 -8
@@ -0,0 +1,7 @@
1
+ .mensa-empty-state
2
+ .mensa-empty-state__icon
3
+ i class=Mensa.config.icons[:search]
4
+ h3.mensa-empty-state__title = t("mensa.empty_state.title", model: model_name_plural)
5
+ p.mensa-empty-state__subtitle = t("mensa.empty_state.subtitle")
6
+ button.mensa-empty-state__button type="button" data-action="click->mensa-table#cancelFiltersAndSearch"
7
+ = t("mensa.empty_state.clear_button")
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa
4
+ module EmptyState
5
+ class Component < ::Mensa::ApplicationComponent
6
+ attr_reader :table
7
+
8
+ def initialize(table:)
9
+ @table = table
10
+ end
11
+
12
+ # "orders", "users", etc. — used inside the translated heading.
13
+ def model_name_plural
14
+ table.model.model_name.human.pluralize.downcase
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ .mensa-filter-pill {
2
+ @apply inline-flex items-stretch text-xs;
3
+
4
+ &__chip {
5
+ @apply flex items-center gap-1 pl-2 pr-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded-l-md cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors;
6
+ }
7
+
8
+ &__column {
9
+ @apply text-gray-600 dark:text-gray-300;
10
+ }
11
+
12
+ &__operator {
13
+ @apply rounded px-1 bg-violet-100 dark:bg-violet-900/40 text-violet-700 dark:text-violet-300;
14
+ }
15
+
16
+ &__value {
17
+ @apply rounded pl-1 bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400 font-medium;
18
+ }
19
+
20
+ &__remove {
21
+ @apply inline-flex items-center justify-center px-1.5 text-gray-400 bg-gray-100 dark:bg-gray-700 rounded-r-md hover:text-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 dark:hover:text-gray-200 cursor-pointer transition-colors;
22
+ }
23
+ }
@@ -1,6 +1,9 @@
1
- .relative.mensa-filter-pill data-controller="mensa-filter-pill" data-mensa-filter-pill-column-name-value=filter.column.name data-mensa-filter-pill-value-value=filter.value data-mensa-filter-pill-operator-value=filter.operator
2
- button.relative.w-full.cursor-default.rounded-md.bg-white.dark:bg-gray-800.py-1.5.pl-3.text-left.text-gray-900.dark:text-gray-400.shadow-sm.ring-1.ring-inset.ring-gray-300.dark:ring-gray-600.focus:outline-none.focus:ring-2.focus:ring-primary-600.sm:text-sm.sm:leading-6[type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"]
3
- span.block.truncate.pr-8
4
- = filter
5
- span.pointer-events-none.absolute.inset-y-0.right-0.flex.items-center.pr-2
6
- .fa.fal.fa-xmark
1
+ .mensa-filter-pill data-controller="mensa-filter-pill" data-mensa-filter-pill-mensa-filter-pill-list-outlet="#mensa-filter-pill-list-#{filter.table.table_id}" data-mensa-filter-pill-column-name-value=filter.column.name data-mensa-filter-pill-value-value=(filter.operator_with_value? ? (filter.value.is_a?(Array) ? filter.value.to_json : filter.value) : nil ) data-mensa-filter-pill-operator-value=filter.operator data-mensa-filter-pill-operator-without-value-value=("true" unless filter.operator_with_value?) data-view-filter=("true" if view_filter?)
2
+ button.mensa-filter-pill__chip[type="button" data-action="click->mensa-filter-pill#edit"]
3
+ span.mensa-filter-pill__column = filter.column.human_name
4
+ span.mensa-filter-pill__operator = filter.operator_label
5
+ - formatted_value = filter.value.is_a?(Array) ? filter.value.join(", ") : filter.value
6
+ - if formatted_value.present? && filter.operator_with_value?
7
+ span.mensa-filter-pill__value = formatted_value
8
+ button.mensa-filter-pill__remove[type="button" title="Remove filter" data-action="click->mensa-filter-pill#remove"]
9
+ i.fa-solid.fa-xmark
@@ -10,6 +10,15 @@ module Mensa
10
10
  def initialize(filter:)
11
11
  @filter = filter
12
12
  end
13
+
14
+ def view_filter?
15
+ view = filter.table.table_view
16
+ return false unless view
17
+
18
+ view_filters = view.config&.dig(:filters) || view.config&.dig("filters") || {}
19
+ col = filter.column.name.to_s
20
+ view_filters.key?(col) || view_filters.key?(col.to_sym)
21
+ end
13
22
  end
14
23
  end
15
24
  end
@@ -1,12 +1,52 @@
1
- import ApplicationController from 'mensa/controllers/application_controller'
1
+ import ApplicationController from "mensa/controllers/application_controller";
2
2
 
3
3
  export default class FilterPillComponentController extends ApplicationController {
4
- static values = {
5
- columnName: String,
6
- operator: String,
7
- value: String,
8
- };
9
-
10
- connect() {
11
- }
12
- }
4
+ static outlets = ["mensa-filter-pill-list"];
5
+
6
+ static values = {
7
+ columnName: String,
8
+ operator: String,
9
+ value: String,
10
+ };
11
+
12
+ connect() {}
13
+
14
+ // Re-opens the value selector for this filter's column (reusing the
15
+ // add-filter popover), pre-selected to the current value. Choosing a new
16
+ // value re-requests the table via the add-filter flow.
17
+ edit(event) {
18
+ event.preventDefault();
19
+
20
+ if (!this.hasMensaFilterPillListOutlet) return;
21
+
22
+ let value = this.hasValueValue ? this.valueValue : null;
23
+ if (this.hasValueValue) {
24
+ try {
25
+ value = JSON.parse(value);
26
+ } catch {}
27
+ }
28
+
29
+ this.mensaFilterPillListOutlet.editFilter(
30
+ this.columnNameValue,
31
+ value,
32
+ this.operatorValue,
33
+ this.element,
34
+ );
35
+ }
36
+
37
+ // Removes this filter pill and re-requests the table with the remaining
38
+ // filters. The list controller reads the active filters straight from the
39
+ // DOM, so we drop our element first, then ask it to refresh.
40
+ remove(event) {
41
+ event.preventDefault();
42
+ event.stopPropagation();
43
+
44
+ const list = this.hasMensaFilterPillListOutlet
45
+ ? this.mensaFilterPillListOutlet
46
+ : null;
47
+
48
+ this.element.remove();
49
+
50
+ if (list) list.refreshFilters();
51
+ }
52
+ }
@@ -1,14 +1,63 @@
1
1
  .mensa-table {
2
- &__filters {
3
- @apply p-2 shadow-lg border-b dark:bg-gray-700 dark:border-gray-600;
2
+ &__search-bar {
3
+ @apply flex items-center gap-1.5 flex-1 min-w-0 h-full px-2;
4
4
 
5
- a.filter {
6
- @apply bg-gray-200 dark:bg-gray-500 hover:bg-gray-400 text-gray-800 rounded-md px-3 py-1.5 text-sm;
7
- }
5
+ &__pills-area {
6
+ @apply flex flex-1 flex-wrap items-center gap-1 min-w-0;
7
+ }
8
+
9
+ &__input-wrapper {
10
+ @apply relative flex items-center flex-1 min-w-[4rem];
11
+ }
12
+
13
+ &__search-icon {
14
+ @apply absolute left-2 text-gray-400 pointer-events-none -ml-4;
15
+ font-size: 0.9rem;
16
+ /* icon is after input in DOM but floats left via absolute positioning */
17
+ }
18
+
19
+ &__input {
20
+ @apply w-full bg-transparent text-xs text-gray-700 dark:text-gray-200 placeholder:text-gray-400;
21
+ /* Override @tailwindcss/forms — direct CSS beats attribute selector specificity */
22
+ border: none !important;
23
+ outline: none !important;
24
+ box-shadow: none !important;
25
+ padding: 0 0.25rem 0 1.5rem;
8
26
 
9
- a.filter.selected {
10
- @apply bg-gray-100 text-gray-600 dark:text-gray-400 hover:text-gray-800 hover:bg-gray-200;
27
+ &:focus {
28
+ border: none !important;
29
+ outline: none !important;
30
+ box-shadow: none !important;
31
+ }
32
+
33
+ /* Hide the icon that follows when input has content (placeholder not shown) */
34
+ &:not(:placeholder-shown) ~ .mensa-table__search-bar__search-icon {
35
+ display: none;
36
+ }
37
+ }
38
+
39
+ &__clear {
40
+ @apply flex-none p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer;
41
+ }
11
42
  }
43
+ }
44
+
45
+ /* Hide placeholder when visible filter pills exist.
46
+ User-added pills (no data-view-filter) are always visible.
47
+ View-filter pills are only visible when the eye toggle is showing them. */
48
+ .mensa-table__search-bar__pills-area:has(.mensa-filter-pill:not([data-view-filter="true"]))
49
+ .mensa-table__search-bar__input::placeholder,
50
+ .mensa-table:not(.mensa-table--view-filters-hidden)
51
+ .mensa-table__search-bar__pills-area:has(.mensa-filter-pill[data-view-filter="true"])
52
+ .mensa-table__search-bar__input::placeholder {
53
+ opacity: 0;
54
+ }
12
55
 
13
- }
14
- }
56
+ /* Hide search icon under the same conditions */
57
+ .mensa-table__search-bar__pills-area:has(.mensa-filter-pill:not([data-view-filter="true"]))
58
+ .mensa-table__search-bar__search-icon,
59
+ .mensa-table:not(.mensa-table--view-filters-hidden)
60
+ .mensa-table__search-bar__pills-area:has(.mensa-filter-pill[data-view-filter="true"])
61
+ .mensa-table__search-bar__search-icon {
62
+ display: none;
63
+ }
@@ -1,8 +1,11 @@
1
- .mensa-table__filters.hidden id="mensa-filter-pill-list-#{table.table_id}" data-controller="mensa-filter-pill-list" data-mensa-table-target="filterList" data-mensa-filter-pill-list-mensa-table-outlet=".mensa-table#table-#{table.table_id}" data-mensa-filter-pill-list-mensa-filter-pill-outlet=".mensa-filter-pill" data-mensa-filter-pill-list-mensa-add-filter-outlet="[data-controller=mensa-add-filter]"
2
- .block
3
- nav
4
- .flex.space-x-2.overflow-none.whitespace-nowrap.scroll-p-0[aria-label="Tabs"]
5
- / existing filters first
6
- = render Mensa::FilterPill::Component.with_collection(table.active_filters)
7
-
8
- = render Mensa::AddFilter::Component.new(table: table)
1
+ - has_filterable_columns = table.columns.any?(&:filter?)
2
+ .mensa-table__search-bar id="mensa-filter-pill-list-#{table.table_id}" data-controller="mensa-filter-pill-list" data-mensa-table-target="filterList" data-mensa-filter-pill-list-table-name-value=table.name data-mensa-filter-pill-list-mensa-table-outlet=".mensa-table#table-#{table.table_id}" data-mensa-filter-pill-list-mensa-filter-pill-outlet=".mensa-filter-pill" data-mensa-filter-pill-list-mensa-add-filter-outlet="[data-controller=mensa-add-filter]"
3
+ .mensa-table__search-bar__pills-area
4
+ = render Mensa::FilterPill::Component.with_collection(table.active_filters)
5
+ - if has_filterable_columns
6
+ = render Mensa::AddFilter::Component.new(table: table)
7
+ .mensa-table__search-bar__input-wrapper
8
+ input.mensa-table__search-bar__input[type="text" name="search_query" autocomplete="off" placeholder=t(has_filterable_columns ? ".search" : ".search_only") data-mensa-filter-pill-list-target="searchInput" data-action="input->mensa-filter-pill-list#monitorSearch keydown.enter->mensa-filter-pill-list#search keydown.esc->mensa-filter-pill-list#resetSearch focus->mensa-filter-pill-list#searchFocused keydown.down->mensa-filter-pill-list#navigateDown keydown.up->mensa-filter-pill-list#navigateUp" value=params[:query]]
9
+ i.mensa-table__search-bar__search-icon class=Mensa.config.icons[:search]
10
+ button.mensa-table__search-bar__clear.hidden[type="button" data-mensa-filter-pill-list-target="resetSearchButton" data-action="mensa-filter-pill-list#resetSearch"]
11
+ i.fas.fa-xmark