cabloy 5.1.166 → 5.1.167

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 (48) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/scheduled_tasks.lock +1 -1
  3. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
  4. package/CHANGELOG.md +13 -0
  5. package/package.json +1 -1
  6. package/repo-docs/frontend/page-params-guide.md +1 -1
  7. package/repo-docs/frontend/page-query-guide.md +6 -0
  8. package/repo-docs/frontend/zod-guide.md +9 -7
  9. package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
  10. package/vona/pnpm-lock.yaml +6 -0
  11. package/zova/package.original.json +1 -1
  12. package/zova/packages-cli/cli/package.json +2 -2
  13. package/zova/packages-cli/cli-set-front/package.json +1 -1
  14. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateBeanGenerals.ts +1 -1
  15. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScopeResources.ts +1 -1
  16. package/zova/packages-zova/zova/package.json +2 -2
  17. package/zova/pnpm-lock.yaml +2014 -2053
  18. package/zova/pnpm-workspace.yaml +1 -1
  19. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +66 -18
  20. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialog.ts +9 -0
  21. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogDetail.ts +23 -0
  22. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogEntry.ts +23 -0
  23. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialog/controller.tsx +301 -0
  24. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogDetail/controller.tsx +135 -0
  25. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogEntry/controller.tsx +108 -0
  26. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +19 -0
  27. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +63 -0
  28. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/index.ts +44 -2
  29. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +306 -18
  30. package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +51 -0
  31. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/config.ts +19 -0
  32. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/en-us.ts +1 -0
  33. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/zh-cn.ts +1 -0
  34. package/zova/src/suite/cabloy-basic/modules/basic-app/src/lib/routedDialogHistory.ts +76 -0
  35. package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +156 -1
  36. package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +92 -13
  37. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts +92 -0
  38. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts +35 -0
  39. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  40. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
  41. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  42. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  43. package/zova/src/suite-vendor/a-zova/modules/a-router/src/.metadata/index.ts +6 -6
  44. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +60 -3
  45. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +12 -0
  46. package/zova/src/suite-vendor/a-zova/modules/a-router/src/component/routerViewEmpty/controller.tsx +4 -1
  47. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +5 -1
  48. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -0,0 +1,51 @@
1
+ import type { IComponentOptions } from 'zova';
2
+
3
+ import { h } from 'vue';
4
+ import { BeanControllerBase, cast } from 'zova';
5
+ import { Controller } from 'zova-module-a-bean';
6
+ import { ZRouterViewStack } from 'zova-module-a-routerstack';
7
+
8
+ import type { IModalRoutedDialogItem } from '../../types/appModal.js';
9
+
10
+ export interface ControllerRoutedDialogProps {
11
+ item: IModalRoutedDialogItem;
12
+ }
13
+
14
+ @Controller()
15
+ export class ControllerRoutedDialog extends BeanControllerBase {
16
+ static $propsDefault = {};
17
+ static $componentOptions: IComponentOptions = { inheritAttrs: false };
18
+
19
+ private $$modelStack: object | undefined;
20
+
21
+ protected async __init__() {
22
+ const item = (this.$props as ControllerRoutedDialogProps).item as IModalRoutedDialogItem;
23
+ const router = item.state.router;
24
+ if (!router) return;
25
+ const scene = `routedDialog:${item.id}`;
26
+ this.bean._setBean('a-router.bean.router', router);
27
+ this.$$modelStack = await this.bean._getBeanSelector(
28
+ 'a-routerstack.model.stack',
29
+ true,
30
+ scene,
31
+ scene,
32
+ {},
33
+ );
34
+ }
35
+
36
+ protected render() {
37
+ const item = (this.$props as ControllerRoutedDialogProps).item as IModalRoutedDialogItem;
38
+ if (item.state.status === 'error') {
39
+ return <div class="text-error whitespace-pre-wrap">{String(item.state.error)}</div>;
40
+ }
41
+ if (!this.$$modelStack) return <div class="loading loading-spinner"></div>;
42
+ const router = item.state.router;
43
+ if (!router) return <div class="text-error">Router unavailable</div>;
44
+ const vnode = h(ZRouterViewStack);
45
+ cast(vnode).zovaHostProviders = {
46
+ 'a-router.bean.router': router,
47
+ '$$modelStack': this.$$modelStack,
48
+ };
49
+ return vnode;
50
+ }
51
+ }
@@ -40,6 +40,25 @@ export const config = (_sys: ZovaSys) => {
40
40
  showCloseButton: false,
41
41
  },
