@jskit-ai/users-web 0.1.52 → 0.1.54

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 (73) hide show
  1. package/package.descriptor.mjs +14 -96
  2. package/package.json +16 -11
  3. package/src/client/account-settings/sections.js +74 -0
  4. package/src/client/composables/account-settings/accountSettingsRuntimeHelpers.js +2 -38
  5. package/src/client/composables/crud/crudLookupFieldRuntime.js +2 -2
  6. package/src/client/composables/internal/crudListParentTitleSupport.js +1 -1
  7. package/src/client/composables/internal/useOperationScope.js +12 -12
  8. package/src/client/composables/records/useAddEdit.js +2 -2
  9. package/src/client/composables/records/useList.js +3 -3
  10. package/src/client/composables/records/useView.js +2 -2
  11. package/src/client/composables/support/scopeHelpers.js +19 -19
  12. package/src/client/composables/useAccess.js +3 -3
  13. package/src/client/composables/useAccountSettingsRuntime.js +8 -156
  14. package/src/client/composables/useCommand.js +2 -2
  15. package/src/client/composables/useCrudListParentTitle.js +2 -2
  16. package/src/client/composables/usePaths.js +50 -38
  17. package/src/client/composables/useScopeRuntime.js +55 -27
  18. package/src/client/composables/useSurfaceRouteContext.js +1 -7
  19. package/src/client/index.js +0 -1
  20. package/src/client/lib/bootstrap.js +0 -63
  21. package/src/client/lib/httpClient.js +2 -59
  22. package/src/client/lib/theme.js +12 -189
  23. package/src/client/providers/UsersWebClientProvider.js +2 -25
  24. package/src/client/providers/bootUsersWebClientProvider.js +28 -0
  25. package/src/shared/toolsOutletContracts.js +1 -8
  26. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +33 -21
  27. package/test/accountSettingsSections.test.js +79 -0
  28. package/test/exportsContract.test.js +2 -2
  29. package/test/scopeHelpers.test.js +6 -6
  30. package/test/settingsPlacementContract.test.js +4 -49
  31. package/test/theme.test.js +0 -56
  32. package/src/client/components/ConsoleSettingsClientElement.vue +0 -24
  33. package/src/client/components/MembersAdminClientElement.vue +0 -400
  34. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +0 -39
  35. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +0 -36
  36. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +0 -90
  37. package/src/client/components/UsersWorkspaceSelector.vue +0 -248
  38. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +0 -39
  39. package/src/client/components/UsersWorkspaceToolsWidget.vue +0 -12
  40. package/src/client/components/WorkspaceMembersClientElement.vue +0 -655
  41. package/src/client/components/WorkspaceProfileClientElement.vue +0 -116
  42. package/src/client/components/WorkspaceSettingsClientElement.vue +0 -102
  43. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +0 -265
  44. package/src/client/components/WorkspacesClientElement.vue +0 -509
  45. package/src/client/composables/account-settings/accountSettingsInvitesRuntime.js +0 -88
  46. package/src/client/composables/useBootstrapQuery.js +0 -52
  47. package/src/client/composables/useWorkspaceRouteContext.js +0 -28
  48. package/src/client/composables/useWorkspaceSurfaceId.js +0 -43
  49. package/src/client/lib/menuIcons.js +0 -210
  50. package/src/client/lib/profileSurfaceMenuLinks.js +0 -142
  51. package/src/client/lib/surfaceAccessPolicy.js +0 -350
  52. package/src/client/lib/workspaceLinkResolver.js +0 -207
  53. package/src/client/lib/workspaceSurfaceContext.js +0 -82
  54. package/src/client/lib/workspaceSurfacePaths.js +0 -163
  55. package/src/client/providers/UsersWorkspacesClientProvider.js +0 -24
  56. package/src/client/runtime/bootstrapPlacementRouteGuards.js +0 -371
  57. package/src/client/runtime/bootstrapPlacementRuntime.js +0 -463
  58. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +0 -28
  59. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +0 -147
  60. package/src/client/support/menuLinkTarget.js +0 -93
  61. package/src/client/support/realtimeWorkspace.js +0 -21
  62. package/src/client/support/runtimeNormalization.js +0 -27
  63. package/src/client/support/workspaceQueryKeys.js +0 -15
  64. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +0 -77
  65. package/templates/src/pages/console/settings/index.vue +0 -8
  66. package/templates/src/pages/console/settings.vue +0 -32
  67. package/test/bootstrapPlacementRuntime.test.js +0 -1095
  68. package/test/menuIcons.test.js +0 -35
  69. package/test/menuLinkTarget.test.js +0 -116
  70. package/test/profileSurfaceMenuLinks.test.js +0 -207
  71. package/test/surfaceAccessPolicy.test.js +0 -129
  72. package/test/workspaceLinkResolver.test.js +0 -61
  73. package/test/workspaceSurfacePaths.test.js +0 -39
