@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.
- package/package.descriptor.mjs +61 -25
- package/package.json +13 -3
- package/src/client/account-settings/accountSettingsInvitesRuntime.js +88 -0
- package/src/client/account-settings/useAccountSettingsInvitesSectionRuntime.js +217 -0
- package/src/client/components/AccountSettingsInvitesSection.vue +72 -0
- package/src/client/components/MembersAdminClientElement.vue +400 -0
- package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
- package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
- package/src/client/components/UsersWorkspacePermissionMenuItem.vue +89 -0
- package/src/client/components/UsersWorkspaceSelector.vue +242 -0
- package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
- package/src/client/components/UsersWorkspaceToolsWidget.vue +12 -0
- package/src/client/components/WorkspaceMembersClientElement.vue +657 -0
- package/src/client/components/WorkspaceProfileClientElement.vue +116 -0
- package/src/client/components/WorkspaceSettingsClientElement.vue +102 -0
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +265 -0
- package/src/client/components/WorkspacesClientElement.vue +540 -0
- package/src/client/composables/useBootstrapQuery.js +52 -0
- package/src/client/composables/useWorkspaceRouteContext.js +28 -0
- package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
- package/src/client/index.js +1 -0
- package/src/client/lib/bootstrap.js +59 -0
- package/src/client/lib/httpClient.js +10 -0
- package/src/client/lib/permissions.js +27 -0
- package/src/client/lib/profileSurfaceMenuLinks.js +163 -0
- package/src/client/lib/surfaceAccessPolicy.js +350 -0
- package/src/client/lib/theme.js +332 -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/WorkspacesWebClientProvider.js +59 -2
- package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
- package/src/client/runtime/bootstrapPlacementRuntime.js +463 -0
- package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +28 -0
- package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +147 -0
- package/src/client/support/realtimeWorkspace.js +21 -0
- package/src/client/support/runtimeNormalization.js +23 -0
- package/src/client/support/workspaceQueryKeys.js +15 -0
- package/src/shared/toolsOutletContracts.js +9 -0
- package/templates/src/pages/admin/members/index.vue +1 -1
- package/test/bootstrapPlacementRuntime.test.js +1095 -0
- package/test/exportsContract.test.js +2 -1
- package/test/profileSurfaceMenuLinks.test.js +208 -0
- package/test/settingsPlacementContract.test.js +19 -1
- package/test/surfaceAccessPolicy.test.js +129 -0
- package/test/theme.test.js +101 -0
- package/test/workspaceLinkResolver.test.js +61 -0
- package/test/workspaceSurfacePaths.test.js +39 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/workspaces-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.15",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Workspace web module: workspace selector, tools widget, workspace surfaces, and members/settings UI.",
|
|
7
7
|
dependsOn: [
|
|
@@ -33,19 +33,30 @@ export default Object.freeze({
|
|
|
33
33
|
metadata: {
|
|
34
34
|
apiSummary: {
|
|
35
35
|
surfaces: [
|
|
36
|
+
{
|
|
37
|
+
subpath: "./client",
|
|
38
|
+
summary: "Exports workspaces-web client provider registration surface."
|
|
39
|
+
},
|
|
36
40
|
{
|
|
37
41
|
subpath: "./client/providers/WorkspacesWebClientProvider",
|
|
38
42
|
summary: "Exports workspaces-web client provider class."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
subpath: "./client/composables/useWorkspaceRouteContext",
|
|
46
|
+
summary: "Exports workspace route context composable."
|
|
39
47
|
}
|
|
40
48
|
],
|
|
41
49
|
containerTokens: {
|
|
42
50
|
server: [],
|
|
43
51
|
client: [
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
52
|
+
"workspaces.web.profile.menu.surface-switch-item",
|
|
53
|
+
"workspaces.web.workspace.selector",
|
|
54
|
+
"workspaces.web.workspace.tools.widget",
|
|
55
|
+
"workspaces.web.workspace-settings.menu-item",
|
|
56
|
+
"workspaces.web.workspace-members.menu-item",
|
|
57
|
+
"workspaces.web.members-admin.element",
|
|
58
|
+
"workspaces.web.bootstrap-placement.runtime",
|
|
59
|
+
"workspaces.web.account-settings.section.invites"
|
|
49
60
|
]
|
|
50
61
|
}
|
|
51
62
|
},
|
|
@@ -61,46 +72,55 @@ export default Object.freeze({
|
|
|
61
72
|
],
|
|
62
73
|
contributions: [
|
|
63
74
|
{
|
|
64
|
-
id: "
|
|
75
|
+
id: "workspaces.profile.menu.surface-switch",
|
|
76
|
+
target: "auth-profile-menu:primary-menu",
|
|
77
|
+
surfaces: ["*"],
|
|
78
|
+
order: 100,
|
|
79
|
+
componentToken: "workspaces.web.profile.menu.surface-switch-item",
|
|
80
|
+
when: "auth.authenticated === true",
|
|
81
|
+
source: "mutations.text#workspaces-web-profile-surface-switch-placement"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "workspaces.workspace.selector",
|
|
65
85
|
target: "shell-layout:top-left",
|
|
66
86
|
surfaces: ["*"],
|
|
67
87
|
order: 200,
|
|
68
|
-
componentToken: "
|
|
88
|
+
componentToken: "workspaces.web.workspace.selector",
|
|
69
89
|
when: "auth.authenticated === true",
|
|
70
|
-
source: "mutations.text#
|
|
90
|
+
source: "mutations.text#workspaces-web-placement-block"
|
|
71
91
|
},
|
|
72
92
|
{
|
|
73
|
-
id: "
|
|
93
|
+
id: "workspaces.account.invites.cue",
|
|
74
94
|
target: "shell-layout:top-right",
|
|
75
95
|
surfaces: ["*"],
|
|
76
96
|
order: 850,
|
|
77
97
|
componentToken: "local.main.account.pending-invites.cue",
|
|
78
98
|
when: "auth.authenticated === true",
|
|
79
|
-
source: "mutations.text#
|
|
99
|
+
source: "mutations.text#workspaces-web-placement-block"
|
|
80
100
|
},
|
|
81
101
|
{
|
|
82
|
-
id: "
|
|
102
|
+
id: "workspaces.workspace.tools.widget",
|
|
83
103
|
target: "shell-layout:top-right",
|
|
84
104
|
surfaces: ["admin"],
|
|
85
105
|
order: 900,
|
|
86
|
-
componentToken: "
|
|
87
|
-
source: "mutations.text#
|
|
106
|
+
componentToken: "workspaces.web.workspace.tools.widget",
|
|
107
|
+
source: "mutations.text#workspaces-web-placement-block"
|
|
88
108
|
},
|
|
89
109
|
{
|
|
90
|
-
id: "
|
|
110
|
+
id: "workspaces.workspace.menu.workspace-settings",
|
|
91
111
|
target: "workspace-tools:primary-menu",
|
|
92
112
|
surfaces: ["admin"],
|
|
93
113
|
order: 100,
|
|
94
|
-
componentToken: "
|
|
95
|
-
source: "mutations.text#
|
|
114
|
+
componentToken: "workspaces.web.workspace-settings.menu-item",
|
|
115
|
+
source: "mutations.text#workspaces-web-placement-block"
|
|
96
116
|
},
|
|
97
117
|
{
|
|
98
|
-
id: "
|
|
118
|
+
id: "workspaces.workspace.menu.members",
|
|
99
119
|
target: "workspace-tools:primary-menu",
|
|
100
120
|
surfaces: ["admin"],
|
|
101
121
|
order: 200,
|
|
102
|
-
componentToken: "
|
|
103
|
-
source: "mutations.text#
|
|
122
|
+
componentToken: "workspaces.web.workspace-members.menu-item",
|
|
123
|
+
source: "mutations.text#workspaces-web-placement-block"
|
|
104
124
|
},
|
|
105
125
|
]
|
|
106
126
|
}
|
|
@@ -109,8 +129,9 @@ export default Object.freeze({
|
|
|
109
129
|
mutations: {
|
|
110
130
|
dependencies: {
|
|
111
131
|
runtime: {
|
|
112
|
-
"@jskit-ai/workspaces-core": "0.1.
|
|
113
|
-
"@jskit-ai/users-web": "0.1.
|
|
132
|
+
"@jskit-ai/workspaces-core": "0.1.15",
|
|
133
|
+
"@jskit-ai/users-web": "0.1.54",
|
|
134
|
+
"vuetify": "^4.0.0"
|
|
114
135
|
},
|
|
115
136
|
dev: {}
|
|
116
137
|
},
|
|
@@ -243,11 +264,26 @@ export default Object.freeze({
|
|
|
243
264
|
op: "append-text",
|
|
244
265
|
file: "src/placement.js",
|
|
245
266
|
position: "bottom",
|
|
246
|
-
skipIfContains: "id: \"
|
|
247
|
-
value:
|
|
267
|
+
skipIfContains: "id: \"workspaces.profile.menu.surface-switch\"",
|
|
268
|
+
value:
|
|
269
|
+
"\naddPlacement({\n id: \"workspaces.profile.menu.surface-switch\",\n target: \"auth-profile-menu:primary-menu\",\n surfaces: [\"*\"],\n order: 100,\n componentToken: \"workspaces.web.profile.menu.surface-switch-item\",\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
|
|
270
|
+
reason: "Append workspaces-web profile surface switch placement into app-owned placement registry.",
|
|
271
|
+
category: "workspaces-web",
|
|
272
|
+
id: "workspaces-web-profile-surface-switch-placement",
|
|
273
|
+
when: {
|
|
274
|
+
config: "tenancyMode",
|
|
275
|
+
in: ["personal", "workspaces"]
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
op: "append-text",
|
|
280
|
+
file: "src/placement.js",
|
|
281
|
+
position: "bottom",
|
|
282
|
+
skipIfContains: "id: \"workspaces.workspace.selector\"",
|
|
283
|
+
value: "\naddPlacement({\n id: \"workspaces.workspace.selector\",\n target: \"shell-layout:top-left\",\n surfaces: [\"*\"],\n order: 200,\n componentToken: \"workspaces.web.workspace.selector\",\n props: {\n allowOnNonWorkspaceSurface: true,\n targetSurfaceId: \"app\"\n },\n when: ({ auth }) => {\n return Boolean(auth?.authenticated);\n }\n});\n\naddPlacement({\n id: \"workspaces.account.invites.cue\",\n target: \"shell-layout:top-right\",\n surfaces: [\"*\"],\n order: 850,\n componentToken: \"local.main.account.pending-invites.cue\",\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n\naddPlacement({\n id: \"workspaces.workspace.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"admin\"],\n order: 900,\n componentToken: \"workspaces.web.workspace.tools.widget\"\n});\n\naddPlacement({\n id: \"workspaces.workspace.menu.workspace-settings\",\n target: \"workspace-tools:primary-menu\",\n surfaces: [\"admin\"],\n order: 100,\n componentToken: \"workspaces.web.workspace-settings.menu-item\"\n});\n\naddPlacement({\n id: \"workspaces.workspace.menu.members\",\n target: \"workspace-tools:primary-menu\",\n surfaces: [\"admin\"],\n order: 200,\n componentToken: \"workspaces.web.workspace-members.menu-item\"\n});\n",
|
|
248
284
|
reason: "Append workspace placement entries into app-owned placement registry.",
|
|
249
285
|
category: "workspaces-web",
|
|
250
|
-
id: "
|
|
286
|
+
id: "workspaces-web-placement-block",
|
|
251
287
|
when: {
|
|
252
288
|
config: "tenancyMode",
|
|
253
289
|
in: ["personal", "workspaces"]
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
|
-
"./client/providers/WorkspacesWebClientProvider": "./src/client/providers/WorkspacesWebClientProvider.js"
|
|
10
|
+
"./client/providers/WorkspacesWebClientProvider": "./src/client/providers/WorkspacesWebClientProvider.js",
|
|
11
|
+
"./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
|
|
12
|
+
"./client/composables/useWorkspaceRouteContext": "./src/client/composables/useWorkspaceRouteContext.js"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
|
-
"@
|
|
15
|
+
"@tanstack/vue-query": "5.92.12",
|
|
16
|
+
"@mdi/js": "^7.4.47",
|
|
17
|
+
"@jskit-ai/http-runtime": "0.1.38",
|
|
18
|
+
"@jskit-ai/kernel": "0.1.39",
|
|
19
|
+
"@jskit-ai/shell-web": "0.1.38",
|
|
20
|
+
"@jskit-ai/users-core": "0.1.49",
|
|
21
|
+
"@jskit-ai/users-web": "0.1.54",
|
|
22
|
+
"@jskit-ai/workspaces-core": "0.1.15",
|
|
23
|
+
"vuetify": "^4.0.0"
|
|
14
24
|
}
|
|
15
25
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { computed, reactive, ref } from "vue";
|
|
2
|
+
import { useRoute, useRouter } from "vue-router";
|
|
3
|
+
import {
|
|
4
|
+
useWebPlacementContext,
|
|
5
|
+
resolveSurfaceNavigationTargetFromPlacementContext
|
|
6
|
+
} from "@jskit-ai/shell-web/client/placement";
|
|
7
|
+
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
8
|
+
import { ROUTE_VISIBILITY_PUBLIC } from "@jskit-ai/kernel/shared/support/visibility";
|
|
9
|
+
import { useCommand } from "@jskit-ai/users-web/client/composables/useCommand";
|
|
10
|
+
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
11
|
+
import { useView } from "@jskit-ai/users-web/client/composables/useView";
|
|
12
|
+
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
13
|
+
import { useWorkspaceSurfaceId } from "../composables/useWorkspaceSurfaceId.js";
|
|
14
|
+
import { createAccountSettingsInvitesRuntime } from "./accountSettingsInvitesRuntime.js";
|
|
15
|
+
|
|
16
|
+
function normalizePendingInvite(entry) {
|
|
17
|
+
if (!entry || typeof entry !== "object") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const id = normalizeRecordId(entry.id, { fallback: null });
|
|
22
|
+
const workspaceId = normalizeRecordId(entry.workspaceId, { fallback: null });
|
|
23
|
+
if (!id || !workspaceId) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const workspaceSlug = String(entry.workspaceSlug || "").trim();
|
|
28
|
+
if (!workspaceSlug) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const token = String(entry.token || "").trim();
|
|
33
|
+
if (!token) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
token,
|
|
40
|
+
workspaceId,
|
|
41
|
+
workspaceSlug,
|
|
42
|
+
workspaceName: String(entry.workspaceName || workspaceSlug).trim() || workspaceSlug,
|
|
43
|
+
workspaceAvatarUrl: String(entry.workspaceAvatarUrl || "").trim(),
|
|
44
|
+
roleSid: String(entry.roleSid || "member").trim().toLowerCase() || "member",
|
|
45
|
+
status: String(entry.status || "pending").trim().toLowerCase() || "pending",
|
|
46
|
+
expiresAt: String(entry.expiresAt || "").trim()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useAccountSettingsInvitesSectionRuntime() {
|
|
51
|
+
const route = useRoute();
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
54
|
+
const errorRuntime = useShellWebErrorRuntime();
|
|
55
|
+
const paths = usePaths();
|
|
56
|
+
|
|
57
|
+
const pendingInvitesQueryKey = ["workspaces-web", "account-settings", "pending-invites"];
|
|
58
|
+
const pendingInvitesModel = reactive({
|
|
59
|
+
pendingInvites: [],
|
|
60
|
+
workspaceInvitesEnabled: false
|
|
61
|
+
});
|
|
62
|
+
const inviteAction = ref({
|
|
63
|
+
token: "",
|
|
64
|
+
decision: ""
|
|
65
|
+
});
|
|
66
|
+
const redeemInviteModel = reactive({
|
|
67
|
+
token: "",
|
|
68
|
+
decision: ""
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function reportAccountFeedback({
|
|
72
|
+
message,
|
|
73
|
+
severity = "error",
|
|
74
|
+
channel = "banner",
|
|
75
|
+
dedupeKey = ""
|
|
76
|
+
} = {}) {
|
|
77
|
+
const normalizedMessage = String(message || "").trim();
|
|
78
|
+
if (!normalizedMessage) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
errorRuntime.report({
|
|
83
|
+
source: "workspaces-web.account-settings-invites",
|
|
84
|
+
message: normalizedMessage,
|
|
85
|
+
severity,
|
|
86
|
+
channel,
|
|
87
|
+
dedupeKey: dedupeKey || `workspaces-web.account-settings-invites:${severity}:${normalizedMessage}`,
|
|
88
|
+
dedupeWindowMs: 3000
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const pendingInvitesView = useView({
|
|
93
|
+
ownershipFilter: ROUTE_VISIBILITY_PUBLIC,
|
|
94
|
+
apiSuffix: "/bootstrap",
|
|
95
|
+
queryKeyFactory: () => pendingInvitesQueryKey,
|
|
96
|
+
realtime: {
|
|
97
|
+
event: "workspace.invitations.pending.changed"
|
|
98
|
+
},
|
|
99
|
+
fallbackLoadError: "Unable to load invitations.",
|
|
100
|
+
model: pendingInvitesModel,
|
|
101
|
+
mapLoadedToModel: (model, payload = {}) => {
|
|
102
|
+
model.workspaceInvitesEnabled = payload?.app?.features?.workspaceInvites === true;
|
|
103
|
+
model.pendingInvites = model.workspaceInvitesEnabled
|
|
104
|
+
? (Array.isArray(payload?.pendingInvites) ? payload.pendingInvites : [])
|
|
105
|
+
.map(normalizePendingInvite)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
: [];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const redeemInviteCommand = useCommand({
|
|
112
|
+
ownershipFilter: ROUTE_VISIBILITY_PUBLIC,
|
|
113
|
+
apiSuffix: "/workspace/invitations/redeem",
|
|
114
|
+
writeMethod: "POST",
|
|
115
|
+
fallbackRunError: "Unable to respond to invitation.",
|
|
116
|
+
suppressSuccessMessage: true,
|
|
117
|
+
model: redeemInviteModel,
|
|
118
|
+
buildRawPayload: (model) => ({
|
|
119
|
+
token: String(model.token || "").trim(),
|
|
120
|
+
decision: String(model.decision || "").trim().toLowerCase()
|
|
121
|
+
}),
|
|
122
|
+
messages: {
|
|
123
|
+
error: "Unable to respond to invitation."
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const invitesAvailable = computed(() => pendingInvitesModel.workspaceInvitesEnabled === true);
|
|
128
|
+
const loadingInvites = computed(() => Boolean(pendingInvitesView.isLoading));
|
|
129
|
+
const refreshingInvites = computed(() => Boolean(pendingInvitesView.isRefetching));
|
|
130
|
+
const pendingInvites = computed(() =>
|
|
131
|
+
Array.isArray(pendingInvitesModel.pendingInvites) ? pendingInvitesModel.pendingInvites : []
|
|
132
|
+
);
|
|
133
|
+
const isResolvingInvite = computed(() => Boolean(redeemInviteCommand.isRunning.value));
|
|
134
|
+
|
|
135
|
+
const { workspaceSurfaceId } = useWorkspaceSurfaceId({
|
|
136
|
+
route,
|
|
137
|
+
placementContext
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function workspaceHomePath(workspaceSlug) {
|
|
141
|
+
const normalizedSlug = String(workspaceSlug || "").trim();
|
|
142
|
+
if (!normalizedSlug || !workspaceSurfaceId.value) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return paths.page("/", {
|
|
147
|
+
surface: workspaceSurfaceId.value,
|
|
148
|
+
params: {
|
|
149
|
+
workspaceSlug: normalizedSlug
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function openWorkspace(workspaceSlug) {
|
|
155
|
+
const targetPath = workspaceHomePath(workspaceSlug);
|
|
156
|
+
if (!targetPath) {
|
|
157
|
+
reportAccountFeedback({
|
|
158
|
+
message: "Workspace surface is not configured.",
|
|
159
|
+
severity: "error",
|
|
160
|
+
channel: "banner",
|
|
161
|
+
dedupeKey: "workspaces-web.account-settings-invites:workspace-surface-missing"
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
168
|
+
path: targetPath,
|
|
169
|
+
surfaceId: workspaceSurfaceId.value
|
|
170
|
+
});
|
|
171
|
+
if (navigationTarget.sameOrigin) {
|
|
172
|
+
await router.push(navigationTarget.href);
|
|
173
|
+
} else if (typeof window === "object" && window?.location && typeof window.location.assign === "function") {
|
|
174
|
+
window.location.assign(navigationTarget.href);
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error("Cross-origin navigation is unavailable in this environment.");
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
reportAccountFeedback({
|
|
180
|
+
message: String(error?.message || "Unable to open workspace."),
|
|
181
|
+
severity: "error",
|
|
182
|
+
channel: "banner",
|
|
183
|
+
dedupeKey: `workspaces-web.account-settings-invites:open-workspace:${String(workspaceSlug || "").trim()}`
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const invitesRuntime = createAccountSettingsInvitesRuntime({
|
|
189
|
+
invitesAvailable,
|
|
190
|
+
isResolvingInvite,
|
|
191
|
+
inviteAction,
|
|
192
|
+
redeemInviteModel,
|
|
193
|
+
redeemInviteCommand,
|
|
194
|
+
pendingInvites,
|
|
195
|
+
pendingInvitesModel,
|
|
196
|
+
pendingInvitesView,
|
|
197
|
+
openWorkspace,
|
|
198
|
+
reportAccountFeedback
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
isAvailable: invitesAvailable,
|
|
203
|
+
items: pendingInvites,
|
|
204
|
+
isLoading: loadingInvites,
|
|
205
|
+
isRefetching: refreshingInvites,
|
|
206
|
+
isResolving: isResolvingInvite,
|
|
207
|
+
action: inviteAction,
|
|
208
|
+
accept(invite) {
|
|
209
|
+
return invitesRuntime.accept(invite);
|
|
210
|
+
},
|
|
211
|
+
refuse(invite) {
|
|
212
|
+
return invitesRuntime.refuse(invite);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { useAccountSettingsInvitesSectionRuntime };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useAccountSettingsInvitesSectionRuntime } from "../account-settings/useAccountSettingsInvitesSectionRuntime.js";
|
|
3
|
+
|
|
4
|
+
const invites = useAccountSettingsInvitesSectionRuntime();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<v-card rounded="lg" elevation="0" border>
|
|
9
|
+
<v-card-item>
|
|
10
|
+
<v-card-title class="text-subtitle-1">Invitations</v-card-title>
|
|
11
|
+
<v-card-subtitle>Accept or refuse workspace invitations.</v-card-subtitle>
|
|
12
|
+
</v-card-item>
|
|
13
|
+
<v-divider />
|
|
14
|
+
<v-card-text>
|
|
15
|
+
<template v-if="invites.isLoading.value">
|
|
16
|
+
<v-skeleton-loader type="text@2, list-item-two-line@3" />
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template v-else-if="invites.items.value.length < 1">
|
|
20
|
+
<v-progress-linear v-if="invites.isRefetching.value" indeterminate class="mb-4" />
|
|
21
|
+
<p class="text-body-2 text-medium-emphasis mb-0">No pending invitations.</p>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<template v-else>
|
|
25
|
+
<v-progress-linear v-if="invites.isRefetching.value" indeterminate class="mb-4" />
|
|
26
|
+
<v-list density="comfortable" class="pa-0">
|
|
27
|
+
<v-list-item
|
|
28
|
+
v-for="invite in invites.items.value"
|
|
29
|
+
:key="invite.id"
|
|
30
|
+
:title="invite.workspaceName"
|
|
31
|
+
:subtitle="`/${invite.workspaceSlug} • role: ${invite.roleSid}`"
|
|
32
|
+
class="px-0"
|
|
33
|
+
>
|
|
34
|
+
<template #prepend>
|
|
35
|
+
<v-avatar color="warning" size="28">
|
|
36
|
+
<span class="text-caption font-weight-bold">?</span>
|
|
37
|
+
</v-avatar>
|
|
38
|
+
</template>
|
|
39
|
+
<template #append>
|
|
40
|
+
<div class="d-flex ga-2">
|
|
41
|
+
<v-btn
|
|
42
|
+
size="small"
|
|
43
|
+
variant="text"
|
|
44
|
+
color="error"
|
|
45
|
+
:loading="invites.action.value.token === invite.token && invites.action.value.decision === 'refuse'"
|
|
46
|
+
:disabled="
|
|
47
|
+
invites.isRefetching.value || (invites.isResolving.value && invites.action.value.token !== invite.token)
|
|
48
|
+
"
|
|
49
|
+
@click="invites.refuse(invite)"
|
|
50
|
+
>
|
|
51
|
+
Refuse
|
|
52
|
+
</v-btn>
|
|
53
|
+
<v-btn
|
|
54
|
+
size="small"
|
|
55
|
+
variant="tonal"
|
|
56
|
+
color="primary"
|
|
57
|
+
:loading="invites.action.value.token === invite.token && invites.action.value.decision === 'accept'"
|
|
58
|
+
:disabled="
|
|
59
|
+
invites.isRefetching.value || (invites.isResolving.value && invites.action.value.token !== invite.token)
|
|
60
|
+
"
|
|
61
|
+
@click="invites.accept(invite)"
|
|
62
|
+
>
|
|
63
|
+
Join
|
|
64
|
+
</v-btn>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
</v-list-item>
|
|
68
|
+
</v-list>
|
|
69
|
+
</template>
|
|
70
|
+
</v-card-text>
|
|
71
|
+
</v-card>
|
|
72
|
+
</template>
|