@jskit-ai/ui-generator 0.1.16 → 0.1.17
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 +19 -16
- package/package.json +3 -2
- package/src/server/buildTemplateContext.js +2 -2
- package/src/server/subcommands/addSubpages.js +4 -5
- package/src/server/subcommands/element.js +1 -2
- package/src/server/subcommands/outlet.js +15 -21
- package/src/server/subcommands/page.js +3 -3
- package/src/server/subcommands/pageSupport.js +57 -114
- package/src/server/subcommands/support.js +5 -31
- package/test/addSubpagesSubcommand.test.js +38 -20
- package/test/buildTemplateContext.test.js +82 -28
- package/test/elementSubcommand.test.js +9 -7
- package/test/outletSubcommand.test.js +18 -18
- package/test/pageSubcommand.test.js +66 -14
- package/README.md +0 -214
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/ui-generator",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.17",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
|
|
7
7
|
options: {
|
|
@@ -40,14 +40,14 @@ export default Object.freeze({
|
|
|
40
40
|
inputType: "text",
|
|
41
41
|
defaultValue: "",
|
|
42
42
|
promptLabel: "Placement target",
|
|
43
|
-
promptHint: "Optional
|
|
43
|
+
promptHint: "Optional target for placed-element placement (format: host:position, default: shell-layout:top-right)."
|
|
44
44
|
},
|
|
45
45
|
"link-placement": {
|
|
46
46
|
required: false,
|
|
47
47
|
inputType: "text",
|
|
48
48
|
defaultValue: "",
|
|
49
49
|
promptLabel: "Link placement",
|
|
50
|
-
promptHint: "Optional
|
|
50
|
+
promptHint: "Optional target for the generated page link placement (format: host:position)."
|
|
51
51
|
},
|
|
52
52
|
"link-component-token": {
|
|
53
53
|
required: false,
|
|
@@ -63,15 +63,14 @@ export default Object.freeze({
|
|
|
63
63
|
defaultValue: "",
|
|
64
64
|
promptLabel: "Link to",
|
|
65
65
|
promptHint:
|
|
66
|
-
"Optional explicit props.to value for the generated page link placement (example: ./notes). If omitted for pages under a detected parent subpages
|
|
66
|
+
"Optional explicit props.to value for the generated page link placement (example: ./notes). If omitted for pages under a detected parent subpages target, it is inferred from the page path."
|
|
67
67
|
},
|
|
68
68
|
target: {
|
|
69
69
|
required: false,
|
|
70
70
|
inputType: "text",
|
|
71
71
|
defaultValue: "",
|
|
72
72
|
promptLabel: "Outlet target",
|
|
73
|
-
promptHint:
|
|
74
|
-
"Used by add-subpages and outlet. Accepts host or host:position. If only host is given, position defaults to sub-pages."
|
|
73
|
+
promptHint: "Used by add-subpages and outlet. Must be a target in host:position format."
|
|
75
74
|
},
|
|
76
75
|
title: {
|
|
77
76
|
required: false,
|
|
@@ -105,13 +104,14 @@ export default Object.freeze({
|
|
|
105
104
|
generatorPrimarySubcommand: "page",
|
|
106
105
|
generatorSubcommands: {
|
|
107
106
|
page: {
|
|
107
|
+
requiresShellWeb: true,
|
|
108
108
|
entrypoint: "src/server/subcommands/page.js",
|
|
109
109
|
export: "runGeneratorSubcommand",
|
|
110
110
|
description: "Create a route page at an explicit target file and add a link placement entry for it.",
|
|
111
111
|
longDescription: [
|
|
112
112
|
"This command always creates one route page file. By default, its page link is placed from the page path itself.",
|
|
113
|
-
"If an ancestor page has already been enhanced with sub-pages, JSKIT treats that ancestor as the real
|
|
114
|
-
"That means the generated link normally becomes a tab or child-page link under that ancestor
|
|
113
|
+
"If an ancestor page has already been enhanced with sub-pages, JSKIT treats that ancestor outlet as the real placement target. In that case the new page is linked into the nearest parent sub-pages outlet instead of the shell menu.",
|
|
114
|
+
"That means the generated link normally becomes a tab or child-page link under that ancestor target, and `props.to` is inferred relative to that target. If the outlet page is `index.vue`, child pages belong under `index/...` so the router keeps the parent page visible while the child route renders underneath it."
|
|
115
115
|
],
|
|
116
116
|
positionalArgs: [
|
|
117
117
|
{
|
|
@@ -122,8 +122,8 @@ export default Object.freeze({
|
|
|
122
122
|
],
|
|
123
123
|
optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
|
|
124
124
|
notes: [
|
|
125
|
-
"If a nearest parent subpages
|
|
126
|
-
"If the parent
|
|
125
|
+
"If a nearest parent subpages target is found, placement, link component token, and props.to are inferred automatically.",
|
|
126
|
+
"If the parent target page is index.vue, child pages belong under index/...",
|
|
127
127
|
"If the target page file already exists, rerun with --force to overwrite it."
|
|
128
128
|
],
|
|
129
129
|
examples: [
|
|
@@ -147,6 +147,7 @@ export default Object.freeze({
|
|
|
147
147
|
]
|
|
148
148
|
},
|
|
149
149
|
"placed-element": {
|
|
150
|
+
requiresShellWeb: true,
|
|
150
151
|
entrypoint: "src/server/subcommands/element.js",
|
|
151
152
|
export: "runGeneratorSubcommand",
|
|
152
153
|
description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
|
|
@@ -179,6 +180,7 @@ export default Object.freeze({
|
|
|
179
180
|
]
|
|
180
181
|
},
|
|
181
182
|
"add-subpages": {
|
|
183
|
+
requiresShellWeb: true,
|
|
182
184
|
entrypoint: "src/server/subcommands/addSubpages.js",
|
|
183
185
|
export: "runGeneratorSubcommand",
|
|
184
186
|
description: "Upgrade an existing page into a routed subpage host with SectionContainerShell, ShellOutlet, and RouterView.",
|
|
@@ -192,7 +194,7 @@ export default Object.freeze({
|
|
|
192
194
|
optionNames: ["target", "path", "title", "subtitle"],
|
|
193
195
|
notes: [
|
|
194
196
|
"Use this when the page should render shared content plus child routes below it.",
|
|
195
|
-
"If the
|
|
197
|
+
"If the outlet page is index.vue, create child pages under index/..."
|
|
196
198
|
],
|
|
197
199
|
examples: [
|
|
198
200
|
{
|
|
@@ -218,13 +220,14 @@ export default Object.freeze({
|
|
|
218
220
|
]
|
|
219
221
|
},
|
|
220
222
|
outlet: {
|
|
223
|
+
requiresShellWeb: true,
|
|
221
224
|
entrypoint: "src/server/subcommands/outlet.js",
|
|
222
225
|
export: "runGeneratorSubcommand",
|
|
223
226
|
description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
|
|
224
227
|
longDescription: [
|
|
225
228
|
"A ShellOutlet creates a named placement target inside a Vue file. That target is what other parts of JSKIT render into later.",
|
|
226
|
-
"After an outlet exists, `jskit list-placements` will discover it and show
|
|
227
|
-
"Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same
|
|
229
|
+
"After an outlet exists, `jskit list-placements` will discover it and show its `target`. That makes the outlet visible to humans and to generators that need a placement destination.",
|
|
230
|
+
"Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same target."
|
|
228
231
|
],
|
|
229
232
|
positionalArgs: [
|
|
230
233
|
{
|
|
@@ -236,7 +239,7 @@ export default Object.freeze({
|
|
|
236
239
|
optionNames: ["target"],
|
|
237
240
|
requiredOptionNames: ["target"],
|
|
238
241
|
notes: [
|
|
239
|
-
"Use --target host
|
|
242
|
+
"Use --target host:position."
|
|
240
243
|
],
|
|
241
244
|
examples: [
|
|
242
245
|
{
|
|
@@ -244,7 +247,7 @@ export default Object.freeze({
|
|
|
244
247
|
lines: [
|
|
245
248
|
"npx jskit generate ui-generator outlet \\",
|
|
246
249
|
" src/components/ContactSummaryCard.vue \\",
|
|
247
|
-
" --target contact-view"
|
|
250
|
+
" --target contact-view:sub-pages"
|
|
248
251
|
]
|
|
249
252
|
},
|
|
250
253
|
{
|
|
@@ -274,7 +277,7 @@ export default Object.freeze({
|
|
|
274
277
|
mutations: {
|
|
275
278
|
dependencies: {
|
|
276
279
|
runtime: {
|
|
277
|
-
"@jskit-ai/users-web": "0.1.
|
|
280
|
+
"@jskit-ai/users-web": "0.1.49"
|
|
278
281
|
},
|
|
279
282
|
dev: {}
|
|
280
283
|
},
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/ui-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@jskit-ai/kernel": "0.1.
|
|
9
|
+
"@jskit-ai/kernel": "0.1.34",
|
|
10
|
+
"@jskit-ai/shell-web": "0.1.33"
|
|
10
11
|
},
|
|
11
12
|
"exports": {
|
|
12
13
|
"./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
|
|
@@ -32,11 +32,11 @@ async function buildUiPageTemplateContext({
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
__JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
|
|
35
|
-
|
|
36
|
-
__JSKIT_UI_LINK_PLACEMENT_POSITION__: String(linkTarget.placementTarget?.position || ""),
|
|
35
|
+
__JSKIT_UI_LINK_PLACEMENT_TARGET__: String(linkTarget.placementTarget?.id || ""),
|
|
37
36
|
__JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
|
|
38
37
|
__JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
39
38
|
__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
39
|
+
__JSKIT_UI_LINK_WHEN_LINE__: String(linkTarget.whenLine || ""),
|
|
40
40
|
__JSKIT_UI_LINK_TO_PROP_LINE__: resolveLinkToPropLine(linkTarget.linkTo)
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
|
|
15
15
|
function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
|
|
16
16
|
const rawTarget = normalizeText(options?.target);
|
|
17
|
-
|
|
17
|
+
const defaultTarget = `${deriveDefaultSubpagesHost(pageTarget)}:${DEFAULT_SUBPAGES_POSITION}`;
|
|
18
|
+
return resolveOutletTargetId(rawTarget || defaultTarget, {
|
|
18
19
|
context: "ui-generator add-subpages",
|
|
19
|
-
optionName: "target"
|
|
20
|
-
defaultPosition: DEFAULT_SUBPAGES_POSITION
|
|
20
|
+
optionName: "target"
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -52,8 +52,7 @@ async function runGeneratorSubcommand({
|
|
|
52
52
|
const result = await upgradePageFileToSubpages({
|
|
53
53
|
appRoot,
|
|
54
54
|
targetFile,
|
|
55
|
-
|
|
56
|
-
position: outletTarget.position,
|
|
55
|
+
target: outletTarget.id,
|
|
57
56
|
title,
|
|
58
57
|
subtitle,
|
|
59
58
|
componentDirectory,
|
|
@@ -135,8 +135,7 @@ async function runGeneratorSubcommand({
|
|
|
135
135
|
"{\n" +
|
|
136
136
|
" addPlacement({\n" +
|
|
137
137
|
` id: "ui-generator.element.${elementNameKebab}",\n` +
|
|
138
|
-
`
|
|
139
|
-
` position: "${placementTarget.position}",\n` +
|
|
138
|
+
` target: "${placementTarget.id}",\n` +
|
|
140
139
|
` surfaces: ["${surface}"],\n` +
|
|
141
140
|
" order: 155,\n" +
|
|
142
141
|
` componentToken: "${componentToken}"\n` +
|
|
@@ -12,25 +12,21 @@ import {
|
|
|
12
12
|
indentBlock
|
|
13
13
|
} from "./support.js";
|
|
14
14
|
|
|
15
|
-
const DEFAULT_OUTLET_POSITION = "sub-pages";
|
|
16
|
-
|
|
17
15
|
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
18
16
|
const TEMPLATE_CLOSE_TAG_PATTERN = /<\/template>/gi;
|
|
19
17
|
const SHELL_OUTLET_TAG_PATTERN = /<ShellOutlet\b([^>]*)\/?>/gi;
|
|
20
18
|
|
|
21
|
-
function hasShellOutletTarget(source = "", {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (!normalizedHost || !normalizedPosition) {
|
|
19
|
+
function hasShellOutletTarget(source = "", { target = "" } = {}) {
|
|
20
|
+
const normalizedTarget = normalizeText(target);
|
|
21
|
+
if (!normalizedTarget) {
|
|
25
22
|
return false;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
const sourceText = String(source || "");
|
|
29
26
|
for (const match of sourceText.matchAll(SHELL_OUTLET_TAG_PATTERN)) {
|
|
30
27
|
const attributes = parseTagAttributes(match[1]);
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (outletHost === normalizedHost && outletPosition === normalizedPosition) {
|
|
28
|
+
const outletTarget = normalizeText(attributes.target);
|
|
29
|
+
if (outletTarget === normalizedTarget) {
|
|
34
30
|
return true;
|
|
35
31
|
}
|
|
36
32
|
}
|
|
@@ -75,8 +71,8 @@ function applyScriptImports(source = "") {
|
|
|
75
71
|
};
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
function createOutletBlock({
|
|
79
|
-
return `<ShellOutlet
|
|
74
|
+
function createOutletBlock({ target = "" } = {}) {
|
|
75
|
+
return `<ShellOutlet target="${target}" />`;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
function findLastTemplateCloseTag(source = "") {
|
|
@@ -88,9 +84,9 @@ function findLastTemplateCloseTag(source = "") {
|
|
|
88
84
|
return lastMatch;
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
function applyOutletTemplateBlock(source = "", {
|
|
87
|
+
function applyOutletTemplateBlock(source = "", { target = "" } = {}) {
|
|
92
88
|
const sourceText = String(source || "");
|
|
93
|
-
const outletBlock = createOutletBlock({
|
|
89
|
+
const outletBlock = createOutletBlock({ target });
|
|
94
90
|
|
|
95
91
|
const templateTagMatch = findLastTemplateCloseTag(sourceText);
|
|
96
92
|
if (!templateTagMatch) {
|
|
@@ -133,10 +129,9 @@ async function runGeneratorSubcommand({
|
|
|
133
129
|
|
|
134
130
|
const outletTarget = resolveOutletTargetId(options?.target, {
|
|
135
131
|
context: "ui-generator outlet",
|
|
136
|
-
optionName: "target"
|
|
137
|
-
defaultPosition: DEFAULT_OUTLET_POSITION
|
|
132
|
+
optionName: "target"
|
|
138
133
|
});
|
|
139
|
-
const
|
|
134
|
+
const targetId = outletTarget.id;
|
|
140
135
|
|
|
141
136
|
const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
|
|
142
137
|
context: "ui-generator outlet"
|
|
@@ -154,12 +149,11 @@ async function runGeneratorSubcommand({
|
|
|
154
149
|
throw new Error(`ui-generator outlet target file not found: ${targetFilePath.relativePath}.`);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
const hasTargetOutlet = hasShellOutletTarget(source, {
|
|
152
|
+
const hasTargetOutlet = hasShellOutletTarget(source, { target: targetId });
|
|
158
153
|
const templateApplied = hasTargetOutlet
|
|
159
154
|
? { changed: false, content: source }
|
|
160
155
|
: applyOutletTemplateBlock(source, {
|
|
161
|
-
|
|
162
|
-
position
|
|
156
|
+
target: targetId
|
|
163
157
|
});
|
|
164
158
|
const scriptApplied = applyScriptImports(templateApplied.content);
|
|
165
159
|
|
|
@@ -171,8 +165,8 @@ async function runGeneratorSubcommand({
|
|
|
171
165
|
return {
|
|
172
166
|
touchedFiles: changed ? [targetFilePath.relativePath] : [],
|
|
173
167
|
summary: changed
|
|
174
|
-
? `Injected outlet
|
|
175
|
-
: `Outlet
|
|
168
|
+
? `Injected outlet "${targetId}" into ${targetFilePath.relativePath}.`
|
|
169
|
+
: `Outlet "${targetId}" is already present in ${targetFilePath.relativePath}.`
|
|
176
170
|
};
|
|
177
171
|
}
|
|
178
172
|
|
|
@@ -25,8 +25,7 @@ function renderPageLinkPlacementBlock({
|
|
|
25
25
|
"{\n" +
|
|
26
26
|
" addPlacement({\n" +
|
|
27
27
|
` id: "${context.__JSKIT_UI_LINK_PLACEMENT_ID__}",\n` +
|
|
28
|
-
`
|
|
29
|
-
` position: "${context.__JSKIT_UI_LINK_PLACEMENT_POSITION__}",\n` +
|
|
28
|
+
` target: "${context.__JSKIT_UI_LINK_PLACEMENT_TARGET__}",\n` +
|
|
30
29
|
` surfaces: ["${surface}"],\n` +
|
|
31
30
|
" order: 155,\n" +
|
|
32
31
|
` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n` +
|
|
@@ -36,7 +35,7 @@ function renderPageLinkPlacementBlock({
|
|
|
36
35
|
` workspaceSuffix: "${context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__}",\n` +
|
|
37
36
|
` nonWorkspaceSuffix: "${context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__}",\n` +
|
|
38
37
|
`${context.__JSKIT_UI_LINK_TO_PROP_LINE__} },\n` +
|
|
39
|
-
|
|
38
|
+
`${String(context.__JSKIT_UI_LINK_WHEN_LINE__ || "")}` +
|
|
40
39
|
" });\n" +
|
|
41
40
|
"}\n"
|
|
42
41
|
);
|
|
@@ -124,6 +123,7 @@ async function runGeneratorSubcommand({
|
|
|
124
123
|
|
|
125
124
|
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
126
125
|
return {
|
|
126
|
+
placementComponentTokens: [String(placementContext.__JSKIT_UI_LINK_COMPONENT_TOKEN__ || "").trim()].filter(Boolean),
|
|
127
127
|
touchedFiles: touchedFileList,
|
|
128
128
|
summary: !pageAlreadyExisted
|
|
129
129
|
? `Generated UI page "${pageTarget.routeUrlSuffix}" at ${pageRelativePath}.`
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
resolveRequiredAppRoot,
|
|
8
8
|
toPosixPath
|
|
9
9
|
} from "@jskit-ai/kernel/server/support";
|
|
10
|
+
import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
11
|
+
import {
|
|
12
|
+
findLocalLinkItemDefinition,
|
|
13
|
+
readLocalLinkItemComponentSource
|
|
14
|
+
} from "@jskit-ai/shell-web/server/support/localLinkItemScaffolds";
|
|
10
15
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
11
16
|
import {
|
|
12
17
|
DEFAULT_COMPONENT_DIRECTORY,
|
|
@@ -20,8 +25,15 @@ import {
|
|
|
20
25
|
|
|
21
26
|
const DEFAULT_SUBPAGES_POSITION = "sub-pages";
|
|
22
27
|
const SECTION_CONTAINER_SHELL_COMPONENT = "SectionContainerShell";
|
|
23
|
-
const TAB_LINK_COMPONENT = "TabLinkItem";
|
|
24
28
|
const TAB_LINK_COMPONENT_TOKEN = "local.main.ui.tab-link-item";
|
|
29
|
+
const DEFAULT_MENU_COMPONENT_DIRECTORY = path.join(DEFAULT_COMPONENT_DIRECTORY, "menus");
|
|
30
|
+
const TAB_LINK_COMPONENT_DEFINITION = findLocalLinkItemDefinition(TAB_LINK_COMPONENT_TOKEN);
|
|
31
|
+
|
|
32
|
+
if (!TAB_LINK_COMPONENT_DEFINITION) {
|
|
33
|
+
throw new Error(`ui-generator add-subpages could not resolve ${TAB_LINK_COMPONENT_TOKEN} scaffold definition.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TAB_LINK_COMPONENT = TAB_LINK_COMPONENT_DEFINITION.componentName;
|
|
25
37
|
|
|
26
38
|
const ROUTE_TAG_PATTERN = /<route\b[^>]*>[\s\S]*?<\/route>\s*/gi;
|
|
27
39
|
const TEMPLATE_TOKEN_PATTERN = /<\/?template\b[^>]*>/gi;
|
|
@@ -110,89 +122,6 @@ const hasTabs = computed(() => Boolean(slots.tabs));
|
|
|
110
122
|
`;
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
function renderTabLinkItemSource() {
|
|
114
|
-
return `<script setup>
|
|
115
|
-
import { computed } from "vue";
|
|
116
|
-
import { useRoute } from "vue-router";
|
|
117
|
-
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
118
|
-
import {
|
|
119
|
-
normalizeMenuLinkPathname,
|
|
120
|
-
resolveMenuLinkTarget
|
|
121
|
-
} from "@jskit-ai/users-web/client/support/menuLinkTarget";
|
|
122
|
-
|
|
123
|
-
const props = defineProps({
|
|
124
|
-
label: {
|
|
125
|
-
type: String,
|
|
126
|
-
default: ""
|
|
127
|
-
},
|
|
128
|
-
to: {
|
|
129
|
-
type: String,
|
|
130
|
-
default: ""
|
|
131
|
-
},
|
|
132
|
-
surface: {
|
|
133
|
-
type: String,
|
|
134
|
-
default: ""
|
|
135
|
-
},
|
|
136
|
-
workspaceSuffix: {
|
|
137
|
-
type: String,
|
|
138
|
-
default: "/"
|
|
139
|
-
},
|
|
140
|
-
nonWorkspaceSuffix: {
|
|
141
|
-
type: String,
|
|
142
|
-
default: "/"
|
|
143
|
-
},
|
|
144
|
-
disabled: {
|
|
145
|
-
type: Boolean,
|
|
146
|
-
default: false
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const route = useRoute();
|
|
151
|
-
const paths = usePaths();
|
|
152
|
-
|
|
153
|
-
const resolvedTo = computed(() => {
|
|
154
|
-
return resolveMenuLinkTarget({
|
|
155
|
-
to: props.to,
|
|
156
|
-
surface: props.surface,
|
|
157
|
-
currentSurfaceId: paths.currentSurfaceId.value,
|
|
158
|
-
placementContext: paths.placementContext.value,
|
|
159
|
-
workspaceSuffix: props.workspaceSuffix,
|
|
160
|
-
nonWorkspaceSuffix: props.nonWorkspaceSuffix,
|
|
161
|
-
routeParams: route.params || {},
|
|
162
|
-
resolvePagePath(relativePath, options = {}) {
|
|
163
|
-
return paths.page(relativePath, options);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const isActive = computed(() => {
|
|
169
|
-
const targetPathname = normalizeMenuLinkPathname(resolvedTo.value);
|
|
170
|
-
const currentPathname = normalizeMenuLinkPathname(route.fullPath || route.path);
|
|
171
|
-
if (!targetPathname || !currentPathname) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
return currentPathname === targetPathname || currentPathname.startsWith(\`\${targetPathname}/\`);
|
|
175
|
-
});
|
|
176
|
-
</script>
|
|
177
|
-
|
|
178
|
-
<template>
|
|
179
|
-
<v-btn
|
|
180
|
-
v-if="resolvedTo"
|
|
181
|
-
class="tab-link-item text-none"
|
|
182
|
-
:to="resolvedTo"
|
|
183
|
-
rounded="pill"
|
|
184
|
-
size="small"
|
|
185
|
-
:variant="isActive ? 'flat' : 'tonal'"
|
|
186
|
-
:color="isActive ? 'primary' : undefined"
|
|
187
|
-
:disabled="props.disabled"
|
|
188
|
-
:aria-current="isActive ? 'page' : undefined"
|
|
189
|
-
>
|
|
190
|
-
{{ props.label }}
|
|
191
|
-
</v-btn>
|
|
192
|
-
</template>
|
|
193
|
-
`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
125
|
async function ensureSubpagesSupportScaffold({
|
|
197
126
|
appRoot,
|
|
198
127
|
componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
|
|
@@ -202,6 +131,10 @@ async function ensureSubpagesSupportScaffold({
|
|
|
202
131
|
context: "ui-generator add-subpages"
|
|
203
132
|
});
|
|
204
133
|
const normalizedComponentDirectory = normalizeText(componentDirectory) || DEFAULT_COMPONENT_DIRECTORY;
|
|
134
|
+
const normalizedTabLinkComponentDirectory =
|
|
135
|
+
normalizedComponentDirectory === DEFAULT_COMPONENT_DIRECTORY
|
|
136
|
+
? DEFAULT_MENU_COMPONENT_DIRECTORY
|
|
137
|
+
: normalizedComponentDirectory;
|
|
205
138
|
const providerPath = resolvePathWithinApp(resolvedAppRoot, MAIN_CLIENT_PROVIDER_FILE, {
|
|
206
139
|
context: "ui-generator add-subpages"
|
|
207
140
|
});
|
|
@@ -212,21 +145,34 @@ async function ensureSubpagesSupportScaffold({
|
|
|
212
145
|
);
|
|
213
146
|
const tabLinkPath = resolvePathWithinApp(
|
|
214
147
|
resolvedAppRoot,
|
|
215
|
-
path.join(
|
|
148
|
+
path.join(normalizedTabLinkComponentDirectory, `${TAB_LINK_COMPONENT}.vue`),
|
|
216
149
|
{ context: "ui-generator add-subpages" }
|
|
217
150
|
);
|
|
218
151
|
|
|
152
|
+
const providerSource = await readFile(providerPath.absolutePath, "utf8");
|
|
153
|
+
if (!/\bregisterMainClientComponent\s*\(/.test(providerSource)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`ui-generator add-subpages could not find registerMainClientComponent() contract in ${MAIN_CLIENT_PROVIDER_FILE}.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const providerRegisterLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT});`;
|
|
160
|
+
const providerHasTabLinkRegistration = providerSource.includes(providerRegisterLine);
|
|
219
161
|
const touchedFiles = new Set();
|
|
220
|
-
|
|
162
|
+
const supportFiles = [
|
|
221
163
|
{
|
|
222
164
|
path: sectionContainerShellPath,
|
|
223
165
|
desiredSource: renderSectionContainerShellSource()
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
path: tabLinkPath,
|
|
227
|
-
desiredSource: renderTabLinkItemSource()
|
|
228
166
|
}
|
|
229
|
-
]
|
|
167
|
+
];
|
|
168
|
+
if (!providerHasTabLinkRegistration) {
|
|
169
|
+
supportFiles.push({
|
|
170
|
+
path: tabLinkPath,
|
|
171
|
+
desiredSource: await readLocalLinkItemComponentSource(TAB_LINK_COMPONENT_DEFINITION)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const supportFile of supportFiles) {
|
|
230
176
|
let alreadyExists = true;
|
|
231
177
|
try {
|
|
232
178
|
await readFile(supportFile.path.absolutePath, "utf8");
|
|
@@ -245,15 +191,13 @@ async function ensureSubpagesSupportScaffold({
|
|
|
245
191
|
touchedFiles.add(supportFile.path.relativePath);
|
|
246
192
|
}
|
|
247
193
|
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
194
|
+
const providerImportLine = `import ${TAB_LINK_COMPONENT} from "/${toPosixPath(path.join(normalizedTabLinkComponentDirectory, `${TAB_LINK_COMPONENT}.vue`))}";`;
|
|
195
|
+
if (providerHasTabLinkRegistration) {
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
|
|
198
|
+
sectionContainerComponentImportPath: `/${toPosixPath(path.join(normalizedComponentDirectory, `${SECTION_CONTAINER_SHELL_COMPONENT}.vue`))}`
|
|
199
|
+
});
|
|
253
200
|
}
|
|
254
|
-
|
|
255
|
-
const providerImportLine = `import ${TAB_LINK_COMPONENT} from "/${toPosixPath(path.join(normalizedComponentDirectory, `${TAB_LINK_COMPONENT}.vue`))}";`;
|
|
256
|
-
const providerRegisterLine = `registerMainClientComponent("${TAB_LINK_COMPONENT_TOKEN}", () => ${TAB_LINK_COMPONENT});`;
|
|
257
201
|
const providerImportApplied = insertImportIfMissing(providerSource, providerImportLine);
|
|
258
202
|
const providerRegisterApplied = insertBeforeClassDeclaration(
|
|
259
203
|
providerImportApplied.content,
|
|
@@ -358,14 +302,18 @@ function renderSubpagesTemplate({
|
|
|
358
302
|
bodyContent = "",
|
|
359
303
|
title = "",
|
|
360
304
|
subtitle = "",
|
|
361
|
-
|
|
362
|
-
position = DEFAULT_SUBPAGES_POSITION
|
|
305
|
+
target = ""
|
|
363
306
|
} = {}) {
|
|
307
|
+
const normalizedTarget = normalizeShellOutletTargetId(target);
|
|
308
|
+
if (!normalizedTarget) {
|
|
309
|
+
throw new Error("ui-generator add-subpages requires target in \"host:position\" format.");
|
|
310
|
+
}
|
|
311
|
+
|
|
364
312
|
const lines = [
|
|
365
313
|
"<template>",
|
|
366
314
|
renderSectionContainerOpenTag({ title, subtitle }),
|
|
367
315
|
" <template #tabs>",
|
|
368
|
-
` <ShellOutlet
|
|
316
|
+
` <ShellOutlet target="${normalizedTarget}" default-link-component-token="${TAB_LINK_COMPONENT_TOKEN}" />`,
|
|
369
317
|
" </template>"
|
|
370
318
|
];
|
|
371
319
|
|
|
@@ -430,18 +378,16 @@ function applySubpagesScriptImports(source = "", { sectionContainerComponentImpo
|
|
|
430
378
|
function applySubpagesUpgradeToPageSource(
|
|
431
379
|
source = "",
|
|
432
380
|
{
|
|
433
|
-
|
|
434
|
-
position = DEFAULT_SUBPAGES_POSITION,
|
|
381
|
+
target = "",
|
|
435
382
|
title = "",
|
|
436
383
|
subtitle = "",
|
|
437
384
|
sectionContainerComponentImportPath = "/src/components/SectionContainerShell.vue",
|
|
438
385
|
preserveExistingContent = true
|
|
439
386
|
} = {}
|
|
440
387
|
) {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
throw new Error("ui-generator add-subpages requires a valid host.");
|
|
388
|
+
const normalizedTarget = normalizeShellOutletTargetId(target);
|
|
389
|
+
if (!normalizedTarget) {
|
|
390
|
+
throw new Error('ui-generator add-subpages requires target in "host:position" format.');
|
|
445
391
|
}
|
|
446
392
|
|
|
447
393
|
const sourceText = String(source || "");
|
|
@@ -454,8 +400,7 @@ function applySubpagesUpgradeToPageSource(
|
|
|
454
400
|
bodyContent,
|
|
455
401
|
title,
|
|
456
402
|
subtitle,
|
|
457
|
-
|
|
458
|
-
position: normalizedPosition
|
|
403
|
+
target: normalizedTarget
|
|
459
404
|
});
|
|
460
405
|
|
|
461
406
|
const nextSource = templateBlock
|
|
@@ -478,8 +423,7 @@ function hasExistingSubpagesRouting(source = "") {
|
|
|
478
423
|
async function upgradePageFileToSubpages({
|
|
479
424
|
appRoot,
|
|
480
425
|
targetFile,
|
|
481
|
-
|
|
482
|
-
position = DEFAULT_SUBPAGES_POSITION,
|
|
426
|
+
target = "",
|
|
483
427
|
title = "",
|
|
484
428
|
subtitle = "",
|
|
485
429
|
componentDirectory = DEFAULT_COMPONENT_DIRECTORY,
|
|
@@ -512,8 +456,7 @@ async function upgradePageFileToSubpages({
|
|
|
512
456
|
});
|
|
513
457
|
|
|
514
458
|
const upgradeApplied = applySubpagesUpgradeToPageSource(source, {
|
|
515
|
-
|
|
516
|
-
position,
|
|
459
|
+
target,
|
|
517
460
|
title,
|
|
518
461
|
subtitle,
|
|
519
462
|
sectionContainerComponentImportPath: supportScaffold.sectionContainerComponentImportPath,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveRequiredAppRoot,
|
|
4
4
|
toPosixPath
|
|
5
5
|
} from "@jskit-ai/kernel/server/support";
|
|
6
|
+
import { normalizeShellOutletTargetId } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
|
|
6
7
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
7
8
|
import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
|
|
8
9
|
|
|
@@ -44,32 +45,11 @@ function requireSinglePositionalTargetFile(args = [], { context = "ui-generator"
|
|
|
44
45
|
return positionalArgs[0];
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function normalizeExplicitOutletTargetId(value = "") {
|
|
48
|
-
const normalizedValue = normalizeText(value);
|
|
49
|
-
if (!normalizedValue) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const separatorIndex = normalizedValue.indexOf(":");
|
|
54
|
-
if (separatorIndex <= 0 || separatorIndex >= normalizedValue.length - 1) {
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const host = normalizeText(normalizedValue.slice(0, separatorIndex));
|
|
59
|
-
const position = normalizeText(normalizedValue.slice(separatorIndex + 1));
|
|
60
|
-
if (!host || !position) {
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return `${host}:${position}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
48
|
function resolveOutletTargetId(
|
|
68
49
|
rawTarget = "",
|
|
69
50
|
{
|
|
70
51
|
context = "ui-generator",
|
|
71
|
-
optionName = "target"
|
|
72
|
-
defaultPosition = ""
|
|
52
|
+
optionName = "target"
|
|
73
53
|
} = {}
|
|
74
54
|
) {
|
|
75
55
|
const normalizedTarget = normalizeText(rawTarget);
|
|
@@ -77,18 +57,13 @@ function resolveOutletTargetId(
|
|
|
77
57
|
throw new Error(`${context} requires --${optionName}.`);
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
const targetId = normalizedTarget
|
|
81
|
-
? normalizeExplicitOutletTargetId(normalizedTarget)
|
|
82
|
-
: normalizeExplicitOutletTargetId(`${normalizedTarget}:${normalizeText(defaultPosition)}`);
|
|
60
|
+
const targetId = normalizeShellOutletTargetId(normalizedTarget);
|
|
83
61
|
if (!targetId) {
|
|
84
|
-
throw new Error(`${context} option "${optionName}" must be
|
|
62
|
+
throw new Error(`${context} option "${optionName}" must be a target in "host:position" format.`);
|
|
85
63
|
}
|
|
86
64
|
|
|
87
|
-
const separatorIndex = targetId.indexOf(":");
|
|
88
65
|
return Object.freeze({
|
|
89
|
-
id: targetId
|
|
90
|
-
host: targetId.slice(0, separatorIndex),
|
|
91
|
-
position: targetId.slice(separatorIndex + 1)
|
|
66
|
+
id: targetId
|
|
92
67
|
});
|
|
93
68
|
}
|
|
94
69
|
|
|
@@ -295,7 +270,6 @@ export {
|
|
|
295
270
|
toPascalCase,
|
|
296
271
|
requireOption,
|
|
297
272
|
requireSinglePositionalTargetFile,
|
|
298
|
-
normalizeExplicitOutletTargetId,
|
|
299
273
|
resolveOutletTargetId,
|
|
300
274
|
rejectUnexpectedOptions,
|
|
301
275
|
resolvePathWithinApp,
|