@jskit-ai/users-web 0.1.53 → 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 (70) hide show
  1. package/package.descriptor.mjs +15 -53
  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 -11
  31. package/test/theme.test.js +0 -56
  32. package/src/client/components/MembersAdminClientElement.vue +0 -400
  33. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +0 -39
  34. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +0 -36
  35. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +0 -90
  36. package/src/client/components/UsersWorkspaceSelector.vue +0 -248
  37. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +0 -39
  38. package/src/client/components/UsersWorkspaceToolsWidget.vue +0 -12
  39. package/src/client/components/WorkspaceMembersClientElement.vue +0 -655
  40. package/src/client/components/WorkspaceProfileClientElement.vue +0 -116
  41. package/src/client/components/WorkspaceSettingsClientElement.vue +0 -102
  42. package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +0 -265
  43. package/src/client/components/WorkspacesClientElement.vue +0 -509
  44. package/src/client/composables/account-settings/accountSettingsInvitesRuntime.js +0 -88
  45. package/src/client/composables/useBootstrapQuery.js +0 -52
  46. package/src/client/composables/useWorkspaceRouteContext.js +0 -28
  47. package/src/client/composables/useWorkspaceSurfaceId.js +0 -43
  48. package/src/client/lib/menuIcons.js +0 -201
  49. package/src/client/lib/profileSurfaceMenuLinks.js +0 -142
  50. package/src/client/lib/surfaceAccessPolicy.js +0 -350
  51. package/src/client/lib/workspaceLinkResolver.js +0 -207
  52. package/src/client/lib/workspaceSurfaceContext.js +0 -82
  53. package/src/client/lib/workspaceSurfacePaths.js +0 -163
  54. package/src/client/providers/UsersWorkspacesClientProvider.js +0 -24
  55. package/src/client/runtime/bootstrapPlacementRouteGuards.js +0 -371
  56. package/src/client/runtime/bootstrapPlacementRuntime.js +0 -463
  57. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +0 -28
  58. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +0 -147
  59. package/src/client/support/menuLinkTarget.js +0 -93
  60. package/src/client/support/realtimeWorkspace.js +0 -21
  61. package/src/client/support/runtimeNormalization.js +0 -27
  62. package/src/client/support/workspaceQueryKeys.js +0 -15
  63. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +0 -77
  64. package/test/bootstrapPlacementRuntime.test.js +0 -1095
  65. package/test/menuIcons.test.js +0 -34
  66. package/test/menuLinkTarget.test.js +0 -116
  67. package/test/profileSurfaceMenuLinks.test.js +0 -208
  68. package/test/surfaceAccessPolicy.test.js +0 -129
  69. package/test/workspaceLinkResolver.test.js +0 -61
  70. package/test/workspaceSurfacePaths.test.js +0 -39
