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.
Files changed (84) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/scheduled_tasks.lock +1 -1
  3. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
  4. package/CHANGELOG.md +20 -0
  5. package/package.json +1 -1
  6. package/repo-docs/frontend/page-params-guide.md +1 -1
  7. package/repo-docs/frontend/page-query-guide.md +6 -0
  8. package/repo-docs/frontend/zod-guide.md +9 -7
  9. package/repo-e2e/specs/cabloy-basic.spec.ts +75 -0
  10. package/repo-e2e/specs/routed-dialog.spec.ts +368 -0
  11. package/vona/pnpm-lock.yaml +237 -44
  12. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +2 -0
  13. package/zova/package.original.json +1 -1
  14. package/zova/packages-cli/cli/package.json +2 -2
  15. package/zova/packages-cli/cli-set-front/package.json +1 -1
  16. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateBeanGenerals.ts +1 -1
  17. package/zova/packages-cli/cli-set-front/src/lib/bean/toolsMetadata/generateScopeResources.ts +1 -1
  18. package/zova/packages-zova/zova/package.json +2 -2
  19. package/zova/pnpm-lock.yaml +1998 -2008
  20. package/zova/pnpm-workspace.yaml +1 -1
  21. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/index.ts +66 -18
  22. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialog.ts +9 -0
  23. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogDetail.ts +23 -0
  24. package/zova/src/suite/a-demo/modules/demo-basic/src/.metadata/page/routedDialogEntry.ts +23 -0
  25. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialog/controller.tsx +301 -0
  26. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogDetail/controller.tsx +135 -0
  27. package/zova/src/suite/a-demo/modules/demo-basic/src/page/routedDialogEntry/controller.tsx +108 -0
  28. package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +19 -0
  29. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/component/routedDialog.ts +64 -0
  30. package/zova/src/suite/cabloy-basic/modules/basic-app/src/.metadata/index.ts +44 -2
  31. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +306 -18
  32. package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +55 -0
  33. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/config.ts +19 -0
  34. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/en-us.ts +1 -0
  35. package/zova/src/suite/cabloy-basic/modules/basic-app/src/config/locale/zh-cn.ts +1 -0
  36. package/zova/src/suite/cabloy-basic/modules/basic-app/src/lib/routedDialogHistory.ts +76 -0
  37. package/zova/src/suite/cabloy-basic/modules/basic-app/src/service/appModal.ts +208 -1
  38. package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +124 -13
  39. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogHistory.test.ts +92 -0
  40. package/zova/src/suite/cabloy-basic/modules/basic-app/test/lib/routedDialogRouter.test.ts +35 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/index.ts +6 -6
  42. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +95 -10
  43. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +1 -0
  45. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +1 -0
  46. package/zova/src/suite/cabloy-basic/modules/basic-page/src/index.ts +1 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +35 -0
  48. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +24 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/component/blockResourcePickerActions.ts +34 -0
  50. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/index.ts +30 -8
  51. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/.metadata/locales.ts +7 -0
  52. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/blockResourcePickerActions/controller.tsx +92 -0
  53. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +219 -11
  54. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/en-us.ts +8 -0
  55. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/config/locale/zh-cn.ts +7 -0
  56. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/index.ts +2 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePicker.ts +29 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-resource/test/lib/resourcePicker.test.ts +32 -0
  59. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +18 -12
  60. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  61. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/component/resourcePickerContext.ts +31 -0
  62. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/index.ts +48 -5
  63. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/.metadata/page/resourcePicker.ts +16 -0
  64. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/component/resourcePickerContext/controller.tsx +26 -0
  65. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/index.ts +2 -0
  66. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/resourcePicker.ts +1 -0
  67. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -1
  68. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resourcePicker/controller.tsx +144 -0
  69. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +9 -0
  70. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  71. package/zova/src/suite-vendor/a-zova/modules/a-bean/cli/controller/metadata/generateFileComponent.ts +40 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  74. package/zova/src/suite-vendor/a-zova/modules/a-router/src/.metadata/index.ts +6 -6
  75. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +60 -3
  76. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +12 -0
  77. package/zova/src/suite-vendor/a-zova/modules/a-router/src/component/routerViewEmpty/controller.tsx +4 -1
  78. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routerViewBase.tsx +10 -1
  79. package/zova/src/suite-vendor/a-zova/modules/a-routerstack/package.json +1 -1
  80. package/zova/src/suite-vendor/a-zova/modules/a-routerstack/src/component/routerViewStack/controller.tsx +7 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  82. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +2 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +13 -10
  84. 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 {
@@ -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 { reconcileSelection, selectionKey, selectionRowId } from '../../lib/selection.js';
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.rowSelection = {};
84
- this.selectedIds = [];
85
- this.selectedRows = new Map();
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.select(this.query);
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
- this.rowSelection = reconcileSelection(this.rowSelection, data, next);
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']] } : {};
@@ -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',
@@ -6,6 +6,7 @@ export default {
6
6
  Select: '选择',
7
7
  Done: '完成',
8
8
  SelectedItems: '已选择 %d 项',
9
+ SelectedItemsWithMax: '已选择 %d / %d 项',
9
10
  BulkActions: '批量操作',
10
11
  SelectionActionUnavailable: '请选择一个或多个有权限的行以启用此操作',
11
12
  SelectionActionMaxExceeded: '最多选择 %d 项以启用此操作',
@@ -1,3 +1,4 @@
1
1
  export * from './.metadata/index.js';
2
2
  export * from './.metadata/locales.js';
3
3
  export * from './types/index.js';
4
+ export * from './lib/selection.js';
@@ -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 { ControllerFormFieldResourcePicker } from '../component/formFieldResourcePicker/controller.jsx';
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.formFieldResourcePicker': ControllerFormFieldResourcePicker;
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
- 'formFieldResourcePicker': ZFormFieldResourcePicker,
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:formFieldResourcePicker': ControllerFormFieldResourcePicker;
48
+ 'basic-resource:blockResourcePickerActions': ControllerBlockResourcePickerActions;
49
+ 'basic-resource:formFieldResourcePicker': ControllerFormFieldResourcePicker;
38
50
  }
39
51
  export interface IZovaComponentRecord {
40
- 'basic-resource:formFieldResourcePicker': typeof ZFormFieldResourcePicker;
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,7 @@
1
+ import locale_en_us from '../config/locale/en-us.js';
2
+ import locale_zh_cn from '../config/locale/zh-cn.js';
3
+
4
+ export const locales = {
5
+ 'en-us': locale_en_us,
6
+ 'zh-cn': locale_zh_cn,
7
+ };
@@ -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
+ }