cabloy 5.1.152 → 5.1.154
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-module-removal/SKILL.md +1 -1
- package/.claude/skills/cabloy-resource-field-update/SKILL.md +1 -1
- package/.claude/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +1 -1
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +46 -11
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +13 -0
- package/.claude/skills/cabloy-workflow/SKILL.md +13 -14
- package/.claude/skills/cabloy-workflow/evals/evals.json +4 -4
- package/CHANGELOG.md +27 -0
- package/CLAUDE.md +2 -3
- package/package.json +1 -1
- package/repo-docs/.vitepress/config.mjs +5 -0
- package/repo-docs/ai/cli-to-skill-map.md +19 -0
- package/repo-docs/ai/docs-skills-rules-mapping.md +21 -6
- package/repo-docs/ai/future-skill-roadmap.md +17 -72
- package/repo-docs/ai/introduction.md +15 -5
- package/repo-docs/ai/playbook-spec-execution.md +144 -0
- package/repo-docs/ai/playbook-spec-generation.md +155 -0
- package/repo-docs/ai/repo-guidance.md +9 -5
- package/repo-docs/ai/rules-and-config.md +2 -2
- package/repo-docs/ai/skills.md +4 -0
- package/repo-docs/ai/virtual-decorator-guidance.md +1 -1
- package/repo-docs/backend/introduction.md +1 -1
- package/repo-docs/backend/menu-guide.md +3 -2
- package/repo-docs/frontend/model-resource-cookbook.md +38 -7
- package/repo-docs/frontend/model-resource-usage-guide.md +2 -1
- package/repo-docs/frontend/router-tabs-introduction.md +1 -1
- package/repo-docs/frontend/use-state-data-best-practices.md +39 -6
- package/repo-docs/reference/package-map.md +4 -1
- package/repo-docs/reference/repo-scripts.md +25 -0
- package/repo-e2e/specs/cabloy-basic.spec.ts +69 -20
- package/test-results/.last-run.json +4 -2
- package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md +195 -0
- package/vona/env/.env +8 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +81 -145
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/package.json +1 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +40 -65
- package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +78 -72
- package/vona/src/suite/a-home/modules/home-base/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-home/modules/home-base/src/index.ts +1 -0
- package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +17 -10
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +5 -0
- package/vona/src/suite/a-home/modules/home-user/src/config/config.ts +36 -5
- package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +7 -2
- package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/types/env.ts +11 -0
- package/vona/src/suite/a-home/modules/home-user/src/types/index.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/config.test.ts +65 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +14 -6
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/.metadata/index.ts +17 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/event.resolveMenuVisibility.ts +19 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +32 -16
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/index.ts +2 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuEligibility.ts +107 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuVisibility.ts +91 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssr.ts +30 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +67 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuEligibility.test.ts +135 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuVisibility.test.ts +64 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/site.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/pnpm-lock.yaml +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +3 -2
- package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +6 -6
- package/zova/src/suite/a-home/modules/home-base/src/lib/index.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/lib/roleSiteAdmission.ts +15 -0
- package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +3 -3
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/menu.ts +4 -3
- package/zova/src/suite/a-home/modules/home-layoutweb/src/model/menu.ts +4 -3
- package/zova/src/suite/a-home/modules/home-passport/package.json +2 -1
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -2
- package/zova/src/suite/a-home/modules/home-passport/test/lib/roleSiteAdmission.test.ts +23 -0
- package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +29 -6
- package/zova/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/mutationSuccess.ts +11 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +12 -7
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/mutationSuccess.test.ts +95 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/meta.version.ts +0 -32
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/meta.version.test.ts +0 -44
|
@@ -15,6 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import { BeanBase, cast, deepExtend, SymbolModuleName } from 'vona';
|
|
16
16
|
import { checkErrorJwtExpiredAndThrow } from 'vona-module-a-jwt';
|
|
17
17
|
|
|
18
|
+
import type { TypeEventResolveMenuVisibilityResult } from '../bean/event.resolveMenuVisibility.ts';
|
|
18
19
|
import type { TypeEventRetrieveMenusResult } from '../bean/event.retrieveMenus.ts';
|
|
19
20
|
import type { TypeEventRetrieveMenusSiteResult } from '../bean/event.retrieveMenusSite.ts';
|
|
20
21
|
import type {
|
|
@@ -34,6 +35,13 @@ import type {
|
|
|
34
35
|
import { ServiceDevProxy } from '../service/devProxy.ts';
|
|
35
36
|
import { ServiceSsrHandler } from '../service/ssrHandler.ts';
|
|
36
37
|
import { SymbolCacheMenus } from './const.ts';
|
|
38
|
+
import { checkSsrBinding } from './ssrMenuEligibility.ts';
|
|
39
|
+
import {
|
|
40
|
+
projectPublicSsrMenuGroups,
|
|
41
|
+
projectPublicSsrMenus,
|
|
42
|
+
resolveSsrMenuVisibilityDefault,
|
|
43
|
+
resolveVisibleSsrMenuGroups,
|
|
44
|
+
} from './ssrMenuVisibility.ts';
|
|
37
45
|
|
|
38
46
|
export class BeanSsrSiteBase<
|
|
39
47
|
SsrSiteOptions extends IDecoratorSsrSiteOptions = IDecoratorSsrSiteOptions,
|
|
@@ -154,13 +162,22 @@ export class BeanSsrSiteBase<
|
|
|
154
162
|
async retrieveMenus(): Promise<TypeEventRetrieveMenusResult | undefined> {
|
|
155
163
|
const menusPrepared = await this._getMenusCache(this.ctx.locale);
|
|
156
164
|
if (!menusPrepared) return;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
165
|
+
const menusPreparedVisible = await this.$scope.ssr.event.resolveMenuVisibility.emit(
|
|
166
|
+
{
|
|
167
|
+
ssrSiteName: this.$onionName,
|
|
168
|
+
menus: menusPrepared.menus ?? [],
|
|
169
|
+
currentRoleIds: (this.bean.passport.currentRoles ?? []).map(role => role.id),
|
|
170
|
+
},
|
|
171
|
+
async data => this._resolveMenuVisibilityDefault(data.menus),
|
|
172
|
+
);
|
|
173
|
+
const groupsPreparedVisible = resolveVisibleSsrMenuGroups(
|
|
174
|
+
menusPreparedVisible,
|
|
175
|
+
menusPrepared.groups ?? [],
|
|
176
|
+
);
|
|
177
|
+
return {
|
|
178
|
+
menus: projectPublicSsrMenus(menusPreparedVisible),
|
|
179
|
+
groups: projectPublicSsrMenuGroups(groupsPreparedVisible),
|
|
180
|
+
};
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
public get siteOptions() {
|
|
@@ -282,6 +299,12 @@ export class BeanSsrSiteBase<
|
|
|
282
299
|
};
|
|
283
300
|
}
|
|
284
301
|
|
|
302
|
+
private _resolveMenuVisibilityDefault(
|
|
303
|
+
menus: ISsrMenuItemPrepared[],
|
|
304
|
+
): TypeEventResolveMenuVisibilityResult {
|
|
305
|
+
return resolveSsrMenuVisibilityDefault(menus, roles => this.bean.passport.checkRoleName(roles));
|
|
306
|
+
}
|
|
307
|
+
|
|
285
308
|
private async _getMenusCache(
|
|
286
309
|
locale: keyof ILocaleRecord,
|
|
287
310
|
): Promise<TypeEventRetrieveMenusSiteResult | undefined> {
|
|
@@ -335,8 +358,8 @@ export class BeanSsrSiteBase<
|
|
|
335
358
|
for (const ssrMenu of ssrMenus) {
|
|
336
359
|
const siteMenuOptions = ssrMenu.beanOptions
|
|
337
360
|
.options as IDecoratorSsrMenuOptions<IDecoratorSsrSiteOptions>;
|
|
338
|
-
if (!
|
|
339
|
-
if (!
|
|
361
|
+
if (!checkSsrBinding(siteOnionName, siteMenuOptions.site)) continue;
|
|
362
|
+
if (!checkSsrBinding(locale, siteMenuOptions.locale)) continue;
|
|
340
363
|
const menusFrom =
|
|
341
364
|
siteMenuOptions.items || (siteMenuOptions.item && { '': siteMenuOptions.item });
|
|
342
365
|
if (!menusFrom) continue;
|
|
@@ -356,10 +379,3 @@ export class BeanSsrSiteBase<
|
|
|
356
379
|
return menus;
|
|
357
380
|
}
|
|
358
381
|
}
|
|
359
|
-
|
|
360
|
-
function _checkValidSiteOrLocale(expect: any, target: any) {
|
|
361
|
-
if (!target) return true;
|
|
362
|
-
if (Array.isArray(target) && target.includes(expect)) return true;
|
|
363
|
-
if (expect === target) return true;
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
@@ -2,5 +2,7 @@ export * from './beanSsrSiteBase.ts';
|
|
|
2
2
|
export * from './const.ts';
|
|
3
3
|
export * from './ssr.ts';
|
|
4
4
|
export * from './ssrMenu.ts';
|
|
5
|
+
export * from './ssrMenuEligibility.ts';
|
|
6
|
+
export * from './ssrMenuVisibility.ts';
|
|
5
7
|
export * from './ssrMenuGroup.ts';
|
|
6
8
|
export * from './ssrSite.ts';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { IOnionSlice } from 'vona-module-a-onion';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
IDecoratorSsrMenuOptions,
|
|
5
|
+
ISsrMenuCatalog,
|
|
6
|
+
ISsrMenuEligibility,
|
|
7
|
+
ISsrMenuRecord,
|
|
8
|
+
} from '../types/ssrMenu.ts';
|
|
9
|
+
import type { IDecoratorSsrMenuGroupOptions, ISsrMenuGroupRecord } from '../types/ssrMenuGroup.ts';
|
|
10
|
+
import type { IDecoratorSsrSiteOptions } from '../types/ssrSite.ts';
|
|
11
|
+
|
|
12
|
+
export function checkSsrBinding<T>(expect: T, target: T | T[] | null | ''): boolean {
|
|
13
|
+
if (target === undefined || target === null || target === '') return true;
|
|
14
|
+
if (Array.isArray(target)) return target.some(item => item === expect);
|
|
15
|
+
return target === expect;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveSsrMenuCatalog(
|
|
19
|
+
sites: ISsrMenuCatalog['sites'],
|
|
20
|
+
ssrMenus: IOnionSlice<ISsrMenuRecord, keyof ISsrMenuRecord, unknown>[],
|
|
21
|
+
ssrMenuGroups: IOnionSlice<ISsrMenuGroupRecord, keyof ISsrMenuGroupRecord, unknown>[] = [],
|
|
22
|
+
): ISsrMenuCatalog {
|
|
23
|
+
const menus: ISsrMenuCatalog['menus'] = [];
|
|
24
|
+
const groups: ISsrMenuCatalog['groups'] = [];
|
|
25
|
+
for (const { ssrSiteName } of sites) {
|
|
26
|
+
for (const ssrMenu of ssrMenus) {
|
|
27
|
+
const options = ssrMenu.beanOptions.options as
|
|
28
|
+
| IDecoratorSsrMenuOptions<IDecoratorSsrSiteOptions>
|
|
29
|
+
| undefined;
|
|
30
|
+
if (!options || !checkSsrBinding(ssrSiteName, options.site)) continue;
|
|
31
|
+
const items = options.items || (options.item && { '': options.item });
|
|
32
|
+
if (!items) continue;
|
|
33
|
+
for (const key of Object.keys(items)) {
|
|
34
|
+
const item = items[key];
|
|
35
|
+
menus.push({
|
|
36
|
+
ssrSiteName,
|
|
37
|
+
ssrMenuName: key ? `${ssrMenu.name}#${key}` : ssrMenu.name,
|
|
38
|
+
onionName: ssrMenu.name,
|
|
39
|
+
roles: item.roles,
|
|
40
|
+
...withoutUndefined({
|
|
41
|
+
title: item.title,
|
|
42
|
+
description: item.description,
|
|
43
|
+
icon: item.icon,
|
|
44
|
+
order: item.order,
|
|
45
|
+
group: item.group,
|
|
46
|
+
separator: item.separator,
|
|
47
|
+
link: item.link,
|
|
48
|
+
external: item.external,
|
|
49
|
+
target: item.target,
|
|
50
|
+
meta: item.meta,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const { ssrSiteName } of sites) {
|
|
57
|
+
for (const ssrMenuGroup of ssrMenuGroups) {
|
|
58
|
+
const options = ssrMenuGroup.beanOptions.options as
|
|
59
|
+
| IDecoratorSsrMenuGroupOptions<IDecoratorSsrSiteOptions>
|
|
60
|
+
| undefined;
|
|
61
|
+
if (!options || !checkSsrBinding(ssrSiteName, options.site) || !options.item) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const item = options.item;
|
|
65
|
+
groups.push({
|
|
66
|
+
ssrSiteName,
|
|
67
|
+
ssrMenuGroupName: ssrMenuGroup.name,
|
|
68
|
+
onionName: ssrMenuGroup.name,
|
|
69
|
+
title: item.title,
|
|
70
|
+
description: item.description,
|
|
71
|
+
icon: item.icon,
|
|
72
|
+
order: item.order,
|
|
73
|
+
group: item.group,
|
|
74
|
+
collapsed: item.collapsed,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { sites: [...sites], menus, groups };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withoutUndefined<T extends object>(value: T): T {
|
|
82
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resolveSsrMenuEligibility(
|
|
86
|
+
ssrSiteName: string,
|
|
87
|
+
ssrMenuName: string,
|
|
88
|
+
ssrMenus: IOnionSlice<ISsrMenuRecord, keyof ISsrMenuRecord, unknown>[],
|
|
89
|
+
): ISsrMenuEligibility | undefined {
|
|
90
|
+
for (const ssrMenu of ssrMenus) {
|
|
91
|
+
const options = ssrMenu.beanOptions.options as
|
|
92
|
+
| IDecoratorSsrMenuOptions<IDecoratorSsrSiteOptions>
|
|
93
|
+
| undefined;
|
|
94
|
+
if (!options || !checkSsrBinding(ssrSiteName, options.site)) continue;
|
|
95
|
+
const menus = options.items || (options.item && { '': options.item });
|
|
96
|
+
if (!menus) continue;
|
|
97
|
+
for (const key of Object.keys(menus)) {
|
|
98
|
+
const name = key ? `${ssrMenu.name}#${key}` : ssrMenu.name;
|
|
99
|
+
if (name !== ssrMenuName) continue;
|
|
100
|
+
return {
|
|
101
|
+
ssrSiteName,
|
|
102
|
+
ssrMenuName,
|
|
103
|
+
rolesDefined: menus[key].roles !== undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { IMenuGroup, IMenuItem } from 'vona-module-a-menu';
|
|
2
|
+
|
|
3
|
+
import type { ISsrMenuItemPrepared } from '../types/ssrMenu.ts';
|
|
4
|
+
|
|
5
|
+
export function resolveSsrMenuVisibilityDefault(
|
|
6
|
+
menus: ISsrMenuItemPrepared[],
|
|
7
|
+
checkRoleName: (roles: NonNullable<ISsrMenuItemPrepared['roles']>) => boolean,
|
|
8
|
+
): ISsrMenuItemPrepared[] {
|
|
9
|
+
return menus.filter(menu => {
|
|
10
|
+
if (menu.roles === undefined) return true;
|
|
11
|
+
if (!menu.roles.length) return false;
|
|
12
|
+
return checkRoleName(menu.roles);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveVisibleSsrMenuGroups(
|
|
17
|
+
menus: ISsrMenuItemPrepared[],
|
|
18
|
+
groups: IMenuGroup[],
|
|
19
|
+
): IMenuGroup[] {
|
|
20
|
+
const groupsByName = new Map(groups.map(group => [group.name, group]));
|
|
21
|
+
const visibleGroupNames = new Set<string>();
|
|
22
|
+
const retainGroup = (name: string) => {
|
|
23
|
+
const group = groupsByName.get(name);
|
|
24
|
+
if (!group || visibleGroupNames.has(name)) return;
|
|
25
|
+
visibleGroupNames.add(name);
|
|
26
|
+
for (const parentName of getGroupNames(group.group)) {
|
|
27
|
+
retainGroup(parentName);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
for (const menu of menus) {
|
|
31
|
+
for (const groupName of getGroupNames(menu.group)) {
|
|
32
|
+
retainGroup(groupName);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return groups.filter(group => visibleGroupNames.has(group.name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function projectPublicSsrMenus(menus: ISsrMenuItemPrepared[]): IMenuItem[] {
|
|
39
|
+
return menus.map(menu => {
|
|
40
|
+
const {
|
|
41
|
+
name,
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
icon,
|
|
45
|
+
order,
|
|
46
|
+
group,
|
|
47
|
+
separator,
|
|
48
|
+
link,
|
|
49
|
+
external,
|
|
50
|
+
target,
|
|
51
|
+
meta,
|
|
52
|
+
} = menu;
|
|
53
|
+
return _withoutUndefined<IMenuItem>({
|
|
54
|
+
name,
|
|
55
|
+
title,
|
|
56
|
+
description,
|
|
57
|
+
icon,
|
|
58
|
+
order,
|
|
59
|
+
group,
|
|
60
|
+
separator,
|
|
61
|
+
link,
|
|
62
|
+
external,
|
|
63
|
+
target,
|
|
64
|
+
meta,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function projectPublicSsrMenuGroups(groups: IMenuGroup[]): IMenuGroup[] {
|
|
70
|
+
return groups.map(group => {
|
|
71
|
+
const { name, title, description, icon, order, group: parentGroup, collapsed } = group;
|
|
72
|
+
return _withoutUndefined<IMenuGroup>({
|
|
73
|
+
name,
|
|
74
|
+
title,
|
|
75
|
+
description,
|
|
76
|
+
icon,
|
|
77
|
+
order,
|
|
78
|
+
group: parentGroup,
|
|
79
|
+
collapsed,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _withoutUndefined<T extends { name: string }>(value: T): T {
|
|
85
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getGroupNames(group: string | string[] | undefined): string[] {
|
|
89
|
+
if (!group) return [];
|
|
90
|
+
return Array.isArray(group) ? group : [group];
|
|
91
|
+
}
|
|
@@ -3,9 +3,11 @@ import type { IOnionSlice } from 'vona-module-a-onion';
|
|
|
3
3
|
import { BeanBase } from 'vona';
|
|
4
4
|
import { Service } from 'vona-module-a-bean';
|
|
5
5
|
|
|
6
|
+
import type { ISsrMenuCatalog, ISsrMenuEligibility } from '../types/ssrMenu.ts';
|
|
6
7
|
import type { IDecoratorSsrSiteOptions, ISsrSiteRecord } from '../types/ssrSite.ts';
|
|
7
8
|
|
|
8
9
|
import { SymbolCacheSites } from '../lib/const.ts';
|
|
10
|
+
import { resolveSsrMenuCatalog, resolveSsrMenuEligibility } from '../lib/ssrMenuEligibility.ts';
|
|
9
11
|
|
|
10
12
|
@Service()
|
|
11
13
|
export class ServiceSsr extends BeanBase {
|
|
@@ -66,6 +68,34 @@ export class ServiceSsr extends BeanBase {
|
|
|
66
68
|
return this.app.meta[SymbolCacheSites][cacheKey];
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
public getMenuCatalog(): ISsrMenuCatalog {
|
|
72
|
+
const sites = this.getSitesEnabled().map(site => {
|
|
73
|
+
const options = site.beanOptions.options as IDecoratorSsrSiteOptions;
|
|
74
|
+
return {
|
|
75
|
+
ssrSiteName: site.name,
|
|
76
|
+
title: this.app.meta.text.locale(this.ctx.locale, options.title),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return resolveSsrMenuCatalog(
|
|
80
|
+
sites,
|
|
81
|
+
this.bean.onion.ssrMenu.getOnionsEnabled(),
|
|
82
|
+
this.bean.onion.ssrMenuGroup.getOnionsEnabled(),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public resolveMenuEligibility(
|
|
87
|
+
ssrSiteName: string,
|
|
88
|
+
ssrMenuName: string,
|
|
89
|
+
): ISsrMenuEligibility | undefined {
|
|
90
|
+
const site = this.getSitesEnabled().find(item => item.name === ssrSiteName);
|
|
91
|
+
if (!site) return;
|
|
92
|
+
return resolveSsrMenuEligibility(
|
|
93
|
+
ssrSiteName,
|
|
94
|
+
ssrMenuName,
|
|
95
|
+
this.bean.onion.ssrMenu.getOnionsEnabled(),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
public prepareMenuLink(
|
|
70
100
|
link?: keyof IDecoratorSsrSiteOptions['pages'],
|
|
71
101
|
): keyof IDecoratorSsrSiteOptions['pages'] | undefined {
|
|
@@ -18,11 +18,78 @@ export interface ISsrMenuItem<Pages extends {} = {}, Icons extends {} = {}> exte
|
|
|
18
18
|
|
|
19
19
|
export type ISsrMenuItemPrepared = IMenuItem<any, any> & Pick<ISsrMenuItem<any, any>, 'roles'>;
|
|
20
20
|
|
|
21
|
+
export interface ISsrMenuEligibility {
|
|
22
|
+
ssrSiteName: string;
|
|
23
|
+
ssrMenuName: string;
|
|
24
|
+
rolesDefined: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ISsrMenuCatalogSiteInfo {
|
|
28
|
+
ssrSiteName: string;
|
|
29
|
+
title: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ISsrMenuCatalogMenu {
|
|
33
|
+
ssrSiteName: string;
|
|
34
|
+
ssrMenuName: string;
|
|
35
|
+
onionName: string;
|
|
36
|
+
roles?: (keyof IRoleNameRecord)[];
|
|
37
|
+
title?: IMenuItem['title'];
|
|
38
|
+
description?: IMenuItem['description'];
|
|
39
|
+
icon?: IMenuItem['icon'];
|
|
40
|
+
order?: IMenuItem['order'];
|
|
41
|
+
group?: IMenuItem['group'];
|
|
42
|
+
separator?: IMenuItem['separator'];
|
|
43
|
+
link?: IMenuItem['link'];
|
|
44
|
+
external?: IMenuItem['external'];
|
|
45
|
+
target?: IMenuItem['target'];
|
|
46
|
+
meta?: IMenuItem['meta'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ISsrMenuCatalogGroup {
|
|
50
|
+
ssrSiteName: string;
|
|
51
|
+
ssrMenuGroupName: string;
|
|
52
|
+
onionName: string;
|
|
53
|
+
title?: IMenuGroup['title'];
|
|
54
|
+
description?: IMenuGroup['description'];
|
|
55
|
+
icon?: IMenuGroup['icon'];
|
|
56
|
+
order?: IMenuGroup['order'];
|
|
57
|
+
group?: IMenuGroup['group'];
|
|
58
|
+
collapsed?: IMenuGroup['collapsed'];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ISsrMenuCatalog {
|
|
62
|
+
sites: ISsrMenuCatalogSiteInfo[];
|
|
63
|
+
menus: ISsrMenuCatalogMenu[];
|
|
64
|
+
groups: ISsrMenuCatalogGroup[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ISsrMenuCatalogPresentationMenu extends IMenuItem<any, any> {
|
|
68
|
+
ssrSiteName: string;
|
|
69
|
+
ssrMenuName: string;
|
|
70
|
+
configurable: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ISsrMenuCatalogPresentationGroup extends IMenuGroup<any> {
|
|
74
|
+
ssrSiteName: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ISsrMenuCatalogPresentation {
|
|
78
|
+
menus: ISsrMenuCatalogPresentationMenu[];
|
|
79
|
+
groups: ISsrMenuCatalogPresentationGroup[];
|
|
80
|
+
}
|
|
81
|
+
|
|
21
82
|
export interface ISsrMenusPrepared {
|
|
22
83
|
menus?: ISsrMenuItemPrepared[];
|
|
23
84
|
groups?: IMenuGroup[];
|
|
24
85
|
}
|
|
25
86
|
|
|
87
|
+
export interface ISsrMenuCatalogSite {
|
|
88
|
+
ssrSiteName: string;
|
|
89
|
+
menus: ISsrMenuItemPrepared[];
|
|
90
|
+
groups: IMenuGroup[];
|
|
91
|
+
}
|
|
92
|
+
|
|
26
93
|
// should not set default generic = IDecoratorSsrSiteOptions
|
|
27
94
|
export interface IDecoratorSsrMenuOptions<
|
|
28
95
|
SsrSiteOptions extends IDecoratorSsrSiteOptions,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
checkSsrBinding,
|
|
6
|
+
resolveSsrMenuCatalog,
|
|
7
|
+
resolveSsrMenuEligibility,
|
|
8
|
+
} from '../src/lib/ssrMenuEligibility.ts';
|
|
9
|
+
|
|
10
|
+
function createMenus() {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
name: 'test:omitted',
|
|
14
|
+
beanOptions: {
|
|
15
|
+
options: {
|
|
16
|
+
item: { roles: [] },
|
|
17
|
+
locale: ['en-us'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'test:keyed',
|
|
23
|
+
beanOptions: {
|
|
24
|
+
options: {
|
|
25
|
+
site: ['test:admin', 'test:web'],
|
|
26
|
+
items: {
|
|
27
|
+
static: { roles: ['systemAdmin'] },
|
|
28
|
+
public: {},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'test:malformed',
|
|
35
|
+
beanOptions: {},
|
|
36
|
+
},
|
|
37
|
+
] as any;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('ssrMenuEligibility.test.ts', () => {
|
|
41
|
+
it('matches unset, scalar, or array bindings', () => {
|
|
42
|
+
assert.equal(checkSsrBinding('test:admin', undefined), true);
|
|
43
|
+
assert.equal(checkSsrBinding('test:admin', null), true);
|
|
44
|
+
assert.equal(checkSsrBinding('test:admin', ''), true);
|
|
45
|
+
assert.equal(checkSsrBinding('test:admin', 'test:admin'), true);
|
|
46
|
+
assert.equal(checkSsrBinding('test:web', 'test:admin'), false);
|
|
47
|
+
assert.equal(checkSsrBinding('test:web', ['test:admin', 'test:web']), true);
|
|
48
|
+
assert.equal(checkSsrBinding('en-us', ['zh-cn', 'en-us']), true);
|
|
49
|
+
assert.equal(checkSsrBinding('en-us', []), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('derives every enabled-site partition with site and onion identities', () => {
|
|
53
|
+
const catalog = resolveSsrMenuCatalog(
|
|
54
|
+
[
|
|
55
|
+
{ ssrSiteName: 'test:admin', title: 'Admin' },
|
|
56
|
+
{ ssrSiteName: 'test:web', title: 'Web' },
|
|
57
|
+
],
|
|
58
|
+
createMenus(),
|
|
59
|
+
);
|
|
60
|
+
assert.deepEqual(catalog.sites, [
|
|
61
|
+
{ ssrSiteName: 'test:admin', title: 'Admin' },
|
|
62
|
+
{ ssrSiteName: 'test:web', title: 'Web' },
|
|
63
|
+
]);
|
|
64
|
+
assert.deepEqual(catalog.menus, [
|
|
65
|
+
{
|
|
66
|
+
ssrSiteName: 'test:admin',
|
|
67
|
+
ssrMenuName: 'test:omitted',
|
|
68
|
+
onionName: 'test:omitted',
|
|
69
|
+
roles: [],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ssrSiteName: 'test:admin',
|
|
73
|
+
ssrMenuName: 'test:keyed#static',
|
|
74
|
+
onionName: 'test:keyed',
|
|
75
|
+
roles: ['systemAdmin'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
ssrSiteName: 'test:admin',
|
|
79
|
+
ssrMenuName: 'test:keyed#public',
|
|
80
|
+
onionName: 'test:keyed',
|
|
81
|
+
roles: undefined,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ssrSiteName: 'test:web',
|
|
85
|
+
ssrMenuName: 'test:omitted',
|
|
86
|
+
onionName: 'test:omitted',
|
|
87
|
+
roles: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
ssrSiteName: 'test:web',
|
|
91
|
+
ssrMenuName: 'test:keyed#static',
|
|
92
|
+
onionName: 'test:keyed',
|
|
93
|
+
roles: ['systemAdmin'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
ssrSiteName: 'test:web',
|
|
97
|
+
ssrMenuName: 'test:keyed#public',
|
|
98
|
+
onionName: 'test:keyed',
|
|
99
|
+
roles: undefined,
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('resolves final leaves independently of locale', () => {
|
|
105
|
+
const menus = createMenus();
|
|
106
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:admin', 'test:omitted', menus), {
|
|
107
|
+
ssrSiteName: 'test:admin',
|
|
108
|
+
ssrMenuName: 'test:omitted',
|
|
109
|
+
rolesDefined: true,
|
|
110
|
+
});
|
|
111
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:web', 'test:omitted', menus), {
|
|
112
|
+
ssrSiteName: 'test:web',
|
|
113
|
+
ssrMenuName: 'test:omitted',
|
|
114
|
+
rolesDefined: true,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('distinguishes dynamic/static leaves from public leaves and exact keyed names', () => {
|
|
119
|
+
const menus = createMenus();
|
|
120
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:admin', 'test:keyed#static', menus), {
|
|
121
|
+
ssrSiteName: 'test:admin',
|
|
122
|
+
ssrMenuName: 'test:keyed#static',
|
|
123
|
+
rolesDefined: true,
|
|
124
|
+
});
|
|
125
|
+
assert.deepEqual(resolveSsrMenuEligibility('test:web', 'test:keyed#public', menus), {
|
|
126
|
+
ssrSiteName: 'test:web',
|
|
127
|
+
ssrMenuName: 'test:keyed#public',
|
|
128
|
+
rolesDefined: false,
|
|
129
|
+
});
|
|
130
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:keyed', menus), undefined);
|
|
131
|
+
assert.equal(resolveSsrMenuEligibility('test:other', 'test:keyed#static', menus), undefined);
|
|
132
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:missing', menus), undefined);
|
|
133
|
+
assert.equal(resolveSsrMenuEligibility('test:admin', 'test:malformed', menus), undefined);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
projectPublicSsrMenuGroups,
|
|
6
|
+
projectPublicSsrMenus,
|
|
7
|
+
resolveSsrMenuVisibilityDefault,
|
|
8
|
+
resolveVisibleSsrMenuGroups,
|
|
9
|
+
} from '../src/lib/ssrMenuVisibility.ts';
|
|
10
|
+
|
|
11
|
+
function createMenus() {
|
|
12
|
+
return [
|
|
13
|
+
{ name: 'test:public', title: 'Public', group: 'root' },
|
|
14
|
+
{ name: 'test:dynamic', title: 'Dynamic', roles: [], group: 'root' },
|
|
15
|
+
{ name: 'test:static', title: 'Static', roles: ['member'] },
|
|
16
|
+
] as any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ssrMenuVisibility.test.ts', () => {
|
|
20
|
+
it('distinguishes public, dynamic-only, and static leaves', () => {
|
|
21
|
+
const menus = createMenus();
|
|
22
|
+
assert.deepEqual(
|
|
23
|
+
resolveSsrMenuVisibilityDefault(menus, roles => roles.includes('member')),
|
|
24
|
+
[menus[0], menus[2]],
|
|
25
|
+
);
|
|
26
|
+
assert.deepEqual(
|
|
27
|
+
resolveSsrMenuVisibilityDefault(menus, () => false),
|
|
28
|
+
[menus[0]],
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('retains only groups that contain a visible descendant', () => {
|
|
33
|
+
const groups = [
|
|
34
|
+
{ name: 'root', title: 'Root' },
|
|
35
|
+
{ name: 'nested', title: 'Nested', group: 'root' },
|
|
36
|
+
{ name: 'empty', title: 'Empty' },
|
|
37
|
+
];
|
|
38
|
+
const menus = [{ name: 'test:visible', group: 'nested' }] as any;
|
|
39
|
+
assert.deepEqual(resolveVisibleSsrMenuGroups(menus, groups), [groups[0], groups[1]]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('projects only public menu and group fields', () => {
|
|
43
|
+
const menus = [
|
|
44
|
+
{
|
|
45
|
+
name: 'test:private',
|
|
46
|
+
title: 'Private',
|
|
47
|
+
roles: ['member'],
|
|
48
|
+
policyRevision: 'private',
|
|
49
|
+
associationState: 'private',
|
|
50
|
+
},
|
|
51
|
+
] as any;
|
|
52
|
+
const groups = [{ name: 'root', title: 'Root', catalogState: 'private' }] as any;
|
|
53
|
+
const result = {
|
|
54
|
+
menus: projectPublicSsrMenus(menus),
|
|
55
|
+
groups: projectPublicSsrMenuGroups(groups),
|
|
56
|
+
};
|
|
57
|
+
assert.deepEqual(result.menus, [{ name: 'test:private', title: 'Private' }]);
|
|
58
|
+
assert.deepEqual(result.groups, [{ name: 'root', title: 'Root' }]);
|
|
59
|
+
const serialized = JSON.stringify(result);
|
|
60
|
+
for (const forbidden of ['roles', 'policyRevision', 'associationState', 'catalogState']) {
|
|
61
|
+
assert.equal(serialized.includes(forbidden), false, forbidden);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-suite-a-cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.48",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"vona-module-a-cabloy": "workspace:^",
|
|
11
11
|
"vona-module-a-markdown": "^5.0.3",
|
|
12
|
-
"vona-module-a-rbac": "^5.0.
|
|
12
|
+
"vona-module-a-rbac": "^5.0.2",
|
|
13
13
|
"vona-module-a-socket": "workspace:^",
|
|
14
14
|
"vona-module-a-ssr": "workspace:^",
|
|
15
15
|
"vona-module-a-ssrhmr": "workspace:^"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const roleSiteIdAll = '*';
|