aloha-vue 2.80.0 → 2.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import {
2
+ nextTick,
3
+ ref,
4
+ } from "vue";
5
+ import {
6
+ describe,
7
+ expect,
8
+ it,
9
+ } from "vitest";
10
+ import {
11
+ shallowMount,
12
+ } from "@vue/test-utils";
13
+
14
+ import AMenu from "../AMenu";
15
+
16
+ describe("AMenu inline placement", () => {
17
+ it("opts into inline placement without changing the default", async() => {
18
+ const wrapper = shallowMount(AMenu, {
19
+ props: {
20
+ canChangeBodyClass: false,
21
+ useBackdropMobile: false,
22
+ },
23
+ global: {
24
+ stubs: {
25
+ AButton: {
26
+ inheritAttrs: false,
27
+ template: "<button />",
28
+ },
29
+ },
30
+ config: {
31
+ globalProperties: {
32
+ $router: {
33
+ currentRoute: ref({ fullPath: "/" }),
34
+ },
35
+ },
36
+ },
37
+ },
38
+ });
39
+ await nextTick();
40
+
41
+ expect(wrapper.vm.searchInDom).toBe(false);
42
+ expect(wrapper.element.tagName).toBe("NAV");
43
+ expect(wrapper.classes()).not.toContain("a_menu_inline");
44
+
45
+ await wrapper.setProps({ isInline: true });
46
+ expect(wrapper.classes()).toContain("a_menu_inline");
47
+ expect(wrapper.classes()).toContain("a_menu_open");
48
+ expect(document.body.classList.contains("a_menu__wrapper_open")).toBe(false);
49
+
50
+ await wrapper.setProps({ isInline: false });
51
+ expect(wrapper.classes()).not.toContain("a_menu_inline");
52
+ wrapper.unmount();
53
+ });
54
+ });
@@ -5,17 +5,28 @@ import {
5
5
  } from "vue";
6
6
 
7
7
  import {
8
+ getElementId,
9
+ } from "../utils/utils";
10
+
11
+ import AKeyBreadcrumbs from "../const/AKeyBreadcrumbs";
12
+ import {
13
+ AKeyId,
8
14
  AKeyLabel,
9
15
  } from "../../const/AKeys";
16
+ import {
17
+ isBrowser,
18
+ } from "../../utils/isBrowser";
10
19
  import {
11
20
  escapeRegExp,
12
21
  forEach,
22
+ map,
13
23
  } from "lodash-es";
14
24
 