@@ -1,29 +1,25 @@
1
1
  import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
2
2
  import { useQueryClient } from "@tanstack/vue-query";
3
3
  import { useTheme } from "vuetify";
4
- import { useRoute, useRouter } from "vue-router";
4
+ import { useRoute } from "vue-router";
5
5
  import {
6
6
  useWebPlacementContext,
7
7
  resolveSurfaceNavigationTargetFromPlacementContext
8
8
  } from "@jskit-ai/shell-web/client/placement";
9
9
  import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
10
10
  import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
11
+ import { ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/kernel/shared/support/visibility";
11
12
  import { userProfileResource } from "@jskit-ai/users-core/shared/resources/userProfileResource";
12
13
  import { userSettingsResource } from "@jskit-ai/users-core/shared/resources/userSettingsResource";
13
- import { USERS_ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/users-core/shared/support/usersVisibility";
14
14
  import {
15
15
  persistThemePreference,
16
16
  resolveThemeNameForPreference,
17
17
  setVuetifyThemeName
18
18
  } from "../lib/theme.js";
19
- import {
20
- useWorkspaceSurfaceId
21
- } from "./useWorkspaceSurfaceId.js";
22
19
  import { useAddEdit } from "./records/useAddEdit.js";
23
20
  import { useCommand } from "./useCommand.js";
24
21
  import { useView } from "./records/useView.js";
25
- import { usePaths } from "./usePaths.js";
26
- import { resolveAccountSettingsPathFromPlacementContext } from "../lib/workspaceSurfacePaths.js";
22
+ import { resolveSurfacePathFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
27
23
  import {
28
24
  ACCOUNT_SETTINGS_DEFAULTS,
29
25
  AVATAR_DEFAULT_SIZE,
@@ -37,19 +33,15 @@ import {
37
33
  } from "./account-settings/accountSettingsRuntimeConstants.js";
38
34
  import {
39
35
  normalizeAvatarSize,
40
- normalizePendingInvite,
41
36
  normalizeReturnToPath,
42
37
  normalizeSettingsPayload,
43
38
  resolveAllowedReturnToOrigins
44
39
  } from "./account-settings/accountSettingsRuntimeHelpers.js";
45
40
  import { createAccountSettingsAvatarUploadRuntime } from "./account-settings/accountSettingsAvatarUploadRuntime.js";
46
- import { createAccountSettingsInvitesRuntime } from "./account-settings/accountSettingsInvitesRuntime.js";
47
41
 
48
42
  function useAccountSettingsRuntime() {
49
43
  const route = useRoute();
50
- const router = useRouter();
51
44
  const { context: placementContext } = useWebPlacementContext();
52
- const paths = usePaths();
53
45
  const queryClient = useQueryClient();
54
46
  const errorRuntime = useShellWebErrorRuntime();
55
47
  const vuetifyTheme = useTheme();
@@ -58,11 +50,12 @@ function useAccountSettingsRuntime() {
58
50
  const accountProfileWriteQueryKey = ["users-web", "settings", "account-profile-write"];
59
51
  const accountPreferencesWriteQueryKey = ["users-web", "settings", "account-preferences-write"];
60
52
  const accountNotificationsWriteQueryKey = ["users-web", "settings", "account-notifications-write"];
61
- const pendingInvitesQueryKey = ["users-web", "settings", "pending-invites"];
62
53
  const sessionQueryKey = Object.freeze(["users-web", "session", "csrf"]);
63
- const OWNERSHIP_PUBLIC = USERS_ROUTE_VISIBILITY_PUBLIC;
54
+ const OWNERSHIP_PUBLIC = ROUTE_VISIBILITY_PUBLIC;
64
55
 
65
- const accountSettingsPath = computed(() => resolveAccountSettingsPathFromPlacementContext(placementContext.value));
56
+ const accountSettingsPath = computed(() =>
57
+ resolveSurfacePathFromPlacementContext(placementContext.value, "account", "/")
58
+ );
66
59
  const allowedReturnToOrigins = computed(() => resolveAllowedReturnToOrigins(placementContext.value));
67
60
  const backTarget = computed(() =>
68
61
  normalizeReturnToPath(route?.query?.returnTo, {
@@ -107,20 +100,7 @@ function useAccountSettingsRuntime() {
107
100
  version: null
108
101
  });
109
102
 
110
- const pendingInvitesModel = reactive({
111
- pendingInvites: [],
112
- workspaceInvitesEnabled: false
113
- });
114
-
115
103
  const selectedAvatarFileName = ref("");
116
- const inviteAction = ref({
117
- token: "",
118
- decision: ""
119
- });
120
- const redeemInviteModel = reactive({
121
- token: "",
122
- decision: ""
123
- });
124
104
 
125
105
  const profileInitials = computed(() => {
126
106
  const source = String(profileForm.displayName || profileForm.email || "U").trim();
@@ -211,41 +191,6 @@ function useAccountSettingsRuntime() {
211
191
  mapLoadedToModel: mapAccountSettingsPayload
212
192
  });
213
193
 
214
- const pendingInvitesView = useView({
215
- ownershipFilter: OWNERSHIP_PUBLIC,
216
- apiSuffix: "/bootstrap",
217
- queryKeyFactory: () => pendingInvitesQueryKey,
218
- realtime: {
219
- event: "workspace.invitations.pending.changed"
220
- },
221
- fallbackLoadError: "Unable to load invitations.",
222
- model: pendingInvitesModel,
223
- mapLoadedToModel: (model, payload = {}) => {
224
- model.workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
225
- model.pendingInvites = model.workspaceInvitesEnabled
226
- ? (Array.isArray(payload?.pendingInvites) ? payload.pendingInvites : [])
227
- .map(normalizePendingInvite)
228
- .filter(Boolean)
229
- : [];
230
- }
231
- });
232
-
233
- const redeemInviteCommand = useCommand({
234
- ownershipFilter: OWNERSHIP_PUBLIC,
235
- apiSuffix: "/workspace/invitations/redeem",
236
- writeMethod: "POST",
237
- fallbackRunError: "Unable to respond to invitation.",
238
- suppressSuccessMessage: true,
239
- model: redeemInviteModel,
240
- buildRawPayload: (model) => ({
241
- token: String(model.token || "").trim(),
242
- decision: String(model.decision || "").trim().toLowerCase()
243
- }),
244
- messages: {
245
- error: "Unable to respond to invitation."
246
- }
247
- });
248
-
249
194
  const profileAddEdit = useAddEdit({
250
195
  ownershipFilter: OWNERSHIP_PUBLIC,
251
196
  resource: userProfileResource,
@@ -349,91 +294,10 @@ function useAccountSettingsRuntime() {
349
294
 
350
295
  const loadingSettings = computed(() => Boolean(settingsView.isLoading));
351
296
  const refreshingSettings = computed(() => Boolean(settingsView.isRefetching));
352
- const invitesAvailable = computed(() => pendingInvitesModel.workspaceInvitesEnabled === true);
353
- const loadingInvites = computed(() => Boolean(pendingInvitesView.isLoading));
354
- const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching));
355
- const pendingInvites = computed(() => {
356
- return Array.isArray(pendingInvitesModel.pendingInvites) ? pendingInvitesModel.pendingInvites : [];
357
- });
358
- const isResolvingInvite = computed(() => Boolean(redeemInviteCommand.isRunning.value));
359
-
360
- const { workspaceSurfaceId } = useWorkspaceSurfaceId({
361
- route,
362
- placementContext
363
- });
364
-
365
- function workspaceHomePath(workspaceSlug) {
366
- const normalizedSlug = String(workspaceSlug || "").trim();
367
- if (!normalizedSlug || !workspaceSurfaceId.value) {
368
- return "";
369
- }
370
-
371
- return paths.page("/", {
372
- surface: workspaceSurfaceId.value,
373
- workspaceSlug: normalizedSlug,
374
- mode: "workspace"
375
- });
376
- }
377
-
378
297
  async function submitProfile() {
379
298
  await profileAddEdit.submit();
380
299
  }
381
300
 
382
- async function openWorkspace(workspaceSlug) {
383
- const targetPath = workspaceHomePath(workspaceSlug);
384
- if (!targetPath) {
385
- reportAccountFeedback({
386
- message: "Workspace surface is not configured.",
387
- severity: "error",
388
- channel: "banner",
389
- dedupeKey: "users-web.account-settings-runtime:workspace-surface-missing"
390
- });
391
- return;
392
- }
393
-
394
- try {
395
- const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
396
- path: targetPath,
397
- surfaceId: workspaceSurfaceId.value
398
- });
399
- if (navigationTarget.sameOrigin) {
400
- await router.push(navigationTarget.href);
401
- } else if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
402
- window.location.assign(navigationTarget.href);
403
- } else {
404
- throw new Error("Cross-origin navigation is unavailable in this environment.");
405
- }
406
- } catch (error) {
407
- reportAccountFeedback({
408
- message: String(error?.message || "Unable to open workspace."),
409
- severity: "error",
410
- channel: "banner",
411
- dedupeKey: `users-web.account-settings-runtime:open-workspace:${String(workspaceSlug || "").trim()}`
412
- });
413
- }
414
- }
415
-
416
- const invitesRuntime = createAccountSettingsInvitesRuntime({
417
- invitesAvailable,
418
- isResolvingInvite,
419
- inviteAction,
420
- redeemInviteModel,
421
- redeemInviteCommand,
422
- pendingInvites,
423
- pendingInvitesModel,
424
- pendingInvitesView,
425
- openWorkspace,
426
- reportAccountFeedback
427
- });
428
-
429
- function acceptInvite(invite) {
430
- return invitesRuntime.accept(invite);
431
- }
432
-
433
- function refuseInvite(invite) {
434
- return invitesRuntime.refuse(invite);
435
- }
436
-
437
301
  function openAvatarEditor() {
438
302
  avatarUploadRuntime.openEditor();
439
303
  }
@@ -508,17 +372,6 @@ function useAccountSettingsRuntime() {
508
372
  submit: submitNotifications
509
373
  });
510
374
 
511
- const invites = Object.freeze({
512
- isAvailable: invitesAvailable,
513
- items: pendingInvites,
514
- isLoading: loadingInvites,
515
- isRefetching: refreshingInvites,
516
- isResolving: isResolvingInvite,
517
- action: inviteAction,
518
- accept: acceptInvite,
519
- refuse: refuseInvite
520
- });
521
-
522
375
  return Object.freeze({
523
376
  backTarget,
524
377
  backNavigationTarget,
@@ -526,8 +379,7 @@ function useAccountSettingsRuntime() {
526
379
  refreshingSettings,
527
380
  profile,
528
381
  preferences,
529
- notifications,
530
- invites
382
+ notifications
531
383
  });
532
384
  }
533
385
 
@@ -1,5 +1,5 @@
1
1
  import { proxyRefs } from "vue";
2
- import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
2
+ import { ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/kernel/shared/support/visibility";
3
3
  import { useCommandCore } from "./runtime/useCommandCore.js";
4
4
  import { useEndpointResource } from "./runtime/useEndpointResource.js";
5
5
  import { useOperationScope } from "./internal/useOperationScope.js";
@@ -11,7 +11,7 @@ import {
11
11
  } from "./runtime/operationUiHelpers.js";
12
12
 
13
13
  function useCommand({
14
- ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
14
+ ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
15
15
  surfaceId = "",
16
16
  access = "auto",
17
17
  apiSuffix = "",
@@ -79,12 +79,12 @@ function useCrudListParentTitle({
79
79
  readEnabled: shouldLoadParentRecord,
80
80
  recordIdParam: normalizeText(initialParentDescriptor.routeParamKey) || "recordId",
81
81
  includeRecordIdInQueryKey: true,
82
- queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
82
+ queryKeyFactory: (surfaceId = "", scopeParamValue = "") => [
83
83
  ...normalizedQueryKeyPrefix,
84
84
  normalizeText(initialParentDescriptor.relationNamespace),
85
85
  normalizeText(initialParentDescriptor.routeParamKey),
86
86
  String(surfaceId || ""),
87
- String(workspaceSlug || "")
87
+ String(scopeParamValue || "")
88
88
  ],
89
89
  placementSource,
90
90
  fallbackLoadError,
@@ -1,9 +1,9 @@
1
1
  import { computed, unref } from "vue";
2
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";
3
+ import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
4
+ import { resolveSurfaceDefinitionFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
5
+ import { useShellLinkResolver } from "@jskit-ai/shell-web/client/navigation/linkResolver";
6
+ import { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
7
7
 
8
8
  function normalizePathSuffix(value = "") {
9
9
  const raw = normalizeText(unref(value));
@@ -28,23 +28,43 @@ function resolveSurfaceId(value, fallback = "") {
28
28
  return "";
29
29
  }
30
30
 
31
- function resolveWorkspaceSlug(value, fallback = "") {
32
- const normalized = normalizeText(unref(value));
33
- if (normalized) {
34
- return normalized;
31
+ function resolveDefaultSurfaceIdFromPlacementContext(placementContext = null) {
32
+ return resolveSurfaceId(placementContext?.surfaceConfig?.defaultSurfaceId, "");
33
+ }
34
+
35
+ function normalizeRouteParams(params = null) {
36
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
37
+ return {};
35
38
  }
36
39
 
37
- return normalizeText(unref(fallback));
40
+ const output = {};
41
+ for (const [rawKey, rawValue] of Object.entries(params)) {
42
+ const key = normalizeText(rawKey);
43
+ if (!key) {
44
+ continue;
45
+ }
46
+
47
+ const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
48
+ const normalizedValue = normalizeText(value);
49
+ if (!normalizedValue) {
50
+ continue;
51
+ }
52
+ output[key] = normalizedValue;
53
+ }
54
+ return output;
38
55
  }
39
56
 
40
- function resolveDefaultSurfaceIdFromPlacementContext(placementContext = null) {
41
- return resolveSurfaceId(placementContext?.surfaceConfig?.defaultSurfaceId, "");
57
+ function resolveRouteParams(baseParams = {}, overrideParams = null) {
58
+ return {
59
+ ...normalizeRouteParams(baseParams),
60
+ ...normalizeRouteParams(overrideParams)
61
+ };
42
62
  }
43
63
 
44
64
  function usePaths({ routeContext: sourceRouteContext = null } = {}) {
45
- const routeContext = sourceRouteContext || useWorkspaceRouteContext();
46
- const workspaceLinkResolver = useWorkspaceLinkResolver();
47
- const workspaceSlug = computed(() => String(routeContext.workspaceSlugFromRoute.value || "").trim());
65
+ const routeContext = sourceRouteContext || useSurfaceRouteContext();
66
+ const shellLinkResolver = useShellLinkResolver();
67
+ const routeParams = computed(() => normalizeRouteParams(routeContext.route?.params));
48
68
 
49
69
  function page(relativePath = "/", options = {}) {
50
70
  const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
@@ -54,13 +74,12 @@ function usePaths({ routeContext: sourceRouteContext = null } = {}) {
54
74
  if (!surface) {
55
75
  return "";
56
76
  }
57
- const nextWorkspaceSlug = resolveWorkspaceSlug(source.workspaceSlug, workspaceSlug.value);
58
- const mode = normalizeText(source.mode).toLowerCase() || "auto";
59
-
60
- return workspaceLinkResolver.resolve(relativePath, {
77
+ return shellLinkResolver.resolve(relativePath, {
61
78
  surface,
62
- workspaceSlug: nextWorkspaceSlug,
63
- mode
79
+ explicitTo: source.explicitTo,
80
+ surfaceRelativePath: source.surfaceRelativePath,
81
+ params: resolveRouteParams(routeParams.value, source.params),
82
+ strictParams: source.strictParams !== false
64
83
  });
65
84
  }
66
85
 
@@ -70,36 +89,29 @@ function usePaths({ routeContext: sourceRouteContext = null } = {}) {
70
89
  resolveSurfaceId(source.surface, routeContext.currentSurfaceId.value) ||
71
90
  resolveDefaultSurfaceIdFromPlacementContext(routeContext.placementContext.value);
72
91
  const suffix = normalizePathSuffix(relativePath);
73
- const workspaceScoped = surfaceRequiresWorkspaceFromPlacementContext(routeContext.placementContext.value, surface);
74
92
 
75
93
  if (!suffix) {
76
94
  throw new TypeError("usePaths().api(relativePath) requires a non-empty relativePath.");
77
95
  }
78
96
 
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
- }
97
+ const routeBase = resolveSurfaceDefinitionFromPlacementContext(
98
+ routeContext.placementContext.value,
99
+ surface
100
+ )?.routeBase || "/";
94
101
 
95
- return templatePath;
102
+ return resolveScopedApiBasePath({
103
+ routeBase,
104
+ relativePath: suffix,
105
+ params: resolveRouteParams(routeParams.value, source.params),
106
+ strictParams: source.strictParams !== false
107
+ });
96
108
  }
97
109
 
98
110
  return Object.freeze({
99
111
  route: routeContext.route,
100
112
  placementContext: routeContext.placementContext,
101
113
  currentSurfaceId: routeContext.currentSurfaceId,
102
- workspaceSlug,
114
+ routeParams,
103
115
  page,
104
116
  api
105
117
  });
@@ -1,10 +1,12 @@
1
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";
2
+ import { normalizeSurfaceId, resolveScopedRouteBase } from "@jskit-ai/kernel/shared/surface";
3
+ import {
4
+ ROUTE_VISIBILITY_WORKSPACE
5
+ } from "@jskit-ai/kernel/shared/support/visibility";
4
6
  import { useAccess } from "./useAccess.js";
5
- import { useWorkspaceRouteContext } from "./useWorkspaceRouteContext.js";
7
+ import { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
6
8
  import { usePaths } from "./usePaths.js";
7
- import { surfaceRequiresWorkspaceFromPlacementContext } from "../lib/workspaceSurfaceContext.js";
9
+ import { resolveSurfaceDefinitionFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
8
10
  import {
9
11
  asPlainObject,
10
12
  ensureAccessModeCompatibility,
@@ -12,9 +14,15 @@ import {
12
14
  normalizeOwnershipFilter,
13
15
  resolveApiSuffix
14
16
  } from "./support/scopeHelpers.js";
17
+ import { extractRouteParamNames, toRouteParamValue } from "./support/routeTemplateHelpers.js";
18
+
19
+ function resolveScopedRouteParamNames(placementContext = null, surfaceId = "") {
20
+ const routeBase = resolveSurfaceDefinitionFromPlacementContext(placementContext, surfaceId)?.routeBase || "/";
21
+ return extractRouteParamNames(resolveScopedRouteBase(routeBase));
22
+ }
15
23
 
16
24
  function useScopeRuntime({
17
- ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
25
+ ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
18
26
  surfaceId = "",
19
27
  accessMode = "auto",
20
28
  hasPermissionRequirements = false,
@@ -29,12 +37,11 @@ function useScopeRuntime({
29
37
  const accessRequired = resolveAccessModeEnabled(normalizedAccessMode, {
30
38
  hasPermissionRequirements
31
39
  });
32
- const routeContext = useWorkspaceRouteContext();
40
+ const routeContext = useSurfaceRouteContext();
33
41
  const paths = usePaths({
34
42
  routeContext
35
43
  });
36
44
 
37
- const workspaceSlugFromRoute = routeContext.workspaceSlugFromRoute;
38
45
  const resolvedSurfaceId = computed(() => {
39
46
  const explicitSurfaceId = normalizeSurfaceId(surfaceId);
40
47
  if (explicitSurfaceId) {
@@ -43,64 +50,85 @@ function useScopeRuntime({
43
50
 
44
51
  return normalizeSurfaceId(routeContext.currentSurfaceId.value);
45
52
  });
46
- const workspaceScoped = computed(() =>
47
- surfaceRequiresWorkspaceFromPlacementContext(routeContext.placementContext.value, resolvedSurfaceId.value)
53
+ const scopedRouteParamNames = computed(() =>
54
+ resolveScopedRouteParamNames(routeContext.placementContext.value, resolvedSurfaceId.value)
48
55
  );
49
- const hasRouteWorkspaceSlug = computed(() => (workspaceScoped.value ? Boolean(workspaceSlugFromRoute.value) : true));
50
- const workspaceRouteError = computed(() => {
51
- if (!workspaceScoped.value || hasRouteWorkspaceSlug.value) {
56
+ const routeScopeParams = computed(() => {
57
+ const source = paths.routeParams.value;
58
+ const next = {};
59
+ for (const paramName of scopedRouteParamNames.value) {
60
+ const paramValue = toRouteParamValue(source[paramName]);
61
+ if (paramValue) {
62
+ next[paramName] = paramValue;
63
+ }
64
+ }
65
+ return Object.freeze(next);
66
+ });
67
+ const missingScopedRouteParamNames = computed(() =>
68
+ scopedRouteParamNames.value.filter((paramName) => !routeScopeParams.value[paramName])
69
+ );
70
+ const requiresScopedRouteParams = computed(() => scopedRouteParamNames.value.length > 0);
71
+ const hasRequiredRouteScope = computed(() => missingScopedRouteParamNames.value.length < 1);
72
+ const scopeParamValue = computed(() => {
73
+ const [primaryScopeParamName = ""] = scopedRouteParamNames.value;
74
+ return primaryScopeParamName ? routeScopeParams.value[primaryScopeParamName] || "" : "";
75
+ });
76
+ const routeScopeError = computed(() => {
77
+ if (!requiresScopedRouteParams.value || hasRequiredRouteScope.value) {
52
78
  return "";
53
79
  }
54
80
 
55
- return `Route parameter workspaceSlug is required for surface "${resolvedSurfaceId.value || "<unknown>"}".`;
81
+ const missingParams = missingScopedRouteParamNames.value.join(", ");
82
+ return `Route parameters ${missingParams} are required for surface "${resolvedSurfaceId.value || "<unknown>"}".`;
56
83
  });
57
84
 
58
85
  const accessRuntime = useAccess({
59
- workspaceSlug: computed(() => (workspaceScoped.value ? workspaceSlugFromRoute.value : "")),
60
- enabled: computed(() => accessRequired && hasRouteWorkspaceSlug.value),
86
+ scopeParamValue,
87
+ enabled: computed(() => accessRequired && hasRequiredRouteScope.value),
61
88
  access: normalizedAccessMode,
62
89
  hasPermissionRequirements,
63
- mergePlacementContext: accessRequired ? routeContext.mergePlacementContext : null,
64
90
  placementSource: String(placementSource || "users-web.scope-runtime")
65
91
  });
66
92
 
67
93
  function resolveApiPath(apiSuffix = "", context = {}) {
68
- if (workspaceRouteError.value) {
94
+ if (routeScopeError.value) {
69
95
  return "";
70
96
  }
71
97
 
72
98
  const suffix = resolveApiSuffix(apiSuffix, {
73
99
  surfaceId: routeContext.currentSurfaceId.value,
74
- workspaceSlug: workspaceSlugFromRoute.value,
100
+ scopeParamValue: scopeParamValue.value,
75
101
  ownershipFilter: normalizedOwnershipFilter,
76
102
  ...asPlainObject(context)
77
103
  });
78
104
 
79
105
  return paths.api(suffix, {
80
106
  surface: resolvedSurfaceId.value,
81
- workspaceSlug: workspaceSlugFromRoute.value
107
+ params: routeScopeParams.value
82
108
  });
83
109
  }
84
110
 
85
- function requireWorkspaceRouteParam(caller = "useScopeRuntime") {
86
- if (workspaceRouteError.value) {
87
- throw new Error(`${caller}: ${workspaceRouteError.value}`);
111
+ function requireRouteScope(caller = "useScopeRuntime") {
112
+ if (routeScopeError.value) {
113
+ throw new Error(`${caller}: ${routeScopeError.value}`);
88
114
  }
89
115
  }
90
116
 
91
117
  return Object.freeze({
92
118
  normalizedOwnershipFilter,
93
- workspaceScoped: workspaceScoped.value,
119
+ requiresScopedRouteParams: requiresScopedRouteParams.value,
94
120
  resolvedSurfaceId,
95
121
  accessMode: normalizedAccessMode,
96
122
  accessRequired,
97
123
  routeContext,
98
- workspaceSlugFromRoute,
99
- hasRouteWorkspaceSlug,
100
- workspaceRouteError,
124
+ scopedRouteParamNames,
125
+ routeScopeParams,
126
+ scopeParamValue,
127
+ hasRequiredRouteScope,
128
+ routeScopeError,
101
129
  access: accessRuntime,
102
130
  resolveApiPath,
103
- requireWorkspaceRouteParam
131
+ requireRouteScope
104
132
  });
105
133
  }
106
134
 
@@ -5,19 +5,13 @@ import {
5
5
  resolveSurfaceIdFromPlacementPathname,
6
6
  useWebPlacementContext
7
7
  } from "@jskit-ai/shell-web/client/placement";
8
- import { resolveWorkspaceSurfaceIdFromPlacementPathname } from "../lib/workspaceSurfacePaths.js";
9
8
 
10
9
  function useSurfaceRouteContext() {
11
10
  const route = useRoute();
12
11
  const { context: placementContext, mergeContext: mergePlacementContext } = useWebPlacementContext();
13
12
  const routePath = computed(() => resolveRuntimePathname(route?.path));
14
13
 
15
- const currentSurfaceId = computed(() => {
16
- return (
17
- resolveWorkspaceSurfaceIdFromPlacementPathname(placementContext.value, routePath.value) ||
18
- resolveSurfaceIdFromPlacementPathname(placementContext.value, routePath.value)
19
- );
20
- });
14
+ const currentSurfaceId = computed(() => resolveSurfaceIdFromPlacementPathname(placementContext.value, routePath.value));
21
15
 
22
16
  return Object.freeze({
23
17
  route,
@@ -1,7 +1,6 @@
1
1
  import { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
2
2
 
3
3
  export { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
4
- export { UsersWorkspacesClientProvider } from "./providers/UsersWorkspacesClientProvider.js";
5
4
 
6
5
  const clientProviders = Object.freeze([UsersWebClientProvider]);
7
6
 
@@ -1,64 +1,5 @@
1
1
  import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
2
 
3
- function buildBootstrapApiPath(workspaceSlug = "") {
4
- const normalizedWorkspaceSlug = String(workspaceSlug || "").trim();
5
- if (!normalizedWorkspaceSlug) {
6
- return "/api/bootstrap";
7
- }
8
-
9
- const query = new URLSearchParams({
10
- workspaceSlug: normalizedWorkspaceSlug
11
- });
12
- return `/api/bootstrap?${query.toString()}`;
13
- }
14
-
15
- function normalizeWorkspaceEntry(entry) {
16
- if (!entry || typeof entry !== "object") {
17
- return null;
18
- }
19
-
20
- const id = normalizeRecordId(entry.id, { fallback: null });
21
- const slug = String(entry.slug || "").trim();
22
- if (!id || !slug) {
23
- return null;
24
- }
25
-
26
- return Object.freeze({
27
- id,
28
- slug,
29
- name: String(entry.name || slug).trim() || slug,
30
- color: String(entry.color || "").trim(),
31
- secondaryColor: String(entry.secondaryColor || "").trim(),
32
- surfaceColor: String(entry.surfaceColor || "").trim(),
33
- surfaceVariantColor: String(entry.surfaceVariantColor || "").trim(),
34
- avatarUrl: String(entry.avatarUrl || "").trim(),
35
- roleSid: String(entry.roleSid || "member").trim().toLowerCase() || "member",
36
- isAccessible: entry.isAccessible !== false
37
- });
38
- }
39
-
40
- function normalizeWorkspaceList(list) {
41
- const source = Array.isArray(list) ? list : [];
42
- return source.map(normalizeWorkspaceEntry).filter(Boolean);
43
- }
44
-
45
- function findWorkspaceBySlug(list, workspaceSlug) {
46
- const normalizedWorkspaceSlug = String(workspaceSlug || "").trim();
47
- if (!normalizedWorkspaceSlug) {
48
- return null;
49
- }
50
-
51
- const source = Array.isArray(list) ? list : [];
52
- for (const entry of source) {
53
- const normalizedEntry = normalizeWorkspaceEntry(entry);
54
- if (normalizedEntry && normalizedEntry.slug === normalizedWorkspaceSlug) {
55
- return normalizedEntry;
56
- }
57
- }
58
-
59
- return null;
60
- }
61
-
62
3
  function resolvePlacementUserFromBootstrapPayload(payload = {}, currentUser = null) {
63
4
  const source = payload && typeof payload === "object" ? payload : {};
64
5
  const session = source.session && typeof source.session === "object" ? source.session : {};
@@ -92,9 +33,5 @@ function resolvePlacementUserFromBootstrapPayload(payload = {}, currentUser = nu
92
33
  }
93
34
 
94
35
  export {
95
- buildBootstrapApiPath,
96
- normalizeWorkspaceEntry,
97
- normalizeWorkspaceList,
98
- findWorkspaceBySlug,
99
36
  resolvePlacementUserFromBootstrapPayload
100
37
  };