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.
Files changed (122) 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/CHANGELOG.md +6 -0
  10. data/Gemfile.lock +155 -153
  11. data/Procfile +1 -1
  12. data/README.md +95 -60
  13. data/app/assets/stylesheets/mensa/application.css +14 -11
  14. data/app/components/mensa/add_filter/component.css +110 -5
  15. data/app/components/mensa/add_filter/component.html.slim +10 -12
  16. data/app/components/mensa/add_filter/component.rb +8 -2
  17. data/app/components/mensa/add_filter/component_controller.js +697 -85
  18. data/app/components/mensa/cell/component.css +9 -0
  19. data/app/components/mensa/column_customizer/component.css +40 -0
  20. data/app/components/mensa/column_customizer/component.html.slim +14 -0
  21. data/app/components/mensa/column_customizer/component.rb +13 -0
  22. data/app/components/mensa/column_customizer/component_controller.js +383 -0
  23. data/app/components/mensa/control_bar/component.css +127 -4
  24. data/app/components/mensa/control_bar/component.html.slim +41 -14
  25. data/app/components/mensa/control_bar/component.rb +2 -6
  26. data/app/components/mensa/empty_state/component.css +20 -0
  27. data/app/components/mensa/empty_state/component.html.slim +7 -0
  28. data/app/components/mensa/empty_state/component.rb +18 -0
  29. data/app/components/mensa/filter_pill/component.css +23 -0
  30. data/app/components/mensa/filter_pill/component.html.slim +9 -0
  31. data/app/components/mensa/filter_pill/component.rb +24 -0
  32. data/app/components/mensa/filter_pill/component_controller.js +52 -0
  33. data/app/components/mensa/filter_pill_list/component.css +63 -0
  34. data/app/components/mensa/filter_pill_list/component.html.slim +11 -0
  35. data/app/components/mensa/{filter_list → filter_pill_list}/component.rb +1 -1
  36. data/app/components/mensa/filter_pill_list/component_controller.js +749 -0
  37. data/app/components/mensa/header/component.css +41 -43
  38. data/app/components/mensa/header/component.html.slim +7 -7
  39. data/app/components/mensa/header/component.rb +1 -1
  40. data/app/components/mensa/row_action/component.html.slim +2 -2
  41. data/app/components/mensa/row_action/component.rb +1 -1
  42. data/app/components/mensa/search/component.css +68 -9
  43. data/app/components/mensa/search/component.html.slim +19 -15
  44. data/app/components/mensa/search/component.rb +1 -1
  45. data/app/components/mensa/search/component_controller.js +39 -49
  46. data/app/components/mensa/selection/component_controller.js +147 -0
  47. data/app/components/mensa/table/component.css +28 -0
  48. data/app/components/mensa/table/component.html.slim +9 -6
  49. data/app/components/mensa/table/component.rb +1 -0
  50. data/app/components/mensa/table/component_controller.js +524 -76
  51. data/app/components/mensa/table_row/component.css +6 -0
  52. data/app/components/mensa/table_row/component.html.slim +8 -3
  53. data/app/components/mensa/table_row/component.rb +1 -1
  54. data/app/components/mensa/view/component.css +97 -29
  55. data/app/components/mensa/view/component.html.slim +23 -10
  56. data/app/components/mensa/view/component.rb +5 -0
  57. data/app/components/mensa/views/component.css +106 -13
  58. data/app/components/mensa/views/component.html.slim +51 -17
  59. data/app/components/mensa/views/component_controller.js +245 -20
  60. data/app/controllers/mensa/application_controller.rb +1 -1
  61. data/app/controllers/mensa/tables/batch_actions_controller.rb +24 -0
  62. data/app/controllers/mensa/tables/exports_controller.rb +96 -0
  63. data/app/controllers/mensa/tables/filters_controller.rb +6 -2
  64. data/app/controllers/mensa/tables/views_controller.rb +108 -0
  65. data/app/controllers/mensa/tables_controller.rb +5 -14
  66. data/app/helpers/mensa/application_helper.rb +4 -1
  67. data/app/javascript/mensa/application.js +2 -2
  68. data/app/javascript/mensa/controllers/application_controller.js +5 -21
  69. data/app/javascript/mensa/controllers/index.js +16 -7
  70. data/app/jobs/mensa/export_job.rb +77 -85
  71. data/app/models/mensa/export.rb +93 -0
  72. data/app/tables/mensa/action.rb +3 -1
  73. data/app/tables/mensa/base.rb +103 -17
  74. data/app/tables/mensa/batch_action.rb +27 -0
  75. data/app/tables/mensa/cell.rb +21 -6
  76. data/app/tables/mensa/column.rb +30 -25
  77. data/app/tables/mensa/config/action_dsl.rb +1 -1
  78. data/app/tables/mensa/config/batch_dsl.rb +13 -0
  79. data/app/tables/mensa/config/column_dsl.rb +1 -0
  80. data/app/tables/mensa/config/dsl_logic.rb +8 -4
  81. data/app/tables/mensa/config/filter_dsl.rb +4 -1
  82. data/app/tables/mensa/config/render_dsl.rb +1 -1
  83. data/app/tables/mensa/config/table_dsl.rb +14 -4
  84. data/app/tables/mensa/config/view_dsl.rb +2 -0
  85. data/app/tables/mensa/config_readers.rb +34 -3
  86. data/app/tables/mensa/filter.rb +94 -14
  87. data/app/tables/mensa/row.rb +1 -1
  88. data/app/tables/mensa/scope.rb +25 -13
  89. data/app/views/mensa/exports/_badge.html.slim +5 -0
  90. data/app/views/mensa/exports/_dialog.html.slim +42 -0
  91. data/app/views/mensa/exports/_list.html.slim +29 -0
  92. data/app/views/mensa/tables/filters/show.turbo_stream.slim +34 -6
  93. data/app/views/mensa/tables/show.html.slim +2 -0
  94. data/app/views/mensa/tables/show.turbo_stream.slim +1 -1
  95. data/app/views/mensa/tables/views/create.turbo_stream.slim +11 -0
  96. data/app/views/mensa/tables/views/destroy.turbo_stream.slim +11 -0
  97. data/app/views/mensa/tables/views/update.turbo_stream.slim +11 -0
  98. data/bin/setup +1 -1
  99. data/config/locales/en.yml +45 -1
  100. data/config/locales/nl.yml +46 -1
  101. data/config/routes.rb +7 -0
  102. data/db/migrate/20260604120000_create_mensa_exports.rb +25 -0
  103. data/docs/columns.png +0 -0
  104. data/docs/export.png +0 -0
  105. data/docs/filters.png +0 -0
  106. data/docs/table.png +0 -0
  107. data/lib/generators/mensa/tailwind_config_generator.rb +3 -3
  108. data/lib/generators/mensa/templates/config/initializers/mensa.rb +1 -1
  109. data/lib/mensa/configuration.rb +35 -15
  110. data/lib/mensa/engine.rb +15 -10
  111. data/lib/mensa/version.rb +1 -1
  112. data/lib/mensa.rb +2 -2
  113. data/lib/tasks/mensa_tasks.rake +1 -1
  114. data/mensa.gemspec +3 -2
  115. data/mise.toml +8 -0
  116. data/package-lock.json +0 -7
  117. metadata +60 -15
  118. data/app/components/mensa/filter/component_controller.js +0 -12
  119. data/app/components/mensa/filter_list/component.css +0 -14
  120. data/app/components/mensa/filter_list/component.html.slim +0 -14
  121. data/app/components/mensa/filter_list/component_controller.js +0 -14
  122. /data/{rubocop.yml → .rubocop.yml} +0 -0
