@jskit-ai/workspaces-web 0.1.1
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 +303 -0
- package/package.json +15 -0
- package/src/client/index.js +6 -0
- package/src/client/providers/WorkspacesWebClientProvider.js +5 -0
- package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
- package/templates/src/components/WorkspaceNotFoundCard.vue +34 -0
- package/templates/src/composables/useWorkspaceNotFoundState.js +41 -0
- package/templates/src/pages/admin/members/index.vue +7 -0
- package/templates/src/pages/admin/workspace/settings/index.vue +17 -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/exportsContract.test.js +42 -0
- package/test/settingsPlacementContract.test.js +63 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/workspaces-web",
|
|
4
|
+
version: "0.1.1",
|
|
5
|
+
kind: "runtime",
|
|
6
|
+
description: "Workspace web module: workspace selector, tools widget, workspace surfaces, and members/settings UI.",
|
|
7
|
+
dependsOn: [
|
|
8
|
+
"@jskit-ai/workspaces-core",
|
|
9
|
+
"@jskit-ai/users-web"
|
|
10
|
+
],
|
|
11
|
+
capabilities: {
|
|
12
|
+
provides: [
|
|
13
|
+
"workspaces.web"
|
|
14
|
+
],
|
|
15
|
+
requires: [
|
|
16
|
+
"users.web",
|
|
17
|
+
"workspaces.core"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
runtime: {
|
|
21
|
+
server: {
|
|
22
|
+
providers: []
|
|
23
|
+
},
|
|
24
|
+
client: {
|
|
25
|
+
providers: [
|
|
26
|
+
{
|
|
27
|
+
entrypoint: "src/client/providers/WorkspacesWebClientProvider.js",
|
|
28
|
+
export: "WorkspacesWebClientProvider"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
metadata: {
|
|
34
|
+
apiSummary: {
|
|
35
|
+
surfaces: [
|
|
36
|
+
{
|
|
37
|
+
subpath: "./client/providers/WorkspacesWebClientProvider",
|
|
38
|
+
summary: "Exports workspaces-web client provider class."
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
containerTokens: {
|
|
42
|
+
server: [],
|
|
43
|
+
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",
|
|
49
|
+
"users.web.workspace-settings.element"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
ui: {
|
|
54
|
+
placements: {
|
|
55
|
+
outlets: [
|
|
56
|
+
{
|
|
57
|
+
host: "admin-settings",
|
|
58
|
+
position: "primary-menu",
|
|
59
|
+
surfaces: ["admin"],
|
|
60
|
+
source: "templates/src/pages/admin/workspace/settings/index.vue"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
host: "admin-settings",
|
|
64
|
+
position: "forms",
|
|
65
|
+
surfaces: ["admin"],
|
|
66
|
+
source: "templates/src/pages/admin/workspace/settings/index.vue"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
contributions: [
|
|
70
|
+
{
|
|
71
|
+
id: "users.workspace.selector",
|
|
72
|
+
host: "shell-layout",
|
|
73
|
+
position: "top-left",
|
|
74
|
+
surfaces: ["*"],
|
|
75
|
+
order: 200,
|
|
76
|
+
componentToken: "users.web.workspace.selector",
|
|
77
|
+
when: "auth.authenticated === true",
|
|
78
|
+
source: "mutations.text#users-web-placement-block"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "users.account.invites.cue",
|
|
82
|
+
host: "shell-layout",
|
|
83
|
+
position: "top-right",
|
|
84
|
+
surfaces: ["*"],
|
|
85
|
+
order: 850,
|
|
86
|
+
componentToken: "local.main.account.pending-invites.cue",
|
|
87
|
+
when: "auth.authenticated === true",
|
|
88
|
+
source: "mutations.text#users-web-placement-block"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "users.workspace.tools.widget",
|
|
92
|
+
host: "shell-layout",
|
|
93
|
+
position: "top-right",
|
|
94
|
+
surfaces: ["admin"],
|
|
95
|
+
order: 900,
|
|
96
|
+
componentToken: "users.web.workspace.tools.widget",
|
|
97
|
+
source: "mutations.text#users-web-placement-block"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "users.workspace.menu.workspace-settings",
|
|
101
|
+
host: "workspace-tools",
|
|
102
|
+
position: "primary-menu",
|
|
103
|
+
surfaces: ["admin"],
|
|
104
|
+
order: 100,
|
|
105
|
+
componentToken: "users.web.workspace-settings.menu-item",
|
|
106
|
+
source: "mutations.text#users-web-placement-block"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "users.workspace.menu.members",
|
|
110
|
+
host: "workspace-tools",
|
|
111
|
+
position: "primary-menu",
|
|
112
|
+
surfaces: ["admin"],
|
|
113
|
+
order: 200,
|
|
114
|
+
componentToken: "users.web.workspace-members.menu-item",
|
|
115
|
+
source: "mutations.text#users-web-placement-block"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "users.workspace.settings.form",
|
|
119
|
+
host: "admin-settings",
|
|
120
|
+
position: "forms",
|
|
121
|
+
surfaces: ["admin"],
|
|
122
|
+
order: 100,
|
|
123
|
+
componentToken: "users.web.workspace-settings.element",
|
|
124
|
+
source: "mutations.text#users-web-workspace-settings-form-placement"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
mutations: {
|
|
131
|
+
dependencies: {
|
|
132
|
+
runtime: {
|
|
133
|
+
"@jskit-ai/workspaces-core": "0.1.1",
|
|
134
|
+
"@jskit-ai/users-web": "0.1.40"
|
|
135
|
+
},
|
|
136
|
+
dev: {}
|
|
137
|
+
},
|
|
138
|
+
packageJson: {
|
|
139
|
+
scripts: {
|
|
140
|
+
"dev:app": "VITE_SURFACE=app vite",
|
|
141
|
+
"dev:admin": "VITE_SURFACE=admin vite",
|
|
142
|
+
"build:app": "VITE_SURFACE=app vite build",
|
|
143
|
+
"build:admin": "VITE_SURFACE=admin vite build"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
procfile: {},
|
|
147
|
+
files: [
|
|
148
|
+
{
|
|
149
|
+
from: "templates/packages/main/src/client/components/AccountPendingInvitesCue.vue",
|
|
150
|
+
to: "packages/main/src/client/components/AccountPendingInvitesCue.vue",
|
|
151
|
+
reason: "Install app-owned account pending invites cue component scaffold.",
|
|
152
|
+
category: "workspaces-web",
|
|
153
|
+
id: "users-web-main-component-account-pending-invites-cue"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
from: "templates/src/components/WorkspaceNotFoundCard.vue",
|
|
157
|
+
to: "src/components/WorkspaceNotFoundCard.vue",
|
|
158
|
+
reason: "Install app-owned workspace not-found card component used by workspace-dependent surfaces.",
|
|
159
|
+
category: "workspaces-web",
|
|
160
|
+
id: "users-web-component-workspace-not-found-card",
|
|
161
|
+
when: {
|
|
162
|
+
config: "tenancyMode",
|
|
163
|
+
in: ["personal", "workspaces"]
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
from: "templates/src/composables/useWorkspaceNotFoundState.js",
|
|
168
|
+
to: "src/composables/useWorkspaceNotFoundState.js",
|
|
169
|
+
reason: "Install app-owned workspace bootstrap status composable for workspace-dependent surfaces.",
|
|
170
|
+
category: "workspaces-web",
|
|
171
|
+
id: "users-web-composable-workspace-not-found-state",
|
|
172
|
+
when: {
|
|
173
|
+
config: "tenancyMode",
|
|
174
|
+
in: ["personal", "workspaces"]
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
from: "templates/src/surfaces/app/root.vue",
|
|
179
|
+
toSurface: "app",
|
|
180
|
+
toSurfaceRoot: true,
|
|
181
|
+
reason: "Install workspace app surface wrapper shell for workspaces-web.",
|
|
182
|
+
category: "workspaces-web",
|
|
183
|
+
id: "users-web-page-app-wrapper",
|
|
184
|
+
when: {
|
|
185
|
+
config: "tenancyMode",
|
|
186
|
+
in: ["personal", "workspaces"]
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
from: "templates/src/surfaces/app/index.vue",
|
|
191
|
+
toSurface: "app",
|
|
192
|
+
toSurfacePath: "index.vue",
|
|
193
|
+
reason: "Install workspace app surface starter page scaffold for workspaces-web.",
|
|
194
|
+
category: "workspaces-web",
|
|
195
|
+
id: "users-web-page-app-index",
|
|
196
|
+
when: {
|
|
197
|
+
config: "tenancyMode",
|
|
198
|
+
in: ["personal", "workspaces"]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
from: "templates/src/surfaces/admin/root.vue",
|
|
203
|
+
toSurface: "admin",
|
|
204
|
+
toSurfaceRoot: true,
|
|
205
|
+
reason: "Install workspace admin surface wrapper shell for workspaces-web.",
|
|
206
|
+
category: "workspaces-web",
|
|
207
|
+
id: "users-web-page-admin-wrapper",
|
|
208
|
+
when: {
|
|
209
|
+
config: "tenancyMode",
|
|
210
|
+
in: ["personal", "workspaces"]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
from: "templates/src/surfaces/admin/index.vue",
|
|
215
|
+
toSurface: "admin",
|
|
216
|
+
toSurfacePath: "index.vue",
|
|
217
|
+
reason: "Install workspace admin surface starter page scaffold for workspaces-web.",
|
|
218
|
+
category: "workspaces-web",
|
|
219
|
+
id: "users-web-page-admin-index",
|
|
220
|
+
when: {
|
|
221
|
+
config: "tenancyMode",
|
|
222
|
+
in: ["personal", "workspaces"]
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
from: "templates/src/pages/admin/members/index.vue",
|
|
227
|
+
toSurface: "admin",
|
|
228
|
+
toSurfacePath: "members/index.vue",
|
|
229
|
+
reason: "Install admin members starter page scaffold for workspaces-web members UI.",
|
|
230
|
+
category: "workspaces-web",
|
|
231
|
+
id: "users-web-page-admin-members",
|
|
232
|
+
when: {
|
|
233
|
+
config: "tenancyMode",
|
|
234
|
+
in: ["personal", "workspaces"]
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
from: "templates/src/pages/admin/workspace/settings/index.vue",
|
|
239
|
+
toSurface: "admin",
|
|
240
|
+
toSurfacePath: "workspace/settings/index.vue",
|
|
241
|
+
reason: "Install workspace settings page scaffold for workspaces-web workspace admin UI.",
|
|
242
|
+
category: "workspaces-web",
|
|
243
|
+
id: "users-web-page-admin-workspace-settings",
|
|
244
|
+
when: {
|
|
245
|
+
config: "tenancyMode",
|
|
246
|
+
in: ["personal", "workspaces"]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
text: [
|
|
251
|
+
{
|
|
252
|
+
op: "append-text",
|
|
253
|
+
file: "src/placement.js",
|
|
254
|
+
position: "bottom",
|
|
255
|
+
skipIfContains: "id: \"users.workspace.selector\"",
|
|
256
|
+
value: "\naddPlacement({\n id: \"users.workspace.selector\",\n host: \"shell-layout\",\n position: \"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 host: \"shell-layout\",\n position: \"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 host: \"shell-layout\",\n position: \"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 host: \"workspace-tools\",\n position: \"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 host: \"workspace-tools\",\n position: \"primary-menu\",\n surfaces: [\"admin\"],\n order: 200,\n componentToken: \"users.web.workspace-members.menu-item\"\n});\n",
|
|
257
|
+
reason: "Append workspace placement entries into app-owned placement registry.",
|
|
258
|
+
category: "workspaces-web",
|
|
259
|
+
id: "users-web-placement-block",
|
|
260
|
+
when: {
|
|
261
|
+
config: "tenancyMode",
|
|
262
|
+
in: ["personal", "workspaces"]
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
op: "append-text",
|
|
267
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
268
|
+
position: "top",
|
|
269
|
+
skipIfContains: "import AccountPendingInvitesCue from \"../components/AccountPendingInvitesCue.vue\";",
|
|
270
|
+
value: "import AccountPendingInvitesCue from \"../components/AccountPendingInvitesCue.vue\";\n",
|
|
271
|
+
reason: "Bind app-owned account pending invites cue component into local main client provider imports.",
|
|
272
|
+
category: "workspaces-web",
|
|
273
|
+
id: "users-web-main-client-provider-account-invites-import"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
op: "append-text",
|
|
277
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
278
|
+
position: "bottom",
|
|
279
|
+
skipIfContains: "registerMainClientComponent(\"local.main.account.pending-invites.cue\", () => AccountPendingInvitesCue);",
|
|
280
|
+
value:
|
|
281
|
+
"\nregisterMainClientComponent(\"local.main.account.pending-invites.cue\", () => AccountPendingInvitesCue);\n",
|
|
282
|
+
reason: "Bind app-owned account pending invites cue component token into local main client provider registry.",
|
|
283
|
+
category: "workspaces-web",
|
|
284
|
+
id: "users-web-main-client-provider-account-invites-register"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
op: "append-text",
|
|
288
|
+
file: "src/placement.js",
|
|
289
|
+
position: "bottom",
|
|
290
|
+
skipIfContains: "id: \"users.workspace.settings.form\"",
|
|
291
|
+
value:
|
|
292
|
+
"\naddPlacement({\n id: \"users.workspace.settings.form\",\n host: \"admin-settings\",\n position: \"forms\",\n surfaces: [\"admin\"],\n order: 100,\n componentToken: \"users.web.workspace-settings.element\"\n});\n",
|
|
293
|
+
reason: "Append workspace settings form placement into app-owned placement registry.",
|
|
294
|
+
category: "workspaces-web",
|
|
295
|
+
id: "users-web-workspace-settings-form-placement",
|
|
296
|
+
when: {
|
|
297
|
+
config: "tenancyMode",
|
|
298
|
+
in: ["personal", "workspaces"]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/workspaces-web",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./client": "./src/client/index.js",
|
|
10
|
+
"./client/providers/WorkspacesWebClientProvider": "./src/client/providers/WorkspacesWebClientProvider.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@jskit-ai/users-web": "0.1.40"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WorkspacesWebClientProvider } from "./providers/WorkspacesWebClientProvider.js";
|
|
2
|
+
|
|
3
|
+
const clientProviders = Object.freeze([WorkspacesWebClientProvider]);
|
|
4
|
+
|
|
5
|
+
export { WorkspacesWebClientProvider } from "./providers/WorkspacesWebClientProvider.js";
|
|
6
|
+
export { clientProviders };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
import { useQuery } from "@tanstack/vue-query";
|
|
5
|
+
import { mdiEmailAlertOutline } from "@mdi/js";
|
|
6
|
+
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
7
|
+
import {
|
|
8
|
+
useWebPlacementContext,
|
|
9
|
+
resolveSurfaceDefinitionFromPlacementContext,
|
|
10
|
+
resolveSurfacePathFromPlacementContext,
|
|
11
|
+
resolveSurfaceNavigationTargetFromPlacementContext
|
|
12
|
+
} from "@jskit-ai/shell-web/client/placement";
|
|
13
|
+
|
|
14
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
15
|
+
const route = useRoute();
|
|
16
|
+
|
|
17
|
+
function normalizePendingInvitesCount(value) {
|
|
18
|
+
const numeric = Number(value);
|
|
19
|
+
if (!Number.isInteger(numeric) || numeric < 1) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
return numeric;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveReturnTo() {
|
|
26
|
+
const fullPath = String(route?.fullPath || "").trim();
|
|
27
|
+
if (fullPath.startsWith("/") && !fullPath.startsWith("//")) {
|
|
28
|
+
return fullPath;
|
|
29
|
+
}
|
|
30
|
+
const path = String(route?.path || "").trim();
|
|
31
|
+
if (path.startsWith("/") && !path.startsWith("//")) {
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
return "/";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveReturnToHref() {
|
|
38
|
+
if (typeof window === "object" && window?.location?.href) {
|
|
39
|
+
return String(window.location.href || "").trim() || resolveReturnTo();
|
|
40
|
+
}
|
|
41
|
+
return resolveReturnTo();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function countPendingInvites(entries = []) {
|
|
45
|
+
if (!Array.isArray(entries)) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let total = 0;
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry || typeof entry !== "object") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
total += 1;
|
|
55
|
+
}
|
|
56
|
+
return total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const authenticated = computed(() => placementContext.value?.auth?.authenticated === true);
|
|
60
|
+
|
|
61
|
+
const bootstrapSummaryQuery = useQuery({
|
|
62
|
+
queryKey: ["local-main", "account", "invites-cue", "bootstrap"],
|
|
63
|
+
enabled: authenticated,
|
|
64
|
+
staleTime: 5_000,
|
|
65
|
+
refetchInterval: 15_000,
|
|
66
|
+
queryFn: async () => {
|
|
67
|
+
const response = await fetch("/api/bootstrap", {
|
|
68
|
+
method: "GET",
|
|
69
|
+
credentials: "include",
|
|
70
|
+
headers: {
|
|
71
|
+
accept: "application/json"
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Bootstrap request failed with status ${response.status}.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return response.json();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const placementPendingInvitesCount = computed(() =>
|
|
83
|
+
normalizePendingInvitesCount(placementContext.value?.pendingInvitesCount)
|
|
84
|
+
);
|
|
85
|
+
const bootstrapPendingInvitesCount = computed(() => {
|
|
86
|
+
const payload = bootstrapSummaryQuery.data.value;
|
|
87
|
+
const invitesEnabled = payload?.app?.features?.workspaceInvites === true;
|
|
88
|
+
if (!invitesEnabled) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return countPendingInvites(payload?.pendingInvites);
|
|
93
|
+
});
|
|
94
|
+
const pendingInvitesCount = computed(() =>
|
|
95
|
+
Math.max(placementPendingInvitesCount.value, bootstrapPendingInvitesCount.value)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const placementWorkspaceInvitesEnabled = computed(() => placementContext.value?.workspaceInvitesEnabled === true);
|
|
99
|
+
const bootstrapWorkspaceInvitesEnabled = computed(() => {
|
|
100
|
+
const payload = bootstrapSummaryQuery.data.value;
|
|
101
|
+
return payload?.app?.features?.workspaceInvites === true;
|
|
102
|
+
});
|
|
103
|
+
const workspaceInvitesEnabled = computed(
|
|
104
|
+
() => placementWorkspaceInvitesEnabled.value || bootstrapWorkspaceInvitesEnabled.value
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const isVisible = computed(() => {
|
|
108
|
+
return (
|
|
109
|
+
authenticated.value &&
|
|
110
|
+
workspaceInvitesEnabled.value &&
|
|
111
|
+
pendingInvitesCount.value > 0
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const resolvedTo = computed(() => {
|
|
116
|
+
const hasAccountSurface = Boolean(resolveSurfaceDefinitionFromPlacementContext(placementContext.value, "account"));
|
|
117
|
+
const accountSettingsPath = hasAccountSurface
|
|
118
|
+
? resolveSurfacePathFromPlacementContext(placementContext.value, "account", "/")
|
|
119
|
+
: "/account";
|
|
120
|
+
const accountSettingsNavigation = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
121
|
+
path: accountSettingsPath,
|
|
122
|
+
surfaceId: "account"
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const query = new URLSearchParams({
|
|
126
|
+
section: "invites",
|
|
127
|
+
returnTo: accountSettingsNavigation.sameOrigin ? resolveReturnTo() : resolveReturnToHref()
|
|
128
|
+
});
|
|
129
|
+
return appendQueryString(accountSettingsPath, query.toString());
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resolvedNavigationTarget = computed(() =>
|
|
133
|
+
resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
|
|
134
|
+
path: resolvedTo.value,
|
|
135
|
+
surfaceId: "account"
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<v-badge
|
|
142
|
+
v-if="isVisible"
|
|
143
|
+
color="error"
|
|
144
|
+
:content="pendingInvitesCount"
|
|
145
|
+
:model-value="pendingInvitesCount > 0"
|
|
146
|
+
bordered
|
|
147
|
+
offset-x="6"
|
|
148
|
+
offset-y="8"
|
|
149
|
+
>
|
|
150
|
+
<v-btn
|
|
151
|
+
:to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href : undefined"
|
|
152
|
+
:href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href"
|
|
153
|
+
variant="tonal"
|
|
154
|
+
color="warning"
|
|
155
|
+
:prepend-icon="mdiEmailAlertOutline"
|
|
156
|
+
size="small"
|
|
157
|
+
class="text-none"
|
|
158
|
+
>
|
|
159
|
+
Invites
|
|
160
|
+
</v-btn>
|
|
161
|
+
</v-badge>
|
|
162
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { mdiAlertCircleOutline } from "@mdi/js";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
surfaceLabel: {
|
|
7
|
+
type: String,
|
|
8
|
+
default: "Workspace"
|
|
9
|
+
},
|
|
10
|
+
message: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: "Workspace is currently unavailable."
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const normalizedSurfaceLabel = computed(() => String(props.surfaceLabel || "").trim() || "Workspace");
|
|
17
|
+
const normalizedMessage = computed(() => String(props.message || "").trim() || "Workspace is currently unavailable.");
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<v-card rounded="lg" elevation="1" border>
|
|
22
|
+
<v-card-item>
|
|
23
|
+
<template #prepend>
|
|
24
|
+
<v-icon :icon="mdiAlertCircleOutline" color="error" />
|
|
25
|
+
</template>
|
|
26
|
+
<v-card-title class="text-h5">Unavailable</v-card-title>
|
|
27
|
+
<v-card-subtitle>{{ normalizedSurfaceLabel }} surface.</v-card-subtitle>
|
|
28
|
+
</v-card-item>
|
|
29
|
+
<v-divider />
|
|
30
|
+
<v-card-text class="d-flex flex-column ga-4">
|
|
31
|
+
<p class="text-medium-emphasis mb-0">{{ normalizedMessage }}</p>
|
|
32
|
+
</v-card-text>
|
|
33
|
+
</v-card>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRoute } from "vue-router";
|
|
3
|
+
import { normalizeLowerText, normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
5
|
+
|
|
6
|
+
const STATUS_MESSAGES = {
|
|
7
|
+
not_found: "The requested workspace was not found.",
|
|
8
|
+
forbidden: "You do not have access to this workspace.",
|
|
9
|
+
unauthenticated: "You need to sign in to access this workspace.",
|
|
10
|
+
error: "Workspace data could not be loaded right now."
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE = "Workspace is currently unavailable.";
|
|
13
|
+
const RESOLVED_WORKSPACE_STATUS = "resolved";
|
|
14
|
+
|
|
15
|
+
function useWorkspaceNotFoundState() {
|
|
16
|
+
const route = useRoute();
|
|
17
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
18
|
+
|
|
19
|
+
const routeWorkspaceSlug = computed(() => normalizeLowerText(route?.params?.workspaceSlug));
|
|
20
|
+
|
|
21
|
+
const workspaceBootstrapStatus = computed(() => {
|
|
22
|
+
const statuses = normalizeObject(placementContext.value?.workspaceBootstrapStatuses);
|
|
23
|
+
return normalizeLowerText(statuses[routeWorkspaceSlug.value]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const workspaceUnavailable = computed(
|
|
27
|
+
() => Boolean(workspaceBootstrapStatus.value) && workspaceBootstrapStatus.value !== RESOLVED_WORKSPACE_STATUS
|
|
28
|
+
);
|
|
29
|
+
const workspaceUnavailableMessage = computed(
|
|
30
|
+
() => STATUS_MESSAGES[workspaceBootstrapStatus.value] || DEFAULT_WORKSPACE_UNAVAILABLE_MESSAGE
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
routeWorkspaceSlug,
|
|
35
|
+
workspaceBootstrapStatus,
|
|
36
|
+
workspaceUnavailable,
|
|
37
|
+
workspaceUnavailableMessage
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { useWorkspaceNotFoundState };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="settings-page">
|
|
3
|
+
<ShellOutlet host="admin-settings" position="primary-menu" />
|
|
4
|
+
<ShellOutlet host="admin-settings" position="forms" />
|
|
5
|
+
</section>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup>
|
|
9
|
+
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<style scoped>
|
|
13
|
+
.settings-page {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import WorkspaceNotFoundCard from "@/components/WorkspaceNotFoundCard.vue";
|
|
3
|
+
import { useWorkspaceNotFoundState } from "@/composables/useWorkspaceNotFoundState";
|
|
4
|
+
|
|
5
|
+
const { workspaceUnavailable, workspaceUnavailableMessage } = useWorkspaceNotFoundState();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<WorkspaceNotFoundCard
|
|
10
|
+
v-if="workspaceUnavailable"
|
|
11
|
+
:message="workspaceUnavailableMessage"
|
|
12
|
+
surface-label="Admin"
|
|
13
|
+
/>
|
|
14
|
+
<v-card v-else rounded="lg" elevation="1" border>
|
|
15
|
+
<v-card-item>
|
|
16
|
+
<template #prepend>
|
|
17
|
+
<v-chip color="primary" size="small" label>Admin</v-chip>
|
|
18
|
+
</template>
|
|
19
|
+
<v-card-title class="text-h5">Workspace Admin</v-card-title>
|
|
20
|
+
<v-card-subtitle>Privileged workspace workflows.</v-card-subtitle>
|
|
21
|
+
</v-card-item>
|
|
22
|
+
<v-divider />
|
|
23
|
+
<v-card-text class="d-flex flex-column ga-4">
|
|
24
|
+
<p class="text-medium-emphasis mb-0">
|
|
25
|
+
Use this area for workspace administration modules.
|
|
26
|
+
</p>
|
|
27
|
+
</v-card-text>
|
|
28
|
+
</v-card>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<route lang="json">
|
|
2
|
+
{
|
|
3
|
+
"meta": {
|
|
4
|
+
"jskit": {
|
|
5
|
+
"surface": "admin"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
</route>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import ShellLayout from "@/components/ShellLayout.vue";
|
|
13
|
+
import { RouterView } from "vue-router";
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<ShellLayout title="" subtitle="">
|
|
18
|
+
<RouterView />
|
|
19
|
+
</ShellLayout>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import WorkspaceNotFoundCard from "@/components/WorkspaceNotFoundCard.vue";
|
|
3
|
+
import { useWorkspaceNotFoundState } from "@/composables/useWorkspaceNotFoundState";
|
|
4
|
+
|
|
5
|
+
const { workspaceUnavailable, workspaceUnavailableMessage } = useWorkspaceNotFoundState();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<WorkspaceNotFoundCard
|
|
10
|
+
v-if="workspaceUnavailable"
|
|
11
|
+
:message="workspaceUnavailableMessage"
|
|
12
|
+
surface-label="App"
|
|
13
|
+
/>
|
|
14
|
+
<v-card v-else rounded="lg" elevation="1" border>
|
|
15
|
+
<v-card-item>
|
|
16
|
+
<template #prepend>
|
|
17
|
+
<v-chip color="primary" size="small" label>App</v-chip>
|
|
18
|
+
</template>
|
|
19
|
+
<v-card-title class="text-h5">Workspace Home</v-card-title>
|
|
20
|
+
<v-card-subtitle>Primary in-workspace surface.</v-card-subtitle>
|
|
21
|
+
</v-card-item>
|
|
22
|
+
<v-divider />
|
|
23
|
+
<v-card-text class="d-flex flex-column ga-4">
|
|
24
|
+
<p class="text-medium-emphasis mb-0">Replace this page with your workspace dashboard modules.</p>
|
|
25
|
+
</v-card-text>
|
|
26
|
+
</v-card>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<route lang="json">
|
|
2
|
+
{
|
|
3
|
+
"meta": {
|
|
4
|
+
"jskit": {
|
|
5
|
+
"surface": "app"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
</route>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import ShellLayout from "@/components/ShellLayout.vue";
|
|
13
|
+
import { RouterView } from "vue-router";
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<ShellLayout title="" subtitle="">
|
|
18
|
+
<RouterView />
|
|
19
|
+
</ShellLayout>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { evaluatePackageExportsContract } from "../../../tooling/test-support/exportsContract.mjs";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
|
|
9
|
+
const PACKAGE_DIR = path.join(REPO_ROOT, "packages", "workspaces-web");
|
|
10
|
+
|
|
11
|
+
test("workspaces-web exports are explicit and aligned with production usage", () => {
|
|
12
|
+
const result = evaluatePackageExportsContract({
|
|
13
|
+
repoRoot: REPO_ROOT,
|
|
14
|
+
packageDir: PACKAGE_DIR,
|
|
15
|
+
packageId: "@jskit-ai/workspaces-web",
|
|
16
|
+
requiredExports: [
|
|
17
|
+
"./client",
|
|
18
|
+
"./client/providers/WorkspacesWebClientProvider"
|
|
19
|
+
]
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
result.wildcardExports,
|
|
24
|
+
[],
|
|
25
|
+
`workspaces-web exports must be explicit. Remove wildcard keys: ${result.wildcardExports.join(", ")}`
|
|
26
|
+
);
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
result.missingRequiredExports,
|
|
29
|
+
[],
|
|
30
|
+
`workspaces-web required exports missing: ${result.missingRequiredExports.join(", ")}`
|
|
31
|
+
);
|
|
32
|
+
assert.deepEqual(
|
|
33
|
+
result.missingExports,
|
|
34
|
+
[],
|
|
35
|
+
`workspaces-web imports missing from package exports:\n${result.missingExports.join("\n")}`
|
|
36
|
+
);
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
result.staleExports,
|
|
39
|
+
[],
|
|
40
|
+
`Stale workspaces-web exports found. Remove stale keys: ${result.staleExports.join(", ")}`
|
|
41
|
+
);
|
|
42
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
|
|
8
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
+
|
|
11
|
+
function readSettingsOutlets() {
|
|
12
|
+
const outlets = descriptor?.metadata?.ui?.placements?.outlets;
|
|
13
|
+
return Array.isArray(outlets)
|
|
14
|
+
? outlets.filter((entry) => String(entry?.host || "").trim() === "admin-settings")
|
|
15
|
+
: [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findContribution(id) {
|
|
19
|
+
const contributions = descriptor?.metadata?.ui?.placements?.contributions;
|
|
20
|
+
return Array.isArray(contributions)
|
|
21
|
+
? contributions.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("workspaces-web admin settings template exposes surface-derived settings outlets", async () => {
|
|
26
|
+
const source = await readFile(
|
|
27
|
+
path.join(PACKAGE_DIR, "templates", "src", "pages", "admin", "workspace", "settings", "index.vue"),
|
|
28
|
+
"utf8"
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.match(source, /<ShellOutlet host="admin-settings" position="primary-menu" \/>/);
|
|
32
|
+
assert.match(source, /<ShellOutlet host="admin-settings" position="forms" \/>/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("workspaces-web descriptor metadata advertises admin settings outlets and form placement on the derived host", () => {
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
readSettingsOutlets(),
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
host: "admin-settings",
|
|
41
|
+
position: "primary-menu",
|
|
42
|
+
surfaces: ["admin"],
|
|
43
|
+
source: "templates/src/pages/admin/workspace/settings/index.vue"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
host: "admin-settings",
|
|
47
|
+
position: "forms",
|
|
48
|
+
surfaces: ["admin"],
|
|
49
|
+
source: "templates/src/pages/admin/workspace/settings/index.vue"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(findContribution("users.workspace.settings.form"), {
|
|
55
|
+
id: "users.workspace.settings.form",
|
|
56
|
+
host: "admin-settings",
|
|
57
|
+
position: "forms",
|
|
58
|
+
surfaces: ["admin"],
|
|
59
|
+
order: 100,
|
|
60
|
+
componentToken: "users.web.workspace-settings.element",
|
|
61
|
+
source: "mutations.text#users-web-workspace-settings-form-placement"
|
|
62
|
+
});
|
|
63
|
+
});
|