@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
@@ -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.14",
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
- "users.web.workspace.selector",
45
- "users.web.workspace.tools.widget",
46
- "users.web.workspace-settings.menu-item",
47
- "users.web.workspace-members.menu-item",
48
- "users.web.members-admin.element"
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: "users.workspace.selector",
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: "users.web.workspace.selector",
88
+ componentToken: "workspaces.web.workspace.selector",
69
89
  when: "auth.authenticated === true",
70
- source: "mutations.text#users-web-placement-block"
90
+ source: "mutations.text#workspaces-web-placement-block"
71
91
  },
72
92
  {
73
- id: "users.account.invites.cue",
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#users-web-placement-block"
99
+ source: "mutations.text#workspaces-web-placement-block"
80
100
  },
81
101
  {
82
- id: "users.workspace.tools.widget",
102
+ id: "workspaces.workspace.tools.widget",
83
103
  target: "shell-layout:top-right",
84
104
  surfaces: ["admin"],
85
105
  order: 900,
86
- componentToken: "users.web.workspace.tools.widget",
87
- source: "mutations.text#users-web-placement-block"
106
+ componentToken: "workspaces.web.workspace.tools.widget",
107
+ source: "mutations.text#workspaces-web-placement-block"
88
108
  },
89
109
  {
90
- id: "users.workspace.menu.workspace-settings",
110
+ id: "workspaces.workspace.menu.workspace-settings",
91
111
  target: "workspace-tools:primary-menu",
92
112
  surfaces: ["admin"],
93
113
  order: 100,
94
- componentToken: "users.web.workspace-settings.menu-item",
95
- source: "mutations.text#users-web-placement-block"
114
+ componentToken: "workspaces.web.workspace-settings.menu-item",
115
+ source: "mutations.text#workspaces-web-placement-block"
96
116
  },
97
117
  {
98
- id: "users.workspace.menu.members",
118
+ id: "workspaces.workspace.menu.members",
99
119
  target: "workspace-tools:primary-menu",
100
120
  surfaces: ["admin"],
101
121
  order: 200,
102
- componentToken: "users.web.workspace-members.menu-item",
103
- source: "mutations.text#users-web-placement-block"
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.14",
113
- "@jskit-ai/users-web": "0.1.53"
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: \"users.workspace.selector\"",
247
- value: "\naddPlacement({\n id: \"users.workspace.selector\",\n target: \"shell-layout:top-left\",\n surfaces: [\"*\"],\n order: 200,\n componentToken: \"users.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: \"users.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: \"users.workspace.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"admin\"],\n order: 900,\n componentToken: \"users.web.workspace.tools.widget\"\n});\n\naddPlacement({\n id: \"users.workspace.menu.workspace-settings\",\n target: \"workspace-tools:primary-menu\",\n surfaces: [\"admin\"],\n order: 100,\n componentToken: \"users.web.workspace-settings.menu-item\"\n});\n\naddPlacement({\n id: \"users.workspace.menu.members\",\n target: \"workspace-tools:primary-menu\",\n surfaces: [\"admin\"],\n order: 200,\n componentToken: \"users.web.workspace-members.menu-item\"\n});\n",
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: "users-web-placement-block",
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.14",
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
- "@jskit-ai/users-web": "0.1.53"
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>