@antelopejs/dms 0.4.6 → 0.4.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.7
4
+
5
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.4.6...v0.4.7)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **interface-dms:** Export the password policy for modules ([#70](https://github.com/AntelopeJS/dms/pull/70))
10
+
11
+ ### 🩹 Fixes
12
+
13
+ - **dms-ui:** Let Escape close modals opened over a table or a tree ([#71](https://github.com/AntelopeJS/dms/pull/71))
14
+
15
+ ### 🏡 Chore
16
+
17
+ - **release:** @antelopejs/interface-dms v0.2.6 ([67d4e23](https://github.com/AntelopeJS/dms/commit/67d4e23))
18
+
19
+ ### ❤️ Contributors
20
+
21
+ - Antony Rizzitelli <rizzitelli.antony@pm.me>
22
+
3
23
  ## v0.4.6
4
24
 
5
25
  [compare changes](https://github.com/AntelopeJS/dms/compare/v0.4.5...v0.4.6)
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const password_1 = require("@antelopejs/interface-dms/auth/password");
5
+ const COMPLIANT_PASSWORD = "StrongP@ss1";
6
+ const ALLOWED_SPECIAL_CHARS = "@$!%*?&";
7
+ describe("[unit] auth/password policy", () => {
8
+ it("requires at least 8 characters", () => {
9
+ (0, chai_1.expect)(password_1.PASSWORD_MIN_LENGTH).to.equal(8);
10
+ });
11
+ it("accepts a password meeting every rule", () => {
12
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)(COMPLIANT_PASSWORD)).to.equal(true);
13
+ });
14
+ it("accepts a password of exactly the minimum length", () => {
15
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("Abcdef1@")).to.equal(true);
16
+ });
17
+ it("accepts every allowed special character", () => {
18
+ for (const special of ALLOWED_SPECIAL_CHARS) {
19
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)(`Abcdef1${special}`), special).to.equal(true);
20
+ }
21
+ });
22
+ it("rejects a password shorter than the minimum length", () => {
23
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("Abcde1@")).to.equal(false);
24
+ });
25
+ it("rejects a password without an uppercase letter", () => {
26
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("weakp@ss1")).to.equal(false);
27
+ });
28
+ it("rejects a password without a digit", () => {
29
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("Weakpass@")).to.equal(false);
30
+ });
31
+ it("rejects a password without a special character", () => {
32
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("Weakpass1")).to.equal(false);
33
+ });
34
+ it("rejects a character outside the allowed set", () => {
35
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("StrongP@ss1#")).to.equal(false);
36
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("StrongP@ss1 ")).to.equal(false);
37
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("StrongP@ss1é")).to.equal(false);
38
+ });
39
+ it("rejects an empty password", () => {
40
+ (0, chai_1.expect)((0, password_1.isPasswordCompliant)("")).to.equal(false);
41
+ });
42
+ });
43
+ //# sourceMappingURL=password-policy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password-policy.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms/password-policy.test.ts"],"names":[],"mappings":";;AAAA,+BAA8B;AAC9B,sEAGiD;AAEjD,MAAM,kBAAkB,GAAG,aAAa,CAAC;AACzC,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAExC,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,IAAA,aAAM,EAAC,8BAAmB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,KAAK,MAAM,OAAO,IAAI,qBAAqB,EAAE,CAAC;YAC5C,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,UAAU,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5D,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5D,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,IAAA,aAAM,EAAC,IAAA,8BAAmB,EAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,3 +1,2 @@
1
1
  import * as z from "zod";
2
- export declare const PASSWORD_REGEX: RegExp;
3
2
  export declare const passwordSchema: z.ZodString;
@@ -33,8 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.passwordSchema = exports.PASSWORD_REGEX = void 0;
36
+ exports.passwordSchema = void 0;
37
37
  const z = __importStar(require("zod"));
