cabloy 5.1.162 → 5.1.164

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 (96) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +2 -0
  4. package/package.json +1 -1
  5. package/repo-docs/backend/shared-rbac-architecture.md +4 -0
  6. package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
  7. package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
  8. package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
  9. package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
  10. package/repo-docs/blogs/index.md +15 -0
  11. package/repo-docs/editions/overview.md +2 -0
  12. package/repo-docs/frontend/environment-config-guide.md +43 -10
  13. package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
  14. package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
  15. package/repo-docs/frontend/table-guide.md +42 -0
  16. package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
  17. package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
  18. package/repo-e2e/specs/cabloy-basic.spec.ts +249 -2
  19. package/vona/packages-cli/cli/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
  21. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  22. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  23. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  24. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
  25. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
  27. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  28. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
  31. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
  32. package/vona/packages-cli/cli-set-api/package.json +1 -1
  33. package/vona/packages-vona/vona/package.json +1 -1
  34. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
  35. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  37. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
  38. package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
  39. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
  40. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
  41. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
  42. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  43. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
  44. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
  45. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
  46. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
  47. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
  48. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
  49. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
  50. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
  51. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
  52. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  53. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  54. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  55. package/zova/packages-cli/cli/package.json +2 -2
  56. package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
  57. package/zova/packages-cli/cli-set-front/package.json +1 -1
  58. package/zova/packages-zova/zova/package.json +2 -2
  59. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
  60. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
  61. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
  62. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
  63. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
  65. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
  66. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
  67. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
  68. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
  69. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
  70. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
  71. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
  72. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
  73. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
  74. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
  75. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
  76. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
  77. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
  78. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
  79. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  80. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
  81. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
  82. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
  83. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  84. package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
  85. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
  87. package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +3 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  93. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
  94. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
  95. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
  96. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -18,6 +18,7 @@ import { ScopeModuleAOpenapi, SymbolOpenapiSchemaName } from 'zova-module-a-open
18
18
 
19
19
  import { prepareDeleteRequestConfig } from '../lib/deleteRequest.js';
20
20
  import { runMutationSuccess } from '../lib/mutationSuccess.js';
21
+ import { validateSelectedIds } from '../lib/selectedIds.js';
21
22
 
22
23
  export interface IModelOptionsResource extends IDecoratorModelOptions {}
23
24
 
@@ -40,6 +41,14 @@ interface IModelResourceMutationItemOptions<TData = void, TVariables = void> {
40
41
  invalidateSelect?: boolean;
41
42
  }
42
43
 
44
+ interface IModelResourceMutationSelectedOptions<TData = void, TVariables = void> {
45
+ action: string;
46
+ getIds: (variables: TVariables) => readonly TableIdentity[];
47
+ mutationFn: (params: TVariables) => Promise<TData>;
48
+ onSuccess?: (data: TData, variables: TVariables, context: unknown) => void | Promise<void>;
49
+ invalidateSelect?: boolean;
50
+ }
51
+
43
52
  @Model<IModelOptionsResource>({
44
53
  enableSelector: true,
45
54
  })
@@ -187,6 +196,47 @@ export class ModelResource<
187
196
  });
188
197
  }
189
198
 
