cabloy 5.1.168 → 5.1.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/package.json +1 -1
  4. package/repo-e2e/specs/cabloy-basic.spec.ts +45 -0
  5. package/vona/packages-cli/cli/package.json +1 -1
  6. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +4 -0
  7. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +4 -0
  8. package/vona/packages-cli/cli-set-api/package.json +1 -1
  9. package/vona/pnpm-lock.yaml +83 -106
  10. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +4 -0
  11. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +5 -0
  12. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +54 -0
  13. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/index.ts +346 -0
  14. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/this.ts +2 -0
  15. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.index.ts +12 -0
  16. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.redlock.ts +11 -0
  17. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/bean/meta.version.ts +20 -0
  18. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/controller/layoutprofile.ts +38 -0
  19. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfile.tsx +26 -0
  20. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfileLoad.tsx +13 -0
  21. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/dto/layoutProfileSave.tsx +31 -0
  22. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/entity/layoutprofile.ts +20 -0
  23. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/index.ts +2 -0
  24. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +10 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/service/layoutprofile.ts +64 -0
  26. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/types/index.ts +1 -0
  27. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/types/layoutprofile.ts +21 -0
  28. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/test/layoutprofile.test.ts +258 -0
  29. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.build.json +11 -0
  30. package/vona/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.json +7 -0
  31. package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
  32. package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
  33. package/zova/packages-zova/zova/package.json +2 -2
  34. package/zova/pnpm-lock.yaml +21 -6
  35. package/zova/src/suite/a-home/modules/home-base/src/service/ssrLayout.ts +20 -10
  36. package/zova/src/suite/cabloy-basic/modules/basic-app/src/component/routedDialog/controller.tsx +2 -0
  37. package/zova/src/suite/cabloy-basic/modules/basic-app/src/types/appModal.ts +3 -0
  38. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -1
  39. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
  40. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +40 -25
  41. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +11 -0
  42. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +23 -0
  43. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +5 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/lib/resourcePickerPageHost.ts +64 -0
  45. package/zova/src/suite/cabloy-basic/modules/basic-resource/test/lib/resourcePickerPageHost.test.ts +94 -0
  46. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionColumnConfig.ts +31 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +25 -12
  48. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionColumnConfig/controller.tsx +260 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +3 -3
  50. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +10 -0
  51. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +10 -0
  52. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/cli/openapi.config.ts +9 -0
  53. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +53 -0
  54. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/index.ts +140 -0
  55. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/this.ts +2 -0
  56. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/layoutprofile.ts +67 -0
  57. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/baseURL.ts +5 -0
  58. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/index.ts +3 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/schemas.ts +663 -0
  60. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/types.ts +10150 -0
  61. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/apiSchema/layoutprofile.ts +25 -0
  62. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/index.ts +1 -0
  63. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +76 -0
  64. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.build.json +13 -0
  65. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.json +5 -0
  66. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  67. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/resourcePicker.ts +42 -0
  68. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resourcePicker/controller.tsx +20 -79
  69. package/zova/src/suite-vendor/a-cabloy/package.json +3 -2
  70. package/zova/src/suite-vendor/a-zova/modules/a-bean/package.json +1 -1
  71. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +3 -0
  73. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  74. package/zova/src/suite-vendor/a-zova/modules/a-routerstack/package.json +1 -1
  75. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  76. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +13 -11
  77. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  78. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +64 -2
  79. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +2 -3
  80. package/zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.ts +10 -0
  81. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/index.ts +1 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableLayout.ts +101 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-table/test/lib/tableLayout.test.ts +112 -0
  84. package/zova/src/suite-vendor/a-zova/package.json +7 -7
@@ -33,6 +33,7 @@ export function resolvePickerSelectionMax(
33
33
  }
34
34
 
35
35
  export type TypeTableActionBulkDynamicDisabledReason = 'maxExceeded' | 'selectionUnavailable';
36
+ export type TypeTableActionBulkPlacement = 'start' | 'end';
36
37
 
