cabloy 5.1.169 → 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 (73) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +9 -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 +77 -127
  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-page/src/component/blockPage/controller.tsx +44 -1
  37. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +1 -0
  38. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +40 -25
  39. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +11 -0
  40. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +23 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionColumnConfig.ts +31 -0
  42. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +25 -12
  43. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionColumnConfig/controller.tsx +260 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +3 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +10 -0
  46. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +10 -0
  47. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/cli/openapi.config.ts +9 -0
  48. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/package.json +53 -0
  49. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/index.ts +140 -0
  50. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/.metadata/this.ts +2 -0
  51. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/layoutprofile.ts +67 -0
  52. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/baseURL.ts +5 -0
  53. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/index.ts +3 -0
  54. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/schemas.ts +663 -0
  55. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/api/openapi/types.ts +10150 -0
  56. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/apiSchema/layoutprofile.ts +25 -0
  57. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/index.ts +1 -0
  58. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/src/model/layoutprofile.ts +76 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.build.json +13 -0
  60. package/zova/src/suite-vendor/a-cabloy/modules/a-layoutprofile/tsconfig.json +5 -0
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -1
  62. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +3 -0
  64. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  65. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +13 -11
  66. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  67. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +64 -2
  68. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +2 -3
  69. package/zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.ts +10 -0
  70. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/index.ts +1 -0
  71. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableLayout.ts +101 -0
  72. package/zova/src/suite-vendor/a-zova/modules/a-table/test/lib/tableLayout.test.ts +112 -0
  73. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -0,0 +1,25 @@