@@ -1,509 +0,0 @@
1
- <script setup>
2
- import { computed, reactive, ref, watch } from "vue";
3
- import { useRoute, useRouter } from "vue-router";
4
- import {
5
- useWebPlacementContext,
6
- resolveSurfaceNavigationTargetFromPlacementContext
7
- } from "@jskit-ai/shell-web/client/placement";
8
- import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
9
- import { normalizeWorkspaceList } from "../lib/bootstrap.js";
10
- import { useCommand } from "../composables/useCommand.js";
11
- import { useView } from "../composables/records/useView.js";
12
- import { usePaths } from "../composables/usePaths.js";
13
- import { useRealtimeQueryInvalidation } from "../composables/useRealtimeQueryInvalidation.js";
14
- import { useWorkspaceSurfaceId } from "../composables/useWorkspaceSurfaceId.js";
15
- import { USERS_ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/users-core/shared/support/usersVisibility";
16
- import { normalizePendingInvite } from "../composables/account-settings/accountSettingsRuntimeHelpers.js";
17
-
18
- const route = useRoute();
19
- const router = useRouter();
20
- const { context: placementContext } = useWebPlacementContext();
21
- const paths = usePaths();
22
- const errorRuntime = useShellWebErrorRuntime();
23
-
24
- const selectingWorkspaceSlug = ref("");
25
- const bootstrapModel = reactive({
26
- sessionAuthenticated: false,
27
- tenancyMode: "none",
28
- workspaceAllowSelfCreate: false,
29
- workspaceInvitesEnabled: false,
30
- workspaces: [],
31
- pendingInvites: []
32
- });
33
- const inviteAction = ref({
34
- token: "",
35
- decision: ""
36
- });
37
- const createWorkspaceModel = reactive({
38
- name: "",
39
- slug: ""
40
- });
41
- const redeemInviteModel = reactive({
42
- token: "",
43
- decision: ""
44
- });
45
- const bootstrapQueryKey = Object.freeze(["users-web", "bootstrap", "__none__"]);
46
- const OWNERSHIP_PUBLIC = USERS_ROUTE_VISIBILITY_PUBLIC;
47
-
48
- const bootstrapView = useView({
49
- ownershipFilter: OWNERSHIP_PUBLIC,
50
- apiSuffix: "/bootstrap",
51
- queryKeyFactory: () => bootstrapQueryKey,
52
- realtime: {
53
- event: "workspace.settings.changed"
54
- },
55
- fallbackLoadError: "Unable to load workspaces.",
56
- model: bootstrapModel,
57
- mapLoadedToModel: (model, payload = {}) => {
58
- model.sessionAuthenticated = Boolean(payload?.session?.authenticated);
59
- model.tenancyMode = String(payload?.tenancy?.mode || "").trim().toLowerCase() || "none";
60
- model.workspaceAllowSelfCreate = payload?.tenancy?.workspace?.allowSelfCreate === true;
61
- model.workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
62
- model.workspaces = normalizeWorkspaceList(payload?.workspaces);
63
- model.pendingInvites = model.workspaceInvitesEnabled
64
- ? (Array.isArray(payload?.pendingInvites) ? payload.pendingInvites : [])
65
- .map(normalizePendingInvite)
66
- .filter(Boolean)
67
- : [];
68
- }
69
- });
70
-
71
- const redeemInviteCommand = useCommand({
72
- ownershipFilter: OWNERSHIP_PUBLIC,
73
- apiSuffix: "/workspace/invitations/redeem",
74
- writeMethod: "POST",
75
- fallbackRunError: "Unable to respond to invitation.",
76
- suppressSuccessMessage: true,
77
- model: redeemInviteModel,
78
- buildRawPayload: (model) => ({
79
- token: String(model.token || "").trim(),
80
- decision: String(model.decision || "").trim().toLowerCase()
81
- }),
82
- messages: {
83
- error: "Unable to respond to invitation."
84
- }
85
- });
86
-
87
- const createWorkspaceCommand = useCommand({
88
- ownershipFilter: OWNERSHIP_PUBLIC,
89
- apiSuffix: "/workspaces",
90
- writeMethod: "POST",
91
- fallbackRunError: "Unable to create workspace.",
92
- suppressSuccessMessage: true,
93
- model: createWorkspaceModel,
94
- buildRawPayload: (model) => {
95
- const payload = {
96
- name: String(model.name || "").trim()
97
- };
98
- const slug = String(model.slug || "").trim().toLowerCase();
99
- if (slug) {
100
- payload.slug = slug;
101
- }
102
- return payload;
103
- },
104
- messages: {
105
- error: "Unable to create workspace."
106
- }
107
- });
108
-
109
- useRealtimeQueryInvalidation({
110
- event: "workspaces.changed",
111
- queryKey: bootstrapQueryKey
112
- });
113
-
114
- useRealtimeQueryInvalidation({
115
- event: "workspace.invitations.pending.changed",
116
- queryKey: bootstrapQueryKey
117
- });
118
-
119
- const workspaceItems = computed(() => {
120
- return Array.isArray(bootstrapModel.workspaces) ? bootstrapModel.workspaces : [];
121
- });
122
-
123
- const pendingInvites = computed(() => {
124
- return Array.isArray(bootstrapModel.pendingInvites) ? bootstrapModel.pendingInvites : [];
125
- });
126
- const workspaceInvitesEnabled = computed(() => bootstrapModel.workspaceInvitesEnabled === true);
127
-
128
- const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading));
129
- const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching));
130
- const canCreateWorkspace = computed(() => bootstrapModel.workspaceAllowSelfCreate === true);
131
- const isCreatingWorkspace = computed(() => Boolean(createWorkspaceCommand.isRunning.value));
132
-
133
- function reportFeedback({
134
- message,
135
- severity = "error",
136
- channel = "banner",
137
- dedupeKey = ""
138
- } = {}) {
139
- const normalizedMessage = String(message || "").trim();
140
- if (!normalizedMessage) {
141
- return;
142
- }
143
-
144
- errorRuntime.report({
145
- source: "users-web.workspaces-view",
146
- message: normalizedMessage,
147
- severity,
148
- channel,
149
- dedupeKey: dedupeKey || `users-web.workspaces-view:${severity}:${normalizedMessage}`,
150
- dedupeWindowMs: 3000
151
- });
152
- }
153
-
154
- const { workspaceSurfaceId } = useWorkspaceSurfaceId({
155
- route,
156
- placementContext
157
- });
158
-
159
- function workspaceInitials(workspace) {
160
- const source = String(workspace?.name || workspace?.slug || "W").trim();
161
- return source.slice(0, 2).toUpperCase();
162
- }
163
-
164
- function workspaceAvatarStyle(workspace) {
165
- const color = String(workspace?.color || "").trim();
166
- if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
167
- return {};
168
- }
169
-
170
- return {
171
- backgroundColor: color
172
- };
173
- }
174
-
175
- function workspaceHomePath(workspaceSlug) {
176
- const normalizedSlug = String(workspaceSlug || "").trim();
177
- if (!normalizedSlug || !workspaceSurfaceId.value) {
178
- return "";
179
- }
180
-
181
- return paths.page("/", {
182
- surface: workspaceSurfaceId.value,
183
- workspaceSlug: normalizedSlug,
184
- mode: "workspace"
185
- });
186
- }
187
-
188
- async function openWorkspace(workspaceSlug) {
189
- const normalizedSlug = String(workspaceSlug || "").trim();
190
- if (!normalizedSlug) {
191
- return;
192
- }
193
-
194
- const targetPath = workspaceHomePath(normalizedSlug);
195
- if (!targetPath) {
196
- reportFeedback({
197
- message: "Workspace surface is not configured.",
198
- severity: "error",
199
- channel: "banner",
200
- dedupeKey: "users-web.workspaces-view:workspace-surface-missing"
201
- });
202
- return;
203
- }
204
-
205
- selectingWorkspaceSlug.value = normalizedSlug;
206
-
207
- try {
208
- const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
209
- path: targetPath,
210
- surfaceId: workspaceSurfaceId.value
211
- });
212
- if (navigationTarget.sameOrigin && router && typeof router.push === "function") {
213
- await router.push(navigationTarget.href);
214
- } else if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
215
- window.location.assign(navigationTarget.href);
216
- return;
217
- } else {
218
- throw new Error("Workspace navigation is unavailable.");
219
- }
220
- } catch (error) {
221
- reportFeedback({
222
- message: String(error?.message || "Unable to open workspace."),
223
- severity: "error",
224
- channel: "banner",
225
- dedupeKey: `users-web.workspaces-view:open-workspace:${normalizedSlug}`
226
- });
227
- } finally {
228
- selectingWorkspaceSlug.value = "";
229
- }
230
- }
231
-
232
- async function respondToInvite(invite, decision) {
233
- if (!workspaceInvitesEnabled.value) {
234
- return;
235
- }
236
-
237
- const token = String(invite?.token || "").trim();
238
- const normalizedDecision = String(decision || "").trim().toLowerCase();
239
- if (!token || (normalizedDecision !== "accept" && normalizedDecision !== "refuse")) {
240
- return;
241
- }
242
-
243
- inviteAction.value = {
244
- token,
245
- decision: normalizedDecision
246
- };
247
- redeemInviteModel.token = token;
248
- redeemInviteModel.decision = normalizedDecision;
249
-
250
- try {
251
- await redeemInviteCommand.run();
252
-
253
- bootstrapModel.pendingInvites = pendingInvites.value.filter((entry) => entry.token !== token);
254
- await bootstrapView.refresh();
255
-
256
- if (normalizedDecision === "accept") {
257
- const nextWorkspaceSlug = String(invite?.workspaceSlug || "").trim();
258
- if (nextWorkspaceSlug) {
259
- await openWorkspace(nextWorkspaceSlug);
260
- }
261
- return;
262
- }
263
-
264
- reportFeedback({
265
- message: "Invitation refused.",
266
- severity: "success",
267
- channel: "snackbar",
268
- dedupeKey: `users-web.workspaces-view:invite-refused:${token}`
269
- });
270
- } catch (error) {
271
- reportFeedback({
272
- message: String(
273
- error?.message || (normalizedDecision === "accept" ? "Unable to accept invite." : "Unable to refuse invite.")
274
- ),
275
- severity: "error",
276
- channel: "banner",
277
- dedupeKey: `users-web.workspaces-view:invite-${normalizedDecision}:${token}`
278
- });
279
- } finally {
280
- inviteAction.value = {
281
- token: "",
282
- decision: ""
283
- };
284
- redeemInviteModel.token = "";
285
- redeemInviteModel.decision = "";
286
- }
287
- }
288
-
289
- function acceptInvite(invite) {
290
- return respondToInvite(invite, "accept");
291
- }
292
-
293
- function refuseInvite(invite) {
294
- return respondToInvite(invite, "refuse");
295
- }
296
-
297
- async function createWorkspace() {
298
- if (!canCreateWorkspace.value) {
299
- return;
300
- }
301
-
302
- const name = String(createWorkspaceModel.name || "").trim();
303
- if (!name) {
304
- reportFeedback({
305
- message: "Workspace name is required.",
306
- severity: "error",
307
- channel: "banner",
308
- dedupeKey: "users-web.workspaces-view:create-workspace-name-required"
309
- });
310
- return;
311
- }
312
-
313
- try {
314
- const createdWorkspace = await createWorkspaceCommand.run();
315
- await bootstrapView.refresh();
316
- const createdSlug = String(createdWorkspace?.slug || "").trim();
317
- const autoOpenHandledByWatcher = workspaceItems.value.length === 1 && pendingInvites.value.length < 1;
318
- if (createdSlug && !autoOpenHandledByWatcher) {
319
- await openWorkspace(createdSlug);
320
- }
321
- createWorkspaceModel.name = "";
322
- createWorkspaceModel.slug = "";
323
- } catch (error) {
324
- reportFeedback({
325
- message: String(error?.message || "Unable to create workspace."),
326
- severity: "error",
327
- channel: "banner",
328
- dedupeKey: "users-web.workspaces-view:create-workspace-error"
329
- });
330
- }
331
- }
332
-
333
- watch(
334
- () => bootstrapView.resource.data.value,
335
- async (payload) => {
336
- if (!payload) {
337
- return;
338
- }
339
-
340
- if (!bootstrapModel.sessionAuthenticated) {
341
- const loginTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
342
- path: "/auth/login",
343
- surfaceId: "auth"
344
- });
345
- if (loginTarget.sameOrigin && router && typeof router.replace === "function") {
346
- await router.replace(loginTarget.href);
347
- } else if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
348
- window.location.assign(loginTarget.href);
349
- }
350
- return;
351
- }
352
-
353
- if (workspaceItems.value.length === 1 && pendingInvites.value.length < 1) {
354
- await openWorkspace(workspaceItems.value[0].slug);
355
- }
356
- },
357
- {
358
- immediate: true
359
- }
360
- );
361
-
362
- </script>
363
-
364
- <template>
365
- <section class="workspaces-view py-6">
366
- <v-container class="mx-auto" max-width="860">
367
- <v-card rounded="lg" border elevation="1">
368
- <v-card-item>
369
- <v-card-title class="text-h6">You are logged in</v-card-title>
370
- <v-card-subtitle>Select a workspace or respond to invitations.</v-card-subtitle>
371
- </v-card-item>
372
- <v-divider />
373
-
374
- <v-card-text class="pt-4">
375
- <v-progress-linear v-if="!isBootstrapping && isRefreshingBootstrap" indeterminate class="mb-4" />
376
- <v-row>
377
- <v-col cols="12" :md="workspaceInvitesEnabled ? 6 : 12">
378
- <template v-if="isBootstrapping">
379
- <v-skeleton-loader type="text, list-item-avatar-two-line@3" />
380
- </template>
381
- <template v-else>
382
- <div class="text-subtitle-2 mb-2">Your workspaces</div>
383
- <template v-if="workspaceItems.length === 0">
384
- <p class="text-body-1 mb-2">You do not have a workspace yet.</p>
385
- <template v-if="canCreateWorkspace">
386
- <v-text-field
387
- v-model="createWorkspaceModel.name"
388
- density="comfortable"
389
- label="Workspace name"
390
- variant="outlined"
391
- hide-details
392
- class="mb-2"
393
- />
394
- <v-text-field
395
- v-model="createWorkspaceModel.slug"
396
- density="comfortable"
397
- label="Slug (optional)"
398
- variant="outlined"
399
- hide-details
400
- class="mb-3"
401
- />
402
- <v-btn
403
- color="primary"
404
- variant="tonal"
405
- :loading="isCreatingWorkspace"
406
- @click="createWorkspace"
407
- >
408
- Create Workspace
409
- </v-btn>
410
- </template>
411
- <p v-else class="text-body-2 text-medium-emphasis mb-0">
412
- Ask an administrator for an invite, or create one after policy is enabled.
413
- </p>
414
- </template>
415
-
416
- <template v-else>
417
- <v-list density="comfortable" class="pa-0">
418
- <v-list-item
419
- v-for="workspace in workspaceItems"
420
- :key="workspace.id"
421
- :title="workspace.name"
422
- :subtitle="
423
- workspace.isAccessible
424
- ? `/${workspace.slug} • role: ${workspace.roleSid || 'member'}`
425
- : `/${workspace.slug} • unavailable on this surface`
426
- "
427
- class="px-0"
428
- >
429
- <template #prepend>
430
- <v-avatar :style="workspaceAvatarStyle(workspace)" size="28">
431
- <v-img v-if="workspace.avatarUrl" :src="workspace.avatarUrl" cover />
432
- <span v-else class="text-caption">{{ workspaceInitials(workspace) }}</span>
433
- </v-avatar>
434
- </template>
435
- <template #append>
436
- <v-btn
437
- color="primary"
438
- size="small"
439
- variant="tonal"
440
- :disabled="!workspace.isAccessible"
441
- :loading="selectingWorkspaceSlug === workspace.slug"
442
- @click="openWorkspace(workspace.slug)"
443
- >
444
- {{ workspace.isAccessible ? "Open" : "Unavailable" }}
445
- </v-btn>
446
- </template>
447
- </v-list-item>
448
- </v-list>
449
- </template>
450
- </template>
451
- </v-col>
452
-
453
- <v-col v-if="workspaceInvitesEnabled" cols="12" md="6">
454
- <template v-if="isBootstrapping">
455
- <v-skeleton-loader type="text, list-item-two-line@3" />
456
- </template>
457
- <template v-else>
458
- <div class="text-subtitle-2 mb-2">Invitations</div>
459
- <template v-if="pendingInvites.length === 0">
460
- <p class="text-body-2 text-medium-emphasis mb-0">No pending invitations.</p>
461
- </template>
462
-
463
- <template v-else>
464
- <v-list density="comfortable" class="pa-0">
465
- <v-list-item
466
- v-for="invite in pendingInvites"
467
- :key="invite.id"
468
- :title="invite.workspaceName"
469
- :subtitle="`Role: ${invite.roleSid}`"
470
- class="px-0"
471
- >
472
- <template #prepend>
473
- <v-avatar color="warning" size="28">
474
- <span class="text-caption font-weight-bold">?</span>
475
- </v-avatar>
476
- </template>
477
- <template #append>
478
- <div class="d-flex ga-2">
479
- <v-btn
480
- size="small"
481
- variant="text"
482
- color="error"
483
- :loading="inviteAction.token === invite.token && inviteAction.decision === 'refuse'"
484
- @click="refuseInvite(invite)"
485
- >
486
- Refuse
487
- </v-btn>
488
- <v-btn
489
- size="small"
490
- variant="tonal"
491
- color="primary"
492
- :loading="inviteAction.token === invite.token && inviteAction.decision === 'accept'"
493
- @click="acceptInvite(invite)"
494
- >
495
- Join
496
- </v-btn>
497
- </div>
498
- </template>
499
- </v-list-item>
500
- </v-list>
501
- </template>
502
- </template>
503
- </v-col>
504
- </v-row>
505
- </v-card-text>
506
- </v-card>
507
- </v-container>
508
- </section>
509
- </template>
@@ -1,88 +0,0 @@
1
- import { resolveErrorStatusCode } from "../../support/runtimeNormalization.js";
2
-
3
- function createAccountSettingsInvitesRuntime({
4
- invitesAvailable,
5
- isResolvingInvite,
6
- inviteAction,
7
- redeemInviteModel,
8
- redeemInviteCommand,
9
- pendingInvites,
10
- pendingInvitesModel,
11
- pendingInvitesView,
12
- openWorkspace,
13
- reportAccountFeedback
14
- } = {}) {
15
- async function respondToInvite(invite, decision) {
16
- if (!invitesAvailable.value) {
17
- return;
18
- }
19
-
20
- const token = String(invite?.token || "").trim();
21
- const normalizedDecision = String(decision || "").trim().toLowerCase();
22
- if (!token || (normalizedDecision !== "accept" && normalizedDecision !== "refuse")) {
23
- return;
24
- }
25
- if (isResolvingInvite.value) {
26
- return;
27
- }
28
-
29
- inviteAction.value = {
30
- token,
31
- decision: normalizedDecision
32
- };
33
- redeemInviteModel.token = token;
34
- redeemInviteModel.decision = normalizedDecision;
35
-
36
- try {
37
- await redeemInviteCommand.run();
38
- pendingInvitesModel.pendingInvites = pendingInvites.value.filter((entry) => entry.token !== token);
39
- await pendingInvitesView.refresh();
40
-
41
- if (normalizedDecision === "accept") {
42
- const nextWorkspaceSlug = String(invite?.workspaceSlug || "").trim();
43
- if (nextWorkspaceSlug) {
44
- await openWorkspace(nextWorkspaceSlug);
45
- return;
46
- }
47
- }
48
-
49
- reportAccountFeedback({
50
- message: normalizedDecision === "accept" ? "Invitation accepted." : "Invitation refused.",
51
- severity: "success",
52
- channel: "snackbar",
53
- dedupeKey: `users-web.account-settings-runtime:invite-${normalizedDecision}:${token}`
54
- });
55
- } catch (error) {
56
- const statusCode = resolveErrorStatusCode(error);
57
- const fallbackMessage = normalizedDecision === "accept"
58
- ? "Unable to accept invitation."
59
- : "Unable to refuse invitation.";
60
- reportAccountFeedback({
61
- message: statusCode === 404
62
- ? "Invitation no longer exists."
63
- : String(error?.message || fallbackMessage),
64
- severity: "error",
65
- channel: "banner",
66
- dedupeKey: `users-web.account-settings-runtime:invite-${normalizedDecision}-error:${token}`
67
- });
68
- } finally {
69
- inviteAction.value = {
70
- token: "",
71
- decision: ""
72
- };
73
- redeemInviteModel.token = "";
74
- redeemInviteModel.decision = "";
75
- }
76
- }
77
-
78
- return Object.freeze({
79
- accept(invite) {
80
- return respondToInvite(invite, "accept");
81
- },
82
- refuse(invite) {
83
- return respondToInvite(invite, "refuse");
84
- }
85
- });
86
- }
87
-
88
- export { createAccountSettingsInvitesRuntime };
@@ -1,52 +0,0 @@
1
- import {
2
- computed
3
- } from "vue";
4
- import { useQuery } from "@tanstack/vue-query";
5
- import { normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
6
- import { usersWebHttpClient } from "../lib/httpClient.js";
7
- import { buildBootstrapApiPath } from "../lib/bootstrap.js";
8
- import { resolveEnabledRef, resolveTextRef } from "./support/refValueHelpers.js";
9
-
10
- const DEFAULT_BOOTSTRAP_STALE_TIME_MS = 60_000;
11
-
12
- function normalizeStaleTime(value, fallback = DEFAULT_BOOTSTRAP_STALE_TIME_MS) {
13
- const parsed = Number(value);
14
- if (!Number.isFinite(parsed) || parsed < 0) {
15
- return fallback;
16
- }
17
- return parsed;
18
- }
19
-
20
- function useBootstrapQuery({
21
- workspaceSlug = "",
22
- enabled = true,
23
- staleTime = DEFAULT_BOOTSTRAP_STALE_TIME_MS,
24
- refetchOnMount = false,
25
- refetchOnWindowFocus = false
26
- } = {}) {
27
- const normalizedWorkspaceSlug = computed(() => resolveTextRef(workspaceSlug));
28
- const queryKey = computed(() => ["users-web", "bootstrap", normalizeQueryToken(normalizedWorkspaceSlug.value)]);
29
- const bootstrapPath = computed(() => buildBootstrapApiPath(normalizedWorkspaceSlug.value));
30
- const queryEnabled = computed(() => resolveEnabledRef(enabled));
31
- const queryStaleTime = normalizeStaleTime(staleTime, DEFAULT_BOOTSTRAP_STALE_TIME_MS);
32
-
33
- const query = useQuery({
34
- queryKey,
35
- queryFn: () =>
36
- usersWebHttpClient.request(bootstrapPath.value, {
37
- method: "GET"
38
- }),
39
- enabled: queryEnabled,
40
- staleTime: queryStaleTime,
41
- refetchOnMount,
42
- refetchOnWindowFocus
43
- });
44
-
45
- return Object.freeze({
46
- query,
47
- queryKey,
48
- workspaceSlug: normalizedWorkspaceSlug
49
- });
50
- }
51
-
52
- export { useBootstrapQuery };
@@ -1,28 +0,0 @@
1
- import { computed } from "vue";
2
- import {
3
- extractWorkspaceSlugFromSurfacePathname
4
- } from "../lib/workspaceSurfacePaths.js";
5
- import { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
6
-
7
- function useWorkspaceRouteContext() {
8
- const { route, routePath, placementContext, mergePlacementContext, currentSurfaceId } = useSurfaceRouteContext();
9
- const workspaceSlugFromRoute = computed(() => {
10
- const workspaceSlug = extractWorkspaceSlugFromSurfacePathname(
11
- placementContext.value,
12
- currentSurfaceId.value,
13
- routePath.value
14
- );
15
- return String(workspaceSlug || "").trim();
16
- });
17
-
18
- return Object.freeze({
19
- route,
20
- routePath,
21
- placementContext,
22
- mergePlacementContext,
23
- currentSurfaceId,
24
- workspaceSlugFromRoute
25
- });
26
- }
27
-
28
- export { useWorkspaceRouteContext };