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.
- package/.cabloy-version +1 -1
- package/.claude/scheduled_tasks.lock +1 -1
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/repo-docs/frontend/page-params-guide.md +1 -1
- package/repo-docs/frontend/page-query-guide.md +6 -0
- package/repo-docs/frontend/zod-guide.md +9 -7
- package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
- package/vona/pnpm-lock.yaml +6 -0
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateBeanGenerals.ts +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScopeResources.ts +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +2014 -2053
- package/zova/pnpm-workspace.yaml +1 -1
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +66 -18
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialog.ts +9 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogDetail.ts +23 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogEntry.ts +23 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialog/controller.tsx +301 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogDetail/controller.tsx +135 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogEntry/controller.tsx +108 -0
- package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +19 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +63 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/index.ts +44 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +306 -18
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +51 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/config.ts +19 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/lib/routedDialogHistory.ts +76 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +156 -1
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +92 -13
- package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts +92 -0
- package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts +35 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -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/.metadata/index.ts +6 -6
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +60 -3
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +12 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/component/routerViewEmpty/controller.tsx +4 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +5 -1
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx
ADDED
|
@@ -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,
|
|
@@ -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
|
|
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?:
|
|
11
|
-
maxHeight?:
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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-suite-a-cabloy",
|
|
3
|
-
"version": "5.1.
|
|
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.
|
|
11
|
+
"zova-module-rest-resource": "^5.1.52"
|
|
12
12
|
},
|
|
13
13
|
"title": "a-cabloy"
|
|
14
14
|
}
|