cabloy 5.1.158 → 5.1.159

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 (88) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-contract-loop/SKILL.md +24 -7
  3. package/.claude/skills/cabloy-contract-loop/evals/evals.json +12 -0
  4. package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +28 -0
  5. package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +19 -2
  6. package/.claude/skills/cabloy-resource-field-update/SKILL.md +18 -0
  7. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +14 -0
  8. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +10 -0
  9. package/.github/workflows/docs-pages.yml +18 -2
  10. package/CHANGELOG.md +19 -0
  11. package/package.json +2 -1
  12. package/repo-docs/.vitepress/config.mjs +34 -4
  13. package/repo-docs/.vitepress/theme/components/GitHubRepositoriesNav.vue +255 -0
  14. package/repo-docs/.vitepress/theme/custom.css +21 -0
  15. package/repo-docs/.vitepress/theme/index.js +7 -1
  16. package/repo-docs/backend/resource-field-update.md +48 -1
  17. package/repo-docs/blogs/index.md +2 -0
  18. package/repo-docs/frontend/form-layout-guide.md +6 -6
  19. package/repo-docs/frontend/page-guide.md +12 -0
  20. package/repo-docs/frontend/page-meta-guide.md +27 -0
  21. package/repo-docs/frontend/ssr-architecture-overview.md +4 -0
  22. package/repo-docs/frontend/ssr-build-deploy-guide.md +1 -1
  23. package/repo-docs/frontend/zova-form-source-reading-map.md +2 -2
  24. package/repo-docs/frontend/zova-form-under-the-hood.md +1 -1
  25. package/repo-docs/frontend/zova-reactivity-under-the-hood.md +65 -9
  26. package/repo-docs/fullstack/contract-loop-playbook.md +2 -0
  27. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +198 -0
  28. package/repo-docs/fullstack/vona-zova-integration.md +2 -0
  29. package/repo-e2e/docs/playwright.config.ts +37 -0
  30. package/repo-e2e/docs/specs/blogs-index.spec.ts +180 -0
  31. package/repo-e2e/docs/specs/github-repositories-nav.spec.ts +69 -0
  32. package/repo-e2e/docs/test-results/.last-run.json +4 -0
  33. package/repo-e2e/specs/cabloy-basic.spec.ts +136 -2
  34. package/vona/packages-cli/cli/package.json +1 -1
  35. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  36. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
  37. package/vona/packages-cli/cli-set-api/package.json +1 -1
  38. package/vona/packages-vona/vona/package.json +1 -1
  39. package/vona/pnpm-lock.yaml +21 -80
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +2 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +2 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +2 -0
  45. package/vona/src/suite/a-training/modules/training-record/src/dto/recordCreate.tsx +1 -0
  46. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectReq.tsx +3 -7
  47. package/vona/src/suite/a-training/modules/training-record/src/dto/recordUpdate.tsx +1 -0
  48. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +1 -0
  49. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +8 -0
  50. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +51 -0
  51. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  52. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +1 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -0
  56. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +1 -0
  57. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +10 -1
  58. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +99 -1
  59. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  60. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  61. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +85 -57
  64. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  65. package/zova/packages-cli/cli/package.json +2 -2
  66. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  67. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +13 -3
  68. package/zova/packages-cli/cli-set-front/package.json +1 -1
  69. package/zova/packages-zova/zova/package.json +2 -2
  70. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +31 -11
  71. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -0
  72. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +1 -1
  73. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +68 -8
  74. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  75. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +4 -0
  76. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  77. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  78. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +0 -6
  79. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +16 -9
  80. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +4 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +13 -2
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +1 -0
  84. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  85. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +50 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +60 -7
  87. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +6 -4
  88. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -25,6 +25,7 @@ import type {
25
25
  export type TypePipeFilterData = ITableQuery;
26
26
 
27
27
  export type TypePipeFilterResult = TypeQueryParamsPatch;
28
+ type TypeQueryJoin = NonNullable<TypeQueryParamsPatch['joins']>[number];
28
29
 
29
30
  export interface IPipeOptionsFilter
30
31
  extends IDecoratorPipeOptions, IDecoratorPipeOptionsArgument, ValidatorOptions {}
@@ -68,10 +69,13 @@ export class PipeFilter
68
69
  private async _transform(value: TypePipeFilterData, options: IPipeOptionsFilter) {
69
70
  // 1. system: columns/where/orders/pageNo/pageSize
70
71
  const params = this._transformSystem(value);
72
+ const rootOpenapi = ZodMetadata.getOpenapiMetadata(options.schema!) as
73
+ | ISchemaObjectExtensionField
74
+ | undefined;
71
75
  // 2. fields
72
- await this._transformFields(params, value, options);
76
+ await this._transformFields(params, value, options, rootOpenapi);
73
77
  // 3. system: orders
74
- this._transformOrders(params, options);
78
+ this._transformOrders(params, options, rootOpenapi);
75
79
  // ok
76
80
  return params;
77
81
  }
@@ -104,32 +108,75 @@ export class PipeFilter
104
108
  return params;
105
109
  }
