cabloy 5.1.176 → 5.1.178
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/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/repo-docs/backend/menu-guide.md +37 -3
- package/repo-e2e/specs/cabloy-basic.spec.ts +68 -1
- package/repo-e2e/specs/controller-boundary.spec.ts +42 -0
- package/repo-e2e/specs/routed-dialog.spec.ts +15 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +1 -1
- package/zova/packages-zova/zova-core/src/bean/beanContainer.ts +4 -0
- package/zova/packages-zova/zova-core/src/composables/useController.ts +4 -1
- package/zova/pnpm-lock.yaml +1 -1
- package/zova/src/suite/a-home/modules/home-indexadmin/src/page/dashboard/controller.tsx +31 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx +18 -2
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +13 -22
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerGuardDisposers.ts +19 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +12 -3
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/pageMeta.ts +6 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routerView.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routerGuardDisposers.test.ts +24 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +3 -3
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.178
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.178
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add `setPageMeta` support for configuring page metadata.
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- Fix router behavior.
|
|
12
|
+
- Correct `isAttemptActive` handling.
|
|
13
|
+
- Ensure route resources are ready before initializing controller data.
|
|
14
|
+
|
|
15
|
+
## 5.1.177
|
|
16
|
+
|
|
17
|
+
### Improvements
|
|
18
|
+
|
|
19
|
+
- Update the menu guide documentation.
|
|
20
|
+
- Refresh the pnpm lockfile.
|
|
21
|
+
|
|
3
22
|
## 5.1.176
|
|
4
23
|
|
|
5
24
|
### Improvements
|
package/package.json
CHANGED
|
@@ -55,9 +55,11 @@ This makes menu retrieval part of the broader backend contract surface.
|
|
|
55
55
|
|
|
56
56
|
In the current repo implementation, the out-of-the-box menu controller is public and delegates directly to `this.scope.service.menu.retrieveMenus(publicPath)`.
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## SSR Site targeting
|
|
59
59
|
|
|
60
|
-
`@SsrMenu(...)`
|
|
60
|
+
`@SsrMenu(...)` and `@SsrMenuGroup(...)` use `site` to select the SSR Site onion(s) that receive the declaration. It is a navigation-composition filter, not a site-admission, role-visibility, or backend-authorization decision.
|
|
61
|
+
|
|
62
|
+
Use `site` for navigation owned by one SSR Site or deliberately exclusive to a known subset of sites:
|
|
61
63
|
|
|
62
64
|
```typescript
|
|
63
65
|
@SsrMenu({
|
|
@@ -66,10 +68,41 @@ In the current repo implementation, the out-of-the-box menu controller is public
|
|
|
66
68
|
link: 'presetResource',
|
|
67
69
|
roles: ['systemAdmin'],
|
|
68
70
|
},
|
|
69
|
-
|
|
71
|
+
// This Basic Admin-owned entry intentionally targets one SSR Site.
|
|
72
|
+
site: ['basic-siteadmin:admin'],
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For a shared module declaration intended for every compatible SSR Admin site, omit `site`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
@SsrMenu({
|
|
80
|
+
item: {
|
|
81
|
+
title: $locale('SharedOperations'),
|
|
82
|
+
link: 'presetResource',
|
|
83
|
+
roles: ['systemAdmin'],
|
|
84
|
+
},
|
|
85
|
+
// No `site`: the shared declaration is available to compatible SSR Admin sites.
|
|
70
86
|
})
|
|
71
87
|
```
|
|
72
88
|
|
|
89
|
+
Do not bind a shared Admin menu or menu group to the current/default Admin site merely to keep a new independent site's navigation narrow. Isolate an independent site through its enabled module composition and site-owned menus/groups. When a shared capability needs genuinely different navigation contracts per site, use an explicit, approved site-specific composition or owner instead of narrowing the shared declaration by default.
|
|
90
|
+
|
|
91
|
+
`site` is distinct from a role's `siteIds`, menu `roles` or role-menu associations, and Passport/RBAC authorization:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
site -> which SSR Site receives a menu/group declaration
|
|
95
|
+
siteIds -> which frontend Site a subject may enter
|
|
96
|
+
roles / role-menu -> which admitted subject may discover a menu leaf
|
|
97
|
+
Passport / RBAC -> which backend actions or data the subject may use
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See [Menu Authorization](/backend/menu-authorization) for the site-admission, navigation-disclosure, and backend-authority boundaries.
|
|
101
|
+
|
|
102
|
+
## Static menu visibility
|
|
103
|
+
|
|
104
|
+
`@SsrMenu(...)` items can declare static role visibility without changing the public menu DTO:
|
|
105
|
+
|
|
73
106
|
- Omit `roles` to make an item visible to anonymous and authenticated callers.
|
|
74
107
|
- Use `roles: []` when an item has no static role visibility and is intended to be disclosed only through dynamic Role-menu configuration.
|
|
75
108
|
- A nonempty `roles` array is visible when the current Passport has at least one matching role name; it can also be disclosed through dynamic Role-menu configuration.
|
|
@@ -107,5 +140,6 @@ When editing SSR menu behavior, ask:
|
|
|
107
140
|
2. is there a default fallback menu that should remain available?
|
|
108
141
|
3. does the menu contract belong in backend API design, frontend route design, or both?
|
|
109
142
|
4. does the active edition affect the menu structure or public path assumptions?
|
|
143
|
+
5. is this declaration site-owned or deliberately site-exclusive (specify `site`), or shared across compatible SSR Admin sites (omit `site`)?
|
|
110
144
|
|
|
111
145
|
That helps AI keep menu behavior aligned with Cabloy’s shared SSR architecture.
|
|
@@ -28,6 +28,16 @@ function collectPageErrors(page: Page) {
|
|
|
28
28
|
return errors;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function collectConsoleErrors(page: Page) {
|
|
32
|
+
const errors: string[] = [];
|
|
33
|
+
page.on('console', message => {
|
|
34
|
+
if (message.type() === 'error') {
|
|
35
|
+
errors.push(message.text());
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return errors;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
async function loginAsAdmin(page: Page) {
|
|
32
42
|
await page.goto('/admin/', { waitUntil: 'load' });
|
|
33
43
|
if (page.url().includes('/admin/login')) {
|
|
@@ -319,7 +329,64 @@ test(
|
|
|
319
329
|
);
|
|
320
330
|
|
|
321
331
|
test(
|
|
322
|
-
'ATP-BASIC-FLOW-01:
|
|
332
|
+
'ATP-BASIC-FLOW-01: cold Admin navigation initializes dynamic resource selectors',
|
|
333
|
+
{ tag: ['@admin', '@flow'] },
|
|
334
|
+
async ({ browser }) => {
|
|
335
|
+
const context = await browser.newContext();
|
|
336
|
+
const page = await context.newPage();
|
|
337
|
+
const pageErrors = collectPageErrors(page);
|
|
338
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
339
|
+
try {
|
|
340
|
+
await page.goto('/admin/login', { waitUntil: 'load' });
|
|
341
|
+
await page.evaluate(() => {
|
|
342
|
+
indexedDB.deleteDatabase('localforage');
|
|
343
|
+
localStorage.clear();
|
|
344
|
+
sessionStorage.clear();
|
|
345
|
+
});
|
|
346
|
+
await page.goto('/admin/login?returnTo=/rest/resource/training-student%253Astudent', {
|
|
347
|
+
waitUntil: 'load',
|
|
348
|
+
});
|
|
349
|
+
await page.getByPlaceholder('Your Username').fill('admin');
|
|
350
|
+
await page.getByPlaceholder('Your Password').fill('123456');
|
|
351
|
+
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
352
|
+
|
|
353
|
+
const initialSelect = waitForStudentSelect(page);
|
|
354
|
+
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
355
|
+
try {
|
|
356
|
+
await expect(page).toHaveURL(studentResourceUrl);
|
|
357
|
+
await initialSelect;
|
|
358
|
+
} finally {
|
|
359
|
+
await initialSelect.catch(() => {});
|
|
360
|
+
}
|
|
361
|
+
await expect(page.getByLabel('Student Name')).toBeVisible();
|
|
362
|
+
|
|
363
|
+
await page.getByRole('link', { name: 'Home', exact: true }).click();
|
|
364
|
+
await expect(page).toHaveURL(/\/admin\/(?:\?|$)/);
|
|
365
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
366
|
+
await expect(
|
|
367
|
+
page.getByRole('heading', { name: 'Vona integrated SSR', exact: true }),
|
|
368
|
+
).toBeVisible();
|
|
369
|
+
await expect(
|
|
370
|
+
page.getByRole('heading', { name: 'Zova standalone SSR', exact: true }),
|
|
371
|
+
).toBeVisible();
|
|
372
|
+
await expect(page.locator('body')).toBeVisible();
|
|
373
|
+
|
|
374
|
+
expect(pageErrors).toEqual([]);
|
|
375
|
+
expect(
|
|
376
|
+
consoleErrors.some(
|
|
377
|
+
error =>
|
|
378
|
+
error.includes("Cannot read properties of undefined (reading 'resource')") ||
|
|
379
|
+
error.includes('resource not specified'),
|
|
380
|
+
),
|
|
381
|
+
).toBeFalsy();
|
|
382
|
+
} finally {
|
|
383
|
+
await context.close();
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
test(
|
|
389
|
+
'ATP-BASIC-FLOW-02: Training Student flow filter wraps and submits queries',
|
|
323
390
|
{ tag: ['@admin', '@flow'] },
|
|
324
391
|
async ({ page }) => {
|
|
325
392
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
@@ -101,6 +101,48 @@ test(
|
|
|
101
101
|
},
|
|
102
102
|
);
|
|
103
103
|
|
|
104
|
+
test(
|
|
105
|
+
'ATP-BASIC-CONTROLLER-BOUNDARY-06: disposed and stale loads do not surface errors',
|
|
106
|
+
{ tag: ['@web', '@flow'] },
|
|
107
|
+
async ({ page }) => {
|
|
108
|
+
const pageErrors = collectPageErrors(page);
|
|
109
|
+
const consoleErrors = collectConsoleErrors(page);
|
|
110
|
+
const response = await page.goto('/demo/basic/controllerBoundary', { waitUntil: 'load' });
|
|
111
|
+
expect(response?.ok()).toBeTruthy();
|
|
112
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
|
|
113
|
+
await page.waitForTimeout(1_000);
|
|
114
|
+
|
|
115
|
+
const loading = page.getByRole('status');
|
|
116
|
+
const error = page.locator('.alert.alert-error');
|
|
117
|
+
const ready = page.getByText('Controller boundary probe ready', { exact: true });
|
|
118
|
+
|
|
119
|
+
const disposedLoading = loading.waitFor({ state: 'visible' });
|
|
120
|
+
await page.getByRole('button', { name: 'Mount failing probe', exact: true }).click();
|
|
121
|
+
await disposedLoading;
|
|
122
|
+
await page.getByRole('button', { name: 'Clear probe', exact: true }).click();
|
|
123
|
+
await page.waitForTimeout(1_000);
|
|
124
|
+
await expect(loading).toHaveCount(0);
|
|
125
|
+
await expect(error).toHaveCount(0);
|
|
126
|
+
await expect(ready).toHaveCount(0);
|
|
127
|
+
|
|
128
|
+
const staleLoading = loading.waitFor({ state: 'visible' });
|
|
129
|
+
await page.getByRole('button', { name: 'Mount failing probe', exact: true }).click();
|
|
130
|
+
await staleLoading;
|
|
131
|
+
await page.getByRole('button', { name: 'Mount fast success probe', exact: true }).click();
|
|
132
|
+
await expect(ready).toBeVisible();
|
|
133
|
+
await page.waitForTimeout(1_000);
|
|
134
|
+
await expect(error).toHaveCount(0);
|
|
135
|
+
await expect(ready).toBeVisible();
|
|
136
|
+
|
|
137
|
+
expect(pageErrors).toEqual([]);
|
|
138
|
+
expect(
|
|
139
|
+
consoleErrors.filter(error =>
|
|
140
|
+
/Cannot read properties of null \(reading ['"]state['"]\)/.test(error),
|
|
141
|
+
),
|
|
142
|
+
).toEqual([]);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
104
146
|
test(
|
|
105
147
|
'ATP-BASIC-CONTROLLER-BOUNDARY-02: inline boundaries preserve phrasing-content markup',
|
|
106
148
|
{ tag: ['@web', '@flow'] },
|
|
@@ -195,6 +195,21 @@ test(
|
|
|
195
195
|
await expect(dialogA.getByRole('button', { name: 'Back', exact: true })).toHaveCount(0);
|
|
196
196
|
expect(page.url()).toBe(browserUrl);
|
|
197
197
|
|
|
198
|
+
await page.keyboard.press('Escape');
|
|
199
|
+
await expect(dialogA).toHaveCount(0);
|
|
200
|
+
expect(page.url()).toBe(browserUrl);
|
|
201
|
+
|
|
202
|
+
await page.getByRole('button', { name: 'Open A', exact: true }).click();
|
|
203
|
+
await expect(dialogA).toHaveCount(1);
|
|
204
|
+
await expect(
|
|
205
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Entry', exact: true }),
|
|
206
|
+
).toBeVisible();
|
|
207
|
+
await dialogA.getByRole('button', { name: 'Local push to detail', exact: true }).click();
|
|
208
|
+
await expect(
|
|
209
|
+
dialogA.getByRole('heading', { name: 'Routed Dialog Detail', exact: true }),
|
|
210
|
+
).toBeVisible();
|
|
211
|
+
expect(page.url()).toBe(browserUrl);
|
|
212
|
+
|
|
198
213
|
await page.keyboard.press('Escape');
|
|
199
214
|
await expect(dialogA).toHaveCount(0);
|
|
200
215
|
expect(page.url()).toBe(browserUrl);
|
|
@@ -65,5 +65,5 @@ export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
|
|
|
65
65
|
@Passport.systemAdmin()
|
|
66
66
|
async deleteBulk(@Arg.body() command: Dto<%=argv.resourceNameCapitalize%>DeleteBulk): Promise<void> {
|
|
67
67
|
await this.scope.service.<%=argv.resourceName%>.deleteBulk(command.ids);
|
|
68
|
-
}
|
|
68
|
+
}
|
|
69
69
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova-jsx",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.91",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "Zova JSX",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@cabloy/word-utils": "^2.1.14",
|
|
51
51
|
"typestyle": "^2.4.0",
|
|
52
52
|
"vue": "^3.5.38",
|
|
53
|
-
"zova-core": "^5.1.
|
|
53
|
+
"zova-core": "^5.1.91"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"clean-package": "^2.2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.165",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A vue3 framework with ioc",
|
|
6
6
|
"keywords": [
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"postpack": "clean-package restore"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"zova-core": "^5.1.
|
|
49
|
-
"zova-suite-a-zova": "^5.1.
|
|
48
|
+
"zova-core": "^5.1.91",
|
|
49
|
+
"zova-suite-a-zova": "^5.1.164"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"clean-package": "^2.2.0",
|
|
@@ -489,6 +489,10 @@ export class BeanContainer {
|
|
|
489
489
|
// throw new Error(`bean not found: ${beanFullName}`);
|
|
490
490
|
return null!;
|
|
491
491
|
}
|
|
492
|
+
// controller module: ensure route resources are ready before controller data initialization
|
|
493
|
+
if (controllerData) {
|
|
494
|
+
await this._useModule(beanOptions.module);
|
|
495
|
+
}
|
|
492
496
|
// beanFullName
|
|
493
497
|
return await this._createBeanInstance<T>(
|
|
494
498
|
record,
|
|
@@ -111,6 +111,7 @@ async function _useController(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function isAttemptActive(attemptId: number) {
|
|
114
|
+
if (ctx.disposed || !ctx.meta) return false;
|
|
114
115
|
return ctx.meta.state.isLoadAttemptActive(attemptId);
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -133,9 +134,9 @@ async function _useController(
|
|
|
133
134
|
true,
|
|
134
135
|
false,
|
|
135
136
|
controller => {
|
|
137
|
+
if (!isAttemptActive(attemptId)) return;
|
|
136
138
|
loadBeans.add(controller as object);
|
|
137
139
|
ctx.meta.state.setLoadBeanAttempt(controller as object, attemptId);
|
|
138
|
-
if (!isAttemptActive(attemptId)) return;
|
|
139
140
|
ctx.meta.state.setLoadController(controller);
|
|
140
141
|
setLoadBoundaryOptions((controller as any).constructor);
|
|
141
142
|
},
|
|
@@ -151,6 +152,7 @@ async function _useController(
|
|
|
151
152
|
true,
|
|
152
153
|
false,
|
|
153
154
|
style => {
|
|
155
|
+
if (!isAttemptActive(attemptId)) return;
|
|
154
156
|
loadBeans.add(style as object);
|
|
155
157
|
ctx.meta.state.setLoadBeanAttempt(style as object, attemptId);
|
|
156
158
|
},
|
|
@@ -167,6 +169,7 @@ async function _useController(
|
|
|
167
169
|
true,
|
|
168
170
|
false,
|
|
169
171
|
render => {
|
|
172
|
+
if (!isAttemptActive(attemptId)) return;
|
|
170
173
|
loadBeans.add(render as object);
|
|
171
174
|
ctx.meta.state.setLoadBeanAttempt(render as object, attemptId);
|
|
172
175
|
},
|
package/zova/pnpm-lock.yaml
CHANGED
|
@@ -4,6 +4,37 @@ import { ZPage, ZSiteEntryTables } from 'zova-module-home-base';
|
|
|
4
4
|
|
|
5
5
|
@Controller()
|
|
6
6
|
export class ControllerPageDashboard extends BeanControllerPageBase {
|
|
7
|
+
private _timer?: number;
|
|
8
|
+
currentTime = '';
|
|
9
|
+
|
|
10
|
+
protected __init__() {
|
|
11
|
+
if (!this.$pageRoute) return;
|
|
12
|
+
this.$ssr.handleDirectOrOnHydrated(() => {
|
|
13
|
+
this.currentTime = new Date().toLocaleTimeString();
|
|
14
|
+
this._timer = window.setInterval(() => {
|
|
15
|
+
this.currentTime = new Date().toLocaleTimeString();
|
|
16
|
+
}, 1000);
|
|
17
|
+
});
|
|
18
|
+
this.$router.setPageMeta(this.$pageRoute, {
|
|
19
|
+
onCustomRenderIsolate: () => (
|
|
20
|
+
<div class="flex w-full items-center justify-center">
|
|
21
|
+
<div class="badge badge-lg gap-2 border border-primary/30 bg-primary/10 px-4 py-3 text-primary shadow-sm">
|
|
22
|
+
<span class="h-2 w-2 animate-pulse rounded-full bg-primary" aria-hidden="true"></span>
|
|
23
|
+
<span class="font-mono text-base font-semibold tracking-wide tabular-nums">
|
|
24
|
+
{this.currentTime || '--:--:--'}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected __dispose__() {
|
|
33
|
+
if (this._timer !== undefined) {
|
|
34
|
+
window.clearInterval(this._timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
protected render() {
|
|
8
39
|
return (
|
|
9
40
|
<ZPage>
|
package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.tabs.tsx
CHANGED
|
@@ -63,6 +63,20 @@ export class RenderTabs extends BeanRenderBase {
|
|
|
63
63
|
const tabCurrent = $$modelTabs.tabCurrent;
|
|
64
64
|
if (!tabCurrent || !tabCurrent.items) return;
|
|
65
65
|
const tabKey = tabCurrent.tabKey;
|
|
66
|
+
const tabItemAnchor = tabCurrent.items.find(item => item.componentKey === tabKey);
|
|
67
|
+
const onCustomRenderIsolate = tabItemAnchor?.pageMeta?.onCustomRenderIsolate;
|
|
68
|
+
if (onCustomRenderIsolate) {
|
|
69
|
+
const domContent = onCustomRenderIsolate(tabCurrent);
|
|
70
|
+
if (!this.$$modelTabs.cache) return domContent;
|
|
71
|
+
return <ClientOnly>{domContent}</ClientOnly>;
|
|
72
|
+
}
|
|
73
|
+
const pageTitleAnchor = tabItemAnchor?.pageMeta?.pageTitle;
|
|
74
|
+
if (pageTitleAnchor) {
|
|
75
|
+
const domWrapper = <div class="flex items-center justify-center">{pageTitleAnchor}</div>;
|
|
76
|
+
if (!this.$$modelTabs.cache) return domWrapper;
|
|
77
|
+
return <ClientOnly>{domWrapper}</ClientOnly>;
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
const domTabs: VNode[] = [];
|
|
67
81
|
for (const tabItem of tabCurrent.items) {
|
|
68
82
|
// ignore first
|
|
@@ -70,7 +84,9 @@ export class RenderTabs extends BeanRenderBase {
|
|
|
70
84
|
const { componentKey, pageMeta } = tabItem;
|
|
71
85
|
const className =
|
|
72
86
|
componentKey === $$modelTabs.componentKeyCurrent ? 'tab-active text-primary' : '';
|
|
73
|
-
const
|
|
87
|
+
const pageContent = pageMeta?.onCustomRender
|
|
88
|
+
? pageMeta.onCustomRender(tabItem)
|
|
89
|
+
: pageMeta?.pageTitle || '';
|
|
74
90
|
const tabItemIcon = this.getTabItemIcon(tabItem);
|
|
75
91
|
const domTab = (
|
|
76
92
|
<a
|
|
@@ -86,7 +102,7 @@ export class RenderTabs extends BeanRenderBase {
|
|
|
86
102
|
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
|
87
103
|
style={{ display: 'inline-block', maxWidth: this.scope.config.tabItem.maxWidth }}
|
|
88
104
|
>
|
|
89
|
-
{
|
|
105
|
+
{pageContent}
|
|
90
106
|
</div>
|
|
91
107
|
<ZIcon
|
|
92
108
|
class="tab-close hidden hover:bg-slate-400 rounded-sm"
|
|
@@ -6,9 +6,10 @@ import type {
|
|
|
6
6
|
RouterOptions,
|
|
7
7
|
} from '@cabloy/vue-router';
|
|
8
8
|
|
|
9
|
-
import { BeanBase,
|
|
9
|
+
import { BeanBase, Use } from 'zova';
|
|
10
10
|
import { Bean } from 'zova-module-a-bean';
|
|
11
11
|
|
|
12
|
+
import { RouterGuardDisposers } from '../lib/routerGuardDisposers.js';
|
|
12
13
|
import { BeanRouterViewBase } from '../lib/routerViewBase.js';
|
|
13
14
|
import { ModelPageData } from '../model/pageData.js';
|
|
14
15
|
import { IPageMeta } from '../types/pageMeta.js';
|
|
@@ -30,7 +31,7 @@ export interface BeanRouter extends Omit<
|
|
|
30
31
|
@Bean()
|
|
31
32
|
export class BeanRouter extends BeanBase {
|
|
32
33
|
private _vueRouterApp: Router;
|
|
33
|
-
private
|
|
34
|
+
private _routerGuardDisposers = new RouterGuardDisposers();
|
|
34
35
|
private _routerViews: BeanRouterViewBase[] = [];
|
|
35
36
|
private _disposed = false;
|
|
36
37
|
|
|
@@ -65,16 +66,9 @@ export class BeanRouter extends BeanBase {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
public dispose() {
|
|
68
|
-
this.__dispose__();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
protected __dispose__() {
|
|
72
69
|
if (this._disposed) return;
|
|
73
70
|
this._disposed = true;
|
|
74
|
-
|
|
75
|
-
fn();
|
|
76
|
-
}
|
|
77
|
-
this._eventRouterGuards = [];
|
|
71
|
+
this._routerGuardDisposers.dispose();
|
|
78
72
|
this._routerViews = [];
|
|
79
73
|
const router = this._vueRouterApp as Router & {
|
|
80
74
|
__prepareNavigation?: (to: unknown) => Promise<void> | void;
|
|
@@ -82,6 +76,11 @@ export class BeanRouter extends BeanBase {
|
|
|
82
76
|
if (router) router.__prepareNavigation = undefined;
|
|
83
77
|
}
|
|
84
78
|
|
|
79
|
+
protected __dispose__() {
|
|
80
|
+
// Router instances can be borrowed through page host providers. Their
|
|
81
|
+
// creator explicitly calls dispose() when the router itself is finished.
|
|
82
|
+
}
|
|
83
|
+
|
|
85
84
|
protected __get__(prop: string) {
|
|
86
85
|
// SymbolRouter first
|
|
87
86
|
const value = this._vueRouterApp?.[prop];
|
|
@@ -140,27 +139,19 @@ export class BeanRouter extends BeanBase {
|
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
beforeEach(guard: NavigationGuardWithThis<undefined>): () => void {
|
|
143
|
-
|
|
144
|
-
this._eventRouterGuards.push(fn);
|
|
145
|
-
return fn;
|
|
142
|
+
return this._routerGuardDisposers.add(this._vueRouterApp.beforeEach(guard));
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void {
|
|
149
|
-
|
|
150
|
-
this._eventRouterGuards.push(fn);
|
|
151
|
-
return fn;
|
|
146
|
+
return this._routerGuardDisposers.add(this._vueRouterApp.beforeResolve(guard));
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
afterEach(guard: NavigationHookAfter): () => void {
|
|
155
|
-
|
|
156
|
-
this._eventRouterGuards.push(fn);
|
|
157
|
-
return fn;
|
|
150
|
+
return this._routerGuardDisposers.add(this._vueRouterApp.afterEach(guard));
|
|
158
151
|
}
|
|
159
152
|
|
|
160
153
|
onError(handler: TypeErrorListener): () => void {
|
|
161
|
-
|
|
162
|
-
this._eventRouterGuards.push(fn);
|
|
163
|
-
return fn;
|
|
154
|
+
return this._routerGuardDisposers.add(this._vueRouterApp.onError(handler));
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
setPageMeta(route: RouteLocationNormalizedLoadedGeneric, pageMeta: IPageMeta) {
|
|
@@ -388,6 +388,7 @@ export class SysRouter extends BeanBase {
|
|
|
388
388
|
routeData = {
|
|
389
389
|
name: routeNameParent,
|
|
390
390
|
path,
|
|
391
|
+
...(route.alias ? { alias: route.alias } : {}),
|
|
391
392
|
component: this.createAsyncComponent(layout as any),
|
|
392
393
|
children: [{ ...route, name, path: '', component, meta }],
|
|
393
394
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class RouterGuardDisposers {
|
|
2
|
+
private _disposers: (() => void)[] = [];
|
|
3
|
+
private _disposed = false;
|
|
4
|
+
|
|
5
|
+
add(disposer: () => void) {
|
|
6
|
+
this._disposers.push(disposer);
|
|
7
|
+
return disposer;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
dispose() {
|
|
11
|
+
if (this._disposed) return;
|
|
12
|
+
this._disposed = true;
|
|
13
|
+
const disposers = this._disposers;
|
|
14
|
+
this._disposers = [];
|
|
15
|
+
for (const disposer of disposers) {
|
|
16
|
+
disposer();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -12,9 +12,18 @@ export class ServiceRouterGuards extends BeanRouterGuardsBase {
|
|
|
12
12
|
const self = this;
|
|
13
13
|
router.beforeEach(async to => {
|
|
14
14
|
// match path
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const aliasMatch = to.matched.find(item => item.aliasOf);
|
|
16
|
+
let match = aliasMatch;
|
|
17
|
+
if (aliasMatch) {
|
|
18
|
+
match = aliasMatch.aliasOf;
|
|
19
|
+
// A path-config alias can initially resolve to a componentless placeholder
|
|
20
|
+
// before its module registers the real layout route. Reload the same path
|
|
21
|
+
// after loading so the router resolves the newly registered component.
|
|
22
|
+
if (!match?.components || Object.keys(match.components).length === 0) {
|
|
23
|
+
const resLoadModule = await this._forceLoadModule(router, match?.name, match?.path);
|
|
24
|
+
if (resLoadModule && resLoadModule !== true) return resLoadModule;
|
|
25
|
+
return to.fullPath;
|
|
26
|
+
}
|
|
18
27
|
} else {
|
|
19
28
|
match = to.matched[to.matched.length - 1];
|
|
20
29
|
// prepareCheck
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { IFormMeta } from 'zova-module-a-openapi';
|
|
3
|
+
|
|
4
|
+
import type { IRouteViewRouteItem, IRouteViewTabCurrent } from './routerView.js';
|
|
2
5
|
|
|
3
6
|
export interface IPageMeta {
|
|
4
7
|
pageTitle?: string;
|
|
5
8
|
pageDirty?: boolean;
|
|
6
9
|
formMeta?: IFormMeta;
|
|
10
|
+
onCustomRender?: (tabItem: IRouteViewRouteItem) => VNodeChild;
|
|
11
|
+
onCustomRenderIsolate?: (tabCurrent: IRouteViewTabCurrent) => VNodeChild;
|
|
7
12
|
}
|
|
@@ -14,6 +14,11 @@ export interface IRouteViewRouteItem extends IRouteViewRouteItemBase {
|
|
|
14
14
|
pageMeta?: IPageMeta;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface IRouteViewTabCurrent {
|
|
18
|
+
tabKey: string;
|
|
19
|
+
items?: IRouteViewRouteItem[];
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export interface IRouteViewRouteMeta extends IRouteViewRouteItemBase {
|
|
18
23
|
tabKey: string;
|
|
19
24
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { RouterGuardDisposers } from '../../src/lib/routerGuardDisposers.js';
|
|
5
|
+
|
|
6
|
+
test('router guard disposers release every callback only once', () => {
|
|
7
|
+
const disposers = new RouterGuardDisposers();
|
|
8
|
+
const calls: string[] = [];
|
|
9
|
+
|
|
10
|
+
const beforeEach = () => calls.push('beforeEach');
|
|
11
|
+
const beforeResolve = () => calls.push('beforeResolve');
|
|
12
|
+
const afterEach = () => calls.push('afterEach');
|
|
13
|
+
const onError = () => calls.push('onError');
|
|
14
|
+
|
|
15
|
+
assert.equal(disposers.add(beforeEach), beforeEach);
|
|
16
|
+
assert.equal(disposers.add(beforeResolve), beforeResolve);
|
|
17
|
+
assert.equal(disposers.add(afterEach), afterEach);
|
|
18
|
+
assert.equal(disposers.add(onError), onError);
|
|
19
|
+
|
|
20
|
+
disposers.dispose();
|
|
21
|
+
disposers.dispose();
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(calls, ['beforeEach', 'beforeResolve', 'afterEach', 'onError']);
|
|
24
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova-module-a-zova",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.107",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "zova",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@cabloy/word-utils": "^2.1.14",
|
|
44
44
|
"defu": "^6.1.7",
|
|
45
45
|
"luxon": "^3.7.2",
|
|
46
|
-
"zova-jsx": "^1.1.
|
|
46
|
+
"zova-jsx": "^1.1.91"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@cabloy/cli": "^3.1.29",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova-suite-a-zova",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.164",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "zova",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"zova-module-a-meta": "^5.1.22",
|
|
23
23
|
"zova-module-a-model": "^5.1.37",
|
|
24
24
|
"zova-module-a-openapi": "^5.1.54",
|
|
25
|
-
"zova-module-a-router": "^5.1.
|
|
25
|
+
"zova-module-a-router": "^5.1.40",
|
|
26
26
|
"zova-module-a-routerstack": "^5.1.29",
|
|
27
27
|
"zova-module-a-routertabs": "^5.1.34",
|
|
28
28
|
"zova-module-a-ssr": "^5.1.40",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"zova-module-a-style": "^5.1.35",
|
|
32
32
|
"zova-module-a-table": "^5.1.43",
|
|
33
33
|
"zova-module-a-zod": "^5.1.37",
|
|
34
|
-
"zova-module-a-zova": "^5.1.
|
|
34
|
+
"zova-module-a-zova": "^5.1.107"
|
|
35
35
|
},
|
|
36
36
|
"title": "a-zova"
|
|
37
37
|
}
|