@@ -0,0 +1,20 @@
1
+ .mensa-empty-state {
2
+ @apply flex flex-col items-center justify-center py-16 px-6 text-center bg-white dark:bg-gray-800;
3
+
4
+ &__icon {
5
+ @apply text-gray-300 dark:text-gray-600 mb-6;
6
+ font-size: 4.5rem;
7
+ }
8
+
9
+ &__title {
10
+ @apply text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2;
11
+ }
12
+
13
+ &__subtitle {
14
+ @apply text-sm text-gray-500 dark:text-gray-400 mb-8 max-w-sm;
15
+ }
16
+
17
+ &__button {
18
+ @apply inline-flex items-center px-6 py-2.5 rounded-full text-sm font-semibold bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-gray-300 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2;
19
+ }
20
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mensa
4
+ module FilterPill
5
+ class Component < ::Mensa::ApplicationComponent
6
+ with_collection_parameter :filter
7
+
8
+ attr_reader :filter
9
+
10
+ def initialize(filter:)
11
+ @filter = filter
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
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ import ApplicationController from "mensa/controllers/application_controller";
2
+
3
+ export default class FilterPillComponentController extends ApplicationController {
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
+ }
@@ -0,0 +1,63 @@
1
+ .mensa-table {
2
+ &__search-bar {
3
+ @apply flex items-center gap-1.5 flex-1 min-w-0 h-full px-2;
4
+
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;
26
+
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
+ }
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
+ }
55
+
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
+ }
@@ -0,0 +1,11 @@
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mensa
4
- module FilterList
4
+ module FilterPillList
5
5
  class Component < ::Mensa::ApplicationComponent
6
6
  attr_reader :table
7
7