@jskit-ai/workspaces-web 0.1.42 → 0.1.43
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 +7 -4
- package/package.json +11 -10
- package/src/client/components/AccountSettingsInvitesSection.vue +36 -9
- package/src/client/components/WorkspaceMembersClientElement.vue +62 -45
- package/src/client/components/WorkspaceProfileClientElement.vue +55 -12
- package/src/client/components/WorkspaceSettingsClientElement.vue +1 -1
- package/src/client/components/WorkspaceSettingsFieldsClientElement.vue +55 -12
- package/src/client/components/WorkspacesClientElement.vue +67 -17
- package/templates/src/components/WorkspaceNotFoundCard.vue +32 -13
- package/templates/src/pages/admin/workspace/settings/index.vue +2 -6
- package/templates/src/pages/admin/workspace/settings.vue +61 -20
- package/templates/src/surfaces/admin/index.vue +62 -14
- package/templates/src/surfaces/app/index.vue +32 -13
- package/test/exportsContract.test.js +1 -0
- package/test/settingsPlacementContract.test.js +99 -3
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.43",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Workspace web module: workspace selector, tools widget, workspace surfaces, and members/settings UI.",
|
|
7
7
|
dependsOn: [
|
|
@@ -41,6 +41,10 @@ export default Object.freeze({
|
|
|
41
41
|
subpath: "./client/components/AccountSettingsInvitesSection",
|
|
42
42
|
summary: "Exports the default account invites section component used by multihoming installs."
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
subpath: "./client/components/WorkspaceSettingsClientElement",
|
|
46
|
+
summary: "Exports the workspace settings client element used by the workspace admin settings starter page."
|
|
47
|
+
},
|
|
44
48
|
{
|
|
45
49
|
subpath: "./client/providers/WorkspacesWebClientProvider",
|
|
46
50
|
summary: "Exports workspaces-web client provider class."
|
|
@@ -227,9 +231,8 @@ export default Object.freeze({
|
|
|
227
231
|
mutations: {
|
|
228
232
|
dependencies: {
|
|
229
233
|
runtime: {
|
|
230
|
-
"@jskit-ai/workspaces-core": "0.1.
|
|
231
|
-
"@jskit-ai/users-web": "0.1.
|
|
232
|
-
"vuetify": "^4.0.0"
|
|
234
|
+
"@jskit-ai/workspaces-core": "0.1.43",
|
|
235
|
+
"@jskit-ai/users-web": "0.1.82"
|
|
233
236
|
},
|
|
234
237
|
dev: {}
|
|
235
238
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/workspaces-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,21 +10,22 @@
|
|
|
10
10
|
"./client/components/AccountSettingsInvitesSection": "./src/client/components/AccountSettingsInvitesSection.vue",
|
|
11
11
|
"./client/providers/WorkspacesWebClientProvider": "./src/client/providers/WorkspacesWebClientProvider.js",
|
|
12
12
|
"./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
|
|
13
|
+
"./client/components/WorkspaceSettingsClientElement": "./src/client/components/WorkspaceSettingsClientElement.vue",
|
|
13
14
|
"./client/composables/useWorkspaceRouteContext": "./src/client/composables/useWorkspaceRouteContext.js"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@tanstack/vue-query": "5.92.12",
|
|
17
17
|
"@mdi/js": "^7.4.47",
|
|
18
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
19
|
-
"@jskit-ai/kernel": "0.1.
|
|
20
|
-
"@jskit-ai/shell-web": "0.1.
|
|
21
|
-
"@jskit-ai/users-core": "0.1.
|
|
22
|
-
"@jskit-ai/users-web": "0.1.
|
|
23
|
-
"@jskit-ai/workspaces-core": "0.1.
|
|
24
|
-
"vuetify": "^4.0.0"
|
|
18
|
+
"@jskit-ai/http-runtime": "0.1.66",
|
|
19
|
+
"@jskit-ai/kernel": "0.1.67",
|
|
20
|
+
"@jskit-ai/shell-web": "0.1.66",
|
|
21
|
+
"@jskit-ai/users-core": "0.1.77",
|
|
22
|
+
"@jskit-ai/users-web": "0.1.82",
|
|
23
|
+
"@jskit-ai/workspaces-core": "0.1.43"
|
|
25
24
|
},
|
|
26
25
|
"peerDependencies": {
|
|
26
|
+
"@tanstack/vue-query": "^5.90.5",
|
|
27
27
|
"vue": "^3.5.13",
|
|
28
|
-
"vue-router": "^5.0.4"
|
|
28
|
+
"vue-router": "^5.0.4",
|
|
29
|
+
"vuetify": "^4.0.0"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -5,13 +5,13 @@ const invites = useAccountSettingsInvitesSectionRuntime();
|
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
|
-
<v-
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
</
|
|
13
|
-
|
|
14
|
-
<
|
|
8
|
+
<v-sheet rounded="lg" border class="account-invites-section">
|
|
9
|
+
<header class="account-invites-section__header">
|
|
10
|
+
<h2 class="account-invites-section__title">Invitations</h2>
|
|
11
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Accept or refuse workspace invitations.</p>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<div class="account-invites-section__body">
|
|
15
15
|
<template v-if="invites.isLoading.value">
|
|
16
16
|
<v-skeleton-loader type="text@2, list-item-two-line@3" />
|
|
17
17
|
</template>
|
|
@@ -67,6 +67,33 @@ const invites = useAccountSettingsInvitesSectionRuntime();
|
|
|
67
67
|
</v-list-item>
|
|
68
68
|
</v-list>
|
|
69
69
|
</template>
|
|
70
|
-
</
|
|
71
|
-
</v-
|
|
70
|
+
</div>
|
|
71
|
+
</v-sheet>
|
|
72
72
|
</template>
|
|
73
|
+
|
|
74
|
+
<style scoped>
|
|
75
|
+
.account-invites-section {
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.account-invites-section__header {
|
|
80
|
+
padding: 1rem 1rem 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.account-invites-section__title {
|
|
84
|
+
font-size: 1rem;
|
|
85
|
+
font-weight: 650;
|
|
86
|
+
line-height: 1.2;
|
|
87
|
+
margin: 0 0 0.25rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.account-invites-section__body {
|
|
91
|
+
padding: 1rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (max-width: 640px) {
|
|
95
|
+
.account-invites-section__body :deep(.v-btn) {
|
|
96
|
+
min-height: 48px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section class="workspace-members-page">
|
|
3
|
-
<
|
|
4
|
-
{{ loadError }}
|
|
5
|
-
|
|
3
|
+
<div v-if="loadError" class="workspace-members-page__state">
|
|
4
|
+
<p class="text-body-2 text-medium-emphasis mb-4">{{ loadError }}</p>
|
|
5
|
+
<v-btn
|
|
6
|
+
v-if="canRetryLoad"
|
|
7
|
+
color="primary"
|
|
8
|
+
variant="tonal"
|
|
9
|
+
:loading="isRetryingLoad"
|
|
10
|
+
@click="refreshLoad"
|
|
11
|
+
>
|
|
12
|
+
Retry
|
|
13
|
+
</v-btn>
|
|
14
|
+
</div>
|
|
6
15
|
|
|
7
16
|
<MembersAdminClientElement
|
|
8
17
|
v-else
|
|
@@ -38,7 +47,6 @@ import { useView } from "@jskit-ai/users-web/client/composables/useView";
|
|
|
38
47
|
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
39
48
|
import { useAccess } from "@jskit-ai/users-web/client/composables/useAccess";
|
|
40
49
|
import { useUiFeedback } from "@jskit-ai/users-web/client/composables/runtime/useUiFeedback";
|
|
41
|
-
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
42
50
|
import { useWorkspaceRouteContext } from "../composables/useWorkspaceRouteContext.js";
|
|
43
51
|
import { createWorkspaceRealtimeMatcher } from "../support/realtimeWorkspace.js";
|
|
44
52
|
import { buildWorkspaceQueryKey } from "../support/workspaceQueryKeys.js";
|
|
@@ -75,7 +83,6 @@ const removeMemberUserId = ref("");
|
|
|
75
83
|
|
|
76
84
|
const { route, currentSurfaceId, workspaceSlugFromRoute } = useWorkspaceRouteContext();
|
|
77
85
|
const usersPaths = usePaths();
|
|
78
|
-
const errorRuntime = useShellWebErrorRuntime();
|
|
79
86
|
const OWNERSHIP_WORKSPACE = ROUTE_VISIBILITY_WORKSPACE;
|
|
80
87
|
|
|
81
88
|
const hasRouteWorkspaceSlug = computed(() => Boolean(workspaceSlugFromRoute.value));
|
|
@@ -423,27 +430,44 @@ const loadError = computed(() => {
|
|
|
423
430
|
return "Workspace slug is required in the URL.";
|
|
424
431
|
}
|
|
425
432
|
|
|
426
|
-
return
|
|
433
|
+
return (
|
|
434
|
+
access.bootstrapError.value ||
|
|
435
|
+
workspaceSettingsView.loadError ||
|
|
436
|
+
workspaceRolesView.loadError ||
|
|
437
|
+
workspaceMembersList.loadError ||
|
|
438
|
+
workspaceInvitesList.loadError ||
|
|
439
|
+
""
|
|
440
|
+
);
|
|
427
441
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
source: "users-web.workspace-members-view",
|
|
437
|
-
severity: "error",
|
|
438
|
-
channel: "banner",
|
|
439
|
-
message: String(nextLoadError || "Unable to load workspace members."),
|
|
440
|
-
dedupeKey: `users-web.workspace-members-view:bootstrap:${nextLoadError}`,
|
|
441
|
-
dedupeWindowMs: 3000
|
|
442
|
-
});
|
|
443
|
-
},
|
|
444
|
-
{ immediate: true }
|
|
442
|
+
const canRetryLoad = computed(() => Boolean(hasRouteWorkspaceSlug.value && !access.bootstrapError.value));
|
|
443
|
+
const isRetryingLoad = computed(() =>
|
|
444
|
+
Boolean(
|
|
445
|
+
workspaceSettingsView.isFetching ||
|
|
446
|
+
workspaceRolesView.isFetching ||
|
|
447
|
+
workspaceMembersList.isFetching ||
|
|
448
|
+
workspaceInvitesList.isFetching
|
|
449
|
+
)
|
|
445
450
|
);
|
|
446
451
|
|
|
452
|
+
async function refreshLoad() {
|
|
453
|
+
if (!canRetryLoad.value) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const jobs = [];
|
|
458
|
+
if (canInviteMembers.value) {
|
|
459
|
+
jobs.push(workspaceSettingsView.refresh());
|
|
460
|
+
}
|
|
461
|
+
if (canInviteMembers.value || canViewMembers.value) {
|
|
462
|
+
jobs.push(workspaceRolesView.refresh());
|
|
463
|
+
}
|
|
464
|
+
if (canViewMembers.value) {
|
|
465
|
+
jobs.push(workspaceMembersList.reload());
|
|
466
|
+
jobs.push(workspaceInvitesList.reload());
|
|
467
|
+
}
|
|
468
|
+
await Promise.all(jobs);
|
|
469
|
+
}
|
|
470
|
+
|
|
447
471
|
const actions = Object.freeze({
|
|
448
472
|
submitInvite,
|
|
449
473
|
submitRevokeInvite,
|
|
@@ -522,17 +546,6 @@ watch(
|
|
|
522
546
|
{ immediate: true }
|
|
523
547
|
);
|
|
524
548
|
|
|
525
|
-
watch(
|
|
526
|
-
() => workspaceMembersList.loadError,
|
|
527
|
-
(nextLoadError) => {
|
|
528
|
-
if (!nextLoadError) {
|
|
529
|
-
membersFeedback.clear();
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
membersFeedback.error(null, nextLoadError);
|
|
533
|
-
}
|
|
534
|
-
);
|
|
535
|
-
|
|
536
549
|
watch(
|
|
537
550
|
() => workspaceInvitesList.items,
|
|
538
551
|
(nextInvites) => {
|
|
@@ -553,17 +566,6 @@ watch(
|
|
|
553
566
|
{ immediate: true }
|
|
554
567
|
);
|
|
555
568
|
|
|
556
|
-
watch(
|
|
557
|
-
() => workspaceInvitesList.loadError,
|
|
558
|
-
(nextLoadError) => {
|
|
559
|
-
if (!nextLoadError) {
|
|
560
|
-
teamFeedback.clear();
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
teamFeedback.error(null, nextLoadError);
|
|
564
|
-
}
|
|
565
|
-
);
|
|
566
|
-
|
|
567
569
|
watch(
|
|
568
570
|
() => route.fullPath,
|
|
569
571
|
() => {
|
|
@@ -671,3 +673,18 @@ async function submitRemoveMember(member) {
|
|
|
671
673
|
}
|
|
672
674
|
}
|
|
673
675
|
</script>
|
|
676
|
+
|
|
677
|
+
<style scoped>
|
|
678
|
+
.workspace-members-page__state {
|
|
679
|
+
margin-inline: auto;
|
|
680
|
+
max-width: 30rem;
|
|
681
|
+
padding: 2rem 1rem;
|
|
682
|
+
text-align: center;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
@media (max-width: 640px) {
|
|
686
|
+
.workspace-members-page__state :deep(.v-btn) {
|
|
687
|
+
min-height: 48px;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
</style>
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
</
|
|
7
|
-
|
|
8
|
-
<
|
|
2
|
+
<v-sheet rounded="lg" border class="workspace-profile-panel">
|
|
3
|
+
<header class="workspace-profile-panel__header">
|
|
4
|
+
<h2 class="workspace-profile-panel__title">Workspace profile</h2>
|
|
5
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Name and avatar used across the workspace.</p>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<div class="workspace-profile-panel__body">
|
|
9
9
|
<template v-if="showSkeleton">
|
|
10
10
|
<v-skeleton-loader type="text@2, list-item-two-line@3, button" />
|
|
11
11
|
</template>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
{{ addEdit.loadError }}
|
|
15
|
-
|
|
13
|
+
<div v-else-if="addEdit.loadError" class="workspace-profile-panel__state">
|
|
14
|
+
<p class="text-body-2 text-medium-emphasis mb-4">{{ addEdit.loadError }}</p>
|
|
15
|
+
<v-btn
|
|
16
|
+
v-if="addEdit.canRetryLoad"
|
|
17
|
+
color="primary"
|
|
18
|
+
variant="tonal"
|
|
19
|
+
:loading="addEdit.isFetching"
|
|
20
|
+
@click="addEdit.refresh"
|
|
21
|
+
>
|
|
22
|
+
Retry
|
|
23
|
+
</v-btn>
|
|
24
|
+
</div>
|
|
16
25
|
|
|
17
26
|
<p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
|
|
18
27
|
You do not have permission to view workspace profile.
|
|
@@ -62,8 +71,8 @@
|
|
|
62
71
|
</v-row>
|
|
63
72
|
</v-form>
|
|
64
73
|
</template>
|
|
65
|
-
</
|
|
66
|
-
</v-
|
|
74
|
+
</div>
|
|
75
|
+
</v-sheet>
|
|
67
76
|
</template>
|
|
68
77
|
|
|
69
78
|
<script setup>
|
|
@@ -110,3 +119,37 @@ const addEdit = useAddEdit({
|
|
|
110
119
|
|
|
111
120
|
const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
|
|
112
121
|
</script>
|
|
122
|
+
|
|
123
|
+
<style scoped>
|
|
124
|
+
.workspace-profile-panel {
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.workspace-profile-panel__header {
|
|
129
|
+
padding: 1rem 1rem 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.workspace-profile-panel__title {
|
|
133
|
+
font-size: 1rem;
|
|
134
|
+
font-weight: 650;
|
|
135
|
+
line-height: 1.2;
|
|
136
|
+
margin: 0 0 0.25rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.workspace-profile-panel__body {
|
|
140
|
+
padding: 1rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.workspace-profile-panel__state {
|
|
144
|
+
margin-inline: auto;
|
|
145
|
+
max-width: 30rem;
|
|
146
|
+
padding: 1.5rem 1rem;
|
|
147
|
+
text-align: center;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@media (max-width: 640px) {
|
|
151
|
+
.workspace-profile-panel__body :deep(.v-btn) {
|
|
152
|
+
min-height: 48px;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="workspace-settings-client-element">
|
|
2
|
+
<section class="workspace-settings-client-element d-flex flex-column ga-4">
|
|
3
3
|
<WorkspaceProfileClientElement @saved="handleFormSaved" />
|
|
4
4
|
<WorkspaceSettingsFieldsClientElement @saved="handleFormSaved" />
|
|
5
5
|
</section>
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
</
|
|
7
|
-
|
|
8
|
-
<
|
|
2
|
+
<v-sheet rounded="lg" border class="workspace-settings-panel">
|
|
3
|
+
<header class="workspace-settings-panel__header">
|
|
4
|
+
<h2 class="workspace-settings-panel__title">Workspace settings</h2>
|
|
5
|
+
<p class="text-body-2 text-medium-emphasis mb-0">These values apply to everyone in this workspace.</p>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<div class="workspace-settings-panel__body">
|
|
9
9
|
<template v-if="showSkeleton">
|
|
10
10
|
<v-skeleton-loader type="text@2, list-item-two-line@4, button" />
|
|
11
11
|
</template>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
{{ addEdit.loadError }}
|
|
15
|
-
|
|
13
|
+
<div v-else-if="addEdit.loadError" class="workspace-settings-panel__state">
|
|
14
|
+
<p class="text-body-2 text-medium-emphasis mb-4">{{ addEdit.loadError }}</p>
|
|
15
|
+
<v-btn
|
|
16
|
+
v-if="addEdit.canRetryLoad"
|
|
17
|
+
color="primary"
|
|
18
|
+
variant="tonal"
|
|
19
|
+
:loading="addEdit.isFetching"
|
|
20
|
+
@click="addEdit.refresh"
|
|
21
|
+
>
|
|
22
|
+
Retry
|
|
23
|
+
</v-btn>
|
|
24
|
+
</div>
|
|
16
25
|
|
|
17
26
|
<p v-else-if="!addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
|
|
18
27
|
You do not have permission to view workspace settings.
|
|
@@ -162,8 +171,8 @@
|
|
|
162
171
|
</v-row>
|
|
163
172
|
</v-form>
|
|
164
173
|
</template>
|
|
165
|
-
</
|
|
166
|
-
</v-
|
|
174
|
+
</div>
|
|
175
|
+
</v-sheet>
|
|
167
176
|
</template>
|
|
168
177
|
|
|
169
178
|
<script setup>
|
|
@@ -259,3 +268,37 @@ const addEdit = useAddEdit({
|
|
|
259
268
|
|
|
260
269
|
const showSkeleton = computed(() => Boolean(addEdit.isInitialLoading));
|
|
261
270
|
</script>
|
|
271
|
+
|
|
272
|
+
<style scoped>
|
|
273
|
+
.workspace-settings-panel {
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.workspace-settings-panel__header {
|
|
278
|
+
padding: 1rem 1rem 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.workspace-settings-panel__title {
|
|
282
|
+
font-size: 1rem;
|
|
283
|
+
font-weight: 650;
|
|
284
|
+
line-height: 1.2;
|
|
285
|
+
margin: 0 0 0.25rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.workspace-settings-panel__body {
|
|
289
|
+
padding: 1rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.workspace-settings-panel__state {
|
|
293
|
+
margin-inline: auto;
|
|
294
|
+
max-width: 30rem;
|
|
295
|
+
padding: 1.5rem 1rem;
|
|
296
|
+
text-align: center;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@media (max-width: 640px) {
|
|
300
|
+
.workspace-settings-panel__body :deep(.v-btn) {
|
|
301
|
+
min-height: 48px;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
</style>
|
|
@@ -163,13 +163,14 @@ const workspaceInvitesEnabled = computed(() => bootstrapModel.workspaceInvitesEn
|
|
|
163
163
|
|
|
164
164
|
const isBootstrapping = computed(() => Boolean(bootstrapView.isLoading));
|
|
165
165
|
const isRefreshingBootstrap = computed(() => Boolean(bootstrapView.isRefetching));
|
|
166
|
+
const bootstrapLoadError = computed(() => String(bootstrapView.loadError || "").trim());
|
|
166
167
|
const canCreateWorkspace = computed(() => bootstrapModel.workspaceAllowSelfCreate === true);
|
|
167
168
|
const isCreatingWorkspace = computed(() => Boolean(createWorkspaceCommand.isRunning.value));
|
|
168
169
|
|
|
169
170
|
function reportFeedback({
|
|
170
171
|
message,
|
|
171
172
|
severity = "error",
|
|
172
|
-
channel = "
|
|
173
|
+
channel = "",
|
|
173
174
|
dedupeKey = ""
|
|
174
175
|
} = {}) {
|
|
175
176
|
const normalizedMessage = String(message || "").trim();
|
|
@@ -180,6 +181,7 @@ function reportFeedback({
|
|
|
180
181
|
errorRuntime.report({
|
|
181
182
|
source: "users-web.workspaces-view",
|
|
182
183
|
message: normalizedMessage,
|
|
184
|
+
intent: "action-feedback",
|
|
183
185
|
severity,
|
|
184
186
|
channel,
|
|
185
187
|
dedupeKey: dedupeKey || `users-web.workspaces-view:${severity}:${normalizedMessage}`,
|
|
@@ -233,7 +235,6 @@ async function openWorkspace(workspaceSlug) {
|
|
|
233
235
|
reportFeedback({
|
|
234
236
|
message: "Workspace surface is not configured.",
|
|
235
237
|
severity: "error",
|
|
236
|
-
channel: "banner",
|
|
237
238
|
dedupeKey: "users-web.workspaces-view:workspace-surface-missing"
|
|
238
239
|
});
|
|
239
240
|
return;
|
|
@@ -258,7 +259,6 @@ async function openWorkspace(workspaceSlug) {
|
|
|
258
259
|
reportFeedback({
|
|
259
260
|
message: String(error?.message || "Unable to open workspace."),
|
|
260
261
|
severity: "error",
|
|
261
|
-
channel: "banner",
|
|
262
262
|
dedupeKey: `users-web.workspaces-view:open-workspace:${normalizedSlug}`
|
|
263
263
|
});
|
|
264
264
|
} finally {
|
|
@@ -301,7 +301,6 @@ async function respondToInvite(invite, decision) {
|
|
|
301
301
|
reportFeedback({
|
|
302
302
|
message: "Invitation refused.",
|
|
303
303
|
severity: "success",
|
|
304
|
-
channel: "snackbar",
|
|
305
304
|
dedupeKey: `users-web.workspaces-view:invite-refused:${token}`
|
|
306
305
|
});
|
|
307
306
|
} catch (error) {
|
|
@@ -310,7 +309,6 @@ async function respondToInvite(invite, decision) {
|
|
|
310
309
|
error?.message || (normalizedDecision === "accept" ? "Unable to accept invite." : "Unable to refuse invite.")
|
|
311
310
|
),
|
|
312
311
|
severity: "error",
|
|
313
|
-
channel: "banner",
|
|
314
312
|
dedupeKey: `users-web.workspaces-view:invite-${normalizedDecision}:${token}`
|
|
315
313
|
});
|
|
316
314
|
} finally {
|
|
@@ -341,7 +339,6 @@ async function createWorkspace() {
|
|
|
341
339
|
reportFeedback({
|
|
342
340
|
message: "Workspace name is required.",
|
|
343
341
|
severity: "error",
|
|
344
|
-
channel: "banner",
|
|
345
342
|
dedupeKey: "users-web.workspaces-view:create-workspace-name-required"
|
|
346
343
|
});
|
|
347
344
|
return;
|
|
@@ -361,12 +358,15 @@ async function createWorkspace() {
|
|
|
361
358
|
reportFeedback({
|
|
362
359
|
message: String(error?.message || "Unable to create workspace."),
|
|
363
360
|
severity: "error",
|
|
364
|
-
channel: "banner",
|
|
365
361
|
dedupeKey: "users-web.workspaces-view:create-workspace-error"
|
|
366
362
|
});
|
|
367
363
|
}
|
|
368
364
|
}
|
|
369
365
|
|
|
366
|
+
async function refreshBootstrap() {
|
|
367
|
+
return bootstrapView.refresh();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
370
|
watch(
|
|
371
371
|
() => bootstrapView.resource.data.value,
|
|
372
372
|
async (payload) => {
|
|
@@ -401,20 +401,31 @@ watch(
|
|
|
401
401
|
<template>
|
|
402
402
|
<section class="workspaces-view py-6">
|
|
403
403
|
<v-container class="mx-auto" max-width="860">
|
|
404
|
-
<v-
|
|
405
|
-
<
|
|
406
|
-
<
|
|
407
|
-
<
|
|
408
|
-
</
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<v-card-text class="pt-4">
|
|
404
|
+
<v-sheet rounded="lg" border class="workspaces-view__panel">
|
|
405
|
+
<header class="workspaces-view__header">
|
|
406
|
+
<h1 class="workspaces-view__title">You are logged in</h1>
|
|
407
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Select a workspace or respond to invitations.</p>
|
|
408
|
+
</header>
|
|
409
|
+
|
|
410
|
+
<div class="workspaces-view__body">
|
|
412
411
|
<v-progress-linear v-if="!isBootstrapping && isRefreshingBootstrap" indeterminate class="mb-4" />
|
|
413
412
|
<v-row>
|
|
414
413
|
<v-col cols="12" :md="workspaceInvitesEnabled ? 6 : 12">
|
|
415
414
|
<template v-if="isBootstrapping">
|
|
416
415
|
<v-skeleton-loader type="text, list-item-avatar-two-line@3" />
|
|
417
416
|
</template>
|
|
417
|
+
<div v-else-if="bootstrapLoadError" class="workspaces-view__state">
|
|
418
|
+
<h2 class="text-h6 mb-2">Unable to load workspaces</h2>
|
|
419
|
+
<p class="text-body-2 text-medium-emphasis mb-4">{{ bootstrapLoadError }}</p>
|
|
420
|
+
<v-btn
|
|
421
|
+
color="primary"
|
|
422
|
+
variant="tonal"
|
|
423
|
+
:loading="isRefreshingBootstrap"
|
|
424
|
+
@click="refreshBootstrap"
|
|
425
|
+
>
|
|
426
|
+
Retry
|
|
427
|
+
</v-btn>
|
|
428
|
+
</div>
|
|
418
429
|
<template v-else>
|
|
419
430
|
<div class="text-subtitle-2 mb-2">Your workspaces</div>
|
|
420
431
|
<template v-if="workspaceItems.length === 0">
|
|
@@ -539,8 +550,47 @@ watch(
|
|
|
539
550
|
</template>
|
|
540
551
|
</v-col>
|
|
541
552
|
</v-row>
|
|
542
|
-
</
|
|
543
|
-
</v-
|
|
553
|
+
</div>
|
|
554
|
+
</v-sheet>
|
|
544
555
|
</v-container>
|
|
545
556
|
</section>
|
|
546
557
|
</template>
|
|
558
|
+
|
|
559
|
+
<style scoped>
|
|
560
|
+
.workspaces-view__panel {
|
|
561
|
+
overflow: hidden;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.workspaces-view__header {
|
|
565
|
+
padding: 1rem 1rem 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.workspaces-view__title {
|
|
569
|
+
font-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
570
|
+
font-weight: 650;
|
|
571
|
+
letter-spacing: -0.02em;
|
|
572
|
+
line-height: 1.15;
|
|
573
|
+
margin: 0 0 0.35rem;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.workspaces-view__body {
|
|
577
|
+
padding: 1rem;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.workspaces-view__state {
|
|
581
|
+
margin-inline: auto;
|
|
582
|
+
max-width: 30rem;
|
|
583
|
+
padding: 2rem 1rem;
|
|
584
|
+
text-align: center;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@media (max-width: 640px) {
|
|
588
|
+
.workspaces-view {
|
|
589
|
+
padding-block: 0.75rem !important;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.workspaces-view__body :deep(.v-btn) {
|
|
593
|
+
min-height: 48px;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
</style>
|
|
@@ -18,17 +18,36 @@ const normalizedMessage = computed(() => String(props.message || "").trim() || "
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<template>
|
|
21
|
-
<v-
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
<p class="text-medium-emphasis mb-0">{{ normalizedMessage }}</p>
|
|
32
|
-
</v-card-text>
|
|
33
|
-
</v-card>
|
|
21
|
+
<v-sheet rounded="lg" border class="workspace-unavailable-panel">
|
|
22
|
+
<div class="workspace-unavailable-panel__header">
|
|
23
|
+
<v-icon :icon="mdiAlertCircleOutline" color="error" />
|
|
24
|
+
<div>
|
|
25
|
+
<h1 class="workspace-unavailable-panel__title">Unavailable</h1>
|
|
26
|
+
<p class="text-body-2 text-medium-emphasis mb-0">{{ normalizedSurfaceLabel }} surface.</p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<p class="text-body-2 text-medium-emphasis mb-0">{{ normalizedMessage }}</p>
|
|
30
|
+
</v-sheet>
|
|
34
31
|
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.workspace-unavailable-panel {
|
|
35
|
+
display: grid;
|
|
36
|
+
gap: 1rem;
|
|
37
|
+
padding: 1rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.workspace-unavailable-panel__header {
|
|
41
|
+
align-items: flex-start;
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: 0.75rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.workspace-unavailable-panel__title {
|
|
47
|
+
font-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
48
|
+
font-weight: 650;
|
|
49
|
+
letter-spacing: -0.02em;
|
|
50
|
+
line-height: 1.15;
|
|
51
|
+
margin: 0 0 0.25rem;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
// import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
|
|
4
|
-
// definePage({
|
|
5
|
-
// redirect: redirectToChild("your_child_segment")
|
|
6
|
-
// });
|
|
2
|
+
import WorkspaceSettingsClientElement from "@jskit-ai/workspaces-web/client/components/WorkspaceSettingsClientElement";
|
|
7
3
|
</script>
|
|
8
4
|
|
|
9
5
|
<template>
|
|
10
|
-
<
|
|
6
|
+
<WorkspaceSettingsClientElement />
|
|
11
7
|
</template>
|
|
@@ -5,25 +5,66 @@ import { RouterView } from "vue-router";
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<section class="settings-shell d-flex flex-column ga-4">
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</v-col>
|
|
25
|
-
</v-row>
|
|
26
|
-
</v-card-text>
|
|
27
|
-
</v-card>
|
|
8
|
+
<header>
|
|
9
|
+
<p class="text-overline text-medium-emphasis mb-1">Settings</p>
|
|
10
|
+
<h1 class="settings-shell__title">Workspace settings</h1>
|
|
11
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Configure the current workspace and its members.</p>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<v-sheet rounded="lg" border class="settings-shell__panel">
|
|
15
|
+
<nav class="settings-shell__nav" aria-label="Workspace settings sections">
|
|
16
|
+
<v-list nav density="comfortable" class="settings-shell__nav-list">
|
|
17
|
+
<ShellOutlet target="admin-settings:primary-menu" />
|
|
18
|
+
</v-list>
|
|
19
|
+
</nav>
|
|
20
|
+
<main class="settings-shell__content">
|
|
21
|
+
<RouterView />
|
|
22
|
+
</main>
|
|
23
|
+
</v-sheet>
|
|
28
24
|
</section>
|
|
29
25
|
</template>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
.settings-shell__title {
|
|
29
|
+
font-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
30
|
+
font-weight: 650;
|
|
31
|
+
letter-spacing: -0.02em;
|
|
32
|
+
line-height: 1.15;
|
|
33
|
+
margin: 0 0 0.35rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.settings-shell__panel {
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: 1rem;
|
|
39
|
+
grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
|
|
40
|
+
padding: 1rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.settings-shell__nav-list {
|
|
44
|
+
padding: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.settings-shell__content {
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@media (max-width: 960px) {
|
|
52
|
+
.settings-shell__panel {
|
|
53
|
+
grid-template-columns: 1fr;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.settings-shell__nav {
|
|
57
|
+
overflow-x: auto;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.settings-shell__nav-list {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: 0.5rem;
|
|
63
|
+
min-width: max-content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.settings-shell__nav-list :deep(.v-list-item) {
|
|
67
|
+
min-height: 48px;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -11,19 +11,67 @@ const { workspaceUnavailable, workspaceUnavailableMessage } = useWorkspaceNotFou
|
|
|
11
11
|
:message="workspaceUnavailableMessage"
|
|
12
12
|
surface-label="Admin"
|
|
13
13
|
/>
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
<section v-else class="workspace-admin-screen d-flex flex-column ga-4">
|
|
15
|
+
<header class="workspace-admin-screen__header">
|
|
16
|
+
<div>
|
|
17
|
+
<p class="text-overline text-medium-emphasis mb-1">Admin</p>
|
|
18
|
+
<h1 class="workspace-admin-screen__title">Workspace Admin</h1>
|
|
19
|
+
<p class="text-body-2 text-medium-emphasis mb-0">Manage members and workspace settings.</p>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="workspace-admin-screen__actions">
|
|
22
|
+
<v-btn color="primary" variant="flat" to="./members">Members</v-btn>
|
|
23
|
+
<v-btn color="primary" variant="tonal" to="./workspace/settings">Settings</v-btn>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<v-sheet rounded="lg" border class="workspace-admin-screen__panel">
|
|
28
|
+
<h2 class="text-h6 mb-2">Admin tasks</h2>
|
|
29
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
30
|
+
Review workspace access, members, and operational settings from this surface.
|
|
26
31
|
</p>
|
|
27
|
-
</v-
|
|
28
|
-
</
|
|
32
|
+
</v-sheet>
|
|
33
|
+
</section>
|
|
29
34
|
</template>
|
|
35
|
+
|
|
36
|
+
<style scoped>
|
|
37
|
+
.workspace-admin-screen__header {
|
|
38
|
+
align-items: flex-start;
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 1rem;
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.workspace-admin-screen__title {
|
|
45
|
+
font-size: clamp(1.5rem, 2.5vw, 2.25rem);
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
letter-spacing: -0.03em;
|
|
48
|
+
line-height: 1.1;
|
|
49
|
+
margin: 0 0 0.4rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.workspace-admin-screen__actions {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
gap: 0.5rem;
|
|
56
|
+
justify-content: flex-end;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.workspace-admin-screen__panel {
|
|
60
|
+
padding: 1rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (max-width: 640px) {
|
|
64
|
+
.workspace-admin-screen__header {
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.workspace-admin-screen__actions {
|
|
69
|
+
width: 100%;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.workspace-admin-screen__actions :deep(.v-btn) {
|
|
73
|
+
min-height: 48px;
|
|
74
|
+
flex: 1 1 10rem;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -11,17 +11,36 @@ const { workspaceUnavailable, workspaceUnavailableMessage } = useWorkspaceNotFou
|
|
|
11
11
|
:message="workspaceUnavailableMessage"
|
|
12
12
|
surface-label="App"
|
|
13
13
|
/>
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
<section v-else class="workspace-home-screen d-flex flex-column ga-4">
|
|
15
|
+
<header>
|
|
16
|
+
<p class="text-overline text-medium-emphasis mb-1">Workspace</p>
|
|
17
|
+
<h1 class="workspace-home-screen__title">Workspace Home</h1>
|
|
18
|
+
<p class="text-body-2 text-medium-emphasis mb-0">The current workspace is active.</p>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<v-sheet rounded="lg" border class="workspace-home-screen__panel">
|
|
22
|
+
<h2 class="text-h6 mb-2">No workspace activity yet</h2>
|
|
23
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
24
|
+
Activity from workspace workflows will appear here.
|
|
25
|
+
</p>
|
|
26
|
+
</v-sheet>
|
|
27
|
+
</section>
|
|
27
28
|
</template>
|
|
29
|
+
|
|
30
|
+
<style scoped>
|
|
31
|
+
.workspace-home-screen__title {
|
|
32
|
+
font-size: clamp(1.5rem, 2.5vw, 2.25rem);
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
letter-spacing: -0.03em;
|
|
35
|
+
line-height: 1.1;
|
|
36
|
+
margin: 0 0 0.4rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.workspace-home-screen__panel {
|
|
40
|
+
margin-inline: auto;
|
|
41
|
+
max-width: 34rem;
|
|
42
|
+
padding: 2rem 1.25rem;
|
|
43
|
+
text-align: center;
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -16,6 +16,7 @@ test("workspaces-web exports are explicit and aligned with production usage", ()
|
|
|
16
16
|
requiredExports: [
|
|
17
17
|
"./client",
|
|
18
18
|
"./client/components/AccountSettingsInvitesSection",
|
|
19
|
+
"./client/components/WorkspaceSettingsClientElement",
|
|
19
20
|
"./client/providers/WorkspacesWebClientProvider",
|
|
20
21
|
"./client/composables/useWorkspaceRouteContext"
|
|
21
22
|
]
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
6
7
|
import descriptor from "../package.descriptor.mjs";
|
|
7
8
|
|
|
8
9
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -56,8 +57,25 @@ test("workspaces-web admin settings template exposes surface-derived settings ou
|
|
|
56
57
|
"utf8"
|
|
57
58
|
);
|
|
58
59
|
|
|
60
|
+
assertGeneratedUiSourceContract(source, {
|
|
61
|
+
forbidCardShell: true,
|
|
62
|
+
sourceName: "admin/workspace/settings.vue",
|
|
63
|
+
requiredPatterns: [
|
|
64
|
+
{
|
|
65
|
+
id: "admin-settings-outlet",
|
|
66
|
+
pattern: /target="admin-settings:primary-menu"/,
|
|
67
|
+
message: "Admin settings shell needs the semantic settings outlet host."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "admin-settings-router-view",
|
|
71
|
+
pattern: /<RouterView \/>/,
|
|
72
|
+
message: "Admin settings shell needs to host child settings routes."
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
});
|
|
59
76
|
assert.match(source, /target="admin-settings:primary-menu"/);
|
|
60
77
|
assert.doesNotMatch(source, /default-link-component-token/);
|
|
78
|
+
assert.doesNotMatch(source, /<v-card\b/);
|
|
61
79
|
assert.match(source, /<RouterView \/>/);
|
|
62
80
|
});
|
|
63
81
|
|
|
@@ -80,6 +98,38 @@ test("workspaces-web installs an app-owned account invites section wrapper", asy
|
|
|
80
98
|
});
|
|
81
99
|
});
|
|
82
100
|
|
|
101
|
+
test("workspaces-web settings components use direct panels instead of card scaffolds", async () => {
|
|
102
|
+
for (const relativePath of [
|
|
103
|
+
path.join("templates", "src", "components", "WorkspaceNotFoundCard.vue"),
|
|
104
|
+
path.join("src", "client", "components", "WorkspaceProfileClientElement.vue"),
|
|
105
|
+
path.join("src", "client", "components", "WorkspaceSettingsFieldsClientElement.vue"),
|
|
106
|
+
path.join("src", "client", "components", "WorkspacesClientElement.vue"),
|
|
107
|
+
path.join("src", "client", "components", "AccountSettingsInvitesSection.vue")
|
|
108
|
+
]) {
|
|
109
|
+
const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
|
|
110
|
+
|
|
111
|
+
assertGeneratedUiSourceContract(source, {
|
|
112
|
+
forbidCardShell: true,
|
|
113
|
+
sourceName: relativePath
|
|
114
|
+
});
|
|
115
|
+
assert.doesNotMatch(source, /<v-card\b|v-card-title|v-card-subtitle/);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("workspaces-web resource load states expose local retry actions", async () => {
|
|
120
|
+
const expectations = new Map([
|
|
121
|
+
["src/client/components/WorkspaceProfileClientElement.vue", /addEdit\.canRetryLoad[\s\S]*@click="addEdit\.refresh"/],
|
|
122
|
+
["src/client/components/WorkspaceSettingsFieldsClientElement.vue", /addEdit\.canRetryLoad[\s\S]*@click="addEdit\.refresh"/],
|
|
123
|
+
["src/client/components/WorkspacesClientElement.vue", /bootstrapLoadError[\s\S]*@click="refreshBootstrap"/],
|
|
124
|
+
["src/client/components/WorkspaceMembersClientElement.vue", /canRetryLoad[\s\S]*@click="refreshLoad"/]
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
for (const [relativePath, pattern] of expectations) {
|
|
128
|
+
const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
|
|
129
|
+
assert.match(source, pattern, `${relativePath} must expose a local retry action for load errors.`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
83
133
|
test("workspaces-web installs an account invites cue scaffold that reads placement runtime state", async () => {
|
|
84
134
|
const source = await readFile(
|
|
85
135
|
path.join(PACKAGE_DIR, "templates", "packages", "main", "src", "client", "components", "AccountPendingInvitesCue.vue"),
|
|
@@ -99,14 +149,60 @@ test("workspaces-web installs an account invites cue scaffold that reads placeme
|
|
|
99
149
|
});
|
|
100
150
|
});
|
|
101
151
|
|
|
102
|
-
test("workspaces-web admin settings index template
|
|
152
|
+
test("workspaces-web admin settings index template renders real workspace settings controls", async () => {
|
|
103
153
|
const source = await readFile(
|
|
104
154
|
path.join(PACKAGE_DIR, "templates", "src", "pages", "admin", "workspace", "settings", "index.vue"),
|
|
105
155
|
"utf8"
|
|
106
156
|
);
|
|
107
157
|
|
|
108
|
-
assert.match(source, /
|
|
109
|
-
assert.match(source,
|
|
158
|
+
assert.match(source, /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceSettingsClientElement/);
|
|
159
|
+
assert.match(source, /<WorkspaceSettingsClientElement \/>/);
|
|
160
|
+
assert.doesNotMatch(source, /your_child_segment|To redirect this settings shell/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("workspaces-web starter surfaces avoid instructional placeholder copy", async () => {
|
|
164
|
+
const appSource = await readFile(
|
|
165
|
+
path.join(PACKAGE_DIR, "templates", "src", "surfaces", "app", "index.vue"),
|
|
166
|
+
"utf8"
|
|
167
|
+
);
|
|
168
|
+
const adminSource = await readFile(
|
|
169
|
+
path.join(PACKAGE_DIR, "templates", "src", "surfaces", "admin", "index.vue"),
|
|
170
|
+
"utf8"
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
assertGeneratedUiSourceContract(appSource, {
|
|
174
|
+
forbidCardShell: true,
|
|
175
|
+
sourceName: "workspaces app surface",
|
|
176
|
+
requiredPatterns: [
|
|
177
|
+
{
|
|
178
|
+
id: "workspace-app-empty-state",
|
|
179
|
+
pattern: /No workspace activity yet/,
|
|
180
|
+
message: "Workspace app surface needs a product-shaped empty state."
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
assertGeneratedUiSourceContract(adminSource, {
|
|
185
|
+
forbidCardShell: true,
|
|
186
|
+
sourceName: "workspaces admin surface",
|
|
187
|
+
requiredPatterns: [
|
|
188
|
+
{
|
|
189
|
+
id: "workspace-admin-member-link",
|
|
190
|
+
pattern: /to="\.\/members"/,
|
|
191
|
+
message: "Workspace admin surface needs a direct members action."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "workspace-admin-settings-link",
|
|
195
|
+
pattern: /to="\.\/workspace\/settings"/,
|
|
196
|
+
message: "Workspace admin surface needs a direct settings action."
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
assert.match(appSource, /No workspace activity yet/);
|
|
201
|
+
assert.match(adminSource, /Manage members and workspace settings/);
|
|
202
|
+
assert.match(adminSource, /to="\.\/members"/);
|
|
203
|
+
assert.match(adminSource, /to="\.\/workspace\/settings"/);
|
|
204
|
+
assert.doesNotMatch(appSource, /Replace this page|Primary in-workspace surface/);
|
|
205
|
+
assert.doesNotMatch(adminSource, /Use this area|Privileged workspace workflows/);
|
|
110
206
|
});
|
|
111
207
|
|
|
112
208
|
test("workspaces-web descriptor metadata advertises admin settings outlets", () => {
|