@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,663 @@
1
+ <template>
2
+ <section class="workspace-members-page">
3
+ <p v-if="loadError" class="text-body-2 text-medium-emphasis mb-4">
4
+ {{ loadError }}
5
+ </p>
6
+
7
+ <MembersAdminClientElement
8
+ v-else
9
+ :forms="forms"
10
+ :options="options"
11
+ :collections="collections"
12
+ :permissions="permissionState"
13
+ :revokeInviteId="revokeInviteId"
14
+ :removeMemberUserId="removeMemberUserId"
15
+ :status="status"
16
+ :actions="actions"
17
+ />
18
+ </section>
19
+ </template>
20
+
21
+ <script setup>
22
+ import { computed, reactive, ref, watch } from "vue";
23
+ import MembersAdminClientElement from "./MembersAdminClientElement.vue";
24
+ import { useCommand } from "../composables/useCommand.js";
25
+ import { useList } from "../composables/useList.js";
26
+ import { useView } from "../composables/useView.js";
27
+ import { usePaths } from "../composables/usePaths.js";
28
+ import { useAccess } from "../composables/useAccess.js";
29
+ import { useUiFeedback } from "../composables/useUiFeedback.js";
30
+ import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
31
+ import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
32
+ import { matchesCurrentWorkspaceEvent } from "../support/realtimeWorkspace.js";
33
+ import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
34
+ import {
35
+ WORKSPACE_SETTINGS_CHANGED_EVENT,
36
+ WORKSPACE_MEMBERS_CHANGED_EVENT,
37
+ WORKSPACE_INVITES_CHANGED_EVENT
38
+ } from "@jskit-ai/users-core/shared/events/usersEvents";
39
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
40
+
41
+ const forms = reactive({
42
+ invite: {
43
+ email: "",
44
+ roleId: "member"
45
+ },
46
+ workspace: {
47
+ invitesEnabled: false,
48
+ invitesAvailable: false
49
+ }
50
+ });
51
+
52
+ const options = reactive({
53
+ inviteRoleOptions: [],
54
+ memberRoleOptions: [],
55
+ formatDateTime(value) {
56
+ const parsedDate = new Date(value);
57
+ if (Number.isNaN(parsedDate.getTime())) {
58
+ return "unknown";
59
+ }
60
+ return parsedDate.toLocaleString();
61
+ }
62
+ });
63
+
64
+ const collections = reactive({
65
+ members: [],
66
+ invites: []
67
+ });
68
+
69
+ const inviteFeedback = useUiFeedback();
70
+ const membersFeedback = useUiFeedback();
71
+ const teamFeedback = useUiFeedback();
72
+ const revokeInviteId = ref(0);
73
+ const removeMemberUserId = ref(0);
74
+
75
+ const { route, currentSurfaceId, workspaceSlugFromRoute, mergePlacementContext } =
76
+ useWorkspaceRouteContext();
77
+ const usersPaths = usePaths();
78
+ const errorRuntime = useShellWebErrorRuntime();
79
+ const OWNERSHIP_WORKSPACE = USERS_ROUTE_VISIBILITY_WORKSPACE;
80
+
81
+ const hasRouteWorkspaceSlug = computed(() => Boolean(workspaceSlugFromRoute.value));
82
+ const workspaceMembersApiPath = computed(() =>
83
+ usersPaths.api("/members", {
84
+ workspaceSlug: workspaceSlugFromRoute.value
85
+ })
86
+ );
87
+ const workspaceInvitesApiPath = computed(() =>
88
+ usersPaths.api("/invites", {
89
+ workspaceSlug: workspaceSlugFromRoute.value
90
+ })
91
+ );
92
+
93
+ function workspaceMembersPath(memberId) {
94
+ return `${workspaceMembersApiPath.value}/${Number(memberId || 0)}`;
95
+ }
96
+
97
+ function workspaceInvitePath(inviteId) {
98
+ const encodedInviteId = encodeURIComponent(String(inviteId || ""));
99
+ return `${workspaceInvitesApiPath.value}/${encodedInviteId}`;
100
+ }
101
+
102
+ const access = useAccess({
103
+ workspaceSlug: workspaceSlugFromRoute,
104
+ enabled: hasRouteWorkspaceSlug,
105
+ mergePlacementContext,
106
+ placementSource: "users-web.workspace-members-view"
107
+ });
108
+
109
+ function isCurrentWorkspaceRealtimeEvent({ payload = {} } = {}) {
110
+ return matchesCurrentWorkspaceEvent(payload, workspaceSlugFromRoute.value);
111
+ }
112
+
113
+ const canViewMembers = computed(() => {
114
+ return access.canAny(["workspace.members.view", "workspace.members.manage"]);
115
+ });
116
+
117
+ const canInviteMembers = computed(() => {
118
+ return access.can("workspace.members.invite");
119
+ });
120
+
121
+ const canManageMembers = computed(() => {
122
+ return access.can("workspace.members.manage");
123
+ });
124
+
125
+ const canRevokeInvites = computed(() => {
126
+ return access.can("workspace.invites.revoke");
127
+ });
128
+
129
+ const permissionState = computed(() => {
130
+ return {
131
+ canViewMembers: canViewMembers.value,
132
+ canInviteMembers: canInviteMembers.value,
133
+ canManageMembers: canManageMembers.value,
134
+ canRevokeInvites: canRevokeInvites.value
135
+ };
136
+ });
137
+
138
+ function resetMessages() {
139
+ inviteFeedback.clear();
140
+ membersFeedback.clear();
141
+ teamFeedback.clear();
142
+ }
143
+
144
+ function clearRoleOptions() {
145
+ options.inviteRoleOptions = [];
146
+ options.memberRoleOptions = [];
147
+ }
148
+
149
+ function resetViewState() {
150
+ resetMessages();
151
+ forms.invite.email = "";
152
+ forms.invite.roleId = "member";
153
+ forms.workspace.invitesEnabled = false;
154
+ forms.workspace.invitesAvailable = false;
155
+ collections.members = [];
156
+ collections.invites = [];
157
+ clearRoleOptions();
158
+ revokeInviteId.value = 0;
159
+ removeMemberUserId.value = 0;
160
+ }
161
+
162
+ function toRoleTitle(roleId) {
163
+ const normalizedRoleId = String(roleId || "").trim();
164
+ if (!normalizedRoleId) {
165
+ return "";
166
+ }
167
+ return normalizedRoleId.charAt(0).toUpperCase() + normalizedRoleId.slice(1);
168
+ }
169
+
170
+ function normalizeRoleCatalog(payload = {}) {
171
+ const source =
172
+ payload?.roleCatalog && typeof payload.roleCatalog === "object"
173
+ ? payload.roleCatalog
174
+ : payload && typeof payload === "object"
175
+ ? payload
176
+ : {};
177
+
178
+ const roles = Array.isArray(source.roles) ? source.roles : [];
179
+ const assignableRoleIdsFromCatalog = Array.isArray(source.assignableRoleIds)
180
+ ? source.assignableRoleIds
181
+ : [];
182
+
183
+ let assignableRoleIds = assignableRoleIdsFromCatalog
184
+ .map((entry) => String(entry || "").trim().toLowerCase())
185
+ .filter(Boolean);
186
+
187
+ if (assignableRoleIds.length < 1) {
188
+ assignableRoleIds = roles
189
+ .filter((entry) => entry?.assignable === true)
190
+ .map((entry) => String(entry?.id || "").trim().toLowerCase())
191
+ .filter(Boolean);
192
+ }
193
+
194
+ const uniqueRoleIds = Array.from(new Set(assignableRoleIds));
195
+ const roleOptions = uniqueRoleIds.map((roleId) => ({
196
+ title: toRoleTitle(roleId),
197
+ value: roleId
198
+ }));
199
+
200
+ const defaultInviteRole = String(source.defaultInviteRole || "")
201
+ .trim()
202
+ .toLowerCase();
203
+
204
+ return {
205
+ roleOptions,
206
+ defaultInviteRole
207
+ };
208
+ }
209
+
210
+ function applyRoleCatalog(payload = {}) {
211
+ const normalizedCatalog = normalizeRoleCatalog(payload);
212
+ options.inviteRoleOptions = [...normalizedCatalog.roleOptions];
213
+ options.memberRoleOptions = [...normalizedCatalog.roleOptions];
214
+
215
+ const selectedInviteRole = String(forms.invite.roleId || "").trim().toLowerCase();
216
+ const hasSelectedInviteRole = normalizedCatalog.roleOptions.some((entry) => entry.value === selectedInviteRole);
217
+
218
+ if (
219
+ normalizedCatalog.defaultInviteRole &&
220
+ normalizedCatalog.roleOptions.some((entry) => entry.value === normalizedCatalog.defaultInviteRole)
221
+ ) {
222
+ forms.invite.roleId = normalizedCatalog.defaultInviteRole;
223
+ return;
224
+ }
225
+
226
+ if (!hasSelectedInviteRole && normalizedCatalog.roleOptions.length > 0) {
227
+ forms.invite.roleId = normalizedCatalog.roleOptions[0].value;
228
+ }
229
+ }
230
+
231
+ function normalizeMembers(entries) {
232
+ const source = Array.isArray(entries) ? entries : [];
233
+ return source.map((entry) => {
234
+ const value = entry && typeof entry === "object" ? entry : {};
235
+ return {
236
+ userId: Number(value.userId || 0),
237
+ roleId: String(value.roleId || "").trim().toLowerCase(),
238
+ status: String(value.status || "").trim().toLowerCase(),
239
+ displayName: String(value.displayName || "").trim(),
240
+ email: String(value.email || "").trim().toLowerCase(),
241
+ isOwner: Boolean(value.isOwner)
242
+ };
243
+ });
244
+ }
245
+
246
+ function normalizeInvites(entries) {
247
+ const source = Array.isArray(entries) ? entries : [];
248
+ return source.map((entry) => {
249
+ const value = entry && typeof entry === "object" ? entry : {};
250
+ return {
251
+ id: Number(value.id || 0),
252
+ email: String(value.email || "").trim().toLowerCase(),
253
+ roleId: String(value.roleId || "").trim().toLowerCase(),
254
+ status: String(value.status || "").trim().toLowerCase(),
255
+ expiresAt: value.expiresAt || "",
256
+ invitedByUserId: value.invitedByUserId == null ? null : Number(value.invitedByUserId)
257
+ };
258
+ });
259
+ }
260
+
261
+ function latestPage(pages) {
262
+ if (!Array.isArray(pages) || pages.length < 1) {
263
+ return null;
264
+ }
265
+
266
+ return pages[pages.length - 1];
267
+ }
268
+
269
+ function applyWorkspaceSettingsPolicy(payload = {}) {
270
+ const settings = payload?.settings && typeof payload.settings === "object" ? payload.settings : {};
271
+ forms.workspace.invitesEnabled = settings.invitesEnabled !== false;
272
+ forms.workspace.invitesAvailable = settings.invitesAvailable !== false;
273
+ }
274
+
275
+ const workspaceSettingsView = useView({
276
+ ownershipFilter: OWNERSHIP_WORKSPACE,
277
+ apiSuffix: "/settings",
278
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
279
+ buildWorkspaceQueryKey("settings", surfaceId, workspaceSlug),
280
+ viewPermissions: ["workspace.members.invite"],
281
+ realtime: {
282
+ event: WORKSPACE_SETTINGS_CHANGED_EVENT,
283
+ matches: isCurrentWorkspaceRealtimeEvent
284
+ },
285
+ fallbackLoadError: "Unable to load workspace settings."
286
+ });
287
+
288
+ const workspaceRolesView = useView({
289
+ ownershipFilter: OWNERSHIP_WORKSPACE,
290
+ apiSuffix: "/roles",
291
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") => buildWorkspaceQueryKey("roles", surfaceId, workspaceSlug),
292
+ viewPermissions: ["workspace.members.view", "workspace.members.invite", "workspace.members.manage"],
293
+ fallbackLoadError: "Unable to load workspace roles."
294
+ });
295
+
296
+ const workspaceMembersList = useList({
297
+ ownershipFilter: OWNERSHIP_WORKSPACE,
298
+ apiSuffix: "/members",
299
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
300
+ buildWorkspaceQueryKey("members", surfaceId, workspaceSlug),
301
+ viewPermissions: ["workspace.members.view", "workspace.members.manage"],
302
+ realtime: {
303
+ event: WORKSPACE_MEMBERS_CHANGED_EVENT,
304
+ matches: isCurrentWorkspaceRealtimeEvent
305
+ },
306
+ selectItems: (payload) => normalizeMembers(payload?.members),
307
+ fallbackLoadError: "Unable to load workspace members."
308
+ });
309
+
310
+ const workspaceInvitesList = useList({
311
+ ownershipFilter: OWNERSHIP_WORKSPACE,
312
+ apiSuffix: "/invites",
313
+ queryKeyFactory: (surfaceId = "", workspaceSlug = "") =>
314
+ buildWorkspaceQueryKey("invites", surfaceId, workspaceSlug),
315
+ viewPermissions: ["workspace.members.view", "workspace.members.manage"],
316
+ realtime: {
317
+ event: WORKSPACE_INVITES_CHANGED_EVENT,
318
+ matches: isCurrentWorkspaceRealtimeEvent
319
+ },
320
+ selectItems: (payload) => normalizeInvites(payload?.invites),
321
+ fallbackLoadError: "Unable to load workspace invites."
322
+ });
323
+
324
+ const inviteCreateCommand = useCommand({
325
+ ownershipFilter: OWNERSHIP_WORKSPACE,
326
+ apiSuffix: "/invites",
327
+ runPermissions: ["workspace.members.invite"],
328
+ writeMethod: "POST",
329
+ fallbackRunError: "Unable to send invite.",
330
+ buildRawPayload: () => ({
331
+ email: forms.invite.email,
332
+ roleId: forms.invite.roleId
333
+ }),
334
+ messages: {
335
+ success: "Invite sent.",
336
+ error: "Unable to send invite."
337
+ }
338
+ });
339
+
340
+ const revokeInviteCommand = useCommand({
341
+ ownershipFilter: OWNERSHIP_WORKSPACE,
342
+ apiSuffix: "/invites",
343
+ runPermissions: ["workspace.invites.revoke"],
344
+ writeMethod: "DELETE",
345
+ fallbackRunError: "Unable to revoke invite.",
346
+ buildCommandOptions: (_parsed, { context }) => {
347
+ return {
348
+ method: "DELETE",
349
+ path: workspaceInvitePath(context?.inviteId)
350
+ };
351
+ },
352
+ messages: {
353
+ success: "Invite revoked.",
354
+ error: "Unable to revoke invite."
355
+ }
356
+ });
357
+
358
+ const memberRoleCommand = useCommand({
359
+ ownershipFilter: OWNERSHIP_WORKSPACE,
360
+ apiSuffix: "/members",
361
+ runPermissions: ["workspace.members.manage"],
362
+ writeMethod: "PATCH",
363
+ fallbackRunError: "Unable to update member role.",
364
+ buildRawPayload: (_model, { context }) => ({
365
+ roleId: String(context?.roleId || "").trim().toLowerCase()
366
+ }),
367
+ buildCommandOptions: (_parsed, { context }) => {
368
+ return {
369
+ method: "PATCH",
370
+ path: `${workspaceMembersPath(context?.memberUserId)}/role`
371
+ };
372
+ },
373
+ messages: {
374
+ success: "Member role updated.",
375
+ error: "Unable to update member role."
376
+ }
377
+ });
378
+
379
+ const memberRemoveCommand = useCommand({
380
+ ownershipFilter: OWNERSHIP_WORKSPACE,
381
+ apiSuffix: "/members",
382
+ runPermissions: ["workspace.members.manage"],
383
+ writeMethod: "DELETE",
384
+ fallbackRunError: "Unable to remove member.",
385
+ buildCommandOptions: (_parsed, { context }) => {
386
+ return {
387
+ method: "DELETE",
388
+ path: workspaceMembersPath(context?.memberUserId)
389
+ };
390
+ },
391
+ messages: {
392
+ success: "Member removed.",
393
+ error: "Unable to remove member."
394
+ }
395
+ });
396
+
397
+ const status = computed(() => {
398
+ return {
399
+ isCreatingInvite: Boolean(inviteCreateCommand.isRunning.value),
400
+ isRevokingInvite: Boolean(revokeInviteCommand.isRunning.value),
401
+ isRemovingMember: Boolean(memberRemoveCommand.isRunning.value),
402
+ hasLoadedWorkspaceSettings: !canInviteMembers.value || !workspaceSettingsView.isLoading.value,
403
+ hasLoadedMembersList: !canViewMembers.value || !workspaceMembersList.isInitialLoading.value,
404
+ hasLoadedInviteList: !canViewMembers.value || !workspaceInvitesList.isInitialLoading.value,
405
+ isRefreshingWorkspaceSettings: canInviteMembers.value && Boolean(workspaceSettingsView.isRefetching.value),
406
+ isRefreshingMembersList: canViewMembers.value && Boolean(workspaceMembersList.isRefetching.value),
407
+ isRefreshingInviteList: canViewMembers.value && Boolean(workspaceInvitesList.isRefetching.value)
408
+ };
409
+ });
410
+
411
+ const loadError = computed(() => {
412
+ if (!hasRouteWorkspaceSlug.value) {
413
+ return "Workspace slug is required in the URL.";
414
+ }
415
+
416
+ return access.bootstrapError.value;
417
+ });
418
+
419
+ watch(
420
+ loadError,
421
+ (nextLoadError) => {
422
+ if (!nextLoadError) {
423
+ return;
424
+ }
425
+ errorRuntime.report({
426
+ source: "users-web.workspace-members-view",
427
+ severity: "error",
428
+ channel: "banner",
429
+ message: String(nextLoadError || "Unable to load workspace members."),
430
+ dedupeKey: `users-web.workspace-members-view:bootstrap:${nextLoadError}`,
431
+ dedupeWindowMs: 3000
432
+ });
433
+ },
434
+ { immediate: true }
435
+ );
436
+
437
+ const actions = Object.freeze({
438
+ submitInvite,
439
+ submitRevokeInvite,
440
+ submitMemberRoleUpdate,
441
+ submitRemoveMember
442
+ });
443
+
444
+ watch(
445
+ () => `${currentSurfaceId.value}:${workspaceSlugFromRoute.value}`,
446
+ () => {
447
+ resetViewState();
448
+ },
449
+ { immediate: true }
450
+ );
451
+
452
+ watch(
453
+ () => workspaceSettingsView.record.value,
454
+ (payload) => {
455
+ if (!payload) {
456
+ return;
457
+ }
458
+ applyWorkspaceSettingsPolicy(payload);
459
+ },
460
+ { immediate: true }
461
+ );
462
+
463
+ watch(
464
+ () => workspaceSettingsView.loadError.value,
465
+ (nextLoadError) => {
466
+ if (!nextLoadError) {
467
+ return;
468
+ }
469
+ forms.workspace.invitesEnabled = false;
470
+ forms.workspace.invitesAvailable = false;
471
+ }
472
+ );
473
+
474
+ watch(
475
+ () => workspaceRolesView.record.value,
476
+ (payload) => {
477
+ if (!payload) {
478
+ return;
479
+ }
480
+ applyRoleCatalog(payload);
481
+ },
482
+ { immediate: true }
483
+ );
484
+
485
+ watch(
486
+ () => workspaceRolesView.loadError.value,
487
+ (nextLoadError) => {
488
+ if (!nextLoadError) {
489
+ return;
490
+ }
491
+ clearRoleOptions();
492
+ }
493
+ );
494
+
495
+ watch(
496
+ () => workspaceMembersList.items.value,
497
+ (nextMembers) => {
498
+ collections.members = Array.isArray(nextMembers) ? [...nextMembers] : [];
499
+ },
500
+ { immediate: true }
501
+ );
502
+
503
+ watch(
504
+ () => workspaceMembersList.pages.value,
505
+ (pages) => {
506
+ const payload = latestPage(pages);
507
+ if (!payload) {
508
+ return;
509
+ }
510
+ applyRoleCatalog(payload);
511
+ },
512
+ { immediate: true }
513
+ );
514
+
515
+ watch(
516
+ () => workspaceMembersList.loadError.value,
517
+ (nextLoadError) => {
518
+ if (!nextLoadError) {
519
+ membersFeedback.clear();
520
+ return;
521
+ }
522
+ membersFeedback.error(null, nextLoadError);
523
+ }
524
+ );
525
+
526
+ watch(
527
+ () => workspaceInvitesList.items.value,
528
+ (nextInvites) => {
529
+ collections.invites = Array.isArray(nextInvites) ? [...nextInvites] : [];
530
+ },
531
+ { immediate: true }
532
+ );
533
+
534
+ watch(
535
+ () => workspaceInvitesList.pages.value,
536
+ (pages) => {
537
+ const payload = latestPage(pages);
538
+ if (!payload) {
539
+ return;
540
+ }
541
+ applyRoleCatalog(payload);
542
+ },
543
+ { immediate: true }
544
+ );
545
+
546
+ watch(
547
+ () => workspaceInvitesList.loadError.value,
548
+ (nextLoadError) => {
549
+ if (!nextLoadError) {
550
+ teamFeedback.clear();
551
+ return;
552
+ }
553
+ teamFeedback.error(null, nextLoadError);
554
+ }
555
+ );
556
+
557
+ watch(
558
+ () => route.fullPath,
559
+ () => {
560
+ resetMessages();
561
+ }
562
+ );
563
+
564
+ async function submitInvite() {
565
+ if (inviteCreateCommand.isRunning.value || !canInviteMembers.value) {
566
+ return;
567
+ }
568
+
569
+ inviteFeedback.clear();
570
+
571
+ try {
572
+ await inviteCreateCommand.run();
573
+ forms.invite.email = "";
574
+ await Promise.all([
575
+ workspaceInvitesList.reload(),
576
+ workspaceRolesView.refresh()
577
+ ]);
578
+ inviteFeedback.success("Invite sent.");
579
+ } catch (error) {
580
+ inviteFeedback.error(error, "Unable to send invite.");
581
+ }
582
+ }
583
+
584
+ async function submitRevokeInvite(inviteId) {
585
+ if (revokeInviteCommand.isRunning.value || !canRevokeInvites.value) {
586
+ return;
587
+ }
588
+
589
+ revokeInviteId.value = Number(inviteId || 0);
590
+ teamFeedback.clear();
591
+
592
+ try {
593
+ await revokeInviteCommand.run({
594
+ inviteId
595
+ });
596
+ await Promise.all([
597
+ workspaceInvitesList.reload(),
598
+ workspaceRolesView.refresh()
599
+ ]);
600
+ teamFeedback.success("Invite revoked.");
601
+ } catch (error) {
602
+ teamFeedback.error(error, "Unable to revoke invite.");
603
+ } finally {
604
+ revokeInviteId.value = 0;
605
+ }
606
+ }
607
+
608
+ async function submitMemberRoleUpdate(member, roleId) {
609
+ if (!canManageMembers.value) {
610
+ return;
611
+ }
612
+
613
+ membersFeedback.clear();
614
+
615
+ try {
616
+ const memberUserId = Number(member?.userId || 0);
617
+ if (!Number.isInteger(memberUserId) || memberUserId < 1) {
618
+ throw new Error("Member user id is invalid.");
619
+ }
620
+
621
+ await memberRoleCommand.run({
622
+ memberUserId,
623
+ roleId
624
+ });
625
+ await Promise.all([
626
+ workspaceMembersList.reload(),
627
+ workspaceRolesView.refresh()
628
+ ]);
629
+ membersFeedback.success("Member role updated.");
630
+ } catch (error) {
631
+ membersFeedback.error(error, "Unable to update member role.");
632
+ }
633
+ }
634
+
635
+ async function submitRemoveMember(member) {
636
+ if (memberRemoveCommand.isRunning.value || !canManageMembers.value) {
637
+ return;
638
+ }
639
+
640
+ membersFeedback.clear();
641
+
642
+ try {
643
+ const memberUserId = Number(member?.userId || 0);
644
+ if (!Number.isInteger(memberUserId) || memberUserId < 1) {
645
+ throw new Error("Member user id is invalid.");
646
+ }
647
+
648
+ removeMemberUserId.value = memberUserId;
649
+ await memberRemoveCommand.run({
650
+ memberUserId
651
+ });
652
+ await Promise.all([
653
+ workspaceMembersList.reload(),
654
+ workspaceRolesView.refresh()
655
+ ]);
656
+ membersFeedback.success("Member removed.");
657
+ } catch (error) {
658
+ membersFeedback.error(error, "Unable to remove member.");
659
+ } finally {
660
+ removeMemberUserId.value = 0;
661
+ }
662
+ }
663
+ </script>