cabloy 5.1.167 → 5.1.168
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 +7 -0
- package/package.json +1 -1
- package/repo-e2e/specs/cabloy-basic.spec.ts +75 -0
- package/vona/pnpm-lock.yaml +235 -48
- package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +2 -0
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +113 -84
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +6 -2
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +60 -8
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +40 -8
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/index.ts +6 -6
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +95 -10
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +35 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +24 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/component/blockResourcePickerActions.ts +34 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/index.ts +30 -8
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/locales.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/blockResourcePickerActions/controller.tsx +92 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +219 -11
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/en-us.ts +8 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/zh-cn.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/index.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePicker.ts +29 -0
- package/zova/src/suite/cabloy-basic/modules/basic-resource/test/lib/resourcePicker.test.ts +32 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +18 -12
- 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/.metadata/component/resourcePickerContext.ts +31 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/index.ts +48 -5
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/page/resourcePicker.ts +16 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/component/resourcePickerContext/controller.tsx +26 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/index.ts +2 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/resourcePicker.ts +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resourcePicker/controller.tsx +144 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +9 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateFileComponent.ts +40 -1
- package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
- 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/lib/routerViewBase.tsx +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routerstack/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routerstack/src/component/routerViewStack/controller.tsx +7 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +2 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +13 -10
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
routerViewLocationKey,
|
|
7
7
|
viewDepthKey,
|
|
8
8
|
} from '@cabloy/vue-router';
|
|
9
|
-
import { computed,
|
|
9
|
+
import { computed, provide, shallowReactive } from 'vue';
|
|
10
|
+
import { defineComponent } from 'vue';
|
|
10
11
|
import { prepareComponentOptions, useController } from 'zova';
|
|
11
12
|
|
|
12
13
|
import type { ControllerRoutedDialogProps } from '../../component/routedDialog/controller.jsx';
|
package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { BeanControllerBase, cast } from 'zova';
|
|
|
5
5
|
import { Controller } from 'zova-module-a-bean';
|
|
6
6
|
import { ZRouterViewStack } from 'zova-module-a-routerstack';
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { IModalRoutedDialogItem, routedDialogContextKey } from '../../types/appModal.js';
|
|
9
9
|
|
|
10
10
|
export interface ControllerRoutedDialogProps {
|
|
11
11
|
item: IModalRoutedDialogItem;
|
|
@@ -41,7 +41,11 @@ export class ControllerRoutedDialog extends BeanControllerBase {
|
|
|
41
41
|
if (!this.$$modelStack) return <div class="loading loading-spinner"></div>;
|
|
42
42
|
const router = item.state.router;
|
|
43
43
|
if (!router) return <div class="text-error">Router unavailable</div>;
|
|
44
|
-
const vnode = h(ZRouterViewStack
|
|
44
|
+
const vnode = h(ZRouterViewStack, {
|
|
45
|
+
pageHostProviders: {
|
|
46
|
+
[routedDialogContextKey]: item.state.context,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
45
49
|
cast(vnode).zovaHostProviders = {
|
|
46
50
|
'a-router.bean.router': router,
|
|
47
51
|
'$$modelStack': this.$$modelStack,
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
IModalPromptOptions,
|
|
15
15
|
IModalRoutedDialogOptions,
|
|
16
16
|
IModalRoutedDialogPresentationOptions,
|
|
17
|
+
IModalRoutedDialogItem,
|
|
17
18
|
IModalRoutedDialogState,
|
|
19
|
+
IRoutedDialogContext,
|
|
18
20
|
IRoutedDialogHandle,
|
|
19
21
|
} from '../types/appModal.js';
|
|
20
22
|
|
|
@@ -27,6 +29,11 @@ export class ServiceAppModal extends BeanBase {
|
|
|
27
29
|
{ resolve: () => void; reject: (error: unknown) => void }
|
|
28
30
|
>();
|
|
29
31
|
|
|
32
|
+
private routedDialogResultSettlers = new Map<
|
|
33
|
+
number,
|
|
34
|
+
{ resolve: (value: unknown | undefined) => void; reject: (error: unknown) => void }
|
|
35
|
+
>();
|
|
36
|
+
|
|
30
37
|
private routedDialogHistories = new Map<number, RoutedDialogHistory>();
|
|
31
38
|
|
|
32
39
|
protected async __init__() {}
|
|
@@ -101,10 +108,14 @@ export class ServiceAppModal extends BeanBase {
|
|
|
101
108
|
return new AppModalItem(this, modalItem);
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
public routedDialog
|
|
105
|
-
|
|
111
|
+
public routedDialog<
|
|
112
|
+
TResult = unknown,
|
|
113
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
114
|
+
TSession = unknown,
|
|
115
|
+
>(
|
|
116
|
+
options: IModalRoutedDialogOptions<TProps, TSession>,
|
|
106
117
|
dialogOptions?: IModalRoutedDialogPresentationOptions,
|
|
107
|
-
): IRoutedDialogHandle {
|
|
118
|
+
): IRoutedDialogHandle<TResult> {
|
|
108
119
|
const id = this.newModalItemId();
|
|
109
120
|
let resolveReady!: () => void;
|
|
110
121
|
let rejectReady!: (error: unknown) => void;
|
|
@@ -112,25 +123,47 @@ export class ServiceAppModal extends BeanBase {
|
|
|
112
123
|
resolveReady = resolve;
|
|
113
124
|
rejectReady = reject;
|
|
114
125
|
});
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
let resolveResult!: (value: TResult | undefined) => void;
|
|
127
|
+
let rejectResult!: (error: unknown) => void;
|
|
128
|
+
const result = new Promise<TResult | undefined>((resolve, reject) => {
|
|
129
|
+
resolveResult = resolve;
|
|
130
|
+
rejectResult = reject;
|
|
131
|
+
});
|
|
132
|
+
// Both promises remain observable through the handle without becoming
|
|
133
|
+
// unhandled rejections when callers only need one lifecycle channel.
|
|
117
134
|
void ready.catch(() => {});
|
|
118
|
-
|
|
135
|
+
void result.catch(() => {});
|
|
136
|
+
let modalItem!: IModalRoutedDialogItem<TResult, TProps, TSession>;
|
|
137
|
+
const context: IRoutedDialogContext<TResult, TProps, TSession> = {
|
|
138
|
+
id,
|
|
139
|
+
props: options.props,
|
|
140
|
+
session: options.session as TSession,
|
|
141
|
+
resolve: value => this._resolveRoutedDialog(modalItem, value),
|
|
142
|
+
cancel: () => this.close(id),
|
|
143
|
+
};
|
|
144
|
+
modalItem = {
|
|
119
145
|
id,
|
|
120
146
|
type: 'routedDialog',
|
|
121
147
|
options,
|
|
122
148
|
dialogOptions,
|
|
123
|
-
state: shallowReactive<IModalRoutedDialogState
|
|
149
|
+
state: shallowReactive<IModalRoutedDialogState<TResult, TProps, TSession>>({
|
|
124
150
|
status: 'loading',
|
|
125
151
|
canGoBack: false,
|
|
126
152
|
ready,
|
|
153
|
+
result,
|
|
154
|
+
context,
|
|
127
155
|
}),
|
|
128
156
|
};
|
|
129
157
|
this.modalItems.push(modalItem);
|
|
130
158
|
this.routedDialogReadySettlers.set(id, { resolve: resolveReady, reject: rejectReady });
|
|
131
|
-
|
|
159
|
+
this.routedDialogResultSettlers.set(id, {
|
|
160
|
+
resolve: value => resolveResult(value as TResult | undefined),
|
|
161
|
+
reject: rejectResult,
|
|
162
|
+
});
|
|
163
|
+
const handle = new AppModalItem(this, modalItem) as IRoutedDialogHandle<TResult>;
|
|
132
164
|
Object.defineProperties(handle, {
|
|
133
165
|
ready: { enumerable: true, get: () => ready },
|
|
166
|
+
result: { enumerable: true, get: () => result },
|
|
134
167
|
push: {
|
|
135
168
|
enumerable: true,
|
|
136
169
|
value: (to: any) => this._navigateRoutedDialog(modalItem, to, false),
|
|
@@ -197,6 +230,7 @@ export class ServiceAppModal extends BeanBase {
|
|
|
197
230
|
modalItem.state.status = 'error';
|
|
198
231
|
modalItem.state.error = error;
|
|
199
232
|
this._settleRoutedDialogReady(modalItem.id, 'reject', error);
|
|
233
|
+
this._settleRoutedDialogResult(modalItem.id, 'reject', error);
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
|
|
@@ -233,6 +267,23 @@ export class ServiceAppModal extends BeanBase {
|
|
|
233
267
|
else settler.reject(error);
|
|
234
268
|
}
|
|
235
269
|
|
|
270
|
+
private _settleRoutedDialogResult(id: number, action: 'resolve' | 'reject', value?: unknown) {
|
|
271
|
+
const settler = this.routedDialogResultSettlers.get(id);
|
|
272
|
+
if (!settler) return;
|
|
273
|
+
this.routedDialogResultSettlers.delete(id);
|
|
274
|
+
if (action === 'resolve') settler.resolve(value);
|
|
275
|
+
else settler.reject(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private _resolveRoutedDialog(
|
|
279
|
+
modalItem: Extract<IModalItem, { type: 'routedDialog' }>,
|
|
280
|
+
value: unknown,
|
|
281
|
+
) {
|
|
282
|
+
if (!this._isRoutedDialogOpen(modalItem)) return;
|
|
283
|
+
this._settleRoutedDialogResult(modalItem.id, 'resolve', value);
|
|
284
|
+
this.close(modalItem.id);
|
|
285
|
+
}
|
|
286
|
+
|
|
236
287
|
public close(id: number, _reason: string = 'api') {
|
|
237
288
|
const [index, modalItem] = this.findModalItem(id);
|
|
238
289
|
if (index === -1 || !modalItem) return;
|
|
@@ -245,6 +296,7 @@ export class ServiceAppModal extends BeanBase {
|
|
|
245
296
|
this.routedDialogHistories.delete(modalItem.id);
|
|
246
297
|
modalItem.state.router = undefined;
|
|
247
298
|
this._settleRoutedDialogReady(modalItem.id, 'reject', new Error('routedDialog is closed'));
|
|
299
|
+
this._settleRoutedDialogResult(modalItem.id, 'resolve');
|
|
248
300
|
modalItem.options.onClose?.();
|
|
249
301
|
return;
|
|
250
302
|
}
|
|
@@ -71,15 +71,37 @@ export interface IModalDialogRenderOptions {
|
|
|
71
71
|
onClose?: () => void;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export
|
|
74
|
+
export const routedDialogContextKey = '$routedDialogContext';
|
|
75
|
+
|
|
76
|
+
export interface IRoutedDialogContext<
|
|
77
|
+
TResult = unknown,
|
|
78
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
79
|
+
TSession = unknown,
|
|
80
|
+
> {
|
|
81
|
+
readonly id: number;
|
|
82
|
+
readonly props?: TProps;
|
|
83
|
+
readonly session: TSession;
|
|
84
|
+
resolve(value: TResult): void;
|
|
85
|
+
cancel(): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface IModalRoutedDialogOptions<
|
|
89
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
90
|
+
TSession = unknown,
|
|
91
|
+
> extends IModalRoutedDialogPresentationOptions {
|
|
75
92
|
route: RouteLocationRaw;
|
|
76
93
|
icon?: keyof IIconRecord;
|
|
77
94
|
title?: string;
|
|
78
|
-
props?:
|
|
95
|
+
props?: TProps;
|
|
96
|
+
session?: TSession;
|
|
79
97
|
onClose?: () => void;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
export interface IModalRoutedDialogState
|
|
100
|
+
export interface IModalRoutedDialogState<
|
|
101
|
+
TResult = unknown,
|
|
102
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
103
|
+
TSession = unknown,
|
|
104
|
+
> {
|
|
83
105
|
status: 'loading' | 'ready' | 'error' | 'closed';
|
|
84
106
|
closed?: boolean;
|
|
85
107
|
error?: unknown;
|
|
@@ -87,24 +109,34 @@ export interface IModalRoutedDialogState {
|
|
|
87
109
|
currentRoute?: RouteLocationNormalizedLoadedGeneric;
|
|
88
110
|
canGoBack: boolean;
|
|
89
111
|
ready: Promise<void>;
|
|
112
|
+
result: Promise<TResult | undefined>;
|
|
113
|
+
context: IRoutedDialogContext<TResult, TProps, TSession>;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
export type IModalRoutedDialogInput
|
|
116
|
+
export type IModalRoutedDialogInput<
|
|
117
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
118
|
+
TSession = unknown,
|
|
119
|
+
> = Omit<IModalRoutedDialogOptions<TProps, TSession>, 'route'>;
|
|
93
120
|
|
|
94
121
|
export interface IModalRoutedDialogNavigate {
|
|
95
122
|
navigate(to: RouteLocationRaw, replace?: boolean): Promise<NavigationFailure | void | undefined>;
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
export interface IModalRoutedDialogItem
|
|
125
|
+
export interface IModalRoutedDialogItem<
|
|
126
|
+
TResult = unknown,
|
|
127
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
128
|
+
TSession = unknown,
|
|
129
|
+
> {
|
|
99
130
|
id: number;
|
|
100
131
|
type: 'routedDialog';
|
|
101
|
-
options: IModalRoutedDialogOptions
|
|
132
|
+
options: IModalRoutedDialogOptions<TProps, TSession>;
|
|
102
133
|
dialogOptions?: IModalRoutedDialogPresentationOptions;
|
|
103
|
-
state: IModalRoutedDialogState
|
|
134
|
+
state: IModalRoutedDialogState<TResult, TProps, TSession>;
|
|
104
135
|
}
|
|
105
136
|
|
|
106
|
-
export interface IRoutedDialogHandle extends AppModalItem {
|
|
137
|
+
export interface IRoutedDialogHandle<TResult = unknown> extends AppModalItem {
|
|
107
138
|
readonly ready: Promise<void>;
|
|
139
|
+
readonly result: Promise<TResult | undefined>;
|
|
108
140
|
push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>;
|
|
109
141
|
replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>;
|
|
110
142
|
}
|
|
@@ -46,12 +46,12 @@ declare module 'zova-module-basic-page' {
|
|
|
46
46
|
}
|
|
47
47
|
/** controller: end */
|
|
48
48
|
/** controller: begin */
|
|
49
|
-
import { ControllerBlockFilter } from '../component/blockFilter/controller.jsx';
|
|
50
|
-
import { ControllerBlockFilterActions } from '../component/blockFilterActions/controller.jsx';
|
|
51
|
-
import { ControllerBlockPage } from '../component/blockPage/controller.jsx';
|
|
52
|
-
import { ControllerBlockPager } from '../component/blockPager/controller.jsx';
|
|
53
|
-
import { ControllerBlockTable } from '../component/blockTable/controller.jsx';
|
|
54
|
-
import { ControllerBlockToolbarBulk } from '../component/blockToolbarBulk/controller.jsx';
|
|
49
|
+
import type { ControllerBlockFilter } from '../component/blockFilter/controller.jsx';
|
|
50
|
+
import type { ControllerBlockFilterActions } from '../component/blockFilterActions/controller.jsx';
|
|
51
|
+
import type { ControllerBlockPage } from '../component/blockPage/controller.jsx';
|
|
52
|
+
import type { ControllerBlockPager } from '../component/blockPager/controller.jsx';
|
|
53
|
+
import type { ControllerBlockTable } from '../component/blockTable/controller.jsx';
|
|
54
|
+
import type { ControllerBlockToolbarBulk } from '../component/blockToolbarBulk/controller.jsx';
|
|
55
55
|
import 'zova';
|
|
56
56
|
declare module 'zova' {
|
|
57
57
|
export interface IBeanRecordLocal {
|
package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
IPageScope,
|
|
7
7
|
IResourceBlockOptionsBase,
|
|
8
8
|
IResourceRenderBlockOptionsBlock,
|
|
9
|
+
IResourceTableSelectionPayload,
|
|
9
10
|
ITablePaged,
|
|
10
11
|
ITableQuery,
|
|
11
12
|
ITableResPaged,
|
|
@@ -21,7 +22,14 @@ import { $QueriesEnsureLoaded } from 'zova-module-a-model';
|
|
|
21
22
|
import { BeanControllerTableBase } from 'zova-module-a-table';
|
|
22
23
|
import { ModelResource } from 'zova-module-rest-resource';
|
|
23
24
|
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
limitPickerRowSelection,
|
|
27
|
+
reconcileSelection,
|
|
28
|
+
resolvePickerSelectionMax,
|
|
29
|
+
selectionKey,
|
|
30
|
+
selectionRowId,
|
|
31
|
+
TypeResourcePickerSelectionMode,
|
|
32
|
+
} from '../../lib/selection.js';
|
|
25
33
|
|
|
26
34
|
declare module 'zova-module-a-openapi' {
|
|
27
35
|
export interface IResourceBlockRecord {
|
|
@@ -32,9 +40,15 @@ declare module 'zova-module-a-openapi' {
|
|
|
32
40
|
export interface ControllerBlockPageProps extends IResourceBlockOptionsBase {
|
|
33
41
|
blocks?: IResourceRenderBlockOptionsBlock[];
|
|
34
42
|
resource?: string;
|
|
43
|
+
actionPath?: string;
|
|
35
44
|
pageSize?: number;
|
|
36
45
|
queryFixed?: ITableQuery;
|
|
37
46
|
selectionPolicy?: 'always' | 'onDemand' | false;
|
|
47
|
+
selectionMode?: TypeResourcePickerSelectionMode;
|
|
48
|
+
selectionMax?: number;
|
|
49
|
+
selectedIds?: readonly TableIdentity[];
|
|
50
|
+
selectedRows?: readonly Record<string, unknown>[];
|
|
51
|
+
onSelectionChange?: (selection: IResourceTableSelectionPayload) => void;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
@Controller()
|
|
@@ -65,6 +79,10 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
65
79
|
selectionVisible = false;
|
|
66
80
|
selectionRequiredCount = 0;
|
|
67
81
|
|
|
82
|
+
private _selectionChangeListeners = new Set<
|
|
83
|
+
(selection: IResourceTableSelectionPayload<TData>) => void
|
|
84
|
+
>();
|
|
85
|
+
|
|
68
86
|
$$modelResource: ModelResource<TData>;
|
|
69
87
|
|
|
70
88
|
protected async __init__() {
|
|
@@ -80,9 +98,9 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
80
98
|
this.queryFilterData = {};
|
|
81
99
|
this.querySortingData = {};
|
|
82
100
|
this.sorting = [];
|
|
83
|
-
this.
|
|
84
|
-
this.
|
|
85
|
-
this.selectedRows =
|
|
101
|
+
this.selectedIds = this._normalizeSelectedIds(this.$props.selectedIds ?? []);
|
|
102
|
+
this.rowSelection = Object.fromEntries(this.selectedIds.map(id => [selectionKey(id), true]));
|
|
103
|
+
this.selectedRows = this._normalizeSelectedRows(this.$props.selectedRows ?? []);
|
|
86
104
|
this.$watch(
|
|
87
105
|
() => this.$props.queryFixed,
|
|
88
106
|
queryFixed => {
|
|
@@ -103,6 +121,8 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
103
121
|
() => this.$$modelResource.apiSchemasSelect.sdk,
|
|
104
122
|
() => this.queryData,
|
|
105
123
|
);
|
|
124
|
+
this._refreshSelectedRows((this.data ?? []) as unknown as Record<string, unknown>[]);
|
|
125
|
+
this._notifySelectionChange();
|
|
106
126
|
// watch
|
|
107
127
|
this.$watch(
|
|
108
128
|
() => this.permissions,
|
|
@@ -115,6 +135,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
115
135
|
() => this.data,
|
|
116
136
|
data => {
|
|
117
137
|
this._refreshSelectedRows((data ?? []) as unknown as Record<string, unknown>[]);
|
|
138
|
+
this._notifySelectionChange();
|
|
118
139
|
},
|
|
119
140
|
);
|
|
120
141
|
}
|
|
@@ -123,6 +144,32 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
123
144
|
return this.$props.resource;
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
private _normalizeSelectedIds(ids: readonly TableIdentity[]) {
|
|
148
|
+
const selectedIds: TableIdentity[] = [];
|
|
149
|
+
const keys = new Set<string>();
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
const key = selectionKey(id);
|
|
152
|
+
if (keys.has(key)) continue;
|
|
153
|
+
keys.add(key);
|
|
154
|
+
selectedIds.push(id);
|
|
155
|
+
}
|
|
156
|
+
return this.selectionMode === 'single'
|
|
157
|
+
? selectedIds.slice(-1)
|
|
158
|
+
: selectedIds.slice(0, this.selectionMax);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private _normalizeSelectedRows(rows: readonly Record<string, unknown>[]) {
|
|
162
|
+
const selectedKeys = new Set(this.selectedIds.map(selectionKey));
|
|
163
|
+
const selectedRows = new Map<string, TData>();
|
|
164
|
+
for (const row of rows) {
|
|
165
|
+
const id = selectionRowId(row);
|
|
166
|
+
const key = selectionKey(id);
|
|
167
|
+
if (!selectedKeys.has(key) || selectedRows.has(key)) continue;
|
|
168
|
+
selectedRows.set(key, row as TData);
|
|
169
|
+
}
|
|
170
|
+
return selectedRows;
|
|
171
|
+
}
|
|
172
|
+
|
|
126
173
|
get query() {
|
|
127
174
|
const { where: whereFixed, ...queryFixed } = this.queryFixedData;
|
|
128
175
|
const { where: whereFilter, ...queryFilter } = this.queryFilterData;
|
|
@@ -136,7 +183,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
136
183
|
}
|
|
137
184
|
|
|
138
185
|
get queryData() {
|
|
139
|
-
return this.$$modelResource.
|
|
186
|
+
return this.$$modelResource.selectGeneral(this.$props.actionPath, this.query);
|
|
140
187
|
}
|
|
141
188
|
|
|
142
189
|
get data() {
|
|
@@ -174,6 +221,18 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
174
221
|
};
|
|
175
222
|
}
|
|
176
223
|
|
|
224
|
+
get selectionMode(): TypeResourcePickerSelectionMode {
|
|
225
|
+
return this.$props.selectionMode ?? 'multiple';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get selectionMax() {
|
|
229
|
+
return resolvePickerSelectionMax(this.$props.selectionMode, this.$props.selectionMax);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get selectionPersistent() {
|
|
233
|
+
return this.$props.selectionMode !== undefined || this.$props.selectionMax !== undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
177
236
|
get selectionAvailable() {
|
|
178
237
|
const policy = this.$props.selectionPolicy;
|
|
179
238
|
return (
|
|
@@ -192,19 +251,45 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
192
251
|
return policy === 'always' || (this.selectionVisible && policy !== false);
|
|
193
252
|
}
|
|
194
253
|
|
|
254
|
+
onSelectionChange(listener: (selection: IResourceTableSelectionPayload<TData>) => void) {
|
|
255
|
+
this._selectionChangeListeners.add(listener);
|
|
256
|
+
return () => this._selectionChangeListeners.delete(listener);
|
|
257
|
+
}
|
|
258
|
+
|
|
195
259
|
onRowSelectionChange(
|
|
196
260
|
updater: RowSelectionState | ((old: RowSelectionState) => RowSelectionState),
|
|
197
261
|
) {
|
|
198
262
|
const data = (this.data ?? []) as unknown as Record<string, unknown>[];
|
|
199
263
|
const next = functionalUpdate(updater, this.rowSelection);
|
|
200
|
-
|
|
264
|
+
const reconciled = reconcileSelection(this.rowSelection, data, next);
|
|
265
|
+
this.rowSelection = this._limitRowSelection(reconciled, data);
|
|
201
266
|
this._reconcileSelectedRows(data);
|
|
267
|
+
this._notifySelectionChange();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private _limitRowSelection(
|
|
271
|
+
rowSelection: RowSelectionState,
|
|
272
|
+
data: readonly Record<string, unknown>[],
|
|
273
|
+
) {
|
|
274
|
+
if (this.$props.selectionMode === undefined && this.$props.selectionMax === undefined) {
|
|
275
|
+
return rowSelection;
|
|
276
|
+
}
|
|
277
|
+
return limitPickerRowSelection(rowSelection, data, this.selectionMode, this.selectionMax);
|
|
202
278
|
}
|
|
203
279
|
|
|
204
280
|
clearSelection() {
|
|
205
281
|
this.rowSelection = {};
|
|
206
282
|
this.selectedIds = [];
|
|
207
283
|
this.selectedRows = new Map();
|
|
284
|
+
this._notifySelectionChange();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private _notifySelectionChange() {
|
|
288
|
+
const selection = this.selection;
|
|
289
|
+
this.$props.onSelectionChange?.(selection);
|
|
290
|
+
for (const listener of this._selectionChangeListeners) {
|
|
291
|
+
listener(selection);
|
|
292
|
+
}
|
|
208
293
|
}
|
|
209
294
|
|
|
210
295
|
private _refreshSelectedRows(data: readonly Record<string, unknown>[]) {
|
|
@@ -263,7 +348,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
263
348
|
|
|
264
349
|
setPageSize(pageSize: number) {
|
|
265
350
|
if (this.queryPaged.pageSize !== pageSize) {
|
|
266
|
-
this.clearSelection();
|
|
351
|
+
if (!this.selectionPersistent) this.clearSelection();
|
|
267
352
|
this.queryPaged.pageSize = pageSize;
|
|
268
353
|
this.queryPaged.pageNo = 1;
|
|
269
354
|
}
|
|
@@ -272,14 +357,14 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
272
357
|
setQueryFixed(queryFixed?: ITableQuery) {
|
|
273
358
|
const next = queryFixed ?? {};
|
|
274
359
|
if (deepEqual(this.queryFixedData, next)) return;
|
|
275
|
-
this.clearSelection();
|
|
360
|
+
if (!this.selectionPersistent) this.clearSelection();
|
|
276
361
|
this.queryFixedData = next;
|
|
277
362
|
this.queryPaged.pageNo = 1;
|
|
278
363
|
}
|
|
279
364
|
|
|
280
365
|
onFilter(data: ITableQuery) {
|
|
281
366
|
if (deepEqual(this.queryFilterData, data)) return;
|
|
282
|
-
this.clearSelection();
|
|
367
|
+
if (!this.selectionPersistent) this.clearSelection();
|
|
283
368
|
this.queryFilterData = data;
|
|
284
369
|
this.queryPaged.pageNo = 1;
|
|
285
370
|
}
|
|
@@ -287,7 +372,7 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
|
|
|
287
372
|
onSortingChange(updater: SortingState | ((old: SortingState) => SortingState)) {
|
|
288
373
|
const sorting = functionalUpdate(updater, this.sorting).slice(0, 1);
|
|
289
374
|
if (deepEqual(this.sorting, sorting)) return;
|
|
290
|
-
this.clearSelection();
|
|
375
|
+
if (!this.selectionPersistent) this.clearSelection();
|
|
291
376
|
this.sorting = sorting;
|
|
292
377
|
const first = this.sorting[0];
|
|
293
378
|
this.querySortingData = first ? { orders: [[first.id, first.desc ? 'desc' : 'asc']] } : {};
|
package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx
CHANGED
|
@@ -46,6 +46,7 @@ export class ControllerBlockTable<TData extends {} = {}> extends BeanControllerB
|
|
|
46
46
|
sorting={$$page.sorting}
|
|
47
47
|
onSortingChange={updater => $$page.onSortingChange(updater)}
|
|
48
48
|
enableRowSelection={$$page.selectionEnabled}
|
|
49
|
+
selectionMode={$$page.selectionMode}
|
|
49
50
|
rowSelection={$$page.rowSelection}
|
|
50
51
|
onRowSelectionChange={updater => $$page.onRowSelectionChange(updater)}
|
|
51
52
|
tableScope={$$page.jsxCelScope}
|
|
@@ -7,6 +7,7 @@ export default {
|
|
|
7
7
|
Done: 'Done',
|
|
8
8
|
SelectedItems: 'Selected %d items',
|
|
9
9
|
SelectedItems_1: 'Selected one item',
|
|
10
|
+
SelectedItemsWithMax: 'Selected %d / %d items',
|
|
10
11
|
BulkActions: 'Bulk actions',
|
|
11
12
|
SelectionActionUnavailable: 'Select one or more permitted rows to enable this action',
|
|
12
13
|
SelectionActionMaxExceeded: 'Select at most %d items to enable this action',
|
|
@@ -8,11 +8,30 @@ import type {
|
|
|
8
8
|
import { isNil } from '@cabloy/utils';
|
|
9
9
|
|
|
10
10
|
export const ResourceSelectedMaxIds = 100;
|
|
11
|
+
export const ResourcePickerSelectionMax = 100;
|
|
12
|
+
|
|
13
|
+
export type TypeResourcePickerSelectionMode = 'single' | 'multiple';
|
|
11
14
|
|
|
12
15
|
export function resolveSelectionMaxIds(selectedMaxIds: number | undefined): number {
|
|
13
16
|
return selectedMaxIds ?? ResourceSelectedMaxIds;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
export function resolvePickerSelectionMax(
|
|
20
|
+
selectionMode: TypeResourcePickerSelectionMode | undefined,
|
|
21
|
+
selectionMax: number | undefined,
|
|
22
|
+
): number {
|
|
23
|
+
if (selectionMode === 'single') return 1;
|
|
24
|
+
if (
|
|
25
|
+
selectionMax === undefined ||
|
|
26
|
+
!Number.isFinite(selectionMax) ||
|
|
27
|
+
!Number.isInteger(selectionMax) ||
|
|
28
|
+
selectionMax < 0
|
|
29
|
+
) {
|
|
30
|
+
return ResourcePickerSelectionMax;
|
|
31
|
+
}
|
|
32
|
+
return selectionMax;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
export type TypeTableActionBulkDynamicDisabledReason = 'maxExceeded' | 'selectionUnavailable';
|
|
17
36
|
|
|
18
37
|
export interface IResourceTableActionBulkDynamicProps {
|
|
@@ -82,3 +101,19 @@ export function reconcileSelection(
|
|
|
82
101
|
}
|
|
83
102
|
return result;
|
|
84
103
|
}
|
|
104
|
+
|
|
105
|
+
export function limitPickerRowSelection(
|
|
106
|
+
selection: RowSelectionState,
|
|
107
|
+
rows: readonly Record<string, unknown>[],
|
|
108
|
+
selectionMode: TypeResourcePickerSelectionMode,
|
|
109
|
+
selectionMax: number,
|
|
110
|
+
): RowSelectionState {
|
|
111
|
+
const selectedKeys = Object.keys(selection).filter(key => selection[key]);
|
|
112
|
+
if (selectedKeys.length <= selectionMax) return selection;
|
|
113
|
+
const currentPageKeys = selectionRowIds(rows);
|
|
114
|
+
const retainedKeys =
|
|
115
|
+
selectionMode === 'single'
|
|
116
|
+
? selectedKeys.filter(key => currentPageKeys.has(key)).slice(-1)
|
|
117
|
+
: selectedKeys.slice(0, selectionMax);
|
|
118
|
+
return Object.fromEntries(retainedKeys.map(key => [key, true]));
|
|
119
|
+
}
|
|
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
limitPickerRowSelection,
|
|
5
6
|
reconcileSelection,
|
|
7
|
+
resolvePickerSelectionMax,
|
|
6
8
|
resolveSelectionMaxIds,
|
|
7
9
|
resolveTableActionBulkDynamicProps,
|
|
8
10
|
selectionKey,
|
|
@@ -16,6 +18,17 @@ test('selection maximum uses action metadata before the frontend fallback', () =
|
|
|
16
18
|
assert.equal(resolveSelectionMaxIds(0), 0);
|
|
17
19
|
});
|
|
18
20
|
|
|
21
|
+
test('picker selection maximum distinguishes single and multiple modes', () => {
|
|
22
|
+
assert.equal(resolvePickerSelectionMax(undefined, undefined), 100);
|
|
23
|
+
assert.equal(resolvePickerSelectionMax('multiple', undefined), 100);
|
|
24
|
+
assert.equal(resolvePickerSelectionMax('multiple', 3), 3);
|
|
25
|
+
assert.equal(resolvePickerSelectionMax('multiple', 0), 0);
|
|
26
|
+
assert.equal(resolvePickerSelectionMax('single', 99), 1);
|
|
27
|
+
assert.equal(resolvePickerSelectionMax('multiple', -1), 100);
|
|
28
|
+
assert.equal(resolvePickerSelectionMax('multiple', 1.5), 100);
|
|
29
|
+
assert.equal(resolvePickerSelectionMax('multiple', Number.NaN), 100);
|
|
30
|
+
});
|
|
31
|
+
|
|
19
32
|
test('bulk action runtime props do not overwrite static action metadata', () => {
|
|
20
33
|
const selection = { ids: [1], rows: [{ id: 1 }], count: 1 };
|
|
21
34
|
|
|
@@ -97,3 +110,14 @@ test('current-page reconciliation removes only deselected loaded identities', ()
|
|
|
97
110
|
|
|
98
111
|
assert.deepEqual(selection, { previous: true });
|
|
99
112
|
});
|
|
113
|
+
|
|
114
|
+
test('picker selection caps page-wide updates while retaining earlier identities', () => {
|
|
115
|
+
const rows = [{ id: '2' }, { id: '3' }, { id: '4' }];
|
|
116
|
+
assert.deepEqual(
|
|
117
|
+
limitPickerRowSelection({ 1: true, 2: true, 3: true, 4: true }, rows, 'multiple', 3),
|
|
118
|
+
{ 1: true, 2: true, 3: true },
|
|
119
|
+
);
|
|
120
|
+
assert.deepEqual(limitPickerRowSelection({ previous: true, 2: true }, rows, 'single', 1), {
|
|
121
|
+
2: true,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TypeControllerInnerProps } from 'zova';
|
|
2
|
+
|
|
3
|
+
import { defineComponent } from 'vue';
|
|
4
|
+
import { prepareComponentOptions, useController } from 'zova';
|
|
5
|
+
|
|
6
|
+
import type { ControllerBlockResourcePickerActionsProps } from '../../component/blockResourcePickerActions/controller.jsx';
|
|
7
|
+
|
|
8
|
+
import { ControllerBlockResourcePickerActions } from '../../component/blockResourcePickerActions/controller.jsx';
|
|
9
|
+
export type ZBlockResourcePickerActionsProps = {
|
|
10
|
+
controllerRef?: (ref: ControllerBlockResourcePickerActions) => void;
|
|
11
|
+
} & ControllerBlockResourcePickerActionsProps;
|
|
12
|
+
|
|
13
|
+
type ControllerInnerProps = TypeControllerInnerProps<
|
|
14
|
+
ControllerBlockResourcePickerActionsProps,
|
|
15
|
+
keyof typeof ControllerBlockResourcePickerActions.$propsDefault
|
|
16
|
+
>;
|
|
17
|
+
declare module 'zova-module-basic-resource' {
|
|
18
|
+
export interface ControllerBlockResourcePickerActions {
|
|
19
|
+
$props: ControllerInnerProps;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ZBlockResourcePickerActions = defineComponent(
|
|
24
|
+
(_props: ZBlockResourcePickerActionsProps) => {
|
|
25
|
+
useController(ControllerBlockResourcePickerActions, undefined, undefined);
|
|
26
|
+
return () => {};
|
|
27
|
+
},
|
|
28
|
+
prepareComponentOptions(ControllerBlockResourcePickerActions.$componentOptions),
|
|
29
|
+
);
|
|
30
|
+
declare module 'zova-module-a-bean' {
|
|
31
|
+
export interface IVonaComponentRecord {
|
|
32
|
+
'basic-resource:blockResourcePickerActions': ControllerBlockResourcePickerActionsProps;
|
|
33
|
+
}
|
|
34
|
+
}
|