@jskit-ai/workspaces-web 0.1.139-oldline.2 → 0.1.140

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 (40) hide show
  1. package/package.json +247 -9
  2. package/patterns/workspace-surfaces/PATTERN.md +63 -0
  3. package/src/client/account-settings/accountSettingsInvitesRuntime.js +2 -2
  4. package/src/client/account-settings/useAccountSettingsInvitesSectionRuntime.js +3 -3
  5. package/src/client/components/MembersAdminClientElement.vue +1 -1
  6. package/src/client/components/WorkspaceInviteLanding.vue +4 -4
  7. package/src/client/components/WorkspaceMembersClientElement.vue +6 -6
  8. package/src/client/components/{UsersWorkspaceMembersMenuItem.vue → WorkspaceMembersMenuItem.vue} +2 -2
  9. package/src/client/components/{UsersWorkspacePermissionMenuItem.vue → WorkspacePermissionMenuItem.vue} +3 -3
  10. package/src/client/components/{UsersProfileSurfaceSwitchMenuItem.vue → WorkspaceProfileSurfaceSwitchMenuItem.vue} +1 -1
  11. package/src/client/components/{UsersWorkspaceSelector.vue → WorkspaceSelector.vue} +1 -1
  12. package/src/client/components/{UsersWorkspaceSettingsMenuItem.vue → WorkspaceSettingsMenuItem.vue} +2 -2
  13. package/src/client/components/WorkspacesClientElement.vue +13 -13
  14. package/src/client/composables/useWorkspaceRouteContext.js +1 -1
  15. package/src/client/index.js +0 -5
  16. package/src/client/lib/surfaceAccessPolicy.js +4 -1
  17. package/src/client/providers/WorkspacesWebClientProvider.js +54 -50
  18. package/src/client/runtime/bootstrapPlacementRuntime.js +18 -32
  19. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +4 -4
  20. package/test/bootstrapPlacementRuntime.test.js +40 -125
  21. package/test/packageMetadataOwnership.test.js +19 -0
  22. package/test/settingsPlacementContract.test.js +87 -389
  23. package/test/{usersWorkspaceSelectorContract.test.js → workspaceSelectorContract.test.js} +3 -3
  24. package/package.descriptor.mjs +0 -492
  25. package/src/client/lib/httpClient.js +0 -10
  26. package/src/client/lib/permissions.js +0 -27
  27. package/test/packageDescriptorOwnership.test.js +0 -16
  28. /package/{templates → patterns/workspace-surfaces/example}/packages/main/src/client/components/AccountPendingInvitesCue.vue +0 -0
  29. /package/{templates → patterns/workspace-surfaces/example}/packages/main/src/client/components/AccountSettingsInvitesSection.vue +0 -0
  30. /package/{templates → patterns/workspace-surfaces/example}/src/components/WorkspaceNotFoundCard.vue +0 -0
  31. /package/{templates → patterns/workspace-surfaces/example}/src/composables/useWorkspaceNotFoundState.js +0 -0
  32. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/members/index.vue +0 -0
  33. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/workspace/settings/index.vue +0 -0
  34. /package/{templates → patterns/workspace-surfaces/example}/src/pages/admin/workspace/settings.vue +0 -0
  35. /package/{templates → patterns/workspace-surfaces/example}/src/pages/invite/[token].vue +0 -0
  36. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/admin/index.vue +0 -0
  37. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/admin/root.vue +0 -0
  38. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/app/index.vue +0 -0
  39. /package/{templates → patterns/workspace-surfaces/example}/src/surfaces/app/root.vue +0 -0
  40. /package/src/client/components/{UsersWorkspaceToolsWidget.vue → WorkspaceToolsWidget.vue} +0 -0
@@ -3,449 +3,147 @@ 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";
7
- import descriptor from "../package.descriptor.mjs";
6
+ import packageJson from "../package.json" with { type: "json" };
8
7
 
9
- const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
10
- const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const exampleRoot = path.join(packageRoot, "patterns", "workspace-surfaces", "example");
10
+ const placements = packageJson.jskit?.metadata?.ui?.placements;
11
11
 