199
+ mutationSelected<TData = void, TVariables = void>(
200
+ options: IModelResourceMutationSelectedOptions<TData, TVariables>,
201
+ ) {
202
+ const { action, getIds, mutationFn, onSuccess, invalidateSelect = true } = options;
203
+ return this.$useMutationData<TData, TVariables>({
204
+ mutationKey: ['selected', action, 'mutation'],
205
+ mutationFn: async variables => {
206
+ this._getMutationSelectedIds(getIds, variables);
207
+ return await mutationFn(variables);
208
+ },
209
+ onSuccess: async (data, variables, context) => {
210
+ const ids = this._getMutationSelectedIds(getIds, variables);
211
+ await runMutationSuccess({
212
+ invalidateSelect: invalidateSelect
213
+ ? () => this.$invalidateQueries({ queryKey: ['select'] })
214
+ : undefined,
215
+ invalidateItem: async () => {
216
+ await Promise.all(
217
+ ids.map(id => this.$invalidateQueries({ queryKey: this.keyItemRoot(id) })),
218
+ );
219
+ },
220
+ onSuccess: () => onSuccess?.(data, variables, context),
221
+ });
222
+ },
223
+ });
224
+ }
225
+
226
+ deleteBulk() {
227
+ return this.mutationSelected<void, { ids: readonly TableIdentity[] }>({
228
+ action: 'delete',
229
+ getIds: body => body.ids,
230
+ mutationFn: async body => {
231
+ return this.$fetch.post<any, void, { ids: readonly TableIdentity[] }>(
232
+ this.sys.util.apiActionPathTranslate(`${this.resourceApi}/bulk/delete`),
233
+ body,
234
+ this.sys.util.apiActionConfigPrepare(),
235
+ );
236
+ },
237
+ });
238
+ }
239
+
190
240
  update(id: TableIdentity) {
191
241
  return this.mutationItem<void, EntityUpdate>({
192
242
  id,
@@ -276,6 +326,13 @@ export class ModelResource<
276
326
  return this.$sdk.getSchemaDefaultValue(schemaName) as EntityCreate | undefined;
277
327
  }
278
328
 
329
+ private _getMutationSelectedIds<TVariables>(
330
+ getIds: (variables: TVariables) => readonly TableIdentity[],
331
+ variables: TVariables,
332
+ ) {
333
+ return validateSelectedIds(getIds(variables));
334
+ }
335
+
279
336
  protected keySelect(actionPath?: string, query?: ITableQuery) {
280
337
  return ['select', actionPath ?? '', hashkey(query)] as const;
281
338
  }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { validateSelectedIds } from '../../src/lib/selectedIds.ts';
5
+
6
+ test('selected mutation IDs require a nonempty unique normalized collection', () => {
7
+ assert.deepEqual(validateSelectedIds([7, 'student-8']), [7, 'student-8']);
8
+ assert.throws(() => validateSelectedIds([]), /selected ids cannot empty/);
9
+ assert.throws(() => validateSelectedIds([undefined as never]), /selected row id cannot empty/);
10
+ assert.throws(() => validateSelectedIds(['']), /selected row id cannot empty/);
11
+ assert.throws(() => validateSelectedIds([1, '1']), /duplicate selected row id: 1/);
12
+ });
13
+
14
+ test('selected mutation IDs do not impose a frontend count limit', () => {
15
+ const ids = Array.from({ length: 101 }, (_, index) => index + 1);
16
+ const validatedIds = validateSelectedIds(ids);
17
+
18
+ assert.deepEqual(validatedIds, ids);
19
+ assert.notEqual(validatedIds, ids);
20
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.51",
3
+ "version": "5.1.52",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "zova-module-a-rbac": "^5.0.1",
11
- "zova-module-rest-resource": "^5.1.50"
11
+ "zova-module-rest-resource": "^5.1.51"
12
12
  },
13
13
  "title": "a-cabloy"
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-command",
3
- "version": "5.1.34",
3
+ "version": "5.1.35",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -0,0 +1,33 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IJsxRenderContextBase, IJsxRenderContextPage } from 'zova-module-a-openapi';
3
+
4
+ import { isNil } from '@cabloy/utils';
5
+ import { BeanBase } from 'zova';
6
+
7
+ import type { ICommandSelectedOptionsBase } from '../types/command.js';
8
+
9
+ export class BeanCommandSelectedBase extends BeanBase {
10
+ getResourceAndIds(
11
+ options: ICommandSelectedOptionsBase,
12
+ renderContext: IJsxRenderContextBase,
13
+ ): { resource: string; ids: readonly TableIdentity[] } {
14
+ let resource: string | undefined = options.resource;
15
+ let ids = options.ids;
16
+ if (renderContext.$scene === 'page') {
17
+ const { $celScope } = renderContext as IJsxRenderContextPage;
18
+ resource = resource ?? $celScope.resource;
19
+ ids = ids ?? $celScope.selection?.ids;
20
+ }
21
+ if (isNil(resource) || !ids || ids.length === 0) {
22
+ throw new Error(`should specify resource and selected ids in scene: ${renderContext.$scene}`);
23
+ }
24
+ const keys = new Set<string>();
25
+ for (const id of ids) {
26
+ if (isNil(id) || id === '') throw new Error('selected row id cannot empty');
27
+ const key = String(id);
28
+ if (keys.has(key)) throw new Error(`duplicate selected row id: ${key}`);
29
+ keys.add(key);
30
+ }
31
+ return { resource, ids };
32
+ }
33
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './beanCommandBulkBase.js';
2
2
  export * from './beanCommandRowBase.js';
3
+ export * from './beanCommandSelectedBase.js';
3
4
  export * from './command.js';
4
5
  export * from './performCommand.js';
@@ -40,6 +40,11 @@ export interface ICommandRowOptionsBase<Result = any> extends ICommandOptionsBas
40
40
  id?: TableIdentity;
41
41
  }
42
42
 
43
+ export interface ICommandSelectedOptionsBase<Result = any> extends ICommandOptionsBase<Result> {
44
+ resource?: string;
45
+ ids?: readonly TableIdentity[];
46
+ }
47
+
43
48
  export interface ICommandDetailsRowOptionsBase<Result = any> extends ICommandOptionsBase<Result> {}
44
49
 
45
50
  declare module 'zova-module-a-bean' {
@@ -0,0 +1,47 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { BeanCommandSelectedBase } from '../../src/lib/beanCommandSelectedBase.ts';
5
+
6
+ const command = Object.create(BeanCommandSelectedBase.prototype) as BeanCommandSelectedBase;
7
+ const renderContext = { $scene: 'app' } as any;
8
+
9
+ test('selected command IDs do not impose a frontend count limit', () => {
10
+ const ids = Array.from({ length: 101 }, (_, index) => index + 1);
11
+
12
+ assert.deepEqual(
13
+ command.getResourceAndIds({ resource: 'test:resource', ids } as any, renderContext),
14
+ {
15
+ resource: 'test:resource',
16
+ ids,
17
+ },
18
+ );
19
+ });
20
+
21
+ test('selected command IDs require a nonempty unique normalized collection', () => {
22
+ assert.throws(
23
+ () => command.getResourceAndIds({ resource: 'test:resource', ids: [] } as any, renderContext),
24
+ /should specify resource and selected ids/,
25
+ );
26
+ assert.throws(
27
+ () => command.getResourceAndIds({ ids: [1] } as any, renderContext),
28
+ /should specify resource and selected ids/,
29
+ );
30
+ assert.throws(
31
+ () =>
32
+ command.getResourceAndIds(
33
+ { resource: 'test:resource', ids: [undefined as never] } as any,
34
+ renderContext,
35
+ ),
36
+ /selected row id cannot empty/,
37
+ );
38
+ assert.throws(
39
+ () => command.getResourceAndIds({ resource: 'test:resource', ids: [''] } as any, renderContext),
40
+ /selected row id cannot empty/,
41
+ );
42
+ assert.throws(
43
+ () =>
44
+ command.getResourceAndIds({ resource: 'test:resource', ids: [1, '1'] } as any, renderContext),
45
+ /duplicate selected row id: 1/,
46
+ );
47
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-openapi",
3
- "version": "5.1.52",
3
+ "version": "5.1.53",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -3,6 +3,8 @@ import type { ZovaJsx } from 'zova-jsx';
3
3
 
4
4
  import { TableIdentity } from 'table-identity';
5
5
 
6
+ import type { IResourceTableSelectionPayload } from './resource/tableActionBulk.js';
7
+
6
8
  import { TypeOpenapiPermissions } from './permissions.js';
7
9
 
8
10
  export interface IJsxRenderContextBase {
@@ -41,6 +43,7 @@ export interface IJsxRenderSceneRecord {
41
43
  export interface IPageScope {
42
44
  resource?: string;
43
45
  permissions?: TypeOpenapiPermissions;
46
+ selection?: IResourceTableSelectionPayload;
44
47
  }
45
48
 
46
49
  export interface IJsxRenderContextPage extends IJsxRenderContextBase {
@@ -1,3 +1,5 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
1
3
  import { types } from 'typestyle';
2
4
  import { TypeRenderComponentJsx } from 'zova-jsx';
3
5
 
@@ -11,11 +13,28 @@ export type IResourceTableActionBulkNameRecord = {
11
13
 
12
14
  export interface IResourceTableActionBulkRecord {}
13
15
 
16
+ export interface IResourceTableSelectionPayload<TData extends {} = Record<string, unknown>> {
17
+ ids: readonly TableIdentity[];
18
+ rows: readonly TData[];
19
+ count: number;
20
+ }
21
+
22
+ /** Declarative bulk-action metadata authored by a resource schema. */
14
23
  export interface IResourceTableActionBulkOptionsBase {
15
24
  class?: any;
16
25
  style?: types.NestedCSSProperties;
17
26
  resource?: string;
18
27
  permission?: IPermissionHintTableActionBulk;
28
+ requiresSelection?: boolean;
29
+ selectedMaxIds?: number;
30
+ disabled?: boolean;
31
+ }
32
+
33
+ /** Runtime bulk-action props supplied by the owning toolbar for the current render. */
34
+ export interface IResourceTableActionBulkPropsBase extends IResourceTableActionBulkOptionsBase {
35
+ dynamicSelection?: IResourceTableSelectionPayload;
36
+ dynamicDisabled?: boolean;
37
+ dynamicDisabledReason?: string;
19
38
  }
20
39
 
21
40
  export interface IResourceRenderTableActionBulkOptionsAction {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-table",
3
- "version": "5.1.39",
3
+ "version": "5.1.40",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -14,6 +14,7 @@ import {
14
14
  getCoreRowModel,
15
15
  OnChangeFn,
16
16
  Row,
17
+ RowSelectionState,
17
18
  SortingState,
18
19
  TableOptionsWithReactiveData,
19
20
  } from '@tanstack/vue-table';
@@ -46,6 +47,7 @@ import type {
46
47
  } from '../../types/tableColumn.js';
47
48
 
48
49
  import { BeanControllerTableBase } from '../../lib/beanControllerTableBase.js';
50
+ import { TableColumnIdSelection } from '../../types/table.js';
49
51
  import { ITableCellRenderColumnOptions } from '../../types/tableColumn.js';
50
52
 
51
53
  export interface ControllerTableProps<TData extends {} = {}> {
@@ -54,6 +56,9 @@ export interface ControllerTableProps<TData extends {} = {}> {
54
56
  schemaOrder?: SchemaObject;
55
57
  sorting?: SortingState;
56
58
  onSortingChange?: OnChangeFn<SortingState>;
59
+ enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
60
+ rowSelection?: RowSelectionState;
61
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>;
57
62
  tableScope?: ITableScope;
58
63
  getColumns?: TypeTableGetColumns<TData>;
59
64
  slotDefault?: (table: ControllerTable<TData>) => VNode;
@@ -89,6 +94,13 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
89
94
  await this.refreshMeta();
90
95
  },
91
96
  );
97
+ this.$watch(
98
+ () => this.$props.enableRowSelection,
99
+ async (newValue, oldValue) => {
100
+ if (newValue === oldValue) return;
101
+ await this.refreshMeta();
102
+ },
103
+ );
92
104
  // table
93
105
  this._createTable();
94
106
  }
@@ -105,6 +117,10 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
105
117
  return this.$props.sorting ?? [];
106
118
  }
107
119
 
120
+ public get rowSelection() {
121
+ return this.$props.rowSelection ?? {};
122
+ }
123
+
108
124
  public async refreshMeta() {
109
125
  this.tableMeta = await this._createTableMeta();
110
126
  this.columns = await this._createColumns();
@@ -115,22 +131,30 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
115
131
  // eslint-disable-next-line
116
132
  const self = this;
117
133
  const tableOptions: TableOptionsWithReactiveData<TData> = {
118
- getRowId: this.$props.getRowId ?? ((originalRow: TData) => cast(originalRow).id),
134
+ getRowId: (originalRow, index, parent) => this._getRowId(originalRow, index, parent),
119
135
  getCoreRowModel: getCoreRowModel(),
120
136
  renderFallbackValue: this.scope.config.renderFallbackValue,
121
137
  manualPagination: true,
122
138
  manualSorting: true,
123
139
  enableMultiSort: false,
124
140
  enableSortingRemoval: false,
141
+ enableRowSelection: row => self._canSelectRow(row),
125
142
  state: {
126
143
  get sorting() {
127
144
  return self.sorting;
128
145
  },
146
+ get rowSelection() {
147
+ return self.rowSelection;
148
+ },
129
149
  },
130
150
  onSortingChange: updater => {
131
151
  const sorting = functionalUpdate(updater, self.sorting).slice(0, 1);
132
152
  self.$props.onSortingChange?.(sorting);
133
153
  },
154
+ onRowSelectionChange: updater => {
155
+ const rowSelection = functionalUpdate(updater, self.rowSelection);
156
+ self.$props.onRowSelectionChange?.(rowSelection);
157
+ },
134
158
  get initialState() {
135
159
  const columnPinning = self._getColumnPinning();
136
160
  return { columnPinning };
@@ -148,31 +172,50 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
148
172
 
149
173
  private async _createColumns() {
150
174
  if (!this.properties) return [];
151
- if (!this.$props.getColumns) return await this._createColumnsMiddle(this.tableMeta.properties);
152
- return await this.$props.getColumns(
153
- async properties => {
154
- return await this._createColumnsMiddle(properties ?? this.tableMeta.properties);
155
- },
156
- async (
157
- key: string,
158
- render: TypeTableCellRenderComponent,
159
- ): Promise<TypeTableCellRender<TData, any> | undefined> => {
160
- // columnScope
161
- const columnScope = this.getColumnScope(key);
162
- // renderContext
163
- const jsxRenderContext = this.getColumnJsxRenderContext(columnScope);
164
- // columnProps
165
- const { visible, columnProps } = this.getColumnComponentPropsTop(
166
- key,
167
- columnScope,
168
- jsxRenderContext,
175
+ const columns = !this.$props.getColumns
176
+ ? await this._createColumnsMiddle(this.tableMeta.properties)
177
+ : await this.$props.getColumns(
178
+ async properties => {
179
+ return await this._createColumnsMiddle(properties ?? this.tableMeta.properties);
180
+ },
181
+ async (
182
+ key: string,
183
+ render: TypeTableCellRenderComponent,
184
+ ): Promise<TypeTableCellRender<TData, any> | undefined> => {
185
+ // columnScope
186
+ const columnScope = this.getColumnScope(key);
187
+ // renderContext
188
+ const jsxRenderContext = this.getColumnJsxRenderContext(columnScope);
189
+ // columnProps
190
+ const { visible, columnProps } = this.getColumnComponentPropsTop(
191
+ key,
192
+ columnScope,
193
+ jsxRenderContext,
194
+ );
195
+ // visible
196
+ if (visible === false) return;
197
+ return await this._createColumnRender(
198
+ render,
199
+ columnProps,
200
+ columnScope,
201
+ jsxRenderContext,
202
+ );
203
+ },
204
+ this,
169
205
  );
170
- // visible
171
- if (visible === false) return;
172
- return await this._createColumnRender(render, columnProps, columnScope, jsxRenderContext);
173
- },
174
- this,
175
- );
206
+ if (!this.$props.enableRowSelection) return columns;
207
+ const columnHelper = createColumnHelper<TData>();
208
+ return [
209
+ columnHelper.display({
210
+ id: TableColumnIdSelection,
211
+ header: '',
212
+ cell: '',
213
+ size: 48,
214
+ enableSorting: false,
215
+ meta: { rest: { fixed: 'left', width: 48, align: 'center' } },
216
+ }),
217
+ ...columns,
218
+ ];
176
219
  }
177
220
 
178
221
  private async _createColumnsMiddle(
@@ -200,8 +243,25 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
200
243
  return columns;
201
244
  }
202
245
 
246
+ private _getRowId(originalRow: TData, index: number, parent?: Row<TData>) {
247
+ const id = this.$props.getRowId?.(originalRow, index, parent) ?? cast(originalRow).id;
248
+ if (isNilOrEmptyString(id)) {
249
+ return `__unselectable_${parent ? `${parent.id}.` : ''}${index}`;
250
+ }
251
+ return String(id);
252
+ }
253
+
254
+ private _canSelectRow(row: Row<TData>) {
255
+ const enableRowSelection = this.$props.enableRowSelection;
256
+ if (!enableRowSelection) return false;
257
+ const originalId =
258
+ this.$props.getRowId?.(row.original, row.index, row.getParentRow()) ?? cast(row.original).id;
259
+ if (isNilOrEmptyString(originalId)) return false;
260
+ return typeof enableRowSelection === 'function' ? enableRowSelection(row) : true;
261
+ }
262
+
203
263
  private _getColumnPinning() {
204
- const left: string[] = [];
264
+ const left: string[] = this.$props.enableRowSelection ? [TableColumnIdSelection] : [];
205
265
  const right: string[] = [];
206
266
  for (const property of this.tableMeta?.properties ?? []) {
207
267
  const fixed = property.rest?.fixed;
@@ -4,6 +4,8 @@ import { FlexRender } from '@tanstack/vue-table';
4
4
  import { BeanRenderBase } from 'zova';
5
5
  import { Render } from 'zova-module-a-bean';
6
6
 
7
+ import { TableColumnIdSelection } from '../../types/table.js';
8
+
7
9
  @Render()
8
10
  export class RenderTable extends BeanRenderBase {
9
11
  private _getColumnStyle(column: any): CSSProperties {
@@ -33,7 +35,18 @@ export class RenderTable extends BeanRenderBase {
33
35
  const sorted = column.getIsSorted();
34
36
  const fixed = column.getIsPinned();
35
37
  const headerDefinition = column.columnDef.header;
36
- const headerContent = (
38
+ const selection = column.id === TableColumnIdSelection;
39
+ const headerContent = selection ? (
40
+ <input
41
+ type="checkbox"
42
+ checked={table.getIsAllPageRowsSelected()}
43
+ indeterminate={table.getIsSomePageRowsSelected()}
44
+ aria-checked={table.getIsSomePageRowsSelected() ? 'mixed' : undefined}
45
+ aria-label="Select all rows on this page"
46
+ disabled={table.getRowModel().rows.every(row => !row.getCanSelect())}
47
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
48
+ />
49
+ ) : (
37
50
  <FlexRender render={headerDefinition} props={header.getContext()}></FlexRender>
38
51
  );
39
52
  let sortAria: AriaAttributes['aria-sort'] = 'none';
@@ -73,19 +86,30 @@ export class RenderTable extends BeanRenderBase {
73
86
  <tbody>
74
87
  {table.getRowModel().rows.map(row => {
75
88
  return (
76
- <tr key={row.id}>
89
+ <tr key={row.id} aria-selected={row.getIsSelected()}>
77
90
  {row.getVisibleCells().map(cell => {
78
91
  const column = cell.column;
92
+ const selection = column.id === TableColumnIdSelection;
79
93
  return (
80
94
  <td
81
95
  key={cell.id}
82
96
  class={column.getIsPinned() ? 'bg-base-100' : undefined}
83
97
  style={this._getColumnStyle(column)}
84
98
  >
85
- <FlexRender
86
- render={column.columnDef.cell}
87
- props={cell.getContext()}
88
- ></FlexRender>
99
+ {selection ? (
100
+ <input
101
+ type="checkbox"
102
+ checked={row.getIsSelected()}
103
+ disabled={!row.getCanSelect()}
104
+ aria-label={`Select row ${row.id}`}
105
+ onChange={row.getToggleSelectedHandler()}
106
+ />
107
+ ) : (
108
+ <FlexRender
109
+ render={column.columnDef.cell}
110
+ props={cell.getContext()}
111
+ ></FlexRender>
112
+ )}
89
113
  </td>
90
114
  );
91
115
  })}
@@ -11,6 +11,8 @@ export type TypeTable<TData extends {} = {}> = ReturnType<typeof useVueTable<TDa
11
11
 
12
12
  export type TypeColumn<TData extends {} = {}> = ColumnDef<TData, any>;
13
13
 
14
+ export const TableColumnIdSelection = '$$selection';
15
+
14
16
  export interface ITableMeta<TData extends {} = {}> {
15
17
  properties: ISchemaObjectExtensionField[];
16
18
  renders: Record<string, TypeTableCellRender<TData>>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-zova",
3
- "version": "5.1.155",
3
+ "version": "5.1.156",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "zova",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "zova-module-a-behavior": "^5.1.26",
15
15
  "zova-module-a-behaviors": "^5.1.22",
16
16
  "zova-module-a-boundary": "^5.1.22",
17
- "zova-module-a-command": "^5.1.34",
17
+ "zova-module-a-command": "^5.1.35",
18
18
  "zova-module-a-fetch": "^5.1.25",
19
19
  "zova-module-a-form": "^5.1.51",
20
20
  "zova-module-a-icon": "^5.1.27",
@@ -22,7 +22,7 @@
22
22
  "zova-module-a-logger": "^5.1.28",
23
23
  "zova-module-a-meta": "^5.1.22",
24
24
  "zova-module-a-model": "^5.1.36",
25
- "zova-module-a-openapi": "^5.1.52",
25
+ "zova-module-a-openapi": "^5.1.53",
26
26
  "zova-module-a-router": "^5.1.34",
27
27
  "zova-module-a-routerstack": "^5.1.27",
28
28
  "zova-module-a-routertabs": "^5.1.34",
@@ -30,7 +30,7 @@
30
30
  "zova-module-a-ssrhmr": "^5.1.23",
31
31
  "zova-module-a-ssrserver": "^5.1.26",
32
32
  "zova-module-a-style": "^5.1.35",
33
- "zova-module-a-table": "^5.1.39",
33
+ "zova-module-a-table": "^5.1.40",
34
34
  "zova-module-a-zod": "^5.1.37",
35
35
  "zova-module-a-zova": "^5.1.103"
36
36
  },