37
38
  export interface IResourceTableActionBulkDynamicProps {
38
39
  dynamicSelection: IResourceTableSelectionPayload;
@@ -41,6 +42,16 @@ export interface IResourceTableActionBulkDynamicProps {
41
42
  selectedMaxIds?: number;
42
43
  }
43
44
 
45
+ export function groupTableActionBulkActions<T>(
46
+ items: readonly { item: T; placement?: unknown }[],
47
+ ): Record<TypeTableActionBulkPlacement, T[]> {
48
+ const groups: Record<TypeTableActionBulkPlacement, T[]> = { start: [], end: [] };
49
+ for (const { item, placement } of items) {
50
+ groups[placement === 'end' ? 'end' : 'start'].push(item);
51
+ }
52
+ return groups;
53
+ }
54
+
44
55
  export function resolveTableActionBulkDynamicProps(
45
56
  options: IResourceTableActionBulkOptionsBase | undefined,
46
57
  selection: IResourceTableSelectionPayload,
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
 
4
4
  import {
5
+ groupTableActionBulkActions,
5
6
  limitPickerRowSelection,
6
7
  reconcileSelection,
7
8
  resolvePickerSelectionMax,
@@ -11,6 +12,28 @@ import {
11
12
  selectionRowIds,
12
13
  } from '../../src/lib/selection.ts';
13
14
 
15
+ test('bulk actions group by placement without changing declaration order', () => {
16
+ const items = [
17
+ { item: 'create' },
18
+ { item: 'columnConfig', placement: 'end' },
19
+ { item: 'delete' },
20
+ { item: 'export', placement: 'end' },
21
+ { item: 'unknown', placement: 'other' },
22
+ ];
23
+
24
+ assert.deepEqual(groupTableActionBulkActions(items), {
25
+ start: ['create', 'delete', 'unknown'],
26
+ end: ['columnConfig', 'export'],
27
+ });
28
+ assert.deepEqual(items, [
29
+ { item: 'create' },
30
+ { item: 'columnConfig', placement: 'end' },
31
+ { item: 'delete' },
32
+ { item: 'export', placement: 'end' },
33
+ { item: 'unknown', placement: 'other' },
34
+ ]);
35
+ });
36
+
14
37
  test('selection maximum uses action metadata before the frontend fallback', () => {
15
38
  assert.equal(resolveSelectionMaxIds(undefined), 100);
16
39
  assert.equal(resolveSelectionMaxIds(100), 100);
@@ -18,6 +18,7 @@ import { ZFormField, ZFormFieldPreset } from 'zova-module-a-form';
18
18
  import { $QueryEnsureLoaded } from 'zova-module-a-model';
19
19
  import { resolvePickerSelectionMax } from 'zova-module-basic-page';
20
20
  import { ZSelect, ZSelectProps } from 'zova-module-basic-select';
21
+ import { resourcePickerPageHostKey } from 'zova-module-rest-resource';
21
22
  import { ModelResource } from 'zova-module-rest-resource';
22
23
 
23
24
  import type { TypeResourcePickerSelectionMode } from '../../lib/resourcePicker.js';
@@ -26,6 +27,7 @@ import {
26
27
  normalizeResourcePickerIds,
27
28
  resolveResourcePickerValue,
28
29
  } from '../../lib/resourcePicker.js';
30
+ import { createResourcePickerPageHost } from '../../lib/resourcePickerPageHost.js';
29
31
 
30
32
  export type TypeResourcePickerMode = 'select' | 'routedDialog';
31
33
 
@@ -227,6 +229,9 @@ export class ControllerFormFieldResourcePicker extends BeanControllerBase {
227
229
  },
228
230
  props: options,
229
231
  session,
232
+ createPageHostProviders: dialog => ({
233
+ [resourcePickerPageHostKey]: createResourcePickerPageHost(dialog),
234
+ }),
230
235
  });
231
236
  const result = await handle.result;
232
237
  if (!result) return;
@@ -0,0 +1,64 @@
1
+ import type {
2
+ IResourceBlockOptionsBase,
3
+ IResourceRenderBlockOptionsBlock,
4
+ IResourceTableSelectionPayload,
5
+ } from 'zova-module-a-openapi';
6
+ import type { IRoutedDialogContext } from 'zova-module-basic-app';
7
+ import type {
8
+ IResourcePickerPageContext,
9
+ IResourcePickerPageHost,
10
+ } from 'zova-module-rest-resource';
11
+
12
+ import { deepExtend } from 'zova';
13
+
14
+ export function createResourcePickerPageHost(
15
+ dialog: IRoutedDialogContext<
16
+ IResourceTableSelectionPayload,
17
+ IResourcePickerPageContext['options'],
18
+ IResourcePickerPageContext['session']
19
+ >,
20
+ ): IResourcePickerPageHost {
21
+ const options = dialog.props;
22
+ const session = dialog.session;
23
+ if (!options || !session) {
24
+ throw new Error('resource picker requires dialog options and session');
25
+ }
26
+ return {
27
+ options,
28
+ session,
29
+ resolve: selection => dialog.resolve(selection),
30
+ cancel: () => dialog.cancel(),
31
+ prepareBlocks: (blocks, pickerContext) => {
32
+ if (!blocks) return;
33
+ return blocks.map(block => {
34
+ if (block.render !== 'basic-page:blockPage') return block;
35
+ return {
36
+ ...block,
37
+ options: deepExtend({}, block.options, {
38
+ actionPath: pickerContext.options.actionPath,
39
+ queryFixed: pickerContext.options.query,
40
+ selectionPolicy: 'always',
41
+ selectionMode: pickerContext.options.selectionMode,
42
+ selectionMax: pickerContext.options.selectionMax,
43
+ selectedIds: pickerContext.session.selectedIds,
44
+ selectedRows: pickerContext.session.selectedRows,
45
+ onSelectionChange: (selection: IResourceTableSelectionPayload) => {
46
+ pickerContext.session.selectedIds = [...selection.ids];
47
+ pickerContext.session.selectedRows = selection.rows.map(row => ({ ...row }));
48
+ },
49
+ blocks: [
50
+ ...((
51
+ block.options as IResourceBlockOptionsBase & {
52
+ blocks?: IResourceRenderBlockOptionsBlock[];
53
+ }
54
+ ).blocks ?? []),
55
+ {
56
+ render: 'basic-resource:blockResourcePickerActions',
57
+ },
58
+ ],
59
+ }),
60
+ };
61
+ });
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,94 @@
1
+ import type { IResourceRenderBlockOptionsBlock } from 'zova-module-a-openapi';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import test from 'node:test';
5
+
6
+ import { createResourcePickerPageHost } from '../../src/lib/resourcePickerPageHost.ts';
7
+
8
+ function createDialog() {
9
+ const resolved: unknown[] = [];
10
+ let cancelled = false;
11
+ const dialog = {
12
+ id: 1,
13
+ props: {
14
+ resource: 'training-student:student',
15
+ selectionMode: 'multiple' as const,
16
+ selectionMax: 2,
17
+ },
18
+ session: {
19
+ selectedIds: [1],
20
+ selectedRows: [{ id: 1, name: 'One' }],
21
+ },
22
+ resolve(value: unknown) {
23
+ resolved.push(value);
24
+ },
25
+ cancel() {
26
+ cancelled = true;
27
+ },
28
+ };
29
+ return {
30
+ dialog,
31
+ resolved,
32
+ get cancelled() {
33
+ return cancelled;
34
+ },
35
+ };
36
+ }
37
+
38
+ test('Basic picker host adapts only the Basic page block and preserves picker state', () => {
39
+ const { dialog } = createDialog();
40
+ const host = createResourcePickerPageHost(dialog);
41
+ const unrelated: IResourceRenderBlockOptionsBlock = {
42
+ render: 'other:block' as never,
43
+ options: { class: 'other' },
44
+ };
45
+ const basicPage: IResourceRenderBlockOptionsBlock = {
46
+ render: 'basic-page:blockPage' as never,
47
+ options: {
48
+ blocks: [{ render: 'basic-page:blockTable' as never }],
49
+ } as any,
50
+ };
51
+
52
+ const context = {
53
+ options: dialog.props,
54
+ session: dialog.session,
55
+ dialog: host,
56
+ };
57
+ const blocks = host.prepareBlocks([unrelated, basicPage], context)!;
58
+
59
+ assert.equal(blocks[0], unrelated);
60
+ assert.deepEqual(blocks[1].options, {
61
+ actionPath: undefined,
62
+ queryFixed: undefined,
63
+ selectionPolicy: 'always',
64
+ selectionMode: 'multiple',
65
+ selectionMax: 2,
66
+ selectedIds: [1],
67
+ selectedRows: [{ id: 1, name: 'One' }],
68
+ blocks: [
69
+ { render: 'basic-page:blockTable' },
70
+ { render: 'basic-resource:blockResourcePickerActions' },
71
+ ],
72
+ });
73
+
74
+ const onSelectionChange = (blocks[1].options as any).onSelectionChange as (
75
+ selection: any,
76
+ ) => void;
77
+ onSelectionChange({ ids: [2], rows: [{ id: 2, name: 'Two' }], count: 1 });
78
+ assert.deepEqual(dialog.session, {
79
+ selectedIds: [2],
80
+ selectedRows: [{ id: 2, name: 'Two' }],
81
+ });
82
+ });
83
+
84
+ test('Basic picker host forwards resolve and cancel', () => {
85
+ const { dialog, resolved, cancelled } = createDialog();
86
+ const host = createResourcePickerPageHost(dialog);
87
+ const selection = { ids: [1], rows: [{ id: 1 }], count: 1 };
88
+
89
+ host.resolve(selection);
90
+ host.cancel();
91
+
92
+ assert.deepEqual(resolved, [selection]);
93
+ assert.equal(cancelled, true);
94
+ });
@@ -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 { ControllerActionColumnConfigProps } from '../../component/actionColumnConfig/controller.jsx';
7
+
8
+ import { ControllerActionColumnConfig } from '../../component/actionColumnConfig/controller.jsx';
9
+ export type ZActionColumnConfigProps = {
10
+ controllerRef?: (ref: ControllerActionColumnConfig) => void;
11
+ } & ControllerActionColumnConfigProps;
12
+
13
+ type ControllerInnerProps = TypeControllerInnerProps<
14
+ ControllerActionColumnConfigProps,
15
+ keyof typeof ControllerActionColumnConfig.$propsDefault
16
+ >;
17
+ declare module 'zova-module-basic-table' {
18
+ export interface ControllerActionColumnConfig {
19
+ $props: ControllerInnerProps;
20
+ }
21
+ }
22
+
23
+ export const ZActionColumnConfig = defineComponent((_props: ZActionColumnConfigProps) => {
24
+ useController(ControllerActionColumnConfig, undefined, undefined);
25
+ return () => {};
26
+ }, prepareComponentOptions(ControllerActionColumnConfig.$componentOptions));
27
+ declare module 'zova-module-a-bean' {
28
+ export interface IVonaComponentRecord {
29
+ 'basic-table:actionColumnConfig': ControllerActionColumnConfigProps;
30
+ }
31
+ }
@@ -1,5 +1,6 @@
1
1
  // eslint-disable
2
2
  /** controller: begin */
3
+ export * from '../component/actionColumnConfig/controller.jsx';
3
4
  export * from '../component/actionCreate/controller.jsx';
4
5
  export * from '../component/actionDeleteBulk/controller.jsx';
5
6
  export * from '../component/table/controller.jsx';
@@ -11,6 +12,11 @@ declare module 'zova' {
11
12
  }
12
13
  declare module 'zova-module-basic-table' {
13
14
 
15
+ export interface ControllerActionColumnConfig {
16
+ /** @internal */
17
+ get scope(): ScopeModuleBasicTable;
18
+ }
19
+
14
20
  export interface ControllerActionCreate {
15
21
  /** @internal */
16
22
  get scope(): ScopeModuleBasicTable;
@@ -28,13 +34,15 @@ declare module 'zova-module-basic-table' {
28
34
  }
29
35
  /** controller: end */
30
36
  /** controller: begin */
31
- import { ControllerActionCreate } from '../component/actionCreate/controller.jsx';
32
- import { ControllerActionDeleteBulk } from '../component/actionDeleteBulk/controller.jsx';
33
- import { ControllerTable } from '../component/table/controller.jsx';
37
+ import type { ControllerActionColumnConfig } from '../component/actionColumnConfig/controller.jsx';
38
+ import type { ControllerActionCreate } from '../component/actionCreate/controller.jsx';
39
+ import type { ControllerActionDeleteBulk } from '../component/actionDeleteBulk/controller.jsx';
40
+ import type { ControllerTable } from '../component/table/controller.jsx';
34
41
  import 'zova';
35
42
  declare module 'zova' {
36
43
  export interface IBeanRecordLocal {
37
- 'basic-table.controller.actionCreate': ControllerActionCreate;
44
+ 'basic-table.controller.actionColumnConfig': ControllerActionColumnConfig;
45
+ 'basic-table.controller.actionCreate': ControllerActionCreate;
38
46
  'basic-table.controller.actionDeleteBulk': ControllerActionDeleteBulk;
39
47
  'basic-table.controller.table': ControllerTable;
40
48
  }
@@ -42,6 +50,8 @@ declare module 'zova' {
42
50
  /** controller: end */
43
51
 
44
52
  /** components: begin */
53
+ export * from './component/actionColumnConfig.js';
54
+ import { ZActionColumnConfig } from './component/actionColumnConfig.js';
45
55
  export * from './component/actionCreate.js';
46
56
  import { ZActionCreate } from './component/actionCreate.js';
47
57
  export * from './component/actionDeleteBulk.js';
@@ -49,19 +59,22 @@ import { ZActionDeleteBulk } from './component/actionDeleteBulk.js';
49
59
  export * from './component/table.js';
50
60
  import { ZTable } from './component/table.js';
51
61
  export const components = {
52
- 'actionCreate': ZActionCreate,
62
+ 'actionColumnConfig': ZActionColumnConfig,
63
+ 'actionCreate': ZActionCreate,
53
64
  'actionDeleteBulk': ZActionDeleteBulk,
54
65
  'table': ZTable,
55
66
  };
56
67
  import 'zova';
57
68
  declare module 'zova' {
58
69
  export interface IComponentRecord {
59
- 'basic-table:actionCreate': ControllerActionCreate;
70
+ 'basic-table:actionColumnConfig': ControllerActionColumnConfig;
71
+ 'basic-table:actionCreate': ControllerActionCreate;
60
72
  'basic-table:actionDeleteBulk': ControllerActionDeleteBulk;
61
73
  'basic-table:table': ControllerTable;
62
74
  }
63
75
  export interface IZovaComponentRecord {
64
- 'basic-table:actionCreate': typeof ZActionCreate;
76
+ 'basic-table:actionColumnConfig': typeof ZActionColumnConfig;
77
+ 'basic-table:actionCreate': typeof ZActionCreate;
65
78
  'basic-table:actionDeleteBulk': typeof ZActionDeleteBulk;
66
79
  'basic-table:table': typeof ZTable;
67
80
  }
@@ -84,7 +97,7 @@ declare module 'zova-module-basic-table' {
84
97
  }
85
98
  /** render: end */
86
99
  /** render: begin */
87
- import { RenderTable } from '../component/table/render.jsx';
100
+ import type { RenderTable } from '../component/table/render.jsx';
88
101
  import 'zova';
89
102
  declare module 'zova' {
90
103
  export interface IBeanRecordLocal {
@@ -161,10 +174,10 @@ declare module 'zova-module-basic-table' {
161
174
  }
162
175
  /** tableCell: end */
163
176
  /** tableCell: begin */
164
- import { TableCellActionDelete } from '../bean/tableCell.actionDelete.jsx';
165
- import { TableCellActionOperationsRow } from '../bean/tableCell.actionOperationsRow.jsx';
166
- import { TableCellActionUpdate } from '../bean/tableCell.actionUpdate.jsx';
167
- import { TableCellActionView } from '../bean/tableCell.actionView.jsx';
177
+ import type { TableCellActionDelete } from '../bean/tableCell.actionDelete.jsx';
178
+ import type { TableCellActionOperationsRow } from '../bean/tableCell.actionOperationsRow.jsx';
179
+ import type { TableCellActionUpdate } from '../bean/tableCell.actionUpdate.jsx';
180
+ import type { TableCellActionView } from '../bean/tableCell.actionView.jsx';
168
181
  import 'zova';
169
182
  declare module 'zova' {
170
183
  export interface IBeanRecordGeneral {
@@ -0,0 +1,260 @@
1
+ import type { IComponentOptions } from 'zova';
2
+ import type {
3
+ IJsxRenderContextPage,
4
+ IResourceTableActionBulkPropsBase,
5
+ ISchemaObjectExtensionField,
6
+ } from 'zova-module-a-openapi';
7
+
8
+ import { BeanControllerBase, Use } from 'zova';
9
+ import { Controller } from 'zova-module-a-bean';
10
+ import { ZIcon } from 'zova-module-a-icon';
11
+ import { ITableLayout } from 'zova-module-a-table';
12
+ import { reconcileTableLayout } from 'zova-module-a-table';
13
+
14
+ declare module 'zova-module-a-openapi' {
15
+ export interface IResourceTableActionBulkRecord {
16
+ 'basic-table:actionColumnConfig'?: ControllerActionColumnConfigProps;
17
+ }
18
+ }
19
+
20
+ export interface ControllerActionColumnConfigProps extends IResourceTableActionBulkPropsBase {}
21
+
22
+ @Controller()
23
+ export class ControllerActionColumnConfig extends BeanControllerBase {
24
+ static $propsDefault = { class: 'btn join-item' };
25
+ static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
26
+
27
+ private _saving = false;
28
+ private _error: string | undefined;
29
+ private _reset = false;
30
+ private _draft: ITableLayout | undefined;
31
+
32
+ @Use({ injectionScope: 'host' })
33
+ $$renderContext: IJsxRenderContextPage;
34
+
35
+ get $$page() {
36
+ return this.$$renderContext.$$page;
37
+ }
38
+
39
+ get seed() {
40
+ return this.$$page.columnConfigSeed;
41
+ }
42
+
43
+ protected render() {
44
+ // const disabled =
45
+ // this.$props.disabled === true || this.$props.dynamicDisabled === true || !this.seed;
46
+ const disabled = this.$props.disabled === true || this.$props.dynamicDisabled === true;
47
+ return (
48
+ <button
49
+ class={[this.$props.class, 'btn-square']}
50
+ type="button"
51
+ disabled={disabled}
52
+ aria-label={this.scope.locale.ColumnConfiguration()}
53
+ title={this.scope.locale.ColumnConfiguration()}
54
+ onClick={() => this._open()}
55
+ >
56
+ <ZIcon name="::settings" width={20}></ZIcon>
57
+ </button>
58
+ );
59
+ }
60
+
61
+ private _open() {
62
+ const seed = this.seed;
63
+ if (!seed) return;
64
+ this._error = undefined;
65
+ this._reset = false;
66
+ this._draft = this._cloneLayout(seed.layout);
67
+ const dialog = this.$appModal.dialog(
68
+ {
69
+ title: this.scope.locale.ColumnConfiguration(),
70
+ slotDefault: () => this._renderDialog(),
71
+ slotActions: modal => (
72
+ <>
73
+ {this._error && (
74
+ <span class="mr-auto text-sm text-error" role="alert">
75
+ {this._error}
76
+ </span>
77
+ )}
78
+ <button
79
+ class="btn btn-ghost"
80
+ type="button"
81
+ disabled={this._saving}
82
+ onClick={modal.close}
83
+ >
84
+ {this.scope.locale.Cancel()}
85
+ </button>
86
+ <button
87
+ class="btn btn-primary"
88
+ type="button"
89
+ disabled={this._saving}
90
+ onClick={() => this._save(dialog)}
91
+ >
92
+ {this._saving && <span class="loading loading-spinner loading-xs" />}
93
+ {this.scope.locale.Save()}
94
+ </button>
95
+ </>
96
+ ),
97
+ },
98
+ { closeOnBackdrop: false, closeOnEscape: false, maxWidth: 560, showCloseButton: true },
99
+ );
100
+ }
101
+
102
+ private _renderDialog() {
103
+ const draft = this._draft;
104
+ const properties = this.seed?.properties ?? [];
105
+ return (
106
+ <div class="max-h-[60vh] space-y-2 overflow-y-auto">
107
+ <div class="text-sm text-base-content/70">{this.scope.locale.Columns()}</div>
108
+ {draft?.columns.map((column, index) => {
109
+ const property = properties.find(item => item.key === column.key);
110
+ const previous = draft.columns[index - 1];
111
+ const next = draft.columns[index + 1];
112
+ const canMoveUp =
113
+ !!previous && this._sameFixedRegion(properties, column.key, previous.key);
114
+ const canMoveDown = !!next && this._sameFixedRegion(properties, column.key, next.key);
115
+ return (
116
+ <div
117
+ key={column.key}
118
+ class="flex items-center gap-2 rounded-box border border-base-300 p-2"
119
+ >
120
+ <input
121
+ class="checkbox checkbox-sm"
122
+ type="checkbox"
123
+ checked={column.visible}
124
+ aria-label={`${this.scope.locale.Visible()} ${property?.title ?? column.key}`}
125
+ onChange={(event: Event) => {
126
+ this._reset = false;
127
+ column.visible = (event.target as HTMLInputElement).checked;
128
+ }}
129
+ />
130
+ <span class="min-w-0 flex-1 truncate">{property?.title ?? column.key}</span>
131
+ <input
132
+ class="input input-bordered input-sm w-20"
133
+ type="number"
134
+ min="1"
135
+ max="2000"
136
+ value={typeof column.width === 'number' ? column.width : ''}
137
+ aria-label={`${this.scope.locale.Width()} ${property?.title ?? column.key}`}
138
+ onInput={(event: Event) => {
139
+ this._reset = false;
140
+ const value = (event.target as HTMLInputElement).value;
141
+ column.width = value === '' ? 'auto' : Number(value);
142
+ }}
143
+ />
144
+ <label class="flex items-center gap-1 text-xs">
145
+ <input
146
+ class="checkbox checkbox-sm"
147
+ type="checkbox"
148
+ checked={column.width === 'auto'}
149
+ aria-label={`${this.scope.locale.Auto()} ${property?.title ?? column.key}`}
150
+ onChange={(event: Event) => {
151
+ if ((event.target as HTMLInputElement).checked) {
152
+ this._reset = false;
153
+ column.width = 'auto';
154
+ }
155
+ }}
156
+ />
157
+ {this.scope.locale.Auto()}
158
+ </label>
159
+ <button
160
+ class="btn btn-ghost btn-xs"
161
+ type="button"
162
+ disabled={!canMoveUp || this._saving}
163
+ aria-label={this.scope.locale.MoveUp()}
164
+ onClick={() => this._moveColumn(index, -1)}
165
+ >
166
+ ↑
167
+ </button>
168
+ <button
169
+ class="btn btn-ghost btn-xs"
170
+ type="button"
171
+ disabled={!canMoveDown || this._saving}
172
+ aria-label={this.scope.locale.MoveDown()}
173
+ onClick={() => this._moveColumn(index, 1)}
174
+ >
175
+ ↓
176
+ </button>
177
+ </div>
178
+ );
179
+ })}
180
+ <button
181
+ class="btn btn-outline btn-sm"
182
+ type="button"
183
+ disabled={this._saving}
184
+ onClick={() => this._resetDraft()}
185
+ >
186
+ {this.scope.locale.Reset() as string}
187
+ </button>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ private _cloneLayout(layout: ITableLayout): ITableLayout {
193
+ return {
194
+ version: 1,
195
+ ...(layout.schemaFingerprint ? { schemaFingerprint: layout.schemaFingerprint } : {}),
196
+ columns: layout.columns.map(column => ({ ...column })),
197
+ };
198
+ }
199
+
200
+ private _sameFixedRegion(
201
+ properties: readonly ISchemaObjectExtensionField[],
202
+ leftKey: string,
203
+ rightKey: string,
204
+ ) {
205
+ const left = properties.find(item => item.key === leftKey)?.rest?.fixed;
206
+ const right = properties.find(item => item.key === rightKey)?.rest?.fixed;
207
+ return (left ?? 'center') === (right ?? 'center');
208
+ }
209
+
210
+ private _moveColumn(index: number, offset: -1 | 1) {
211
+ const columns = this._draft?.columns;
212
+ const properties = this.seed?.properties;
213
+ if (!columns || !properties) return;
214
+ const target = index + offset;
215
+ if (
216
+ target < 0 ||
217
+ target >= columns.length ||
218
+ !this._sameFixedRegion(properties, columns[index].key, columns[target].key)
219
+ ) {
220
+ return;
221
+ }
222
+ this._reset = false;
223
+ [columns[index], columns[target]] = [columns[target], columns[index]];
224
+ }
225
+
226
+ private _resetDraft() {
227
+ const seed = this.seed;
228
+ if (!seed || this._saving) return;
229
+ this._reset = true;
230
+ this._draft = reconcileTableLayout(seed.properties);
231
+ }
232
+
233
+ private async _save(dialog: { close: () => void }) {
234
+ const seed = this.seed;
235
+ const draft = this._draft;
236
+ if (!seed || !draft || this._saving) return;
237
+ this._saving = true;
238
+ this._error = undefined;
239
+ try {
240
+ if (this._reset) {
241
+ await this.$$page.resetColumnConfig();
242
+ } else {
243
+ await this.$$page.saveColumnConfig(this._normalizeLayout(seed.properties, draft));
244
+ }
245
+ this._reset = false;
246
+ dialog.close();
247
+ } catch (error) {
248
+ this._error = error instanceof Error ? error.message : String(error);
249
+ } finally {
250
+ this._saving = false;
251
+ }
252
+ }
253
+
254
+ private _normalizeLayout(
255
+ properties: readonly ISchemaObjectExtensionField[],
256
+ draft: ITableLayout,
257
+ ): ITableLayout {
258
+ return reconcileTableLayout(properties, draft);
259
+ }
260
+ }
@@ -22,6 +22,7 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
22
22
  tableScope={this.$props.tableScope}
23
23
  getColumns={this.$props.getColumns}
24
24
  getRowId={this.$props.getRowId}
25
+ layout={this.$props.layout}
25
26
  controllerRef={ref => {
26
27
  this.$props?.tableRef?.(ref);
27
28
  }}
@@ -35,10 +36,9 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
35
36
  private _getColumnStyle(column: any): CSSProperties {
36
37
  const rest = (column.columnDef.meta as any)?.rest;
37
38
  const fixed = column.getIsPinned();
38
- const width = rest?.width ? `${rest.width}px` : undefined;
39
+ const width = column.getSize();
39
40
  return {
40
- width,
41
- minWidth: width,
41
+ ...(typeof rest?.width === 'number' ? { width: `${width}px`, minWidth: `${width}px` } : {}),
42
42
  textAlign: rest?.align,
43
43
  position: fixed ? ('sticky' as const) : undefined,
44
44
  left: fixed === 'left' ? `${column.getStart('left')}px` : undefined,
@@ -5,4 +5,14 @@ export default {
5
5
  SelectRow: 'Select row %s',
6
6
  DeleteBulk: 'Bulk Delete',
7
7
  DeleteBulkConfirm: 'Are you sure you want to bulk delete %d selected items?',
8
+ ColumnConfiguration: 'Column configuration',
9
+ Columns: 'Columns',
10
+ Visible: 'Visible',
11
+ Width: 'Width',
12
+ Auto: 'Auto',
13
+ MoveUp: 'Move up',
14
+ MoveDown: 'Move down',
15
+ Save: 'Save',
16
+ Cancel: 'Cancel',
17
+ Reset: 'Reset',
8
18
  };