cabloy 5.1.153 → 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-spec-generation/scripts/generate-implementation-charts.mjs +46 -11
- package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +13 -0
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/repo-docs/backend/menu-guide.md +3 -2
- package/vona/env/.env +8 -0
- package/vona/packages-vona/vona/package.json +1 -1
- 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-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/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
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import type { LocalizedTextMap } from 'vona-module-a-user';
|
|
2
2
|
|
|
3
|
+
import { roleSiteIdAll } from 'vona-module-a-openapiutils';
|
|
4
|
+
|
|
3
5
|
export interface IRoleConfig {
|
|
4
6
|
title: string;
|
|
5
7
|
titleLocales?: LocalizedTextMap;
|
|
6
8
|
siteIds: string[];
|
|
9
|
+
builtin: boolean;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export const builtinRoles: Record<string, IRoleConfig> = {
|
|
10
13
|
registeredUser: {
|
|
11
14
|
title: 'Registered User',
|
|
15
|
+
builtin: true,
|
|
12
16
|
titleLocales: {
|
|
13
17
|
'zh-cn': '注册用户',
|
|
14
18
|
},
|
|
15
|
-
siteIds: [
|
|
19
|
+
siteIds: [roleSiteIdAll],
|
|
16
20
|
},
|
|
17
21
|
systemAdmin: {
|
|
18
22
|
title: 'System Administrator',
|
|
23
|
+
builtin: true,
|
|
19
24
|
titleLocales: {
|
|
20
25
|
'zh-cn': '系统管理员',
|
|
21
26
|
},
|
|
22
|
-
siteIds: [
|
|
27
|
+
siteIds: [roleSiteIdAll],
|
|
23
28
|
},
|
|
24
29
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module 'vona' {
|
|
2
|
+
export interface VonaConfigEnv {
|
|
3
|
+
HOME_USER_PASSWORD_DEFAULT_ADMIN: string | undefined;
|
|
4
|
+
HOME_USER_DISABLE_BOOTSTRAP_SYSTEM_ADMIN: string | undefined;
|
|
5
|
+
HOME_USER_DISABLE_USER_ADMIN: string | undefined;
|
|
6
|
+
HOME_USER_BUILTIN_ROLE_REGISTERED_USER_SITE_IDS: string | undefined;
|
|
7
|
+
HOME_USER_BUILTIN_ROLE_SYSTEM_ADMIN_SITE_IDS: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { config } from '../src/config/config.ts';
|
|
5
|
+
import { builtinRoles } from '../src/config/roles.ts';
|
|
6
|
+
|
|
7
|
+
function createConfig(env: Record<string, string | undefined>) {
|
|
8
|
+
return config({ meta: { env } } as never);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('config.test.ts', () => {
|
|
12
|
+
it('uses environment overrides and defaults without mutating static role definitions', () => {
|
|
13
|
+
const configured = createConfig({
|
|
14
|
+
HOME_USER_PASSWORD_DEFAULT_ADMIN: 'strong-admin-password',
|
|
15
|
+
HOME_USER_DISABLE_BOOTSTRAP_SYSTEM_ADMIN: 'true',
|
|
16
|
+
HOME_USER_DISABLE_USER_ADMIN: 'true',
|
|
17
|
+
HOME_USER_BUILTIN_ROLE_REGISTERED_USER_SITE_IDS: 'web',
|
|
18
|
+
HOME_USER_BUILTIN_ROLE_SYSTEM_ADMIN_SITE_IDS: 'web,admin',
|
|
19
|
+
});
|
|
20
|
+
assert.equal(configured.passwordDefault.admin, 'strong-admin-password');
|
|
21
|
+
assert.equal(configured.disableBootstrapSystemAdmin, true);
|
|
22
|
+
assert.equal(configured.disableUserAdmin, true);
|
|
23
|
+
assert.deepEqual(configured.builtinRoles.registeredUser.siteIds, ['web']);
|
|
24
|
+
assert.deepEqual(configured.builtinRoles.systemAdmin.siteIds, ['web', 'admin']);
|
|
25
|
+
assert.deepEqual(builtinRoles.registeredUser.siteIds, ['*']);
|
|
26
|
+
assert.deepEqual(builtinRoles.systemAdmin.siteIds, ['*']);
|
|
27
|
+
|
|
28
|
+
const defaults = createConfig({
|
|
29
|
+
HOME_USER_PASSWORD_DEFAULT_ADMIN: '',
|
|
30
|
+
HOME_USER_DISABLE_BOOTSTRAP_SYSTEM_ADMIN: 'false',
|
|
31
|
+
HOME_USER_DISABLE_USER_ADMIN: 'invalid',
|
|
32
|
+
});
|
|
33
|
+
assert.equal(defaults.passwordDefault.admin, '123456');
|
|
34
|
+
assert.equal(defaults.disableBootstrapSystemAdmin, false);
|
|
35
|
+
assert.equal(defaults.disableUserAdmin, false);
|
|
36
|
+
assert.deepEqual(defaults.builtinRoles.registeredUser.siteIds, ['*']);
|
|
37
|
+
assert.deepEqual(defaults.builtinRoles.systemAdmin.siteIds, ['*']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('parses comma-delimited role site IDs and falls back for blank values', () => {
|
|
41
|
+
const configured = createConfig({
|
|
42
|
+
HOME_USER_BUILTIN_ROLE_REGISTERED_USER_SITE_IDS: ' web, admin, , store ',
|
|
43
|
+
HOME_USER_BUILTIN_ROLE_SYSTEM_ADMIN_SITE_IDS: ' , ',
|
|
44
|
+
});
|
|
45
|
+
assert.deepEqual(configured.builtinRoles.registeredUser.siteIds, ['web', 'admin', 'store']);
|
|
46
|
+
assert.deepEqual(configured.builtinRoles.systemAdmin.siteIds, ['*']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects non-exclusive wildcard role site IDs', () => {
|
|
50
|
+
assert.throws(
|
|
51
|
+
() =>
|
|
52
|
+
createConfig({
|
|
53
|
+
HOME_USER_BUILTIN_ROLE_REGISTERED_USER_SITE_IDS: '*,web',
|
|
54
|
+
}),
|
|
55
|
+
/HOME_USER_BUILTIN_ROLE_REGISTERED_USER_SITE_IDS/,
|
|
56
|
+
);
|
|
57
|
+
assert.throws(
|
|
58
|
+
() =>
|
|
59
|
+
createConfig({
|
|
60
|
+
HOME_USER_BUILTIN_ROLE_SYSTEM_ADMIN_SITE_IDS: 'web,*',
|
|
61
|
+
}),
|
|
62
|
+
/HOME_USER_BUILTIN_ROLE_SYSTEM_ADMIN_SITE_IDS/,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -9,13 +9,17 @@ describe('role.test.ts', () => {
|
|
|
9
9
|
const roleRegisteredUser = await scope.model.role.getByName('registeredUser');
|
|
10
10
|
assert.equal(roleRegisteredUser?.title, 'Registered User');
|
|
11
11
|
assert.deepEqual(roleRegisteredUser?.titleLocales, { 'zh-cn': '注册用户' });
|
|
12
|
-
assert.
|
|
12
|
+
assert.deepEqual(
|
|
13
|
+
roleRegisteredUser?.siteIds,
|
|
14
|
+
scope.config.builtinRoles.registeredUser.siteIds,
|
|
15
|
+
);
|
|
16
|
+
assert.equal(roleRegisteredUser?.builtin, true);
|
|
13
17
|
|
|
14
18
|
const roleSystemAdmin = await scope.model.role.getByName('systemAdmin');
|
|
15
19
|
assert.equal(roleSystemAdmin?.title, 'System Administrator');
|
|
16
20
|
assert.deepEqual(roleSystemAdmin?.titleLocales, { 'zh-cn': '系统管理员' });
|
|
17
|
-
assert.
|
|
18
|
-
assert.
|
|
21
|
+
assert.deepEqual(roleSystemAdmin?.siteIds, scope.config.builtinRoles.systemAdmin.siteIds);
|
|
22
|
+
assert.equal(roleSystemAdmin?.builtin, true);
|
|
19
23
|
|
|
20
24
|
await app.bean.passport.signinMock();
|
|
21
25
|
const passport = await app.bean.executor.performAction('get', '/home/user/passport/current');
|
|
@@ -24,13 +28,17 @@ describe('role.test.ts', () => {
|
|
|
24
28
|
);
|
|
25
29
|
assert.equal(passportRegisteredUser?.title, 'Registered User');
|
|
26
30
|
assert.deepEqual(passportRegisteredUser?.titleLocales, { 'zh-cn': '注册用户' });
|
|
27
|
-
assert.
|
|
31
|
+
assert.deepEqual(
|
|
32
|
+
passportRegisteredUser?.siteIds,
|
|
33
|
+
scope.config.builtinRoles.registeredUser.siteIds,
|
|
34
|
+
);
|
|
35
|
+
assert.equal(passportRegisteredUser?.builtin, true);
|
|
28
36
|
|
|
29
37
|
const passportSystemAdmin = passport.roles.find((role: any) => role.name === 'systemAdmin');
|
|
30
38
|
assert.equal(passportSystemAdmin?.title, 'System Administrator');
|
|
31
39
|
assert.deepEqual(passportSystemAdmin?.titleLocales, { 'zh-cn': '系统管理员' });
|
|
32
|
-
assert.
|
|
33
|
-
assert.
|
|
40
|
+
assert.deepEqual(passportSystemAdmin?.siteIds, scope.config.builtinRoles.systemAdmin.siteIds);
|
|
41
|
+
assert.equal(passportSystemAdmin?.builtin, true);
|
|
34
42
|
});
|
|
35
43
|
});
|
|
36
44
|
});
|
|
@@ -162,6 +162,7 @@ declare module 'vona' {
|
|
|
162
162
|
}
|
|
163
163
|
/** service: end */
|
|
164
164
|
/** event: begin */
|
|
165
|
+
export * from '../bean/event.resolveMenuVisibility.ts';
|
|
165
166
|
export * from '../bean/event.retrieveMenus.ts';
|
|
166
167
|
export * from '../bean/event.retrieveMenusSite.ts';
|
|
167
168
|
|
|
@@ -172,6 +173,16 @@ declare module 'vona' {
|
|
|
172
173
|
}
|
|
173
174
|
declare module 'vona-module-a-ssr' {
|
|
174
175
|
|
|
176
|
+
export interface EventResolveMenuVisibility {
|
|
177
|
+
/** @internal */
|
|
178
|
+
get scope(): ScopeModuleASsr;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface EventResolveMenuVisibility {
|
|
182
|
+
get $beanFullName(): 'a-ssr.event.resolveMenuVisibility';
|
|
183
|
+
get $onionName(): 'a-ssr:resolveMenuVisibility';
|
|
184
|
+
}
|
|
185
|
+
|
|
175
186
|
export interface EventRetrieveMenus {
|
|
176
187
|
/** @internal */
|
|
177
188
|
get scope(): ScopeModuleASsr;
|
|
@@ -194,20 +205,24 @@ declare module 'vona-module-a-ssr' {
|
|
|
194
205
|
}
|
|
195
206
|
/** event: end */
|
|
196
207
|
/** event: begin */
|
|
208
|
+
import type { EventResolveMenuVisibility } from '../bean/event.resolveMenuVisibility.ts';
|
|
197
209
|
import type { EventRetrieveMenus } from '../bean/event.retrieveMenus.ts';
|
|
198
210
|
import type { EventRetrieveMenusSite } from '../bean/event.retrieveMenusSite.ts';
|
|
199
211
|
export interface IModuleEvent {
|
|
200
|
-
'
|
|
212
|
+
'resolveMenuVisibility': EventResolveMenuVisibility;
|
|
213
|
+
'retrieveMenus': EventRetrieveMenus;
|
|
201
214
|
'retrieveMenusSite': EventRetrieveMenusSite;
|
|
202
215
|
}
|
|
203
216
|
/** event: end */
|
|
204
217
|
/** event: begin */
|
|
218
|
+
import type { TypeEventResolveMenuVisibilityData, TypeEventResolveMenuVisibilityResult } from '../bean/event.resolveMenuVisibility.ts';
|
|
205
219
|
import type { TypeEventRetrieveMenusData, TypeEventRetrieveMenusResult } from '../bean/event.retrieveMenus.ts';
|
|
206
220
|
import type { TypeEventRetrieveMenusSiteData, TypeEventRetrieveMenusSiteResult } from '../bean/event.retrieveMenusSite.ts';
|
|
207
221
|
import type { EventOn } from 'vona-module-a-event';
|
|
208
222
|
declare module 'vona-module-a-event' {
|
|
209
223
|
export interface IEventRecord {
|
|
210
|
-
'a-ssr:
|
|
224
|
+
'a-ssr:resolveMenuVisibility': EventOn<TypeEventResolveMenuVisibilityData, TypeEventResolveMenuVisibilityResult>;
|
|
225
|
+
'a-ssr:retrieveMenus': EventOn<TypeEventRetrieveMenusData, TypeEventRetrieveMenusResult>;
|
|
211
226
|
'a-ssr:retrieveMenusSite': EventOn<TypeEventRetrieveMenusSiteData, TypeEventRetrieveMenusSiteResult>;
|
|
212
227
|
}
|
|
213
228
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
|
|
3
|
+
import { BeanEventBase, Event } from 'vona-module-a-event';
|
|
4
|
+
|
|
5
|
+
import type { ISsrMenuItemPrepared } from '../types/ssrMenu.ts';
|
|
6
|
+
|
|
7
|
+
export interface TypeEventResolveMenuVisibilityData {
|
|
8
|
+
ssrSiteName: string;
|
|
9
|
+
menus: ISsrMenuItemPrepared[];
|
|
10
|
+
currentRoleIds: TableIdentity[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type TypeEventResolveMenuVisibilityResult = ISsrMenuItemPrepared[];
|
|
14
|
+
|
|
15
|
+
@Event()
|
|
16
|
+
export class EventResolveMenuVisibility extends BeanEventBase<
|
|
17
|
+
TypeEventResolveMenuVisibilityData,
|
|
18
|
+
TypeEventResolveMenuVisibilityResult
|
|
19
|
+
> {}
|
|
@@ -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,
|