38
- exports.PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/;
39
- exports.passwordSchema = z.string().min(8).regex(exports.PASSWORD_REGEX);
38
+ const password_1 = require("@antelopejs/interface-dms/auth/password");
39
+ exports.passwordSchema = z
40
+ .string()
41
+ .min(password_1.PASSWORD_MIN_LENGTH)
42
+ .regex(password_1.PASSWORD_PATTERN);
40
43
  //# sourceMappingURL=password.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/validation/password.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AAEZ,QAAA,cAAc,GACzB,wDAAwD,CAAC;AAE9C,QAAA,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,sBAAc,CAAC,CAAC"}
1
+ {"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/validation/password.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,sEAGiD;AAEpC,QAAA,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,GAAG,CAAC,8BAAmB,CAAC;KACxB,KAAK,CAAC,2BAAgB,CAAC,CAAC"}
@@ -44,6 +44,10 @@
44
44
  "title": "Restoring your session...",
45
45
  "description": "Your session has expired. Refreshing your access and reloading the page."
46
46
  },
47
+ "network": {
48
+ "title": "Connection lost",
49
+ "description": "The server could not be reached. Check your connection and try again."
50
+ },
47
51
  "switch_account_failed": "Failed to switch account",
48
52
  "email_already_used": "This email address is already used.",
49
53
  "user_not_found": "No user found with this email address.",
@@ -44,6 +44,10 @@
44
44
  "title": "Restauration de votre session...",
45
45
  "description": "Votre session a expiré. Renouvellement de l'accès et rechargement de la page."
46
46
  },
47
+ "network": {
48
+ "title": "Connexion perdue",
49
+ "description": "Le serveur est injoignable. Vérifiez votre connexion et réessayez."
50
+ },
47
51
  "switch_account_failed": "Échec du changement de compte",
48
52
  "email_already_requested": "Cette adresse e-mail a déjà été demandée. Veuillez attendre avant de réessayer.",
49
53
  "email_already_used": "Cette adresse e-mail est déjà utilisée.",
@@ -7,7 +7,7 @@ import type {
7
7
  Table,
8
8
  VisibilityState,
9
9
  } from "@tanstack/vue-table";
10
- import { refDebounced, watchDebounced } from "@vueuse/core";
10
+ import { refDebounced, useActiveElement, watchDebounced } from "@vueuse/core";
11
11
  import {
12
12
  QUICK_ACTION_BUTTON_KEY,
13
13
  QUICK_ACTION_COMPONENT_KEY,
@@ -1010,6 +1010,7 @@ defineShortcuts(
1010
1010
  globalFilter,
1011
1011
  rowSelect,
1012
1012
  data: shownData,
1013
+ activeElement: useActiveElement(),
1013
1014
  }),
1014
1015
  );
1015
1016
 
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { useActiveElement } from "@vueuse/core";
2
3
  import type { TreeNode, TreeProps } from "../../composables/tree/types";
