mensa 0.9.1 → 0.9.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 746ba9c00c7b9578ba6a54aed7d05cb0e3a4d97d39097f0bb20ee5741ad4c598
4
- data.tar.gz: febf42e4a305586f26bf0f8d738bec3e92125c13c900944a780a268e2a043ca8
3
+ metadata.gz: d4dc7c940f4eff95458542c2ac2d54a71df5735105019fb09d2fc4b70f588124
4
+ data.tar.gz: 6f43f89a33e27295b268b6c45c4a018599e313418b1aeb4b3ae758de63180211
5
5
  SHA512:
6
- metadata.gz: 78a2354e8bfdd6258a5b454dd83f6d42afbf0e1aebe0249833a805238a1e1a6d1b053032f51bd0a07f4b6b005423bc2d37c9253ddddefa97f2ec9391fb423b12
7
- data.tar.gz: 790b53409e027df4e5f9d06495321e7aa92008e4b297e709cda31d5044ff70fbb4ef5b7879d6f8914ee98071d37c54c3441444dd53b84e231d1a296b69262aa3
6
+ metadata.gz: 0a038582389be24ec077d6234974d30700bcef87ca6d3524abdecfed97bcb09280fcbbb46095af18fac2339243ebcf3f39e3cc54adad0e96519183e6e5435a67
7
+ data.tar.gz: 2803b2f1fa2e42b19ca2ed2aff842e7bdd2bc1315f2fef13b590010105725746f832113fec55861e8ea911eeaf18e2454b9798822d0330ad73068b472f6b7dce
data/.shipkit.yml CHANGED
@@ -1,2 +1,2 @@
1
1
  ---
2
- previous_version: 0.9.0
2
+ previous_version: 0.9.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3
4
+
5
+ - A Mensa table can be rendered inside a host `<form>`, such as a tab on an
6
+ edit page. Before, the save-view, rename-view and export dialogs (and the
7
+ delete button of each export) rendered their own `<form>`s. HTML has no
8
+ nested forms, so the outer form was closed early, its submit was silently
9
+ blocked by Mensa's empty `required` inputs, and the export options were
10
+ sent along with it. Mensa now renders no `<form>` elements, no `required`
11
+ attributes and no controls that a host form submits.
12
+ - The save-view and rename-view dialogs confirm with their button or Enter in
13
+ the name field. An empty name still shows the browser's validation message,
14
+ now from the translatable `mensa.search.component.view_name_required` and
15
+ `mensa.views.component.view_name_required` keys.
16
+ - **Behaviour change:** the export dialog's controls have a `form` attribute
17
+ that matches no element and names that include the table id (e.g.
18
+ `mensa-export-users-scope`), so they never belong to a host form and don't
19
+ group with radios elsewhere on the page. Select them with
20
+ `data-mensa-export-field="scope"` (or `export_format`, `repeat_mode`,
21
+ `repeat`) instead of `name`.
22
+ - **Behaviour change:** the search input no longer has `name="search_query"`,
23
+ and the `.mensa-table__export-dialog__delete-form` class is gone: an export
24
+ is deleted with a plain `button.mensa-table__export-dialog__delete`.
25
+ - Group header aggregates of `bigint` columns no longer show a trailing `.0`
26
+ (`3,000` instead of `3,000.0`). Postgres returns their SUM as `numeric`;
27
+ whole numbers are now shown without decimals.
28
+ - A selected view survives a page reload when the default view has filters.
29
+ Before, the table showed the selected view's rows, but the views dropdown
30
+ and the filter pills still showed the default view.
31
+ - A view filter that the user changes (another value or operator) becomes a
32
+ user filter: it's always shown, and the eye button disappears when no
33
+ unchanged view filters remain. Before, a pill was a view filter as long as
34
+ the view filtered on its column, so a changed filter could still be hidden.
35
+ - The views dropdown is positioned `fixed`, like the other popovers, so a
36
+ table with few rows (or a host element with `overflow: hidden`) no longer
37
+ clips it.
38
+
39
+ ## 0.9.2
40
+
41
+ - The table tour no longer needs the `driver.js` npm package. Mensa now ships
42
+ driver.js (1.10.1, MIT) itself in `app/components/mensa/table/driver.js` and
43
+ imports it as `mensa/components/table/driver`, so host apps that bundle
44
+ Mensa's JS with esbuild (jsbundling-rails) build again without adding
45
+ anything. In 0.9.0 and 0.9.1 those builds failed with
46
+ `Could not resolve "driver.js"`.
47
+ - driver.js is updated from 1.3.6 to 1.10.1; its CSS
48
+ (`app/assets/stylesheets/mensa/driver.css`) is updated to the same version.
49
+ - The table tour has a dark mode. It follows the host app's Tailwind dark mode
50
+ setting, like the rest of Mensa.
51
+ - The engine's importmap no longer pins `driver.js`. Host apps that import
52
+ `driver.js` themselves need their own pin or npm package.
53
+
3
54
  ## 0.8.0
4
55
 
5
56
  - **Behaviour change:** a column shows what was selected in SQL under its name
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mensa (0.9.0)
4
+ mensa (0.9.2)
5
5
  csv
6
6
  importmap-rails
7
7
  pagy (>= 43)
@@ -1,4 +1,5 @@
1
1
  @import "./driver.css";
2
+ @import "../../../components/mensa/table/tour.css";
2
3
  @import "../../../components/mensa/add_filter/component.css";
3
4
  @import "../../../components/mensa/column_customizer/component.css";
4
5
  @import "../../../components/mensa/cell/component.css";
