@jskit-ai/users-web 0.1.4
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 +507 -0
- package/package.json +31 -0
- package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
- package/src/client/components/MembersAdminClientElement.vue +404 -0
- package/src/client/components/ProfileClientElement.vue +242 -0
- package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
- package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
- package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
- package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
- package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
- package/src/client/components/UsersWorkspaceSelector.vue +237 -0
- package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
- package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
- package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
- package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
- package/src/client/components/WorkspacesClientElement.vue +514 -0
- package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
- package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
- package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
- package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
- package/src/client/composables/errorMessageHelpers.js +66 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/modelStateHelpers.js +49 -0
- package/src/client/composables/operationUiHelpers.js +121 -0
- package/src/client/composables/operationValidationHelpers.js +52 -0
- package/src/client/composables/refValueHelpers.js +19 -0
- package/src/client/composables/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useAccountSettingsRuntime.js +533 -0
- package/src/client/composables/useAddEdit.js +135 -0
- package/src/client/composables/useAddEditCore.js +137 -0
- package/src/client/composables/useBootstrapQuery.js +52 -0
- package/src/client/composables/useCommand.js +112 -0
- package/src/client/composables/useCommandCore.js +130 -0
- package/src/client/composables/useEndpointResource.js +104 -0
- package/src/client/composables/useFieldErrorBag.js +61 -0
- package/src/client/composables/useList.js +85 -0
- package/src/client/composables/useListCore.js +65 -0
- package/src/client/composables/usePagedCollection.js +125 -0
- package/src/client/composables/usePaths.js +108 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
- package/src/client/composables/useScopeRuntime.js +107 -0
- package/src/client/composables/useSurfaceRouteContext.js +31 -0
- package/src/client/composables/useUiFeedback.js +96 -0
- package/src/client/composables/useView.js +89 -0
- package/src/client/composables/useViewCore.js +104 -0
- package/src/client/composables/useWorkspaceRouteContext.js +28 -0
- package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
- package/src/client/index.js +7 -0
- package/src/client/lib/bootstrap.js +95 -0
- package/src/client/lib/httpClient.js +67 -0
- package/src/client/lib/menuIcons.js +192 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
- package/src/client/lib/surfaceAccessPolicy.js +350 -0
- package/src/client/lib/theme.js +99 -0
- package/src/client/lib/workspaceLinkResolver.js +207 -0
- package/src/client/lib/workspaceSurfaceContext.js +82 -0
- package/src/client/lib/workspaceSurfacePaths.js +163 -0
- package/src/client/providers/UsersWebClientProvider.js +85 -0
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
- package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/realtimeWorkspace.js +12 -0
- package/src/client/support/runtimeNormalization.js +27 -0
- package/src/client/support/workspaceQueryKeys.js +15 -0
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
- package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
- package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
- package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
- package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
- package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
- package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
- package/templates/src/pages/account/index.vue +17 -0
- package/templates/src/pages/admin/members/index.vue +7 -0
- package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
- package/templates/src/pages/console/settings/index.vue +16 -0
- package/templates/src/surfaces/admin/index.vue +29 -0
- package/templates/src/surfaces/admin/root.vue +20 -0
- package/templates/src/surfaces/app/index.vue +27 -0
- package/templates/src/surfaces/app/root.vue +20 -0
- package/test/bootstrap.test.js +38 -0
- package/test/bootstrapPlacementRuntime.test.js +991 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +39 -0
- package/test/menuIcons.test.js +33 -0
- package/test/permissions.test.js +35 -0
- package/test/profileSurfaceMenuLinks.test.js +207 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/surfaceAccessPolicy.test.js +129 -0
- package/test/theme.test.js +95 -0
- package/test/workspaceLinkResolver.test.js +61 -0
- package/test/workspaceSurfacePaths.test.js +39 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
3
|
+
import { usersWebHttpClient } from "../lib/httpClient.js";
|
|
4
|
+
import { asPlainObject } from "./scopeHelpers.js";
|
|
5
|
+
import { resolveEnabledRef, resolveTextRef } from "./refValueHelpers.js";
|
|
6
|
+
import { usePagedCollection } from "./usePagedCollection.js";
|
|
7
|
+
|
|
8
|
+
function appendPageParam(path, pageParam) {
|
|
9
|
+
const normalizedPath = String(path || "").trim();
|
|
10
|
+
if (!normalizedPath) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
if (pageParam === null || pageParam === undefined || pageParam === "") {
|
|
14
|
+
return normalizedPath;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const query = new URLSearchParams();
|
|
18
|
+
query.set("cursor", String(pageParam));
|
|
19
|
+
return appendQueryString(normalizedPath, query.toString());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useListCore({
|
|
23
|
+
queryKey,
|
|
24
|
+
path = "",
|
|
25
|
+
enabled = true,
|
|
26
|
+
client = usersWebHttpClient,
|
|
27
|
+
initialPageParam = null,
|
|
28
|
+
getNextPageParam,
|
|
29
|
+
selectItems,
|
|
30
|
+
requestOptions = null,
|
|
31
|
+
queryOptions = null,
|
|
32
|
+
fallbackLoadError = "Unable to load list."
|
|
33
|
+
} = {}) {
|
|
34
|
+
if (!client || typeof client.request !== "function") {
|
|
35
|
+
throw new TypeError("useListCore requires a client with request().");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalizedPath = computed(() => resolveTextRef(path));
|
|
39
|
+
const queryEnabled = computed(() => resolveEnabledRef(enabled) && Boolean(normalizedPath.value));
|
|
40
|
+
|
|
41
|
+
const collection = usePagedCollection({
|
|
42
|
+
queryKey,
|
|
43
|
+
initialPageParam,
|
|
44
|
+
enabled: queryEnabled,
|
|
45
|
+
queryFn: async ({ pageParam }) => {
|
|
46
|
+
const requestPath = appendPageParam(normalizedPath.value, pageParam);
|
|
47
|
+
if (!requestPath) {
|
|
48
|
+
throw new Error("List path is required.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return client.request(requestPath, {
|
|
52
|
+
method: "GET",
|
|
53
|
+
...(asPlainObject(requestOptions))
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
getNextPageParam,
|
|
57
|
+
selectItems,
|
|
58
|
+
queryOptions,
|
|
59
|
+
fallbackLoadError
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return collection;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { useListCore };
|
|
@@ -0,0 +1,125 @@
|
|
|
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";
|
|
6
|
+
|
|
7
|
+
function defaultSelectItems(page) {
|
|
8
|
+
return Array.isArray(page?.items) ? page.items : [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultGetNextPageParam(lastPage) {
|
|
12
|
+
return lastPage?.nextCursor ?? null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function usePagedCollection({
|
|
16
|
+
queryKey,
|
|
17
|
+
enabled = true,
|
|
18
|
+
initialPageParam = null,
|
|
19
|
+
queryFn,
|
|
20
|
+
getNextPageParam = defaultGetNextPageParam,
|
|
21
|
+
selectItems = defaultSelectItems,
|
|
22
|
+
dedupeBy = null,
|
|
23
|
+
queryOptions = null,
|
|
24
|
+
fallbackLoadError = "Unable to load list."
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (typeof queryFn !== "function") {
|
|
27
|
+
throw new TypeError("usePagedCollection requires queryFn().");
|
|
28
|
+
}
|
|
29
|
+
if (typeof getNextPageParam !== "function") {
|
|
30
|
+
throw new TypeError("usePagedCollection requires getNextPageParam().");
|
|
31
|
+
}
|
|
32
|
+
if (typeof selectItems !== "function") {
|
|
33
|
+
throw new TypeError("usePagedCollection requires selectItems().");
|
|
34
|
+
}
|
|
35
|
+
if (dedupeBy != null && typeof dedupeBy !== "function") {
|
|
36
|
+
throw new TypeError("usePagedCollection dedupeBy must be a function when provided.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const queryEnabled = computed(() => resolveEnabledRef(enabled));
|
|
40
|
+
|
|
41
|
+
const query = useInfiniteQuery({
|
|
42
|
+
queryKey,
|
|
43
|
+
initialPageParam,
|
|
44
|
+
enabled: queryEnabled,
|
|
45
|
+
queryFn,
|
|
46
|
+
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
|
47
|
+
getNextPageParam(lastPage, allPages, lastPageParam, allPageParams),
|
|
48
|
+
...(asPlainObject(queryOptions))
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const pages = computed(() => {
|
|
52
|
+
const pageList = query.data.value?.pages;
|
|
53
|
+
return Array.isArray(pageList) ? pageList : [];
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const items = computed(() => {
|
|
57
|
+
const result = [];
|
|
58
|
+
const seen = dedupeBy ? new Set() : null;
|
|
59
|
+
|
|
60
|
+
for (const page of pages.value) {
|
|
61
|
+
const pageItems = selectItems(page);
|
|
62
|
+
if (!Array.isArray(pageItems)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!seen) {
|
|
67
|
+
result.push(...pageItems);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const item of pageItems) {
|
|
72
|
+
const key = dedupeBy(item);
|
|
73
|
+
const normalizedKey = String(key ?? "").trim();
|
|
74
|
+
if (!normalizedKey) {
|
|
75
|
+
result.push(item);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (seen.has(normalizedKey)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.add(normalizedKey);
|
|
82
|
+
result.push(item);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const isInitialLoading = computed(() => Boolean(query.isPending.value));
|
|
90
|
+
const isFetching = computed(() => Boolean(query.isFetching.value));
|
|
91
|
+
const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
|
|
92
|
+
const isLoading = computed(() => Boolean(isInitialLoading.value || isFetching.value));
|
|
93
|
+
const isLoadingMore = computed(() => Boolean(query.isFetchingNextPage.value));
|
|
94
|
+
const hasMore = computed(() => Boolean(query.hasNextPage.value));
|
|
95
|
+
const loadError = computed(() => toQueryErrorMessage(query.error.value, fallbackLoadError, "Unable to load list."));
|
|
96
|
+
|
|
97
|
+
async function reload() {
|
|
98
|
+
return query.refetch();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function loadMore() {
|
|
102
|
+
if (!hasMore.value || isLoadingMore.value) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return query.fetchNextPage();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
query,
|
|
111
|
+
pages,
|
|
112
|
+
items,
|
|
113
|
+
isInitialLoading,
|
|
114
|
+
isFetching,
|
|
115
|
+
isRefetching,
|
|
116
|
+
isLoading,
|
|
117
|
+
isLoadingMore,
|
|
118
|
+
hasMore,
|
|
119
|
+
loadError,
|
|
120
|
+
reload,
|
|
121
|
+
loadMore
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { usePagedCollection };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
|
|
4
|
+
import { useWorkspaceRouteContext } from "./useWorkspaceRouteContext.js";
|
|
5
|
+
import { useWorkspaceLinkResolver } from "../lib/workspaceLinkResolver.js";
|
|
6
|
+
import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
|
|
7
|
+
|
|
8
|
+
function normalizePathSuffix(value = "") {
|
|
9
|
+
const raw = normalizeText(unref(value));
|
|
10
|
+
if (!raw) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveSurfaceId(value, fallback = "") {
|
|
18
|
+
const normalized = normalizeText(unref(value)).toLowerCase();
|
|
19
|
+
if (normalized && normalized !== "*") {
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalizedFallback = normalizeText(unref(fallback)).toLowerCase();
|
|
24
|
+
if (normalizedFallback && normalizedFallback !== "*") {
|
|
25
|
+
return normalizedFallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveWorkspaceSlug(value, fallback = "") {
|
|
32
|
+
const normalized = normalizeText(unref(value));
|
|
33
|
+
if (normalized) {
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return normalizeText(unref(fallback));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveDefaultSurfaceIdFromPlacementContext(placementContext = null) {
|
|
41
|
+
return resolveSurfaceId(placementContext?.surfaceConfig?.defaultSurfaceId, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function usePaths({ routeContext: sourceRouteContext = null } = {}) {
|
|
45
|
+
const routeContext = sourceRouteContext || useWorkspaceRouteContext();
|
|
46
|
+
const workspaceLinkResolver = useWorkspaceLinkResolver();
|
|
47
|
+
const workspaceSlug = computed(() => String(routeContext.workspaceSlugFromRoute.value || "").trim());
|
|
48
|
+
|
|
49
|
+
function page(relativePath = "/", options = {}) {
|
|
50
|
+
const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
51
|
+
const surface =
|
|
52
|
+
resolveSurfaceId(source.surface, routeContext.currentSurfaceId.value) ||
|
|
53
|
+
resolveDefaultSurfaceIdFromPlacementContext(routeContext.placementContext.value);
|
|
54
|
+
if (!surface) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
const nextWorkspaceSlug = resolveWorkspaceSlug(source.workspaceSlug, workspaceSlug.value);
|
|
58
|
+
const mode = normalizeText(source.mode).toLowerCase() || "auto";
|
|
59
|
+
|
|
60
|
+
return workspaceLinkResolver.resolve(relativePath, {
|
|
61
|
+
surface,
|
|
62
|
+
workspaceSlug: nextWorkspaceSlug,
|
|
63
|
+
mode
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function api(relativePath = "", options = {}) {
|
|
68
|
+
const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
69
|
+
const surface =
|
|
70
|
+
resolveSurfaceId(source.surface, routeContext.currentSurfaceId.value) ||
|
|
71
|
+
resolveDefaultSurfaceIdFromPlacementContext(routeContext.placementContext.value);
|
|
72
|
+
const suffix = normalizePathSuffix(relativePath);
|
|
73
|
+
const workspaceScoped = surfaceRequiresWorkspaceFromPlacementContext(routeContext.placementContext.value, surface);
|
|
74
|
+
|
|
75
|
+
if (!suffix) {
|
|
76
|
+
throw new TypeError("usePaths().api(relativePath) requires a non-empty relativePath.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const templatePath = resolveApiBasePath({
|
|
80
|
+
surfaceRequiresWorkspace: workspaceScoped,
|
|
81
|
+
relativePath: suffix
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (workspaceScoped) {
|
|
85
|
+
const nextWorkspaceSlug = resolveWorkspaceSlug(source.workspaceSlug, workspaceSlug.value);
|
|
86
|
+
if (!nextWorkspaceSlug) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`usePaths().api(${suffix}) requires workspace slug for workspace surface "${surface || "<unknown>"}".`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return templatePath.replace(":workspaceSlug", nextWorkspaceSlug);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return templatePath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
route: routeContext.route,
|
|
100
|
+
placementContext: routeContext.placementContext,
|
|
101
|
+
currentSurfaceId: routeContext.currentSurfaceId,
|
|
102
|
+
workspaceSlug,
|
|
103
|
+
page,
|
|
104
|
+
api
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { usePaths };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { useRealtimeEvent } from "@jskit-ai/realtime/client/composables/useRealtimeEvent";
|
|
5
|
+
|
|
6
|
+
function normalizeRealtimeOptions(value = {}) {
|
|
7
|
+
if (value === null || value === undefined || value === false) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
throw new TypeError("realtime must be an object when configured.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveEnabled(value) {
|
|
18
|
+
if (typeof value === "undefined") {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Boolean(unref(value));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toQueryKeyList(value) {
|
|
26
|
+
const resolved = typeof value === "function" ? value() : unref(value);
|
|
27
|
+
if (!Array.isArray(resolved) || resolved.length < 1) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(resolved[0])) {
|
|
32
|
+
return resolved
|
|
33
|
+
.filter((entry) => Array.isArray(entry) && entry.length > 0)
|
|
34
|
+
.map((entry) => Object.freeze([...entry]));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [Object.freeze([...resolved])];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function useRealtimeQueryInvalidation({
|
|
41
|
+
event = "",
|
|
42
|
+
enabled = true,
|
|
43
|
+
matches = null,
|
|
44
|
+
queryKey = null,
|
|
45
|
+
onEvent = null
|
|
46
|
+
} = {}) {
|
|
47
|
+
const queryClient = useQueryClient();
|
|
48
|
+
const normalizedEvent = computed(() => normalizeText(unref(event)));
|
|
49
|
+
const active = computed(() => resolveEnabled(enabled) && Boolean(normalizedEvent.value));
|
|
50
|
+
|
|
51
|
+
const listener = useRealtimeEvent({
|
|
52
|
+
event: normalizedEvent,
|
|
53
|
+
enabled: active,
|
|
54
|
+
matches,
|
|
55
|
+
onEvent: async (context) => {
|
|
56
|
+
if (typeof onEvent === "function") {
|
|
57
|
+
await onEvent(context);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const keys = toQueryKeyList(queryKey);
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
await queryClient.invalidateQueries({
|
|
63
|
+
queryKey: key
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
active: listener.active
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function useOperationRealtime({
|
|
75
|
+
realtime = null,
|
|
76
|
+
queryKey = null,
|
|
77
|
+
enabled = true
|
|
78
|
+
} = {}) {
|
|
79
|
+
const source = normalizeRealtimeOptions(realtime);
|
|
80
|
+
if (!source.event) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const event = source.event;
|
|
85
|
+
const matches = typeof source.matches === "function" ? source.matches : null;
|
|
86
|
+
const onEvent = typeof source.onEvent === "function" ? source.onEvent : null;
|
|
87
|
+
const resolvedQueryKey = Object.hasOwn(source, "queryKey") ? source.queryKey : queryKey;
|
|
88
|
+
const active = computed(() => {
|
|
89
|
+
const sourceEnabled = Object.hasOwn(source, "enabled") ? source.enabled : true;
|
|
90
|
+
return resolveEnabled(enabled) && resolveEnabled(sourceEnabled);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return useRealtimeQueryInvalidation({
|
|
94
|
+
event,
|
|
95
|
+
enabled: active,
|
|
96
|
+
matches,
|
|
97
|
+
queryKey: resolvedQueryKey,
|
|
98
|
+
onEvent
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
useRealtimeQueryInvalidation,
|
|
104
|
+
useOperationRealtime
|
|
105
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
+
import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
4
|
+
import { useAccess } from "./useAccess.js";
|
|
5
|
+
import { useWorkspaceRouteContext } from "./useWorkspaceRouteContext.js";
|
|
6
|
+
import { usePaths } from "./usePaths.js";
|
|
7
|
+
import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
|
|
8
|
+
import {
|
|
9
|
+
asPlainObject,
|
|
10
|
+
ensureAccessModeCompatibility,
|
|
11
|
+
resolveAccessModeEnabled,
|
|
12
|
+
normalizeOwnershipFilter,
|
|
13
|
+
resolveApiSuffix
|
|
14
|
+
} from "./scopeHelpers.js";
|
|
15
|
+
|
|
16
|
+
function useScopeRuntime({
|
|
17
|
+
ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
18
|
+
surfaceId = "",
|
|
19
|
+
accessMode = "auto",
|
|
20
|
+
hasPermissionRequirements = false,
|
|
21
|
+
placementSource = "users-web.scope-runtime"
|
|
22
|
+
} = {}) {
|
|
23
|
+
const normalizedOwnershipFilter = normalizeOwnershipFilter(ownershipFilter);
|
|
24
|
+
const normalizedAccessMode = ensureAccessModeCompatibility({
|
|
25
|
+
accessMode,
|
|
26
|
+
hasPermissionRequirements,
|
|
27
|
+
caller: "useScopeRuntime"
|
|
28
|
+
});
|
|
29
|
+
const accessRequired = resolveAccessModeEnabled(normalizedAccessMode, {
|
|
30
|
+
hasPermissionRequirements
|
|
31
|
+
});
|
|
32
|
+
const routeContext = useWorkspaceRouteContext();
|
|
33
|
+
const paths = usePaths({
|
|
34
|
+
routeContext
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const workspaceSlugFromRoute = routeContext.workspaceSlugFromRoute;
|
|
38
|
+
const resolvedSurfaceId = computed(() => {
|
|
39
|
+
const explicitSurfaceId = normalizeSurfaceId(surfaceId);
|
|
40
|
+
if (explicitSurfaceId) {
|
|
41
|
+
return explicitSurfaceId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return normalizeSurfaceId(routeContext.currentSurfaceId.value);
|
|
45
|
+
});
|
|
46
|
+
const workspaceScoped = computed(() =>
|
|
47
|
+
surfaceRequiresWorkspaceFromPlacementContext(routeContext.placementContext.value, resolvedSurfaceId.value)
|
|
48
|
+
);
|
|
49
|
+
const hasRouteWorkspaceSlug = computed(() => (workspaceScoped.value ? Boolean(workspaceSlugFromRoute.value) : true));
|
|
50
|
+
const workspaceRouteError = computed(() => {
|
|
51
|
+
if (!workspaceScoped.value || hasRouteWorkspaceSlug.value) {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `Route parameter workspaceSlug is required for surface "${resolvedSurfaceId.value || "<unknown>"}".`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const accessRuntime = useAccess({
|
|
59
|
+
workspaceSlug: computed(() => (workspaceScoped.value ? workspaceSlugFromRoute.value : "")),
|
|
60
|
+
enabled: computed(() => accessRequired && hasRouteWorkspaceSlug.value),
|
|
61
|
+
access: normalizedAccessMode,
|
|
62
|
+
hasPermissionRequirements,
|
|
63
|
+
mergePlacementContext: accessRequired ? routeContext.mergePlacementContext : null,
|
|
64
|
+
placementSource: String(placementSource || "users-web.scope-runtime")
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function resolveApiPath(apiSuffix = "", context = {}) {
|
|
68
|
+
if (workspaceRouteError.value) {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const suffix = resolveApiSuffix(apiSuffix, {
|
|
73
|
+
surfaceId: routeContext.currentSurfaceId.value,
|
|
74
|
+
workspaceSlug: workspaceSlugFromRoute.value,
|
|
75
|
+
ownershipFilter: normalizedOwnershipFilter,
|
|
76
|
+
...asPlainObject(context)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return paths.api(suffix, {
|
|
80
|
+
surface: resolvedSurfaceId.value,
|
|
81
|
+
workspaceSlug: workspaceSlugFromRoute.value
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireWorkspaceRouteParam(caller = "useScopeRuntime") {
|
|
86
|
+
if (workspaceRouteError.value) {
|
|
87
|
+
throw new Error(`${caller}: ${workspaceRouteError.value}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
normalizedOwnershipFilter,
|
|
93
|
+
workspaceScoped: workspaceScoped.value,
|
|
94
|
+
resolvedSurfaceId,
|
|
95
|
+
accessMode: normalizedAccessMode,
|
|
96
|
+
accessRequired,
|
|
97
|
+
routeContext,
|
|
98
|
+
workspaceSlugFromRoute,
|
|
99
|
+
hasRouteWorkspaceSlug,
|
|
100
|
+
workspaceRouteError,
|
|
101
|
+
access: accessRuntime,
|
|
102
|
+
resolveApiPath,
|
|
103
|
+
requireWorkspaceRouteParam
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { useScopeRuntime };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import {
|
|
4
|
+
resolveRuntimePathname,
|
|
5
|
+
resolveSurfaceIdFromPlacementPathname,
|
|
6
|
+
useWebPlacementContext
|
|
7
|
+
} from "@jskit-ai/shell-web/client/placement";
|
|
8
|
+
import { resolveWorkspaceSurfaceIdFromPlacementPathname } from "../lib/workspaceSurfacePaths.js";
|
|
9
|
+
|
|
10
|
+
function useSurfaceRouteContext() {
|
|
11
|
+
const route = useRoute();
|
|
12
|
+
const { context: placementContext, mergeContext: mergePlacementContext } = useWebPlacementContext();
|
|
13
|
+
const routePath = computed(() => resolveRuntimePathname(route?.path));
|
|
14
|
+
|
|
15
|
+
const currentSurfaceId = computed(() => {
|
|
16
|
+
return (
|
|
17
|
+
resolveWorkspaceSurfaceIdFromPlacementPathname(placementContext.value, routePath.value) ||
|
|
18
|
+
resolveSurfaceIdFromPlacementPathname(placementContext.value, routePath.value)
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
route,
|
|
24
|
+
routePath,
|
|
25
|
+
placementContext,
|
|
26
|
+
mergePlacementContext,
|
|
27
|
+
currentSurfaceId
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { useSurfaceRouteContext };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
3
|
+
import { toUiErrorMessage } from "./errorMessageHelpers.js";
|
|
4
|
+
|
|
5
|
+
function useUiFeedback({
|
|
6
|
+
initialType = "success",
|
|
7
|
+
source = "users-web.ui-feedback",
|
|
8
|
+
successChannel = "snackbar",
|
|
9
|
+
errorChannel = "banner",
|
|
10
|
+
dedupeWindowMs = 2000
|
|
11
|
+
} = {}) {
|
|
12
|
+
const message = ref("");
|
|
13
|
+
const messageType = ref(String(initialType || "success"));
|
|
14
|
+
const errorRuntime = useShellWebErrorRuntime();
|
|
15
|
+
const normalizedSource = String(source || "").trim() || "users-web.ui-feedback";
|
|
16
|
+
let lastErrorPresentation = null;
|
|
17
|
+
|
|
18
|
+
function rememberErrorPresentation(reportResult = null) {
|
|
19
|
+
const presentationId = String(reportResult?.presentationId || "").trim();
|
|
20
|
+
const presenterId = String(reportResult?.decision?.presenterId || "").trim();
|
|
21
|
+
if (!presentationId || !presenterId) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
lastErrorPresentation = Object.freeze({
|
|
26
|
+
presentationId,
|
|
27
|
+
presenterId
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dismissLastErrorPresentation() {
|
|
32
|
+
if (!lastErrorPresentation) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
errorRuntime.dismiss(lastErrorPresentation.presentationId, {
|
|
37
|
+
presenterId: lastErrorPresentation.presenterId
|
|
38
|
+
});
|
|
39
|
+
lastErrorPresentation = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clear() {
|
|
43
|
+
message.value = "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function success(nextMessage = "") {
|
|
47
|
+
messageType.value = "success";
|
|
48
|
+
const normalizedMessage = String(nextMessage || "").trim();
|
|
49
|
+
message.value = normalizedMessage;
|
|
50
|
+
dismissLastErrorPresentation();
|
|
51
|
+
if (!normalizedMessage) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
errorRuntime.report({
|
|
56
|
+
source: normalizedSource,
|
|
57
|
+
message: normalizedMessage,
|
|
58
|
+
severity: "success",
|
|
59
|
+
channel: successChannel,
|
|
60
|
+
dedupeKey: `${normalizedSource}:success:${normalizedMessage}`,
|
|
61
|
+
dedupeWindowMs
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function error(errorValue, fallbackMessage = "") {
|
|
66
|
+
messageType.value = "error";
|
|
67
|
+
message.value = toUiErrorMessage(errorValue, fallbackMessage, "Request failed.");
|
|
68
|
+
if (!message.value) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const reportResult = errorRuntime.report({
|
|
73
|
+
source: normalizedSource,
|
|
74
|
+
message: message.value,
|
|
75
|
+
cause: errorValue || null,
|
|
76
|
+
severity: "error",
|
|
77
|
+
channel: errorChannel,
|
|
78
|
+
dedupeKey: `${normalizedSource}:error:${message.value}`,
|
|
79
|
+
dedupeWindowMs
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!reportResult?.skipped) {
|
|
83
|
+
rememberErrorPresentation(reportResult);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
message,
|
|
89
|
+
messageType,
|
|
90
|
+
clear,
|
|
91
|
+
success,
|
|
92
|
+
error
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { useUiFeedback };
|