3
4
  import {
4
5
  TreeSelectionBehavior,
@@ -120,15 +121,15 @@ function navigateTree(direction: TreeNavigationDirection) {
120
121
  navigationHandlers[direction]();
121
122
  }
122
123
 
123
- const shortcutHandlers: Record<string, () => void> = (
124
- buildTreeShortcuts as (...args: unknown[]) => Record<string, () => void>
125
- )({
126
- navigateTree,
127
- props,
128
- items,
129
- selected,
130
- });
131
- defineShortcuts(shortcutHandlers as Record<string, (() => void) | undefined>);
124
+ defineShortcuts(
125
+ buildTreeShortcuts({
126
+ navigateTree,
127
+ props,
128
+ items,
129
+ selected,
130
+ activeElement: useActiveElement(),
131
+ }),
132
+ );
132
133
  </script>
133
134
 
134
135
  <template>
@@ -1,4 +1,4 @@
1
- import type { ShallowRef } from "vue";
1
+ import { computed, type ComputedRef, type ShallowRef } from "vue";
2
2
  import type { RowSelectionState } from "@tanstack/vue-table";
3
3
  import type {
4
4
  TableRowActionOptions,
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  TABLE_VIEW_ESCAPE_METADATA,
17
17
  createTableViewEscapeShortcut,
18
+ isTableViewEscapeActive,
18
19
  } from "./tableViewEscape";
19
20
  import {
20
21
  TABLE_VIEW_SHIFT_A_METADATA,
@@ -42,8 +43,11 @@ interface BuildTableViewShortcutsParams<T extends Data> {
42
43
  globalFilter: ShallowRef<string | undefined>;
43
44
  rowSelect: Ref<RowSelectionState>;
44
45
  data: Ref<{ results: T[] } | null | undefined>;
46
+ activeElement: Ref<Element | null | undefined>;
45
47
  }
46
48
 
49
+ type TableViewShortcuts = Record<string, (() => void) | undefined>;
50
+
47
51
  export function buildTableViewShortcuts<T extends Data>({
48
52
  refresh,
49
53
  tableProps,
@@ -51,11 +55,20 @@ export function buildTableViewShortcuts<T extends Data>({
51
55
  globalFilter,
52
56
  rowSelect,
53
57
  data,
54
- }: BuildTableViewShortcutsParams<T>): Record<string, () => void> {
55
- return {
58
+ activeElement,
59
+ }: BuildTableViewShortcutsParams<T>): ComputedRef<TableViewShortcuts> {
60
+ const shortcuts = {
56
61
  meta_r: createTableViewMetaRShortcut(refresh),
57
62
  shift_n: createTableViewShiftNShortcut(tableProps, newRow),
58
- escape: createTableViewEscapeShortcut(globalFilter, rowSelect),
59
63
  shift_a: createTableViewShiftAShortcut(tableProps, data, rowSelect),
60
64
  };
65
+ const escape = createTableViewEscapeShortcut(globalFilter, rowSelect);
66
+ // An unregistered key is left untouched by `defineShortcuts`, so Escape
67
+ // reaches overlays whenever the table has nothing to clear.
68
+ return computed(() => ({
69
+ ...shortcuts,
70
+ escape: isTableViewEscapeActive(globalFilter, rowSelect, activeElement)
71
+ ? escape
72
+ : undefined,
73
+ }));
61
74
  }
@@ -1,12 +1,34 @@
1
1
  import type { ShallowRef } from "vue";
2
2
  import type { RowSelectionState } from "@tanstack/vue-table";
3
+ import { isFocusInsideOverlay } from "../../../utils/overlayFocus";
3
4
 
4
5
  export const TABLE_VIEW_ESCAPE_METADATA: ShortcutMetadata = {
5
6
  key: ["$keyboard.escape"],
6
7
  descriptionKey: "$dms.shortcuts.tableview.escape.description",
7
8
  component: "$dms.components.tableview",
9
+ condition: {
10
+ descriptionKey: "$dms.shortcuts.tableview.escape.condition",
11
+ },
8
12
  };
9
13
 
14
+ /**
15
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
16
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. The
17
+ * shortcut is therefore only registered when there is a search or a
18
+ * selection to clear, and never while focus sits inside an overlay, so the
19
+ * overlay above the table keeps closing on Escape.
20
+ */
21
+ export function isTableViewEscapeActive(
22
+ globalFilter: ShallowRef<string | undefined>,
23
+ rowSelect: Ref<RowSelectionState>,
24
+ activeElement: Ref<Element | null | undefined>,
25
+ ): boolean {
26
+ if (isFocusInsideOverlay(activeElement.value)) return false;
27
+ return (
28
+ globalFilter.value !== undefined || Object.keys(rowSelect.value).length > 0
29
+ );
30
+ }
31
+
10
32
  export function createTableViewEscapeShortcut(
11
33
  globalFilter: ShallowRef<string | undefined>,
12
34
  rowSelect: Ref<RowSelectionState>,
@@ -1,3 +1,4 @@
1
+ import { computed, type ComputedRef } from "vue";
1
2
  import {
2
3
  TREE_ARROW_DOWN_METADATA,
3
4
  createTreeArrowDownShortcut,
@@ -11,7 +12,11 @@ import { TREE_SPACE_METADATA, createTreeSpaceShortcut } from "./treeSpace";
11
12
  import { TREE_HOME_METADATA, createTreeHomeShortcut } from "./treeHome";
12
13
  import { TREE_END_METADATA, createTreeEndShortcut } from "./treeEnd";
13
14
  import { TREE_SHIFT_A_METADATA, createTreeShiftAShortcut } from "./treeShiftA";
14
- import { TREE_ESCAPE_METADATA, createTreeEscapeShortcut } from "./treeEscape";
15
+ import {
16
+ TREE_ESCAPE_METADATA,
17
+ createTreeEscapeShortcut,
18
+ isTreeEscapeActive,
19
+ } from "./treeEscape";
15
20
 
16
21
  export const TREE_SHORTCUTS_METADATA = [
17
22
  TREE_ARROW_DOWN_METADATA,
@@ -29,15 +34,19 @@ interface BuildTreeShortcutsParams {
29
34
  props: TreeProps;
30
35
  items: ComputedRef<TreeNode[]>;
31
36
  selected: Ref<TreeNode | TreeNode[] | undefined>;
37
+ activeElement: Ref<Element | null | undefined>;
32
38
  }
33
39
 
40
+ type TreeShortcuts = Record<string, (() => void) | undefined>;
41
+
34
42
  export function buildTreeShortcuts({
35
43
  navigateTree,
36
44
  props,
37
45
  items,
38
46
  selected,
39
- }: BuildTreeShortcutsParams): Record<string, () => void> {
40
- return {
47
+ activeElement,
48
+ }: BuildTreeShortcutsParams): ComputedRef<TreeShortcuts> {
49
+ const shortcuts = {
41
50
  arrowdown: createTreeArrowDownShortcut(navigateTree),
42
51
  arrowup: createTreeArrowUpShortcut(navigateTree),
43
52
  enter: createTreeEnterShortcut(navigateTree),
@@ -45,6 +54,12 @@ export function buildTreeShortcuts({
45
54
  home: createTreeHomeShortcut(navigateTree),
46
55
  end: createTreeEndShortcut(navigateTree),
47
56
  shift_a: createTreeShiftAShortcut(props, items, selected),
48
- escape: createTreeEscapeShortcut(props, selected),
49
57
  };
58
+ const escape = createTreeEscapeShortcut(props, selected);
59
+ // An unregistered key is left untouched by `defineShortcuts`, so Escape
60
+ // reaches overlays whenever the tree has nothing to clear.
61
+ return computed(() => ({
62
+ ...shortcuts,
63
+ escape: isTreeEscapeActive(selected, activeElement) ? escape : undefined,
64
+ }));
50
65
  }
@@ -1,9 +1,30 @@
1
+ import { isFocusInsideOverlay } from "../../../utils/overlayFocus";
2
+
1
3
  export const TREE_ESCAPE_METADATA: ShortcutMetadata = {
2
4
  key: ["$keyboard.escape"],
3
5
  descriptionKey: "$dms.shortcuts.tree.escape.description",
4
6
  component: "$dms.components.tree",
7
+ condition: {
8
+ descriptionKey: "$dms.shortcuts.tree.escape.condition",
9
+ },
5
10
  };
6
11
 
12
+ /**
13
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
14
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. The
15
+ * shortcut is therefore only registered when there is a selection to clear,
16
+ * and never while focus sits inside an overlay, so the overlay above the tree
17
+ * keeps closing on Escape.
18
+ */
19
+ export function isTreeEscapeActive(
20
+ selected: Ref<TreeNode | TreeNode[] | undefined>,
21
+ activeElement: Ref<Element | null | undefined>,
22
+ ): boolean {
23
+ if (isFocusInsideOverlay(activeElement.value)) return false;
24
+ const value = selected.value;
25
+ return Array.isArray(value) ? value.length > 0 : value != null;
26
+ }
27
+
7
28
  export function createTreeEscapeShortcut(
8
29
  props: TreeProps,
9
30
  selected: Ref<TreeNode | TreeNode[] | undefined>,
@@ -0,0 +1,16 @@
1
+ // Overlays reka-ui dismisses on Escape (modals, slideovers, popovers, menus,
2
+ // selects). They move focus inside themselves while open.
3
+ const OVERLAY_SELECTOR =
4
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
5
+
6
+ /**
7
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
8
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. A
9
+ * component-level Escape shortcut must therefore stay unregistered while
10
+ * focus sits inside an overlay, so the overlay keeps closing on Escape.
11
+ */
12
+ export function isFocusInsideOverlay(
13
+ activeElement: Element | null | undefined,
14
+ ): boolean {
15
+ return Boolean(activeElement?.closest(OVERLAY_SELECTOR));
16
+ }
@@ -292,7 +292,8 @@
292
292
  "description": "Refresh table data"
293
293
  },
294
294
  "escape": {
295
- "description": "Close search or deselect rows"
295
+ "description": "Close search or deselect rows",
296
+ "condition": "Only when a search or a row selection is active and no dialog is open"
296
297
  },
297
298
  "shift_n": {
298
299
  "description": "Create new row",
@@ -330,7 +331,8 @@
330
331
  "description": "Go to last node"
331
332
  },
332
333
  "escape": {
333
- "description": "Deselect all nodes"
334
+ "description": "Deselect all nodes",
335
+ "condition": "Only when nodes are selected and no dialog is open"
334
336
  },
335
337
  "shift_a": {
336
338
  "description": "Select all nodes recursively",
@@ -293,7 +293,8 @@
293
293
  "description": "Actualiser les données du tableau"
294
294
  },
295
295
  "escape": {
296
- "description": "Fermer la recherche ou désélectionner les lignes"
296
+ "description": "Fermer la recherche ou désélectionner les lignes",
297
+ "condition": "Uniquement quand une recherche ou une sélection est active et qu'aucune fenêtre n'est ouverte"
297
298
  },
298
299
  "shift_n": {
299
300
  "description": "Créer une nouvelle ligne",
@@ -331,7 +332,8 @@
331
332
  "description": "Aller au dernier nœud"
332
333
  },
333
334
  "escape": {
334
- "description": "Désélectionner tous les nœuds"
335
+ "description": "Désélectionner tous les nœuds",
336
+ "condition": "Uniquement quand des nœuds sont sélectionnés et qu'aucune fenêtre n'est ouverte"
335
337
  },
336
338
  "shift_a": {
337
339
  "description": "Sélectionner tous les nœuds récursivement",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@antelopejs/core": ">=1.9.1 <2",
62
- "@antelopejs/dms-frontend": ">=0.2.8 <1.0.0",
62
+ "@antelopejs/dms-frontend": ">=0.3.2 <1.0.0",
63
63
  "@eslint/js": "^8.57.0",
64
64
  "@inertiajs/vue3": "^3.7.0",
65
65
  "@nuxt/ui": "4.10.0",
@@ -124,8 +124,8 @@ importers:
124
124
  specifier: '>=1.9.1 <2'
125
125
  version: 1.9.1(@types/node@24.8.1)
126
126
  '@antelopejs/dms-frontend':
127
- specifier: '>=0.2.8 <1.0.0'
128
- version: 0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))
127
+ specifier: '>=0.3.2 <1.0.0'
128
+ version: 0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))
129
129
  '@eslint/js':
130
130
  specifier: ^8.57.0
131
131
  version: 8.57.0
@@ -200,8 +200,8 @@ packages:
200
200
  resolution: {integrity: sha512-MPLJak0rPluV4lgsdsO5ba03sgTxIGSncajBcoaoppuppQmksdpaemojIHiVLJ4LiG0iiH21Roaed8XXy1XkYQ==}
201
201
  hasBin: true
202
202
 
203
- '@antelopejs/dms-frontend@0.2.8':
204
- resolution: {integrity: sha512-7qrlf2M3J26DZxGZVvM/3qjPDeDfoRMrE7HEPFxWav8Pe1QFct7LtgY7NmKup9XdrRyUw4nqckxvW8UZDSjpeg==}
203
+ '@antelopejs/dms-frontend@0.3.2':
204
+ resolution: {integrity: sha512-BNqLqW3UbfeOMnd4RkEynr/1mMWFhwB8dt1JbggaGte8/KrJQ3MQ5TOovnACMmrqAD7FxsMLBsNqjzGjX4O4ZA==}
205
205
  engines: {node: '>=20.12.0'}
206
206
  hasBin: true
207
207
  peerDependencies:
@@ -4331,7 +4331,7 @@ snapshots:
4331
4331
  transitivePeerDependencies:
4332
4332
  - '@types/node'
4333
4333
 
4334
- '@antelopejs/dms-frontend@0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))':
4334
+ '@antelopejs/dms-frontend@0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))':
4335
4335
  dependencies:
4336
4336
  boxen: 5.1.2
4337
4337
  chalk: 4.1.2
@@ -0,0 +1,141 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import {
4
+ computed,
5
+ createApp,
6
+ defineComponent,
7
+ nextTick,
8
+ ref,
9
+ shallowRef,
10
+ type App,
11
+ type Ref,
12
+ type ShallowRef,
13
+ } from "vue";
14
+ import { useActiveElement } from "@vueuse/core";
15
+ import type { RowSelectionState } from "@tanstack/vue-table";
16
+ import { defineShortcuts } from "@nuxt/ui/runtime/composables/defineShortcuts.js";
17
+ import {
18
+ TABLE_VIEW_SHORTCUTS_METADATA,
19
+ buildTableViewShortcuts,
20
+ } from "../layers/dms-ui/app/composables/table-view/shortcuts";
21
+
22
+ let app: App;
23
+ let host: HTMLDivElement;
24
+ let globalFilter: ShallowRef<string | undefined>;
25
+ let rowSelect: Ref<RowSelectionState>;
26
+
27
+ function mountTableShortcuts() {
28
+ const Harness = defineComponent({
29
+ setup() {
30
+ defineShortcuts(
31
+ buildTableViewShortcuts({
32
+ refresh: () => {},
33
+ tableProps: computed(() => ({})),
34
+ newRow: () => {},
35
+ globalFilter,
36
+ rowSelect,
37
+ data: ref(null),
38
+ activeElement: useActiveElement(),
39
+ }),
40
+ );
41
+ return () => null;
42
+ },
43
+ });
44
+ app = createApp(Harness);
45
+ app.mount(host);
46
+ }
47
+
48
+ function pressEscape(target: EventTarget = document.body): KeyboardEvent {
49
+ const event = new KeyboardEvent("keydown", {
50
+ key: "Escape",
51
+ code: "Escape",
52
+ bubbles: true,
53
+ cancelable: true,
54
+ });
55
+ target.dispatchEvent(event);
56
+ return event;
57
+ }
58
+
59
+ beforeEach(() => {
60
+ host = document.createElement("div");
61
+ document.body.appendChild(host);
62
+ globalFilter = shallowRef<string | undefined>(undefined);
63
+ rowSelect = ref<RowSelectionState>({});
64
+ });
65
+
66
+ afterEach(() => {
67
+ app.unmount();
68
+ document.body.innerHTML = "";
69
+ });
70
+
71
+ describe("TableView Escape shortcut", () => {
72
+ it("leaves Escape to overlays when there is nothing to clear", async () => {
73
+ mountTableShortcuts();
74
+ await nextTick();
75
+
76
+ expect(pressEscape().defaultPrevented).toBe(false);
77
+ });
78
+
79
+ it("closes the search when a global filter is set", async () => {
80
+ globalFilter.value = "acme";
81
+ mountTableShortcuts();
82
+ await nextTick();
83
+
84
+ expect(pressEscape().defaultPrevented).toBe(true);
85
+ expect(globalFilter.value).toBeUndefined();
86
+ });
87
+
88
+ it("clears the row selection when no search is open", async () => {
89
+ rowSelect.value = { a: true, b: true };
90
+ mountTableShortcuts();
91
+ await nextTick();
92
+
93
+ expect(pressEscape().defaultPrevented).toBe(true);
94
+ expect(rowSelect.value).toEqual({});
95
+ });
96
+
97
+ it("closes the search before touching the selection", async () => {
98
+ globalFilter.value = "";
99
+ rowSelect.value = { a: true };
100
+ mountTableShortcuts();
101
+ await nextTick();
102
+
103
+ pressEscape();
104
+ expect(globalFilter.value).toBeUndefined();
105
+ expect(rowSelect.value).toEqual({ a: true });
106
+ });
107
+
108
+ it("stops handling Escape once the filter is cleared", async () => {
109
+ globalFilter.value = "acme";
110
+ mountTableShortcuts();
111
+ await nextTick();
112
+
113
+ pressEscape();
114
+ await nextTick();
115
+ expect(pressEscape().defaultPrevented).toBe(false);
116
+ });
117
+
118
+ it("lets an open dialog take Escape even with a filter set", async () => {
119
+ globalFilter.value = "acme";
120
+ mountTableShortcuts();
121
+ const dialog = document.createElement("div");
122
+ dialog.setAttribute("role", "dialog");
123
+ const button = document.createElement("button");
124
+ dialog.appendChild(button);
125
+ document.body.appendChild(dialog);
126
+ button.focus();
127
+ await nextTick();
128
+
129
+ expect(pressEscape(button).defaultPrevented).toBe(false);
130
+ expect(globalFilter.value).toBe("acme");
131
+ });
132
+
133
+ it("describes when the shortcut applies in the help list", () => {
134
+ const escape = TABLE_VIEW_SHORTCUTS_METADATA.find((metadata) =>
135
+ metadata.key.includes("$keyboard.escape"),
136
+ );
137
+ expect(escape?.condition?.descriptionKey).toBe(
138
+ "$dms.shortcuts.tableview.escape.condition",
139
+ );
140
+ });
141
+ });
@@ -0,0 +1,138 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import {
4
+ computed,
5
+ createApp,
6
+ defineComponent,
7
+ nextTick,
8
+ ref,
9
+ type App,
10
+ type Ref,
11
+ } from "vue";
12
+ import { useActiveElement } from "@vueuse/core";
13
+ import { defineShortcuts } from "@nuxt/ui/runtime/composables/defineShortcuts.js";
14
+ import {
15
+ TREE_SHORTCUTS_METADATA,
16
+ buildTreeShortcuts,
17
+ } from "../layers/dms-ui/app/composables/tree/shortcuts";
18
+ import type {
19
+ TreeNode,
20
+ TreeProps,
21
+ } from "../layers/dms-ui/app/composables/tree/types";
22
+
23
+ let app: App;
24
+ let host: HTMLDivElement;
25
+ let selected: Ref<TreeNode | TreeNode[] | undefined>;
26
+
27
+ const nodeA = { label: "A", value: "a" } as TreeNode;
28
+ const nodeB = { label: "B", value: "b" } as TreeNode;
29
+
30
+ function mountTreeShortcuts(multiple = false) {
31
+ const Harness = defineComponent({
32
+ setup() {
33
+ defineShortcuts(
34
+ buildTreeShortcuts({
35
+ navigateTree: () => {},
36
+ props: { multiple } as TreeProps,
37
+ items: computed(() => [nodeA, nodeB]),
38
+ selected,
39
+ activeElement: useActiveElement(),
40
+ }),
41
+ );
42
+ return () => null;
43
+ },
44
+ });
45
+ app = createApp(Harness);
46
+ app.mount(host);
47
+ }
48
+
49
+ function pressEscape(target: EventTarget = document.body): KeyboardEvent {
50
+ const event = new KeyboardEvent("keydown", {
51
+ key: "Escape",
52
+ code: "Escape",
53
+ bubbles: true,
54
+ cancelable: true,
55
+ });
56
+ target.dispatchEvent(event);
57
+ return event;
58
+ }
59
+
60
+ beforeEach(() => {
61
+ host = document.createElement("div");
62
+ document.body.appendChild(host);
63
+ selected = ref<TreeNode | TreeNode[] | undefined>(undefined);
64
+ });
65
+
66
+ afterEach(() => {
67
+ app.unmount();
68
+ document.body.innerHTML = "";
69
+ });
70
+
71
+ describe("Tree Escape shortcut", () => {
72
+ it("leaves Escape to overlays when nothing is selected", async () => {
73
+ mountTreeShortcuts();
74
+ await nextTick();
75
+
76
+ expect(pressEscape().defaultPrevented).toBe(false);
77
+ });
78
+
79
+ it("leaves Escape to overlays when a multiple selection is empty", async () => {
80
+ selected.value = [];
81
+ mountTreeShortcuts(true);
82
+ await nextTick();
83
+
84
+ expect(pressEscape().defaultPrevented).toBe(false);
85
+ });
86
+
87
+ it("deselects the node in single selection mode", async () => {
88
+ selected.value = nodeA;
89
+ mountTreeShortcuts();
90
+ await nextTick();
91
+
92
+ expect(pressEscape().defaultPrevented).toBe(true);
93
+ expect(selected.value).toBeUndefined();
94
+ });
95
+
96
+ it("clears every node in multiple selection mode", async () => {
97
+ selected.value = [nodeA, nodeB];
98
+ mountTreeShortcuts(true);
99
+ await nextTick();
100
+
101
+ expect(pressEscape().defaultPrevented).toBe(true);
102
+ expect(selected.value).toEqual([]);
103
+ });
104
+
105
+ it("stops handling Escape once the selection is cleared", async () => {
106
+ selected.value = [nodeA];
107
+ mountTreeShortcuts(true);
108
+ await nextTick();
109
+
110
+ pressEscape();
111
+ await nextTick();
112
+ expect(pressEscape().defaultPrevented).toBe(false);
113
+ });
114
+
115
+ it("lets an open dialog take Escape even with a selection", async () => {
116
+ selected.value = nodeA;
117
+ mountTreeShortcuts();
118
+ const dialog = document.createElement("div");
119
+ dialog.setAttribute("role", "dialog");
120
+ const button = document.createElement("button");
121
+ dialog.appendChild(button);
122
+ document.body.appendChild(dialog);
123
+ button.focus();
124
+ await nextTick();
125
+
126
+ expect(pressEscape(button).defaultPrevented).toBe(false);
127
+ expect(selected.value).toStrictEqual(nodeA);
128
+ });
129
+
130
+ it("describes when the shortcut applies in the help list", () => {
131
+ const escape = TREE_SHORTCUTS_METADATA.find((metadata) =>
132
+ metadata.key.includes("$keyboard.escape"),
133
+ );
134
+ expect(escape?.condition?.descriptionKey).toBe(
135
+ "$dms.shortcuts.tree.escape.condition",
136
+ );
137
+ });
138
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -36,7 +36,7 @@
36
36
  "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
37
37
  "@antelopejs/interface-database": ">=0.1.7 <1.0.0",
38
38
  "@antelopejs/interface-database-decorators": ">=0.1.6 <1.0.0",
39
- "@antelopejs/interface-dms": ">=0.2.5 <1.0.0",
39
+ "@antelopejs/interface-dms": ">=0.2.6 <1.0.0",
40
40
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
41
41
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
42
42
  "archiver": "^7.0.1",