@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.
Files changed (97) hide show
  1. package/package.descriptor.mjs +507 -0
  2. package/package.json +31 -0
  3. package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
  4. package/src/client/components/MembersAdminClientElement.vue +404 -0
  5. package/src/client/components/ProfileClientElement.vue +242 -0
  6. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
  7. package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
  8. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
  9. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
  10. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
  11. package/src/client/components/UsersWorkspaceSelector.vue +237 -0
  12. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
  13. package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
  14. package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
  15. package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
  16. package/src/client/components/WorkspacesClientElement.vue +514 -0
  17. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
  18. package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
  19. package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
  20. package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
  21. package/src/client/composables/errorMessageHelpers.js +66 -0
  22. package/src/client/composables/internal/useOperationScope.js +144 -0
  23. package/src/client/composables/modelStateHelpers.js +49 -0
  24. package/src/client/composables/operationUiHelpers.js +121 -0
  25. package/src/client/composables/operationValidationHelpers.js +52 -0
  26. package/src/client/composables/refValueHelpers.js +19 -0
  27. package/src/client/composables/scopeHelpers.js +145 -0
  28. package/src/client/composables/useAccess.js +109 -0
  29. package/src/client/composables/useAccountSettingsRuntime.js +533 -0
  30. package/src/client/composables/useAddEdit.js +135 -0
  31. package/src/client/composables/useAddEditCore.js +137 -0
  32. package/src/client/composables/useBootstrapQuery.js +52 -0
  33. package/src/client/composables/useCommand.js +112 -0
  34. package/src/client/composables/useCommandCore.js +130 -0
  35. package/src/client/composables/useEndpointResource.js +104 -0
  36. package/src/client/composables/useFieldErrorBag.js +61 -0
  37. package/src/client/composables/useList.js +85 -0
  38. package/src/client/composables/useListCore.js +65 -0
  39. package/src/client/composables/usePagedCollection.js +125 -0
  40. package/src/client/composables/usePaths.js +108 -0
  41. package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
  42. package/src/client/composables/useScopeRuntime.js +107 -0
  43. package/src/client/composables/useSurfaceRouteContext.js +31 -0
  44. package/src/client/composables/useUiFeedback.js +96 -0
  45. package/src/client/composables/useView.js +89 -0
  46. package/src/client/composables/useViewCore.js +104 -0
  47. package/src/client/composables/useWorkspaceRouteContext.js +28 -0
  48. package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
  49. package/src/client/index.js +7 -0
  50. package/src/client/lib/bootstrap.js +95 -0
  51. package/src/client/lib/httpClient.js +67 -0
  52. package/src/client/lib/menuIcons.js +192 -0
  53. package/src/client/lib/permissions.js +34 -0
  54. package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
  55. package/src/client/lib/surfaceAccessPolicy.js +350 -0
  56. package/src/client/lib/theme.js +99 -0
  57. package/src/client/lib/workspaceLinkResolver.js +207 -0
  58. package/src/client/lib/workspaceSurfaceContext.js +82 -0
  59. package/src/client/lib/workspaceSurfacePaths.js +163 -0
  60. package/src/client/providers/UsersWebClientProvider.js +85 -0
  61. package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
  62. package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
  63. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
  64. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
  65. package/src/client/support/contractGuards.js +34 -0
  66. package/src/client/support/realtimeWorkspace.js +12 -0
  67. package/src/client/support/runtimeNormalization.js +27 -0
  68. package/src/client/support/workspaceQueryKeys.js +15 -0
  69. package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
  70. package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
  71. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
  72. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
  73. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
  74. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
  75. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
  76. package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
  77. package/templates/src/pages/account/index.vue +17 -0
  78. package/templates/src/pages/admin/members/index.vue +7 -0
  79. package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
  80. package/templates/src/pages/console/settings/index.vue +16 -0
  81. package/templates/src/surfaces/admin/index.vue +29 -0
  82. package/templates/src/surfaces/admin/root.vue +20 -0
  83. package/templates/src/surfaces/app/index.vue +27 -0
  84. package/templates/src/surfaces/app/root.vue +20 -0
  85. package/test/bootstrap.test.js +38 -0
  86. package/test/bootstrapPlacementRuntime.test.js +991 -0
  87. package/test/errorMessageHelpers.test.js +28 -0
  88. package/test/exportsContract.test.js +39 -0
  89. package/test/menuIcons.test.js +33 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/profileSurfaceMenuLinks.test.js +207 -0
  92. package/test/refValueHelpers.test.js +14 -0
  93. package/test/scopeHelpers.test.js +57 -0
  94. package/test/surfaceAccessPolicy.test.js +129 -0
  95. package/test/theme.test.js +95 -0
  96. package/test/workspaceLinkResolver.test.js +61 -0
  97. 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 };