1
+ import type { IApiSchemaOptions } from 'zova-module-a-api';
2
+
3
+ import { BeanBase } from 'zova';
4
+ import { ApiSchema } from 'zova-module-a-api';
5
+
6
+ import {
7
+ ApiApiLayoutprofileloadPath,
8
+ ApiApiLayoutprofilesavePath,
9
+ ApiApiLayoutprofileresetPath,
10
+ } from '../api/layoutprofile.js';
11
+
12
+ @ApiSchema()
13
+ export class ApiSchemaLayoutprofile extends BeanBase {
14
+ load(options?: IApiSchemaOptions) {
15
+ return this.$sdk.createApiSchemas(ApiApiLayoutprofileloadPath, 'get', options);
16
+ }
17
+
18
+ save(options?: IApiSchemaOptions) {
19
+ return this.$sdk.createApiSchemas(ApiApiLayoutprofilesavePath, 'post', options);
20
+ }
21
+
22
+ reset(options?: IApiSchemaOptions) {
23
+ return this.$sdk.createApiSchemas(ApiApiLayoutprofileresetPath, 'delete', options);
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export * from './.metadata/index.js';
@@ -0,0 +1,76 @@
1
+ import type { IDecoratorModelOptions } from 'zova-module-a-model';
2
+
3
+ import { BeanModelBase, Model } from 'zova-module-a-model';
4
+
5
+ import type {
6
+ ApiApiLayoutprofileloadResponseBody,
7
+ ApiApiLayoutprofileresetResponseBody,
8
+ ApiApiLayoutprofilesaveRequestBody,
9
+ ApiApiLayoutprofilesaveResponseBody,
10
+ } from '../api/layoutprofile.js';
11
+
12
+ export interface IModelOptionsLayoutProfile extends IDecoratorModelOptions {
13
+ enableSelector: true;
14
+ }
15
+
16
+ @Model<IModelOptionsLayoutProfile>({
17
+ enableSelector: true,
18
+ })
19
+ export class ModelLayoutProfile extends BeanModelBase {
20
+ layoutKey: string;
21
+
22
+ protected async __init__(layoutKey: string) {
23
+ if (!layoutKey) throw new Error('layout key not specified');
24
+ await super.__init__(layoutKey);
25
+ this.layoutKey = layoutKey;
26
+ }
27
+
28
+ load() {
29
+ if (this.$ssr.cookieDisabledOnServer || !this.$passport.isAuthenticated) return;
30
+ if (process.env.CLIENT && this.$ssr.profile === 'public' && this.$ssr.isRuntimeSsrPreHydration)
31
+ return;
32
+ return this.$useStateData<ApiApiLayoutprofileloadResponseBody>({
33
+ queryKey: ['load'],
34
+ queryFn: async () => {
35
+ return await this.scope.api.layoutprofile.load({
36
+ query: { layoutKey: this.layoutKey },
37
+ });
38
+ },
39
+ meta: {
40
+ disableErrorEffect: true,
41
+ },
42
+ });
43
+ }
44
+
45
+ save() {
46
+ return this.$useMutationData<
47
+ ApiApiLayoutprofilesaveResponseBody,
48
+ ApiApiLayoutprofilesaveRequestBody['profile']
49
+ >({
50
+ mutationKey: ['save'],
51
+ mutationFn: async profile => {
52
+ return await this.scope.api.layoutprofile.save({
53
+ layoutKey: this.layoutKey,
54
+ profile,
55
+ });
56
+ },
57
+ onSuccess: profile => {
58
+ this.$setQueryData<ApiApiLayoutprofilesaveResponseBody>(['load'], profile);
59
+ },
60
+ });
61
+ }
62
+
63
+ reset() {
64
+ return this.$useMutationData<ApiApiLayoutprofileresetResponseBody>({
65
+ mutationKey: ['reset'],
66
+ mutationFn: async () => {
67
+ return await this.scope.api.layoutprofile.reset({
68
+ query: { layoutKey: this.layoutKey },
69
+ });
70
+ },
71
+ onSuccess: async () => {
72
+ this.$setQueryData<ApiApiLayoutprofileloadResponseBody>(['load'], undefined);
73
+ },
74
+ });
75
+ }
76
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "emitDeclarationOnly": true,
9
+ "allowImportingTsExtensions": true,
10
+ "noEmit": false
11
+ },
12
+ "include": ["src", "src/.metadata/**/*"]
13
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {},
4
+ "include": ["src", "src/.metadata/**/*"]
5
+ }
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.55",
3
+ "version": "5.1.56",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
7
7
  "files": [],
8
8
  "type": "module",
9
9
  "dependencies": {
10
+ "zova-module-a-layoutprofile": "^5.0.2",
10
11
  "zova-module-a-rbac": "^5.0.1",
11
12
  "zova-module-rest-resource": "^5.1.54"
12
13
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-openapi",
3
- "version": "5.1.53",
3
+ "version": "5.1.54",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -20,9 +20,12 @@ export interface IResourceTableSelectionPayload<TData extends {} = Record<string
20
20
  }
21
21
 
22
22
  /** Declarative bulk-action metadata authored by a resource schema. */
23
+ export type TypeResourceTableActionBulkPlacement = 'start' | 'end';
24
+
23
25
  export interface IResourceTableActionBulkOptionsBase {
24
26
  class?: any;
25
27
  style?: types.NestedCSSProperties;
28
+ placement?: TypeResourceTableActionBulkPlacement;
26
29
  resource?: string;
27
30
  permission?: IPermissionHintTableActionBulk;
28
31
  requiresSelection?: boolean;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-ssr",
3
- "version": "5.1.38",
3
+ "version": "5.1.39",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,19 +1,21 @@
1
1
  export function getBodyReadyObserverScript() {
2
2
  return `<script id="ssr-body-ready-observer">
3
3
  window.ssr_bodyReadyObserverClear=()=>{
4
- if(window.ssr_bodyReadyObserver){
5
- window.ssr_body_ready_condition=undefined;
6
- window.ssr_body_ready_callback=undefined;
7
- window.ssr_bodyReadyObserver.disconnect();
8
- window.ssr_bodyReadyObserver=undefined;
9
- document.body.style.display='block';
10
- document.querySelector('#ssr-body-ready-observer').remove();
11
- }
4
+ window.ssr_body_ready_condition=undefined;
5
+ window.ssr_body_ready_callback=undefined;
6
+ window.ssr_bodyReadyObserver?.disconnect();
7
+ window.ssr_bodyReadyObserver=undefined;
8
+ document.body.style.display='block';
9
+ document.querySelector('#ssr-body-ready-observer')?.remove();
12
10
  };
13
11
  window.ssr_bodyReadyObserver = new MutationObserver(() => {
14
- if(window.ssr_body_ready_condition && window.ssr_body_ready_condition()){
15
- window.ssr_body_ready_callback();
16
- window.ssr_bodyReadyObserverClear();
12
+ const __targets=window.ssr_body_ready_condition?.();
13
+ if(__targets){
14
+ try{
15
+ window.ssr_body_ready_callback?.(__targets);
16
+ }finally{
17
+ window.ssr_bodyReadyObserverClear();
18
+ }
17
19
  }
18
20
  });
19
21
  window.ssr_bodyReadyObserver.observe(document.body, {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-table",
3
- "version": "5.1.42",
3
+ "version": "5.1.43",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -45,13 +45,16 @@ import type {
45
45
  ITableScope,
46
46
  TypeTableCellRender,
47
47
  } from '../../types/tableColumn.js';
48
+ import type { ITableLayout } from '../../types/tableLayout.js';
48
49
 
49
50
  import { BeanControllerTableBase } from '../../lib/beanControllerTableBase.js';
50
51
  import { TableColumnIdSelection } from '../../types/table.js';
51
52
  import { ITableCellRenderColumnOptions } from '../../types/tableColumn.js';
53
+ import { reconcileTableLayout } from '../../types/tableLayout.js';
52
54
 
53
55
  export interface ControllerTableProps<TData extends {} = {}> {
54
56
  data?: TData[];
57
+ layout?: ITableLayout;
55
58
  schema?: SchemaObject;
56
59
  schemaOrder?: SchemaObject;
57
60
  sorting?: SortingState;
@@ -102,6 +105,13 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
102
105
  await this.refreshMeta();
103
106
  },
104
107
  );
108
+ this.$watch(
109
+ () => this.$props.layout,
110
+ async (newValue, oldValue) => {
111
+ if (deepEqual(newValue, oldValue)) return;
112
+ await this.refreshMeta();
113
+ },
114
+ );
105
115
  // table
106
116
  this._createTable();
107
117
  }
@@ -122,6 +132,14 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
122
132
  return this.$props.rowSelection ?? {};
123
133
  }
124
134
 
135
+ public get layout(): ITableLayout {
136
+ return reconcileTableLayout(this.layoutProperties, this.$props.layout);
137
+ }
138
+
139
+ public get layoutProperties(): readonly ISchemaObjectExtensionField[] {
140
+ return this.tableMeta?.properties ?? [];
141
+ }
142
+
125
143
  public async refreshMeta() {
126
144
  this.tableMeta = await this._createTableMeta();
127
145
  this.columns = await this._createColumns();
@@ -148,6 +166,15 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
148
166
  get rowSelection() {
149
167
  return self.rowSelection;
150
168
  },
169
+ get columnVisibility() {
170
+ return self._getColumnVisibility();
171
+ },
172
+ get columnOrder() {
173
+ return self._getColumnOrder();
174
+ },
175
+ get columnSizing() {
176
+ return self._getColumnSizing();
177
+ },
151
178
  },
152
179
  onSortingChange: updater => {
153
180
  const sorting = functionalUpdate(updater, self.sorting).slice(0, 1);
@@ -235,10 +262,15 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
235
262
  return property?.title || key;
236
263
  },
237
264
  cell: props => tableMeta.renders[key](props),
238
- size: property.rest?.width,
265
+ size: this._getLayoutWidth(key),
239
266
  enableSorting: property.rest?.enableSorting === true && this._isOrderable(property),
240
267
  sortDescFirst: property.rest?.sortDescFirst === true,
241
- meta: { rest: property.rest },
268
+ meta: {
269
+ rest: {
270
+ ...property.rest,
271
+ width: this._getLayoutWidth(key),
272
+ },
273
+ },
242
274
  }),
243
275
  );
244
276
  }
@@ -262,6 +294,36 @@ export class ControllerTable<TData extends {} = {}> extends BeanControllerTableB
262
294
  return typeof enableRowSelection === 'function' ? enableRowSelection(row) : true;
263
295
  }
264
296
 
297
+ private _getLayoutColumn(key: string) {
298
+ return this.layout.columns.find(column => column.key === key);
299
+ }
300
+
301
+ private _getLayoutWidth(key: string) {
302
+ const width = this._getLayoutColumn(key)?.width;
303
+ return typeof width === 'number' ? width : undefined;
304
+ }
305
+
306
+ private _getColumnVisibility() {
307
+ return Object.fromEntries(
308
+ this.layout.columns
309
+ .filter(column => column.key !== TableColumnIdSelection)
310
+ .map(column => [column.key, column.visible]),
311
+ );
312
+ }
313
+
314
+ private _getColumnOrder() {
315
+ const keys = this.layout.columns.map(column => column.key);
316
+ return this.$props.enableRowSelection ? [TableColumnIdSelection, ...keys] : keys;
317
+ }
318
+
319
+ private _getColumnSizing() {
320
+ return Object.fromEntries(
321
+ this.layout.columns
322
+ .filter(column => typeof column.width === 'number')
323
+ .map(column => [column.key, column.width as number]),
324
+ );
325
+ }
326
+
265
327
  private _getColumnPinning() {
266
328
  const left: string[] = this.$props.enableRowSelection ? [TableColumnIdSelection] : [];
267
329
  const right: string[] = [];
@@ -11,10 +11,9 @@ export class RenderTable extends BeanRenderBase {
11
11
  private _getColumnStyle(column: any): CSSProperties {
12
12
  const rest = (column.columnDef.meta as any)?.rest;
13
13
  const fixed = column.getIsPinned();
14
- const width = rest?.width ? `${rest.width}px` : undefined;
14
+ const width = column.getSize();
15
15
  return {
16
- width,
17
- minWidth: width,
16
+ ...(typeof rest?.width === 'number' ? { width: `${width}px`, minWidth: `${width}px` } : {}),
18
17
  textAlign: rest?.align,
19
18
  position: fixed ? ('sticky' as const) : undefined,
20
19
  left: fixed === 'left' ? `${column.getStart('left')}px` : undefined,
@@ -1,15 +1,25 @@
1
1
  import type { Table } from '@tanstack/table-core';
2
2
  import type { TableOptionsWithReactiveData } from '@tanstack/vue-table';
3
+ import type { ISchemaObjectExtensionField } from 'zova-module-a-openapi';
3
4
 
4
5
  import { useVueTable } from '@tanstack/vue-table';
5
6
  import { markRaw } from 'vue';
6
7
  import { BeanControllerBase } from 'zova';
7
8
 
8
9
  import { TypeTable } from '../types/table.js';
10
+ import { ITableLayout } from '../types/tableLayout.js';
9
11
 
10
12
  export class BeanControllerTableBase<TData extends {} = {}> extends BeanControllerBase {
11
13
  table: TypeTable<TData>;
12
14
 
15
+ public get layout(): ITableLayout {
16
+ throw new Error('should implement layout');
17
+ }
18
+
19
+ public get layoutProperties(): readonly ISchemaObjectExtensionField[] {
20
+ throw new Error('should implement layoutProperties');
21
+ }
22
+
13
23
  public $useTable(initialOptions: TableOptionsWithReactiveData<TData>): Table<TData> {
14
24
  return this.ctx.util.instanceScope(() => {
15
25
  return markRaw(useVueTable(initialOptions));
@@ -1,3 +1,4 @@
1
1
  export * from './table.js';
2
2
  export * from './tableCell.js';
3
3
  export * from './tableColumn.js';
4
+ export * from './tableLayout.js';
@@ -0,0 +1,101 @@
1
+ import type { ISchemaObjectExtensionField } from 'zova-module-a-openapi';
2
+
3
+ import { TableColumnIdSelection } from './table.js';
4
+
5
+ export type TypeTableLayoutWidth = number | 'auto';
6
+
7
+ export interface ITableLayoutColumn {
8
+ key: string;
9
+ visible: boolean;
10
+ width: TypeTableLayoutWidth;
11
+ }
12
+
13
+ export interface ITableLayout {
14
+ version: 1;
15
+ schemaFingerprint?: string;
16
+ columns: ITableLayoutColumn[];
17
+ }
18
+
19
+ export interface ITableLayoutProfileLike {
20
+ version?: unknown;
21
+ schemaFingerprint?: unknown;
22
+ columns?: unknown;
23
+ }
24
+
25
+ function isWidth(value: unknown): value is TypeTableLayoutWidth {
26
+ return (
27
+ value === 'auto' ||
28
+ (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 2000)
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Reconciles a persisted layout with the currently eligible schema columns.
34
+ * Schema visibility and fixed regions always remain authoritative.
35
+ */
36
+ export function reconcileTableLayout(
37
+ properties: readonly ISchemaObjectExtensionField[],
38
+ profile?: ITableLayoutProfileLike | ITableLayout,
39
+ ): ITableLayout {
40
+ const entries = new Map<string, ITableLayoutColumn>();
41
+ const profileColumns = Array.isArray(profile?.columns) ? profile.columns : [];
42
+ for (const item of profileColumns) {
43
+ if (!item || typeof item !== 'object') continue;
44
+ const column = item as Record<string, unknown>;
45
+ const key = typeof column.key === 'string' ? column.key.trim() : '';
46
+ if (!key || entries.has(key)) continue;
47
+ if (typeof column.visible !== 'boolean' || !isWidth(column.width)) continue;
48
+ entries.set(key, { key, visible: column.visible, width: column.width });
49
+ }
50
+
51
+ const eligible = properties.filter(
52
+ property =>
53
+ typeof property.key === 'string' && property.key && property.key !== TableColumnIdSelection,
54
+ );
55
+ const schemaKeys = new Set(eligible.map(property => property.key!));
56
+ const regions = new Map<'left' | 'center' | 'right', ISchemaObjectExtensionField[]>();
57
+ regions.set('left', []);
58
+ regions.set('center', []);
59
+ regions.set('right', []);
60
+ for (const property of eligible) {
61
+ const fixed = property.rest?.fixed;
62
+ regions.get(fixed === 'left' ? 'left' : fixed === 'right' ? 'right' : 'center')!.push(property);
63
+ }
64
+
65
+ const result: ITableLayoutColumn[] = [];
66
+ for (const region of ['left', 'center', 'right'] as const) {
67
+ const regionProperties = regions.get(region)!;
68
+ const regionKeys = new Set(regionProperties.map(property => property.key!));
69
+ const ordered = profileColumns
70
+ .map(item =>
71
+ item && typeof item === 'object' ? (item as Record<string, unknown>) : undefined,
72
+ )
73
+ .filter(item => {
74
+ const key = typeof item?.key === 'string' ? item.key.trim() : '';
75
+ return !!key && regionKeys.has(key) && schemaKeys.has(key) && entries.get(key)?.key === key;
76
+ })
77
+ .map(item => item!.key as string);
78
+ const seen = new Set<string>();
79
+ for (const key of [...ordered, ...regionProperties.map(property => property.key!)]) {
80
+ if (seen.has(key)) continue;
81
+ seen.add(key);
82
+ const property = regionProperties.find(item => item.key === key)!;
83
+ const entry = entries.get(key);
84
+ const schemaWidth = property.rest?.width;
85
+ result.push({
86
+ key,
87
+ visible: entry?.visible ?? true,
88
+ width:
89
+ entry?.width ??
90
+ (typeof schemaWidth === 'number' && schemaWidth > 0 ? schemaWidth : 'auto'),
91
+ });
92
+ }
93
+ }
94
+ return {
95
+ version: 1,
96
+ ...(typeof profile?.schemaFingerprint === 'string'
97
+ ? { schemaFingerprint: profile.schemaFingerprint }
98
+ : {}),
99
+ columns: result,
100
+ };
101
+ }
@@ -0,0 +1,112 @@
1
+ import type { ISchemaObjectExtensionField } from 'zova-module-a-openapi';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import test from 'node:test';
5
+
6
+ import { TableColumnIdSelection } from '../../src/types/table.js';
7
+ import { reconcileTableLayout } from '../../src/types/tableLayout.js';
8
+
9
+ function createProperties(...properties: ISchemaObjectExtensionField[]) {
10
+ return properties;
11
+ }
12
+
13
+ test('table layout drops stale and malformed profile columns while preserving schema defaults', () => {
14
+ const layout = reconcileTableLayout(
15
+ createProperties(
16
+ { key: 'id', type: 'number', rest: { width: 60, fixed: 'left' } },
17
+ { key: 'name', type: 'string' },
18
+ { key: 'level', type: 'string', rest: { width: 120 } },
19
+ { key: 'operations', type: 'string', rest: { width: 360, fixed: 'right' } },
20
+ ),
21
+ {
22
+ version: 1,
23
+ schemaFingerprint: 'student-v1',
24
+ columns: [
25
+ { key: 'name', visible: false, width: 480 },
26
+ { key: 'obsolete', visible: true, width: 80 },
27
+ { key: 'level', visible: true, width: 0 },
28
+ { key: 'operations', visible: true, width: 'auto' },
29
+ { key: '', visible: false, width: 90 },
30
+ ],
31
+ },
32
+ );
33
+
34
+ assert.deepEqual(layout, {
35
+ version: 1,
36
+ schemaFingerprint: 'student-v1',
37
+ columns: [
38
+ { key: 'id', visible: true, width: 60 },
39
+ { key: 'name', visible: false, width: 480 },
40
+ { key: 'level', visible: true, width: 120 },
41
+ { key: 'operations', visible: true, width: 'auto' },
42
+ ],
43
+ });
44
+ });
45
+
46
+ test('table layout retains the first valid duplicate and appends new schema fields', () => {
47
+ const layout = reconcileTableLayout(
48
+ createProperties(
49
+ { key: 'name', type: 'string' },
50
+ { key: 'level', type: 'string' },
51
+ { key: 'createdAt', type: 'string', rest: { width: 180 } },
52
+ ),
53
+ {
54
+ columns: [
55
+ { key: 'level', visible: false, width: 240 },
56
+ { key: 'name', visible: true, width: 'auto' },
57
+ { key: 'level', visible: true, width: 300 },
58
+ ],
59
+ },
60
+ );
61
+
62
+ assert.deepEqual(layout.columns, [
63
+ { key: 'level', visible: false, width: 240 },
64
+ { key: 'name', visible: true, width: 'auto' },
65
+ { key: 'createdAt', visible: true, width: 180 },
66
+ ]);
67
+ });
68
+
69
+ test('table layout permits ordering only within schema fixed regions', () => {
70
+ const layout = reconcileTableLayout(
71
+ createProperties(
72
+ { key: 'leftFirst', type: 'string', rest: { fixed: 'left' } },
73
+ { key: 'leftSecond', type: 'string', rest: { fixed: 'left' } },
74
+ { key: 'centerFirst', type: 'string' },
75
+ { key: 'centerSecond', type: 'string' },
76
+ { key: 'rightFirst', type: 'string', rest: { fixed: 'right' } },
77
+ { key: 'rightSecond', type: 'string', rest: { fixed: 'right' } },
78
+ ),
79
+ {
80
+ columns: [
81
+ { key: 'rightSecond', visible: true, width: 'auto' },
82
+ { key: 'centerSecond', visible: true, width: 'auto' },
83
+ { key: 'leftSecond', visible: true, width: 'auto' },
84
+ { key: 'rightFirst', visible: true, width: 'auto' },
85
+ { key: 'centerFirst', visible: true, width: 'auto' },
86
+ { key: 'leftFirst', visible: true, width: 'auto' },
87
+ ],
88
+ },
89
+ );
90
+
91
+ assert.deepEqual(
92
+ layout.columns.map(column => column.key),
93
+ ['leftSecond', 'leftFirst', 'centerSecond', 'centerFirst', 'rightSecond', 'rightFirst'],
94
+ );
95
+ });
96
+
97
+ test('table layout excludes synthetic selection state from persistence', () => {
98
+ const layout = reconcileTableLayout(
99
+ createProperties(
100
+ { key: TableColumnIdSelection, type: 'boolean' },
101
+ { key: 'name', type: 'string' },
102
+ ),
103
+ {
104
+ columns: [
105
+ { key: TableColumnIdSelection, visible: false, width: 40 },
106
+ { key: 'name', visible: true, width: 'auto' },
107
+ ],
108
+ },
109
+ );
110
+
111
+ assert.deepEqual(layout.columns, [{ key: 'name', visible: true, width: 'auto' }]);
112
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-zova",
3
- "version": "5.1.160",
3
+ "version": "5.1.161",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "zova",
6
6
  "license": "MIT",
@@ -22,15 +22,15 @@
22
22
  "zova-module-a-logger": "^5.1.28",
23
23
  "zova-module-a-meta": "^5.1.22",
24
24
  "zova-module-a-model": "^5.1.36",
25
- "zova-module-a-openapi": "^5.1.53",
25
+ "zova-module-a-openapi": "^5.1.54",
26
26
  "zova-module-a-router": "^5.1.37",
27
27
  "zova-module-a-routerstack": "^5.1.29",
28
28
  "zova-module-a-routertabs": "^5.1.34",
29
- "zova-module-a-ssr": "^5.1.38",
29
+ "zova-module-a-ssr": "^5.1.39",
30
30
  "zova-module-a-ssrhmr": "^5.1.23",
31
31
  "zova-module-a-ssrserver": "^5.1.26",
32
32
  "zova-module-a-style": "^5.1.35",
33
- "zova-module-a-table": "^5.1.42",
33
+ "zova-module-a-table": "^5.1.43",
34
34
  "zova-module-a-zod": "^5.1.37",
35
35
  "zova-module-a-zova": "^5.1.104"
36
36
  },