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
@@ -1,15 +1,34 @@
1
+ import type { TableIdentity } from 'table-identity';
1
2
  import type { IComponentOptions } from 'zova';
2
3
  import type { IFormFieldComponentOptions, IJsxRenderContextFormField } from 'zova-module-a-form';
3
- import type { IResourceFormFieldOptionsBase, ITableQuery } from 'zova-module-a-openapi';
4
+ import type {
5
+ IResourceFormFieldOptionsBase,
6
+ IResourceTableSelectionPayload,
7
+ ITableQuery,
8
+ } from 'zova-module-a-openapi';
9
+ import type {
10
+ IResourcePickerPageOptions,
11
+ IResourcePickerPageSession,
12
+ } from 'zova-module-rest-resource';
4
13
 
5
14
  import { classes } from 'typestyle';
6
15
  import { BeanControllerBase, Use } from 'zova';
7
16
  import { Controller } from 'zova-module-a-bean';
8
17
  import { ZFormField, ZFormFieldPreset } from 'zova-module-a-form';
9
18
  import { $QueryEnsureLoaded } from 'zova-module-a-model';
19
+ import { resolvePickerSelectionMax } from 'zova-module-basic-page';
10
20
  import { ZSelect, ZSelectProps } from 'zova-module-basic-select';
11
21
  import { ModelResource } from 'zova-module-rest-resource';
12
22
 
23
+ import type { TypeResourcePickerSelectionMode } from '../../lib/resourcePicker.js';
24
+
25
+ import {
26
+ normalizeResourcePickerIds,
27
+ resolveResourcePickerValue,
28
+ } from '../../lib/resourcePicker.js';
29
+
30
+ export type TypeResourcePickerMode = 'select' | 'routedDialog';
31
+
13
32
  declare module 'zova-module-a-openapi' {
14
33
  export interface IResourceFormFieldRecord {
15
34
  'basic-resource:formFieldResourcePicker'?: IResourceFormFieldResourcePickerOptions;
@@ -22,6 +41,9 @@ export interface IResourceFormFieldResourcePickerOptions extends IResourceFormFi
22
41
  query?: ITableQuery;
23
42
  relationName?: string;
24
43
  selectOptions?: ZSelectProps;
44
+ pickerMode?: TypeResourcePickerMode;
45
+ selectionMode?: TypeResourcePickerSelectionMode;
46
+ selectionMax?: number;
25
47
  }
26
48
 
27
49
  export interface ControllerFormFieldResourcePickerProps extends IFormFieldComponentOptions {
@@ -37,21 +59,21 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
37
59
  static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
38
60
 
39
61
  $$modelResource: ModelResource;
62
+ private _openingDialog = false;
63
+ private _pendingLabelIds = new Set<string>();
64
+ private _labelHydrationAttemptedIds = new Set<string>();
40
65
 
41
66
  @Use({ injectionScope: 'host' })
42
67
  $$renderContext: IJsxRenderContextFormField;
43
68
 
44
69
  protected async __init__() {
45
70
  const { ctx } = this.$$renderContext;
46
- // readonly
47
- if (this.$props.readonly) return;
48
- // modelResource
71
+ if (this.$props.readonly || this.pickerMode === 'routedDialog') return;
49
72
  this.$$modelResource = await ctx.bean._getBeanSelector(
50
73
  'rest-resource.model.resource',
51
74
  true,
52
75
  this.resource,
53
76
  );
54
- // load data
55
77
  await $QueryEnsureLoaded(() => this.queryData);
56
78
  }
57
79
 
@@ -65,6 +87,14 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
65
87
  return this.$props.options;
66
88
  }
67
89
 
90
+ get pickerMode(): TypeResourcePickerMode {
91
+ return this.resourcePickerOptions?.pickerMode ?? 'select';
92
+ }
93
+
94
+ get selectionMode(): TypeResourcePickerSelectionMode {
95
+ return this.resourcePickerOptions?.selectionMode ?? 'single';
96
+ }
97
+
68
98
  get queryData() {
69
99
  return this.$$modelResource.selectGeneral(
70
100
  this.resourcePickerOptions?.actionPath,
@@ -86,6 +116,11 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
86
116
  ></ZFormFieldPreset>
87
117
  );
88
118
  }
119
+ if (this.pickerMode === 'routedDialog') return this._renderRoutedDialogField();
120
+ return this._renderSelectField();
121
+ }
122
+
123
+ private _renderSelectField() {
89
124
  return (
90
125
  <ZFormField
91
126
  {...this.$props}
@@ -131,14 +166,187 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
131
166
  );
132
167
  }