@@ -1 +1,4 @@
1
- .driver-active .driver-overlay,.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in .2s ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in .2s}.driver-popover{all:unset;box-sizing:border-box;color:#2d2d2d;margin:0;padding:15px;border-radius:5px;min-width:250px;max-width:300px;box-shadow:0 1px 10px #0006;z-index:1000000000;position:fixed;top:0;right:0;background-color:#fff}.driver-popover *{font-family:Helvetica Neue,Inter,ui-sans-serif,"Apple Color Emoji",Helvetica,Arial,sans-serif}.driver-popover-title{font:19px/normal sans-serif;font-weight:700;display:block;position:relative;line-height:1.5;zoom:1;margin:0}.driver-popover-close-btn{all:unset;position:absolute;top:0;right:0;width:32px;height:28px;cursor:pointer;font-size:18px;font-weight:500;color:#d2d2d2;z-index:1;text-align:center;transition:color;transition-duration:.2s}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{margin-bottom:0;font:14px/normal sans-serif;line-height:1.5;font-weight:400;zoom:1}.driver-popover-footer{margin-top:15px;text-align:right;zoom:1;display:flex;align-items:center;justify-content:space-between}.driver-popover-progress-text{font-size:13px;font-weight:400;color:#727272;zoom:1}.driver-popover-footer button{all:unset;display:inline-block;box-sizing:border-box;padding:3px 7px;text-decoration:none;text-shadow:1px 1px 0 #fff;background-color:#fff;color:#2d2d2d;font:12px/normal sans-serif;cursor:pointer;outline:0;zoom:1;line-height:1.3;border:1px solid #ccc;border-radius:3px}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}:not(body):has(>.driver-active-element){overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}.driver-popover-footer button:hover,.driver-popover-footer button:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{display:flex;flex-grow:1;justify-content:flex-end}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";position:absolute;border:5px solid #fff}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{left:100%;border-right-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-right{right:100%;border-left-color:transparent;border-bottom-color:transparent;border-top-color:transparent}.driver-popover-arrow-side-top{top:100%;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.driver-popover-arrow-side-bottom{bottom:100%;border-left-color:transparent;border-top-color:transparent;border-right-color:transparent}.driver-popover-arrow-side-center{display:none}.driver-popover-arrow-side-left.driver-popover-arrow-align-start,.driver-popover-arrow-side-right.driver-popover-arrow-align-start{top:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-start,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-start{left:15px}.driver-popover-arrow-align-end.driver-popover-arrow-side-left,.driver-popover-arrow-align-end.driver-popover-arrow-side-right{bottom:15px}.driver-popover-arrow-side-top.driver-popover-arrow-align-end,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-end{right:15px}.driver-popover-arrow-side-left.driver-popover-arrow-align-center,.driver-popover-arrow-side-right.driver-popover-arrow-align-center{top:50%;margin-top:-5px}.driver-popover-arrow-side-top.driver-popover-arrow-align-center,.driver-popover-arrow-side-bottom.driver-popover-arrow-align-center{left:50%;margin-left:-5px}.driver-popover-arrow-none{display:none}
1
+ /*! driver.js 1.10.1 | https://github.com/kamranahmedse/driver.js | MIT License | Copyright (c) Kamran Ahmed */
2
+ /* Vendored from the driver.js npm package (dist/driver.css), unmodified. Keep in sync with
3
+ app/components/mensa/table/driver.js, which comes from the same version. */
4
+ .driver-popover{all:unset;font-family:var(--driver-popover-font-family,"Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif);box-sizing:border-box;color:#2d2d2d;z-index:1000000000;background-color:#fff;border-radius:5px;min-width:250px;max-width:300px;margin:0;padding:15px;position:fixed;top:0;right:0;box-shadow:0 1px 10px #0006}.driver-popover-title{zoom:1;margin:0;font-size:19px;font-weight:700;line-height:1.5;display:block;position:relative}.driver-popover-close-btn{all:unset;cursor:pointer;color:#d2d2d2;z-index:1;text-align:center;width:32px;height:28px;font-size:18px;font-weight:500;transition:color .2s;position:absolute;top:0;right:0}.driver-popover-close-btn:hover,.driver-popover-close-btn:focus-visible{color:#2d2d2d}.driver-popover-title[style*=block]+.driver-popover-description{margin-top:5px}.driver-popover-description{zoom:1;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.5}.driver-popover-footer{text-align:right;zoom:1;justify-content:space-between;align-items:center;margin-top:15px;display:flex}.driver-popover-progress-text{color:#727272;zoom:1;font-size:13px;font-weight:400}.driver-popover-footer-btn{all:unset;box-sizing:border-box;color:#2d2d2d;cursor:pointer;zoom:1;background-color:#fff;border:1px solid #ccc;border-radius:3px;outline:0;padding:3px 7px;font-size:12px;line-height:1.3;text-decoration:none;display:inline-block}.driver-popover-footer .driver-popover-btn-disabled{opacity:.5;pointer-events:none}.driver-popover-footer-btn:hover,.driver-popover-footer-btn:focus{background-color:#f7f7f7}.driver-popover-navigation-btns{flex-grow:1;justify-content:flex-end;display:flex}.driver-popover-navigation-btns button+button{margin-left:4px}.driver-popover-arrow{content:"";border:5px solid #fff;position:absolute}.driver-popover-arrow-side-over{display:none}.driver-popover-arrow-side-left{border-top-color:#0000;border-bottom-color:#0000;border-right-color:#0000;left:100%}.driver-popover-arrow-side-right{border-top-color:#0000;border-bottom-color:#0000;border-left-color:#0000;right:100%}.driver-popover-arrow-side-top{border-bottom-color:#0000;border-left-color:#0000;border-right-color:#0000;top:100%}.driver-popover-arrow-side-bottom{border-top-color:#0000;border-left-color:#0000;border-right-color:#0000;bottom:100%}.driver-popover-arrow-side-center,.driver-popover-arrow-none{display:none}.driver-active .driver-overlay{pointer-events:none}.driver-active.driver-no-scroll{overflow:hidden}.driver-active *{pointer-events:none}.driver-active .driver-active-element,.driver-active .driver-active-element *,.driver-popover,.driver-popover *{pointer-events:auto}@keyframes animate-fade-in{0%{opacity:0}to{opacity:1}}.driver-fade .driver-overlay{animation:animate-fade-in var(--driver-animation-duration,.4s) ease-in-out}.driver-fade .driver-popover{animation:animate-fade-in var(--driver-animation-duration,.4s)}.driver-active-element-parent-no-scroll{overflow:hidden!important}.driver-no-interaction,.driver-no-interaction *{pointer-events:none!important}
@@ -63,12 +63,8 @@
63
63
  @apply flex shrink-0 items-center gap-3 px-3 py-2.5;
64
64
  }
65
65
 
66
- .mensa-table__export-dialog__delete-form {
67
- @apply shrink-0;
68
- }
69
-
70
66
  .mensa-table__export-dialog__delete {
71
- @apply inline-flex h-9 w-9 items-center justify-center rounded-md text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-red-600 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-red-500/30 transition-colors cursor-pointer;
67
+ @apply shrink-0 inline-flex h-9 w-9 items-center justify-center rounded-md text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-red-600 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-red-500/30 transition-colors cursor-pointer;
72
68
  }
73
69
 
74
70
  .mensa-table__export-dialog__item-info {
@@ -11,13 +11,27 @@ module Mensa
11
11
  @filter = filter
12
12
  end
13
13
 
14
+ # A pill is a view filter only while it still filters like the view does,
15
+ # once the user changes it, it's their own filter.
14
16
  def view_filter?
15
17
  view = filter.table.table_view
16
18
  return false unless view
17
19
 
18
20
  view_filters = view.config&.dig(:filters) || view.config&.dig("filters") || {}
19
21
  col = filter.column.name.to_s
20
- view_filters.key?(col) || view_filters.key?(col.to_sym)
22
+ view_filter = view_filters[col] || view_filters[col.to_sym]
23
+ return false unless view_filter
24
+
25
+ view_filter = view_filter.to_h.with_indifferent_access
26
+ return false if view_filter[:operator].present? && view_filter[:operator].to_sym != filter.operator
27
+
28
+ comparable(view_filter[:value]) == comparable(filter.value)
29
+ end
30
+
31
+ private
32
+
33
+ def comparable(value)
34
+ Array(value).map(&:to_s).reject(&:blank?).sort
21
35
  end
22
36
  end
23
37
  end
@@ -18,7 +18,6 @@
18
18
  <div class="mensa-table__search-bar__input-wrapper">
19
19
  <input class="mensa-table__search-bar__input"
20
20
  type="text"
21
- name="search_query"
22
21
  autocomplete="off"
23
22
  placeholder="<%= t(has_filterable_columns ? ".search" : ".search_only") %>"
24
23
  data-mensa-filter-pill-list-target="searchInput"
@@ -319,8 +319,11 @@ export default class FilterPillListComponentController extends ApplicationContro
319
319
  const filtersDiffer =
320
320
  filtersProvided &&
321
321
  JSON.stringify(rendered) !== JSON.stringify(filters);
322
+ // The pills belong to the view the server rendered, so they only count
323
+ // when the stored view is that same view.
324
+ const viewDiffers = view.length > 0 && view !== this.renderedView();
322
325
  const alreadyRendered =
323
- Object.keys(rendered).length > 0 && !filtersDiffer;
326
+ Object.keys(rendered).length > 0 && !filtersDiffer && !viewDiffers;
324
327
 
325
328
  if (!hasState || alreadyRendered || table.frameLoadHandled) {
326
329
  if (typeof table.loadFrame === "function") {
@@ -546,6 +549,18 @@ export default class FilterPillListComponentController extends ApplicationContro
546
549
  this._monitorResetButton();
547
550
  }
548
551
 
552
+ // The id of the view checked in the views dropdown, "" without views.
553
+ renderedView() {
554
+ const root = this.element.closest(".mensa-table");
555
+ const check = root?.querySelector(
556
+ '[data-mensa-views-target="view"] .mensa-table__views__option-check:not(.invisible)',
557
+ );
558
+ return (
559
+ check?.closest('[data-mensa-views-target="view"]')?.dataset
560
+ .viewId || ""
561
+ );
562
+ }
563
+
549
564
  setViewHighlight(view) {
550
565
  if (!view) return;
551
566
 
@@ -1,13 +1,13 @@
1
1
  <% if table.current_user %>
2
2
  <dialog class="mensa-table__save-view-dialog" data-mensa-table-target="saveViewDialog" data-action="click->mensa-table#saveViewDialogBackdrop">
3
- <form class="mensa-table__save-view-dialog__form" data-action="submit->mensa-table#confirmSaveView">
3
+ <div class="mensa-table__save-view-dialog__form">
4
4
  <header class="mensa-table__save-view-dialog__header">
5
5
  <h2 class="mensa-table__save-view-dialog__title"><%= t(".save_view_title", default: "Save view") %></h2>
6
6
  <p class="mensa-table__save-view-dialog__subtitle"><%= t(".save_view_subtitle", default: "Save the current filters, ordering and search as a reusable view.") %></p>
7
7
  </header>
8
8
  <label class="mensa-table__save-view-dialog__field">
9
9
  <span class="mensa-table__save-view-dialog__label"><%= t(".view_name", default: "Name") %></span>
10
- <input class="mensa-table__save-view-dialog__input" type="text" required placeholder="<%= t(".view_name_placeholder", default: "e.g. Active customers") %>" data-mensa-table-target="saveViewName">
10
+ <input class="mensa-table__save-view-dialog__input" type="text" aria-required="true" placeholder="<%= t(".view_name_placeholder", default: "e.g. Active customers") %>" data-required-message="<%= t(".view_name_required", default: "Please enter a name for this view.") %>" data-mensa-table-target="saveViewName" data-action="keydown.enter->mensa-table#confirmSaveView input->mensa-table#clearValidity">
11
11
  </label>
12
12
  <label class="mensa-table__save-view-dialog__field">
13
13
  <span class="mensa-table__save-view-dialog__label"><%= t(".view_description", default: "Description") %></span>
@@ -17,10 +17,10 @@
17
17
  <button class="mensa-table__save-view-dialog__button mensa-table__save-view-dialog__button--secondary" type="button" data-action="mensa-table#cancelSaveView">
18
18
  <%= t(".cancel") %>
19
19
  </button>
20
- <button class="mensa-table__save-view-dialog__button mensa-table__save-view-dialog__button--primary" type="submit">
20
+ <button class="mensa-table__save-view-dialog__button mensa-table__save-view-dialog__button--primary" type="button" data-action="mensa-table#confirmSaveView">
21
21
  <%= t(".save") %>
22
22
  </button>
23
23
  </div>
24
- </form>
24
+ </div>
25
25
  </dialog>
26
26
  <% end %>
@@ -1,5 +1,5 @@
1
1
  import ApplicationController from "mensa/controllers/application_controller";
2
- import { get, post, patch } from "@rails/request.js";
2
+ import { get, post, patch, destroy } from "@rails/request.js";
3
3
 
4
4
  export default class TableComponentController extends ApplicationController {
5
5
  static targets = [
@@ -300,6 +300,8 @@ export default class TableComponentController extends ApplicationController {
300
300
  }
301
301
 
302
302
  async confirmSaveView(event) {
303
+ // Called from a button click or Enter in the name input. The table may
304
+ // sit inside a host <form>; this stops Enter from submitting it.
303
305
  event.preventDefault();
304
306
 
305
307
  const name = this.hasSaveViewNameTarget
@@ -307,7 +309,7 @@ export default class TableComponentController extends ApplicationController {
307
309
  : "";
308
310
  if (!name) {
309
311
  if (this.hasSaveViewNameTarget)
310
- this.saveViewNameTarget.reportValidity();
312
+ this.reportMissing(this.saveViewNameTarget);
311
313
  return;
312
314
  }
313
315
 
@@ -560,28 +562,18 @@ export default class TableComponentController extends ApplicationController {
560
562
  );
561
563
  if (!options) return;
562
564
 
563
- const mode = dialog.querySelector(
564
- 'input[name="repeat_mode"]:checked',
565
- )?.value;
566
- options.hidden = mode !== "repeating";
565
+ options.hidden = this._exportField("repeat_mode") !== "repeating";
567
566
  }
568
567
 
569
568
  confirmExport(event) {
570
- event.preventDefault();
569
+ if (event) event.preventDefault();
571
570
  if (!this.hasExportsUrlValue) return;
572
571
 
573
- const dialog = this.exportDialogTarget;
574
- const scope =
575
- dialog.querySelector('input[name="scope"]:checked')?.value || "all";
576
- const exportFormat =
577
- dialog.querySelector('input[name="export_format"]:checked')
578
- ?.value || "csv_excel";
579
- const repeatMode = dialog.querySelector(
580
- 'input[name="repeat_mode"]:checked',
581
- )?.value;
572
+ const scope = this._exportField("scope") || "all";
573
+ const exportFormat = this._exportField("export_format") || "csv_excel";
582
574
  const repeat =
583
- repeatMode === "repeating"
584
- ? dialog.querySelector('select[name="repeat"]')?.value || ""
575
+ this._exportField("repeat_mode") === "repeating"
576
+ ? this._exportField("repeat") || ""
585
577
  : "";
586
578
 
587
579
  const state = this.currentViewState();
@@ -611,6 +603,27 @@ export default class TableComponentController extends ApplicationController {
611
603
  });
612
604
  }
613
605
 
606
+ deleteExport(event) {
607
+ if (event) event.preventDefault();
608
+ const url = event.currentTarget.dataset.url;
609
+ if (!url) return;
610
+
611
+ destroy(url, { responseKind: "turbo-stream" });
612
+ }
613
+
614
+ // Shows the browser's validation bubble for an empty text input. The
615
+ // inputs aren't `required` (that would block a host <form> around the
616
+ // table), so the message is set by hand and cleared again on input.
617
+ reportMissing(input) {
618
+ input.setCustomValidity(input.dataset.requiredMessage || "");
619
+ input.reportValidity();
620
+ input.focus();
621
+ }
622
+
623
+ clearValidity(event) {
624
+ event.currentTarget.setCustomValidity("");
625
+ }
626
+
614
627
  get ourUrl() {
615
628
  if (
616
629
  this.hasTurboFrameTarget &&
@@ -629,6 +642,17 @@ export default class TableComponentController extends ApplicationController {
629
642
 
630
643
  // --- Private ---
631
644
 
645
+ // Export controls are found by data attribute: their names carry the
646
+ // table id so they don't group with radios elsewhere on the page.
647
+ _exportField(field) {
648
+ const dialog = this.exportDialogTarget;
649
+ const selector = `[data-mensa-export-field="${field}"]`;
650
+ const control =
651
+ dialog.querySelector(`input${selector}:checked`) ||
652
+ dialog.querySelector(`select${selector}`);
653
+ return control?.value;
654
+ }
655
+
632
656
  // Reads the currently-selected view ID from the DOM (after Turbo re-renders
633
657
  // the views component) and persists it to localStorage.
634
658
  _persistSelectedViewId() {
@@ -674,7 +698,10 @@ export default class TableComponentController extends ApplicationController {
674
698
 
675
699
  _openSaveDialog() {
676
700
  if (!this.hasSaveViewDialogTarget) return;
677
- if (this.hasSaveViewNameTarget) this.saveViewNameTarget.value = "";
701
+ if (this.hasSaveViewNameTarget) {
702
+ this.saveViewNameTarget.value = "";
703
+ this.saveViewNameTarget.setCustomValidity("");
704
+ }
678
705
  if (this.hasSaveViewDescriptionTarget)
679
706
  this.saveViewDescriptionTarget.value = "";
680
707
  if (typeof this.saveViewDialogTarget.showModal === "function") {
@@ -0,0 +1,5 @@
1
+ /*! driver.js 1.10.1 | https://github.com/kamranahmedse/driver.js | MIT License | Copyright (c) Kamran Ahmed */
2
+ // Vendored from the driver.js npm package (dist/driver.js.mjs), unmodified. Keep in sync
3
+ // with app/assets/stylesheets/mensa/driver.css, which comes from the same version.
4
+ const e=[`pointerdown`,`mousedown`,`pointerup`,`mouseup`,`click`],t=new WeakMap;function n(n,i,a){r(n);let o=e=>{let t=e.target;n.contains(t)&&((!a||a(t))&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),e.type===`click`&&i?.(e))};for(let t of e)document.addEventListener(t,o,!0);t.set(n,o)}function r(n){let r=t.get(n);if(r){for(let t of e)document.removeEventListener(t,r,!0);t.delete(n)}}function i(e,t){let n=e.wrapper.getBoundingClientRect();return{width:n.width+t,height:n.height+t,realWidth:n.width,realHeight:n.height}}function a(e,t){let{elementDimensions:n,popoverDimensions:r,popoverPadding:i,popoverArrowDimensions:a}=t;return e===`start`?Math.max(Math.min(n.top-i,window.innerHeight-r.realHeight-a.width),a.width):e===`end`?Math.max(Math.min(n.top-r?.realHeight+n.height+i,window.innerHeight-r?.realHeight-a.width),a.width):e===`center`?Math.max(Math.min(n.top+n.height/2-r?.realHeight/2,window.innerHeight-r?.realHeight-a.width),a.width):0}function o(e,t){let{elementDimensions:n,popoverDimensions:r,popoverPadding:i,popoverArrowDimensions:a}=t;return e===`start`?Math.max(Math.min(n.left-i,window.innerWidth-r.realWidth-a.width),a.width):e===`end`?Math.max(Math.min(n.left-r?.realWidth+n.width+i,window.innerWidth-r?.realWidth-a.width),a.width):e===`center`?Math.max(Math.min(n.left+n.width/2-r?.realWidth/2,window.innerWidth-r?.realWidth-a.width),a.width):0}function s(e,t,n){let{align:r,side:s}=n,c=n.centered?`over`:s,l=n.padding,u=i(e,n.offset),f=e.arrow.getBoundingClientRect(),p=t.getBoundingClientRect(),m=p.top-u.height,h=m>=0,g=window.innerHeight-(p.bottom+u.height),_=g>=0,v=p.left-u.width,y=v>=0,b=window.innerWidth-(p.right+u.width),x=b>=0,S=!h&&!_&&!y&&!x,C=c;if(c===`top`&&h?x=y=_=!1:c===`bottom`&&_?x=y=h=!1:c===`left`&&y?x=h=_=!1:c===`right`&&x&&(y=h=_=!1),c===`over`){let t=window.innerWidth/2-u.realWidth/2,n=window.innerHeight/2-u.realHeight/2;e.wrapper.style.left=`${t}px`,e.wrapper.style.right=`auto`,e.wrapper.style.top=`${n}px`,e.wrapper.style.bottom=`auto`}else if(S){let t=window.innerWidth/2-u?.realWidth/2;e.wrapper.style.left=`${t}px`,e.wrapper.style.right=`auto`,e.wrapper.style.bottom=`10px`,e.wrapper.style.top=`auto`}else if(y){let t=Math.min(v,window.innerWidth-u?.realWidth-f.width),n=a(r,{elementDimensions:p,popoverDimensions:u,popoverPadding:l,popoverArrowDimensions:f});e.wrapper.style.left=`${t}px`,e.wrapper.style.top=`${n}px`,e.wrapper.style.bottom=`auto`,e.wrapper.style.right=`auto`,C=`left`}else if(x){let t=Math.min(b,window.innerWidth-u?.realWidth-f.width),n=a(r,{elementDimensions:p,popoverDimensions:u,popoverPadding:l,popoverArrowDimensions:f});e.wrapper.style.right=`${t}px`,e.wrapper.style.top=`${n}px`,e.wrapper.style.bottom=`auto`,e.wrapper.style.left=`auto`,C=`right`}else if(h){let t=Math.min(m,window.innerHeight-u.realHeight-f.width),n=o(r,{elementDimensions:p,popoverDimensions:u,popoverPadding:l,popoverArrowDimensions:f});e.wrapper.style.top=`${t}px`,e.wrapper.style.left=`${n}px`,e.wrapper.style.bottom=`auto`,e.wrapper.style.right=`auto`,C=`top`}else if(_){let t=Math.min(g,window.innerHeight-u?.realHeight-f.width),n=o(r,{elementDimensions:p,popoverDimensions:u,popoverPadding:l,popoverArrowDimensions:f});e.wrapper.style.left=`${n}px`,e.wrapper.style.bottom=`${t}px`,e.wrapper.style.top=`auto`,e.wrapper.style.right=`auto`,C=`bottom`}d(e,S?`over`:C,r,t),[...e.wrapper.classList].filter(e=>e.startsWith(`driver-popover-side-`)||e.startsWith(`driver-popover-align-`)).forEach(t=>e.wrapper.classList.remove(t)),e.wrapper.classList.add(`driver-popover-side-${C}`),e.wrapper.classList.add(`driver-popover-align-${r}`)}function c(e,t,n,r,i,a=10){let o=r-n;return e<=n&&t>=r?i===`start`?15+a/2:i===`end`?o-15-a/2:o/2:(Math.min(Math.max(e,n),r)+Math.min(Math.max(t,n),r))/2-n}function l(e,t,n=10){let r=t-15-n;if(r<15)return Math.max(0,(t-n)/2);let i=e-n/2;return Math.min(Math.max(i,15),r)}function u(e,t,n){return e===`left`||e===`right`?t.bottom>n.top&&t.top<n.bottom?e:t.bottom<=n.top?`bottom`:`top`:t.right>n.left&&t.left<n.right?e:t.right<=n.left?`right`:`left`}function d(e,t,n,r){let i=e.arrow;if(i.className=`driver-popover-arrow`,i.style.top=``,i.style.right=``,i.style.bottom=``,i.style.left=``,t===`over`){i.classList.add(`driver-popover-arrow-none`);return}let a=r.getBoundingClientRect(),o=e.wrapper.getBoundingClientRect(),s=u(t,a,o);i.classList.add(`driver-popover-arrow-side-${s}`);let d=i.getBoundingClientRect().width||10;if(s===`left`||s===`right`){let e=c(a.top,a.bottom,o.top,o.bottom,n,d);i.style.top=`${l(e,o.height,d)}px`}else{let e=c(a.left,a.right,o.left,o.right,n,d);i.style.left=`${l(e,o.width,d)}px`}}function f(e){return typeof e==`function`?e():typeof e==`string`?document.querySelector(e):e}function p(e){let t=window.getComputedStyle(e);return[t.overflow,t.overflowX,t.overflowY].some(e=>e===`auto`||e===`scroll`)}function m(e,t,n,r){return(e/=r/2)<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t}function h(e){let t=`a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])`;return e.flatMap(e=>{let n=e.matches(t),r=Array.from(e.querySelectorAll(t));return[...n?[e]:[],...r]}).filter(e=>getComputedStyle(e).pointerEvents!==`none`&&y(e))}function g(e,t){if(!e||v(e))return;let n=e.offsetHeight>window.innerHeight;e.scrollIntoView({behavior:!t||_(e)?`auto`:`smooth`,inline:`center`,block:n?`start`:`center`})}function _(e){if(!e||!e.parentElement)return;let t=e.parentElement;return t.scrollHeight>t.clientHeight}function v(e){let t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}function y(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}function b(e){return e.closest(`dialog[open]`)??document.body}function x(e){e&&(e.wrapper.style.display=`none`)}function S(e,t){let r=w();b(e).appendChild(r.wrapper);let{title:i,description:a,showButtons:o,disableButtons:c,showProgress:l,nextBtnText:u,prevBtnText:d,progressText:f}=t;r.nextButton.innerHTML=u,r.previousButton.innerHTML=d,r.progress.innerHTML=f,r.closeButton.setAttribute(`aria-label`,t.closeBtnLabel),t.doneButton&&r.nextButton.classList.add(`driver-popover-done-btn`),i?(r.title.innerHTML=i,r.title.style.display=`block`):r.title.style.display=`none`,a?(r.description.innerHTML=a,r.description.style.display=`block`):r.description.style.display=`none`;let p=o.includes(`next`)||o.includes(`previous`)||l;r.closeButton.style.display=o.includes(`close`)?`block`:`none`,p?(r.footer.style.display=`flex`,r.progress.style.display=l?`block`:`none`,r.nextButton.style.display=o.includes(`next`)?`block`:`none`,r.previousButton.style.display=o.includes(`previous`)?`block`:`none`):r.footer.style.display=`none`,c.includes(`next`)&&(r.nextButton.disabled=!0,r.nextButton.classList.add(`driver-popover-btn-disabled`)),c.includes(`previous`)&&(r.previousButton.disabled=!0,r.previousButton.classList.add(`driver-popover-btn-disabled`)),c.includes(`close`)&&(r.closeButton.disabled=!0,r.closeButton.classList.add(`driver-popover-btn-disabled`));let m=r.wrapper;m.style.display=`block`,m.style.left=``,m.style.top=``,m.style.bottom=``,m.style.right=``,m.id=`driver-popover-content`,m.setAttribute(`role`,`dialog`),m.setAttribute(`aria-labelledby`,`driver-popover-title`),m.setAttribute(`aria-describedby`,`driver-popover-description`);let _=r.arrow;_.className=`driver-popover-arrow`,m.className=`driver-popover ${t.popoverClass||``}`.trim(),n(r.wrapper,e=>{let n=e.target instanceof Element?e.target:e.target?.parentElement;if(n){if(n.closest(`.driver-popover-next-btn`))return t.onNextClick?.();if(n.closest(`.driver-popover-prev-btn`))return t.onPrevClick?.();if(n.closest(`.driver-popover-close-btn`))return t.onCloseClick?.()}},e=>r.description.contains(e)||r.title.contains(e)?!1:!!e.closest(`.driver-popover-prev-btn, .driver-popover-next-btn, .driver-popover-close-btn`)),t.onRender?.(r),s(r,e,t.position),C(r,e,t.position),g(m,t.smoothScroll);let v=h([m,e]);return v.length>0&&v[0].focus(),r}function C(e,t,n){e.wrapper.querySelectorAll(`img`).forEach(r=>{if(r.complete)return;let i=()=>s(e,t,n);r.addEventListener(`load`,i,{once:!0}),r.addEventListener(`error`,i,{once:!0})})}function w(){let e=document.createElement(`div`);e.classList.add(`driver-popover`);let t=document.createElement(`div`);t.classList.add(`driver-popover-arrow`);let n=document.createElement(`header`);n.id=`driver-popover-title`,n.classList.add(`driver-popover-title`),n.style.display=`none`,n.innerText=`Popover Title`;let r=document.createElement(`div`);r.id=`driver-popover-description`,r.classList.add(`driver-popover-description`),r.style.display=`none`,r.innerText=`Popover description is here`;let i=document.createElement(`button`);i.type=`button`,i.classList.add(`driver-popover-close-btn`),i.innerHTML=`&times;`;let a=document.createElement(`footer`);a.classList.add(`driver-popover-footer`);let o=document.createElement(`span`);o.classList.add(`driver-popover-progress-text`),o.innerText=``;let s=document.createElement(`span`);s.classList.add(`driver-popover-navigation-btns`);let c=document.createElement(`button`);c.type=`button`,c.classList.add(`driver-popover-prev-btn`,`driver-popover-footer-btn`),c.innerHTML=`Previous`;let l=document.createElement(`button`);return l.type=`button`,l.classList.add(`driver-popover-next-btn`,`driver-popover-footer-btn`),l.innerHTML=`Next`,s.appendChild(c),s.appendChild(l),a.appendChild(o),a.appendChild(s),e.appendChild(i),e.appendChild(t),e.appendChild(n),e.appendChild(r),e.appendChild(a),{wrapper:e,arrow:t,title:n,description:r,footer:a,previousButton:c,nextButton:l,closeButton:i,footerButtons:s,progress:o}}function T(e){e&&(r(e.wrapper),e.wrapper.parentElement?.removeChild(e.wrapper))}function E(e,t){let n=window.innerWidth,r=window.innerHeight,i=t.padding,a=t.radius,o=e.width+i*2,s=e.height+i*2,c=Math.min(a,o/2,s/2),l=Math.floor(Math.max(c,0)),u=e.x-i+l,d=e.y-i,f=o-l*2,p=s-l*2;return`M${n},0L0,0L0,${r}L${n},${r}L${n},0Z
5
+ M${u},${d} h${f} a${l},${l} 0 0 1 ${l},${l} v${p} a${l},${l} 0 0 1 -${l},${l} h-${f} a${l},${l} 0 0 1 -${l},-${l} v-${p} a${l},${l} 0 0 1 ${l},-${l} z`}function D(e,t,n,r,i){let a=e.getState(`__activeStagePosition`),o=a||r.getBoundingClientRect(),s=i.getBoundingClientRect();a={x:m(t,o.x,s.x-o.x,n),y:m(t,o.y,s.y-o.y,n),width:m(t,o.width,s.width-o.width,n),height:m(t,o.height,s.height-o.height,n)},k(e,a),e.setState(`__activeStagePosition`,a)}function O(e,t){if(!t)return;let n=t.getBoundingClientRect(),r={x:n.x,y:n.y,width:n.width,height:n.height};e.setState(`__activeStagePosition`,r),k(e,r)}function ee(e){let t=e.getState(`__activeStagePosition`),n=e.getState(`__overlaySvg`);if(!t)return;if(!n){console.warn(`No stage svg found.`);return}let r=window.innerWidth,i=window.innerHeight;n.setAttribute(`viewBox`,`0 0 ${r} ${i}`)}function te(e,t){let r=j(e,t);document.body.appendChild(r),n(r,t=>{t.target.tagName===`path`&&e.emit(`overlayClick`)}),e.setState(`__overlaySvg`,r)}function k(e,t){let n=e.getState(`__overlaySvg`);if(!n){te(e,t);return}let r=n.firstElementChild;if(r?.tagName!==`path`)throw Error(`no path element found in stage svg`);r.setAttribute(`d`,E(t,A(e)))}function A(e){return e.getState(`activeElement`)?.id===`driver-dummy-element`?{padding:0,radius:0}:{padding:e.getConfig(`stagePadding`)||0,radius:e.getConfig(`stageRadius`)||0}}function j(e,t){let n=window.innerWidth,r=window.innerHeight,i=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);i.classList.add(`driver-overlay`,`driver-overlay-animated`),i.setAttribute(`viewBox`,`0 0 ${n} ${r}`),i.setAttribute(`xmlSpace`,`preserve`),i.setAttribute(`xmlnsXlink`,`http://www.w3.org/1999/xlink`),i.setAttribute(`version`,`1.1`),i.setAttribute(`preserveAspectRatio`,`xMinYMin slice`),i.style.fillRule=`evenodd`,i.style.clipRule=`evenodd`,i.style.strokeLinejoin=`round`,i.style.strokeMiterlimit=`2`,i.style.zIndex=`10000`,i.style.position=`fixed`,i.style.top=`0`,i.style.left=`0`,i.style.width=`100%`,i.style.height=`100%`;let a=document.createElementNS(`http://www.w3.org/2000/svg`,`path`);return a.setAttribute(`d`,E(t,A(e))),a.style.fill=e.getConfig(`overlayColor`)||`rgb(0,0,0)`,a.style.opacity=`${e.getConfig(`overlayOpacity`)}`,a.style.pointerEvents=`auto`,a.style.cursor=`auto`,i.appendChild(a),i}function M(e){let t=e.getState(`__overlaySvg`);t&&(r(t),t.remove())}const N=`{{current}} of {{total}}`;function P(e,t){return!(t.skipMissingElement??e.getConfig(`skipMissingElement`))||!t.element?!1:!f(t.element)}function F(e,t,n){let r=e.getConfig(`steps`)||[];for(let i=t;i>=0&&i<r.length;i+=n)if(!P(e,r[i]))return i}function I(e,t){let n=e.getState(`activeIndex`),r=n!==void 0&&F(e,n+1,1)===void 0,i=t?.popover?.onDoneClick||e.getConfig(`onDoneClick`);return r&&i?i:t?.popover?.onNextClick||e.getConfig(`onNextClick`)}function L(e,t){return t?.popover?.onPrevClick||e.getConfig(`onPrevClick`)}function R(e,t){return t?.popover?.onCloseClick||e.getConfig(`onCloseClick`)}function z(e,t,n){let r=e.getConfig(`steps`)||[],i=r[t],a=i.popover||{},o=F(e,t+1,1)!==void 0,s=F(e,t-1,-1)!==void 0,c=a.doneBtnText||e.getConfig(`doneBtnText`)||`Done`,l=e.getConfig(`allowClose`),u=a.showProgress===void 0?e.getConfig(`showProgress`):a.showProgress,d=(a.progressText||e.getConfig(`progressText`)||N).replace(`{{current}}`,`${t+1}`).replace(`{{total}}`,`${r.length}`),f=a.showButtons||e.getConfig(`showButtons`),p=[`next`,`previous`,...l?[`close`]:[]].filter(e=>!f?.length||f.includes(e)),m=a.onNextClick||e.getConfig(`onNextClick`),h=a.onPrevClick||e.getConfig(`onPrevClick`),g=a.onCloseClick||e.getConfig(`onCloseClick`);return{...i,popover:{showButtons:p,nextBtnText:o?void 0:c,disableButtons:[...e.getConfig(`disableButtons`)||[],...s?[]:[`previous`]],showProgress:u,onNextClick:m||n.onNextClick,onPrevClick:h||n.onPrevClick,onCloseClick:g||n.onCloseClick,...a,progressText:d}}}function B(e,t){return{showButtons:t?.popover?.showButtons||e.getConfig(`showButtons`)||[],disableButtons:t?.popover?.disableButtons||e.getConfig(`disableButtons`)||[]}}function V(e,t,n){let r=e.getConfig(`stagePadding`)||0;return{side:n.popover?.side||`bottom`,align:n.popover?.align||`start`,offset:r+(e.getConfig(`popoverOffset`)||0),padding:r,centered:t.id===`driver-dummy-element`}}function H(e,t,n){let r=n.popover||{},i=e.getState(`activeIndex`),a=i!==void 0&&F(e,i+1,1)===void 0;return{title:r.title,description:r.description,...B(e,n),showProgress:r.showProgress||e.getConfig(`showProgress`)||!1,progressText:r.progressText??(e.getConfig(`progressText`)||N),nextBtnText:r.nextBtnText??(e.getConfig(`nextBtnText`)||`Next`),prevBtnText:r.prevBtnText??(e.getConfig(`prevBtnText`)||`Previous`),closeBtnLabel:r.closeBtnLabel??(e.getConfig(`closeBtnLabel`)||`Close`),doneButton:a,popoverClass:r.popoverClass||e.getConfig(`popoverClass`)||``,smoothScroll:e.getConfig(`smoothScroll`),onNextClick:()=>{a&&e.setState(`__destroyReason`,`done`);let r=I(e,n);return r?r(t,n,e.getHookOpts()):e.emit(`nextClick`)},onPrevClick:()=>{let r=L(e,n);return r?r(t,n,e.getHookOpts()):e.emit(`prevClick`)},onCloseClick:()=>{e.setState(`__destroyReason`,`close`);let r=R(e,n);return r?r(t,n,e.getHookOpts()):e.emit(`closeClick`)},onRender:t=>{e.setState(`popover`,t),(r.onPopoverRender||e.getConfig(`onPopoverRender`))?.(t,e.getHookOpts())},position:V(e,t,n)}}function U(e,t,n){T(e.getState(`popover`)),S(t,H(e,t,n))}function W(e,t,n){let r=e.getState(`popover`);r&&s(r,t,V(e,t,n))}const G={"aria-haspopup":`dialog`,"aria-expanded":`true`,"aria-controls":`driver-popover-content`},K=new WeakMap;function q(e){if(!K.has(e)){let t=Object.fromEntries(Object.keys(G).map(t=>[t,e.getAttribute(t)]));K.set(e,t)}for(let[t,n]of Object.entries(G))e.setAttribute(t,n)}function J(e){let t=K.get(e);if(t){for(let[n,r]of Object.entries(t))r===null?e.removeAttribute(n):e.setAttribute(n,r);K.delete(e)}}function Y(){let e=document.getElementById(`driver-dummy-element`);if(e)return e;let t=document.createElement(`div`);return t.id=`driver-dummy-element`,t.style.width=`0`,t.style.height=`0`,t.style.pointerEvents=`none`,t.style.opacity=`0`,t.style.position=`fixed`,t.style.top=`50%`,t.style.left=`50%`,document.body.appendChild(t),t}function X(e,t){let n=f(t.element);n||(n=Y()),ne(e,n,t)}function Z(e){let t=e.getState(`__activeElement`),n=e.getState(`__activeStep`);t&&(O(e,t),ee(e),W(e,t,n))}function ne(e,t,n){let r=e.getConfig(`duration`)||400,i=Date.now(),a=e.getState(`__activeStep`),o=e.getState(`__activeElement`)||t,s=!o||o===t,c=t.id===`driver-dummy-element`,l=o.id===`driver-dummy-element`,u=e.getConfig(`animate`),d=n.onHighlightStarted||e.getConfig(`onHighlightStarted`),f=n?.onHighlighted||e.getConfig(`onHighlighted`),m=a?.onDeselected||e.getConfig(`onDeselected`),h=e.getHookOpts();a&&a!==n&&m&&m(l?void 0:o,a,h),d&&d(c?void 0:t,n,h);let _=!s&&u,v=!1;x(e.getState(`popover`)),e.setState(`previousStep`,a),e.setState(`previousElement`,o),e.setState(`activeStep`,n),e.setState(`activeElement`,t);let y=()=>{if(e.getState(`__transitionCallback`)!==y)return;let s=Date.now()-i,l=r-s<=r/2;n.popover&&l&&!v&&_&&(U(e,t,n),v=!0),e.getConfig(`animate`)&&s<r?D(e,s,r,o,t):(O(e,t),f&&f(c?void 0:t,n,e.getHookOpts()),e.setState(`__transitionCallback`,void 0),e.setState(`__previousStep`,a),e.setState(`__previousElement`,o),e.setState(`__activeStep`,n),e.setState(`__activeElement`,t)),window.requestAnimationFrame(y)};e.setState(`__transitionCallback`,y),window.requestAnimationFrame(y),g(t,e.getConfig(`smoothScroll`)),!_&&n.popover&&U(e,t,n),document.querySelectorAll(`.driver-active-element-parent`).forEach(e=>{e.classList.remove(`driver-active-element-parent`,`driver-active-element-parent-no-scroll`)}),document.querySelectorAll(`.driver-active-element`).forEach(e=>{e.classList.remove(`driver-active-element`,`driver-no-interaction`),J(e)}),(n.disableActiveInteraction??e.getConfig(`disableActiveInteraction`))&&t.classList.add(`driver-no-interaction`);let b=t.parentElement;b&&b!==document.body&&(b.classList.add(`driver-active-element-parent`),p(b)&&b.classList.add(`driver-active-element-parent-no-scroll`)),t.classList.add(`driver-active-element`),q(t)}function re(){document.getElementById(`driver-dummy-element`)?.remove(),document.querySelectorAll(`.driver-active-element`).forEach(e=>{let t=e.parentElement;t&&t!==document.body&&t.classList.remove(`driver-active-element-parent`,`driver-active-element-parent-no-scroll`),e.classList.remove(`driver-active-element`,`driver-no-interaction`),J(e)})}function Q(e){let t=e.getState(`__resizeTimeout`);t&&window.cancelAnimationFrame(t),e.setState(`__resizeTimeout`,window.requestAnimationFrame(()=>Z(e)))}function ie(e,t){if(!e.getState(`isInitialized`)||!(t.key===`Tab`||t.keyCode===9))return;let n=e.getState(`__activeElement`),r=e.getState(`popover`)?.wrapper,i=h([...r?[r]:[],...n?[n]:[]]),a=i[0],o=i[i.length-1];t.preventDefault(),t.shiftKey?(i[i.indexOf(document.activeElement)-1]||o)?.focus():(i[i.indexOf(document.activeElement)+1]||a)?.focus()}function ae(e,t){(e.getConfig(`allowKeyboardControl`)??!0)&&(t.key===`Escape`?e.emit(`escapePress`):t.key===`ArrowRight`?e.emit(`arrowRightPress`):t.key===`ArrowLeft`&&e.emit(`arrowLeftPress`))}function oe(e,t){let n=e.getState(`__activeElement`),r=t.target;!n||!r||!n.contains(r)||e.emit(`activeElementClick`)}function se(e){let t=t=>ae(e,t),n=t=>ie(e,t),r=()=>Q(e),i=()=>Q(e),a=t=>oe(e,t);e.setState(`__events`,{onKeyup:t,onKeydown:n,onResize:r,onScroll:i,onClick:a}),window.addEventListener(`keyup`,t,!1),window.addEventListener(`keydown`,n,!1),window.addEventListener(`resize`,r),window.addEventListener(`scroll`,i),document.addEventListener(`click`,a,!1)}function ce(e){let t=e.getState(`__events`);t&&(window.removeEventListener(`keyup`,t.onKeyup),window.removeEventListener(`keydown`,t.onKeydown),window.removeEventListener(`resize`,t.onResize),window.removeEventListener(`scroll`,t.onScroll),document.removeEventListener(`click`,t.onClick,!1))}function le(){let e={};function t(t={}){e={animate:!0,duration:400,allowClose:!0,allowScroll:!0,overlayClickBehavior:`close`,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,advanceOnClick:!1,skipMissingElement:!1,waitForElement:0,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:[`next`,`previous`,`close`],disableButtons:[],overlayColor:`#000`,...t}}return t(),{getConfig:(t=>t?e[t]:e),configure:t}}function ue(){let e={},t=(t=>t?e[t]:e),n=(t,n)=>{e[t]=n};function r(){e={}}return{getState:t,setState:n,resetState:r}}function de(){let e={};function t(t,n){e[t]=n}function n(t){e[t]?.()}function r(){e={}}return{listen:t,emit:n,reset:r}}function fe(e={}){let t=le();t.configure(e);let n=ue(),r=de(),i;return{getConfig:t.getConfig,setConfig:t.configure,getState:n.getState,setState:n.setState,resetState:n.resetState,listen:r.listen,emit:r.emit,resetEmitter:r.reset,getDriver:()=>i,setDriver:e=>{i=e},getHookOpts:(e,r)=>{let a=e||n.getState();return{config:t.getConfig(),state:a,driver:i,index:a.activeIndex,...r?{reason:r}:{}}}}}let $;function pe(e={}){let t=fe(e),n=()=>y(`replaced`,!1);function r(e=`close`){t.getConfig(`allowClose`)&&y(e)}function i(){let e=t.getState(`activeIndex`);e!==void 0&&F(t,e+1,1)===void 0&&t.setState(`__destroyReason`,`done`)}function a(){let{disableButtons:e}=B(t,t.getState(`__activeStep`));e.includes(`close`)||r(`escape`)}function o(e){let{showButtons:n,disableButtons:r}=B(t,t.getState(`__activeStep`));return n.includes(e)&&!r.includes(e)}function s(){let e=t.getConfig(`overlayClickBehavior`);if(t.getConfig(`allowClose`)&&e===`close`){y(`overlay`);return}if(typeof e==`function`){t.setState(`__destroyReason`,`overlay`);let n=t.getState(`__activeStep`);e(t.getState(`__activeElement`),n,t.getHookOpts());return}if(e===`nextStep`){let e=t.getState(`activeStep`),n=t.getState(`activeElement`),r=I(t,e);if(r){i(),r(n,e,t.getHookOpts());return}c()}}function c(){let e=t.getState(`activeIndex`),n=t.getConfig(`steps`)||[];if(e===void 0)return;let r=e+1;n[r]?v(r):y(`done`)}function l(){let e=t.getState(`activeIndex`),n=t.getConfig(`steps`)||[];if(e===void 0)return;let r=e-1;n[r]?v(r):y(`api`)}function u(e){(t.getConfig(`steps`)||[])[e]?v(e):y(`api`)}function d(){if(t.getState(`__transitionCallback`))return;let e=t.getState(`__activeStep`);if(!e||!(e.advanceOnClick??t.getConfig(`advanceOnClick`)))return;let n=t.getState(`__activeElement`),r=I(t,e);if(r){i(),r(n,e,t.getHookOpts());return}c()}function p(){if(t.getState(`__transitionCallback`))return;let e=t.getState(`activeIndex`),n=t.getState(`__activeStep`),r=t.getState(`__activeElement`);if(e===void 0||n===void 0||!(t.getConfig(`steps`)||[])[e-1]||!o(`previous`))return;let i=L(t,n);if(i)return i(r,n,t.getHookOpts());l()}function m(){if(t.getState(`__transitionCallback`))return;let e=t.getState(`activeIndex`),n=t.getState(`__activeStep`),r=t.getState(`__activeElement`);if(e===void 0||n===void 0||!o(`next`))return;let a=I(t,n);if(a)return i(),a(r,n,t.getHookOpts());c()}function h(){t.getState(`isInitialized`)||($&&$!==n&&$(),$=n,t.setState(`isInitialized`,!0),document.body.classList.add(`driver-active`,t.getConfig(`animate`)?`driver-fade`:`driver-simple`),t.getConfig(`allowScroll`)||document.body.classList.add(`driver-no-scroll`),document.body.style.setProperty(`--driver-animation-duration`,`${t.getConfig(`duration`)||400}ms`),se(t),t.listen(`overlayClick`,s),t.listen(`activeElementClick`,d),t.listen(`escapePress`,a),t.listen(`closeClick`,r),t.listen(`arrowLeftPress`,p),t.listen(`arrowRightPress`,m))}function g(){let e=t.getState(`__pendingWaitCancel`);e&&(t.setState(`__pendingWaitCancel`,void 0),e())}function _(e,n,r){let i=()=>{a.disconnect(),window.clearTimeout(o),t.setState(`__pendingWaitCancel`,void 0),r()},a=new MutationObserver(()=>{f(e.element)&&i()}),o=window.setTimeout(i,n);t.setState(`__pendingWaitCancel`,()=>{a.disconnect(),window.clearTimeout(o)}),a.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0})}function v(e=0,n=!1){g(),t.setState(`__destroyReason`,void 0);let r=t.getConfig(`steps`);if(!r){console.error(`No steps to drive through`),y(`api`);return}if(!r[e]){y(`api`);return}let i=r[e],a=i.waitForElement??t.getConfig(`waitForElement`)??0;if(!n&&a>0&&i.element&&!f(i.element)){x(t.getState(`popover`)),_(i,a,()=>v(e,!0));return}if(P(t,i)){let n=t.getState(`activeIndex`),i=typeof n==`number`&&e<n?-1:1;r[e+i]?v(e+i):i===1&&y(`done`);return}t.setState(`__activeOnDestroyed`,document.activeElement),t.setState(`activeIndex`,e);let o=r[e+1];X(t,z(t,e,{onNextClick:()=>{o?v(e+1):y(`done`)},onPrevClick:()=>{v(e-1)},onCloseClick:()=>{y(`close`)}}))}function y(e,r=!0){if(!t.getState(`isInitialized`))return;e===`api`&&(e=t.getState(`__destroyReason`)??`api`);let i=t.getState(`__activeElement`),a=t.getState(`__activeStep`),o=t.getState(`__activeOnDestroyed`),s=t.getConfig(`onDestroyStarted`);if(r&&s){t.setState(`__destroyReason`,e),s(!i||i?.id===`driver-dummy-element`?void 0:i,a,t.getHookOpts(void 0,e));return}$===n&&($=void 0);let c=a?.onDeselected||t.getConfig(`onDeselected`),l=t.getConfig(`onDestroyed`);document.body.classList.remove(`driver-active`,`driver-fade`,`driver-simple`,`driver-no-scroll`),document.body.style.removeProperty(`--driver-animation-duration`),g(),ce(t),T(t.getState(`popover`)),re(),M(t),t.resetEmitter();let u=t.getState();if(t.resetState(),i&&a){let n=i.id===`driver-dummy-element`;c&&c(n?void 0:i,a,t.getHookOpts(u,e)),l&&l(n?void 0:i,a,t.getHookOpts(u,e))}o&&o.focus()}let b={isActive:()=>t.getState(`isInitialized`)||!1,refresh:()=>Q(t),drive:(e=0)=>{h(),v(e)},setConfig:t.setConfig,setSteps:e=>{g(),t.resetState(),t.setConfig({...t.getConfig(),steps:e})},getConfig:t.getConfig,getState:t.getState,getActiveIndex:()=>t.getState(`activeIndex`),isFirstStep:()=>{let e=t.getState(`activeIndex`);return e!==void 0&&F(t,e-1,-1)===void 0},isLastStep:()=>{let e=t.getState(`activeIndex`);return e!==void 0&&F(t,e+1,1)===void 0},getActiveStep:()=>t.getState(`activeStep`),getActiveElement:()=>t.getState(`activeElement`),getPreviousElement:()=>t.getState(`previousElement`),getPreviousStep:()=>t.getState(`previousStep`),getNextStep:()=>{let e=t.getConfig(`steps`)||[],n=t.getState(`activeIndex`);if(n===void 0)return;let r=F(t,n+1,1);return r===void 0?void 0:e[r]},moveNext:c,movePrevious:l,moveTo:u,hasNextStep:()=>{let e=t.getState(`activeIndex`);return e!==void 0&&F(t,e+1,1)!==void 0},hasPreviousStep:()=>{let e=t.getState(`activeIndex`);return e!==void 0&&F(t,e-1,-1)!==void 0},highlight:e=>{h(),t.setState(`__destroyReason`,void 0),X(t,{...e,popover:e.popover?{showButtons:[],showProgress:!1,progressText:``,...e.popover}:void 0})},destroy:()=>{y(`api`,!1)}};return t.setDriver(b),b}export{pe as driver};
@@ -0,0 +1,39 @@
1
+ /* Dark mode for the driver.js tour popover. driver.css is vendored unmodified, so the
2
+ overrides live here and must be imported after it. */
3
+ .driver-popover {
4
+ @apply dark:bg-gray-800 dark:text-gray-100 dark:ring-1 dark:ring-white/10;
5
+ }
6
+
7
+ .driver-popover-description {
8
+ @apply dark:text-gray-300;
9
+ }
10
+
11
+ .driver-popover-progress-text {
12
+ @apply dark:text-gray-400;
13
+ }
14
+
15
+ .driver-popover-close-btn {
16
+ @apply dark:text-gray-500 dark:hover:text-gray-200 dark:focus:text-gray-200;
17
+ }
18
+
19
+ .driver-popover-footer button {
20
+ @apply dark:bg-gray-700 dark:text-gray-100 dark:border-gray-600 dark:[text-shadow:none] dark:hover:bg-gray-600 dark:focus:bg-gray-600;
21
+ }
22
+
23
+ /* The arrow is a border triangle: only the side pointing at the element is coloured,
24
+ the other three stay transparent. */
25
+ .driver-popover-arrow-side-left {
26
+ @apply dark:border-l-gray-800;
27
+ }
28
+
29
+ .driver-popover-arrow-side-right {
30
+ @apply dark:border-r-gray-800;
31
+ }
32
+
33
+ .driver-popover-arrow-side-top {
34
+ @apply dark:border-t-gray-800;
35
+ }
36
+
37
+ .driver-popover-arrow-side-bottom {
38
+ @apply dark:border-b-gray-800;
39
+ }
@@ -1,5 +1,5 @@
1
1
  import ApplicationController from "mensa/controllers/application_controller";
2
- import { driver } from "driver.js";
2
+ import { driver } from "mensa/components/table/driver";
3
3
 
4
4
  // Explains the table's features the first time each one is seen. Every feature is
5
5
  // its own segment, remembered in localStorage, so a feature missing on one table is
@@ -11,7 +11,7 @@
11
11
  }
12
12
 
13
13
  .mensa-table__views__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;
14
+ @apply fixed z-50 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
15
  }
16
16
 
17
17
  .mensa-table__views__option {
@@ -55,21 +55,21 @@
55
55
  </div>
56
56
 
57
57
  <dialog class="mensa-table__views__rename-dialog" data-mensa-views-target="renameDialog" data-action="click->mensa-views#renameDialogBackdrop">
58
- <form class="mensa-table__views__rename-form" data-action="submit->mensa-views#confirmRename">
58
+ <div class="mensa-table__views__rename-form">
59
59
  <input type="hidden" data-mensa-views-target="renameViewId">
60
60
  <h3 class="mensa-table__views__rename-title"><%= t(".rename_view", default: "Rename view") %></h3>
61
61
  <label class="mensa-table__views__rename-label">
62
62
  <%= t(".view_name", default: "Name") %>
63
- <input class="mensa-table__views__rename-input" type="text" required placeholder="<%= t(".view_name_placeholder", default: "View name") %>" data-mensa-views-target="renameInput">
63
+ <input class="mensa-table__views__rename-input" type="text" aria-required="true" placeholder="<%= t(".view_name_placeholder", default: "View name") %>" data-required-message="<%= t(".view_name_required", default: "Please enter a name for this view.") %>" data-mensa-views-target="renameInput" data-action="keydown.enter->mensa-views#confirmRename input->mensa-views#clearRenameValidity">
64
64
  </label>
65
65
  <div class="mensa-table__views__rename-actions">
66
66
  <button class="mensa-table__views__rename-btn mensa-table__views__rename-btn--secondary" type="button" data-action="mensa-views#cancelRename">
67
67
  <%= t(".cancel", default: "Cancel") %>
68
68
  </button>
69
- <button class="mensa-table__views__rename-btn mensa-table__views__rename-btn--primary" type="submit">
69
+ <button class="mensa-table__views__rename-btn mensa-table__views__rename-btn--primary" type="button" data-action="mensa-views#confirmRename">
70
70
  <%= t(".rename", default: "Rename") %>
71
71
  </button>
72
72
  </div>
73
- </form>
73
+ </div>
74
74
  </dialog>
75
75
  </div>
@@ -90,6 +90,7 @@ export default class ViewsComponentController extends ApplicationController {
90
90
  if (this.hasRenameDialogTarget) {
91
91
  this.renameViewIdTarget.value = viewId;
92
92
  this.renameInputTarget.value = viewName;
93
+ this.renameInputTarget.setCustomValidity("");
93
94
  if (typeof this.renameDialogTarget.showModal === "function") {
94
95
  this.renameDialogTarget.showModal();
95
96
  } else {
@@ -111,11 +112,18 @@ export default class ViewsComponentController extends ApplicationController {
111
112
  }
112
113
 
113
114
  async confirmRename(event) {
115
+ // Called from a button click or Enter in the name input. The table may
116
+ // sit inside a host <form>; this stops Enter from submitting it.
114
117
  event.preventDefault();
115
118
  const viewId = this.renameViewIdTarget.value;
116
119
  const newName = this.renameInputTarget.value.trim();
117
120
  if (!newName) {
118
- this.renameInputTarget.reportValidity();
121
+ // The input isn't `required` (that would block a host <form>), so
122
+ // the browser's message is set by hand and cleared on input.
123
+ const input = this.renameInputTarget;
124
+ input.setCustomValidity(input.dataset.requiredMessage || "");
125
+ input.reportValidity();
126
+ input.focus();
119
127
  return;
120
128
  }
121
129
 
@@ -131,6 +139,10 @@ export default class ViewsComponentController extends ApplicationController {
131
139
  }
132
140
  }
133
141
 
142
+ clearRenameValidity() {
143
+ this.renameInputTarget.setCustomValidity("");
144
+ }
145
+
134
146
  async duplicateView(event) {
135
147
  event.preventDefault();
136
148
  const viewId = this.submenuTarget.dataset.viewId;
@@ -198,10 +210,19 @@ export default class ViewsComponentController extends ApplicationController {
198
210
  // --- private ---
199
211
 
200
212
  _openDropdown() {
213
+ this._positionDropdown();
201
214
  this.dropdownTarget.classList.remove("hidden");
202
215
  this._bindOutsideClick();
203
216
  }
204
217
 
218
+ // Fixed positioning with viewport coordinates, so a small table (or a host
219
+ // element with overflow hidden) doesn't clip the dropdown.
220
+ _positionDropdown() {
221
+ const rect = this.triggerTarget.getBoundingClientRect();
222
+ this.dropdownTarget.style.top = `${rect.bottom + 4}px`;
223
+ this.dropdownTarget.style.left = `${rect.left}px`;
224
+ }
225
+
205
226
  _closeDropdown() {
206
227
  this.dropdownTarget.classList.add("hidden");
207
228
  this._closeSubmenu();
@@ -34,6 +34,9 @@ module Mensa
34
34
  return unless aggregates.key?(column.name)
35
35
 
36
36
  value = aggregates[column.name]
37
+ # SUM of a bigint column comes back as a BigDecimal, which would show as
38
+ # "3,000.0". Whole numbers are shown without decimals.
39
+ value = value.to_i if value.is_a?(BigDecimal) && value.frac.zero?
37
40
  return ActiveSupport::NumberHelper.number_to_delimited(value) if value.is_a?(Numeric)
38
41
 
39
42
  Mensa::Cell.new(row: nil, column: column, value: value).formatted