12
- function readOutlets(target = "") {
13
- const outlets = descriptor?.metadata?.ui?.placements?.outlets;
14
- const normalizedTarget = String(target || "").trim();
15
- return Array.isArray(outlets)
16
- ? outlets.filter((entry) => String(entry?.target || "").trim() === normalizedTarget)
17
- : [];
12
+ function contribution(id) {
13
+ return placements?.contributions?.find((entry) => entry.id === id) || null;
18
14
  }
19
15
 
20
- function findTopology(id, owner = "") {
21
- const placements = descriptor?.metadata?.ui?.placements?.topology?.placements;
22
- const normalizedId = String(id || "").trim();
23
- const normalizedOwner = String(owner || "").trim();
24
- return Array.isArray(placements)
25
- ? placements.find((entry) => {
26
- const entryId = String(entry?.id || "").trim();
27
- const entryOwner = String(entry?.owner || "").trim();
28
- return entryId === normalizedId && entryOwner === normalizedOwner;
29
- }) || null
30
- : null;
31
- }
32
-
33
- function findContribution(id) {
34
- const contributions = descriptor?.metadata?.ui?.placements?.contributions;
35
- return Array.isArray(contributions)
36
- ? contributions.find((entry) => String(entry?.id || "").trim() === id) || null
37
- : null;
38
- }
39
-
40
- function findTextMutation(id) {
41
- const textMutations = descriptor?.mutations?.text;
42
- return Array.isArray(textMutations)
43
- ? textMutations.find((entry) => String(entry?.id || "").trim() === id) || null
44
- : null;
45
- }
46
-
47
- function findSourceMutation(id) {
48
- const sourceMutations = descriptor?.mutations?.source;
49
- return Array.isArray(sourceMutations)
50
- ? sourceMutations.find((entry) => String(entry?.id || "").trim() === id) || null
51
- : null;
52
- }
53
-
54
- function findFileMutation(id) {
55
- const fileMutations = descriptor?.mutations?.files;
56
- return Array.isArray(fileMutations)
57
- ? fileMutations.find((entry) => String(entry?.id || "").trim() === id) || null
58
- : null;
59
- }
60
-
61
- test("workspaces-web admin settings template exposes surface-derived settings outlets", async () => {
62
- const source = await readFile(
63
- path.join(PACKAGE_DIR, "templates", "src", "pages", "admin", "workspace", "settings.vue"),
16
+ test("workspace surfaces pattern includes settings, invitations, and app-owned integration examples", async () => {
17
+ const settings = await readFile(
18
+ path.join(exampleRoot, "src", "pages", "admin", "workspace", "settings.vue"),
64
19
  "utf8"
65
20
  );
66
-
67
- assertGeneratedUiSourceContract(source, {
68
- forbidCardShell: true,
69
- sourceName: "admin/workspace/settings.vue",
70
- requiredPatterns: [
71
- {
72
- id: "admin-settings-outlet",
73
- pattern: /target="admin-settings:primary-menu"/,
74
- message: "Admin settings shell needs the semantic settings outlet host."
75
- },
76
- {
77
- id: "admin-settings-router-view",
78
- pattern: /<RouterView \/>/,
79
- message: "Admin settings shell needs to host child settings routes."
80
- }
81
- ]
82
- });
83
- assert.match(source, /target="admin-settings:primary-menu"/);
84
- assert.doesNotMatch(source, /default-link-component-token/);
85
- assert.doesNotMatch(source, /<v-card\b/);
86
- assert.match(source, /<RouterView \/>/);
87
- });
88
-
89
- test("workspaces-web installs an app-owned account invites section wrapper", async () => {
90
- const source = await readFile(
91
- path.join(PACKAGE_DIR, "templates", "packages", "main", "src", "client", "components", "AccountSettingsInvitesSection.vue"),
21
+ const invites = await readFile(
22
+ path.join(exampleRoot, "packages", "main", "src", "client", "components", "AccountSettingsInvitesSection.vue"),
92
23
  "utf8"
93
24
  );
94
-
95
- assert.match(
96
- source,
97
- /@jskit-ai\/workspaces-web\/client\/components\/AccountSettingsInvitesSection/
98
- );
99
- assert.deepEqual(findFileMutation("users-web-main-component-account-settings-invites-section"), {
100
- from: "templates/packages/main/src/client/components/AccountSettingsInvitesSection.vue",
101
- to: "packages/main/src/client/components/AccountSettingsInvitesSection.vue",
102
- ownership: "app",
103
- reason: "Install app-owned account invites section scaffold for multihoming account settings.",
104
- category: "workspaces-web",
105
- id: "users-web-main-component-account-settings-invites-section"
106
- });
107
- });
108
-
109
- test("workspaces-web installs a public workspace invite route scaffold", async () => {
110
- const source = await readFile(
111
- path.join(PACKAGE_DIR, "templates", "src", "pages", "invite", "[token].vue"),
25
+ const invitationPage = await readFile(
26
+ path.join(exampleRoot, "src", "pages", "invite", "[token].vue"),
112
27
  "utf8"
113
28
  );
114
29
 
115
- assert.doesNotMatch(source, /\bfetch\s*\(/);
116
- assert.match(
117
- source,
118
- /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceInviteLanding/
119
- );
120
- assert.match(source, /<WorkspaceInviteLanding \/>/);
121
- assert.deepEqual(findFileMutation("workspaces-web-page-public-invite"), {
122
- from: "templates/src/pages/invite/[token].vue",
123
- to: "src/pages/invite/[token].vue",
124
- ownership: "app",
125
- reason: "Install public workspace invite acceptance route scaffold.",
126
- category: "workspaces-web",
127
- id: "workspaces-web-page-public-invite"
128
- });
30
+ assert.match(settings, /target="admin-settings:primary-menu"/);
31
+ assert.match(settings, /<RouterView \/>/);
32
+ assert.doesNotMatch(settings, /<v-card\b|default-link-component-token/);
33
+ assert.match(invites, /@jskit-ai\/workspaces-web\/client\/components\/AccountSettingsInvitesSection/);
34
+ assert.match(invitationPage, /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceInviteLanding/);
35
+ assert.match(invitationPage, /<WorkspaceInviteLanding \/>/);
36
+ assert.doesNotMatch(invitationPage, /\bfetch\s*\(/);
129
37
  });
130
38
 
131
- test("workspaces-web settings components use direct panels instead of card scaffolds", async () => {
132
- for (const relativePath of [
133
- path.join("templates", "src", "components", "WorkspaceNotFoundCard.vue"),
134
- path.join("src", "client", "components", "WorkspacesClientElement.vue"),
135
- path.join("src", "client", "components", "AccountSettingsInvitesSection.vue")
39
+ test("workspace settings and not-found examples use direct panels", async () => {
40
+ for (const sourcePath of [
41
+ path.join(exampleRoot, "src", "components", "WorkspaceNotFoundCard.vue"),
42
+ path.join(packageRoot, "src", "client", "components", "WorkspacesClientElement.vue"),
43
+ path.join(packageRoot, "src", "client", "components", "AccountSettingsInvitesSection.vue")
136
44
  ]) {
137
- const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
138
-
139
- assertGeneratedUiSourceContract(source, {
140
- forbidCardShell: true,
141
- sourceName: relativePath
142
- });
45
+ const source = await readFile(sourcePath, "utf8");
143
46
  assert.doesNotMatch(source, /<v-card\b|v-card-title|v-card-subtitle/);
144
47
  }
145
48
  });
146
49
 
147
- test("workspaces-web resource load states expose local retry actions", async () => {
50
+ test("workspace resource load states expose local retry actions", async () => {
148
51
  const expectations = new Map([
149
52
  ["src/client/components/WorkspacesClientElement.vue", /bootstrapLoadError[\s\S]*@click="refreshBootstrap"/],
150
53
  ["src/client/components/WorkspaceMembersClientElement.vue", /canRetryLoad[\s\S]*@click="refreshLoad"/]
151
54
  ]);
152
55
 
153
56
  for (const [relativePath, pattern] of expectations) {
154
- const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
155
- assert.match(source, pattern, `${relativePath} must expose a local retry action for load errors.`);
57
+ const source = await readFile(path.join(packageRoot, relativePath), "utf8");
58
+ assert.match(source, pattern);
156
59
  }
157
60
  });
158
61
 
159
- test("workspaces-web command loading state uses users-web proxyRefs contract", async () => {
62
+ test("workspace command loading follows the http-web proxyRefs contract", async () => {
160
63
  const expectations = new Map([
161
64
  [
162
65
  "src/client/components/WorkspaceMembersClientElement.vue",
163
- [
164
- /isCreatingInvite: Boolean\(inviteCreateCommand\.isRunning\)/,
165
- /if \(inviteCreateCommand\.isRunning \|\| !canInviteMembers\.value\)/
166
- ]
66
+ [/isCreatingInvite: Boolean\(inviteCreateCommand\.isRunning\)/, /inviteCreateCommand\.isRunning \|\| !canInviteMembers\.value/]
167
67
  ],
168
68
  [
169
69
  "src/client/components/WorkspacesClientElement.vue",
170
- [
171
- /const isCreatingWorkspace = computed\(\(\) => Boolean\(createWorkspaceCommand\.isRunning\)\)/
172
- ]
70
+ [/const isCreatingWorkspace = computed\(\(\) => Boolean\(createWorkspaceCommand\.isRunning\)\)/]
173
71
  ],
174
72
  [
175
73
  "src/client/account-settings/useAccountSettingsInvitesSectionRuntime.js",
176
- [
177
- /const isResolvingInvite = computed\(\(\) => Boolean\(redeemInviteCommand\.isRunning\)\)/
178
- ]
74
+ [/const isResolvingInvite = computed\(\(\) => Boolean\(redeemInviteCommand\.isRunning\)\)/]
179
75
  ]
180
76
  ]);
181
77
 
182
78
  for (const [relativePath, patterns] of expectations) {
183
- const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
184
- assert.doesNotMatch(
185
- source,
186
- /\.isRunning\.value/,
187
- `${relativePath} must not read useCommand().isRunning.value; useCommand returns proxyRefs.`
188
- );
79
+ const source = await readFile(path.join(packageRoot, relativePath), "utf8");
80
+ assert.doesNotMatch(source, /\.isRunning\.value/);
189
81
  for (const pattern of patterns) {
190
- assert.match(source, pattern, `${relativePath} must expose command loading state to the UI.`);
82
+ assert.match(source, pattern);
191
83
  }
192
84
  }
193
85
  });
194
86
 
195
- test("workspaces-web installs an account invites cue scaffold that reads placement runtime state", async () => {
196
- const source = await readFile(
197
- path.join(PACKAGE_DIR, "templates", "packages", "main", "src", "client", "components", "AccountPendingInvitesCue.vue"),
87
+ test("workspace pattern includes pending-invite context and product-ready empty surfaces", async () => {
88
+ const cue = await readFile(
89
+ path.join(exampleRoot, "packages", "main", "src", "client", "components", "AccountPendingInvitesCue.vue"),
198
90
  "utf8"
199
91
  );
200
-
201
- assert.doesNotMatch(source, /\bfetch\s*\(/);
202
- assert.doesNotMatch(source, /\buseQuery\b/);
203
- assert.match(source, /placementContext\.value\?\.pendingInvitesCount/);
204
- assert.match(source, /placementContext\.value\?\.workspaceInvitesEnabled/);
205
- assert.deepEqual(findFileMutation("users-web-main-component-account-pending-invites-cue"), {
206
- from: "templates/packages/main/src/client/components/AccountPendingInvitesCue.vue",
207
- to: "packages/main/src/client/components/AccountPendingInvitesCue.vue",
208
- ownership: "app",
209
- reason: "Install app-owned account pending invites cue component scaffold.",
210
- category: "workspaces-web",
211
- id: "users-web-main-component-account-pending-invites-cue"
212
- });
213
- });
214
-
215
- test("workspaces-web admin settings index template is an app-owned landing slot", async () => {
216
- const source = await readFile(
217
- path.join(PACKAGE_DIR, "templates", "src", "pages", "admin", "workspace", "settings", "index.vue"),
92
+ const settingsLanding = await readFile(
93
+ path.join(exampleRoot, "src", "pages", "admin", "workspace", "settings", "index.vue"),
218
94
  "utf8"
219
95
  );
220
-
221
- assertGeneratedUiSourceContract(source, {
222
- forbidCardShell: true,
223
- sourceName: "admin/workspace/settings/index.vue",
224
- requiredPatterns: [
225
- {
226
- id: "app-owned-settings-landing",
227
- pattern: /No settings sections yet/,
228
- message: "Workspace settings index needs a product-shaped app-owned landing state."
229
- }
230
- ]
231
- });
232
- assert.doesNotMatch(source, /@jskit-ai\/workspaces-web\/client\/components\/WorkspaceSettingsClientElement/);
233
- assert.doesNotMatch(source, /<WorkspaceSettingsClientElement \/>/);
234
- assert.doesNotMatch(source, /your_child_segment|To redirect this settings shell/);
235
- });
236
-
237
- test("workspaces-web starter surfaces avoid instructional placeholder copy", async () => {
238
- const appSource = await readFile(
239
- path.join(PACKAGE_DIR, "templates", "src", "surfaces", "app", "index.vue"),
240
- "utf8"
241
- );
242
- const adminSource = await readFile(
243
- path.join(PACKAGE_DIR, "templates", "src", "surfaces", "admin", "index.vue"),
244
- "utf8"
245
- );
246
-
247
- assertGeneratedUiSourceContract(appSource, {
248
- forbidCardShell: true,
249
- sourceName: "workspaces app surface",
250
- requiredPatterns: [
251
- {
252
- id: "workspace-app-empty-state",
253
- pattern: /No workspace activity yet/,
254
- message: "Workspace app surface needs a product-shaped empty state."
255
- }
256
- ]
257
- });
258
- assertGeneratedUiSourceContract(adminSource, {
259
- forbidCardShell: true,
260
- sourceName: "workspaces admin surface",
261
- requiredPatterns: [
262
- {
263
- id: "workspace-admin-member-link",
264
- pattern: /:to="adminChildPath\('members'\)"/,
265
- message: "Workspace admin surface needs a direct members action."
266
- },
267
- {
268
- id: "workspace-admin-settings-link",
269
- pattern: /:to="adminChildPath\('workspace\/settings'\)"/,
270
- message: "Workspace admin surface needs a direct settings action."
271
- }
272
- ]
273
- });
274
- assert.match(appSource, /No workspace activity yet/);
275
- assert.match(adminSource, /Manage members and workspace settings/);
276
- assert.match(adminSource, /const route = useRoute\(\)/);
277
- assert.match(adminSource, /function adminChildPath/);
278
- assert.match(adminSource, /:to="adminChildPath\('members'\)"/);
279
- assert.match(adminSource, /:to="adminChildPath\('workspace\/settings'\)"/);
280
- assert.doesNotMatch(adminSource, /to="\.\//);
281
- assert.doesNotMatch(appSource, /Replace this page|Primary in-workspace surface/);
282
- assert.doesNotMatch(adminSource, /Use this area|Privileged workspace workflows/);
96
+ const appSurface = await readFile(path.join(exampleRoot, "src", "surfaces", "app", "index.vue"), "utf8");
97
+ const adminSurface = await readFile(path.join(exampleRoot, "src", "surfaces", "admin", "index.vue"), "utf8");
98
+
99
+ assert.match(cue, /placementContext\.value\?\.pendingInvitesCount/);
100
+ assert.match(cue, /placementContext\.value\?\.workspaceInvitesEnabled/);
101
+ assert.doesNotMatch(cue, /\bfetch\s*\(|\buseQuery\b/);
102
+ assert.match(settingsLanding, /No settings sections yet/);
103
+ assert.match(appSurface, /No workspace activity yet/);
104
+ assert.match(adminSurface, /Manage members and workspace settings/);
105
+ assert.doesNotMatch(appSurface, /Replace this page|Primary in-workspace surface/);
106
+ assert.doesNotMatch(adminSurface, /Use this area|Privileged workspace workflows/);
283
107
  });
284
108
 
285
- test("workspaces-web descriptor metadata advertises admin settings outlets", () => {
109
+ test("workspaces-web metadata declares semantic outlets without source mutations", () => {
110
+ assert.equal(packageJson.jskit?.mutations, undefined);
111
+ assert.deepEqual(placements?.outlets, [
112
+ {
113
+ target: "admin-settings:primary-menu",
114
+ surfaces: ["admin"],
115
+ source: "patterns/workspace-surfaces/example/src/pages/admin/workspace/settings.vue"
116
+ },
117
+ {
118
+ target: "admin-cog:primary-menu",
119
+ surfaces: ["admin"],
120
+ source: "src/client/components/WorkspaceToolsWidget.vue"
121
+ }
122
+ ]);
286
123
  assert.deepEqual(
287
- readOutlets("admin-settings:primary-menu"),
124
+ placements?.topology?.placements?.map(({ id, owner = null }) => ({ id, owner })),
288
125
  [
289
- {
290
- target: "admin-settings:primary-menu",
291
- surfaces: ["admin"],
292
- source: "templates/src/pages/admin/workspace/settings.vue"
293
- }
126
+ { id: "page.section-nav", owner: "admin-settings" },
127
+ { id: "admin.tools-menu", owner: null }
294
128
  ]
295
129
  );
296
- assert.deepEqual(
297
- readOutlets("admin-cog:primary-menu"),
298
- [
299
- {
300
- target: "admin-cog:primary-menu",
301
- surfaces: ["admin"],
302
- source: "src/client/components/UsersWorkspaceToolsWidget.vue"
303
- }
304
- ]
130
+ });
131
+
132
+ test("workspace contributions identify live package source or reusable pattern source", () => {
133
+ assert.equal(contribution("workspaces.workspace.menu.app")?.source, "patterns/workspace-surfaces/example/src/surfaces/app/index.vue");
134
+ assert.equal(contribution("workspaces.workspace.menu.admin")?.source, "patterns/workspace-surfaces/example/src/surfaces/admin/index.vue");
135
+ assert.equal(contribution("workspaces.profile.menu.surface-switch")?.source, "src/client/components/WorkspaceProfileSurfaceSwitchMenuItem.vue");
136
+ assert.equal(contribution("workspaces.workspace.selector")?.source, "src/client/components/WorkspaceSelector.vue");
137
+ assert.equal(
138
+ contribution("workspaces.account.invites.cue")?.source,
139
+ "patterns/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue"
305
140
  );
306
- assert.deepEqual(findTopology("page.section-nav", "admin-settings"), {
307
- id: "page.section-nav",
308
- owner: "admin-settings",
309
- description: "Navigation between workspace admin settings child pages.",
310
- surfaces: ["admin"],
311
- variants: {
312
- compact: {
313
- outlet: "admin-settings:primary-menu",
314
- renderers: {
315
- link: "local.main.ui.surface-aware-menu-link-item"
316
- }
317
- },
318
- medium: {
319
- outlet: "admin-settings:primary-menu",
320
- renderers: {
321
- link: "local.main.ui.surface-aware-menu-link-item"
322
- }
323
- },
324
- expanded: {
325
- outlet: "admin-settings:primary-menu",
326
- renderers: {
327
- link: "local.main.ui.surface-aware-menu-link-item"
328
- }
329
- }
330
- }
331
- });
332
- assert.deepEqual(findTopology("admin.tools-menu"), {
333
- id: "admin.tools-menu",
334
- description: "Admin surface tools menu actions.",
335
- surfaces: ["admin"],
336
- variants: {
337
- compact: {
338
- outlet: "admin-cog:primary-menu",
339
- renderers: {
340
- link: "local.main.ui.surface-aware-menu-link-item"
341
- }
342
- },
343
- medium: {
344
- outlet: "admin-cog:primary-menu",
345
- renderers: {
346
- link: "local.main.ui.surface-aware-menu-link-item"
347
- }
348
- },
349
- expanded: {
350
- outlet: "admin-cog:primary-menu",
351
- renderers: {
352
- link: "local.main.ui.surface-aware-menu-link-item"
353
- }
354
- }
355
- }
356
- });
357
- assert.equal(findContribution("workspaces.workspace.settings.general"), null);
358
- assert.deepEqual(findContribution("workspaces.workspace.menu.app"), {
359
- id: "workspaces.workspace.menu.app",
360
- target: "shell.primary-nav",
361
- kind: "link",
362
- surfaces: ["app"],
363
- order: 50,
364
- when: "auth.authenticated === true",
365
- source: "mutations.text#workspaces-web-placement-block"
366
- });
367
- assert.deepEqual(findContribution("workspaces.workspace.menu.admin"), {
368
- id: "workspaces.workspace.menu.admin",
369
- target: "shell.primary-nav",
370
- kind: "link",
371
- surfaces: ["admin"],
372
- order: 60,
373
- when: "auth.authenticated === true",
374
- source: "mutations.text#workspaces-web-placement-block"
375
- });
376
- assert.match(findTextMutation("workspaces-web-placement-block")?.value || "", /id: "workspaces\.workspace\.menu\.app"[\s\S]*surfaces: \["app"\][\s\S]*label: "Home"/);
377
- assert.match(findTextMutation("workspaces-web-placement-block")?.value || "", /id: "workspaces\.workspace\.menu\.admin"[\s\S]*surfaces: \["admin"\][\s\S]*label: "Home"/);
378
- assert.deepEqual(findContribution("workspaces.profile.menu.surface-switch"), {
379
- id: "workspaces.profile.menu.surface-switch",
380
- target: "auth.profile-menu",
381
- kind: "component",
382
- surfaces: ["*"],
383
- order: 100,
384
- componentToken: "workspaces.web.profile.menu.surface-switch-item",
385
- when: "auth.authenticated === true",
386
- source: "mutations.text#workspaces-web-profile-surface-switch-placement"
387
- });
388
- assert.deepEqual(findContribution("workspaces.workspace.selector"), {
389
- id: "workspaces.workspace.selector",
390
- target: "shell.identity",
391
- kind: "component",
392
- surfaces: ["*"],
393
- order: 200,
394
- componentToken: "workspaces.web.workspace.selector",
395
- when: "auth.authenticated === true",
396
- source: "mutations.text#workspaces-web-placement-block"
397
- });
398
- assert.deepEqual(findContribution("workspaces.account.settings.invites"), {
399
- id: "workspaces.account.settings.invites",
400
- target: "settings.sections",
401
- owner: "account-settings",
402
- kind: "component",
403
- surfaces: ["account"],
404
- order: 400,
405
- componentToken: "local.main.account-settings.section.invites",
406
- when: "auth.authenticated === true && workspaceInvitesEnabled === true",
407
- source: "mutations.text#workspaces-web-account-settings-placement"
408
- });
409
- assert.match(findTextMutation("workspaces-web-account-settings-placement")?.value || "", /id: "workspaces\.account\.settings\.invites"/);
410
- assert.match(findTextMutation("workspaces-web-account-settings-placement")?.value || "", /target: "settings\.sections"/);
411
- assert.match(findTextMutation("workspaces-web-account-settings-placement")?.value || "", /owner: "account-settings"/);
412
- assert.match(findTextMutation("workspaces-web-account-settings-placement")?.value || "", /componentToken: "local\.main\.account-settings\.section\.invites"/);
413
- assert.equal(findTextMutation("workspaces-web-admin-placement-topology")?.file, "src/placementTopology.js");
414
- assert.match(findTextMutation("workspaces-web-admin-placement-topology")?.value || "", /id: "page\.section-nav"/);
415
- assert.match(findTextMutation("workspaces-web-admin-placement-topology")?.value || "", /owner: "admin-settings"/);
416
- assert.match(findTextMutation("workspaces-web-admin-placement-topology")?.value || "", /outlet: "admin-settings:primary-menu"/);
417
- assert.match(findTextMutation("workspaces-web-admin-placement-topology")?.value || "", /id: "admin\.tools-menu"/);
418
- assert.match(findTextMutation("workspaces-web-admin-placement-topology")?.value || "", /outlet: "admin-cog:primary-menu"/);
419
- assert.deepEqual(findFileMutation("users-web-page-admin-workspace-settings"), {
420
- from: "templates/src/pages/admin/workspace/settings/index.vue",
421
- toSurface: "admin",
422
- toSurfacePath: "workspace/settings/index.vue",
423
- ownership: "app",
424
- reason: "Install workspace settings index stub scaffold for app-owned landing or redirect behavior.",
425
- category: "workspaces-web",
426
- id: "users-web-page-admin-workspace-settings",
427
- when: {
428
- config: "tenancyMode",
429
- in: ["personal", "workspaces"]
430
- }
431
- });
432
- assert.deepEqual(findSourceMutation("users-web-main-client-provider-account-settings-section-import"), {
433
- op: "ensure-import",
434
- file: "packages/main/src/client/providers/MainClientProvider.js",
435
- defaultImport: "AccountSettingsInvitesSection",
436
- from: "../components/AccountSettingsInvitesSection.vue",
437
- reason: "Bind app-owned account invites section component into local main client provider imports.",
438
- category: "workspaces-web",
439
- id: "users-web-main-client-provider-account-settings-section-import"
440
- });
441
- assert.deepEqual(findSourceMutation("users-web-main-client-provider-account-settings-section-register"), {
442
- op: "ensure-call",
443
- file: "packages/main/src/client/providers/MainClientProvider.js",
444
- callee: "registerMainClientComponent",
445
- args: ["\"local.main.account-settings.section.invites\"", "() => AccountSettingsInvitesSection"],
446
- beforeClass: "MainClientProvider",
447
- reason: "Bind app-owned account invites section component token into local main client provider registry.",
448
- category: "workspaces-web",
449
- id: "users-web-main-client-provider-account-settings-section-register"
450
- });
141
+ assert.equal(
142
+ contribution("workspaces.account.settings.invites")?.source,
143
+ "patterns/workspace-surfaces/example/packages/main/src/client/components/AccountSettingsInvitesSection.vue"
144
+ );
145
+ assert.equal(contribution("workspaces.workspace.tools.widget")?.source, "src/client/components/WorkspaceToolsWidget.vue");
146
+ assert.equal(contribution("workspaces.workspace.menu.workspace-settings")?.source, "src/client/components/WorkspaceSettingsMenuItem.vue");
147
+ assert.equal(contribution("workspaces.workspace.menu.members")?.source, "src/client/components/WorkspaceMembersMenuItem.vue");
148
+ assert.equal(contribution("workspaces.workspace.settings.general"), null);
451
149
  });
@@ -6,9 +6,9 @@ import { readFile } from "node:fs/promises";
6
6
 
7
7
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
8
  const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
9
- const COMPONENT_PATH = path.join(PACKAGE_DIR, "src", "client", "components", "UsersWorkspaceSelector.vue");
9
+ const COMPONENT_PATH = path.join(PACKAGE_DIR, "src", "client", "components", "WorkspaceSelector.vue");
10
10
 
11
- test("UsersWorkspaceSelector passes workspaceSlug through generic page params", async () => {
11
+ test("WorkspaceSelector passes workspaceSlug through generic page params", async () => {
12
12
  const source = await readFile(COMPONENT_PATH, "utf8");
13
13
 
14
14
  assert.match(
@@ -18,7 +18,7 @@ test("UsersWorkspaceSelector passes workspaceSlug through generic page params",
18
18
  assert.doesNotMatch(source, /workspaceSlug:\s*normalizedSlug,\s*mode:\s*"workspace"/s);
19
19
  });
20
20
 
21
- test("UsersWorkspaceSelector activator preserves the generated tap-target contract", async () => {
21
+ test("WorkspaceSelector activator preserves the generated tap-target contract", async () => {
22
22
  const source = await readFile(COMPONENT_PATH, "utf8");
23
23
 
24
24
  assert.match(source, /class="users-workspace-selector__activator text-none"/);