133
168
 
169
+ private _renderRoutedDialogField() {
170
+ return (
171
+ <ZFormField
172
+ {...this.$props}
173
+ slotDefault={({ propsBucket, props }, $$formField) => {
174
+ const className = !propsBucket.needHandleBorder
175
+ ? classes(
176
+ props.class,
177
+ 'grow w-full h-full min-h-0 border-0 rounded-none bg-transparent px-0 py-0 text-left outline-none',
178
+ )
179
+ : classes(props.class, 'input', !$$formField.field.state.meta.isValid && 'input-error');
180
+ return (
181
+ <button
182
+ {...props}
183
+ type="button"
184
+ class={className}
185
+ onBlur={() => $$formField.handleBlur()}
186
+ onClick={() => {
187
+ void this._openPicker(propsBucket.value, value => {
188
+ $$formField.setValue(value, propsBucket.disableNotifyChanged);
189
+ }).catch(error => {
190
+ this.$errorHandler(error, 'ControllerFormFieldResourcePicker.openPicker');
191
+ });
192
+ }}
193
+ >
194
+ {this._formatPickerValue(propsBucket.value) || this.scope.locale.PleaseSelect()}
195
+ </button>
196
+ );
197
+ }}
198
+ ></ZFormField>
199
+ );
200
+ }
201
+
202
+ private async _openPicker(value: unknown, setValue: (value: unknown) => void) {
203
+ if (this._openingDialog) return;
204
+ this._openingDialog = true;
205
+ try {
206
+ const selectedIds = normalizeResourcePickerIds(value);
207
+ const options: IResourcePickerPageOptions = {
208
+ resource: this.resource,
209
+ actionPath: this.resourcePickerOptions?.actionPath,
210
+ query: this.resourcePickerOptions?.query,
211
+ selectionMode: this.selectionMode,
212
+ selectionMax: this.resourcePickerOptions?.selectionMax,
213
+ selectedIds,
214
+ };
215
+ const session: IResourcePickerPageSession = {
216
+ selectedIds: [...selectedIds],
217
+ selectedRows: [],
218
+ };
219
+ const handle = this.$appModal.routedDialog<
220
+ IResourceTableSelectionPayload,
221
+ IResourcePickerPageOptions,
222
+ IResourcePickerPageSession
223
+ >({
224
+ route: {
225
+ name: 'rest-resource:resourcePicker',
226
+ params: { resource: this.resource },
227
+ },
228
+ props: options,
229
+ session,
230
+ });
231
+ const result = await handle.result;
232
+ if (!result) return;
233
+ const nextValue = resolveResourcePickerValue(
234
+ result.ids,
235
+ this.selectionMode,
236
+ resolvePickerSelectionMax(this.selectionMode, this.resourcePickerOptions?.selectionMax),
237
+ );
238
+ this._syncRelationField(nextValue, result.rows);
239
+ setValue(nextValue);
240
+ } finally {
241
+ this._openingDialog = false;
242
+ }
243
+ }
244
+
245
+ private _formatPickerValue(value: unknown) {
246
+ const selectedIds = normalizeResourcePickerIds(value);
247
+ if (selectedIds.length === 0) return '';
248
+ const relationItems = this._getRelationItems();
249
+ const relationMap = new Map(
250
+ relationItems.map(item => [String(item.id as TableIdentity), item] as const),
251
+ );
252
+ const itemTitle = this._itemTitle;
253
+ const missingIds = selectedIds.filter(id => {
254
+ const item = relationMap.get(String(id));
255
+ return (
256
+ (item?.[itemTitle] === undefined || item[itemTitle] === null) &&
257
+ !this._labelHydrationAttemptedIds.has(String(id))
258
+ );
259
+ });
260
+ if (missingIds.length > 0) {
261
+ void this._hydrateMissingLabels(missingIds);
262
+ }
263
+ return selectedIds
264
+ .map(id => {
265
+ const title = relationMap.get(String(id))?.[itemTitle];
266
+ return title === undefined || title === null ? String(id) : String(title);
267
+ })
268
+ .join(', ');
269
+ }
270
+
134
271
  private _getValueByRelation() {
135
- const { $$form } = this.$$renderContext;
136
- let relationName = this.$props.options.relationName;
137
- if (!relationName) {
138
- relationName = this.$props.name!.substring(0, this.$props.name!.lastIndexOf('Id'));
272
+ const item = this._getRelationItems()[0];
273
+ return item?.[this._itemTitle];
274
+ }
275
+
276
+ private get _itemTitle() {
277
+ return String(this.resourcePickerOptions?.selectOptions?.itemTitle ?? 'name');
278
+ }
279
+
280
+ private get _relationName() {
281
+ const relationName = this.resourcePickerOptions?.relationName;
282
+ if (relationName) return relationName;
283
+ const fieldName = this.$props.name;
284
+ const index = fieldName?.lastIndexOf('Id') ?? -1;
285
+ return index > 0 ? fieldName!.substring(0, index) : undefined;
286
+ }
287
+
288
+ private _getRelationItems() {
289
+ const relationName = this._relationName;
290
+ if (!relationName) return [] as Record<string, unknown>[];
291
+ const value = this.$$renderContext.$$form.getFieldValue(relationName as never);
292
+ const items = Array.isArray(value) ? value : value ? [value] : [];
293
+ return items.filter(item => !!item && typeof item === 'object') as Record<string, unknown>[];
294
+ }
295
+
296
+ private _syncRelationField(value: unknown, rows: readonly Record<string, unknown>[]) {
297
+ const relationName = this._relationName;
298
+ if (!relationName) return;
299
+ const selectedIds = normalizeResourcePickerIds(value);
300
+ const itemTitle = this._itemTitle;
301
+ const rowMap = new Map(
302
+ this._getRelationItems().map(item => [String(item.id as TableIdentity), item] as const),
303
+ );
304
+ for (const row of rows) {
305
+ const id = row.id as TableIdentity;
306
+ if (id === undefined || id === null || id === '') continue;
307
+ const title = row[itemTitle];
308
+ if (title === undefined || title === null) continue;
309
+ rowMap.set(String(id), { id, [itemTitle]: title });
310
+ }
311
+ const relationItems = selectedIds.map(id => rowMap.get(String(id)) ?? { id });
312
+ const relationValue = this.selectionMode === 'multiple' ? relationItems : relationItems[0];
313
+ this.$$renderContext.$$form.setFieldValue(relationName as never, relationValue, true);
314
+ }
315
+
316
+ private async _hydrateMissingLabels(ids: readonly TableIdentity[]) {
317
+ const relationName = this._relationName;
318
+ if (!relationName) return;
319
+ const idsToLoad = ids.filter(id => {
320
+ const key = String(id);
321
+ if (this._pendingLabelIds.has(key) || this._labelHydrationAttemptedIds.has(key)) return false;
322
+ this._pendingLabelIds.add(key);
323
+ this._labelHydrationAttemptedIds.add(key);
324
+ return true;
325
+ });
326
+ if (idsToLoad.length === 0) return;
327
+ try {
328
+ const model = (await this.bean._getBeanSelector(
329
+ 'rest-resource.model.resource',
330
+ true,
331
+ this.resource,
332
+ )) as ModelResource;
333
+ const rows = await Promise.all(
334
+ idsToLoad.map(async id => {
335
+ const result = await model.view(id).refetch();
336
+ return result.data;
337
+ }),
338
+ );
339
+ const currentIds = normalizeResourcePickerIds(
340
+ this.$$renderContext.$$form.getFieldValue(this.$props.name as never),
341
+ );
342
+ this._syncRelationField(currentIds, rows.filter(Boolean) as Record<string, unknown>[]);
343
+ } catch (error) {
344
+ this.$errorHandler(error, 'ControllerFormFieldResourcePicker.hydrateMissingLabels');
345
+ } finally {
346
+ for (const id of idsToLoad) {
347
+ this._pendingLabelIds.delete(String(id));
348
+ }
139
349
  }
140
- const obj = $$form.getFieldValue(relationName);
141
- return obj?.[String(this.$props.options.selectOptions!.itemTitle)];
142
350
  }
143
351
 
144
352
  private _ensureEmptyItemFallback(
@@ -0,0 +1,8 @@
1
+ export default {
2
+ Cancel: 'Cancel',
3
+ PleaseSelect: 'Please select...',
4
+ Select: 'Select',
5
+ SelectedItems: 'Selected %d items',
6
+ SelectedItems_1: 'Selected one item',
7
+ SelectedItemsWithMax: 'Selected %d / %d items',
8
+ };
@@ -0,0 +1,7 @@
1
+ export default {
2
+ Cancel: '取消',
3
+ PleaseSelect: '请选择...',
4
+ Select: '选择',
5
+ SelectedItems: '已选择 %d 项',
6
+ SelectedItemsWithMax: '已选择 %d / %d 项',
7
+ };
@@ -1 +1,3 @@
1
+ export * from './.metadata/locales.js';
1
2
  export * from './.metadata/index.js';
3
+ export * from './lib/resourcePicker.js';
@@ -0,0 +1,29 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import { isNil } from '@cabloy/utils';
4
+
5
+ export type TypeResourcePickerSelectionMode = 'single' | 'multiple';
6
+
7
+ export function normalizeResourcePickerIds(value: unknown): TableIdentity[] {
8
+ const values = Array.isArray(value) ? value : isNil(value) || value === '' ? [] : [value];
9
+ const ids: TableIdentity[] = [];
10
+ const keys = new Set<string>();
11
+ for (const value of values) {
12
+ if (isNil(value) || value === '') continue;
13
+ const id = value as TableIdentity;
14
+ const key = String(id);
15
+ if (keys.has(key)) continue;
16
+ keys.add(key);
17
+ ids.push(id);
18
+ }
19
+ return ids;
20
+ }
21
+
22
+ export function resolveResourcePickerValue(
23
+ value: unknown,
24
+ selectionMode: TypeResourcePickerSelectionMode,
25
+ selectionMax: number,
26
+ ): TableIdentity | TableIdentity[] | undefined {
27
+ const selectedIds = normalizeResourcePickerIds(value).slice(0, selectionMax);
28
+ return selectionMode === 'single' ? selectedIds[0] : selectedIds;
29
+ }
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ normalizeResourcePickerIds,
6
+ resolveResourcePickerValue,
7
+ } from '../../src/lib/resourcePicker.ts';
8
+
9
+ test('resource picker normalization preserves identity types and deduplicates equivalent identities', () => {
10
+ assert.deepEqual(normalizeResourcePickerIds(undefined), []);
11
+ assert.deepEqual(normalizeResourcePickerIds(''), []);
12
+ assert.deepEqual(normalizeResourcePickerIds(7), [7]);
13
+ assert.deepEqual(normalizeResourcePickerIds([7, '7', 8, undefined, '']), [7, 8]);
14
+ });
15
+
16
+ test('resource picker result resolves a scalar identity for single mode', () => {
17
+ assert.equal(resolveResourcePickerValue([7, 8], 'single', 1), 7);
18
+ assert.equal(resolveResourcePickerValue([], 'single', 1), undefined);
19
+ });
20
+
21
+ test('resource picker result enforces the configured multiple selection maximum', () => {
22
+ assert.deepEqual(resolveResourcePickerValue([1, 2, 3], 'multiple', 2), [1, 2]);
23
+ assert.deepEqual(resolveResourcePickerValue([1, 2], 'multiple', 0), []);
24
+ assert.deepEqual(
25
+ resolveResourcePickerValue(
26
+ Array.from({ length: 101 }, (_value, index) => index + 1),
27
+ 'multiple',
28
+ 100,
29
+ ),
30
+ Array.from({ length: 100 }, (_value, index) => index + 1),
31
+ );
32
+ });
@@ -16,6 +16,7 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
16
16
  sorting={this.$props.sorting}
17
17
  onSortingChange={this.$props.onSortingChange}
18
18
  enableRowSelection={this.$props.enableRowSelection}
19
+ selectionMode={this.$props.selectionMode}
19
20
  rowSelection={this.$props.rowSelection}
20
21
  onRowSelectionChange={this.$props.onRowSelectionChange}
21
22
  tableScope={this.$props.tableScope}
@@ -48,6 +49,7 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
48
49
 
49
50
  public _renderTable($$table: ControllerTable<TData>) {
50
51
  const table = $$table.table;
52
+ const selectionMode = this.$props.selectionMode ?? 'multiple';
51
53
  const selectionEnabled = $$table.columns.some(column => column.id === TableColumnIdSelection);
52
54
  return (
53
55
  <table class="table">
@@ -61,16 +63,18 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
61
63
  const headerDefinition = column.columnDef.header;
62
64
  const selection = selectionEnabled && column.id === TableColumnIdSelection;
63
65
  const headerContent = selection ? (
64
- <input
65
- type="checkbox"
66
- class="checkbox checkbox-sm"
67
- checked={table.getIsAllPageRowsSelected()}
68
- indeterminate={table.getIsSomePageRowsSelected()}
69
- aria-checked={table.getIsSomePageRowsSelected() ? 'mixed' : undefined}
70
- aria-label={this.scope.locale.SelectAllPageRows()}
71
- disabled={table.getRowModel().rows.every(row => !row.getCanSelect())}
72
- onChange={table.getToggleAllPageRowsSelectedHandler()}
73
- />
66
+ selectionMode === 'single' ? null : (
67
+ <input
68
+ type="checkbox"
69
+ class="checkbox checkbox-sm"
70
+ checked={table.getIsAllPageRowsSelected()}
71
+ indeterminate={table.getIsSomePageRowsSelected()}
72
+ aria-checked={table.getIsSomePageRowsSelected() ? 'mixed' : undefined}
73
+ aria-label={this.scope.locale.SelectAllPageRows()}
74
+ disabled={table.getRowModel().rows.every(row => !row.getCanSelect())}
75
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
76
+ />
77
+ )
74
78
  ) : (
75
79
  <FlexRender render={headerDefinition} props={header.getContext()}></FlexRender>
76
80
  );
@@ -123,8 +127,10 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
123
127
  >
124
128
  {selection ? (
125
129
  <input
126
- type="checkbox"
127
- class="checkbox checkbox-sm"
130
+ type={selectionMode === 'single' ? 'radio' : 'checkbox'}
131
+ class={
132
+ selectionMode === 'single' ? 'radio radio-sm' : 'checkbox checkbox-sm'
133
+ }
128
134
  checked={row.getIsSelected()}
129
135
  disabled={!row.getCanSelect()}
130
136
  aria-label={this.scope.locale.SelectRow(row.id)}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.51",
3
+ "version": "5.1.53",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -0,0 +1,31 @@
1
+ import type { TypeControllerInnerProps } from 'zova';
2
+
3
+ import { defineComponent } from 'vue';
4
+ import { prepareComponentOptions, useController } from 'zova';
5
+
6
+ import type { ControllerResourcePickerContextProps } from '../../component/resourcePickerContext/controller.jsx';
7
+
8
+ import { ControllerResourcePickerContext } from '../../component/resourcePickerContext/controller.jsx';
9
+ export type ZResourcePickerContextProps = {
10
+ controllerRef?: (ref: ControllerResourcePickerContext) => void;
11
+ } & ControllerResourcePickerContextProps;
12
+
13
+ type ControllerInnerProps = TypeControllerInnerProps<
14
+ ControllerResourcePickerContextProps,
15
+ keyof typeof ControllerResourcePickerContext.$propsDefault
16
+ >;
17
+ declare module 'zova-module-rest-resource' {
18
+ export interface ControllerResourcePickerContext {
19
+ $props: ControllerInnerProps;
20
+ }
21
+ }
22
+
23
+ export const ZResourcePickerContext = defineComponent((_props: ZResourcePickerContextProps) => {
24
+ useController(ControllerResourcePickerContext, undefined, undefined);
25
+ return () => {};
26
+ }, prepareComponentOptions());
27
+ declare module 'zova-module-a-bean' {
28
+ export interface IVonaComponentRecord {
29
+ 'rest-resource:resourcePickerContext': ControllerResourcePickerContextProps;
30
+ }
31
+ }
@@ -26,7 +26,7 @@ declare module 'zova-module-rest-resource' {
26
26
  }
27
27
  /** model: end */
28
28
  /** model: begin */
29
- import { ModelResource } from '../model/resource.js';
29
+ import type { ModelResource } from '../model/resource.js';
30
30
  import 'zova';
31
31
  declare module 'zova' {
32
32
  export interface IBeanRecordGeneral {
@@ -35,9 +35,11 @@ declare module 'zova' {
35
35
  }
36
36
  /** model: end */
37
37
  /** controller: begin */
38
+ export * from '../component/resourcePickerContext/controller.jsx';
38
39
  export * from '../page/entry/controller.jsx';
39
40
  export * from '../page/entryCreate/controller.jsx';
40
41
  export * from '../page/resource/controller.jsx';
42
+ export * from '../page/resourcePicker/controller.jsx';
41
43
 
42
44
  import 'zova';
43
45
  declare module 'zova' {
@@ -46,6 +48,11 @@ declare module 'zova' {
46
48
  }
47
49
  declare module 'zova-module-rest-resource' {
48
50
 
51
+ export interface ControllerResourcePickerContext {
52
+ /** @internal */
53
+ get scope(): ScopeModuleRestResource;
54
+ }
55
+
49
56
  export interface ControllerPageEntry {
50
57
  /** @internal */
51
58
  get scope(): ScopeModuleRestResource;
@@ -55,18 +62,27 @@ declare module 'zova-module-rest-resource' {
55
62
  /** @internal */
56
63
  get scope(): ScopeModuleRestResource;
57
64
  }
65
+
66
+ export interface ControllerPageResourcePicker {
67
+ /** @internal */
68
+ get scope(): ScopeModuleRestResource;
69
+ }
58
70
  }
59
71
  /** controller: end */
60
72
  /** controller: begin */
61
- import { ControllerPageEntry } from '../page/entry/controller.jsx';
62
- import { ControllerPageEntryCreate } from '../page/entryCreate/controller.jsx';
63
- import { ControllerPageResource } from '../page/resource/controller.jsx';
73
+ import type { ControllerResourcePickerContext } from '../component/resourcePickerContext/controller.jsx';
74
+ import type { ControllerPageEntry } from '../page/entry/controller.jsx';
75
+ import type { ControllerPageEntryCreate } from '../page/entryCreate/controller.jsx';
76
+ import type { ControllerPageResource } from '../page/resource/controller.jsx';
77
+ import type { ControllerPageResourcePicker } from '../page/resourcePicker/controller.jsx';
64
78
  import 'zova';
65
79
  declare module 'zova' {
66
80
  export interface IBeanRecordLocal {
67
- 'rest-resource.controller.pageEntry': ControllerPageEntry;
81
+ 'rest-resource.controller.resourcePickerContext': ControllerResourcePickerContext;
82
+ 'rest-resource.controller.pageEntry': ControllerPageEntry;
68
83
  'rest-resource.controller.pageEntryCreate': ControllerPageEntryCreate;
69
84
  'rest-resource.controller.pageResource': ControllerPageResource;
85
+ 'rest-resource.controller.pageResourcePicker': ControllerPageResourcePicker;
70
86
  }
71
87
  }
72
88
  /** controller: end */
@@ -77,6 +93,8 @@ export * from './page/entryCreate.js';
77
93
  import { NSControllerPageEntryCreate } from './page/entryCreate.js';
78
94
  export * from './page/resource.js';
79
95
  import { NSControllerPageResource } from './page/resource.js';
96
+ export * from './page/resourcePicker.js';
97
+ import { NSControllerPageResourcePicker } from './page/resourcePicker.js';
80
98
  export * from '../routes.js';
81
99
  import { TypePagePathSchema } from 'zova-module-a-router';
82
100
  import 'zova';
@@ -85,11 +103,13 @@ export interface IPagePathRecord {
85
103
  '/rest/resource/:resource/:id/:formScene?': TypePagePathSchema<NSControllerPageEntry.ParamsInput,undefined>;
86
104
  '/rest/resource/:resource/create': TypePagePathSchema<NSControllerPageEntryCreate.ParamsInput,undefined>;
87
105
  '/rest/resource/:resource': TypePagePathSchema<NSControllerPageResource.ParamsInput,undefined>;
106
+ '/rest/resource/:resource/picker': TypePagePathSchema<NSControllerPageResourcePicker.ParamsInput,undefined>;
88
107
  }
89
108
  export interface IPageNameRecord {
90
109
  'rest-resource:entry': TypePagePathSchema<NSControllerPageEntry.ParamsInput,undefined>;
91
110
  'rest-resource:entryCreate': TypePagePathSchema<NSControllerPageEntryCreate.ParamsInput,undefined>;
92
111
  'rest-resource:resource': TypePagePathSchema<NSControllerPageResource.ParamsInput,undefined>;
112
+ 'rest-resource:resourcePicker': TypePagePathSchema<NSControllerPageResourcePicker.ParamsInput,undefined>;
93
113
  }
94
114
  }
95
115
  export const pagePathSchemas = {
@@ -107,6 +127,10 @@ export const pageNameSchemas = {
107
127
  'rest-resource:resource': {
108
128
  params: NSControllerPageResource.paramsSchema,
109
129
 
130
+ },
131
+ 'rest-resource:resourcePicker': {
132
+ params: NSControllerPageResourcePicker.paramsSchema,
133
+
110
134
  },
111
135
  };
112
136
  declare module 'zova-module-rest-resource' {
@@ -119,9 +143,28 @@ export interface ControllerPageEntryCreate {
119
143
  export interface ControllerPageResource {
120
144
  $params: NSControllerPageResource.ParamsOutput;
121
145
  }
146
+ export interface ControllerPageResourcePicker {
147
+ $params: NSControllerPageResourcePicker.ParamsOutput;
148
+ }
122
149
  }
123
150
  /** pages: end */
124
151
 
152
+ /** components: begin */
153
+ export * from './component/resourcePickerContext.js';
154
+ import { ZResourcePickerContext } from './component/resourcePickerContext.js';
155
+ export const components = {
156
+ 'resourcePickerContext': ZResourcePickerContext,
157
+ };
158
+ import 'zova';
159
+ declare module 'zova' {
160
+ export interface IComponentRecord {
161
+ 'rest-resource:resourcePickerContext': ControllerResourcePickerContext;
162
+ }
163
+ export interface IZovaComponentRecord {
164
+ 'rest-resource:resourcePickerContext': typeof ZResourcePickerContext;
165
+ }
166
+ }
167
+ /** components: end */
125
168
  /** scope: begin */
126
169
  import { BeanScopeBase, type BeanScopeUtil } from 'zova';
127
170
  import { Scope } from 'zova-module-a-bean';
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ import { createZovaComponentPage } from 'zova';
3
+
4
+ import { ControllerPageResourcePicker } from '../../page/resourcePicker/controller.jsx';
5
+ import { ControllerPageResourcePickerSchemaParams } from '../../page/resourcePicker/controller.jsx';
6
+ export namespace NSControllerPageResourcePicker {
7
+ export const paramsSchema = ControllerPageResourcePickerSchemaParams;
8
+ export type ParamsInput = z.input<typeof ControllerPageResourcePickerSchemaParams>;
9
+ export type ParamsOutput = z.output<typeof ControllerPageResourcePickerSchemaParams>;
10
+ }
11
+
12
+ export const ZPageResourcePicker = createZovaComponentPage(
13
+ ControllerPageResourcePicker,
14
+ undefined,
15
+ undefined,
16
+ );
@@ -0,0 +1,26 @@
1
+ import { BeanControllerBase } from 'zova';
2
+ import { Controller } from 'zova-module-a-bean';
3
+
4
+ import type { IResourcePickerPageContext } from '../../page/resourcePicker/controller.js';
5
+
6
+ import { resourcePickerPageContextKey } from '../../lib/resourcePicker.js';
7
+
8
+ export interface ControllerResourcePickerContextProps {
9
+ context: IResourcePickerPageContext;
10
+ }
11
+
12
+ @Controller()
13
+ export class ControllerResourcePickerContext extends BeanControllerBase {
14
+ static $propsDefault = {};
15
+
16
+ protected async __init__() {
17
+ this.bean._setBean(
18
+ resourcePickerPageContextKey,
19
+ (this.$props as ControllerResourcePickerContextProps).context,
20
+ );
21
+ }
22
+
23
+ protected render() {
24
+ return this.$slotDefault?.();
25
+ }
26
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './.metadata/index.js';
2
2
  export * from './types/index.js';
3
+ export * from './lib/resourcePicker.js';
4
+ export * from './page/resourcePicker/controller.js';
@@ -0,0 +1 @@
1
+ export const resourcePickerPageContextKey = '$resourcePickerPageContext';
@@ -153,7 +153,7 @@ export class ModelResource<
153
153
  this.sys.util.apiActionPathTranslate(`${this.resourceApi}/:id`, { id }),
154
154
  this.sys.util.apiActionConfigPrepare(),
155
155
  );
156
- return res ?? null;
156
+ return (res as Entity | null) ?? null;
157
157
  },
158
158
  });
159
159
  }