@jskit-ai/users-web 0.1.48 → 0.1.50

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.
@@ -1,10 +1,17 @@
1
+ import {
2
+ HOME_TOOLS_OUTLET,
3
+ WORKSPACE_TOOLS_OUTLET
4
+ } from "./src/shared/toolsOutletContracts.js";
5
+
1
6
  export default Object.freeze({
2
7
  packageVersion: 1,
3
8
  packageId: "@jskit-ai/users-web",
4
- version: "0.1.48",
9
+ version: "0.1.50",
5
10
  kind: "runtime",
6
- description: "Users web module: account/profile UI plus shared shell link components.",
11
+ description: "Users web module: account/profile UI plus shared users web widgets.",
7
12
  dependsOn: [
13
+ "@jskit-ai/auth-web",
14
+ "@jskit-ai/console-web",
8
15
  "@jskit-ai/http-runtime",
9
16
  "@jskit-ai/shell-web",
10
17
  "@jskit-ai/uploads-image-web",
@@ -47,14 +54,6 @@ export default Object.freeze({
47
54
  subpath: "./client/components/ProfileClientElement",
48
55
  summary: "Exports profile settings client element scaffold component."
49
56
  },
50
- {
51
- subpath: "./client/components/ConsoleSettingsClientElement",
52
- summary: "Exports console settings landing-page client element."
53
- },
54
- {
55
- subpath: "./client/components/WorkspaceSettingsClientElement",
56
- summary: "Exports workspace settings client element."
57
- },
58
57
  {
59
58
  subpath: "./client/composables/useAddEdit",
60
59
  summary: "Exports add/edit operation composable."
@@ -95,9 +94,8 @@ export default Object.freeze({
95
94
  containerTokens: {
96
95
  server: [],
97
96
  client: [
98
- "users.web.shell.menu-link-item",
99
- "users.web.shell.surface-aware-menu-link-item",
100
97
  "users.web.profile.menu.surface-switch-item",
98
+ "users.web.home.tools.widget",
101
99
  "users.web.profile.element",
102
100
  "users.web.bootstrap-placement.runtime"
103
101
  ]
@@ -107,8 +105,20 @@ export default Object.freeze({
107
105
  placements: {
108
106
  outlets: [
109
107
  {
110
- host: "console-settings",
111
- position: "primary-menu",
108
+ target: HOME_TOOLS_OUTLET.target,
109
+ defaultLinkComponentToken: HOME_TOOLS_OUTLET.defaultLinkComponentToken,
110
+ surfaces: ["home"],
111
+ source: "src/client/components/UsersHomeToolsWidget.vue"
112
+ },
113
+ {
114
+ target: WORKSPACE_TOOLS_OUTLET.target,
115
+ defaultLinkComponentToken: WORKSPACE_TOOLS_OUTLET.defaultLinkComponentToken,
116
+ surfaces: ["admin"],
117
+ source: "src/client/components/UsersWorkspaceToolsWidget.vue"
118
+ },
119
+ {
120
+ target: "console-settings:primary-menu",
121
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
112
122
  surfaces: ["console"],
113
123
  source: "templates/src/pages/console/settings.vue"
114
124
  }
@@ -116,8 +126,7 @@ export default Object.freeze({
116
126
  contributions: [
117
127
  {
118
128
  id: "users.profile.menu.surface-switch",
119
- host: "auth-profile-menu",
120
- position: "primary-menu",
129
+ target: "auth-profile-menu:primary-menu",
121
130
  surfaces: ["*"],
122
131
  order: 100,
123
132
  componentToken: "users.web.profile.menu.surface-switch-item",
@@ -126,23 +135,48 @@ export default Object.freeze({
126
135
  },
127
136
  {
128
137
  id: "users.profile.menu.settings",
129
- host: "auth-profile-menu",
130
- position: "primary-menu",
138
+ target: "auth-profile-menu:primary-menu",
131
139
  surfaces: ["*"],
132
140
  order: 500,
133
- componentToken: "users.web.shell.menu-link-item",
141
+ componentToken: "auth.web.profile.menu.link-item",
134
142
  when: "auth.authenticated === true",
135
143
  source: "mutations.text#users-web-profile-settings-placement"
136
144
  },
145
+ {
146
+ id: "users.home.menu.home",
147
+ target: "shell-layout:primary-menu",
148
+ surfaces: ["*"],
149
+ order: 50,
150
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
151
+ when: "auth.authenticated === true",
152
+ source: "mutations.text#users-web-home-shell-menu-placement"
153
+ },
137
154
  {
138
155
  id: "users.console.menu.settings",
139
- host: "shell-layout",
140
- position: "primary-menu",
156
+ target: "shell-layout:primary-menu",
141
157
  surfaces: ["console"],
142
158
  order: 100,
143
- componentToken: "users.web.shell.menu-link-item",
159
+ componentToken: "local.main.ui.menu-link-item",
144
160
  when: "auth.authenticated === true",
145
161
  source: "mutations.text#users-web-console-settings-placement"
162
+ },
163
+ {
164
+ id: "users.home.tools.widget",
165
+ target: "shell-layout:top-right",
166
+ surfaces: ["home"],
167
+ order: 900,
168
+ componentToken: "users.web.home.tools.widget",
169
+ when: "auth.authenticated === true",
170
+ source: "mutations.text#users-web-home-tools-placement"
171
+ },
172
+ {
173
+ id: "users.home.menu.settings",
174
+ target: "home-tools:primary-menu",
175
+ surfaces: ["home"],
176
+ order: 100,
177
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
178
+ when: "auth.authenticated === true",
179
+ source: "mutations.text#users-web-home-tools-placement"
146
180
  }
147
181
  ]
148
182
  }
@@ -153,12 +187,13 @@ export default Object.freeze({
153
187
  runtime: {
154
188
  "@tanstack/vue-query": "5.92.12",
155
189
  "@mdi/js": "^7.4.47",
156
- "@jskit-ai/http-runtime": "0.1.32",
157
- "@jskit-ai/realtime": "0.1.32",
158
- "@jskit-ai/kernel": "0.1.33",
159
- "@jskit-ai/shell-web": "0.1.32",
160
- "@jskit-ai/uploads-image-web": "0.1.11",
161
- "@jskit-ai/users-core": "0.1.43",
190
+ "@jskit-ai/console-web": "0.1.2",
191
+ "@jskit-ai/http-runtime": "0.1.34",
192
+ "@jskit-ai/realtime": "0.1.34",
193
+ "@jskit-ai/kernel": "0.1.35",
194
+ "@jskit-ai/shell-web": "0.1.34",
195
+ "@jskit-ai/uploads-image-web": "0.1.13",
196
+ "@jskit-ai/users-core": "0.1.45",
162
197
  vuetify: "^4.0.0"
163
198
  },
164
199
  dev: {}
@@ -226,7 +261,7 @@ export default Object.freeze({
226
261
  from: "templates/src/pages/console/settings/index.vue",
227
262
  toSurface: "console",
228
263
  toSurfacePath: "settings/index.vue",
229
- reason: "Install console settings landing page scaffold for users-web console UI.",
264
+ reason: "Install console settings index stub scaffold for app-owned landing or redirect behavior.",
230
265
  category: "users-web",
231
266
  id: "users-web-page-console-settings"
232
267
  }
@@ -249,7 +284,7 @@ export default Object.freeze({
249
284
  position: "bottom",
250
285
  skipIfContains: "id: \"users.profile.menu.surface-switch\"",
251
286
  value:
252
- "\naddPlacement({\n id: \"users.profile.menu.surface-switch\",\n host: \"auth-profile-menu\",\n position: \"primary-menu\",\n surfaces: [\"*\"],\n order: 100,\n componentToken: \"users.web.profile.menu.surface-switch-item\",\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
287
+ "\naddPlacement({\n id: \"users.profile.menu.surface-switch\",\n target: \"auth-profile-menu:primary-menu\",\n surfaces: [\"*\"],\n order: 100,\n componentToken: \"users.web.profile.menu.surface-switch-item\",\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
253
288
  reason: "Append users-web profile surface switch placement into app-owned placement registry.",
254
289
  category: "users-web",
255
290
  id: "users-web-profile-surface-switch-placement"
@@ -260,21 +295,43 @@ export default Object.freeze({
260
295
  position: "bottom",
261
296
  skipIfContains: "id: \"users.profile.menu.settings\"",
262
297
  value:
263
- "\naddPlacement({\n id: \"users.profile.menu.settings\",\n host: \"auth-profile-menu\",\n position: \"primary-menu\",\n surfaces: [\"*\"],\n order: 500,\n componentToken: \"users.web.shell.menu-link-item\",\n props: {\n label: \"Settings\",\n to: \"/account\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
298
+ "\naddPlacement({\n id: \"users.profile.menu.settings\",\n target: \"auth-profile-menu:primary-menu\",\n surfaces: [\"*\"],\n order: 500,\n componentToken: \"auth.web.profile.menu.link-item\",\n props: {\n label: \"Settings\",\n to: \"/account\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
264
299
  reason: "Append users-web profile settings menu placement into app-owned placement registry.",
265
300
  category: "users-web",
266
301
  id: "users-web-profile-settings-placement"
267
302
  },
303
+ {
304
+ op: "append-text",
305
+ file: "src/placement.js",
306
+ position: "bottom",
307
+ skipIfContains: "id: \"users.home.menu.home\"",
308
+ value:
309
+ "\naddPlacement({\n id: \"users.home.menu.home\",\n target: \"shell-layout:primary-menu\",\n surfaces: [\"*\"],\n order: 50,\n componentToken: \"local.main.ui.surface-aware-menu-link-item\",\n props: {\n label: \"Home\",\n surface: \"home\",\n workspaceSuffix: \"/\",\n nonWorkspaceSuffix: \"/\",\n exact: true\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
310
+ reason: "Append users-web home shell menu placement into app-owned placement registry.",
311
+ category: "users-web",
312
+ id: "users-web-home-shell-menu-placement"
313
+ },
268
314
  {
269
315
  op: "append-text",
270
316
  file: "src/placement.js",
271
317
  position: "bottom",
272
318
  skipIfContains: "id: \"users.console.menu.settings\"",
273
319
  value:
274
- "\naddPlacement({\n id: \"users.console.menu.settings\",\n host: \"shell-layout\",\n position: \"primary-menu\",\n surfaces: [\"console\"],\n order: 100,\n componentToken: \"users.web.shell.menu-link-item\",\n props: {\n label: \"Settings\",\n to: \"/console/settings\",\n icon: \"mdi-cog-outline\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
320
+ "\naddPlacement({\n id: \"users.console.menu.settings\",\n target: \"shell-layout:primary-menu\",\n surfaces: [\"console\"],\n order: 100,\n componentToken: \"local.main.ui.menu-link-item\",\n props: {\n label: \"Settings\",\n to: \"/console/settings\",\n icon: \"mdi-cog-outline\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
275
321
  reason: "Append users-web console settings menu placement into app-owned placement registry.",
276
322
  category: "users-web",
277
323
  id: "users-web-console-settings-placement"
324
+ },
325
+ {
326
+ op: "append-text",
327
+ file: "src/placement.js",
328
+ position: "bottom",
329
+ skipIfContains: "id: \"users.home.tools.widget\"",
330
+ value:
331
+ "\naddPlacement({\n id: \"users.home.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"home\"],\n order: 900,\n componentToken: \"users.web.home.tools.widget\",\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n\naddPlacement({\n id: \"users.home.menu.settings\",\n target: \"home-tools:primary-menu\",\n surfaces: [\"home\"],\n order: 100,\n componentToken: \"local.main.ui.surface-aware-menu-link-item\",\n props: {\n label: \"Settings\",\n surface: \"home\",\n workspaceSuffix: \"/settings\",\n nonWorkspaceSuffix: \"/settings\"\n },\n when: ({ auth }) => Boolean(auth?.authenticated)\n});\n",
332
+ reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
333
+ category: "users-web",
334
+ id: "users-web-home-tools-placement"
278
335
  }
279
336
  ]
280
337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,8 +8,6 @@
8
8
  "exports": {
9
9
  "./client": "./src/client/index.js",
10
10
  "./client/providers/UsersWorkspacesClientProvider": "./src/client/providers/UsersWorkspacesClientProvider.js",
11
- "./client/components/ConsoleSettingsClientElement": "./src/client/components/ConsoleSettingsClientElement.vue",
12
- "./client/components/WorkspaceSettingsClientElement": "./src/client/components/WorkspaceSettingsClientElement.vue",
13
11
  "./client/components/WorkspaceMembersClientElement": "./src/client/components/WorkspaceMembersClientElement.vue",
14
12
  "./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
15
13
  "./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
@@ -28,12 +26,12 @@
28
26
  "dependencies": {
29
27
  "@tanstack/vue-query": "5.92.12",
30
28
  "@mdi/js": "^7.4.47",
31
- "@jskit-ai/http-runtime": "0.1.32",
32
- "@jskit-ai/kernel": "0.1.33",
33
- "@jskit-ai/realtime": "0.1.32",
34
- "@jskit-ai/shell-web": "0.1.32",
35
- "@jskit-ai/uploads-image-web": "0.1.11",
36
- "@jskit-ai/users-core": "0.1.43",
29
+ "@jskit-ai/http-runtime": "0.1.34",
30
+ "@jskit-ai/kernel": "0.1.35",
31
+ "@jskit-ai/realtime": "0.1.34",
32
+ "@jskit-ai/shell-web": "0.1.34",
33
+ "@jskit-ai/uploads-image-web": "0.1.13",
34
+ "@jskit-ai/users-core": "0.1.45",
37
35
  "vuetify": "^4.0.0"
38
36
  }
39
37
  }
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import ShellOutletMenuWidget from "@jskit-ai/shell-web/client/components/ShellOutletMenuWidget";
3
+ import { HOME_TOOLS_OUTLET } from "../../shared/toolsOutletContracts.js";
4
+ </script>
5
+
6
+ <template>
7
+ <ShellOutletMenuWidget
8
+ :target="HOME_TOOLS_OUTLET.target"
9
+ :default-link-component-token="HOME_TOOLS_OUTLET.defaultLinkComponentToken"
10
+ :aria-label="HOME_TOOLS_OUTLET.ariaLabel"
11
+ />
12
+ </template>
@@ -1,23 +1,12 @@
1
1
  <script setup>
2
- import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
3
- import { mdiCogOutline } from "@mdi/js";
2
+ import ShellOutletMenuWidget from "@jskit-ai/shell-web/client/components/ShellOutletMenuWidget";
3
+ import { WORKSPACE_TOOLS_OUTLET } from "../../shared/toolsOutletContracts.js";
4
4
  </script>
5
5
 
6
6
  <template>
7
- <v-menu location="bottom end" offset="10" eager>
8
- <template #activator="{ props: activatorProps }">
9
- <v-btn
10
- v-bind="activatorProps"
11
- icon
12
- variant="text"
13
- aria-label="Workspace tools"
14
- >
15
- <v-icon :icon="mdiCogOutline" />
16
- </v-btn>
17
- </template>
18
-
19
- <v-list min-width="220" density="comfortable" class="py-1">
20
- <ShellOutlet host="workspace-tools" position="primary-menu" />
21
- </v-list>
22
- </v-menu>
7
+ <ShellOutletMenuWidget
8
+ :target="WORKSPACE_TOOLS_OUTLET.target"
9
+ :default-link-component-token="WORKSPACE_TOOLS_OUTLET.defaultLinkComponentToken"
10
+ :aria-label="WORKSPACE_TOOLS_OUTLET.ariaLabel"
11
+ />
23
12
  </template>
@@ -2,8 +2,6 @@ import { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
2
2
 
3
3
  export { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
4
4
  export { UsersWorkspacesClientProvider } from "./providers/UsersWorkspacesClientProvider.js";
5
- export { default as ConsoleSettingsClientElement } from "./components/ConsoleSettingsClientElement.vue";
6
- export { default as WorkspaceSettingsClientElement } from "./components/WorkspaceSettingsClientElement.vue";
7
5
 
8
6
  const clientProviders = Object.freeze([UsersWebClientProvider]);
9
7
 
@@ -1,6 +1,5 @@
1
- import UsersShellMenuLinkItem from "../components/UsersShellMenuLinkItem.vue";
2
- import UsersSurfaceAwareMenuLinkItem from "../components/UsersSurfaceAwareMenuLinkItem.vue";
3
1
  import UsersProfileSurfaceSwitchMenuItem from "../components/UsersProfileSurfaceSwitchMenuItem.vue";
2
+ import UsersHomeToolsWidget from "../components/UsersHomeToolsWidget.vue";
4
3
  import ProfileClientElement from "../components/ProfileClientElement.vue";
5
4
  import {
6
5
  createBootstrapPlacementRuntime
@@ -15,9 +14,8 @@ class UsersWebClientProvider {
15
14
  throw new Error("UsersWebClientProvider requires application singleton().");
16
15
  }
17
16
 
18
- app.singleton("users.web.shell.menu-link-item", () => UsersShellMenuLinkItem);
19
- app.singleton("users.web.shell.surface-aware-menu-link-item", () => UsersSurfaceAwareMenuLinkItem);
20
17
  app.singleton("users.web.profile.menu.surface-switch-item", () => UsersProfileSurfaceSwitchMenuItem);
18
+ app.singleton("users.web.home.tools.widget", () => UsersHomeToolsWidget);
21
19
  app.singleton("users.web.profile.element", () => ProfileClientElement);
22
20
  app.singleton("users.web.bootstrap-placement.runtime", (scope) => createBootstrapPlacementRuntime({ app: scope }));
23
21
  }
@@ -3,7 +3,6 @@ import UsersWorkspaceToolsWidget from "../components/UsersWorkspaceToolsWidget.v
3
3
  import UsersWorkspaceSettingsMenuItem from "../components/UsersWorkspaceSettingsMenuItem.vue";
4
4
  import UsersWorkspaceMembersMenuItem from "../components/UsersWorkspaceMembersMenuItem.vue";
5
5
  import MembersAdminClientElement from "../components/MembersAdminClientElement.vue";
6
- import WorkspaceSettingsClientElement from "../components/WorkspaceSettingsClientElement.vue";
7
6
 
8
7
  class UsersWorkspacesClientProvider {
9
8
  static id = "workspaces.web.client";
@@ -19,7 +18,6 @@ class UsersWorkspacesClientProvider {
19
18
  app.singleton("users.web.workspace-settings.menu-item", () => UsersWorkspaceSettingsMenuItem);
20
19
  app.singleton("users.web.workspace-members.menu-item", () => UsersWorkspaceMembersMenuItem);
21
20
  app.singleton("users.web.members-admin.element", () => MembersAdminClientElement);
22
- app.singleton("users.web.workspace-settings.element", () => WorkspaceSettingsClientElement);
23
21
  }
24
22
  }
25
23
 
@@ -0,0 +1,19 @@
1
+ const DEFAULT_TOOLS_LINK_COMPONENT_TOKEN = "local.main.ui.surface-aware-menu-link-item";
2
+
3
+ const HOME_TOOLS_OUTLET = Object.freeze({
4
+ target: "home-tools:primary-menu",
5
+ defaultLinkComponentToken: DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
6
+ ariaLabel: "Home tools"
7
+ });
8
+
9
+ const WORKSPACE_TOOLS_OUTLET = Object.freeze({
10
+ target: "workspace-tools:primary-menu",
11
+ defaultLinkComponentToken: DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
12
+ ariaLabel: "Workspace tools"
13
+ });
14
+
15
+ export {
16
+ DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
17
+ HOME_TOOLS_OUTLET,
18
+ WORKSPACE_TOOLS_OUTLET
19
+ };
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
- import ConsoleSettingsClientElement from "@jskit-ai/users-web/client/components/ConsoleSettingsClientElement";
2
+ // To redirect this settings shell to a child page, uncomment and edit the example below.
3
+ // definePage({
4
+ // redirect: (to) => `${to.path}/your_child_segment`
5
+ // });
3
6
  </script>
4
7
 
5
- <template>
6
- <ConsoleSettingsClientElement />
7
- </template>
8
+ <template />
@@ -15,7 +15,10 @@ import { RouterView } from "vue-router";
15
15
  <v-row no-gutters>
16
16
  <v-col cols="12" md="3" lg="2" class="pr-md-4 mb-4 mb-md-0">
17
17
  <v-list nav density="comfortable" rounded="lg" border>
18
- <ShellOutlet host="console-settings" position="primary-menu" />
18
+ <ShellOutlet
19
+ target="console-settings:primary-menu"
20
+ default-link-component-token="local.main.ui.surface-aware-menu-link-item"
21
+ />
19
22
  </v-list>
20
23
  </v-col>
21
24
 
@@ -8,30 +8,217 @@ import descriptor from "../package.descriptor.mjs";
8
8
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
9
9
  const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
10
10
 
11
- function readSettingsOutlets() {
11
+ function readOutlets(host = "") {
12
12
  const outlets = descriptor?.metadata?.ui?.placements?.outlets;
13
+ const normalizedTarget = String(host || "").trim();
13
14
  return Array.isArray(outlets)
14
- ? outlets.filter((entry) => String(entry?.host || "").trim() === "console-settings")
15
+ ? outlets.filter((entry) => String(entry?.target || "").trim() === normalizedTarget)
15
16
  : [];
16
17
  }
17
18
 
19
+ function findContribution(id) {
20
+ const contributions = descriptor?.metadata?.ui?.placements?.contributions;
21
+ return Array.isArray(contributions)
22
+ ? contributions.find((entry) => String(entry?.id || "").trim() === id) || null
23
+ : null;
24
+ }
25
+
26
+ function findTextMutation(id) {
27
+ const textMutations = descriptor?.mutations?.text;
28
+ return Array.isArray(textMutations)
29
+ ? textMutations.find((entry) => String(entry?.id || "").trim() === id) || null
30
+ : null;
31
+ }
32
+
33
+ function findFileMutation(id) {
34
+ const fileMutations = descriptor?.mutations?.files;
35
+ return Array.isArray(fileMutations)
36
+ ? fileMutations.find((entry) => String(entry?.id || "").trim() === id) || null
37
+ : null;
38
+ }
39
+
40
+ function expectContribution(id, expected = {}) {
41
+ const contribution = findContribution(id);
42
+ assert.ok(contribution, `Expected contribution "${id}".`);
43
+
44
+ for (const [key, value] of Object.entries(expected)) {
45
+ assert.deepEqual(contribution[key], value);
46
+ }
47
+ }
48
+
49
+ function expectTextMutation(id, { reason = "", category = "", skipIfContains = "", snippets = [] } = {}) {
50
+ const mutation = findTextMutation(id);
51
+ assert.ok(mutation, `Expected text mutation "${id}".`);
52
+ assert.equal(mutation.op, "append-text");
53
+ assert.equal(mutation.file, "src/placement.js");
54
+ assert.equal(mutation.position, "bottom");
55
+ assert.equal(mutation.id, id);
56
+
57
+ if (reason) {
58
+ assert.equal(mutation.reason, reason);
59
+ }
60
+
61
+ if (category) {
62
+ assert.equal(mutation.category, category);
63
+ }
64
+
65
+ if (skipIfContains) {
66
+ assert.equal(mutation.skipIfContains, skipIfContains);
67
+ }
68
+
69
+ for (const snippet of snippets) {
70
+ assert.ok(mutation.value.includes(snippet), `Expected mutation "${id}" to include "${snippet}".`);
71
+ }
72
+ }
73
+
18
74
  test("users-web console settings template exposes surface-derived settings outlets", async () => {
19
75
  const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "console", "settings.vue"), "utf8");
20
76
 
21
- assert.match(source, /<ShellOutlet host="console-settings" position="primary-menu" \/>/);
77
+ assert.match(source, /target="console-settings:primary-menu"/);
78
+ assert.match(source, /default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item"/);
79
+ assert.match(source, /<RouterView \/>/);
80
+ });
81
+
82
+ test("users-web console settings index template is a simple developer-owned stub", async () => {
83
+ const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "console", "settings", "index.vue"), "utf8");
84
+
85
+ assert.match(source, /definePage/);
86
+ assert.match(source, /your_child_segment/);
22
87
  });
23
88
 
24
89
  test("users-web descriptor metadata advertises console settings outlets with standard positions", () => {
25
- const outlets = readSettingsOutlets();
90
+ const outlets = readOutlets("console-settings:primary-menu");
26
91
  assert.deepEqual(
27
92
  outlets,
28
93
  [
29
94
  {
30
- host: "console-settings",
31
- position: "primary-menu",
95
+ target: "console-settings:primary-menu",
96
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
32
97
  surfaces: ["console"],
33
98
  source: "templates/src/pages/console/settings.vue"
34
99
  }
35
100
  ]
36
101
  );
102
+ assert.deepEqual(findFileMutation("users-web-page-console-settings"), {
103
+ from: "templates/src/pages/console/settings/index.vue",
104
+ toSurface: "console",
105
+ toSurfacePath: "settings/index.vue",
106
+ reason: "Install console settings index stub scaffold for app-owned landing or redirect behavior.",
107
+ category: "users-web",
108
+ id: "users-web-page-console-settings"
109
+ });
110
+ });
111
+
112
+ test("users-web home tools widget exposes home-tools outlet", async () => {
113
+ const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "UsersHomeToolsWidget.vue"), "utf8");
114
+
115
+ assert.match(source, /import \{ HOME_TOOLS_OUTLET \} from "\.\.\/\.\.\/shared\/toolsOutletContracts\.js";/);
116
+ assert.match(source, /<ShellOutletMenuWidget/);
117
+ assert.match(source, /:target="HOME_TOOLS_OUTLET\.target"/);
118
+ assert.match(source, /:default-link-component-token="HOME_TOOLS_OUTLET\.defaultLinkComponentToken"/);
119
+ });
120
+
121
+ test("users-web workspace tools widget exposes workspace-tools outlet", async () => {
122
+ const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "UsersWorkspaceToolsWidget.vue"), "utf8");
123
+
124
+ assert.match(source, /import \{ WORKSPACE_TOOLS_OUTLET \} from "\.\.\/\.\.\/shared\/toolsOutletContracts\.js";/);
125
+ assert.match(source, /<ShellOutletMenuWidget/);
126
+ assert.match(source, /:target="WORKSPACE_TOOLS_OUTLET\.target"/);
127
+ assert.match(source, /:default-link-component-token="WORKSPACE_TOOLS_OUTLET\.defaultLinkComponentToken"/);
128
+ });
129
+
130
+ test("users-web descriptor metadata advertises home tools outlet and standard home settings placements", () => {
131
+ assert.deepEqual(
132
+ readOutlets("home-tools:primary-menu"),
133
+ [
134
+ {
135
+ target: "home-tools:primary-menu",
136
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
137
+ surfaces: ["home"],
138
+ source: "src/client/components/UsersHomeToolsWidget.vue"
139
+ }
140
+ ]
141
+ );
142
+
143
+ expectContribution("users.home.menu.home", {
144
+ target: "shell-layout:primary-menu",
145
+ surfaces: ["*"],
146
+ order: 50,
147
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
148
+ when: "auth.authenticated === true",
149
+ source: "mutations.text#users-web-home-shell-menu-placement"
150
+ });
151
+
152
+ expectContribution("users.profile.menu.settings", {
153
+ target: "auth-profile-menu:primary-menu",
154
+ surfaces: ["*"],
155
+ order: 500,
156
+ componentToken: "auth.web.profile.menu.link-item",
157
+ when: "auth.authenticated === true",
158
+ source: "mutations.text#users-web-profile-settings-placement"
159
+ });
160
+
161
+ expectContribution("users.home.tools.widget", {
162
+ target: "shell-layout:top-right",
163
+ surfaces: ["home"],
164
+ order: 900,
165
+ componentToken: "users.web.home.tools.widget",
166
+ when: "auth.authenticated === true",
167
+ source: "mutations.text#users-web-home-tools-placement"
168
+ });
169
+
170
+ expectContribution("users.home.menu.settings", {
171
+ target: "home-tools:primary-menu",
172
+ surfaces: ["home"],
173
+ order: 100,
174
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
175
+ when: "auth.authenticated === true",
176
+ source: "mutations.text#users-web-home-tools-placement"
177
+ });
178
+ assert.equal(findContribution("users.home.settings.general"), null);
179
+
180
+ expectTextMutation("users-web-home-tools-placement", {
181
+ reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
182
+ category: "users-web",
183
+ skipIfContains: 'id: "users.home.tools.widget"',
184
+ snippets: [
185
+ 'id: "users.home.tools.widget"',
186
+ 'componentToken: "users.web.home.tools.widget"',
187
+ 'id: "users.home.menu.settings"',
188
+ 'target: "home-tools:primary-menu"',
189
+ 'componentToken: "local.main.ui.surface-aware-menu-link-item"',
190
+ 'workspaceSuffix: "/settings"',
191
+ 'nonWorkspaceSuffix: "/settings"'
192
+ ]
193
+ });
194
+
195
+ expectTextMutation("users-web-profile-settings-placement", {
196
+ reason: "Append users-web profile settings menu placement into app-owned placement registry.",
197
+ category: "users-web",
198
+ skipIfContains: 'id: "users.profile.menu.settings"',
199
+ snippets: [
200
+ 'id: "users.profile.menu.settings"',
201
+ 'target: "auth-profile-menu:primary-menu"',
202
+ 'componentToken: "auth.web.profile.menu.link-item"',
203
+ 'label: "Settings"',
204
+ 'to: "/account"'
205
+ ]
206
+ });
207
+
208
+ expectTextMutation("users-web-home-shell-menu-placement", {
209
+ reason: "Append users-web home shell menu placement into app-owned placement registry.",
210
+ category: "users-web",
211
+ skipIfContains: 'id: "users.home.menu.home"',
212
+ snippets: [
213
+ 'id: "users.home.menu.home"',
214
+ 'target: "shell-layout:primary-menu"',
215
+ 'componentToken: "local.main.ui.surface-aware-menu-link-item"',
216
+ 'label: "Home"',
217
+ 'surface: "home"',
218
+ 'workspaceSuffix: "/"',
219
+ 'nonWorkspaceSuffix: "/"',
220
+ 'exact: true'
221
+ ]
222
+ });
223
+
37
224
  });
@@ -1,140 +0,0 @@
1
- <script setup>
2
- import { computed } from "vue";
3
- import { useRoute } from "vue-router";
4
- import { appendQueryString } from "@jskit-ai/kernel/shared/support";
5
- import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
6
- import {
7
- useWebPlacementContext,
8
- resolveSurfaceNavigationTargetFromPlacementContext
9
- } from "@jskit-ai/shell-web/client/placement";
10
- import { resolveAccountSettingsPathFromPlacementContext } from "../lib/workspaceSurfacePaths.js";
11
- import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
12
-
13
- const props = defineProps({
14
- label: {
15
- type: String,
16
- default: ""
17
- },
18
- to: {
19
- type: String,
20
- default: ""
21
- },
22
- icon: {
23
- type: String,
24
- default: ""
25
- },
26
- disabled: {
27
- type: Boolean,
28
- default: false
29
- }
30
- });
31
-
32
- const route = useRoute();
33
- const { context: placementContext } = useWebPlacementContext();
34
-
35
- function resolveFallbackReturnTo() {
36
- if (typeof window !== "object" || !window || !window.location) {
37
- return "/";
38
- }
39
- const pathname = String(window.location.pathname || "").trim() || "/";
40
- const search = String(window.location.search || "").trim();
41
- const hash = String(window.location.hash || "").trim();
42
- return `${pathname}${search}${hash}`;
43
- }
44
-
45
- function resolveFallbackReturnToHref() {
46
- if (typeof window !== "object" || !window || !window.location) {
47
- return "/";
48
- }
49
- return String(window.location.href || "").trim() || resolveFallbackReturnTo();
50
- }
51
-
52
- function resolvePathnameFromLinkTarget(target = "") {
53
- const normalizedTarget = String(target || "").trim();
54
- if (!normalizedTarget) {
55
- return "";
56
- }
57
-
58
- if (isExternalLinkTarget(normalizedTarget)) {
59
- try {
60
- const parsed = new URL(normalizedTarget);
61
- return String(parsed.pathname || "").trim();
62
- } catch {
63
- return "";
64
- }
65
- }
66
-
67
- return splitPathQueryHash(normalizedTarget).pathname;
68
- }
69
-
70
- const accountSettingsPathname = computed(() => {
71
- const settingsPath = resolveAccountSettingsPathFromPlacementContext(placementContext.value);
72
- return resolvePathnameFromLinkTarget(settingsPath);
73
- });
74
-
75
- const resolvedTo = computed(() => {
76
- const target = String(props.to || "").trim();
77
- if (!target) {
78
- return "";
79
- }
80
-
81
- const targetPathname = resolvePathnameFromLinkTarget(target);
82
- if (!targetPathname || targetPathname !== accountSettingsPathname.value) {
83
- return target;
84
- }
85
- if (target.includes("returnTo=")) {
86
- return target;
87
- }
88
-
89
- const accountSettingsTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
90
- path: target,
91
- surfaceId: "account"
92
- });
93
- const routeFullPath = String(route?.fullPath || "").trim();
94
- const routePath = String(route?.path || "").trim();
95
- const returnTo = accountSettingsTarget.sameOrigin
96
- ? routeFullPath || routePath || resolveFallbackReturnTo()
97
- : resolveFallbackReturnToHref();
98
- const queryParams = new URLSearchParams({
99
- returnTo
100
- });
101
-
102
- return appendQueryString(target, queryParams.toString());
103
- });
104
-
105
- const resolvedNavigationTarget = computed(() => {
106
- const target = String(resolvedTo.value || "").trim();
107
- if (!target) {
108
- return {
109
- href: "",
110
- sameOrigin: true
111
- };
112
- }
113
-
114
- const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(placementContext.value, {
115
- path: target
116
- });
117
- return {
118
- href: navigationTarget.href,
119
- sameOrigin: navigationTarget.sameOrigin
120
- };
121
- });
122
-
123
- const resolvedIcon = computed(() =>
124
- resolveMenuLinkIcon({
125
- icon: props.icon,
126
- label: props.label,
127
- to: resolvedTo.value
128
- })
129
- );
130
- </script>
131
-
132
- <template>
133
- <v-list-item
134
- :title="props.label"
135
- :to="resolvedNavigationTarget.sameOrigin ? resolvedNavigationTarget.href : undefined"
136
- :href="resolvedNavigationTarget.sameOrigin ? undefined : resolvedNavigationTarget.href"
137
- :prepend-icon="resolvedIcon || undefined"
138
- :disabled="props.disabled"
139
- />
140
- </template>
@@ -1,76 +0,0 @@
1
- <script setup>
2
- import { computed } from "vue";
3
- import { useRoute } from "vue-router";
4
- import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
5
- import { usePaths } from "../composables/usePaths.js";
6
- import { resolveMenuLinkIcon } from "../lib/menuIcons.js";
7
- import { resolveMenuLinkTarget } from "../support/menuLinkTarget.js";
8
-
9
- const props = defineProps({
10
- label: {
11
- type: String,
12
- default: ""
13
- },
14
- to: {
15
- type: String,
16
- default: ""
17
- },
18
- icon: {
19
- type: String,
20
- default: ""
21
- },
22
- surface: {
23
- type: String,
24
- default: ""
25
- },
26
- workspaceSuffix: {
27
- type: String,
28
- default: "/"
29
- },
30
- nonWorkspaceSuffix: {
31
- type: String,
32
- default: "/"
33
- },
34
- disabled: {
35
- type: Boolean,
36
- default: false
37
- }
38
- });
39
-
40
- const route = useRoute();
41
- const paths = usePaths();
42
- const { context: placementContext } = useWebPlacementContext();
43
-
44
- const resolvedTo = computed(() => {
45
- return resolveMenuLinkTarget({
46
- to: props.to,
47
- surface: props.surface,
48
- currentSurfaceId: paths.currentSurfaceId.value,
49
- placementContext: placementContext.value,
50
- workspaceSuffix: props.workspaceSuffix,
51
- nonWorkspaceSuffix: props.nonWorkspaceSuffix,
52
- routeParams: route.params || {},
53
- resolvePagePath(relativePath, options = {}) {
54
- return paths.page(relativePath, options);
55
- }
56
- });
57
- });
58
-
59
- const resolvedIcon = computed(() =>
60
- resolveMenuLinkIcon({
61
- icon: props.icon,
62
- label: props.label,
63
- to: resolvedTo.value
64
- })
65
- );
66
- </script>
67
-
68
- <template>
69
- <v-list-item
70
- v-if="resolvedTo"
71
- :title="props.label"
72
- :to="resolvedTo"
73
- :prepend-icon="resolvedIcon || undefined"
74
- :disabled="props.disabled"
75
- />
76
- </template>