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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/repo-docs/backend/shared-rbac-architecture.md +4 -0
- package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
- package/repo-docs/blogs/index.md +15 -0
- package/repo-docs/editions/overview.md +2 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -10
- package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
- package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
- package/repo-docs/frontend/table-guide.md +42 -0
- package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
- package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
- package/repo-e2e/specs/cabloy-basic.spec.ts +249 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
- package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
- package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +3 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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
|
|
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
|
+
});
|
package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts
ADDED
|
@@ -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
|
}
|
package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IComponentOptions } from 'zova';
|
|
2
2
|
import type {
|
|
3
3
|
IJsxRenderContextPage,
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
};
|
|
@@ -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
|
+
}
|