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
@@ -9,6 +9,8 @@ import { VNode } from 'vue';
9
9
  import { BeanControllerBase, Use } from 'zova';
10
10
  import { Controller } from 'zova-module-a-bean';
11
11
 
12
+ import { resolveTableActionBulkDynamicProps } from '../../lib/selection.js';
13
+
12
14
  declare module 'zova-module-a-openapi' {
13
15
  export interface IResourceBlockRecord {
14
16
  'basic-page:blockToolbarBulk'?: ControllerBlockToolbarBulkProps;
@@ -22,23 +24,67 @@ export interface ControllerBlockToolbarBulkProps extends IResourceBlockOptionsBa
22
24
  @Controller()
23
25
  export class ControllerBlockToolbarBulk extends BeanControllerBase {
24
26
  static $propsDefault = {};
27
+
28
+ private _selectionRequired = false;
25
29
  static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
26
30
 
27
31
  @Use({ injectionScope: 'host' })
28
32
  $$renderContext: IJsxRenderContextPage;
29
33
 
30
- protected async __init__() {}
34
+ protected async __init__() {
35
+ this._setSelectionRequired(this._hasSelectionAction());
36
+ this.$watch(
37
+ () => this.$props.actions,
38
+ actions => {
39
+ this._setSelectionRequired(
40
+ actions?.some(action => action.options?.requiresSelection) ?? false,
41
+ );
42
+ },
43
+ );
44
+ }
45
+
46
+ protected __dispose__() {
47
+ this._setSelectionRequired(false);
48
+ }
49
+
50
+ private _hasSelectionAction() {
51
+ return this.$props.actions?.some(action => action.options?.requiresSelection) ?? false;
52
+ }
53
+
54
+ private _setSelectionRequired(required: boolean) {
55
+ if (this._selectionRequired === required) return;
56
+ this._selectionRequired = required;
57
+ this.$$renderContext.$$page.setSelectionRequired(required);
58
+ }
31
59
 
32
60
  get permissions() {
33
61
  return this.$$renderContext.$celScope.permissions;
34
62
  }
35
63
 
36
64
  protected render() {
65
+ const { $$page } = this.$$renderContext;
37
66
  const domActions = this._renderActions();
38
- if (!domActions || domActions.length === 0) return;
67
+ const selectionAvailable = $$page.selectionAvailable;
68
+ if ((!domActions || domActions.length === 0) && !selectionAvailable) return;
39
69
  return (
40
70
  <div class={this.$props.class}>
41
- <div class="join">{domActions}</div>
71
+ <div class="join" role="toolbar" aria-label={this.scope.locale.BulkActions()}>
72
+ {$$page.selectionToggleAvailable && (
73
+ <button type="button" class="btn join-item" onClick={() => $$page.toggleSelection()}>
74
+ {$$page.selectionVisible ? this.scope.locale.Done() : this.scope.locale.Select()}
75
+ </button>
76
+ )}
77
+ {$$page.selectionEnabled && (
78
+ <span
79
+ class="join-item border-base-300 bg-base-200 px-3 py-2"
80
+ role="status"
81
+ aria-live="polite"
82
+ >
83
+ {this.scope.locale.SelectedItems($$page.selection.count)}
84
+ </span>
85
+ )}
86
+ {domActions}
87
+ </div>
42
88
  </div>
43
89
  );
44
90
  }
@@ -52,7 +98,33 @@ export class ControllerBlockToolbarBulk extends BeanControllerBase {
52
98
  const actionName = action.name;
53
99
  const permissionHint = action.options?.permission;
54
100
  if (!this.$passport.checkPermission(this.permissions, actionName, permissionHint)) return;
55
- const options = Object.assign({ key: index }, action.options);
101
+ const selection = this.$$renderContext.$$page.selection;
102
+ const completeSelection = selection.rows.length === selection.count;
103
+ const selectionAllowed =
104
+ selection.count > 0 &&
105
+ completeSelection &&
106
+ this.$passport.checkPermission(
107
+ this.permissions,
108
+ actionName,
109
+ permissionHint,
110
+ selection.rows,
111
+ );
112
+ const dynamicProps = resolveTableActionBulkDynamicProps(
113
+ action.options,
114
+ selection,
115
+ selectionAllowed,
116
+ );
117
+ const options = Object.assign({}, action.options, {
118
+ key: index,
119
+ dynamicSelection: dynamicProps.dynamicSelection,
120
+ dynamicDisabled: dynamicProps.dynamicDisabled,
121
+ dynamicDisabledReason:
122
+ dynamicProps.dynamicDisabledReason === 'maxExceeded'
123
+ ? this.scope.locale.SelectionActionMaxExceeded(dynamicProps.selectedMaxIds!)
124
+ : dynamicProps.dynamicDisabledReason === 'selectionUnavailable'
125
+ ? this.scope.locale.SelectionActionUnavailable()
126
+ : undefined,
127
+ });
56
128
  const domAction = $jsx.render(action.render!, options, $celScope, this.$$renderContext);
57
129
  if (!domAction) return;
58
130
  if (Array.isArray(domAction)) {
@@ -3,4 +3,11 @@ export default {
3
3
  Reset: 'Reset',
4
4
  PagedTotalItems: 'Total',
5
5
  PagedTotalPages: 'Pages',
6
+ Select: 'Select',
7
+ Done: 'Done',
8
+ SelectedItems: 'Selected %d items',
9
+ SelectedItems_1: 'Selected one item',
10
+ BulkActions: 'Bulk actions',
11
+ SelectionActionUnavailable: 'Select one or more permitted rows to enable this action',
12
+ SelectionActionMaxExceeded: 'Select at most %d items to enable this action',
6
13
  };
@@ -3,4 +3,10 @@ export default {
3
3
  Reset: '重置',
4
4
  PagedTotalItems: '总条数',
5
5
  PagedTotalPages: '总页数',
6
+ Select: '选择',
7
+ Done: '完成',
8
+ SelectedItems: '已选择 %d 项',
9
+ BulkActions: '批量操作',
10
+ SelectionActionUnavailable: '请选择一个或多个有权限的行以启用此操作',
11
+ SelectionActionMaxExceeded: '最多选择 %d 项以启用此操作',
6
12
  };
@@ -0,0 +1,84 @@
1
+ import type { RowSelectionState } from '@tanstack/vue-table';
2
+ import type { TableIdentity } from 'table-identity';
3
+ import type {
4
+ IResourceTableActionBulkOptionsBase,
5
+ IResourceTableSelectionPayload,
6
+ } from 'zova-module-a-openapi';
7
+
8
+ import { isNil } from '@cabloy/utils';
9
+
10
+ export const ResourceSelectedMaxIds = 100;
11
+
12
+ export function resolveSelectionMaxIds(selectedMaxIds: number | undefined): number {
13
+ return selectedMaxIds ?? ResourceSelectedMaxIds;
14
+ }
15
+
16
+ export type TypeTableActionBulkDynamicDisabledReason = 'maxExceeded' | 'selectionUnavailable';
17
+
18
+ export interface IResourceTableActionBulkDynamicProps {
19
+ dynamicSelection: IResourceTableSelectionPayload;
20
+ dynamicDisabled: boolean;
21
+ dynamicDisabledReason?: TypeTableActionBulkDynamicDisabledReason;
22
+ selectedMaxIds?: number;
23
+ }
24
+
25
+ export function resolveTableActionBulkDynamicProps(
26
+ options: IResourceTableActionBulkOptionsBase | undefined,
27
+ selection: IResourceTableSelectionPayload,
28
+ selectionAllowed: boolean,
29
+ ): IResourceTableActionBulkDynamicProps {
30
+ const requiresSelection = options?.requiresSelection === true;
31
+ const selectedMaxIds =
32
+ options?.selectedMaxIds !== undefined || requiresSelection
33
+ ? resolveSelectionMaxIds(options?.selectedMaxIds)
34
+ : undefined;
35
+ const selectionMaxExceeded = selectedMaxIds !== undefined && selection.count > selectedMaxIds;
36
+ return {
37
+ dynamicSelection: selection,
38
+ dynamicDisabled: selectionMaxExceeded || (requiresSelection && !selectionAllowed),
39
+ dynamicDisabledReason: selectionMaxExceeded
40
+ ? 'maxExceeded'
41
+ : requiresSelection && !selectionAllowed
42
+ ? 'selectionUnavailable'
43
+ : undefined,
44
+ selectedMaxIds,
45
+ };
46
+ }
47
+
48
+ export function selectionKey(id: TableIdentity): string {
49
+ if (isNil(id) || id === '') throw new Error('row id cannot empty');
50
+ return String(id);
51
+ }
52
+
53
+ export function selectionRowId(row: Record<string, unknown>): TableIdentity {
54
+ const id = row.id as TableIdentity;
55
+ selectionKey(id);
56
+ return id;
57
+ }
58
+
59
+ export function selectionRowIds(rows: readonly Record<string, unknown>[]): Set<string> {
60
+ const ids = new Set<string>();
61
+ for (const row of rows) {
62
+ const id = selectionKey(selectionRowId(row));
63
+ if (ids.has(id)) throw new Error(`duplicate row id: ${id}`);
64
+ ids.add(id);
65
+ }
66
+ return ids;
67
+ }
68
+
69
+ export function reconcileSelection(
70
+ selection: RowSelectionState,
71
+ rows: readonly Record<string, unknown>[],
72
+ next: RowSelectionState,
73
+ ): RowSelectionState {
74
+ const rowIds = selectionRowIds(rows);
75
+ const result = { ...selection };
76
+ for (const id of rowIds) {
77
+ if (next[id]) {
78
+ result[id] = true;
79
+ } else {
80
+ delete result[id];
81
+ }
82
+ }
83
+ return result;
84
+ }
@@ -0,0 +1,99 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ reconcileSelection,
6
+ resolveSelectionMaxIds,
7
+ resolveTableActionBulkDynamicProps,
8
+ selectionKey,
9
+ selectionRowIds,
10
+ } from '../../src/lib/selection.ts';
11
+
12
+ test('selection maximum uses action metadata before the frontend fallback', () => {
13
+ assert.equal(resolveSelectionMaxIds(undefined), 100);
14
+ assert.equal(resolveSelectionMaxIds(100), 100);
15
+ assert.equal(resolveSelectionMaxIds(101), 101);
16
+ assert.equal(resolveSelectionMaxIds(0), 0);
17
+ });
18
+
19
+ test('bulk action runtime props do not overwrite static action metadata', () => {
20
+ const selection = { ids: [1], rows: [{ id: 1 }], count: 1 };
21
+
22
+ assert.deepEqual(
23
+ resolveTableActionBulkDynamicProps(
24
+ { requiresSelection: true, disabled: true },
25
+ selection,
26
+ true,
27
+ ),
28
+ {
29
+ dynamicSelection: selection,
30
+ dynamicDisabled: false,
31
+ dynamicDisabledReason: undefined,
32
+ selectedMaxIds: 100,
33
+ },
34
+ );
35
+ assert.deepEqual(
36
+ resolveTableActionBulkDynamicProps(
37
+ { requiresSelection: true, selectedMaxIds: 1 },
38
+ selection,
39
+ true,
40
+ ),
41
+ {
42
+ dynamicSelection: selection,
43
+ dynamicDisabled: false,
44
+ dynamicDisabledReason: undefined,
45
+ selectedMaxIds: 1,
46
+ },
47
+ );
48
+ assert.deepEqual(
49
+ resolveTableActionBulkDynamicProps(
50
+ { requiresSelection: true, selectedMaxIds: 0 },
51
+ selection,
52
+ true,
53
+ ),
54
+ {
55
+ dynamicSelection: selection,
56
+ dynamicDisabled: true,
57
+ dynamicDisabledReason: 'maxExceeded',
58
+ selectedMaxIds: 0,
59
+ },
60
+ );
61
+ assert.deepEqual(
62
+ resolveTableActionBulkDynamicProps({ requiresSelection: true }, selection, false),
63
+ {
64
+ dynamicSelection: selection,
65
+ dynamicDisabled: true,
66
+ dynamicDisabledReason: 'selectionUnavailable',
67
+ selectedMaxIds: 100,
68
+ },
69
+ );
70
+ });
71
+
72
+ test('selection keys normalize numeric and string identities', () => {
73
+ assert.equal(selectionKey(7), '7');
74
+ assert.equal(selectionKey('student-7'), 'student-7');
75
+ assert.throws(() => selectionKey(''), /row id cannot empty/);
76
+ assert.throws(() => selectionKey(undefined as never), /row id cannot empty/);
77
+ });
78
+
79
+ test('selection rejects duplicate loaded row identities', () => {
80
+ assert.throws(() => selectionRowIds([{ id: '1' }, { id: 1 }]), /duplicate row id: 1/);
81
+ });
82
+
83
+ test('current-page selection changes preserve off-page selections', () => {
84
+ const selection = { previous: true, 1: true };
85
+ const rows = [{ id: '1' }, { id: '2' }];
86
+
87
+ const selected = reconcileSelection(selection, rows, { 1: true, 2: true });
88
+ assert.deepEqual(selected, { previous: true, 1: true, 2: true });
89
+
90
+ const deselected = reconcileSelection(selected, rows, { 2: true });
91
+ assert.deepEqual(deselected, { previous: true, 2: true });
92
+ });
93
+
94
+ test('current-page reconciliation removes only deselected loaded identities', () => {
95
+ const rows = [{ id: '1' }, { id: '2' }];
96
+ const selection = reconcileSelection({ 1: true, 2: true, previous: true }, rows, {});
97
+
98
+ assert.deepEqual(selection, { previous: true });
99
+ });
@@ -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 { ControllerActionDeleteBulkProps } from '../../component/actionDeleteBulk/controller.jsx';
7
+
8
+ import { ControllerActionDeleteBulk } from '../../component/actionDeleteBulk/controller.jsx';
9
+ export type ZActionDeleteBulkProps = {
10
+ controllerRef?: (ref: ControllerActionDeleteBulk) => void;
11
+ } & ControllerActionDeleteBulkProps;
12
+
13
+ type ControllerInnerProps = TypeControllerInnerProps<
14
+ ControllerActionDeleteBulkProps,
15
+ keyof typeof ControllerActionDeleteBulk.$propsDefault
16
+ >;
17
+ declare module 'zova-module-basic-table' {
18
+ export interface ControllerActionDeleteBulk {
19
+ $props: ControllerInnerProps;
20
+ }
21
+ }
22
+
23
+ export const ZActionDeleteBulk = defineComponent((_props: ZActionDeleteBulkProps) => {
24
+ useController(ControllerActionDeleteBulk, undefined, undefined);
25
+ return () => {};
26
+ }, prepareComponentOptions(ControllerActionDeleteBulk.$componentOptions));
27
+ declare module 'zova-module-a-bean' {
28
+ export interface IVonaComponentRecord {
29
+ 'basic-table:actionDeleteBulk': ControllerActionDeleteBulkProps;
30
+ }
31
+ }
@@ -1,6 +1,7 @@
1
1
  // eslint-disable
2
2
  /** controller: begin */
3
3
  export * from '../component/actionCreate/controller.jsx';
4
+ export * from '../component/actionDeleteBulk/controller.jsx';
4
5
  export * from '../component/table/controller.jsx';
5
6
 
6
7
  import 'zova';
@@ -15,6 +16,11 @@ declare module 'zova-module-basic-table' {
15
16
  get scope(): ScopeModuleBasicTable;
16
17
  }
17
18
 
19
+ export interface ControllerActionDeleteBulk {
20
+ /** @internal */
21
+ get scope(): ScopeModuleBasicTable;
22
+ }
23
+
18
24
  export interface ControllerTable {
19
25
  /** @internal */
20
26
  get scope(): ScopeModuleBasicTable;
@@ -23,11 +29,13 @@ declare module 'zova-module-basic-table' {
23
29
  /** controller: end */
24
30
  /** controller: begin */
25
31
  import { ControllerActionCreate } from '../component/actionCreate/controller.jsx';
32
+ import { ControllerActionDeleteBulk } from '../component/actionDeleteBulk/controller.jsx';
26
33
  import { ControllerTable } from '../component/table/controller.jsx';
27
34
  import 'zova';
28
35
  declare module 'zova' {
29
36
  export interface IBeanRecordLocal {
30
37
  'basic-table.controller.actionCreate': ControllerActionCreate;
38
+ 'basic-table.controller.actionDeleteBulk': ControllerActionDeleteBulk;
31
39
  'basic-table.controller.table': ControllerTable;
32
40
  }
33
41
  }
@@ -36,20 +44,25 @@ declare module 'zova' {
36
44
  /** components: begin */
37
45
  export * from './component/actionCreate.js';
38
46
  import { ZActionCreate } from './component/actionCreate.js';
47
+ export * from './component/actionDeleteBulk.js';
48
+ import { ZActionDeleteBulk } from './component/actionDeleteBulk.js';
39
49
  export * from './component/table.js';
40
50
  import { ZTable } from './component/table.js';
41
51
  export const components = {
42
52
  'actionCreate': ZActionCreate,
53
+ 'actionDeleteBulk': ZActionDeleteBulk,
43
54
  'table': ZTable,
44
55
  };
45
56
  import 'zova';
46
57
  declare module 'zova' {
47
58
  export interface IComponentRecord {
48
59
  'basic-table:actionCreate': ControllerActionCreate;
60
+ 'basic-table:actionDeleteBulk': ControllerActionDeleteBulk;
49
61
  'basic-table:table': ControllerTable;
50
62
  }
51
63
  export interface IZovaComponentRecord {
52
64
  'basic-table:actionCreate': typeof ZActionCreate;
65
+ 'basic-table:actionDeleteBulk': typeof ZActionDeleteBulk;
53
66
  'basic-table:table': typeof ZTable;
54
67
  }
55
68
  }
@@ -1,7 +1,7 @@
1
1
  import type { IComponentOptions } from 'zova';
2
2
  import type {
3
3
  IJsxRenderContextPage,
4
- IResourceTableActionBulkOptionsBase,
4
+ IResourceTableActionBulkPropsBase,
5
5
  } from 'zova-module-a-openapi';
6
6
 
7
7
  import { BeanControllerBase, Use } from 'zova';
@@ -13,7 +13,7 @@ declare module 'zova-module-a-openapi' {
13
13
  }
14
14
  }
15
15
 
16
- export interface ControllerActionCreateProps extends IResourceTableActionBulkOptionsBase {}
16
+ export interface ControllerActionCreateProps extends IResourceTableActionBulkPropsBase {}
17
17
 
18
18
  @Controller()
19
19
  export class ControllerActionCreate extends BeanControllerBase {
@@ -26,11 +26,14 @@ export class ControllerActionCreate extends BeanControllerBase {
26
26
  protected async __init__() {}
27
27
 
28
28
  protected render() {
29
+ const disabled = this.$props.disabled === true || this.$props.dynamicDisabled === true;
29
30
  return (
30
31
  <button
31
32
  class={this.$props.class}
32
33
  type="button"
34
+ disabled={disabled}
33
35
  onClick={async () => {
36
+ if (disabled) return;
34
37
  await this.$performCommand('basic-commands:create', this.$props, this.$$renderContext);
35
38
  }}
36
39
  >
@@ -0,0 +1,57 @@
1
+ import type { IComponentOptions } from 'zova';
2
+ import type {
3
+ IJsxRenderContextPage,
4
+ IResourceTableActionBulkPropsBase,
5
+ } from 'zova-module-a-openapi';
6
+
7
+ import { BeanControllerBase, Use } from 'zova';
8
+ import { Controller } from 'zova-module-a-bean';
9
+
10
+ declare module 'zova-module-a-openapi' {
11
+ export interface IResourceTableActionBulkRecord {
12
+ 'basic-table:actionDeleteBulk'?: ControllerActionDeleteBulkProps;
13
+ }
14
+ }
15
+
16
+ export interface ControllerActionDeleteBulkProps extends IResourceTableActionBulkPropsBase {}
17
+
18
+ @Controller()
19
+ export class ControllerActionDeleteBulk extends BeanControllerBase {
20
+ static $propsDefault = { class: 'btn btn-error join-item' };
21
+ static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
22
+
23
+ @Use({ injectionScope: 'host' })
24
+ $$renderContext: IJsxRenderContextPage;
25
+
26
+ protected render() {
27
+ const { dynamicDisabledReason, dynamicSelection } = this.$props;
28
+ const disabled = this.$props.disabled === true || this.$props.dynamicDisabled === true;
29
+ return (
30
+ <button
31
+ class={this.$props.class}
32
+ type="button"
33
+ disabled={disabled}
34
+ title={dynamicDisabledReason}
35
+ aria-description={dynamicDisabledReason}
36
+ onClick={async () => {
37
+ if (disabled) return;
38
+ const confirmed = await this.$performCommand(
39
+ 'basic-commands:confirm',
40
+ {
41
+ text: this.scope.locale.DeleteBulkConfirm(dynamicSelection?.count ?? 0),
42
+ },
43
+ this.$$renderContext,
44
+ );
45
+ if (!confirmed) return;
46
+ await this.$performCommand(
47
+ 'basic-commands:deleteBulk',
48
+ this.$props,
49
+ this.$$renderContext,
50
+ );
51
+ }}
52
+ >
53
+ {this.scope.locale.DeleteBulk() as string}
54
+ </button>
55
+ );
56
+ }
57
+ }
@@ -3,7 +3,7 @@ import type { AriaAttributes, CSSProperties } from 'vue';
3
3
  import { FlexRender } from '@tanstack/vue-table';
4
4
  import { BeanRenderBase } from 'zova';
5
5
  import { Render } from 'zova-module-a-bean';
6
- import { ControllerTable, ZTable } from 'zova-module-a-table';
6
+ import { ControllerTable, TableColumnIdSelection, ZTable } from 'zova-module-a-table';
7
7
 
8
8
  @Render()
9
9
  export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
@@ -15,6 +15,9 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
15
15
  schemaOrder={this.$props.schemaOrder}
16
16
  sorting={this.$props.sorting}
17
17
  onSortingChange={this.$props.onSortingChange}
18
+ enableRowSelection={this.$props.enableRowSelection}
19
+ rowSelection={this.$props.rowSelection}
20
+ onRowSelectionChange={this.$props.onRowSelectionChange}
18
21
  tableScope={this.$props.tableScope}
19
22
  getColumns={this.$props.getColumns}
20
23
  getRowId={this.$props.getRowId}
@@ -45,6 +48,7 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
45
48
 
46
49
  public _renderTable($$table: ControllerTable<TData>) {
47
50
  const table = $$table.table;
51
+ const selectionEnabled = $$table.columns.some(column => column.id === TableColumnIdSelection);
48
52
  return (
49
53
  <table class="table">
50
54
  <thead>
@@ -55,7 +59,19 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
55
59
  const sorted = column.getIsSorted();
56
60
  const fixed = column.getIsPinned();
57
61
  const headerDefinition = column.columnDef.header;
58
- const headerContent = (
62
+ const selection = selectionEnabled && column.id === TableColumnIdSelection;
63
+ 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
+ />
74
+ ) : (
59
75
  <FlexRender render={headerDefinition} props={header.getContext()}></FlexRender>
60
76
  );
61
77
  let sortAria: AriaAttributes['aria-sort'] = 'none';
@@ -95,19 +111,31 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
95
111
  <tbody>
96
112
  {table.getRowModel().rows.map(row => {
97
113
  return (
98
- <tr key={row.id}>
114
+ <tr key={row.id} aria-selected={row.getIsSelected()}>
99
115
  {row.getVisibleCells().map(cell => {
100
116
  const column = cell.column;
117
+ const selection = selectionEnabled && column.id === TableColumnIdSelection;
101
118
  return (
102
119
  <td
103
120
  key={cell.id}
104
121
  class={column.getIsPinned() ? 'bg-base-100' : undefined}
105
122
  style={this._getColumnStyle(column)}
106
123
  >
107
- <FlexRender
108
- render={column.columnDef.cell}
109
- props={cell.getContext()}
110
- ></FlexRender>
124
+ {selection ? (
125
+ <input
126
+ type="checkbox"
127
+ class="checkbox checkbox-sm"
128
+ checked={row.getIsSelected()}
129
+ disabled={!row.getCanSelect()}
130
+ aria-label={this.scope.locale.SelectRow(row.id)}
131
+ onChange={row.getToggleSelectedHandler()}
132
+ />
133
+ ) : (
134
+ <FlexRender
135
+ render={column.columnDef.cell}
136
+ props={cell.getContext()}
137
+ ></FlexRender>
138
+ )}
111
139
  </td>
112
140
  );
113
141
  })}
@@ -1,4 +1,8 @@
1
1
  export default {
2
2
  Create: 'Create',
3
3
  DeleteConfirm: 'Are you sure you want to delete this item?',
4
+ SelectAllPageRows: 'Select all rows on this page',
5
+ SelectRow: 'Select row %s',
6
+ DeleteBulk: 'Bulk Delete',
7
+ DeleteBulkConfirm: 'Are you sure you want to bulk delete %d selected items?',
4
8
  };
@@ -1,4 +1,8 @@
1
1
  export default {
2
2
  Create: '创建',
3
3
  DeleteConfirm: '您确认要删除本数据吗?',
4
+ SelectAllPageRows: '选择本页所有行',
5
+ SelectRow: '选择行 %s',
6
+ DeleteBulk: '批量删除',
7
+ DeleteBulkConfirm: '您确认要批量删除已选择的 %d 项吗?',
4
8
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.50",
3
+ "version": "5.1.51",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -0,0 +1,15 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import { isNil } from '@cabloy/utils';
4
+
5
+ export function validateSelectedIds(ids: readonly TableIdentity[]): TableIdentity[] {
6
+ if (ids.length === 0) throw new Error('selected ids cannot empty');
7
+ const keys = new Set<string>();
8
+ for (const id of ids) {
9
+ if (isNil(id) || id === '') throw new Error('selected row id cannot empty');
10
+ const key = String(id);
11
+ if (keys.has(key)) throw new Error(`duplicate selected row id: ${key}`);
12
+ keys.add(key);
13
+ }
14
+ return [...ids];
15
+ }