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.
Files changed (95) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-module-removal/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-resource-field-update/SKILL.md +1 -1
  4. package/.claude/skills/cabloy-spec-generation/SKILL.md +1 -1
  5. package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +1 -1
  6. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +46 -11
  7. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +13 -0
  8. package/.claude/skills/cabloy-workflow/SKILL.md +13 -14
  9. package/.claude/skills/cabloy-workflow/evals/evals.json +4 -4
  10. package/CHANGELOG.md +27 -0
  11. package/CLAUDE.md +2 -3
  12. package/package.json +1 -1
  13. package/repo-docs/.vitepress/config.mjs +5 -0
  14. package/repo-docs/ai/cli-to-skill-map.md +19 -0
  15. package/repo-docs/ai/docs-skills-rules-mapping.md +21 -6
  16. package/repo-docs/ai/future-skill-roadmap.md +17 -72
  17. package/repo-docs/ai/introduction.md +15 -5
  18. package/repo-docs/ai/playbook-spec-execution.md +144 -0
  19. package/repo-docs/ai/playbook-spec-generation.md +155 -0
  20. package/repo-docs/ai/repo-guidance.md +9 -5
  21. package/repo-docs/ai/rules-and-config.md +2 -2
  22. package/repo-docs/ai/skills.md +4 -0
  23. package/repo-docs/ai/virtual-decorator-guidance.md +1 -1
  24. package/repo-docs/backend/introduction.md +1 -1
  25. package/repo-docs/backend/menu-guide.md +3 -2
  26. package/repo-docs/frontend/model-resource-cookbook.md +38 -7
  27. package/repo-docs/frontend/model-resource-usage-guide.md +2 -1
  28. package/repo-docs/frontend/router-tabs-introduction.md +1 -1
  29. package/repo-docs/frontend/use-state-data-best-practices.md +39 -6
  30. package/repo-docs/reference/package-map.md +4 -1
  31. package/repo-docs/reference/repo-scripts.md +25 -0
  32. package/repo-e2e/specs/cabloy-basic.spec.ts +69 -20
  33. package/test-results/.last-run.json +4 -2
  34. package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md +195 -0
  35. package/vona/env/.env +8 -0
  36. package/vona/packages-vona/vona/package.json +1 -1
  37. package/vona/pnpm-lock.yaml +81 -145
  38. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/package.json +1 -2
  39. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +40 -65
  40. package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +78 -72
  41. package/vona/src/suite/a-home/modules/home-base/src/.metadata/locales.ts +18 -0
  42. package/vona/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +3 -0
  43. package/vona/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +3 -0
  44. package/vona/src/suite/a-home/modules/home-base/src/index.ts +1 -0
  45. package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +17 -10
  46. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +5 -0
  47. package/vona/src/suite/a-home/modules/home-user/src/config/config.ts +36 -5
  48. package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +7 -2
  49. package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +3 -0
  50. package/vona/src/suite/a-home/modules/home-user/src/types/env.ts +11 -0
  51. package/vona/src/suite/a-home/modules/home-user/src/types/index.ts +1 -0
  52. package/vona/src/suite/a-home/modules/home-user/test/config.test.ts +65 -0
  53. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +14 -6
  54. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  55. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  56. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/.metadata/index.ts +17 -2
  57. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/event.resolveMenuVisibility.ts +19 -0
  58. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +32 -16
  59. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/index.ts +2 -0
  60. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuEligibility.ts +107 -0
  61. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/ssrMenuVisibility.ts +91 -0
  62. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssr.ts +30 -0
  63. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +67 -0
  64. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuEligibility.test.ts +135 -0
  65. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuVisibility.test.ts +64 -0
  66. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  67. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/index.ts +1 -0
  69. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/site.ts +1 -0
  70. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  71. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +1 -0
  73. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  74. package/zova/pnpm-lock.yaml +1 -1
  75. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +3 -2
  76. package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +6 -6
  77. package/zova/src/suite/a-home/modules/home-base/src/lib/index.ts +1 -0
  78. package/zova/src/suite/a-home/modules/home-base/src/lib/roleSiteAdmission.ts +15 -0
  79. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +3 -3
  80. package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/menu.ts +4 -3
  81. package/zova/src/suite/a-home/modules/home-layoutweb/src/model/menu.ts +4 -3
  82. package/zova/src/suite/a-home/modules/home-passport/package.json +2 -1
  83. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -2
  84. package/zova/src/suite/a-home/modules/home-passport/test/lib/roleSiteAdmission.test.ts +23 -0
  85. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +29 -6
  86. package/zova/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  87. package/zova/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  88. package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +1 -1
  89. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  90. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/mutationSuccess.ts +11 -0
  91. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +12 -7
  92. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/mutationSuccess.test.ts +95 -0
  93. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  94. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/meta.version.ts +0 -32
  95. 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 menus = menusPrepared.menus
158
- ?.filter(menu => !menu.roles?.length || this.bean.passport.checkRoleName(menu.roles))
159
- .map(menu => {
160
- const { roles: _roles, ...menuPublic } = menu;
161
- return menuPublic;
162
- });
163
- return { menus, groups: menusPrepared.groups };
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 (!_checkValidSiteOrLocale(siteOnionName, siteMenuOptions.site)) continue;
339
- if (!_checkValidSiteOrLocale(locale, siteMenuOptions.locale)) continue;
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.47",
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.1",
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:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapiutils",
3
- "version": "5.1.21",
3
+ "version": "5.1.22",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,2 +1,3 @@
1
1
  export * from './database.ts';
2
2
  export * from './decorator.ts';
3
+ export * from './site.ts';
@@ -0,0 +1 @@
1
+ export const roleSiteIdAll = '*';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.17",
3
+ "version": "5.1.18",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-user",
3
- "version": "5.1.23",
3
+ "version": "5.1.24",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -15,6 +15,7 @@ export interface IRole {
15
15
  title: string;
16
16
  titleLocales?: LocalizedTextMap;
17
17
  siteIds: string[];
18
+ builtin?: boolean;
18
19
  }
19
20
 
20
21
  export interface IRoleAdapter {