106
110
 
107
- private _transformOrders(params: TypeQueryParamsPatch, options: IPipeOptionsFilter) {
111
+ private _transformOrders(
112
+ params: TypeQueryParamsPatch,
113
+ options: IPipeOptionsFilter,
114
+ rootOpenapi: ISchemaObjectExtensionField | undefined,
115
+ ) {
108
116
  if (!params.orders) return;
109
- // openapi
110
- const openapi: ISchemaObjectExtensionField | undefined = ZodMetadata.getOpenapiMetadata(
111
- options.schema!,
112
- ) as ISchemaObjectExtensionField | undefined;
113
- const table = openapi?.filter?.table;
114
- // loop
115
117
  for (const order of params.orders) {
116
- const field = order[0] as string;
117
- if (field.includes('.')) continue;
118
- let tableCurrent = table;
119
- let fieldCurrent = field;
120
- const fieldSchema = ZodMetadata.getFieldSchema(options.schema, field);
121
- if (fieldSchema) {
122
- const openapi: ISchemaObjectExtensionField | undefined = ZodMetadata.getOpenapiMetadata(
123
- fieldSchema,
124
- ) as ISchemaObjectExtensionField | undefined;
125
- if (openapi?.filter?.table) {
126
- tableCurrent = openapi?.filter?.table;
127
- }
128
- if (openapi?.filter?.originalName) {
129
- fieldCurrent = openapi?.filter?.originalName;
130
- }
118
+ if (!Array.isArray(order) || order.length !== 2) {
119
+ throw new Error('invalid order');
120
+ }
121
+ const [key, direction] = order;
122
+ if (typeof key !== 'string' || (direction !== 'asc' && direction !== 'desc')) {
123
+ throw new Error('invalid order');
124
+ }
125
+ const info = this._resolveField(key, options, rootOpenapi);
126
+ if (!info || info.openapi?.filter?.capabilities?.order === false) {
127
+ throw new Error(`invalid order field: ${key}`);
131
128
  }
132
- cast(order)[0] = tableCurrent ? `${tableCurrent}.${fieldCurrent}` : fieldCurrent;
129
+ cast(order)[0] = info.orderFullName;
130
+ this._addJoin(params, info.joinInfo);
131
+ }
132
+ }
133
+
134
+ private _resolveField(
135
+ key: string,
136
+ options: IPipeOptionsFilter,
137
+ rootOpenapi: ISchemaObjectExtensionField | undefined,
138
+ ) {
139
+ const fieldSchema = ZodMetadata.getFieldSchema(options.schema!, key);
140
+ if (!fieldSchema) {
141
+ if (!rootOpenapi?.filter?.table) {
142
+ return { orderFullName: key };
143
+ }
144
+ const keyParts = key.split('.');
145
+ const [table, field] = keyParts;
146
+ if (!table || !field || keyParts.length !== 2) return;
147
+ const resolved = this._resolveField(field, options, rootOpenapi);
148
+ if (!resolved || resolved.orderFullName !== key) return;
149
+ return resolved;
150
+ }
151
+ const fieldSchemaInner = ZodMetadata.unwrapChained(fieldSchema);
152
+ const openapi: ISchemaObjectExtensionField | undefined = ZodMetadata.getOpenapiMetadata(
153
+ fieldSchema,
154
+ ) as ISchemaObjectExtensionField | undefined;
155
+ const originalName = openapi?.filter?.originalName ?? key;
156
+ const table = openapi?.filter?.table ?? rootOpenapi?.filter?.table;
157
+ const joinInfo = openapi?.filter?.joinOn
158
+ ? ([
159
+ openapi.filter.joinType ?? 'innerJoin',
160
+ openapi.filter.table,
161
+ openapi.filter.joinOn,
162
+ ] as TypeQueryJoin)
163
+ : undefined;
164
+ return {
165
+ fieldSchema,
166
+ fieldSchemaInner,
167
+ openapi,
168
+ originalName,
169
+ fullName: joinInfo ? `${openapi?.filter?.table}.${originalName}` : originalName,
170
+ orderFullName: table ? `${table}.${originalName}` : originalName,
171
+ joinInfo,
172
+ };
173
+ }
174
+
175
+ private _addJoin(params: TypeQueryParamsPatch, joinInfo: TypeQueryJoin | undefined) {
176
+ if (!joinInfo) return;
177
+ if (!params.joins) params.joins = [];
178
+ if (params.joins.findIndex(item => item[1] === joinInfo[1]) === -1) {
179
+ params.joins.push(joinInfo);
133
180
  }
134
181
  }
135
182
 
@@ -139,36 +186,21 @@ export class PipeFilter
139
186
  params: TypeQueryParamsPatch,
140
187
  value: any,
141
188
  options: IPipeOptionsFilter,
189
+ rootOpenapi: ISchemaObjectExtensionField | undefined,
142
190
  ) {
143
191
  if (__FieldsSystem.includes(key)) return;
144
- const fieldSchema = ZodMetadata.getFieldSchema(options.schema, key);
145
- if (!fieldSchema) return;
146
- const fieldSchemaInner = ZodMetadata.unwrapChained(fieldSchema);
147
- // openapi
148
- const openapi: ISchemaObjectExtensionField | undefined = ZodMetadata.getOpenapiMetadata(
149
- fieldSchema,
150
- ) as ISchemaObjectExtensionField | undefined;
151
- const [transformName] = openapi?.filter?.transform ?? ['a-web:base', undefined];
192
+ const info = this._resolveField(key, options, rootOpenapi);
193
+ if (!info) return;
194
+ const { fieldSchema, fieldSchemaInner, openapi, originalName, fullName, joinInfo } = info;
195
+ const [transformName, transformOptions] = openapi?.filter?.transform ?? [
196
+ 'a-web:base',
197
+ undefined,
198
+ ];
152
199
  const fieldNullable = fieldValue === null && isNullableSchema(fieldSchema);
153
200
  if (isNilOrEmptyString(fieldValue) && !fieldNullable) return;
154
- // name
155
- const originalName = openapi?.filter?.originalName ?? key;
156
- let fullName: string;
157
- // joins
158
- let joinInfo;
159
- if (openapi?.filter?.joinOn) {
160
- const joinType = openapi.filter.joinType ?? 'innerJoin';
161
- const joinTable = openapi.filter.table;
162
- const joinOn = openapi.filter.joinOn;
163
- joinInfo = [joinType, joinTable, joinOn];
164
- fullName = `${joinTable}.${originalName}`;
165
- } else {
166
- fullName = originalName;
167
- }
168
201
  // check where
169
202
  if (Object.prototype.hasOwnProperty.call(params.where, fullName)) return;
170
203
  // filter transform
171
- const [, transformOptions] = openapi?.filter?.transform ?? ['a-web:base', undefined];
172
204
  const transformOptions2 = this.bean.onion.filterTransform.getOnionOptionsDynamic(
173
205
  transformName as keyof IFilterTransformRecord,
174
206
  transformOptions,
@@ -182,7 +214,7 @@ export class PipeFilter
182
214
  if (!beanInstance.where) {
183
215
  throw new Error(`filterTransform.where not found: ${beanFullName}`);
184
216
  }
185
- const info = {
217
+ const transformInfo = {
186
218
  params,
187
219
  query: value,
188
220
  options,
@@ -194,17 +226,12 @@ export class PipeFilter
194
226
  schema: fieldSchema,
195
227
  openapi,
196
228
  };
197
- const resTransform = await beanInstance.where(info, transformOptions2);
229
+ const resTransform = await beanInstance.where(transformInfo, transformOptions2);
198
230
  if (resTransform !== undefined) {
199
231
  // where
200
232
  params.where[fullName] = resTransform;
201
233
  // join
202
- if (joinInfo) {
203
- if (!params.joins) params.joins = [];
204
- if (params.joins.findIndex(item => item[1] === joinInfo.joinTable) === -1) {
205
- params.joins.push(joinInfo);
206
- }
207
- }
234
+ this._addJoin(params, joinInfo);
208
235
  }
209
236
  }
210
237
 
@@ -212,10 +239,11 @@ export class PipeFilter
212
239
  params: TypeQueryParamsPatch,
213
240
  value: any,
214
241
  options: IPipeOptionsFilter,
242
+ rootOpenapi: ISchemaObjectExtensionField | undefined,
215
243
  ) {
216
244
  // loop
217
245
  for (const key in value) {
218
- await this._transformField(key, value[key], params, value, options);
246
+ await this._transformField(key, value[key], params, value, options, rootOpenapi);
219
247
  }
220
248
  }
221
249
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.84",
3
+ "version": "5.1.85",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli",
3
- "version": "1.2.105",
3
+ "version": "1.2.106",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@cabloy/process-helper": "^3.1.8",
45
45
  "fs-extra": "^11.3.5",
46
46
  "semver": "^7.6.2",
47
- "zova-cli-set-front": "^1.2.103"
47
+ "zova-cli-set-front": "^1.2.104"
48
48
  },
49
49
  "devDependencies": {
50
50
  "clean-package": "^2.2.0",
@@ -17,6 +17,7 @@ import {
17
17
  schemaRenderTableActionRowJsx,
18
18
  } from './component.ts';
19
19
  import {
20
+ schemaRenderColumn,
20
21
  schemaRenderDisableNotifyChanged,
21
22
  schemaRenderFieldSource,
22
23
  schemaRenderLayout,
@@ -35,6 +36,7 @@ export const ZovaRender = {
35
36
  disableNotifyChanged: schemaRenderDisableNotifyChanged,
36
37
  onEffect: schemaRenderOnEffect,
37
38
  fieldSource: schemaRenderFieldSource,
39
+ column: schemaRenderColumn,
38
40
  // component
39
41
  field: schemaRenderField,
40
42
  fieldJsx: schemaRenderFieldJsx,
@@ -1,8 +1,12 @@
1
1
  import type z from 'zod';
2
2
 
3
- import type { TypeFormSchemaScene, TypeFormFieldOnEffect } from 'zova-module-a-openapi';
4
- import type { IResourceFormFieldLayoutOptions } from 'zova-module-a-openapi';
5
- import type { TypeSchemaScene } from 'zova-module-a-openapi';
3
+ import type {
4
+ IResourceFormFieldLayoutOptions,
5
+ ITableColumnOptions,
6
+ TypeFormFieldOnEffect,
7
+ TypeFormSchemaScene,
8
+ TypeSchemaScene,
9
+ } from 'zova-module-a-openapi';
6
10
  import type { TypeSchemaOrderLevel } from 'zova-module-a-openapi';
7
11
 
8
12
  import { _generalSchemaRest, _order } from './inner.ts';
@@ -78,3 +82,9 @@ export function schemaRenderOrder<T extends z.ZodType>(
78
82
  return _generalSchemaRest(schema, options, scene);
79
83
  };
80
84
  }
85
+
86
+ export function schemaRenderColumn<T extends z.ZodType>(options: ITableColumnOptions) {
87
+ return function (schema: T): T {
88
+ return schema.openapi({ rest: { table: options } });
89
+ };
90
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli-set-front",
3
- "version": "1.2.103",
3
+ "version": "1.2.104",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli-set-front",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.155",
3
+ "version": "5.1.156",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.88",
49
- "zova-suite-a-zova": "^5.1.154"
49
+ "zova-suite-a-zova": "^5.1.155"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -1,3 +1,4 @@
1
+ import type { SortingState } from '@tanstack/vue-table';
1
2
  import type { IComponentOptions } from 'zova';
2
3
  import type {
3
4
  IJsxRenderContextPage,
@@ -10,6 +11,7 @@ import type {
10
11
  } from 'zova-module-a-openapi';
11
12
 
12
13
  import { celEnvBase } from '@cabloy/utils';
14
+ import { functionalUpdate } from '@tanstack/vue-table';
13
15
  import { VNode } from 'vue';
14
16
  import { BeanControllerBase, deepEqual } from 'zova';
15
17
  import { ZovaJsx } from 'zova-jsx';
@@ -49,8 +51,9 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
49
51
 
50
52
  queryFixedData: ITableQuery;
51
53
  queryFilterData: ITableQuery;
54
+ querySortingData: ITableQuery;
55
+ sorting: SortingState;
52
56
  queryPaged: ITablePaged;
53
- query: ITableQuery;
54
57
 
55
58
  $$modelResource: ModelResource<TData>;
56
59
 
@@ -65,6 +68,8 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
65
68
  // query
66
69
  this.queryFixedData = this.$props.queryFixed ?? {};
67
70
  this.queryFilterData = {};
71
+ this.querySortingData = {};
72
+ this.sorting = [];
68
73
  this.$watch(
69
74
  () => this.$props.queryFixed,
70
75
  queryFixed => {
@@ -72,16 +77,6 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
72
77
  },
73
78
  );
74
79
  this.queryPaged = { pageNo: 1, pageSize: this.$props.pageSize };
75
- this.query = this.$computed(() => {
76
- const { where: whereFixed, ...queryFixed } = this.queryFixedData;
77
- const { where: whereFilter, ...queryFilter } = this.queryFilterData;
78
- return {
79
- ...queryFixed,
80
- ...queryFilter,
81
- where: { ...whereFilter, ...whereFixed },
82
- ...this.queryPaged,
83
- };
84
- });
85
80
  // load schema/data
86
81
  await $QueriesEnsureLoaded(
87
82
  () => this.$$modelResource.apiSchemasSelect.sdk,
@@ -101,6 +96,18 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
101
96
  return this.$props.resource;
102
97
  }
103
98
 
99
+ get query() {
100
+ const { where: whereFixed, ...queryFixed } = this.queryFixedData;
101
+ const { where: whereFilter, ...queryFilter } = this.queryFilterData;
102
+ return {
103
+ ...queryFixed,
104
+ ...queryFilter,
105
+ ...this.querySortingData,
106
+ where: { ...whereFilter, ...whereFixed },
107
+ ...this.queryPaged,
108
+ };
109
+ }
110
+
104
111
  get queryData() {
105
112
  return this.$$modelResource.select(this.query);
106
113
  }
@@ -117,6 +124,10 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
117
124
  return this.$$modelResource.schemaFilter;
118
125
  }
119
126
 
127
+ get schemaOrder() {
128
+ return this.$$modelResource.schemaOrder;
129
+ }
130
+
120
131
  get schemaRow() {
121
132
  return this.$$modelResource.schemaRow;
122
133
  }
@@ -147,6 +158,15 @@ export class ControllerBlockPage<TData extends {} = {}> extends BeanControllerBa
147
158
  this.queryPaged.pageNo = 1;
148
159
  }
149
160
 
161
+ onSortingChange(updater: SortingState | ((old: SortingState) => SortingState)) {
162
+ this.sorting = functionalUpdate(updater, this.sorting).slice(0, 1);
163
+ const sorting = this.sorting[0];
164
+ this.querySortingData = sorting
165
+ ? { orders: [[sorting.id, sorting.desc ? 'desc' : 'asc']] }
166
+ : {};
167
+ this.queryPaged.pageNo = 1;
168
+ }
169
+
150
170
  public setTableRef(tableRef: BeanControllerTableBase<TData> | undefined) {
151
171
  this.tableRef = tableRef;
152
172
  if (!tableRef || !this._tableMetaRefreshPending) return;
@@ -44,6 +44,9 @@ export class ControllerBlockTable<TData extends {} = {}> extends BeanControllerB
44
44
  }}
45
45
  data={$$page.data as unknown as TData[]}
46
46
  schema={$$page.schemaRow}
47
+ schemaOrder={$$page.schemaOrder}
48
+ sorting={$$page.sorting}
49
+ onSortingChange={updater => $$page.onSortingChange(updater)}
47
50
  tableScope={$$page.jsxCelScope}
48
51
  ></ZTable>
49
52
  </div>
@@ -167,7 +167,7 @@ export class ControllerBlockPageEntry<TData extends {} = {}> extends BeanControl
167
167
 
168
168
  setPageMeta(data: any | undefined, pageDirty?: boolean) {
169
169
  if (!this.$pageRoute) return;
170
- const pageTitle = data?.[this.$props.pageTitleKey];
170
+ const pageTitle = data?.[this.$props.pageTitleKey] || this.formSchema?.title;
171
171
  this.$router.setPageMeta(this.$pageRoute, { pageTitle, pageDirty, formMeta: this.formMeta });
172
172
  }
173
173
 
@@ -1,3 +1,5 @@
1
+ import type { AriaAttributes, CSSProperties } from 'vue';
2
+
1
3
  import { FlexRender } from '@tanstack/vue-table';
2
4
  import { BeanRenderBase } from 'zova';
3
5
  import { Render } from 'zova-module-a-bean';
@@ -8,7 +10,14 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
8
10
  public render() {
9
11
  return (
10
12
  <ZTable
11
- {...this.$props}
13
+ data={this.$props.data}
14
+ schema={this.$props.schema}
15
+ schemaOrder={this.$props.schemaOrder}
16
+ sorting={this.$props.sorting}
17
+ onSortingChange={this.$props.onSortingChange}
18
+ tableScope={this.$props.tableScope}
19
+ getColumns={this.$props.getColumns}
20
+ getRowId={this.$props.getRowId}
12
21
  controllerRef={ref => {
13
22
  this.$props?.tableRef?.(ref);
14
23
  }}
@@ -19,6 +28,21 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
19
28
  );
20
29
  }
21
30
 
31
+ private _getColumnStyle(column: any): CSSProperties {
32
+ const rest = (column.columnDef.meta as any)?.rest;
33
+ const fixed = column.getIsPinned();
34
+ const width = rest?.width ? `${rest.width}px` : undefined;
35
+ return {
36
+ width,
37
+ minWidth: width,
38
+ textAlign: rest?.align,
39
+ position: fixed ? ('sticky' as const) : undefined,
40
+ left: fixed === 'left' ? `${column.getStart('left')}px` : undefined,
41
+ right: fixed === 'right' ? `${column.getAfter('right')}px` : undefined,
42
+ zIndex: fixed ? 1 : undefined,
43
+ };
44
+ }
45
+
22
46
  public _renderTable($$table: ControllerTable<TData>) {
23
47
  const table = $$table.table;
24
48
  return (
@@ -26,12 +50,43 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
26
50
  <thead>
27
51
  <tr>
28
52
  {table.getFlatHeaders().map(header => {
53
+ const column = header.column;
54
+ const canSort = column.getCanSort();
55
+ const sorted = column.getIsSorted();
56
+ const fixed = column.getIsPinned();
57
+ const headerDefinition = column.columnDef.header;
58
+ const headerContent = (
59
+ <FlexRender render={headerDefinition} props={header.getContext()}></FlexRender>
60
+ );
61
+ let sortAria: AriaAttributes['aria-sort'] = 'none';
62
+ let sortIcon: '▲' | '▼' | '↕' = '↕';
63
+ if (sorted === 'asc') {
64
+ sortAria = 'ascending';
65
+ sortIcon = '▲';
66
+ } else if (sorted === 'desc') {
67
+ sortAria = 'descending';
68
+ sortIcon = '▼';
69
+ }
29
70
  return (
30
- <th key={header.id}>
31
- <FlexRender
32
- render={header.column.columnDef.header}
33
- props={header.getContext()}
34
- ></FlexRender>
71
+ <th
72
+ key={header.id}
73
+ class={fixed ? 'bg-base-100' : undefined}
74
+ style={this._getColumnStyle(column)}
75
+ aria-sort={sortAria}
76
+ >
77
+ {canSort ? (
78
+ <button
79
+ type="button"
80
+ class="inline-flex w-full items-center gap-1"
81
+ onClick={column.getToggleSortingHandler()}
82
+ aria-label={`Sort by ${column.id}`}
83
+ >
84
+ {headerContent}
85
+ <span aria-hidden="true">{sortIcon}</span>
86
+ </button>
87
+ ) : (
88
+ headerContent
89
+ )}
35
90
  </th>
36
91
  );
37
92
  })}
@@ -42,10 +97,15 @@ export class RenderTable<TData extends {} = {}> extends BeanRenderBase {
42
97
  return (
43
98
  <tr key={row.id}>
44
99
  {row.getVisibleCells().map(cell => {
100
+ const column = cell.column;
45
101
  return (
46
- <td key={cell.id}>
102
+ <td
103
+ key={cell.id}
104
+ class={column.getIsPinned() ? 'bg-base-100' : undefined}
105
+ style={this._getColumnStyle(column)}
106
+ >
47
107
  <FlexRender
48
- render={cell.column.columnDef.cell}
108
+ render={column.columnDef.cell}
49
109
  props={cell.getContext()}
50
110
  ></FlexRender>
51
111
  </td>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-rest-resource",
3
- "version": "5.1.49",
3
+ "version": "5.1.50",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "keywords": [
@@ -57,6 +57,7 @@ export class ModelResource<
57
57
  public schemaCreate?: ISchemaObjectExtensionField;
58
58
  public schemaUpdate?: ISchemaObjectExtensionField;
59
59
  public schemaFilter?: ISchemaObjectExtensionField;
60
+ public schemaOrder?: ISchemaObjectExtensionField;
60
61
  public schemaRow?: ISchemaObjectExtensionField;
61
62
  public schemaPages?: ISchemaObjectExtensionField;
62
63
 
@@ -87,6 +88,9 @@ export class ModelResource<
87
88
  this.schemaFilter = this.$computed(() => {
88
89
  return this.apiSchemasSelect.filter;
89
90
  });
91
+ this.schemaOrder = this.$computed(() => {
92
+ return this.apiSchemasSelect.order;
93
+ });
90
94
  this.schemaRow = this.$computed(() => {
91
95
  return this.apiSchemasSelect.row;
92
96
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-cabloy",
3
- "version": "5.1.50",
3
+ "version": "5.1.51",
4
4
  "gitHead": "4dece229b6d3f4735930e833afcb4b95ec51bf86",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,7 +8,7 @@
8
8
  "type": "module",
9
9
  "dependencies": {
10
10
  "zova-module-a-rbac": "^5.0.1",
11
- "zova-module-rest-resource": "^5.1.49"
11
+ "zova-module-rest-resource": "^5.1.50"
12
12
  },
13
13
  "title": "a-cabloy"
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-form",
3
- "version": "5.1.50",
3
+ "version": "5.1.51",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -58,12 +58,6 @@ export function resolveFormLayout(
58
58
  ),
59
59
  )
60
60
  .filter(Boolean) as IResolvedFormLayoutNode[];
61
- for (const property of properties ?? []) {
62
- const name = property.key;
63
- if (!name || !propertyNames.has(name) || fieldNames.has(name)) continue;
64
- fieldNames.add(name);
65
- children.push({ type: 'field', name });
66
- }
67
61
  return { children, fieldTabPaths, diagnostics };
68
62
  }
69
63
 
@@ -76,6 +76,19 @@ test('Form Layout accepts every preserved alias for the same canonical source',
76
76
  assert.deepEqual(resolved.diagnostics, []);
77
77
  });
78
78
 
79
+ test('Form Layout renders only explicitly declared visible fields', () => {
80
+ const resolved = resolveFormLayout(
81
+ createTabsLayout('name'),
82
+ createProperties({ key: 'name', type: 'string' }, { key: 'level', type: 'string' }),
83
+ );
84
+
85
+ assert.deepEqual(getTabFieldNames(resolved), ['name']);
86
+ assert.deepEqual(resolved.fieldTabPaths, {
87
+ name: [{ tabsId: 'tabs-0', tabId: 'tab-0-0' }],
88
+ });
89
+ assert.deepEqual(resolved.diagnostics, []);
90
+ });
91
+
79
92
  test('Form Layout prefers an exact canonical field over a colliding alias', () => {
80
93
  const resolved = resolveFormLayout(
81
94
  { children: [{ type: 'field', name: 'content' }] },
@@ -89,10 +102,7 @@ test('Form Layout prefers an exact canonical field over a colliding alias', () =
89
102
  ),
90
103
  );
91
104
 
92
- assert.deepEqual(resolved.children, [
93
- { type: 'field', name: 'content' },
94
- { type: 'field', name: 'content.descriptionMarkdown' },
95
- ]);
105
+ assert.deepEqual(resolved.children, [{ type: 'field', name: 'content' }]);
96
106
  assert.deepEqual(resolved.diagnostics, []);
97
107
  });
98
108
 
@@ -131,17 +141,14 @@ test('Form Layout keeps ambiguous aliases and relation prefixes unknown', () =>
131
141
  ),
132
142
  );
133
143
 
134
- assert.deepEqual(resolved.children, [
135
- { type: 'field', name: 'content.descriptionMarkdown' },
136
- { type: 'field', name: 'content.descriptionHtml' },
137
- ]);
144
+ assert.deepEqual(resolved.children, []);
138
145
  assert.deepEqual(resolved.diagnostics, [
139
146
  { type: 'unknownField', value: '_content' },
140
147
  { type: 'unknownField', value: 'content' },
141
148
  ]);
142
149
  });
143
150
 
144
- test('Form Layout does not resolve or append invisible aliases', () => {
151
+ test('Form Layout does not resolve invisible aliases', () => {
145
152
  const resolved = resolveFormLayout(
146
153
  { children: [{ type: 'field', name: '_descriptionMarkdown' }] },
147
154
  createProperties({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-openapi",
3
- "version": "5.1.51",
3
+ "version": "5.1.52",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -183,6 +183,10 @@ export class ModelSdk extends BeanModelBase {
183
183
  const operationObject = sdk.data?.operationObject;
184
184
  return getSchemaOfRequestQueryFilter(operationObject, { where: true });
185
185
  },
186
+ get order() {
187
+ const operationObject = sdk.data?.operationObject;
188
+ return getSchemaOfRequestQueryFilter(operationObject, { order: true });
189
+ },
186
190
  get requestBody() {
187
191
  const operationObject = sdk.data?.operationObject;
188
192
  const schemaBody = getSchemaOfRequestBody(operationObject);