@jskit-ai/users-web 0.1.33 → 0.1.35
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
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.35",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Users web module: workspace selector shell element plus workspace/profile/members UI elements.",
|
|
7
7
|
dependsOn: [
|
|
@@ -73,6 +73,10 @@ export default Object.freeze({
|
|
|
73
73
|
subpath: "./client/composables/useList",
|
|
74
74
|
summary: "Exports list operation composable."
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
subpath: "./client/composables/crudLookupFieldRuntime",
|
|
78
|
+
summary: "Exports CRUD lookup field runtime helpers for generated add/edit forms."
|
|
79
|
+
},
|
|
76
80
|
{
|
|
77
81
|
subpath: "./client/composables/useCommand",
|
|
78
82
|
summary: "Exports command operation composable."
|
|
@@ -242,11 +246,11 @@ export default Object.freeze({
|
|
|
242
246
|
"@uppy/dashboard": "^5.1.1",
|
|
243
247
|
"@uppy/image-editor": "^4.2.0",
|
|
244
248
|
"@uppy/xhr-upload": "^5.1.1",
|
|
245
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
246
|
-
"@jskit-ai/realtime": "0.1.
|
|
247
|
-
"@jskit-ai/kernel": "0.1.
|
|
248
|
-
"@jskit-ai/shell-web": "0.1.
|
|
249
|
-
"@jskit-ai/users-core": "0.1.
|
|
249
|
+
"@jskit-ai/http-runtime": "0.1.20",
|
|
250
|
+
"@jskit-ai/realtime": "0.1.20",
|
|
251
|
+
"@jskit-ai/kernel": "0.1.21",
|
|
252
|
+
"@jskit-ai/shell-web": "0.1.20",
|
|
253
|
+
"@jskit-ai/users-core": "0.1.30",
|
|
250
254
|
"vuetify": "^4.0.0"
|
|
251
255
|
},
|
|
252
256
|
dev: {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
|
|
11
11
|
"./client/composables/useAddEdit": "./src/client/composables/useAddEdit.js",
|
|
12
12
|
"./client/composables/useCrudSchemaForm": "./src/client/composables/useCrudSchemaForm.js",
|
|
13
|
+
"./client/composables/crudLookupFieldRuntime": "./src/client/composables/crudLookupFieldRuntime.js",
|
|
13
14
|
"./client/composables/useList": "./src/client/composables/useList.js",
|
|
14
15
|
"./client/composables/useView": "./src/client/composables/useView.js",
|
|
15
16
|
"./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
|
|
@@ -21,11 +22,11 @@
|
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@tanstack/vue-query": "5.92.12",
|
|
23
24
|
"@mdi/js": "^7.4.47",
|
|
24
|
-
"@jskit-ai/users-core": "0.1.
|
|
25
|
-
"@jskit-ai/realtime": "0.1.
|
|
26
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
27
|
-
"@jskit-ai/kernel": "0.1.
|
|
28
|
-
"@jskit-ai/shell-web": "0.1.
|
|
25
|
+
"@jskit-ai/users-core": "0.1.30",
|
|
26
|
+
"@jskit-ai/realtime": "0.1.20",
|
|
27
|
+
"@jskit-ai/http-runtime": "0.1.20",
|
|
28
|
+
"@jskit-ai/kernel": "0.1.21",
|
|
29
|
+
"@jskit-ai/shell-web": "0.1.20",
|
|
29
30
|
"vuetify": "^4.0.0"
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -389,12 +389,12 @@ const status = computed(() => {
|
|
|
389
389
|
isCreatingInvite: Boolean(inviteCreateCommand.isRunning.value),
|
|
390
390
|
isRevokingInvite: Boolean(revokeInviteCommand.isRunning.value),
|
|
391
391
|
isRemovingMember: Boolean(memberRemoveCommand.isRunning.value),
|
|
392
|
-
hasLoadedWorkspaceSettings: !canInviteMembers.value || !workspaceSettingsView.isLoading
|
|
393
|
-
hasLoadedMembersList: !canViewMembers.value || !workspaceMembersList.isInitialLoading
|
|
394
|
-
hasLoadedInviteList: !canViewMembers.value || !workspaceInvitesList.isInitialLoading
|
|
395
|
-
isRefreshingWorkspaceSettings: canInviteMembers.value && Boolean(workspaceSettingsView.isRefetching
|
|
396
|
-
isRefreshingMembersList: canViewMembers.value && Boolean(workspaceMembersList.isRefetching
|
|
397
|
-
isRefreshingInviteList: canViewMembers.value && Boolean(workspaceInvitesList.isRefetching
|
|
392
|
+
hasLoadedWorkspaceSettings: !canInviteMembers.value || !workspaceSettingsView.isLoading,
|
|
393
|
+
hasLoadedMembersList: !canViewMembers.value || !workspaceMembersList.isInitialLoading,
|
|
394
|
+
hasLoadedInviteList: !canViewMembers.value || !workspaceInvitesList.isInitialLoading,
|
|
395
|
+
isRefreshingWorkspaceSettings: canInviteMembers.value && Boolean(workspaceSettingsView.isRefetching),
|
|
396
|
+
isRefreshingMembersList: canViewMembers.value && Boolean(workspaceMembersList.isRefetching),
|
|
397
|
+
isRefreshingInviteList: canViewMembers.value && Boolean(workspaceInvitesList.isRefetching)
|
|
398
398
|
};
|
|
399
399
|
});
|
|
400
400
|
|
|
@@ -440,7 +440,7 @@ watch(
|
|
|
440
440
|
);
|
|
441
441
|
|
|
442
442
|
watch(
|
|
443
|
-
() => workspaceSettingsView.record
|
|
443
|
+
() => workspaceSettingsView.record,
|
|
444
444
|
(payload) => {
|
|
445
445
|
if (!payload) {
|
|
446
446
|
return;
|
|
@@ -451,7 +451,7 @@ watch(
|
|
|
451
451
|
);
|
|
452
452
|
|
|
453
453
|
watch(
|
|
454
|
-
() => workspaceSettingsView.loadError
|
|
454
|
+
() => workspaceSettingsView.loadError,
|
|
455
455
|
(nextLoadError) => {
|
|
456
456
|
if (!nextLoadError) {
|
|
457
457
|
return;
|
|
@@ -462,7 +462,7 @@ watch(
|
|
|
462
462
|
);
|
|
463
463
|
|
|
464
464
|
watch(
|
|
465
|
-
() => workspaceRolesView.record
|
|
465
|
+
() => workspaceRolesView.record,
|
|
466
466
|
(payload) => {
|
|
467
467
|
if (!payload) {
|
|
468
468
|
return;
|
|
@@ -473,7 +473,7 @@ watch(
|
|
|
473
473
|
);
|
|
474
474
|
|
|
475
475
|
watch(
|
|
476
|
-
() => workspaceRolesView.loadError
|
|
476
|
+
() => workspaceRolesView.loadError,
|
|
477
477
|
(nextLoadError) => {
|
|
478
478
|
if (!nextLoadError) {
|
|
479
479
|
return;
|
|
@@ -483,7 +483,7 @@ watch(
|
|
|
483
483
|
);
|
|
484
484
|
|
|
485
485
|
watch(
|
|
486
|
-
() => workspaceMembersList.items
|
|
486
|
+
() => workspaceMembersList.items,
|
|
487
487
|
(nextMembers) => {
|
|
488
488
|
collections.members = Array.isArray(nextMembers) ? [...nextMembers] : [];
|
|
489
489
|
},
|
|
@@ -491,7 +491,7 @@ watch(
|
|
|
491
491
|
);
|
|
492
492
|
|
|
493
493
|
watch(
|
|
494
|
-
() => workspaceMembersList.pages
|
|
494
|
+
() => workspaceMembersList.pages,
|
|
495
495
|
(pages) => {
|
|
496
496
|
const payload = latestPage(pages);
|
|
497
497
|
if (!payload) {
|
|
@@ -503,7 +503,7 @@ watch(
|
|
|
503
503
|
);
|
|
504
504
|
|
|
505
505
|
watch(
|
|
506
|
-
() => workspaceMembersList.loadError
|
|
506
|
+
() => workspaceMembersList.loadError,
|
|
507
507
|
(nextLoadError) => {
|
|
508
508
|
if (!nextLoadError) {
|
|
509
509
|
membersFeedback.clear();
|
|
@@ -514,7 +514,7 @@ watch(
|
|
|
514
514
|
);
|
|
515
515
|
|
|
516
516
|
watch(
|
|
517
|
-
() => workspaceInvitesList.items
|
|
517
|
+
() => workspaceInvitesList.items,
|
|
518
518
|
(nextInvites) => {
|
|
519
519
|
collections.invites = Array.isArray(nextInvites) ? [...nextInvites] : [];
|
|
520
520
|
},
|
|
@@ -522,7 +522,7 @@ watch(
|
|
|
522
522
|
);
|
|
523
523
|
|
|
524
524
|
watch(
|
|
525
|
-
() => workspaceInvitesList.pages
|
|
525
|
+
() => workspaceInvitesList.pages,
|
|
526
526
|
(pages) => {
|
|
527
527
|
const payload = latestPage(pages);
|
|
528
528
|
if (!payload) {
|
|
@@ -534,7 +534,7 @@ watch(
|
|
|
534
534
|
);
|
|
535
535
|
|
|
536
536
|
watch(
|
|
537
|
-
() => workspaceInvitesList.loadError
|
|
537
|
+
() => workspaceInvitesList.loadError,
|
|
538
538
|
(nextLoadError) => {
|
|
539
539
|
if (!nextLoadError) {
|
|
540
540
|
teamFeedback.clear();
|
|
@@ -125,8 +125,8 @@ const pendingInvites = computed(() => {
|
|
|
125
125
|
});
|
|
126
126
|
const workspaceInvitesEnabled = computed(() => bootstrapModel.workspaceInvitesEnabled === true);
|
|
127
127
|
|
|
128
|
-
const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading
|
|
129
|
-
const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching
|
|
128
|
+
const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading));
|
|
129
|
+
const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching));
|
|
130
130
|
const canCreateWorkspace = computed(() => bootstrapModel.workspaceAllowSelfCreate === true);
|
|
131
131
|
const isCreatingWorkspace = computed(() => Boolean(createWorkspaceCommand.isRunning.value));
|
|
132
132
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeCrudLookupContainerKey } from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
3
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
4
|
+
|
|
5
|
+
const LOOKUP_LABEL_COMPOSITION_CANDIDATES = Object.freeze([
|
|
6
|
+
Object.freeze(["name", "surname"]),
|
|
7
|
+
Object.freeze(["firstName", "surname"]),
|
|
8
|
+
Object.freeze(["name"]),
|
|
9
|
+
Object.freeze(["firstName"])
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function hasDisplayValue(value) {
|
|
13
|
+
if (value == null) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
return normalizeText(value).length > 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveLookupItemLabel(item = {}, labelKey = "") {
|
|
24
|
+
const source = asPlainObject(item);
|
|
25
|
+
for (const candidate of LOOKUP_LABEL_COMPOSITION_CANDIDATES) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
for (const key of candidate) {
|
|
28
|
+
const part = normalizeText(source[key]);
|
|
29
|
+
if (!part) {
|
|
30
|
+
parts.length = 0;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
parts.push(part);
|
|
34
|
+
}
|
|
35
|
+
if (parts.length === candidate.length) {
|
|
36
|
+
return parts.join(" ");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalizedLabelKey = normalizeText(labelKey);
|
|
41
|
+
if (!normalizedLabelKey) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalizeText(source[normalizedLabelKey]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveLookupFieldDescriptor(field = {}, relationKind = "", valueKey = "", labelKey = "") {
|
|
49
|
+
if (typeof field === "string") {
|
|
50
|
+
return {
|
|
51
|
+
key: normalizeText(field),
|
|
52
|
+
relation: {
|
|
53
|
+
kind: normalizeText(relationKind).toLowerCase(),
|
|
54
|
+
valueKey: normalizeText(valueKey) || "id",
|
|
55
|
+
labelKey: normalizeText(labelKey),
|
|
56
|
+
containerKey: ""
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sourceField = asPlainObject(field);
|
|
62
|
+
const relation = asPlainObject(sourceField.relation);
|
|
63
|
+
return {
|
|
64
|
+
key: normalizeText(sourceField.key),
|
|
65
|
+
relation: {
|
|
66
|
+
kind: normalizeText(relation.kind).toLowerCase(),
|
|
67
|
+
valueKey: normalizeText(relation.valueKey) || "id",
|
|
68
|
+
labelKey: normalizeText(relation.labelKey),
|
|
69
|
+
containerKey: normalizeText(relation.containerKey)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveLookupFieldDisplayValue(record = {}, field = {}, relationKind = "", valueKey = "", labelKey = "") {
|
|
75
|
+
const sourceRecord = asPlainObject(record);
|
|
76
|
+
const descriptor = resolveLookupFieldDescriptor(field, relationKind, valueKey, labelKey);
|
|
77
|
+
const key = descriptor.key;
|
|
78
|
+
if (!key) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (descriptor.relation.kind !== "lookup") {
|
|
83
|
+
return sourceRecord[key];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lookupContainerKey = normalizeCrudLookupContainerKey(descriptor.relation.containerKey, {
|
|
87
|
+
context: `lookup relation "${key}" containerKey`
|
|
88
|
+
});
|
|
89
|
+
const sourceLookups = asPlainObject(sourceRecord[lookupContainerKey]);
|
|
90
|
+
const lookupRecord = asPlainObject(sourceLookups[key]);
|
|
91
|
+
const lookupLabel = resolveLookupItemLabel(lookupRecord, descriptor.relation.labelKey);
|
|
92
|
+
if (lookupLabel) {
|
|
93
|
+
return lookupLabel;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lookupValue = lookupRecord[descriptor.relation.valueKey];
|
|
97
|
+
if (hasDisplayValue(lookupValue)) {
|
|
98
|
+
return lookupValue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return sourceRecord[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
resolveLookupItemLabel,
|
|
106
|
+
resolveLookupFieldDisplayValue
|
|
107
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import {
|
|
3
|
+
normalizeCrudLookupApiPath,
|
|
4
|
+
normalizeCrudLookupNamespace,
|
|
5
|
+
normalizeCrudLookupContainerKey,
|
|
6
|
+
resolveCrudLookupApiPathFromNamespace
|
|
7
|
+
} from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
8
|
+
import { useList } from "./useList.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveLookupItemLabel,
|
|
11
|
+
resolveLookupFieldDisplayValue
|
|
12
|
+
} from "./crudLookupFieldLabelSupport.js";
|
|
13
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
14
|
+
|
|
15
|
+
function normalizeQueryKeyPrefix(value) {
|
|
16
|
+
const source = Array.isArray(value) ? value : [];
|
|
17
|
+
return source
|
|
18
|
+
.map((entry) => normalizeText(entry))
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isSameLookupValue(left, right) {
|
|
23
|
+
if (left === right) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return String(left ?? "") === String(right ?? "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeLookupValue(value) {
|
|
31
|
+
if (value == null) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return String(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createSelectedLookupItem(selectedValue, selectedRecord = {}, entry = {}) {
|
|
39
|
+
if (selectedValue == null || selectedValue === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sourceRecord = asPlainObject(selectedRecord);
|
|
44
|
+
const sourceLookups = asPlainObject(sourceRecord[entry.lookupContainerKey]);
|
|
45
|
+
const hydratedLookup = asPlainObject(sourceLookups[entry.fieldKey]);
|
|
46
|
+
const hydratedValue = hydratedLookup[entry.valueKey];
|
|
47
|
+
const value = normalizeLookupValue(
|
|
48
|
+
hydratedValue == null || hydratedValue === "" ? selectedValue : hydratedValue
|
|
49
|
+
);
|
|
50
|
+
if (!value) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const displayValue = resolveLookupFieldDisplayValue(
|
|
55
|
+
{
|
|
56
|
+
[entry.fieldKey]: value,
|
|
57
|
+
[entry.lookupContainerKey]: {
|
|
58
|
+
[entry.fieldKey]: hydratedLookup
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: entry.fieldKey,
|
|
63
|
+
relation: entry.relation
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
const label = displayValue == null || displayValue === "" ? value : displayValue;
|
|
67
|
+
return {
|
|
68
|
+
value,
|
|
69
|
+
label: String(label ?? "")
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createCrudLookupFieldRuntime({
|
|
74
|
+
formFields = [],
|
|
75
|
+
adapter = null,
|
|
76
|
+
recordIdParam = "recordId",
|
|
77
|
+
queryKeyPrefix = [],
|
|
78
|
+
placementSourcePrefix = "",
|
|
79
|
+
lookupContainerKey = "lookups"
|
|
80
|
+
} = {}) {
|
|
81
|
+
const runtimes = new Map();
|
|
82
|
+
const normalizedRecordIdParam = normalizeText(recordIdParam) || "recordId";
|
|
83
|
+
const normalizedPlacementSourcePrefix = normalizeText(placementSourcePrefix);
|
|
84
|
+
const normalizedQueryKeyPrefix = normalizeQueryKeyPrefix(queryKeyPrefix);
|
|
85
|
+
const defaultLookupContainerKey = normalizeCrudLookupContainerKey(lookupContainerKey, {
|
|
86
|
+
context: "createCrudLookupFieldRuntime lookupContainerKey"
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const field of Array.isArray(formFields) ? formFields : []) {
|
|
90
|
+
const key = normalizeText(field?.key);
|
|
91
|
+
const rawRelation = field?.relation;
|
|
92
|
+
if (!key || !rawRelation || typeof rawRelation !== "object" || Array.isArray(rawRelation) || runtimes.has(key)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const relationKind = normalizeText(rawRelation.kind).toLowerCase();
|
|
97
|
+
const namespace =
|
|
98
|
+
normalizeCrudLookupNamespace(rawRelation.namespace) ||
|
|
99
|
+
normalizeCrudLookupNamespace(rawRelation.apiPath);
|
|
100
|
+
if (relationKind !== "lookup" || !namespace) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const explicitApiPath = normalizeCrudLookupApiPath(rawRelation.apiPath);
|
|
104
|
+
const apiPath = explicitApiPath || resolveCrudLookupApiPathFromNamespace(namespace);
|
|
105
|
+
const valueKey = normalizeText(rawRelation.valueKey);
|
|
106
|
+
const labelKey = normalizeText(rawRelation.labelKey);
|
|
107
|
+
const relationLookupContainerKey = normalizeCrudLookupContainerKey(rawRelation.containerKey, {
|
|
108
|
+
defaultValue: defaultLookupContainerKey,
|
|
109
|
+
context: `createCrudLookupFieldRuntime formFields["${key}"].relation.containerKey`
|
|
110
|
+
});
|
|
111
|
+
if (!valueKey) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const runtime = useList({
|
|
116
|
+
adapter: adapter || undefined,
|
|
117
|
+
apiSuffix: apiPath,
|
|
118
|
+
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
119
|
+
...normalizedQueryKeyPrefix,
|
|
120
|
+
key,
|
|
121
|
+
String(surfaceId || ""),
|
|
122
|
+
String(workspaceSlug || "")
|
|
123
|
+
],
|
|
124
|
+
search: {
|
|
125
|
+
enabled: true,
|
|
126
|
+
mode: "query",
|
|
127
|
+
queryParam: "q"
|
|
128
|
+
},
|
|
129
|
+
placementSource: normalizedPlacementSourcePrefix
|
|
130
|
+
? `${normalizedPlacementSourcePrefix}.${key}`
|
|
131
|
+
: `crud.lookup.${key}`,
|
|
132
|
+
fallbackLoadError: `Unable to load lookup options (${apiPath}).`,
|
|
133
|
+
recordIdParam: normalizedRecordIdParam,
|
|
134
|
+
recordIdSelector: (item = {}) => item[valueKey],
|
|
135
|
+
viewUrlTemplate: "",
|
|
136
|
+
editUrlTemplate: ""
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
runtimes.set(key, Object.freeze({
|
|
140
|
+
runtime,
|
|
141
|
+
fieldKey: key,
|
|
142
|
+
lookupContainerKey: relationLookupContainerKey,
|
|
143
|
+
valueKey,
|
|
144
|
+
labelKey,
|
|
145
|
+
relation: Object.freeze({
|
|
146
|
+
kind: "lookup",
|
|
147
|
+
namespace,
|
|
148
|
+
...(explicitApiPath ? { apiPath: explicitApiPath } : {}),
|
|
149
|
+
containerKey: relationLookupContainerKey,
|
|
150
|
+
valueKey,
|
|
151
|
+
...(labelKey ? { labelKey } : {})
|
|
152
|
+
})
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveLookupItems(fieldKey = "", options = {}) {
|
|
157
|
+
const key = normalizeText(fieldKey);
|
|
158
|
+
const entry = runtimes.get(key);
|
|
159
|
+
if (!entry) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const items = (Array.isArray(entry.runtime.items) ? entry.runtime.items : []).map((item = {}) => {
|
|
164
|
+
const value = normalizeLookupValue(item?.[entry.valueKey]);
|
|
165
|
+
const resolvedLabel = resolveLookupItemLabel(item, entry.labelKey);
|
|
166
|
+
const label = resolvedLabel || value;
|
|
167
|
+
return {
|
|
168
|
+
value,
|
|
169
|
+
label: String(label ?? "")
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const selectedItem = createSelectedLookupItem(
|
|
174
|
+
options?.selectedValue,
|
|
175
|
+
options?.selectedRecord,
|
|
176
|
+
entry
|
|
177
|
+
);
|
|
178
|
+
if (!selectedItem) {
|
|
179
|
+
return items;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (items.some((item) => item?.value === selectedItem.value)) {
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const matchingItem = items.find((item) => isSameLookupValue(item?.value, selectedItem.value));
|
|
187
|
+
if (matchingItem) {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
...selectedItem,
|
|
191
|
+
label: String(matchingItem.label ?? selectedItem.label ?? "")
|
|
192
|
+
},
|
|
193
|
+
...items
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [selectedItem, ...items];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveLookupLoading(fieldKey = "") {
|
|
201
|
+
const key = normalizeText(fieldKey);
|
|
202
|
+
const entry = runtimes.get(key);
|
|
203
|
+
if (!entry) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Boolean(entry.runtime.isInitialLoading || entry.runtime.isFetching || entry.runtime.isRefetching);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resolveLookupSearch(fieldKey = "") {
|
|
211
|
+
const key = normalizeText(fieldKey);
|
|
212
|
+
const entry = runtimes.get(key);
|
|
213
|
+
if (!entry) {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return String(entry.runtime.searchQuery || "");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setLookupSearch(fieldKey = "", searchValue = "") {
|
|
221
|
+
const key = normalizeText(fieldKey);
|
|
222
|
+
const entry = runtimes.get(key);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
entry.runtime.searchQuery = String(searchValue || "");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
resolveLookupItems,
|
|
232
|
+
resolveLookupLoading,
|
|
233
|
+
resolveLookupSearch,
|
|
234
|
+
setLookupSearch
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { createCrudLookupFieldRuntime };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
2
2
|
import { asPlainObject } from "./scopeHelpers.js";
|
|
3
|
+
import { toRouteParamValue } from "./routeTemplateHelpers.js";
|
|
3
4
|
|
|
4
5
|
function normalizeCrudFormFields(fields = []) {
|
|
5
6
|
const normalizedFields = [];
|
|
@@ -109,6 +110,38 @@ function applyCrudPayloadToForm(fields = [], model = {}, payload = {}) {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function resolveCrudRouteBoundFieldValues(fields = [], routeParams = {}) {
|
|
114
|
+
const sourceRouteParams = asPlainObject(routeParams);
|
|
115
|
+
const values = {};
|
|
116
|
+
|
|
117
|
+
for (const field of normalizeCrudFormFields(fields)) {
|
|
118
|
+
const routeParamKey = String(field?.routeParamKey || "").trim();
|
|
119
|
+
if (!routeParamKey) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!Object.prototype.hasOwnProperty.call(sourceRouteParams, routeParamKey)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const routeValue = toRouteParamValue(sourceRouteParams[routeParamKey]);
|
|
127
|
+
if (!routeValue) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
values[field.key] = routeValue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return values;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function applyCrudRouteBoundFieldValues(fields = [], target = {}, routeParams = {}) {
|
|
137
|
+
const resolved = resolveCrudRouteBoundFieldValues(fields, routeParams);
|
|
138
|
+
const destination = asPlainObject(target);
|
|
139
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
140
|
+
destination[key] = value;
|
|
141
|
+
}
|
|
142
|
+
return resolved;
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
function resolveCrudFieldErrors(fieldErrors = {}, fieldKey = "") {
|
|
113
146
|
const key = String(fieldKey || "").trim();
|
|
114
147
|
if (!key) {
|
|
@@ -147,6 +180,8 @@ export {
|
|
|
147
180
|
createCrudFormModel,
|
|
148
181
|
buildCrudFormPayload,
|
|
149
182
|
applyCrudPayloadToForm,
|
|
183
|
+
resolveCrudRouteBoundFieldValues,
|
|
184
|
+
applyCrudRouteBoundFieldValues,
|
|
150
185
|
resolveCrudFieldErrors,
|
|
151
186
|
parseCrudResourceOperationInput
|
|
152
187
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeInteger,
|
|
3
|
+
normalizeText,
|
|
4
|
+
normalizeUniqueTextList
|
|
5
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LIST_SEARCH_DEBOUNCE_MS = 250;
|
|
9
|
+
const DEFAULT_LIST_SEARCH_MIN_LENGTH = 1;
|
|
10
|
+
const LIST_SEARCH_MODE_LOCAL = "local";
|
|
11
|
+
const LIST_SEARCH_MODE_QUERY = "query";
|
|
12
|
+
|
|
13
|
+
function normalizeListSearchConfig(value = {}) {
|
|
14
|
+
const source = asPlainObject(value);
|
|
15
|
+
const modeRaw = normalizeText(source.mode).toLowerCase();
|
|
16
|
+
const mode = modeRaw === LIST_SEARCH_MODE_LOCAL
|
|
17
|
+
? LIST_SEARCH_MODE_LOCAL
|
|
18
|
+
: LIST_SEARCH_MODE_QUERY;
|
|
19
|
+
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
enabled: source.enabled === true,
|
|
22
|
+
mode,
|
|
23
|
+
queryParam: normalizeText(source.queryParam) || "q",
|
|
24
|
+
label: normalizeText(source.label) || "Search",
|
|
25
|
+
placeholder: normalizeText(source.placeholder),
|
|
26
|
+
initialQuery: normalizeText(source.initialQuery),
|
|
27
|
+
debounceMs: normalizeInteger(source.debounceMs, {
|
|
28
|
+
fallback: DEFAULT_LIST_SEARCH_DEBOUNCE_MS,
|
|
29
|
+
min: 0
|
|
30
|
+
}),
|
|
31
|
+
minLength: normalizeInteger(source.minLength, {
|
|
32
|
+
fallback: DEFAULT_LIST_SEARCH_MIN_LENGTH,
|
|
33
|
+
min: DEFAULT_LIST_SEARCH_MIN_LENGTH
|
|
34
|
+
}),
|
|
35
|
+
fields: Object.freeze(normalizeUniqueTextList(source.fields))
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSearchableValue(value) {
|
|
40
|
+
if (value == null) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return normalizeText(value).toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
47
|
+
return String(value).toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function matchesLocalSearch(item = {}, query = "", searchFields = []) {
|
|
53
|
+
const source = asPlainObject(item);
|
|
54
|
+
const normalizedQuery = normalizeText(query).toLowerCase();
|
|
55
|
+
if (!normalizedQuery) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fields = Array.isArray(searchFields) ? searchFields : [];
|
|
60
|
+
if (fields.length > 0) {
|
|
61
|
+
return fields.some((field) => normalizeSearchableValue(source[field]).includes(normalizedQuery));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Object.values(source).some((value) => normalizeSearchableValue(value).includes(normalizedQuery));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
normalizeListSearchConfig,
|
|
69
|
+
matchesLocalSearch
|
|
70
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
|
|
3
|
+
function hasResolvedQueryData({ query = null, data = null } = {}) {
|
|
4
|
+
const querySucceeded = Boolean(unref(query?.isSuccess));
|
|
5
|
+
const hasDataPayload = unref(data) != null;
|
|
6
|
+
|
|
7
|
+
return querySucceeded || hasDataPayload;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { hasResolvedQueryData };
|