@jskit-ai/shell-web 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +114 -15
- package/package.json +8 -2
- package/src/client/components/ShellLayout.vue +11 -4
- package/src/client/components/ShellMenuLinkItem.vue +71 -0
- package/src/client/components/ShellOutlet.vue +10 -7
- package/src/client/components/ShellOutletMenuWidget.vue +57 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +116 -0
- package/src/client/components/ShellTabLinkItem.vue +128 -0
- package/src/client/index.js +4 -0
- package/src/client/lib/menuIcons.js +210 -0
- package/src/client/placement/runtime.js +22 -22
- package/src/client/placement/validators.js +19 -49
- package/src/client/support/menuLinkTarget.js +97 -0
- package/src/server/support/localLinkItemScaffolds.js +80 -0
- package/templates/expected-existing/src/App.vue +13 -0
- package/templates/expected-existing/src/pages/console/index.vue +12 -0
- package/templates/expected-existing/src/pages/console.vue +13 -0
- package/templates/expected-existing/src/pages/home/index.vue +12 -0
- package/templates/expected-existing/src/pages/home.vue +13 -0
- package/templates/src/components/ShellLayout.vue +11 -4
- package/templates/src/components/menus/MenuLinkItem.vue +30 -0
- package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/templates/src/components/menus/TabLinkItem.vue +34 -0
- package/templates/src/pages/home/settings/index.vue +8 -8
- package/templates/src/pages/home/settings.vue +4 -1
- package/test/bootstrapClaimContract.test.js +88 -0
- package/test/linkItemScaffoldContract.test.js +209 -0
- package/test/outletMenuWidgetContract.test.js +33 -0
- package/test/placementRegistry.test.js +17 -6
- package/test/placementRuntime.test.js +59 -44
- package/test/settingsPlacementContract.test.js +16 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
6
|
+
|
|
7
|
+
function createLocalLinkItemDefinition({
|
|
8
|
+
token = "",
|
|
9
|
+
componentFile = "",
|
|
10
|
+
componentName = "",
|
|
11
|
+
templateFile = ""
|
|
12
|
+
} = {}) {
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
token: String(token || "").trim(),
|
|
15
|
+
componentFile: String(componentFile || "").trim(),
|
|
16
|
+
componentName: String(componentName || "").trim(),
|
|
17
|
+
templateFile: String(templateFile || "").trim()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS = Object.freeze([
|
|
22
|
+
createLocalLinkItemDefinition({
|
|
23
|
+
token: "local.main.ui.menu-link-item",
|
|
24
|
+
componentFile: "src/components/menus/MenuLinkItem.vue",
|
|
25
|
+
componentName: "MenuLinkItem",
|
|
26
|
+
templateFile: "templates/src/components/menus/MenuLinkItem.vue"
|
|
27
|
+
}),
|
|
28
|
+
createLocalLinkItemDefinition({
|
|
29
|
+
token: "local.main.ui.surface-aware-menu-link-item",
|
|
30
|
+
componentFile: "src/components/menus/SurfaceAwareMenuLinkItem.vue",
|
|
31
|
+
componentName: "SurfaceAwareMenuLinkItem",
|
|
32
|
+
templateFile: "templates/src/components/menus/SurfaceAwareMenuLinkItem.vue"
|
|
33
|
+
}),
|
|
34
|
+
createLocalLinkItemDefinition({
|
|
35
|
+
token: "local.main.ui.tab-link-item",
|
|
36
|
+
componentFile: "src/components/menus/TabLinkItem.vue",
|
|
37
|
+
componentName: "TabLinkItem",
|
|
38
|
+
templateFile: "templates/src/components/menus/TabLinkItem.vue"
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const LOCAL_LINK_ITEM_COMPONENT_TOKENS = Object.freeze(
|
|
43
|
+
LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS.map((entry) => entry.token)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function findLocalLinkItemDefinition(componentToken = "") {
|
|
47
|
+
const normalizedComponentToken = String(componentToken || "").trim();
|
|
48
|
+
return LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS.find((entry) => entry.token === normalizedComponentToken) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveLocalLinkItemDefinition(componentTokenOrDefinition = "") {
|
|
52
|
+
if (
|
|
53
|
+
componentTokenOrDefinition &&
|
|
54
|
+
typeof componentTokenOrDefinition === "object" &&
|
|
55
|
+
!Array.isArray(componentTokenOrDefinition)
|
|
56
|
+
) {
|
|
57
|
+
return componentTokenOrDefinition;
|
|
58
|
+
}
|
|
59
|
+
return findLocalLinkItemDefinition(componentTokenOrDefinition);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveLocalLinkItemTemplateAbsolutePath(componentTokenOrDefinition = "") {
|
|
63
|
+
const definition = resolveLocalLinkItemDefinition(componentTokenOrDefinition);
|
|
64
|
+
if (!definition) {
|
|
65
|
+
throw new Error(`Unknown local link-item scaffold: ${String(componentTokenOrDefinition || "").trim() || "(empty)"}.`);
|
|
66
|
+
}
|
|
67
|
+
return path.join(PACKAGE_DIR, definition.templateFile);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readLocalLinkItemComponentSource(componentTokenOrDefinition = "") {
|
|
71
|
+
return await readFile(resolveLocalLinkItemTemplateAbsolutePath(componentTokenOrDefinition), "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS,
|
|
76
|
+
LOCAL_LINK_ITEM_COMPONENT_TOKENS,
|
|
77
|
+
findLocalLinkItemDefinition,
|
|
78
|
+
readLocalLinkItemComponentSource,
|
|
79
|
+
resolveLocalLinkItemTemplateAbsolutePath
|
|
80
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="mx-auto" max-width="960" rounded="xl" border elevation="1">
|
|
3
|
+
<v-card-item class="px-6 py-5 px-md-8 py-md-7">
|
|
4
|
+
<v-card-title class="text-h4">console</v-card-title>
|
|
5
|
+
<v-card-subtitle class="text-subtitle-1 mt-2">operations surface</v-card-subtitle>
|
|
6
|
+
</v-card-item>
|
|
7
|
+
<v-divider />
|
|
8
|
+
<v-card-text class="px-6 py-5 px-md-8 py-md-7 text-body-1 text-medium-emphasis">
|
|
9
|
+
This surface is intended for operational tooling.
|
|
10
|
+
</v-card-text>
|
|
11
|
+
</v-card>
|
|
12
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="mx-auto" max-width="960" rounded="xl" border elevation="1">
|
|
3
|
+
<v-card-item class="px-6 py-5 px-md-8 py-md-7">
|
|
4
|
+
<v-card-title class="text-h4">welcome</v-card-title>
|
|
5
|
+
<v-card-subtitle class="text-subtitle-1 mt-2">starter app</v-card-subtitle>
|
|
6
|
+
</v-card-item>
|
|
7
|
+
<v-divider />
|
|
8
|
+
<v-card-text class="px-6 py-5 px-md-8 py-md-7 text-body-1 text-medium-emphasis">
|
|
9
|
+
Start by adding packages and pages to this app.
|
|
10
|
+
</v-card-text>
|
|
11
|
+
</v-card>
|
|
12
|
+
</template>
|
|
@@ -32,7 +32,7 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
32
32
|
<slot name="top-left" :surface="resolvedSurface">
|
|
33
33
|
<div class="d-flex align-center ga-2">
|
|
34
34
|
<v-chip color="primary" size="small" label>{{ resolvedSurfaceLabel }}</v-chip>
|
|
35
|
-
<ShellOutlet
|
|
35
|
+
<ShellOutlet target="shell-layout:top-left" />
|
|
36
36
|
</div>
|
|
37
37
|
</slot>
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
40
40
|
|
|
41
41
|
<slot name="top-right" :surface="resolvedSurface">
|
|
42
42
|
<div class="d-flex align-center ga-2">
|
|
43
|
-
<ShellOutlet
|
|
43
|
+
<ShellOutlet target="shell-layout:top-right" />
|
|
44
44
|
</div>
|
|
45
45
|
</slot>
|
|
46
46
|
</v-app-bar>
|
|
@@ -49,9 +49,16 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
49
49
|
<slot name="menu" :surface="resolvedSurface">
|
|
50
50
|
<v-list nav density="comfortable" class="pt-2">
|
|
51
51
|
<v-list-subheader class="text-uppercase text-caption">{{ resolvedSurfaceLabel }}</v-list-subheader>
|
|
52
|
-
<ShellOutlet
|
|
52
|
+
<ShellOutlet
|
|
53
|
+
target="shell-layout:primary-menu"
|
|
54
|
+
default
|
|
55
|
+
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
56
|
+
/>
|
|
53
57
|
<v-divider class="my-2" />
|
|
54
|
-
<ShellOutlet
|
|
58
|
+
<ShellOutlet
|
|
59
|
+
target="shell-layout:secondary-menu"
|
|
60
|
+
default-link-component-token="local.main.ui.surface-aware-menu-link-item"
|
|
61
|
+
/>
|
|
55
62
|
</v-list>
|
|
56
63
|
</slot>
|
|
57
64
|
</v-navigation-drawer>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ShellMenuLinkItem from "@jskit-ai/shell-web/client/components/ShellMenuLinkItem";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
label: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
},
|
|
9
|
+
to: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ""
|
|
12
|
+
},
|
|
13
|
+
icon: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: ""
|
|
16
|
+
},
|
|
17
|
+
disabled: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false
|
|
20
|
+
},
|
|
21
|
+
exact: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: false
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<ShellMenuLinkItem v-bind="props" />
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ShellSurfaceAwareMenuLinkItem from "@jskit-ai/shell-web/client/components/ShellSurfaceAwareMenuLinkItem";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
label: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
},
|
|
9
|
+
to: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ""
|
|
12
|
+
},
|
|
13
|
+
icon: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: ""
|
|
16
|
+
},
|
|
17
|
+
surface: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: ""
|
|
20
|
+
},
|
|
21
|
+
workspaceSuffix: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: "/"
|
|
24
|
+
},
|
|
25
|
+
nonWorkspaceSuffix: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: "/"
|
|
28
|
+
},
|
|
29
|
+
disabled: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: false
|
|
32
|
+
},
|
|
33
|
+
exact: {
|
|
34
|
+
type: Boolean,
|
|
35
|
+
default: false
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<ShellSurfaceAwareMenuLinkItem v-bind="props" />
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import ShellTabLinkItem from "@jskit-ai/shell-web/client/components/ShellTabLinkItem";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
label: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: ""
|
|
8
|
+
},
|
|
9
|
+
to: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ""
|
|
12
|
+
},
|
|
13
|
+
surface: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: ""
|
|
16
|
+
},
|
|
17
|
+
workspaceSuffix: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: "/"
|
|
20
|
+
},
|
|
21
|
+
nonWorkspaceSuffix: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: "/"
|
|
24
|
+
},
|
|
25
|
+
disabled: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
default: false
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<ShellTabLinkItem v-bind="props" />
|
|
34
|
+
</template>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
<script setup>
|
|
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
|
+
// });
|
|
6
|
+
</script>
|
|
7
|
+
|
|
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
|
|
18
|
+
<ShellOutlet
|
|
19
|
+
target="home-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
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
|
|
8
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
+
const CREATE_APP_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, "..", "..", "tooling", "create-app", "templates", "base-shell");
|
|
11
|
+
|
|
12
|
+
function findFileMutation(id) {
|
|
13
|
+
const files = descriptor?.mutations?.files;
|
|
14
|
+
return Array.isArray(files)
|
|
15
|
+
? files.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("shell-web claims starter shell files as app-owned scaffolds", () => {
|
|
20
|
+
assert.deepEqual(findFileMutation("shell-web-app-root"), {
|
|
21
|
+
from: "templates/src/App.vue",
|
|
22
|
+
to: "src/App.vue",
|
|
23
|
+
ownership: "app",
|
|
24
|
+
expectedExistingFrom: "templates/expected-existing/src/App.vue",
|
|
25
|
+
reason: "Install full-width shell app root with shell-web error host and edge-to-edge layout.",
|
|
26
|
+
category: "shell-web",
|
|
27
|
+
id: "shell-web-app-root"
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(findFileMutation("shell-web-page-home-wrapper"), {
|
|
30
|
+
from: "templates/src/pages/home.vue",
|
|
31
|
+
toSurface: "home",
|
|
32
|
+
toSurfaceRoot: true,
|
|
33
|
+
ownership: "app",
|
|
34
|
+
expectedExistingFrom: "templates/expected-existing/src/pages/home.vue",
|
|
35
|
+
reason: "Install shell-driven home wrapper page.",
|
|
36
|
+
category: "shell-web",
|
|
37
|
+
id: "shell-web-page-home-wrapper"
|
|
38
|
+
});
|
|
39
|
+
assert.deepEqual(findFileMutation("shell-web-page-home"), {
|
|
40
|
+
from: "templates/src/pages/home/index.vue",
|
|
41
|
+
toSurface: "home",
|
|
42
|
+
toSurfacePath: "index.vue",
|
|
43
|
+
ownership: "app",
|
|
44
|
+
expectedExistingFrom: "templates/expected-existing/src/pages/home/index.vue",
|
|
45
|
+
reason: "Install shell-driven home surface starter page.",
|
|
46
|
+
category: "shell-web",
|
|
47
|
+
id: "shell-web-page-home"
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(findFileMutation("shell-web-page-console-wrapper"), {
|
|
50
|
+
from: "templates/src/pages/console.vue",
|
|
51
|
+
toSurface: "console",
|
|
52
|
+
toSurfaceRoot: true,
|
|
53
|
+
ownership: "app",
|
|
54
|
+
expectedExistingFrom: "templates/expected-existing/src/pages/console.vue",
|
|
55
|
+
reason: "Install shell-driven console wrapper page.",
|
|
56
|
+
category: "shell-web",
|
|
57
|
+
id: "shell-web-page-console-wrapper"
|
|
58
|
+
});
|
|
59
|
+
assert.deepEqual(findFileMutation("shell-web-page-console"), {
|
|
60
|
+
from: "templates/src/pages/console/index.vue",
|
|
61
|
+
toSurface: "console",
|
|
62
|
+
toSurfacePath: "index.vue",
|
|
63
|
+
ownership: "app",
|
|
64
|
+
expectedExistingFrom: "templates/expected-existing/src/pages/console/index.vue",
|
|
65
|
+
reason: "Install shell-driven console page starter.",
|
|
66
|
+
category: "shell-web",
|
|
67
|
+
id: "shell-web-page-console"
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("shell-web expected-existing starter files stay aligned with create-app base-shell", async () => {
|
|
72
|
+
const comparedFiles = [
|
|
73
|
+
"src/App.vue",
|
|
74
|
+
"src/pages/home.vue",
|
|
75
|
+
"src/pages/home/index.vue",
|
|
76
|
+
"src/pages/console.vue",
|
|
77
|
+
"src/pages/console/index.vue"
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
for (const relativeFile of comparedFiles) {
|
|
81
|
+
const shellWebExpectedSource = await readFile(
|
|
82
|
+
path.join(PACKAGE_DIR, "templates", "expected-existing", relativeFile),
|
|
83
|
+
"utf8"
|
|
84
|
+
);
|
|
85
|
+
const createAppSource = await readFile(path.join(CREATE_APP_TEMPLATE_DIR, relativeFile), "utf8");
|
|
86
|
+
assert.equal(shellWebExpectedSource, createAppSource, relativeFile);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import descriptor from "../package.descriptor.mjs";
|
|
7
|
+
import {
|
|
8
|
+
LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS,
|
|
9
|
+
findLocalLinkItemDefinition,
|
|
10
|
+
readLocalLinkItemComponentSource
|
|
11
|
+
} from "../src/server/support/localLinkItemScaffolds.js";
|
|
12
|
+
|
|
13
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
15
|
+
|
|
16
|
+
function findFileMutation(id) {
|
|
17
|
+
const files = descriptor?.mutations?.files;
|
|
18
|
+
return Array.isArray(files)
|
|
19
|
+
? files.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findTextMutation(id) {
|
|
24
|
+
const textMutations = descriptor?.mutations?.text;
|
|
25
|
+
return Array.isArray(textMutations)
|
|
26
|
+
? textMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("shell-web exports generic link-item components for app-owned shell wrappers", async () => {
|
|
31
|
+
const clientIndexSource = await readFile(path.join(PACKAGE_DIR, "src", "client", "index.js"), "utf8");
|
|
32
|
+
assert.match(clientIndexSource, /ShellMenuLinkItem/);
|
|
33
|
+
assert.match(clientIndexSource, /ShellSurfaceAwareMenuLinkItem/);
|
|
34
|
+
assert.match(clientIndexSource, /ShellTabLinkItem/);
|
|
35
|
+
|
|
36
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
|
|
37
|
+
assert.equal(
|
|
38
|
+
packageJson?.exports?.["./client/components/ShellMenuLinkItem"],
|
|
39
|
+
"./src/client/components/ShellMenuLinkItem.vue"
|
|
40
|
+
);
|
|
41
|
+
assert.equal(
|
|
42
|
+
packageJson?.exports?.["./client/components/ShellSurfaceAwareMenuLinkItem"],
|
|
43
|
+
"./src/client/components/ShellSurfaceAwareMenuLinkItem.vue"
|
|
44
|
+
);
|
|
45
|
+
assert.equal(
|
|
46
|
+
packageJson?.exports?.["./client/components/ShellTabLinkItem"],
|
|
47
|
+
"./src/client/components/ShellTabLinkItem.vue"
|
|
48
|
+
);
|
|
49
|
+
assert.equal(
|
|
50
|
+
packageJson?.exports?.["./server/support/localLinkItemScaffolds"],
|
|
51
|
+
"./src/server/support/localLinkItemScaffolds.js"
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("shell-web scaffolds app-owned local link-item wrappers under src/components/menus", async () => {
|
|
56
|
+
const menuWrapperSource = await readFile(
|
|
57
|
+
path.join(PACKAGE_DIR, "templates", "src", "components", "menus", "MenuLinkItem.vue"),
|
|
58
|
+
"utf8"
|
|
59
|
+
);
|
|
60
|
+
const surfaceAwareWrapperSource = await readFile(
|
|
61
|
+
path.join(PACKAGE_DIR, "templates", "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"),
|
|
62
|
+
"utf8"
|
|
63
|
+
);
|
|
64
|
+
const tabWrapperSource = await readFile(
|
|
65
|
+
path.join(PACKAGE_DIR, "templates", "src", "components", "menus", "TabLinkItem.vue"),
|
|
66
|
+
"utf8"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
assert.match(menuWrapperSource, /@jskit-ai\/shell-web\/client\/components\/ShellMenuLinkItem/);
|
|
70
|
+
assert.match(surfaceAwareWrapperSource, /@jskit-ai\/shell-web\/client\/components\/ShellSurfaceAwareMenuLinkItem/);
|
|
71
|
+
assert.match(tabWrapperSource, /@jskit-ai\/shell-web\/client\/components\/ShellTabLinkItem/);
|
|
72
|
+
assert.match(menuWrapperSource, /exact:\s*\{/);
|
|
73
|
+
assert.match(surfaceAwareWrapperSource, /exact:\s*\{/);
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(findFileMutation("shell-web-component-menu-link-item"), {
|
|
76
|
+
from: "templates/src/components/menus/MenuLinkItem.vue",
|
|
77
|
+
to: "src/components/menus/MenuLinkItem.vue",
|
|
78
|
+
ownership: "app",
|
|
79
|
+
reason: "Install app-owned shell menu link-item scaffold for local placement customization.",
|
|
80
|
+
category: "shell-web",
|
|
81
|
+
id: "shell-web-component-menu-link-item"
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(findFileMutation("shell-web-component-surface-aware-menu-link-item"), {
|
|
84
|
+
from: "templates/src/components/menus/SurfaceAwareMenuLinkItem.vue",
|
|
85
|
+
to: "src/components/menus/SurfaceAwareMenuLinkItem.vue",
|
|
86
|
+
ownership: "app",
|
|
87
|
+
reason: "Install app-owned surface-aware shell menu link-item scaffold for local placement customization.",
|
|
88
|
+
category: "shell-web",
|
|
89
|
+
id: "shell-web-component-surface-aware-menu-link-item"
|
|
90
|
+
});
|
|
91
|
+
assert.deepEqual(findFileMutation("shell-web-component-tab-link-item"), {
|
|
92
|
+
from: "templates/src/components/menus/TabLinkItem.vue",
|
|
93
|
+
to: "src/components/menus/TabLinkItem.vue",
|
|
94
|
+
ownership: "app",
|
|
95
|
+
reason: "Install app-owned shell tab link-item scaffold for local placement customization.",
|
|
96
|
+
category: "shell-web",
|
|
97
|
+
id: "shell-web-component-tab-link-item"
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(
|
|
101
|
+
LOCAL_LINK_ITEM_COMPONENT_DEFINITIONS.map((entry) => ({
|
|
102
|
+
token: entry.token,
|
|
103
|
+
componentFile: entry.componentFile,
|
|
104
|
+
componentName: entry.componentName,
|
|
105
|
+
templateFile: entry.templateFile
|
|
106
|
+
})),
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
token: "local.main.ui.menu-link-item",
|
|
110
|
+
componentFile: "src/components/menus/MenuLinkItem.vue",
|
|
111
|
+
componentName: "MenuLinkItem",
|
|
112
|
+
templateFile: "templates/src/components/menus/MenuLinkItem.vue"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
token: "local.main.ui.surface-aware-menu-link-item",
|
|
116
|
+
componentFile: "src/components/menus/SurfaceAwareMenuLinkItem.vue",
|
|
117
|
+
componentName: "SurfaceAwareMenuLinkItem",
|
|
118
|
+
templateFile: "templates/src/components/menus/SurfaceAwareMenuLinkItem.vue"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
token: "local.main.ui.tab-link-item",
|
|
122
|
+
componentFile: "src/components/menus/TabLinkItem.vue",
|
|
123
|
+
componentName: "TabLinkItem",
|
|
124
|
+
templateFile: "templates/src/components/menus/TabLinkItem.vue"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
);
|
|
128
|
+
assert.equal(findLocalLinkItemDefinition("local.main.ui.tab-link-item")?.componentName, "TabLinkItem");
|
|
129
|
+
assert.equal(await readLocalLinkItemComponentSource("local.main.ui.tab-link-item"), tabWrapperSource);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("shell-web generic menu link items support exact route matching", async () => {
|
|
133
|
+
const shellMenuSource = await readFile(
|
|
134
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "ShellMenuLinkItem.vue"),
|
|
135
|
+
"utf8"
|
|
136
|
+
);
|
|
137
|
+
const shellSurfaceAwareSource = await readFile(
|
|
138
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "ShellSurfaceAwareMenuLinkItem.vue"),
|
|
139
|
+
"utf8"
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
assert.match(shellMenuSource, /exact:\s*\{/);
|
|
143
|
+
assert.match(shellMenuSource, /:exact="props\.exact"/);
|
|
144
|
+
assert.match(shellSurfaceAwareSource, /exact:\s*\{/);
|
|
145
|
+
assert.match(shellSurfaceAwareSource, /:exact="props\.exact"/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("shell-web binds the local link-item wrapper tokens into MainClientProvider", () => {
|
|
149
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-menu-link-item-import"), {
|
|
150
|
+
op: "append-text",
|
|
151
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
152
|
+
position: "top",
|
|
153
|
+
skipIfContains: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";",
|
|
154
|
+
value: "import MenuLinkItem from \"/src/components/menus/MenuLinkItem.vue\";\n",
|
|
155
|
+
reason: "Bind app-owned shell menu link-item scaffold into local main client provider imports.",
|
|
156
|
+
category: "shell-web",
|
|
157
|
+
id: "shell-web-main-client-provider-menu-link-item-import"
|
|
158
|
+
});
|
|
159
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-surface-aware-menu-link-item-import"), {
|
|
160
|
+
op: "append-text",
|
|
161
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
162
|
+
position: "top",
|
|
163
|
+
skipIfContains: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";",
|
|
164
|
+
value: "import SurfaceAwareMenuLinkItem from \"/src/components/menus/SurfaceAwareMenuLinkItem.vue\";\n",
|
|
165
|
+
reason: "Bind app-owned shell surface-aware menu link-item scaffold into local main client provider imports.",
|
|
166
|
+
category: "shell-web",
|
|
167
|
+
id: "shell-web-main-client-provider-surface-aware-menu-link-item-import"
|
|
168
|
+
});
|
|
169
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-tab-link-item-import"), {
|
|
170
|
+
op: "append-text",
|
|
171
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
172
|
+
position: "top",
|
|
173
|
+
skipIfContains: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";",
|
|
174
|
+
value: "import TabLinkItem from \"/src/components/menus/TabLinkItem.vue\";\n",
|
|
175
|
+
reason: "Bind app-owned shell tab link-item scaffold into local main client provider imports.",
|
|
176
|
+
category: "shell-web",
|
|
177
|
+
id: "shell-web-main-client-provider-tab-link-item-import"
|
|
178
|
+
});
|
|
179
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-menu-link-item-register"), {
|
|
180
|
+
op: "append-text",
|
|
181
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
182
|
+
position: "bottom",
|
|
183
|
+
skipIfContains: "registerMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);",
|
|
184
|
+
value: "\nregisterMainClientComponent(\"local.main.ui.menu-link-item\", () => MenuLinkItem);\n",
|
|
185
|
+
reason: "Bind app-owned shell menu link-item token into local main client provider registry.",
|
|
186
|
+
category: "shell-web",
|
|
187
|
+
id: "shell-web-main-client-provider-menu-link-item-register"
|
|
188
|
+
});
|
|
189
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-surface-aware-menu-link-item-register"), {
|
|
190
|
+
op: "append-text",
|
|
191
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
192
|
+
position: "bottom",
|
|
193
|
+
skipIfContains: "registerMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);",
|
|
194
|
+
value: "\nregisterMainClientComponent(\"local.main.ui.surface-aware-menu-link-item\", () => SurfaceAwareMenuLinkItem);\n",
|
|
195
|
+
reason: "Bind app-owned shell surface-aware menu link-item token into local main client provider registry.",
|
|
196
|
+
category: "shell-web",
|
|
197
|
+
id: "shell-web-main-client-provider-surface-aware-menu-link-item-register"
|
|
198
|
+
});
|
|
199
|
+
assert.deepEqual(findTextMutation("shell-web-main-client-provider-tab-link-item-register"), {
|
|
200
|
+
op: "append-text",
|
|
201
|
+
file: "packages/main/src/client/providers/MainClientProvider.js",
|
|
202
|
+
position: "bottom",
|
|
203
|
+
skipIfContains: "registerMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);",
|
|
204
|
+
value: "\nregisterMainClientComponent(\"local.main.ui.tab-link-item\", () => TabLinkItem);\n",
|
|
205
|
+
reason: "Bind app-owned shell tab link-item token into local main client provider registry.",
|
|
206
|
+
category: "shell-web",
|
|
207
|
+
id: "shell-web-main-client-provider-tab-link-item-register"
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
9
|
+
|
|
10
|
+
test("shell-web outlet menu widget exposes a configurable nested outlet", async () => {
|
|
11
|
+
const source = await readFile(
|
|
12
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "ShellOutletMenuWidget.vue"),
|
|
13
|
+
"utf8"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
assert.match(source, /import \{ mdiCogOutline \} from "@mdi\/js";/);
|
|
17
|
+
assert.match(source, /defaultLinkComponentToken: \{/);
|
|
18
|
+
assert.match(source, /:target="props\.target"/);
|
|
19
|
+
assert.match(source, /:default-link-component-token="props\.defaultLinkComponentToken"/);
|
|
20
|
+
assert.match(source, /default: mdiCogOutline/);
|
|
21
|
+
assert.doesNotMatch(source, /mdi-[a-z0-9-]+/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("shell-web exports the outlet menu widget from both client index and package exports", async () => {
|
|
25
|
+
const clientIndexSource = await readFile(path.join(PACKAGE_DIR, "src", "client", "index.js"), "utf8");
|
|
26
|
+
assert.match(clientIndexSource, /export \{ default as ShellOutletMenuWidget \} from "\.\/components\/ShellOutletMenuWidget\.vue";/);
|
|
27
|
+
|
|
28
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
|
|
29
|
+
assert.equal(
|
|
30
|
+
packageJson?.exports?.["./client/components/ShellOutletMenuWidget"],
|
|
31
|
+
"./src/client/components/ShellOutletMenuWidget.vue"
|
|
32
|
+
);
|
|
33
|
+
});
|