@jskit-ai/users-web 0.1.48 → 0.1.49

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,16 @@
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.49",
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",
8
14
  "@jskit-ai/http-runtime",
9
15
  "@jskit-ai/shell-web",
10
16
  "@jskit-ai/uploads-image-web",
@@ -47,14 +53,6 @@ export default Object.freeze({
47
53
  subpath: "./client/components/ProfileClientElement",
48
54
  summary: "Exports profile settings client element scaffold component."
49
55
  },
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
56
  {
59
57
  subpath: "./client/composables/useAddEdit",
60
58
  summary: "Exports add/edit operation composable."
@@ -95,9 +93,8 @@ export default Object.freeze({
95
93
  containerTokens: {
96
94
  server: [],
97
95
  client: [
98
- "users.web.shell.menu-link-item",
99
- "users.web.shell.surface-aware-menu-link-item",
100
96
  "users.web.profile.menu.surface-switch-item",
97
+ "users.web.home.tools.widget",
101
98
  "users.web.profile.element",
102
99
  "users.web.bootstrap-placement.runtime"
103
100
  ]
@@ -107,8 +104,20 @@ export default Object.freeze({
107
104
  placements: {
108
105
  outlets: [
109
106
  {
110
- host: "console-settings",
111
- position: "primary-menu",
107
+ target: HOME_TOOLS_OUTLET.target,
108
+ defaultLinkComponentToken: HOME_TOOLS_OUTLET.defaultLinkComponentToken,
109
+ surfaces: ["home"],
110
+ source: "src/client/components/UsersHomeToolsWidget.vue"
111
+ },
112
+ {
113
+ target: WORKSPACE_TOOLS_OUTLET.target,
114
+ defaultLinkComponentToken: WORKSPACE_TOOLS_OUTLET.defaultLinkComponentToken,
115
+ surfaces: ["admin"],
116
+ source: "src/client/components/UsersWorkspaceToolsWidget.vue"
117
+ },
118
+ {
119
+ target: "console-settings:primary-menu",
120
+ defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
112
121
  surfaces: ["console"],
113
122
  source: "templates/src/pages/console/settings.vue"
114
123
  }
@@ -116,8 +125,7 @@ export default Object.freeze({
116
125
  contributions: [
117
126
  {
118
127
  id: "users.profile.menu.surface-switch",
119
- host: "auth-profile-menu",
120
- position: "primary-menu",
128
+ target: "auth-profile-menu:primary-menu",
121
129
  surfaces: ["*"],
122
130
  order: 100,
123
131
  componentToken: "users.web.profile.menu.surface-switch-item",
@@ -126,23 +134,48 @@ export default Object.freeze({
126
134
  },
127
135
  {
128
136
  id: "users.profile.menu.settings",
129
- host: "auth-profile-menu",
130
- position: "primary-menu",
137
+ target: "auth-profile-menu:primary-menu",
131
138
  surfaces: ["*"],
132
139
  order: 500,
133
- componentToken: "users.web.shell.menu-link-item",
140
+ componentToken: "auth.web.profile.menu.link-item",
134
141
  when: "auth.authenticated === true",
135
142
  source: "mutations.text#users-web-profile-settings-placement"
136
143
  },
144
+ {
145
+ id: "users.home.menu.home",
146
+ target: "shell-layout:primary-menu",
147
+ surfaces: ["*"],
148
+ order: 50,
149
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
150
+ when: "auth.authenticated === true",
151
+ source: "mutations.text#users-web-home-shell-menu-placement"
152
+ },
137
153
  {
138
154
  id: "users.console.menu.settings",
139
- host: "shell-layout",
140
- position: "primary-menu",
155
+ target: "shell-layout:primary-menu",
141
156
  surfaces: ["console"],
142
157
  order: 100,
143
- componentToken: "users.web.shell.menu-link-item",
158
+ componentToken: "local.main.ui.menu-link-item",
144
159
  when: "auth.authenticated === true",
145
160
  source: "mutations.text#users-web-console-settings-placement"
161
+ },
162
+ {
163
+ id: "users.home.tools.widget",
164
+ target: "shell-layout:top-right",
165
+ surfaces: ["home"],
166
+ order: 900,
167
+ componentToken: "users.web.home.tools.widget",
168
+ when: "auth.authenticated === true",
169
+ source: "mutations.text#users-web-home-tools-placement"
170
+ },
171
+ {
172
+ id: "users.home.menu.settings",
173
+ target: "home-tools:primary-menu",
174
+ surfaces: ["home"],
175
+ order: 100,
176
+ componentToken: "local.main.ui.surface-aware-menu-link-item",
177
+ when: "auth.authenticated === true",
178
+ source: "mutations.text#users-web-home-tools-placement"
146
179
  }
147
180
  ]
148
181
  }
@@ -153,12 +186,12 @@ export default Object.freeze({
153
186
  runtime: {
154
187
  "@tanstack/vue-query": "5.92.12",
155
188
  "@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",
189
+ "@jskit-ai/http-runtime": "0.1.33",
190
+ "@jskit-ai/realtime": "0.1.33",
191
+ "@jskit-ai/kernel": "0.1.34",
192
+ "@jskit-ai/shell-web": "0.1.33",
193
+ "@jskit-ai/uploads-image-web": "0.1.12",
194
+ "@jskit-ai/users-core": "0.1.44",
162
195
  vuetify: "^4.0.0"
163
196
  },
164
197
  dev: {}
@@ -226,7 +259,7 @@ export default Object.freeze({
226
259
  from: "templates/src/pages/console/settings/index.vue",
227
260
  toSurface: "console",
228
261
  toSurfacePath: "settings/index.vue",
229
- reason: "Install console settings landing page scaffold for users-web console UI.",
262
+ reason: "Install console settings index stub scaffold for app-owned landing or redirect behavior.",
230
263
  category: "users-web",
231
264
  id: "users-web-page-console-settings"
232
265
  }
@@ -249,7 +282,7 @@ export default Object.freeze({
249
282
  position: "bottom",
250
283
  skipIfContains: "id: \"users.profile.menu.surface-switch\"",
251
284
  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",
285
+ "\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
286
  reason: "Append users-web profile surface switch placement into app-owned placement registry.",
254
287
  category: "users-web",
255
288
  id: "users-web-profile-surface-switch-placement"
@@ -260,21 +293,43 @@ export default Object.freeze({
260
293
  position: "bottom",
261
294
  skipIfContains: "id: \"users.profile.menu.settings\"",
262
295
  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",
296
+ "\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
297
  reason: "Append users-web profile settings menu placement into app-owned placement registry.",
265
298
  category: "users-web",
266
299
  id: "users-web-profile-settings-placement"
267
300
  },
301
+ {
302
+ op: "append-text",
303
+ file: "src/placement.js",
304
+ position: "bottom",
305
+ skipIfContains: "id: \"users.home.menu.home\"",
306
+ value:
307
+ "\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",
308
+ reason: "Append users-web home shell menu placement into app-owned placement registry.",
309
+ category: "users-web",
310
+ id: "users-web-home-shell-menu-placement"
311
+ },
268
312
  {
269
313
  op: "append-text",
270
314
  file: "src/placement.js",
271
315
  position: "bottom",
272
316
  skipIfContains: "id: \"users.console.menu.settings\"",
273
317
  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",
318
+ "\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
319
  reason: "Append users-web console settings menu placement into app-owned placement registry.",
276
320
  category: "users-web",
277
321
  id: "users-web-console-settings-placement"
322
+ },
323
+ {
324
+ op: "append-text",
325
+ file: "src/placement.js",
326
+ position: "bottom",
327
+ skipIfContains: "id: \"users.home.tools.widget\"",
328
+ value:
329
+ "\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",
330
+ reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
331
+ category: "users-web",
332
+ id: "users-web-home-tools-placement"
278
333
  }
279
334
  ]
280
335
  }
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.49",
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.33",
30
+ "@jskit-ai/kernel": "0.1.34",
31
+ "@jskit-ai/realtime": "0.1.33",
32
+ "@jskit-ai/shell-web": "0.1.33",
33
+ "@jskit-ai/uploads-image-web": "0.1.12",
34
+ "@jskit-ai/users-core": "0.1.44",
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>