cabloy 5.1.166 → 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/.claude/scheduled_tasks.lock +1 -1
- package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
- package/CHANGELOG.md +20 -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/cabloy-basic.spec.ts +75 -0
- package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
- package/vona/pnpm-lock.yaml +237 -44
- package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +2 -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 +1998 -2008
- 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 +64 -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 +55 -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 +208 -1
- package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +124 -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/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/model/resource.ts +1 -1
- 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/.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 +10 -1
- 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
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// eslint-disable
|
|
2
2
|
/** controller: begin */
|
|
3
|
+
export * from '../component/blockResourcePickerActions/controller.jsx';
|
|
3
4
|
export * from '../component/formFieldResourcePicker/controller.jsx';
|
|
4
5
|
|
|
5
6
|
import 'zova';
|
|
@@ -9,6 +10,11 @@ declare module 'zova' {
|
|
|
9
10
|
}
|
|
10
11
|
declare module 'zova-module-basic-resource' {
|
|
11
12
|
|
|
13
|
+
export interface ControllerBlockResourcePickerActions {
|
|
14
|
+
/** @internal */
|
|
15
|
+
get scope(): ScopeModuleBasicResource;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export interface ControllerFormFieldResourcePicker {
|
|
13
19
|
/** @internal */
|
|
14
20
|
get scope(): ScopeModuleBasicResource;
|
|
@@ -16,28 +22,35 @@ declare module 'zova-module-basic-resource' {
|
|
|
16
22
|
}
|
|
17
23
|
/** controller: end */
|
|
18
24
|
/** controller: begin */
|
|
19
|
-
import {
|
|
25
|
+
import type { ControllerBlockResourcePickerActions } from '../component/blockResourcePickerActions/controller.jsx';
|
|
26
|
+
import type { ControllerFormFieldResourcePicker } from '../component/formFieldResourcePicker/controller.jsx';
|
|
20
27
|
import 'zova';
|
|
21
28
|
declare module 'zova' {
|
|
22
29
|
export interface IBeanRecordLocal {
|
|
23
|
-
'basic-resource.controller.
|
|
30
|
+
'basic-resource.controller.blockResourcePickerActions': ControllerBlockResourcePickerActions;
|
|
31
|
+
'basic-resource.controller.formFieldResourcePicker': ControllerFormFieldResourcePicker;
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
/** controller: end */
|
|
27
35
|
|
|
28
36
|
/** components: begin */
|
|
37
|
+
export * from './component/blockResourcePickerActions.js';
|
|
38
|
+
import { ZBlockResourcePickerActions } from './component/blockResourcePickerActions.js';
|
|
29
39
|
export * from './component/formFieldResourcePicker.js';
|
|
30
40
|
import { ZFormFieldResourcePicker } from './component/formFieldResourcePicker.js';
|
|
31
41
|
export const components = {
|
|
32
|
-
'
|
|
42
|
+
'blockResourcePickerActions': ZBlockResourcePickerActions,
|
|
43
|
+
'formFieldResourcePicker': ZFormFieldResourcePicker,
|
|
33
44
|
};
|
|
34
45
|
import 'zova';
|
|
35
46
|
declare module 'zova' {
|
|
36
47
|
export interface IComponentRecord {
|
|
37
|
-
'basic-resource:
|
|
48
|
+
'basic-resource:blockResourcePickerActions': ControllerBlockResourcePickerActions;
|
|
49
|
+
'basic-resource:formFieldResourcePicker': ControllerFormFieldResourcePicker;
|
|
38
50
|
}
|
|
39
51
|
export interface IZovaComponentRecord {
|
|
40
|
-
'basic-resource:
|
|
52
|
+
'basic-resource:blockResourcePickerActions': typeof ZBlockResourcePickerActions;
|
|
53
|
+
'basic-resource:formFieldResourcePicker': typeof ZFormFieldResourcePicker;
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
/** components: end */
|
|
@@ -68,7 +81,7 @@ declare module 'zova-module-basic-resource' {
|
|
|
68
81
|
}
|
|
69
82
|
/** tableCell: end */
|
|
70
83
|
/** tableCell: begin */
|
|
71
|
-
import { TableCellResourcePicker } from '../bean/tableCell.resourcePicker.jsx';
|
|
84
|
+
import type { TableCellResourcePicker } from '../bean/tableCell.resourcePicker.jsx';
|
|
72
85
|
import 'zova';
|
|
73
86
|
declare module 'zova' {
|
|
74
87
|
export interface IBeanRecordGeneral {
|
|
@@ -76,8 +89,11 @@ declare module 'zova' {
|
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
/** tableCell: end */
|
|
92
|
+
/** locale: begin */
|
|
93
|
+
import { locales } from './locales.js';
|
|
94
|
+
/** locale: end */
|
|
79
95
|
/** scope: begin */
|
|
80
|
-
import { BeanScopeBase, type BeanScopeUtil } from 'zova';
|
|
96
|
+
import { BeanScopeBase, type BeanScopeUtil, TypeModuleLocales, TypeLocaleBase } from 'zova';
|
|
81
97
|
import { Scope } from 'zova-module-a-bean';
|
|
82
98
|
|
|
83
99
|
@Scope()
|
|
@@ -85,6 +101,7 @@ export class ScopeModuleBasicResource extends BeanScopeBase {}
|
|
|
85
101
|
|
|
86
102
|
export interface ScopeModuleBasicResource {
|
|
87
103
|
util: BeanScopeUtil;
|
|
104
|
+
locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
import 'zova';
|
|
@@ -95,9 +112,14 @@ declare module 'zova' {
|
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
export interface IBeanScopeLocale {
|
|
116
|
+
'basic-resource': (typeof locales)[TypeLocaleBase];
|
|
117
|
+
}
|
|
99
118
|
|
|
100
119
|
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
export function locale<K extends keyof (typeof locales)[TypeLocaleBase]>(key: K): `basic-resource::${K}` {
|
|
123
|
+
return `basic-resource::${key}`;
|
|
124
|
+
}
|
|
103
125
|
/** scope: end */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { IComponentOptions } from 'zova';
|
|
2
|
+
import type { IJsxRenderContextPage, IResourceBlockOptionsBase } from 'zova-module-a-openapi';
|
|
3
|
+
import type { IResourcePickerPageContext } from 'zova-module-rest-resource';
|
|
4
|
+
|
|
5
|
+
import { BeanControllerBase, Use } from 'zova';
|
|
6
|
+
import { Controller } from 'zova-module-a-bean';
|
|
7
|
+
import { resolvePickerSelectionMax } from 'zova-module-basic-page';
|
|
8
|
+
import { resourcePickerPageContextKey } from 'zova-module-rest-resource';
|
|
9
|
+
|
|
10
|
+
declare module 'zova-module-a-openapi' {
|
|
11
|
+
export interface IResourceBlockRecord {
|
|
12
|
+
'basic-resource:blockResourcePickerActions'?: ControllerBlockResourcePickerActionsProps;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ControllerBlockResourcePickerActionsProps extends IResourceBlockOptionsBase {}
|
|
17
|
+
|
|
18
|
+
@Controller()
|
|
19
|
+
export class ControllerBlockResourcePickerActions extends BeanControllerBase {
|
|
20
|
+
static $propsDefault = {
|
|
21
|
+
class: 'flex items-center justify-between gap-3',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
|
|
25
|
+
|
|
26
|
+
@Use({ injectionScope: 'host' })
|
|
27
|
+
$$renderContext: IJsxRenderContextPage;
|
|
28
|
+
|
|
29
|
+
@Use({ name: resourcePickerPageContextKey, injectionScope: 'host' })
|
|
30
|
+
$$pickerContext: IResourcePickerPageContext;
|
|
31
|
+
|
|
32
|
+
protected async __init__() {}
|
|
33
|
+
|
|
34
|
+
get pickerOptions() {
|
|
35
|
+
return this.$$pickerContext.options;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get selectionMax() {
|
|
39
|
+
return resolvePickerSelectionMax(
|
|
40
|
+
this.pickerOptions.selectionMode,
|
|
41
|
+
this.pickerOptions.selectionMax,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get selection() {
|
|
46
|
+
return this.$$renderContext.$$page.selection.ids;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get canConfirm() {
|
|
50
|
+
const count = this.selection.length;
|
|
51
|
+
return (
|
|
52
|
+
count > 0 &&
|
|
53
|
+
count <= this.selectionMax &&
|
|
54
|
+
(this.pickerOptions.selectionMode !== 'single' || count === 1)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public confirm() {
|
|
59
|
+
if (!this.canConfirm) return;
|
|
60
|
+
this.$$pickerContext.dialog.resolve(this.$$renderContext.$$page.selection);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public cancel() {
|
|
64
|
+
this.$$pickerContext.dialog.cancel();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected render() {
|
|
68
|
+
const count = this.selection.length;
|
|
69
|
+
return (
|
|
70
|
+
<div class="flex items-center justify-between gap-3">
|
|
71
|
+
<span class="text-sm text-base-content/70" role="status" aria-live="polite">
|
|
72
|
+
{this.pickerOptions.selectionMode === 'multiple'
|
|
73
|
+
? this.scope.locale.SelectedItemsWithMax(count, this.selectionMax)
|
|
74
|
+
: this.scope.locale.SelectedItems(count)}
|
|
75
|
+
</span>
|
|
76
|
+
<div class="join">
|
|
77
|
+
<button type="button" class="btn join-item" onClick={() => this.cancel()}>
|
|
78
|
+
{this.scope.locale.Cancel()}
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="btn btn-primary join-item"
|
|
83
|
+
disabled={!this.canConfirm}
|
|
84
|
+
onClick={() => this.confirm()}
|
|
85
|
+
>
|
|
86
|
+
{this.scope.locale.Select()}
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|