@jskit-ai/workspaces-web 0.1.14 → 0.1.15

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