@jskit-ai/users-web 0.1.36 → 0.1.38
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 +8 -22
- package/package.json +16 -11
- package/src/client/components/MembersAdminClientElement.vue +5 -5
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +14 -25
- package/src/client/components/WorkspaceMembersClientElement.vue +19 -19
- package/src/client/components/WorkspaceProfileClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +1 -1
- package/src/client/components/WorkspacesClientElement.vue +4 -4
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +61 -0
- package/src/client/composables/{accountSettingsInvitesRuntime.js → account-settings/accountSettingsInvitesRuntime.js} +1 -1
- package/src/client/composables/{accountSettingsRuntimeConstants.js → account-settings/accountSettingsRuntimeConstants.js} +0 -4
- package/src/client/composables/{accountSettingsRuntimeHelpers.js → account-settings/accountSettingsRuntimeHelpers.js} +2 -2
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/{crudLookupFieldLabelSupport.js → crud/crudLookupFieldLabelSupport.js} +37 -5
- package/src/client/composables/{crudLookupFieldRuntime.js → crud/crudLookupFieldRuntime.js} +11 -4
- package/src/client/composables/{crudSchemaFormHelpers.js → crud/crudSchemaFormHelpers.js} +178 -5
- package/src/client/composables/internal/crudListParentTitleSupport.js +168 -0
- package/src/client/composables/internal/useOperationScope.js +1 -1
- package/src/client/composables/{useAddEdit.js → records/useAddEdit.js} +18 -8
- package/src/client/composables/{useCrudSchemaForm.js → records/useCrudAddEdit.js} +32 -15
- package/src/client/composables/records/useCrudList.js +83 -0
- package/src/client/composables/records/useCrudView.js +35 -0
- package/src/client/composables/records/useList.js +482 -0
- package/src/client/composables/{useView.js → records/useView.js} +7 -7
- package/src/client/composables/{addEditUiRuntime.js → runtime/addEditUiRuntime.js} +13 -4
- package/src/client/composables/{listUiRuntime.js → runtime/listUiRuntime.js} +20 -8
- package/src/client/composables/{operationAdapters.js → runtime/operationAdapters.js} +1 -1
- package/src/client/composables/{useEndpointResource.js → runtime/useEndpointResource.js} +5 -5
- package/src/client/composables/{useListCore.js → runtime/useListCore.js} +4 -4
- package/src/client/composables/{useUiFeedback.js → runtime/useUiFeedback.js} +1 -1
- package/src/client/composables/{viewUiRuntime.js → runtime/viewUiRuntime.js} +13 -4
- package/src/client/composables/support/listQueryParamSupport.js +459 -0
- package/src/client/composables/{routeTemplateHelpers.js → support/routeTemplateHelpers.js} +122 -0
- package/src/client/composables/useAccess.js +2 -2
- package/src/client/composables/useAccountSettingsRuntime.js +6 -6
- package/src/client/composables/useBootstrapQuery.js +1 -1
- package/src/client/composables/useCommand.js +5 -5
- package/src/client/composables/useCrudListParentTitle.js +131 -0
- package/src/client/composables/usePagedCollection.js +58 -7
- package/src/client/composables/useScopeRuntime.js +1 -1
- package/src/client/lib/bootstrap.js +1 -1
- package/src/client/lib/menuIcons.js +27 -6
- package/src/client/support/menuLinkTarget.js +93 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +2 -1
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +1 -1
- package/test/addEditUiRuntime.test.js +19 -1
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudLookupFieldRuntime.test.js +52 -2
- package/test/errorMessageHelpers.test.js +1 -1
- package/test/exportsContract.test.js +10 -1
- package/test/listQueryParamSupport.test.js +190 -0
- package/test/listUiRuntime.test.js +22 -1
- package/test/menuIcons.test.js +2 -0
- package/test/menuLinkTarget.test.js +116 -0
- package/test/permissions.test.js +2 -2
- package/test/refValueHelpers.test.js +1 -1
- package/test/resourceLoadStateHelpers.test.js +1 -1
- package/test/routeTemplateHelpers.test.js +57 -1
- package/test/scopeHelpers.test.js +1 -1
- package/test/{useCrudSchemaForm.test.js → useCrudAddEdit.test.js} +81 -1
- package/test/useCrudListParentTitle.test.js +143 -0
- package/test/useListSearchSupport.test.js +1 -1
- package/test/usePagedCollection.test.js +53 -0
- package/test/viewCoreLoading.test.js +1 -1
- package/test/viewUiRuntime.test.js +36 -1
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +0 -241
- package/src/client/composables/useList.js +0 -268
- /package/src/client/composables/{modelStateHelpers.js → runtime/modelStateHelpers.js} +0 -0
- /package/src/client/composables/{operationUiHelpers.js → runtime/operationUiHelpers.js} +0 -0
- /package/src/client/composables/{operationValidationHelpers.js → runtime/operationValidationHelpers.js} +0 -0
- /package/src/client/composables/{useAddEditCore.js → runtime/useAddEditCore.js} +0 -0
- /package/src/client/composables/{useCommandCore.js → runtime/useCommandCore.js} +0 -0
- /package/src/client/composables/{useFieldErrorBag.js → runtime/useFieldErrorBag.js} +0 -0
- /package/src/client/composables/{useViewCore.js → runtime/useViewCore.js} +0 -0
- /package/src/client/composables/{errorMessageHelpers.js → support/errorMessageHelpers.js} +0 -0
- /package/src/client/composables/{listSearchSupport.js → support/listSearchSupport.js} +0 -0
- /package/src/client/composables/{refValueHelpers.js → support/refValueHelpers.js} +0 -0
- /package/src/client/composables/{resourceLoadStateHelpers.js → support/resourceLoadStateHelpers.js} +0 -0
- /package/src/client/composables/{scopeHelpers.js → support/scopeHelpers.js} +0 -0
|
@@ -40,6 +40,17 @@ function resolveRouteParamsSource(source = null) {
|
|
|
40
40
|
return asPlainObject(resolveRouteSourceValue(source));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function normalizeRouteParamNameList(value = []) {
|
|
44
|
+
const source = Array.isArray(value) ? value : [];
|
|
45
|
+
return source
|
|
46
|
+
.map((entry) => String(entry || "").trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveRouteParamNamesSource(source = []) {
|
|
51
|
+
return normalizeRouteParamNameList(resolveRouteSourceValue(source));
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
function normalizeRoutePathname(value = "") {
|
|
44
55
|
const rawPathname = String(value || "").trim();
|
|
45
56
|
const sanitizedPathname = rawPathname.split(/[?#]/u, 1)[0] || "";
|
|
@@ -54,6 +65,115 @@ function resolveRoutePathnameSource(source = "") {
|
|
|
54
65
|
return normalizeRoutePathname(resolveRouteSourceValue(source));
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
function segmentMatchesParamValue(segment = "", paramValue = "") {
|
|
69
|
+
const normalizedSegment = String(segment || "").trim();
|
|
70
|
+
const normalizedParamValue = toRouteParamValue(paramValue);
|
|
71
|
+
if (!normalizedSegment || !normalizedParamValue) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const encodedParamValue = encodeURIComponent(normalizedParamValue);
|
|
76
|
+
if (normalizedSegment === encodedParamValue) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return decodeURIComponent(normalizedSegment) === normalizedParamValue;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findRouteParamSegmentIndex(segments = [], paramValue = "", fromIndex = 0) {
|
|
88
|
+
const source = Array.isArray(segments) ? segments : [];
|
|
89
|
+
const cursor = Number.isInteger(fromIndex) && fromIndex > 0 ? fromIndex : 0;
|
|
90
|
+
for (let index = cursor; index < source.length; index += 1) {
|
|
91
|
+
if (segmentMatchesParamValue(source[index], paramValue)) {
|
|
92
|
+
return index;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizePathPrefix(segments = [], endIndex = -1) {
|
|
99
|
+
const source = Array.isArray(segments) ? segments : [];
|
|
100
|
+
if (!Number.isInteger(endIndex) || endIndex < 0) {
|
|
101
|
+
return "/";
|
|
102
|
+
}
|
|
103
|
+
return `/${source.slice(0, endIndex + 1).join("/")}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveAnchorEndIndex(segmentIndex = -1, totalSegments = 0, anchorMode = "at") {
|
|
107
|
+
const normalizedSegmentIndex = Number.isInteger(segmentIndex) ? segmentIndex : -1;
|
|
108
|
+
if (normalizedSegmentIndex < 0) {
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalizedTotalSegments = Number.isInteger(totalSegments) && totalSegments > 0 ? totalSegments : 0;
|
|
113
|
+
const normalizedMode = String(anchorMode || "at").trim().toLowerCase();
|
|
114
|
+
if (normalizedMode === "before") {
|
|
115
|
+
return normalizedSegmentIndex - 1;
|
|
116
|
+
}
|
|
117
|
+
if (normalizedMode === "after") {
|
|
118
|
+
return normalizedTotalSegments > 0
|
|
119
|
+
? Math.min(normalizedSegmentIndex + 1, normalizedTotalSegments - 1)
|
|
120
|
+
: normalizedSegmentIndex + 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return normalizedSegmentIndex;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveScopedRoutePathname({
|
|
127
|
+
currentPathname = "/",
|
|
128
|
+
params = {},
|
|
129
|
+
orderedParamNames = [],
|
|
130
|
+
anchorParamName = "",
|
|
131
|
+
anchorParamValue = "",
|
|
132
|
+
anchorMode = "at"
|
|
133
|
+
} = {}) {
|
|
134
|
+
const normalizedCurrentPathname = resolveRoutePathnameSource(currentPathname);
|
|
135
|
+
const normalizedAnchorParamName = String(anchorParamName || "").trim();
|
|
136
|
+
if (!normalizedAnchorParamName) {
|
|
137
|
+
return normalizedCurrentPathname;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sourceParams = asPlainObject(params);
|
|
141
|
+
const segments = normalizedCurrentPathname.split("/").filter(Boolean);
|
|
142
|
+
if (segments.length < 1) {
|
|
143
|
+
return normalizedCurrentPathname;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const paramNames = resolveRouteParamNamesSource(orderedParamNames);
|
|
147
|
+
let cursor = 0;
|
|
148
|
+
for (const paramName of paramNames) {
|
|
149
|
+
const segmentIndex = findRouteParamSegmentIndex(segments, sourceParams[paramName], cursor);
|
|
150
|
+
if (segmentIndex < 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (paramName === normalizedAnchorParamName) {
|
|
155
|
+
const endIndex = resolveAnchorEndIndex(segmentIndex, segments.length, anchorMode);
|
|
156
|
+
return normalizePathPrefix(segments, endIndex);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cursor = segmentIndex + 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const fallbackAnchorValue = toRouteParamValue(anchorParamValue) ||
|
|
163
|
+
toRouteParamValue(sourceParams[normalizedAnchorParamName]);
|
|
164
|
+
if (!fallbackAnchorValue) {
|
|
165
|
+
return normalizedCurrentPathname;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fallbackSegmentIndex = findRouteParamSegmentIndex(segments, fallbackAnchorValue, 0);
|
|
169
|
+
if (fallbackSegmentIndex < 0) {
|
|
170
|
+
return normalizedCurrentPathname;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fallbackEndIndex = resolveAnchorEndIndex(fallbackSegmentIndex, segments.length, anchorMode);
|
|
174
|
+
return normalizePathPrefix(segments, fallbackEndIndex);
|
|
175
|
+
}
|
|
176
|
+
|
|
57
177
|
function resolveRouteTemplatePath(routeTemplate = "", params = {}) {
|
|
58
178
|
const normalizedTemplate = String(routeTemplate || "").trim();
|
|
59
179
|
if (!normalizedTemplate) {
|
|
@@ -155,7 +275,9 @@ export {
|
|
|
155
275
|
normalizeRouteParamName,
|
|
156
276
|
toRouteParamValue,
|
|
157
277
|
resolveRouteParamsSource,
|
|
278
|
+
resolveRouteParamNamesSource,
|
|
158
279
|
resolveRoutePathnameSource,
|
|
280
|
+
resolveScopedRoutePathname,
|
|
159
281
|
resolveRouteTemplatePath,
|
|
160
282
|
resolveRouteTemplateLocation,
|
|
161
283
|
extractRouteParamNames,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { hasPermission, normalizePermissionList } from "../lib/permissions.js";
|
|
3
|
-
import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
|
|
3
|
+
import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
|
|
4
4
|
import {
|
|
5
5
|
normalizeAccessMode,
|
|
6
6
|
resolveAccessModeEnabled
|
|
7
|
-
} from "./scopeHelpers.js";
|
|
7
|
+
} from "./support/scopeHelpers.js";
|
|
8
8
|
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
9
9
|
|
|
10
10
|
function asPermissionList(value) {
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
useWorkspaceSurfaceId
|
|
21
21
|
} from "./useWorkspaceSurfaceId.js";
|
|
22
|
-
import { useAddEdit } from "./useAddEdit.js";
|
|
22
|
+
import { useAddEdit } from "./records/useAddEdit.js";
|
|
23
23
|
import { useCommand } from "./useCommand.js";
|
|
24
|
-
import { useView } from "./useView.js";
|
|
24
|
+
import { useView } from "./records/useView.js";
|
|
25
25
|
import { usePaths } from "./usePaths.js";
|
|
26
26
|
import { resolveAccountSettingsPathFromPlacementContext } from "../lib/workspaceSurfacePaths.js";
|
|
27
27
|
import {
|
|
@@ -34,16 +34,16 @@ import {
|
|
|
34
34
|
NUMBER_FORMAT_OPTIONS,
|
|
35
35
|
THEME_OPTIONS,
|
|
36
36
|
TIME_ZONE_OPTIONS
|
|
37
|
-
} from "./accountSettingsRuntimeConstants.js";
|
|
37
|
+
} from "./account-settings/accountSettingsRuntimeConstants.js";
|
|
38
38
|
import {
|
|
39
39
|
normalizeAvatarSize,
|
|
40
40
|
normalizePendingInvite,
|
|
41
41
|
normalizeReturnToPath,
|
|
42
42
|
normalizeSettingsPayload,
|
|
43
43
|
resolveAllowedReturnToOrigins
|
|
44
|
-
} from "./accountSettingsRuntimeHelpers.js";
|
|
45
|
-
import { createAccountSettingsAvatarUploadRuntime } from "./accountSettingsAvatarUploadRuntime.js";
|
|
46
|
-
import { createAccountSettingsInvitesRuntime } from "./accountSettingsInvitesRuntime.js";
|
|
44
|
+
} from "./account-settings/accountSettingsRuntimeHelpers.js";
|
|
45
|
+
import { createAccountSettingsAvatarUploadRuntime } from "./account-settings/accountSettingsAvatarUploadRuntime.js";
|
|
46
|
+
import { createAccountSettingsInvitesRuntime } from "./account-settings/accountSettingsInvitesRuntime.js";
|
|
47
47
|
|
|
48
48
|
function useAccountSettingsRuntime() {
|
|
49
49
|
const route = useRoute();
|
|
@@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/vue-query";
|
|
|
5
5
|
import { normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
|
|
6
6
|
import { usersWebHttpClient } from "../lib/httpClient.js";
|
|
7
7
|
import { buildBootstrapApiPath } from "../lib/bootstrap.js";
|
|
8
|
-
import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
|
|
8
|
+
import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_BOOTSTRAP_STALE_TIME_MS = 60_000;
|
|
11
11
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { proxyRefs } from "vue";
|
|
2
2
|
import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
3
|
-
import { useCommandCore } from "./useCommandCore.js";
|
|
4
|
-
import { useEndpointResource } from "./useEndpointResource.js";
|
|
3
|
+
import { useCommandCore } from "./runtime/useCommandCore.js";
|
|
4
|
+
import { useEndpointResource } from "./runtime/useEndpointResource.js";
|
|
5
5
|
import { useOperationScope } from "./internal/useOperationScope.js";
|
|
6
|
-
import { useUiFeedback } from "./useUiFeedback.js";
|
|
7
|
-
import { useFieldErrorBag } from "./useFieldErrorBag.js";
|
|
6
|
+
import { useUiFeedback } from "./runtime/useUiFeedback.js";
|
|
7
|
+
import { useFieldErrorBag } from "./runtime/useFieldErrorBag.js";
|
|
8
8
|
import {
|
|
9
9
|
setupRouteChangeCleanup,
|
|
10
10
|
setupOperationErrorReporting
|
|
11
|
-
} from "./operationUiHelpers.js";
|
|
11
|
+
} from "./runtime/operationUiHelpers.js";
|
|
12
12
|
|
|
13
13
|
function useCommand({
|
|
14
14
|
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { computed, proxyRefs } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import {
|
|
5
|
+
resolveRouteParamsSource,
|
|
6
|
+
toRouteParamValue
|
|
7
|
+
} from "./support/routeTemplateHelpers.js";
|
|
8
|
+
import { useView } from "./records/useView.js";
|
|
9
|
+
import {
|
|
10
|
+
resolveCrudListParentDescriptor,
|
|
11
|
+
resolveCrudListParentRecordTitle,
|
|
12
|
+
resolveCrudListParentTitleFromItems
|
|
13
|
+
} from "./internal/crudListParentTitleSupport.js";
|
|
14
|
+
|
|
15
|
+
function normalizeQueryKeyPrefix(value = []) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value
|
|
18
|
+
.map((entry) => normalizeText(entry))
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizedValue = normalizeText(value);
|
|
23
|
+
return normalizedValue ? [normalizedValue] : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function useCrudListParentTitle({
|
|
27
|
+
listRuntime = null,
|
|
28
|
+
resource = {},
|
|
29
|
+
adapter = null,
|
|
30
|
+
recordIdParam = "recordId",
|
|
31
|
+
queryKeyPrefix = ["users-web", "crud-list-parent-title"],
|
|
32
|
+
placementSource = "users-web.crud-list-parent-title",
|
|
33
|
+
fallbackLoadError = "Unable to load parent record.",
|
|
34
|
+
notFoundMessage = "Parent record not found.",
|
|
35
|
+
route = null,
|
|
36
|
+
viewRuntimeFactory = useView
|
|
37
|
+
} = {}) {
|
|
38
|
+
const sourceRoute = route && typeof route === "object" ? route : useRoute();
|
|
39
|
+
const parentDescriptor = computed(() => {
|
|
40
|
+
const descriptor = resolveCrudListParentDescriptor({
|
|
41
|
+
resource,
|
|
42
|
+
route: sourceRoute,
|
|
43
|
+
recordIdParam
|
|
44
|
+
});
|
|
45
|
+
if (!descriptor) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const routeParams = resolveRouteParamsSource(sourceRoute?.params || {});
|
|
50
|
+
const routeParamValue = toRouteParamValue(routeParams[descriptor.routeParamKey]);
|
|
51
|
+
if (!routeParamValue) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
...descriptor,
|
|
57
|
+
routeParamValue
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const initialParentDescriptor = parentDescriptor.value || {};
|
|
62
|
+
const normalizedQueryKeyPrefix = normalizeQueryKeyPrefix(queryKeyPrefix);
|
|
63
|
+
const shouldLoadParentRecord = computed(() => {
|
|
64
|
+
const descriptor = parentDescriptor.value;
|
|
65
|
+
if (!descriptor?.apiUrlTemplate) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (Boolean(listRuntime?.isInitialLoading) || normalizeText(listRuntime?.loadError)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const items = Array.isArray(listRuntime?.items) ? listRuntime.items : [];
|
|
73
|
+
return items.length < 1;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const parentView = viewRuntimeFactory({
|
|
77
|
+
adapter,
|
|
78
|
+
apiUrlTemplate: normalizeText(initialParentDescriptor.apiUrlTemplate),
|
|
79
|
+
readEnabled: shouldLoadParentRecord,
|
|
80
|
+
recordIdParam: normalizeText(initialParentDescriptor.routeParamKey) || "recordId",
|
|
81
|
+
includeRecordIdInQueryKey: true,
|
|
82
|
+
queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
|
|
83
|
+
...normalizedQueryKeyPrefix,
|
|
84
|
+
normalizeText(initialParentDescriptor.relationNamespace),
|
|
85
|
+
normalizeText(initialParentDescriptor.routeParamKey),
|
|
86
|
+
String(surfaceId || ""),
|
|
87
|
+
String(workspaceSlug || "")
|
|
88
|
+
],
|
|
89
|
+
placementSource,
|
|
90
|
+
fallbackLoadError,
|
|
91
|
+
notFoundMessage
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const title = computed(() => {
|
|
95
|
+
const descriptor = parentDescriptor.value;
|
|
96
|
+
if (!descriptor) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parentRecordTitle = resolveCrudListParentRecordTitle(parentView?.record || {}, descriptor);
|
|
101
|
+
if (parentRecordTitle && shouldLoadParentRecord.value) {
|
|
102
|
+
return parentRecordTitle;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const listTitle = resolveCrudListParentTitleFromItems(listRuntime?.items, descriptor);
|
|
106
|
+
if (listTitle) {
|
|
107
|
+
return listTitle;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parentRecordTitle) {
|
|
111
|
+
return parentRecordTitle;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (descriptor.routeParamValue) {
|
|
115
|
+
return `${descriptor.entityLabel} #${descriptor.routeParamValue}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return descriptor.entityLabel;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return proxyRefs({
|
|
122
|
+
title,
|
|
123
|
+
descriptor: parentDescriptor,
|
|
124
|
+
shouldLoadParentRecord,
|
|
125
|
+
record: computed(() => parentView?.record || null),
|
|
126
|
+
isLoading: computed(() => Boolean(parentView?.isLoading)),
|
|
127
|
+
loadError: computed(() => normalizeText(parentView?.loadError))
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { useCrudListParentTitle };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
2
|
-
import { useInfiniteQuery } from "@tanstack/vue-query";
|
|
3
|
-
import { asPlainObject } from "./scopeHelpers.js";
|
|
4
|
-
import { resolveEnabledRef } from "./refValueHelpers.js";
|
|
5
|
-
import { toQueryErrorMessage } from "./errorMessageHelpers.js";
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { asPlainObject } from "./support/scopeHelpers.js";
|
|
4
|
+
import { resolveEnabledRef } from "./support/refValueHelpers.js";
|
|
5
|
+
import { toQueryErrorMessage } from "./support/errorMessageHelpers.js";
|
|
6
6
|
|
|
7
7
|
function defaultSelectItems(page) {
|
|
8
8
|
return Array.isArray(page?.items) ? page.items : [];
|
|
@@ -12,6 +12,42 @@ function defaultGetNextPageParam(lastPage) {
|
|
|
12
12
|
return lastPage?.nextCursor ?? null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function normalizeQueryKeyValue(queryKey = null) {
|
|
16
|
+
const resolved = unref(queryKey);
|
|
17
|
+
if (Array.isArray(resolved)) {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
if (resolved === null || resolved === undefined || resolved === "") {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [resolved];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function trimInfinitePagesToFirst(data = null) {
|
|
28
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sourcePages = Array.isArray(data.pages) ? data.pages : [];
|
|
33
|
+
const sourcePageParams = Array.isArray(data.pageParams) ? data.pageParams : [];
|
|
34
|
+
if (sourcePages.length < 2 && sourcePageParams.length < 2) {
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
const pages = sourcePages.slice(0, 1);
|
|
38
|
+
const pageParams = sourcePageParams.length > 0
|
|
39
|
+
? sourcePageParams.slice(0, 1)
|
|
40
|
+
: pages.length > 0
|
|
41
|
+
? [null]
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...data,
|
|
46
|
+
pages,
|
|
47
|
+
pageParams
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
function usePagedCollection({
|
|
16
52
|
queryKey,
|
|
17
53
|
enabled = true,
|
|
@@ -36,7 +72,9 @@ function usePagedCollection({
|
|
|
36
72
|
throw new TypeError("usePagedCollection dedupeBy must be a function when provided.");
|
|
37
73
|
}
|
|
38
74
|
|
|
75
|
+
const queryClient = useQueryClient();
|
|
39
76
|
const queryEnabled = computed(() => resolveEnabledRef(enabled));
|
|
77
|
+
const normalizedQueryKey = computed(() => normalizeQueryKeyValue(queryKey));
|
|
40
78
|
|
|
41
79
|
const query = useInfiniteQuery({
|
|
42
80
|
queryKey,
|
|
@@ -106,6 +144,15 @@ function usePagedCollection({
|
|
|
106
144
|
return query.fetchNextPage();
|
|
107
145
|
}
|
|
108
146
|
|
|
147
|
+
function trimToFirstPage() {
|
|
148
|
+
const key = normalizedQueryKey.value;
|
|
149
|
+
if (key.length < 1) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
queryClient.setQueryData(key, (data) => trimInfinitePagesToFirst(data));
|
|
154
|
+
}
|
|
155
|
+
|
|
109
156
|
return Object.freeze({
|
|
110
157
|
query,
|
|
111
158
|
pages,
|
|
@@ -118,8 +165,12 @@ function usePagedCollection({
|
|
|
118
165
|
hasMore,
|
|
119
166
|
loadError,
|
|
120
167
|
reload,
|
|
121
|
-
loadMore
|
|
168
|
+
loadMore,
|
|
169
|
+
trimToFirstPage
|
|
122
170
|
});
|
|
123
171
|
}
|
|
124
172
|
|
|
125
|
-
export {
|
|
173
|
+
export {
|
|
174
|
+
usePagedCollection,
|
|
175
|
+
trimInfinitePagesToFirst
|
|
176
|
+
};
|
|
@@ -30,7 +30,7 @@ function normalizeWorkspaceEntry(entry) {
|
|
|
30
30
|
surfaceColor: String(entry.surfaceColor || "").trim(),
|
|
31
31
|
surfaceVariantColor: String(entry.surfaceVariantColor || "").trim(),
|
|
32
32
|
avatarUrl: String(entry.avatarUrl || "").trim(),
|
|
33
|
-
|
|
33
|
+
roleSid: String(entry.roleSid || "member").trim().toLowerCase() || "member",
|
|
34
34
|
isAccessible: entry.isAccessible !== false
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as mdiIcons from "@mdi/js";
|
|
1
2
|
import {
|
|
2
3
|
mdiAccountCircleOutline,
|
|
3
4
|
mdiAccountCogOutline,
|
|
@@ -26,6 +27,26 @@ const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
|
|
|
26
27
|
console: mdiConsoleNetworkOutline
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
function resolveExplicitIconValue(explicitIcon = "") {
|
|
31
|
+
const normalizedExplicitIcon = normalizeText(explicitIcon);
|
|
32
|
+
if (!normalizedExplicitIcon) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!normalizedExplicitIcon.startsWith("mdi-")) {
|
|
37
|
+
return normalizedExplicitIcon;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const iconKey = normalizedExplicitIcon
|
|
41
|
+
.slice("mdi-".length)
|
|
42
|
+
.split("-")
|
|
43
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
44
|
+
.join("");
|
|
45
|
+
const exportName = `mdi${iconKey}`;
|
|
46
|
+
const resolvedIcon = mdiIcons[exportName];
|
|
47
|
+
return typeof resolvedIcon === "string" && resolvedIcon ? resolvedIcon : normalizedExplicitIcon;
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
function normalizePathname(value) {
|
|
30
51
|
const normalizedValue = normalizeText(value);
|
|
31
52
|
if (!normalizedValue) {
|
|
@@ -70,9 +91,9 @@ function resolveSurfaceSwitchIdFromLabel(label = "") {
|
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
94
|
+
const resolvedExplicitIcon = resolveExplicitIconValue(explicitIcon);
|
|
95
|
+
if (resolvedExplicitIcon) {
|
|
96
|
+
return resolvedExplicitIcon;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
const normalizedSurfaceId = normalizeText(surfaceId).toLowerCase();
|
|
@@ -80,9 +101,9 @@ function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
|
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
function resolveMenuLinkIcon({ icon = "", label = "", to = "" } = {}) {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
104
|
+
const resolvedExplicitIcon = resolveExplicitIconValue(icon);
|
|
105
|
+
if (resolvedExplicitIcon) {
|
|
106
|
+
return resolvedExplicitIcon;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
const normalizedLabel = normalizeText(label).toLowerCase();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
|
|
3
|
+
|
|
4
|
+
function normalizeMenuLinkPathname(pathname = "") {
|
|
5
|
+
const source = String(pathname || "").trim();
|
|
6
|
+
if (!source) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const queryIndex = source.indexOf("?");
|
|
11
|
+
const hashIndex = source.indexOf("#");
|
|
12
|
+
const cutoff =
|
|
13
|
+
queryIndex < 0
|
|
14
|
+
? hashIndex
|
|
15
|
+
: hashIndex < 0
|
|
16
|
+
? queryIndex
|
|
17
|
+
: Math.min(queryIndex, hashIndex);
|
|
18
|
+
|
|
19
|
+
return cutoff < 0 ? source : source.slice(0, cutoff);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveMenuLinkSurfaceId(surface = "", fallbackSurfaceId = "") {
|
|
23
|
+
const explicitSurface = normalizeText(surface).toLowerCase();
|
|
24
|
+
if (explicitSurface && explicitSurface !== "*") {
|
|
25
|
+
return explicitSurface;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalizeText(fallbackSurfaceId).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function interpolateBracketParams(pathTemplate = "", params = {}) {
|
|
32
|
+
const source = String(pathTemplate || "").trim();
|
|
33
|
+
if (!source) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return source.replace(/\[([^\]]+)\]/g, (_match, rawKey) => {
|
|
38
|
+
const key = String(rawKey || "").trim();
|
|
39
|
+
if (!key) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = params?.[key];
|
|
44
|
+
return value == null ? `[${key}]` : encodeURIComponent(String(value));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRelativeMenuLinkTarget(target = "") {
|
|
49
|
+
const normalizedTarget = normalizeText(target);
|
|
50
|
+
return normalizedTarget.startsWith("./") || normalizedTarget.startsWith("../");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveMenuLinkTarget({
|
|
54
|
+
to = "",
|
|
55
|
+
surface = "",
|
|
56
|
+
currentSurfaceId = "",
|
|
57
|
+
placementContext = null,
|
|
58
|
+
workspaceSuffix = "/",
|
|
59
|
+
nonWorkspaceSuffix = "/",
|
|
60
|
+
routeParams = {},
|
|
61
|
+
resolvePagePath = null
|
|
62
|
+
} = {}) {
|
|
63
|
+
const explicitTarget = normalizeText(to);
|
|
64
|
+
const targetSurfaceId = resolveMenuLinkSurfaceId(surface, currentSurfaceId);
|
|
65
|
+
const workspaceRequired = surfaceRequiresWorkspaceFromPlacementContext(placementContext, targetSurfaceId);
|
|
66
|
+
const suffixTemplate = normalizeText(workspaceRequired ? workspaceSuffix : nonWorkspaceSuffix) || "/";
|
|
67
|
+
const interpolatedSuffix = interpolateBracketParams(suffixTemplate, routeParams);
|
|
68
|
+
const resolvedSuffixTarget =
|
|
69
|
+
typeof resolvePagePath === "function" &&
|
|
70
|
+
targetSurfaceId &&
|
|
71
|
+
interpolatedSuffix &&
|
|
72
|
+
!interpolatedSuffix.includes("[")
|
|
73
|
+
? normalizeText(resolvePagePath(interpolatedSuffix, {
|
|
74
|
+
surface: targetSurfaceId,
|
|
75
|
+
mode: "auto"
|
|
76
|
+
}))
|
|
77
|
+
: "";
|
|
78
|
+
|
|
79
|
+
if (!explicitTarget) {
|
|
80
|
+
return resolvedSuffixTarget;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isRelativeMenuLinkTarget(explicitTarget)) {
|
|
84
|
+
return resolvedSuffixTarget;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return explicitTarget;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
normalizeMenuLinkPathname,
|
|
92
|
+
resolveMenuLinkTarget
|
|
93
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { mdiAlertCircleOutline } from "@mdi/js";
|
|
2
3
|
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
const props = defineProps({
|
|
@@ -20,7 +21,7 @@ const normalizedMessage = computed(() => String(props.message || "").trim() || "
|
|
|
20
21
|
<v-card rounded="lg" elevation="1" border>
|
|
21
22
|
<v-card-item>
|
|
22
23
|
<template #prepend>
|
|
23
|
-
<v-icon icon="
|
|
24
|
+
<v-icon :icon="mdiAlertCircleOutline" color="error" />
|
|
24
25
|
</template>
|
|
25
26
|
<v-card-title class="text-h5">Unavailable</v-card-title>
|
|
26
27
|
<v-card-subtitle>{{ normalizedSurfaceLabel }} surface.</v-card-subtitle>
|
|
@@ -33,7 +33,7 @@ const invites = props.runtime.invites;
|
|
|
33
33
|
v-for="invite in invites.items.value"
|
|
34
34
|
:key="invite.id"
|
|
35
35
|
:title="invite.workspaceName"
|
|
36
|
-
:subtitle="`/${invite.workspaceSlug} • role: ${invite.
|
|
36
|
+
:subtitle="`/${invite.workspaceSlug} • role: ${invite.roleSid}`"
|
|
37
37
|
class="px-0"
|
|
38
38
|
>
|
|
39
39
|
<template #prepend>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import { createAddEditUiRuntime } from "../src/client/composables/addEditUiRuntime.js";
|
|
4
|
+
import { createAddEditUiRuntime } from "../src/client/composables/runtime/addEditUiRuntime.js";
|
|
5
5
|
|
|
6
6
|
test("createAddEditUiRuntime resolves api/list/cancel paths from route params", () => {
|
|
7
7
|
const runtime = createAddEditUiRuntime({
|
|
@@ -51,6 +51,24 @@ test("createAddEditUiRuntime resolves edit-page relative list and cancel links",
|
|
|
51
51
|
assert.equal(runtime.cancelUrl.value, "/contacts/7/addresses/42");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("createAddEditUiRuntime resolves nested edit links from the record scope", () => {
|
|
55
|
+
const runtime = createAddEditUiRuntime({
|
|
56
|
+
recordIdParam: "petId",
|
|
57
|
+
routeParams: ref({
|
|
58
|
+
workspaceSlug: "dogandgroom",
|
|
59
|
+
contactId: "541841",
|
|
60
|
+
petId: "715528"
|
|
61
|
+
}),
|
|
62
|
+
routeParamNames: ref(["workspaceSlug", "contactId", "petId"]),
|
|
63
|
+
routePath: ref("/w/dogandgroom/admin/contacts/541841/pets/715528/edit/advanced"),
|
|
64
|
+
viewUrlTemplate: "..",
|
|
65
|
+
listUrlTemplate: "../.."
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.equal(runtime.cancelUrl.value, "/w/dogandgroom/admin/contacts/541841/pets/715528");
|
|
69
|
+
assert.equal(runtime.listUrl.value, "/w/dogandgroom/admin/contacts/541841/pets");
|
|
70
|
+
});
|
|
71
|
+
|
|
54
72
|
test("createAddEditUiRuntime supports custom saved-record selector", () => {
|
|
55
73
|
const runtime = createAddEditUiRuntime({
|
|
56
74
|
recordIdParam: "addressId",
|