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