42
42
  },
43
+ routedDialog: {
44
+ default: {
45
+ maxWidth: {
46
+ default: 640,
47
+ md: 768,
48
+ lg: 1024,
49
+ },
50
+ topGutter: {
51
+ default: 16,
52
+ md: 32,
53
+ lg: 48,
54
+ },
55
+ maxHeight: 'calc(100vh - 2rem)',
56
+ closeOnBackdrop: false,
57
+ closeOnEscape: true,
58
+ showCloseButton: true,
59
+ showBackButton: true,
60
+ },
61
+ },
43
62
  dialog: {
44
63
  default: {
45
64
  maxWidth: 640,
@@ -1,4 +1,5 @@
1
1
  export default {
2
+ Back: 'Back',
2
3
  Close: 'Close',
3
4
  Yes: 'Yes',
4
5
  No: 'No',
@@ -1,4 +1,5 @@
1
1
  export default {
2
+ Back: '返回',
2
3
  Close: '关闭',
3
4
  Yes: '是',
4
5
  No: '否',
@@ -0,0 +1,76 @@
1
+ import type { RouterHistory } from '@cabloy/vue-router';
2
+
3
+ import { createMemoryHistory } from '@cabloy/vue-router';
4
+
5
+ export class RoutedDialogHistory {
6
+ private readonly sourceHistory = createMemoryHistory();
7
+ private position = 0;
8
+ private length = 1;
9
+ private listeners = new Set<(canGoBack: boolean) => void>();
10
+ public readonly history: RouterHistory;
11
+
12
+ constructor() {
13
+ const sourceHistory = this.sourceHistory;
14
+ this.history = {
15
+ get base() {
16
+ return sourceHistory.base;
17
+ },
18
+ get location() {
19
+ return sourceHistory.location;
20
+ },
21
+ get state() {
22
+ return sourceHistory.state;
23
+ },
24
+ createHref: (...args) => sourceHistory.createHref(...args),
25
+ push: (...args) => {
26
+ this._push();
27
+ sourceHistory.push(...args);
28
+ },
29
+ replace: (...args) => {
30
+ sourceHistory.replace(...args);
31
+ },
32
+ go: (...args) => {
33
+ const [delta] = args;
34
+ const position = Math.max(0, Math.min(this.position + delta, this.length - 1));
35
+ this._setPosition(position);
36
+ sourceHistory.go(...args);
37
+ },
38
+ listen: (...args) => sourceHistory.listen(...args),
39
+ destroy: () => {
40
+ sourceHistory.destroy();
41
+ },
42
+ };
43
+ }
44
+
45
+ public get canGoBack() {
46
+ return this.position > 0;
47
+ }
48
+
49
+ public onCanGoBackChange(listener: (canGoBack: boolean) => void) {
50
+ this.listeners.add(listener);
51
+ return () => {
52
+ this.listeners.delete(listener);
53
+ };
54
+ }
55
+
56
+ public destroy() {
57
+ this.history.destroy();
58
+ this.position = 0;
59
+ this.length = 1;
60
+ this.listeners.clear();
61
+ }
62
+
63
+ private _push() {
64
+ this.length = this.position + 2;
65
+ this._setPosition(this.position + 1);
66
+ }
67
+
68
+ private _setPosition(position: number) {
69
+ const canGoBack = this.canGoBack;
70
+ this.position = position;
71
+ if (this.canGoBack === canGoBack) return;
72
+ for (const listener of this.listeners) {
73
+ listener(this.canGoBack);
74
+ }
75
+ }
76
+ }
@@ -1,7 +1,9 @@
1
+ import { shallowReactive } from 'vue';
1
2
  import { BeanBase } from 'zova';
2
3
  import { Service } from 'zova-module-a-bean';
3
4
 
4
5
  import { AppModalItem } from '../lib/appModalItem.js';
6
+ import { RoutedDialogHistory } from '../lib/routedDialogHistory.js';
5
7
  import {
6
8
  IModalAlertOptions,
7
9
  IModalConfirmOptions,
@@ -10,12 +12,22 @@ import {
10
12
  IModalItem,
11
13
  IModalMessageOptions,
12
14
  IModalPromptOptions,
15
+ IModalRoutedDialogOptions,
16
+ IModalRoutedDialogPresentationOptions,
17
+ IModalRoutedDialogState,
18
+ IRoutedDialogHandle,
13
19
  } from '../types/appModal.js';
14
20
 
15
21
  @Service()
16
22
  export class ServiceAppModal extends BeanBase {
17
23
  public modalItems: IModalItem[] = [];
18
24
  private modalItemIdCounter: number = 0;
25
+ private routedDialogReadySettlers = new Map<
26
+ number,
27
+ { resolve: () => void; reject: (error: unknown) => void }
28
+ >();
29
+
30
+ private routedDialogHistories = new Map<number, RoutedDialogHistory>();
19
31
 
20
32
  protected async __init__() {}
21
33
 
@@ -89,10 +101,153 @@ export class ServiceAppModal extends BeanBase {
89
101
  return new AppModalItem(this, modalItem);
90
102
  }
91
103
 
92
- public close(id: number) {
104
+ public routedDialog(
105
+ options: IModalRoutedDialogOptions,
106
+ dialogOptions?: IModalRoutedDialogPresentationOptions,
107
+ ): IRoutedDialogHandle {
108
+ const id = this.newModalItemId();
109
+ let resolveReady!: () => void;
110
+ let rejectReady!: (error: unknown) => void;
111
+ const ready = new Promise<void>((resolve, reject) => {
112
+ resolveReady = resolve;
113
+ rejectReady = reject;
114
+ });
115
+ // A close-before-ready rejection remains observable through the handle,
116
+ // without becoming an unhandled rejection when callers do not await it.
117
+ void ready.catch(() => {});
118
+ const modalItem: IModalItem = {
119
+ id,
120
+ type: 'routedDialog',
121
+ options,
122
+ dialogOptions,
123
+ state: shallowReactive<IModalRoutedDialogState>({
124
+ status: 'loading',
125
+ canGoBack: false,
126
+ ready,
127
+ }),
128
+ };
129
+ this.modalItems.push(modalItem);
130
+ this.routedDialogReadySettlers.set(id, { resolve: resolveReady, reject: rejectReady });
131
+ const handle = new AppModalItem(this, modalItem) as IRoutedDialogHandle;
132
+ Object.defineProperties(handle, {
133
+ ready: { enumerable: true, get: () => ready },
134
+ push: {
135
+ enumerable: true,
136
+ value: (to: any) => this._navigateRoutedDialog(modalItem, to, false),
137
+ },
138
+ replace: {
139
+ enumerable: true,
140
+ value: (to: any) => this._navigateRoutedDialog(modalItem, to, true),
141
+ },
142
+ });
143
+ void this._initRoutedDialog(modalItem);
144
+ return handle;
145
+ }
146
+
147
+ private async _initRoutedDialog(modalItem: Extract<IModalItem, { type: 'routedDialog' }>) {
148
+ let router: IModalRoutedDialogState['router'];
149
+ try {
150
+ if (!process.env.CLIENT) throw new Error('routedDialog is client-only');
151
+ const history = new RoutedDialogHistory();
152
+ this.routedDialogHistories.set(modalItem.id, history);
153
+ history.onCanGoBackChange(canGoBack => {
154
+ if (!this._isRoutedDialogOpen(modalItem)) return;
155
+ modalItem.state.canGoBack = canGoBack;
156
+ });
157
+ router = await this.app.bean._newBean('a-router.bean.router', false, {
158
+ mainRouter: false,
159
+ embeddedView: 'page',
160
+ prepareNavigation: to => this.app.meta.$router.ensureRoute(String(to)),
161
+ routerOptions: {
162
+ history: history.history,
163
+ },
164
+ });
165
+ router.afterEach((to, _from, failure) => {
166
+ if (failure || !this._isRoutedDialogOpen(modalItem)) return;
167
+ modalItem.state.currentRoute = to;
168
+ modalItem.state.canGoBack = history.canGoBack;
169
+ });
170
+ if (!this._isRoutedDialogOpen(modalItem)) {
171
+ router.dispose();
172
+ history.destroy();
173
+ this.routedDialogHistories.delete(modalItem.id);
174
+ return;
175
+ }
176
+ modalItem.state.router = router;
177
+ const result = await router.replace(modalItem.options.route as any);
178
+ if (result) throw result;
179
+ await router.isReady();
180
+ if (!this._isRoutedDialogOpen(modalItem)) {
181
+ router.dispose();
182
+ history.destroy();
183
+ this.routedDialogHistories.delete(modalItem.id);
184
+ modalItem.state.router = undefined;
185
+ return;
186
+ }
187
+ modalItem.state.currentRoute = router.currentRoute.value;
188
+ modalItem.state.canGoBack = history.canGoBack;
189
+ modalItem.state.status = 'ready';
190
+ this._settleRoutedDialogReady(modalItem.id, 'resolve');
191
+ } catch (error) {
192
+ router?.dispose();
193
+ this.routedDialogHistories.get(modalItem.id)?.destroy();
194
+ this.routedDialogHistories.delete(modalItem.id);
195
+ if (!this._isRoutedDialogOpen(modalItem)) return;
196
+ modalItem.state.router = undefined;
197
+ modalItem.state.status = 'error';
198
+ modalItem.state.error = error;
199
+ this._settleRoutedDialogReady(modalItem.id, 'reject', error);
200
+ }
201
+ }
202
+
203
+ private async _navigateRoutedDialog(
204
+ modalItem: Extract<IModalItem, { type: 'routedDialog' }>,
205
+ to: any,
206
+ replace: boolean,
207
+ ) {
208
+ this._ensureRoutedDialogOpen(modalItem);
209
+ await modalItem.state.ready;
210
+ this._ensureRoutedDialogOpen(modalItem);
211
+ const router = modalItem.state.router;
212
+ if (!router) throw new Error('routedDialog router is unavailable');
213
+ const result = replace ? await router.replace(to) : await router.push(to);
214
+ this._ensureRoutedDialogOpen(modalItem);
215
+ return result;
216
+ }
217
+
218
+ private _isRoutedDialogOpen(modalItem: Extract<IModalItem, { type: 'routedDialog' }>) {
219
+ return !modalItem.state.closed && this.modalItems.some(item => item.id === modalItem.id);
220
+ }
221
+
222
+ private _ensureRoutedDialogOpen(modalItem: Extract<IModalItem, { type: 'routedDialog' }>) {
223
+ if (!this._isRoutedDialogOpen(modalItem)) {
224
+ throw new Error('routedDialog is closed');
225
+ }
226
+ }
227
+
228
+ private _settleRoutedDialogReady(id: number, action: 'resolve' | 'reject', error?: unknown) {
229
+ const settler = this.routedDialogReadySettlers.get(id);
230
+ if (!settler) return;
231
+ this.routedDialogReadySettlers.delete(id);
232
+ if (action === 'resolve') settler.resolve();
233
+ else settler.reject(error);
234
+ }
235
+
236
+ public close(id: number, _reason: string = 'api') {
93
237
  const [index, modalItem] = this.findModalItem(id);
94
238
  if (index === -1 || !modalItem) return;
95
239
  this.modalItems.splice(index, 1);
240
+ if (modalItem.type === 'routedDialog') {
241
+ modalItem.state.closed = true;
242
+ modalItem.state.status = 'closed';
243
+ modalItem.state.router?.dispose();
244
+ this.routedDialogHistories.get(modalItem.id)?.destroy();
245
+ this.routedDialogHistories.delete(modalItem.id);
246
+ modalItem.state.router = undefined;
247
+ this._settleRoutedDialogReady(modalItem.id, 'reject', new Error('routedDialog is closed'));
248
+ modalItem.options.onClose?.();
249
+ return;
250
+ }
96
251
  if (modalItem.type === 'dialog') {
97
252
  const options = modalItem.options as IModalDialogRenderOptions | undefined;
98
253
  options?.onClose?.();
@@ -1,14 +1,44 @@
1
+ import type {
2
+ NavigationFailure,
3
+ RouteLocationNormalizedLoadedGeneric,
4
+ RouteLocationRaw,
5
+ } from '@cabloy/vue-router';
1
6
  import type { VNode } from 'vue';
2
7
  import type { IIconRecord } from 'zova-module-a-icon';
8
+ import type { BeanRouter } from 'zova-module-a-router';
3
9
 
10
+ import type { AppModalItem } from '../lib/appModalItem.js';
4
11
  import type { ServiceAppModal } from '../service/appModal.js';
5
12
 
6
- export type ModalType = 'alert' | 'confirm' | 'prompt' | 'dialog';
13
+ export type ModalType = 'alert' | 'confirm' | 'prompt' | 'dialog' | 'routedDialog';
14
+ export type ModalCloseReason = 'api' | 'backdrop' | 'escape' | 'button' | 'error' | 'unmount';
7
15
  export type AlertType = 'success' | 'info' | 'warning' | 'error';
16
+ export type ModalWidth = number | string;
17
+
18
+ /**
19
+ * Mobile-first maximum-width values for routed dialogs.
20
+ *
21
+ * `md` and `lg` use the Cabloy Basic Tailwind breakpoints. Omitted values
22
+ * inherit the last applicable lower breakpoint through the CSS cascade.
23
+ */
24
+ export interface IModalResponsiveMaxWidth {
25
+ default?: ModalWidth;
26
+ md?: ModalWidth;
27
+ lg?: ModalWidth;
28
+ }
29
+
30
+ export interface IModalResponsiveTopGutter {
31
+ default?: ModalWidth;
32
+ md?: ModalWidth;
33
+ lg?: ModalWidth;
34
+ }
35
+
36
+ export type ModalRoutedDialogMaxWidth = ModalWidth | IModalResponsiveMaxWidth;
37
+ export type ModalRoutedDialogTopGutter = ModalWidth | IModalResponsiveTopGutter;
8
38
 
9
39
  export interface IModalBaseOptions {
10
- maxWidth?: number | string;
11
- maxHeight?: number | string;
40
+ maxWidth?: ModalWidth;
41
+ maxHeight?: ModalWidth;
12
42
  closeOnBackdrop?: boolean;
13
43
  closeOnEscape?: boolean;
14
44
  }
@@ -19,6 +49,15 @@ export interface IModalDialogOptions extends IModalBaseOptions {
19
49
 
20
50
  export interface IModalMessageOptions extends IModalBaseOptions {}
21
51
 
52
+ export interface IModalRoutedDialogPresentationOptions extends Omit<
53
+ IModalDialogOptions,
54
+ 'maxWidth'
55
+ > {
56
+ maxWidth?: ModalRoutedDialogMaxWidth;
57
+ topGutter?: ModalRoutedDialogTopGutter;
58
+ showBackButton?: boolean;
59
+ }
60
+
22
61
  export interface IModalDialogRenderContext {
23
62
  id: number;
24
63
  close: () => void;
@@ -32,6 +71,44 @@ export interface IModalDialogRenderOptions {
32
71
  onClose?: () => void;
33
72
  }
34
73
 
74
+ export interface IModalRoutedDialogOptions extends IModalRoutedDialogPresentationOptions {
75
+ route: RouteLocationRaw;
76
+ icon?: keyof IIconRecord;
77
+ title?: string;
78
+ props?: Record<string, unknown>;
79
+ onClose?: () => void;
80
+ }
81
+
82
+ export interface IModalRoutedDialogState {
83
+ status: 'loading' | 'ready' | 'error' | 'closed';
84
+ closed?: boolean;
85
+ error?: unknown;
86
+ router?: BeanRouter;
87
+ currentRoute?: RouteLocationNormalizedLoadedGeneric;
88
+ canGoBack: boolean;
89
+ ready: Promise<void>;
90
+ }
91
+
92
+ export type IModalRoutedDialogInput = Omit<IModalRoutedDialogOptions, 'route'>;
93
+
94
+ export interface IModalRoutedDialogNavigate {
95
+ navigate(to: RouteLocationRaw, replace?: boolean): Promise<NavigationFailure | void | undefined>;
96
+ }
97
+
98
+ export interface IModalRoutedDialogItem {
99
+ id: number;
100
+ type: 'routedDialog';
101
+ options: IModalRoutedDialogOptions;
102
+ dialogOptions?: IModalRoutedDialogPresentationOptions;
103
+ state: IModalRoutedDialogState;
104
+ }
105
+
106
+ export interface IRoutedDialogHandle extends AppModalItem {
107
+ readonly ready: Promise<void>;
108
+ push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>;
109
+ replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>;
110
+ }
111
+
35
112
  export interface IModalAlertOptions {
36
113
  type?: AlertType;
37
114
  icon?: keyof IIconRecord;
@@ -60,16 +137,18 @@ export interface IModalPromptOptionsInner extends IModalPromptOptions {
60
137
  onCallback?: (res: string | undefined) => void;
61
138
  }
62
139
 
63
- export interface IModalItem {
64
- id: number;
65
- type: ModalType;
66
- options?:
67
- | IModalAlertOptions
68
- | IModalConfirmOptionsInner
69
- | IModalPromptOptionsInner
70
- | IModalDialogRenderOptions;
71
- dialogOptions?: IModalDialogOptions | IModalMessageOptions;
72
- }
140
+ export type IModalItem =
141
+ | {
142
+ id: number;
143
+ type: 'alert' | 'confirm' | 'prompt' | 'dialog';
144
+ options?:
145
+ | IModalAlertOptions
146
+ | IModalConfirmOptionsInner
147
+ | IModalPromptOptionsInner
148
+ | IModalDialogRenderOptions;
149
+ dialogOptions?: IModalDialogOptions | IModalMessageOptions;
150
+ }
151
+ | IModalRoutedDialogItem;
73
152
 
74
153
  declare module 'zova' {
75
154
  export interface BeanBase {
@@ -0,0 +1,92 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { RoutedDialogHistory } from '../../src/lib/routedDialogHistory.js';
5
+
6
+ function createHistory() {
7
+ const routedDialogHistory = new RoutedDialogHistory();
8
+ return { history: routedDialogHistory.history, routedDialogHistory };
9
+ }
10
+
11
+ test('routed dialog history establishes replace as the root entry', () => {
12
+ const { history, routedDialogHistory } = createHistory();
13
+
14
+ history.replace('/entry');
15
+
16
+ assert.equal(routedDialogHistory.canGoBack, false);
17
+ });
18
+
19
+ test('routed dialog history updates Back availability before navigation listeners', () => {
20
+ const { history, routedDialogHistory } = createHistory();
21
+ const canGoBackOnChange: boolean[] = [];
22
+ const canGoBackOnNavigation: boolean[] = [];
23
+
24
+ routedDialogHistory.onCanGoBackChange(canGoBack => {
25
+ canGoBackOnChange.push(canGoBack);
26
+ });
27
+ history.listen(() => {
28
+ canGoBackOnNavigation.push(routedDialogHistory.canGoBack);
29
+ });
30
+ history.replace('/entry');
31
+ history.push('/detail');
32
+ history.replace('/detail-edited');
33
+ assert.equal(routedDialogHistory.canGoBack, true);
34
+
35
+ history.go(-1);
36
+ assert.equal(routedDialogHistory.canGoBack, false);
37
+ history.go(1);
38
+ assert.equal(routedDialogHistory.canGoBack, true);
39
+
40
+ assert.deepEqual(canGoBackOnChange, [true, false, true]);
41
+ assert.deepEqual(canGoBackOnNavigation, [false, true]);
42
+ });
43
+
44
+ test('routed dialog history truncates forward entries after a new push', () => {
45
+ const { history, routedDialogHistory } = createHistory();
46
+
47
+ history.replace('/entry');
48
+ history.push('/detail/1');
49
+ history.push('/detail/2');
50
+ history.go(-1);
51
+ history.push('/detail/3');
52
+ history.go(1);
53
+ assert.equal(history.location, '/detail/3');
54
+ assert.equal(routedDialogHistory.canGoBack, true);
55
+
56
+ history.go(-2);
57
+ assert.equal(history.location, '/entry');
58
+ assert.equal(routedDialogHistory.canGoBack, false);
59
+ });
60
+
61
+ test('routed dialog history mirrors a silent rejected-pop repair', () => {
62
+ const { history, routedDialogHistory } = createHistory();
63
+
64
+ history.replace('/entry');
65
+ history.push('/detail');
66
+ history.go(-1);
67
+ assert.equal(routedDialogHistory.canGoBack, false);
68
+
69
+ // Vue Router repairs a rejected pop with history.go(-delta, false).
70
+ history.go(1, false);
71
+ assert.equal(history.location, '/detail');
72
+ assert.equal(routedDialogHistory.canGoBack, true);
73
+ });
74
+
75
+ test('routed dialog histories remain independent and reset after destruction', () => {
76
+ const first = createHistory();
77
+ const second = createHistory();
78
+ const firstChanges: boolean[] = [];
79
+
80
+ first.routedDialogHistory.onCanGoBackChange(canGoBack => {
81
+ firstChanges.push(canGoBack);
82
+ });
83
+ first.history.replace('/first');
84
+ second.history.replace('/second');
85
+ first.history.push('/first-detail');
86
+ assert.equal(first.routedDialogHistory.canGoBack, true);
87
+ assert.equal(second.routedDialogHistory.canGoBack, false);
88
+
89
+ first.routedDialogHistory.destroy();
90
+ assert.deepEqual(firstChanges, [true]);
91
+ assert.equal(first.routedDialogHistory.canGoBack, false);
92
+ });
@@ -0,0 +1,35 @@
1
+ import { createRouter } from '@cabloy/vue-router';
2
+ import assert from 'node:assert/strict';
3
+ import test from 'node:test';
4
+
5
+ import { RoutedDialogHistory } from '../../src/lib/routedDialogHistory.js';
6
+
7
+ function createRouterWithHistory() {
8
+ const routedDialogHistory = new RoutedDialogHistory();
9
+ const router = createRouter({
10
+ history: routedDialogHistory.history,
11
+ routes: [
12
+ { path: '/entry', component: {} },
13
+ { path: '/detail', component: {} },
14
+ ],
15
+ });
16
+ return { router, routedDialogHistory };
17
+ }
18
+
19
+ test('routed dialog router synchronizes Back availability after a pop navigation', async () => {
20
+ const { router, routedDialogHistory } = createRouterWithHistory();
21
+ const canGoBackAfterEach: boolean[] = [];
22
+
23
+ router.afterEach((_to, _from, failure) => {
24
+ if (!failure) canGoBackAfterEach.push(routedDialogHistory.canGoBack);
25
+ });
26
+ await router.replace('/entry');
27
+ await router.isReady();
28
+ await router.push('/detail');
29
+ router.back();
30
+ await new Promise(resolve => setTimeout(resolve));
31
+
32
+ assert.equal(router.currentRoute.value.fullPath, '/entry');
33
+ assert.equal(routedDialogHistory.canGoBack, false);
34
+ assert.deepEqual(canGoBackAfterEach, [false, true, false]);
35
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.51",
3
+ "version": "5.1.52",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -153,7 +153,7 @@ export class ModelResource<
153
153
  this.sys.util.apiActionPathTranslate(`${this.resourceApi}/:id`, { id }),
154
154
  this.sys.util.apiActionConfigPrepare(),
155
155
  );
156
- return res ?? null;
156
+ return (res as Entity | null) ?? null;
157
157
  },
158
158
  });
159
159
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.52",
3
+ "version": "5.1.53",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "zova-module-a-rbac": "^5.0.1",
11
- "zova-module-rest-resource": "^5.1.51"
11
+ "zova-module-rest-resource": "^5.1.52"
12
12
  },
13
13
  "title": "a-cabloy"
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-router",
3
- "version": "5.1.34",
3
+ "version": "5.1.35",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "router",
6
6
  "keywords": [