15
25
  export default function SearchAPI(props, {
16
26
  dataKeyById = computed(() => ({})),
17
27
  }) {
18
28
  const menuId = toRef(props, "menuId");
29
+ const searchInDom = toRef(props, "searchInDom");
19
30
 
20
31
  const modelSearch = ref("");
21
32
 
@@ -47,15 +58,47 @@ export default function SearchAPI(props, {
47
58
  return escapeRegExp(modelSearch.value);
48
59
  });
49
60
 
61
+ const getDomLabel = id => {
62
+ if (!isBrowser()) {
63
+ return undefined;
64
+ }
65
+ const LINK = document.getElementById(getElementId({
66
+ menuId: menuId.value,
67
+ id,
68
+ suffix: "link",
69
+ }));
70
+ // Read source panels only; search results contain copies of the same links.
71
+ if (!LINK?.closest(".a_menu__panels__main:not(.a_menu__panel_search)")) {
72
+ return undefined;
73
+ }
74
+ return (LINK.querySelector(".a_menu__link__text") || LINK).textContent;
75
+ };
76
+
77
+ // shortcut: DOM translations refresh on query changes; observe mutations if live updates are needed.
50
78
  const itemsWithSearch = computed(() => {
51
79
  const ITEMS = [];
52
80
  if (isSearchActive.value) {
53
81
  const RE = new RegExp(modelSearchEscapeRegExp.value, "gi");
54
82
  forEach(dataKeyById.value, item => {
55
- const ITEM_LABEL = item[AKeyLabel];
56
- if (`${ ITEM_LABEL }`.search(RE) !== -1) {
83
+ const ITEM_LABEL = searchInDom.value ? getDomLabel(item[AKeyId]) : item[AKeyLabel];
84
+ if (searchInDom.value && ITEM_LABEL === undefined) {
85
+ return;
86
+ }
87
+ if (`${ ITEM_LABEL }`.search(RE) === -1) {
88
+ return;
89
+ }
90
+ if (!searchInDom.value) {
57
91
  ITEMS.push(item);
92
+ return;
58
93
  }
94
+ ITEMS.push({
95
+ ...item,
96
+ [AKeyLabel]: ITEM_LABEL,
97
+ [AKeyBreadcrumbs]: map(item[AKeyBreadcrumbs], breadcrumb => ({
98
+ ...breadcrumb,
99
+ label: getDomLabel(breadcrumb.parentId) ?? breadcrumb.label,
100
+ })),
101
+ });
59
102
  });
60
103
  }
61
104
  return ITEMS;
@@ -0,0 +1,108 @@
1
+ import {
2
+ computed,
3
+ reactive,
4
+ } from "vue";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "vitest";
12
+
13
+ import LabelAPI from "../../AMenuPanelLink/compositionAPI/LabelAPI";
14
+ import SearchAPI from "../SearchAPI";
15
+
16
+ import AKeyBreadcrumbs from "../../const/AKeyBreadcrumbs";
17
+ import {
18
+ AKeyId,
19
+ AKeyLabel,
20
+ } from "../../../const/AKeys";
21
+
22
+ describe("AMenu search", () => {
23
+ const ITEMS = {
24
+ forms: { [AKeyId]: "forms", [AKeyLabel]: "Forms" },
25
+ input: {
26
+ [AKeyId]: "input",
27
+ [AKeyLabel]: "Input",
28
+ [AKeyBreadcrumbs]: [{ parentId: "forms", label: "Forms" }],
29
+ to: { name: "input" },
30
+ disabled: true,
31
+ },
32
+ missing: { [AKeyId]: "missing", [AKeyLabel]: "Eingabe missing" },
33
+ };
34
+
35
+ beforeEach(() => {
36
+ document.body.innerHTML = `
37
+ <nav id="test_menu">
38
+ <div class="a_menu__panels__main">
39
+ <a id="test_menu_forms_link"><span class="a_menu__link__text">Formulare</span></a>
40
+ <div style="display: none">
41
+ <a id="test_menu_input_link">
42
+ <span class="a_menu__link__text"><span>Eingabe</span> [DE]</span>
43
+ <span class="a_menu__link__counter">99</span>
44
+ </a>
45
+ </div>
46
+ </div>
47
+ <div class="a_menu__panels__main a_menu__panel_search">
48
+ <a id="test_menu_input_link"><span class="a_menu__link__text">Stale result</span></a>
49
+ <a id="test_menu_missing_link"><span class="a_menu__link__text">Eingabe missing</span></a>
50
+ </div>
51
+ </nav>`;
52
+ });
53
+
54
+ afterEach(() => {
55
+ document.body.innerHTML = "";
56
+ });
57
+
58
+ it("searches data by default even when DOM text differs", () => {
59
+ const search = SearchAPI(reactive({ menuId: "test_menu" }), {
60
+ dataKeyById: computed(() => ITEMS),
61
+ });
62
+ search.updateModelSearch("INPUT");
63
+ expect(search.itemsWithSearch.value).toEqual([ITEMS.input]);
64
+ expect(search.itemsWithSearch.value[0]).toBe(ITEMS.input);
65
+ search.updateModelSearch("Formulare");
66
+ expect(search.itemsWithSearch.value).toEqual([]);
67
+ });
68
+
69
+ it("searches translated text in source panels and preserves link behavior and breadcrumbs", () => {
70
+ const search = SearchAPI(reactive({ menuId: "test_menu", searchInDom: true }), {
71
+ dataKeyById: computed(() => ITEMS),
72
+ });
73
+ search.updateModelSearch("eINGABE");
74
+ expect(search.itemsWithSearch.value).toEqual([{
75
+ ...ITEMS.input,
76
+ [AKeyLabel]: "Eingabe [DE]",
77
+ [AKeyBreadcrumbs]: [{ parentId: "forms", label: "Formulare" }],
78
+ }]);
79
+ const {
80
+ label,
81
+ } = LabelAPI(reactive({
82
+ item: search.itemsWithSearch.value[0],
83
+ isLinkInSearchPanel: true,
84
+ modelSearch: "eINGABE",
85
+ }));
86
+ expect(label.value).toBe("<mark class=\"a_search_highlight\">Eingabe</mark> [DE]");
87
+ expect(ITEMS.input[AKeyLabel]).toBe("Input");
88
+ expect(ITEMS.input[AKeyBreadcrumbs][0].label).toBe("Forms");
89
+ search.updateModelSearch("Input");
90
+ expect(search.itemsWithSearch.value).toEqual([]);
91
+ search.updateModelSearch("99");
92
+ expect(search.itemsWithSearch.value).toEqual([]);
93
+ });
94
+
95
+ it("treats regular expression characters literally and rereads DOM for the next query", () => {
96
+ const search = SearchAPI(reactive({ menuId: "test_menu", searchInDom: true }), {
97
+ dataKeyById: computed(() => ITEMS),
98
+ });
99
+ search.updateModelSearch("[DE]");
100
+ expect(search.itemsWithSearch.value).toHaveLength(1);
101
+ document.querySelector("#test_menu_input_link .a_menu__link__text").textContent = "Saisie (FR)";
102
+ search.updateModelSearch("(FR)");
103
+ expect(search.itemsWithSearch.value[0][AKeyLabel]).toBe("Saisie (FR)");
104
+ search.resetSearch();
105
+ expect(search.isSearchActive.value).toBe(false);
106
+ expect(search.itemsWithSearch.value).toEqual([]);
107
+ });
108
+ });
@@ -118,4 +118,27 @@ describe("AValidatedJson valueKey rendering", () => {
118
118
  list.vm.closeModalCreateListMode({ model: { value: "Vue" } });
119
119
  expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([{ tags: ["Aloha", "Vue"] }]);
120
120
  });
121
+
122
+ it("saves a parent item containing a required primitive list", async() => {
123
+ const wrapper = mountList({
124
+ valueKey: undefined,
125
+ modelValue: [{ tags: ["Aloha"] }],
126
+ children: [{
127
+ id: "tags",
128
+ type: "validatedJson",
129
+ mode: "list",
130
+ valueKey: "value",
131
+ required: true,
132
+ children: [{ id: "value", type: "text", required: true }],
133
+ }],
134
+ });
135
+ wrapper.findComponent(AValidatedJsonItem).vm.openModalUpdate();
136
+ await nextTick();
137
+ const modal = wrapper.findComponent(AValidatedJsonModalCreateOrUpdate);
138
+ expect(modal.vm.isAnyRequiredElementEmpty).toBe(false);
139
+ modal.vm.changeDataForm({ fullModel: { tags: ["Aloha", "Vue"] } });
140
+ await nextTick();
141
+ modal.vm.save();
142
+ expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([[{ tags: ["Aloha", "Vue"] }]]);
143
+ });
121
144
  });
@@ -0,0 +1,53 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from "vitest";
6
+
7
+ import {
8
+ filterVisibleRequiredUiElements,
9
+ isAnyRequiredUiElementEmpty,
10
+ } from "../utils";
11
+
12
+ function requiredList(children = [{ id: "value", type: "text", required: true }]) {
13
+ return { id: "tags", type: "validatedJson", mode: "list", valueKey: "value", required: true, children };
14
+ }
15
+
16
+ describe("required validatedJson list fields", () => {
17
+ it.each([
18
+ { values: ["Aloha"] },
19
+ { values: [0] },
20
+ { values: [false] },
21
+ { values: [null] },
22
+ { values: [{ text: "Aloha" }] },
23
+ ])("accepts a populated list $values without treating it as one child form", ({ values }) => {
24
+ const fields = filterVisibleRequiredUiElements({ elements: [requiredList()] });
25
+ expect(isAnyRequiredUiElementEmpty({ elements: fields, model: { tags: values } })).toBe(false);
26
+ });
27
+
28
+ it.each([{ values: [] }, { values: undefined }, { values: null }])("rejects an empty or absent required list $values", ({ values }) => {
29
+ expect(isAnyRequiredUiElementEmpty({ elements: [requiredList()], model: { tags: values } })).toBe(true);
30
+ });
31
+
32
+ it("retains a required list with optional item fields", () => {
33
+ const field = requiredList([{ id: "value", type: "text" }]);
34
+ const fields = filterVisibleRequiredUiElements({ elements: [field] });
35
+ expect(fields).toHaveLength(1);
36
+ expect(fields[0].children).toEqual(field.children);
37
+ expect(isAnyRequiredUiElementEmpty({ elements: fields, model: { tags: [] } })).toBe(true);
38
+ });
39
+
40
+ it("ignores optional and hidden lists", () => {
41
+ const fields = filterVisibleRequiredUiElements({ elements: [
42
+ { ...requiredList(), required: false },
43
+ { ...requiredList(), isHide: true },
44
+ ] });
45
+ expect(fields).toEqual([]);
46
+ });
47
+
48
+ it("continues checking required children in single mode", () => {
49
+ const fields = filterVisibleRequiredUiElements({ elements: [{ ...requiredList(), mode: "single" }] });
50
+ expect(isAnyRequiredUiElementEmpty({ elements: fields, model: { tags: { value: "" } } })).toBe(true);
51
+ expect(isAnyRequiredUiElementEmpty({ elements: fields, model: { tags: { value: "Aloha" } } })).toBe(false);
52
+ });
53
+ });
@@ -12,6 +12,8 @@ import {
12
12
  isNil,
13
13
  } from "lodash-es";
14
14
 
15
+ const isValidatedJsonList = element => element.type === "validatedJson" && element.mode === "list";
16
+
15
17
  export function getLabelClickEventName(id) {
16
18
  return `clickALabel_${ id }`;
17
19
  }
@@ -33,7 +35,7 @@ export function filterVisibleRequiredUiElements({ elements }) {
33
35
  return false;
34
36
  }
35
37
 
36
- if (element?.children?.length) {
38
+ if (element?.children?.length && !isValidatedJsonList(element)) {
37
39
  element.children = filterVisibleRequiredUiElements({ elements: element.children });
38
40
 
39
41
  if (!element.children.length) {
@@ -50,7 +52,7 @@ export function isAnyRequiredUiElementEmpty({ elements, model }) {
50
52
 
51
53
  forEach(elements, element => {
52
54
  const CURREN_MODEL = get(model, element.id);
53
- if (typesModelArray.value[element.type]) {
55
+ if (typesModelArray.value[element.type] || isValidatedJsonList(element)) {
54
56
  if (!isArray(CURREN_MODEL) || !CURREN_MODEL.length) {
55
57
  isAnyEmpty = true;
56
58
  return false;