@jskit-ai/users-web 0.1.33 → 0.1.34
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/package.descriptor.mjs +10 -6
- package/package.json +7 -6
- package/src/client/components/WorkspaceMembersClientElement.vue +16 -16
- package/src/client/components/WorkspacesClientElement.vue +2 -2
- package/src/client/composables/crudLookupFieldLabelSupport.js +107 -0
- package/src/client/composables/crudLookupFieldRuntime.js +238 -0
- package/src/client/composables/crudSchemaFormHelpers.js +35 -0
- package/src/client/composables/listSearchSupport.js +70 -0
- package/src/client/composables/resourceLoadStateHelpers.js +10 -0
- package/src/client/composables/routeTemplateHelpers.js +54 -1
- package/src/client/composables/useAccountSettingsRuntime.js +14 -14
- package/src/client/composables/useAddEdit.js +2 -1
- package/src/client/composables/useCrudSchemaForm.js +37 -11
- package/src/client/composables/useEndpointResource.js +6 -1
- package/src/client/composables/useList.js +164 -8
- package/src/client/composables/useRealtimeQueryInvalidation.js +33 -8
- package/src/client/composables/useView.js +4 -2
- package/src/client/composables/useViewCore.js +12 -2
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +3 -6
- package/templates/src/composables/useWorkspaceNotFoundState.js +8 -15
- package/test/crudLookupFieldRuntime.test.js +189 -0
- package/test/resourceLoadStateHelpers.test.js +39 -0
- package/test/routeTemplateHelpers.test.js +29 -0
- package/test/useCrudSchemaForm.test.js +39 -0
- package/test/useListSearchSupport.test.js +61 -0
- package/test/viewCoreLoading.test.js +44 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
resolveLookupItemLabel,
|
|
5
|
+
resolveLookupFieldDisplayValue
|
|
6
|
+
} from "../src/client/composables/crudLookupFieldLabelSupport.js";
|
|
7
|
+
|
|
8
|
+
test("resolveLookupItemLabel composes name + surname", () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
resolveLookupItemLabel(
|
|
11
|
+
{
|
|
12
|
+
name: "South",
|
|
13
|
+
surname: "Clinic"
|
|
14
|
+
},
|
|
15
|
+
"name"
|
|
16
|
+
),
|
|
17
|
+
"South Clinic"
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("resolveLookupItemLabel composes firstName + surname", () => {
|
|
22
|
+
assert.equal(
|
|
23
|
+
resolveLookupItemLabel(
|
|
24
|
+
{
|
|
25
|
+
firstName: "Ana",
|
|
26
|
+
surname: "Marin"
|
|
27
|
+
},
|
|
28
|
+
"name"
|
|
29
|
+
),
|
|
30
|
+
"Ana Marin"
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("resolveLookupItemLabel falls back to explicit labelKey", () => {
|
|
35
|
+
assert.equal(
|
|
36
|
+
resolveLookupItemLabel(
|
|
37
|
+
{
|
|
38
|
+
clinicName: "Harbor Vet"
|
|
39
|
+
},
|
|
40
|
+
"clinicName"
|
|
41
|
+
),
|
|
42
|
+
"Harbor Vet"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("resolveLookupItemLabel resolves name when surname is missing", () => {
|
|
47
|
+
assert.equal(
|
|
48
|
+
resolveLookupItemLabel(
|
|
49
|
+
{
|
|
50
|
+
name: "Harbor Vet"
|
|
51
|
+
},
|
|
52
|
+
""
|
|
53
|
+
),
|
|
54
|
+
"Harbor Vet"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("resolveLookupItemLabel returns empty when no label fields match", () => {
|
|
59
|
+
assert.equal(resolveLookupItemLabel({ id: 42 }, "name"), "");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("resolveLookupFieldDisplayValue returns hydrated label for lookup fields", () => {
|
|
63
|
+
assert.equal(
|
|
64
|
+
resolveLookupFieldDisplayValue(
|
|
65
|
+
{
|
|
66
|
+
vetId: 17,
|
|
67
|
+
lookups: {
|
|
68
|
+
vetId: {
|
|
69
|
+
id: 17,
|
|
70
|
+
name: "Harbor Vet"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "vetId",
|
|
76
|
+
relation: {
|
|
77
|
+
kind: "lookup",
|
|
78
|
+
valueKey: "id",
|
|
79
|
+
labelKey: "name"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
"Harbor Vet"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("resolveLookupFieldDisplayValue falls back to hydrated valueKey", () => {
|
|
88
|
+
assert.equal(
|
|
89
|
+
resolveLookupFieldDisplayValue(
|
|
90
|
+
{
|
|
91
|
+
vetId: 17,
|
|
92
|
+
lookups: {
|
|
93
|
+
vetId: {
|
|
94
|
+
id: 99
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "vetId",
|
|
100
|
+
relation: {
|
|
101
|
+
kind: "lookup",
|
|
102
|
+
valueKey: "id",
|
|
103
|
+
labelKey: "name"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
99
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("resolveLookupFieldDisplayValue falls back to raw id when lookup is not hydrated", () => {
|
|
112
|
+
assert.equal(
|
|
113
|
+
resolveLookupFieldDisplayValue(
|
|
114
|
+
{
|
|
115
|
+
vetId: 17
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "vetId",
|
|
119
|
+
relation: {
|
|
120
|
+
kind: "lookup",
|
|
121
|
+
valueKey: "id",
|
|
122
|
+
labelKey: "name"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
17
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("resolveLookupFieldDisplayValue supports custom lookup container key", () => {
|
|
131
|
+
assert.equal(
|
|
132
|
+
resolveLookupFieldDisplayValue(
|
|
133
|
+
{
|
|
134
|
+
vetId: 17,
|
|
135
|
+
lookupData: {
|
|
136
|
+
vetId: {
|
|
137
|
+
id: 17,
|
|
138
|
+
name: "Harbor Vet"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: "vetId",
|
|
144
|
+
relation: {
|
|
145
|
+
kind: "lookup",
|
|
146
|
+
containerKey: "lookupData",
|
|
147
|
+
valueKey: "id",
|
|
148
|
+
labelKey: "name"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
),
|
|
152
|
+
"Harbor Vet"
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("resolveLookupFieldDisplayValue returns raw value for non-lookup fields", () => {
|
|
157
|
+
assert.equal(
|
|
158
|
+
resolveLookupFieldDisplayValue(
|
|
159
|
+
{
|
|
160
|
+
firstName: "Ana"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "firstName"
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
"Ana"
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("resolveLookupFieldDisplayValue supports scalar lookup descriptor arguments", () => {
|
|
171
|
+
assert.equal(
|
|
172
|
+
resolveLookupFieldDisplayValue(
|
|
173
|
+
{
|
|
174
|
+
vetId: 17,
|
|
175
|
+
lookups: {
|
|
176
|
+
vetId: {
|
|
177
|
+
id: 17,
|
|
178
|
+
name: "Harbor Vet"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"vetId",
|
|
183
|
+
"lookup",
|
|
184
|
+
"id",
|
|
185
|
+
"name"
|
|
186
|
+
),
|
|
187
|
+
"Harbor Vet"
|
|
188
|
+
);
|
|
189
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { hasResolvedQueryData } from "../src/client/composables/resourceLoadStateHelpers.js";
|
|
5
|
+
|
|
6
|
+
test("hasResolvedQueryData returns true when the query succeeded", () => {
|
|
7
|
+
const query = {
|
|
8
|
+
isSuccess: ref(true)
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
assert.equal(hasResolvedQueryData({
|
|
12
|
+
query,
|
|
13
|
+
data: ref(null)
|
|
14
|
+
}), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("hasResolvedQueryData returns true when data payload is available", () => {
|
|
18
|
+
const query = {
|
|
19
|
+
isSuccess: ref(false)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
assert.equal(hasResolvedQueryData({
|
|
23
|
+
query,
|
|
24
|
+
data: ref({
|
|
25
|
+
id: 2971
|
|
26
|
+
})
|
|
27
|
+
}), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("hasResolvedQueryData returns false when query and payload are unresolved", () => {
|
|
31
|
+
const query = {
|
|
32
|
+
isSuccess: ref(false)
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
assert.equal(hasResolvedQueryData({
|
|
36
|
+
query,
|
|
37
|
+
data: ref(null)
|
|
38
|
+
}), false);
|
|
39
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
extractRouteParamNames,
|
|
5
|
+
resolveRouteParamNamesInOrder
|
|
6
|
+
} from "../src/client/composables/routeTemplateHelpers.js";
|
|
7
|
+
|
|
8
|
+
test("extractRouteParamNames reads dynamic params from route templates", () => {
|
|
9
|
+
assert.deepEqual(
|
|
10
|
+
extractRouteParamNames("/w/:workspaceSlug/admin/contacts/:contactId/addresses/:addressId"),
|
|
11
|
+
["workspaceSlug", "contactId", "addressId"]
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("resolveRouteParamNamesInOrder prefers matched route templates", () => {
|
|
16
|
+
const route = {
|
|
17
|
+
matched: [
|
|
18
|
+
{ path: "/w/:workspaceSlug/admin" },
|
|
19
|
+
{ path: "/contacts/:contactId/addresses/:addressId" }
|
|
20
|
+
],
|
|
21
|
+
params: {
|
|
22
|
+
workspaceSlug: "acme",
|
|
23
|
+
contactId: "7",
|
|
24
|
+
addressId: "42"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(resolveRouteParamNamesInOrder(route), ["workspaceSlug", "contactId", "addressId"]);
|
|
29
|
+
});
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
createCrudFormModel,
|
|
8
8
|
buildCrudFormPayload,
|
|
9
9
|
applyCrudPayloadToForm,
|
|
10
|
+
resolveCrudRouteBoundFieldValues,
|
|
11
|
+
applyCrudRouteBoundFieldValues,
|
|
10
12
|
resolveCrudFieldErrors,
|
|
11
13
|
parseCrudResourceOperationInput
|
|
12
14
|
} from "../src/client/composables/crudSchemaFormHelpers.js";
|
|
@@ -87,6 +89,43 @@ test("applyCrudPayloadToForm maps payload values into reactive form model", () =
|
|
|
87
89
|
});
|
|
88
90
|
});
|
|
89
91
|
|
|
92
|
+
test("resolveCrudRouteBoundFieldValues maps route params for route-bound form fields", () => {
|
|
93
|
+
const values = resolveCrudRouteBoundFieldValues(
|
|
94
|
+
[
|
|
95
|
+
{ key: "contactId", routeParamKey: "contactId" },
|
|
96
|
+
{ key: "name" }
|
|
97
|
+
],
|
|
98
|
+
{
|
|
99
|
+
contactId: " 2971 "
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.deepEqual(values, {
|
|
104
|
+
contactId: "2971"
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("applyCrudRouteBoundFieldValues enforces route-bound field values onto target payload", () => {
|
|
109
|
+
const payload = {
|
|
110
|
+
name: "Address one",
|
|
111
|
+
contactId: "123"
|
|
112
|
+
};
|
|
113
|
+
applyCrudRouteBoundFieldValues(
|
|
114
|
+
[
|
|
115
|
+
{ key: "contactId", routeParamKey: "contactId" }
|
|
116
|
+
],
|
|
117
|
+
payload,
|
|
118
|
+
{
|
|
119
|
+
contactId: "2971"
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(payload, {
|
|
124
|
+
name: "Address one",
|
|
125
|
+
contactId: "2971"
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
90
129
|
test("resolveCrudFieldErrors returns Vuetify-compatible error arrays", () => {
|
|
91
130
|
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "name"), ["Name is required."]);
|
|
92
131
|
assert.deepEqual(resolveCrudFieldErrors({ name: "Name is required." }, "email"), []);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
normalizeListSearchConfig,
|
|
5
|
+
matchesLocalSearch
|
|
6
|
+
} from "../src/client/composables/listSearchSupport.js";
|
|
7
|
+
|
|
8
|
+
test("normalizeListSearchConfig defaults to disabled query search", () => {
|
|
9
|
+
const config = normalizeListSearchConfig();
|
|
10
|
+
assert.equal(config.enabled, false);
|
|
11
|
+
assert.equal(config.mode, "query");
|
|
12
|
+
assert.equal(config.queryParam, "q");
|
|
13
|
+
assert.equal(config.label, "Search");
|
|
14
|
+
assert.equal(config.minLength, 1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("normalizeListSearchConfig accepts query mode and fields", () => {
|
|
18
|
+
const config = normalizeListSearchConfig({
|
|
19
|
+
enabled: true,
|
|
20
|
+
mode: "query",
|
|
21
|
+
queryParam: "search",
|
|
22
|
+
fields: ["name", "name", "email"]
|
|
23
|
+
});
|
|
24
|
+
assert.equal(config.enabled, true);
|
|
25
|
+
assert.equal(config.mode, "query");
|
|
26
|
+
assert.equal(config.queryParam, "search");
|
|
27
|
+
assert.deepEqual(config.fields, ["name", "email"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("normalizeListSearchConfig accepts explicit local mode", () => {
|
|
31
|
+
const config = normalizeListSearchConfig({
|
|
32
|
+
enabled: true,
|
|
33
|
+
mode: "local"
|
|
34
|
+
});
|
|
35
|
+
assert.equal(config.mode, "local");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("matchesLocalSearch checks configured fields", () => {
|
|
39
|
+
const result = matchesLocalSearch(
|
|
40
|
+
{
|
|
41
|
+
firstName: "Ana",
|
|
42
|
+
surname: "Marin",
|
|
43
|
+
phone: "0400"
|
|
44
|
+
},
|
|
45
|
+
"mar",
|
|
46
|
+
["surname"]
|
|
47
|
+
);
|
|
48
|
+
assert.equal(result, true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("matchesLocalSearch scans scalar values when fields are not configured", () => {
|
|
52
|
+
const result = matchesLocalSearch(
|
|
53
|
+
{
|
|
54
|
+
firstName: "Ana",
|
|
55
|
+
surname: "Marin",
|
|
56
|
+
count: 42
|
|
57
|
+
},
|
|
58
|
+
"42"
|
|
59
|
+
);
|
|
60
|
+
assert.equal(result, true);
|
|
61
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ref } from "vue";
|
|
4
|
+
import { useViewCore } from "../src/client/composables/useViewCore.js";
|
|
5
|
+
|
|
6
|
+
test("useViewCore prefers resource isInitialLoading/isFetching signals when provided", () => {
|
|
7
|
+
const resource = {
|
|
8
|
+
data: ref({ id: 42 }),
|
|
9
|
+
isInitialLoading: ref(false),
|
|
10
|
+
isFetching: ref(true),
|
|
11
|
+
query: {
|
|
12
|
+
isPending: ref(true),
|
|
13
|
+
isFetching: ref(true),
|
|
14
|
+
error: ref(null)
|
|
15
|
+
},
|
|
16
|
+
loadError: ref("")
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const view = useViewCore({
|
|
20
|
+
resource
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.equal(view.isLoading.value, false);
|
|
24
|
+
assert.equal(view.isFetching.value, true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("useViewCore falls back to query pending/fetching when resource-level loading refs are absent", () => {
|
|
28
|
+
const resource = {
|
|
29
|
+
data: ref(null),
|
|
30
|
+
query: {
|
|
31
|
+
isPending: ref(true),
|
|
32
|
+
isFetching: ref(false),
|
|
33
|
+
error: ref(null)
|
|
34
|
+
},
|
|
35
|
+
loadError: ref("")
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const view = useViewCore({
|
|
39
|
+
resource
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.equal(view.isLoading.value, true);
|
|
43
|
+
assert.equal(view.isFetching.value, false);
|
|
44
|
+
});
|