cabloy 5.1.107 → 5.1.109

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 (48) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/cabloy-docs/.vitepress/config.mjs +6 -0
  4. package/cabloy-docs/backend/multi-instance-and-instance-resolution.md +18 -0
  5. package/cabloy-docs/backend/runtime-and-flavors.md +2 -2
  6. package/cabloy-docs/backend/scripts.md +3 -2
  7. package/cabloy-docs/frontend/ssr-env.md +20 -0
  8. package/cabloy-docs/frontend/table-resource-crud-cookbook.md +33 -1
  9. package/cabloy-docs/fullstack/deploy-cloudflare-docker.md +136 -0
  10. package/package.json +1 -1
  11. package/vona/docker-compose-original/config/nginx/conf.d/nginx.conf +1 -1
  12. package/vona/env/.env +1 -1
  13. package/vona/pnpm-lock.yaml +16 -16
  14. package/vona/src/backend/config/config/config.ts +1 -1
  15. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +2 -0
  16. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +2 -0
  17. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +50 -1
  18. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +21 -1
  19. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +47 -1
  20. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +47 -1
  21. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +60 -2
  22. package/zova/packages-zova/zova/package.json +2 -2
  23. package/zova/pnpm-lock.yaml +27 -2
  24. package/zova/src/suite/cabloy-basic/modules/basic-form/src/.metadata/component/blockFormLayout.ts +31 -0
  25. package/zova/src/suite/cabloy-basic/modules/basic-form/src/.metadata/index.ts +13 -0
  26. package/zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx +165 -0
  27. package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/component/blockFilterActions.ts +31 -0
  28. package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/index.ts +13 -0
  29. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockFilter/controller.tsx +61 -31
  30. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockFilterActions/controller.tsx +51 -0
  31. package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +10 -0
  32. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +9 -2
  33. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +5 -3
  34. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +44 -3
  35. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/render.tsx +22 -4
  36. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +3 -2
  37. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +203 -0
  38. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/index.ts +1 -0
  39. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts +7 -4
  40. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formLayout.ts +59 -0
  41. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/index.ts +1 -0
  42. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  43. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +1 -1
  44. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts +53 -0
  45. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/index.ts +1 -0
  46. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  47. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +1 -1
  48. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -4,12 +4,16 @@ import type {
4
4
  IJsxRenderContextPage,
5
5
  IResourceBlockOptionsBase,
6
6
  IResourceFormFieldLayoutOptions,
7
+ IResourceRenderBlockOptionsBlock,
7
8
  } from 'zova-module-a-openapi';
8
9
 
9
10
  import { isNilOrEmptyString } from '@cabloy/utils';
10
- import { BeanControllerBase, Use } from 'zova';
11
+ import { VNode } from 'vue';
12
+ import { BeanControllerBase, objectAssignReactive, Use } from 'zova';
11
13
  import { Controller } from 'zova-module-a-bean';
12
- import { TypeFormOnSubmitData, ZForm } from 'zova-module-a-form';
14
+ import { ControllerForm, TypeFormOnSubmitData, ZForm } from 'zova-module-a-form';
15
+
16
+ import { IPageFilterScope } from '../../types/page.js';
13
17
 
14
18
  declare module 'zova-module-a-openapi' {
15
19
  export interface IResourceBlockRecord {
@@ -17,22 +21,29 @@ declare module 'zova-module-a-openapi' {
17
21
  }
18
22
  }
19
23
 
20
- export interface ControllerBlockFilterProps extends IResourceBlockOptionsBase {}
24
+ export interface ControllerBlockFilterProps extends IResourceBlockOptionsBase {
25
+ blocks?: IResourceRenderBlockOptionsBlock[];
26
+ formFieldLayout?: IResourceFormFieldLayoutOptions;
27
+ }
21
28
 
22
29
  @Controller()
23
30
  export class ControllerBlockFilter extends BeanControllerBase {
24
- static $propsDefault = {};
31
+ static $propsDefault = {
32
+ formFieldLayout: { inline: true },
33
+ };
34
+
25
35
  static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
26
36
 
27
37
  formMeta: IFormMeta;
28
- formFieldLayout: IResourceFormFieldLayoutOptions;
38
+ formRef: ControllerForm | undefined;
39
+ formScope: IPageFilterScope;
29
40
 
30
41
  @Use({ injectionScope: 'host' })
31
42
  $$renderContext: IJsxRenderContextPage;
32
43
 
33
44
  protected async __init__() {
34
45
  this.formMeta = { formMode: 'edit' };
35
- this.formFieldLayout = { inline: true };
46
+ this.formScope = this._prepareFormScope();
36
47
  }
37
48
 
38
49
  get schemaFilter() {
@@ -48,6 +59,16 @@ export class ControllerBlockFilter extends BeanControllerBase {
48
59
  this._onFilter(data);
49
60
  }
50
61
 
62
+ public submitFilter() {
63
+ return this.formRef?.submit() ?? Promise.resolve(false);
64
+ }
65
+
66
+ public resetFilter() {
67
+ if (!this.formRef) return;
68
+ const data = this.formRef.reset();
69
+ this.resetData(data);
70
+ }
71
+
51
72
  _onFilter(dataOld: any) {
52
73
  const { $$page } = this.$$renderContext;
53
74
  const dataNew = {};
@@ -62,40 +83,49 @@ export class ControllerBlockFilter extends BeanControllerBase {
62
83
 
63
84
  protected render() {
64
85
  const { $$page } = this.$$renderContext;
86
+ const blocks = this.$props.blocks;
87
+ const hasBlocks = !!blocks && blocks.length > 0;
88
+ const formFieldLayout = this.$props.formFieldLayout;
65
89
  return (
66
90
  <ZForm
67
91
  class={this.$props.class}
68
- inline={true}
92
+ controllerRef={ref => {
93
+ this.formRef = ref;
94
+ }}
69
95
  data={$$page.queryFilterData}
70
96
  schema={this.schemaFilter}
71
97
  schemaScene="filter"
72
98
  formMeta={this.formMeta}
73
- formFieldLayout={this.formFieldLayout}
99
+ formFieldLayout={formFieldLayout}
100
+ blocks={blocks}
101
+ formScope={this.formScope}
74
102
  onSubmitData={data => this.submitData(data as never)}
75
- slotFooter={$$form => {
76
- return (
77
- <>
78
- <button
79
- class="btn btn-primary"
80
- onClick={() => {
81
- $$form.submit();
82
- }}
83
- >
84
- {this.scope.locale.Search()}
85
- </button>
86
- <button
87
- class="btn btn-warning"
88
- onClick={() => {
89
- const data = $$form.reset();
90
- this.resetData(data);
91
- }}
92
- >
93
- {this.scope.locale.Reset()}
94
- </button>
95
- </>
96
- );
97
- }}
103
+ slotFooter={hasBlocks ? undefined : $$form => this._renderActions($$form)}
98
104
  ></ZForm>
99
105
  );
100
106
  }
107
+
108
+ private _prepareFormScope(): IPageFilterScope {
109
+ // eslint-disable-next-line
110
+ const self = this;
111
+ const $$filter = this.$customRef(() => {
112
+ return {
113
+ get() {
114
+ return self;
115
+ },
116
+ set(_value) {},
117
+ };
118
+ }) as any;
119
+ return objectAssignReactive({}, this.$$renderContext.$$page.jsxCelScope, { $$filter });
120
+ }
121
+
122
+ private _renderActions($$form: ControllerForm): VNode {
123
+ const jsxRenderContext = $$form.getFormJsxRenderContext(this.formScope);
124
+ return $$form.zovaJsx.render(
125
+ 'basic-page:blockFilterActions',
126
+ {},
127
+ this.formScope,
128
+ jsxRenderContext,
129
+ ) as VNode;
130
+ }
101
131
  }
@@ -0,0 +1,51 @@
1
+ import type { IComponentOptions } from 'zova';
2
+ import type { IJsxRenderContextForm } from 'zova-module-a-form';
3
+ import type { IResourceBlockOptionsBase } from 'zova-module-a-openapi';
4
+
5
+ import { BeanControllerBase, Use } from 'zova';
6
+ import { Controller } from 'zova-module-a-bean';
7
+
8
+ declare module 'zova-module-a-openapi' {
9
+ export interface IResourceBlockRecord {
10
+ 'basic-page:blockFilterActions'?: ControllerBlockFilterActionsProps;
11
+ }
12
+ }
13
+
14
+ export interface ControllerBlockFilterActionsProps extends IResourceBlockOptionsBase {}
15
+
16
+ @Controller()
17
+ export class ControllerBlockFilterActions extends BeanControllerBase {
18
+ static $propsDefault = {};
19
+ static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
20
+
21
+ @Use({ injectionScope: 'host' })
22
+ $$renderContext: IJsxRenderContextForm;
23
+
24
+ protected async __init__() {}
25
+
26
+ protected render() {
27
+ const { $$filter } = this.$$renderContext.$celScope;
28
+ return (
29
+ <div class={this.$props.class}>
30
+ <button
31
+ class="btn btn-primary"
32
+ type="button"
33
+ onClick={() => {
34
+ $$filter?.submitFilter();
35
+ }}
36
+ >
37
+ {this.scope.locale.Search()}
38
+ </button>
39
+ <button
40
+ class="btn btn-warning"
41
+ type="button"
42
+ onClick={() => {
43
+ $$filter?.resetFilter();
44
+ }}
45
+ >
46
+ {this.scope.locale.Reset()}
47
+ </button>
48
+ </div>
49
+ );
50
+ }
51
+ }
@@ -1,15 +1,25 @@
1
1
  import type { IPageScope } from 'zova-module-a-openapi';
2
2
 
3
+ import 'zova-module-a-form';
3
4
  import 'zova-module-a-table';
4
5
  import 'zova-module-a-openapi';
6
+ import { ControllerBlockFilter } from '../component/blockFilter/controller.jsx';
5
7
  import { ControllerBlockPage } from '../component/blockPage/controller.jsx';
6
8
 
9
+ export interface IPageFilterScope extends IPageScope {
10
+ $$filter?: ControllerBlockFilter;
11
+ }
12
+
7
13
  declare module 'zova-module-a-openapi' {
8
14
  export interface IJsxRenderContextPage<TData extends {} = {}> {
9
15
  $$page: ControllerBlockPage<TData>;
10
16
  }
11
17
  }
12
18
 
19
+ declare module 'zova-module-a-form' {
20
+ export interface IFormScope extends IPageScope, IPageFilterScope {}
21
+ }
22
+
13
23
  declare module 'zova-module-a-table' {
14
24
  export interface ITableScope extends IPageScope {}
15
25
  }
@@ -1,5 +1,9 @@
1
1
  import type { IComponentOptions } from 'zova';
2
- import type { IJsxRenderContextPageEntry, IResourceBlockOptionsBase } from 'zova-module-a-openapi';
2
+ import type {
3
+ IJsxRenderContextPageEntry,
4
+ IResourceBlockOptionsBase,
5
+ IResourceRenderBlockOptionsBlock,
6
+ } from 'zova-module-a-openapi';
3
7
 
4
8
  import { BeanControllerBase, Use } from 'zova';
5
9
  import { Controller } from 'zova-module-a-bean';
@@ -11,7 +15,9 @@ declare module 'zova-module-a-openapi' {
11
15
  }
12
16
  }
13
17
 
14
- export interface ControllerBlockFormProps extends IResourceBlockOptionsBase {}
18
+ export interface ControllerBlockFormProps extends IResourceBlockOptionsBase {
19
+ blocks?: IResourceRenderBlockOptionsBlock[];
20
+ }
15
21
 
16
22
  @Controller()
17
23
  export class ControllerBlockForm extends BeanControllerBase {
@@ -40,6 +46,7 @@ export class ControllerBlockForm extends BeanControllerBase {
40
46
  schemaScene={$$pageEntry.schemaScene}
41
47
  formMeta={$$pageEntry.formMeta}
42
48
  formProvider={$$pageEntry.formProvider}
49
+ blocks={this.$props.blocks}
43
50
  formScope={$$pageEntry.jsxCelScope}
44
51
  onSubmitData={data => $$pageEntry.submitData(data)}
45
52
  onShowError={async ({ error }) => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-form",
3
- "version": "5.1.43",
3
+ "version": "5.1.44",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "",
6
6
  "keywords": [
@@ -34,7 +34,8 @@
34
34
  "postpack": "clean-package restore"
35
35
  },
36
36
  "dependencies": {
37
- "@tanstack/vue-form": "^1.32.0"
37
+ "@tanstack/vue-form": "^1.33.2",
38
+ "@tanstack/vue-store": "^0.11.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "clean-package": "^2.2.0",
@@ -54,7 +55,8 @@
54
55
  "title": "a-form",
55
56
  "zovaModule": {
56
57
  "globalDependencies": {
57
- "@tanstack/vue-form": true
58
+ "@tanstack/vue-form": true,
59
+ "@tanstack/vue-store": true
58
60
  },
59
61
  "bundle": {
60
62
  "vendors": [
@@ -7,10 +7,10 @@ import {
7
7
  getBy,
8
8
  isGlobalFormValidationError,
9
9
  revalidateLogic,
10
- useStore,
11
10
  ValidationCause,
12
11
  ValidationError,
13
12
  } from '@tanstack/vue-form';
13
+ import { useSelector } from '@tanstack/vue-store';
14
14
  import { SchemaObject } from 'openapi3-ts/oas31';
15
15
  import { VNode } from 'vue';
16
16
  import { z } from 'zod';
@@ -22,6 +22,7 @@ import {
22
22
  IFormMeta,
23
23
  IFormProvider,
24
24
  IResourceFormFieldLayoutOptions,
25
+ IResourceRenderBlockOptionsBlock,
25
26
  ISchemaObjectExtensionField,
26
27
  renderFormFieldTopPropsSystem,
27
28
  ScopeModuleAOpenapi,
@@ -30,6 +31,8 @@ import {
30
31
  TypeFormSchemaScene,
31
32
  } from 'zova-module-a-openapi';
32
33
 
34
+ import type { IResolvedFormLayoutNode, IResolvedFormLayoutTab } from '../../types/formLayout.js';
35
+
33
36
  import { BeanControllerFormBase } from '../../lib/beanControllerFormBase.js';
34
37
  import {
35
38
  IFormScope,
@@ -43,13 +46,13 @@ import {
43
46
  IFormFieldOptions,
44
47
  IFormFieldRenderContextPropsBucket,
45
48
  IFormFieldScope,
49
+ IJsxRenderContextForm,
46
50
  IJsxRenderContextFormField,
47
51
  } from '../../types/formField.js';
48
52
  import { ControllerFormField } from '../formField/controller.jsx';
49
53
 
50
54
  export interface ControllerFormProps<TFormData extends {} = {}, TSubmitMeta = never> {
51
55
  formTag?: string;
52
- inline?: boolean;
53
56
  data?: TFormData;
54
57
  schema?: SchemaObject;
55
58
  schemaScene?: TypeFormSchemaScene;
@@ -60,6 +63,7 @@ export interface ControllerFormProps<TFormData extends {} = {}, TSubmitMeta = ne
60
63
  formProvider?: IFormProvider;
61
64
  formScope?: IFormScope;
62
65
  formFieldLayout?: IResourceFormFieldLayoutOptions;
66
+ blocks?: IResourceRenderBlockOptionsBlock[];
63
67
  onFormSubmit?: (e: SubmitEvent, form: ControllerForm<TFormData, TSubmitMeta>) => any;
64
68
  onSubmitInvalid?: TypeFormOnSubmitInvalid<TFormData, TSubmitMeta>;
65
69
  onSubmitData?: TypeFormOnSubmit<TFormData, TSubmitMeta>;
@@ -94,7 +98,7 @@ export class ControllerForm<
94
98
  protected async __init__() {
95
99
  this.bean._setBean('$$form', this);
96
100
  this.form = this._createForm();
97
- this.formState = useStore(this.form.store, state => state) as any;
101
+ this.formState = useSelector(this.form.store, state => state) as any;
98
102
  this.formProvider = this.$computed(() => {
99
103
  const formProvider = this.$$scopeOpenapi.config.formProvider;
100
104
  return this.$props.formProvider
@@ -143,6 +147,14 @@ export class ControllerForm<
143
147
  return this.$props.formMeta;
144
148
  }
145
149
 
150
+ public hasErrors(node: IResolvedFormLayoutNode | IResolvedFormLayoutTab) {
151
+ return this.getErrorFieldCount(node) > 0;
152
+ }
153
+
154
+ public getErrorFieldCount(node: IResolvedFormLayoutNode | IResolvedFormLayoutTab) {
155
+ return this._getFormLayoutErrorFieldCount(node);
156
+ }
157
+
146
158
  public getFieldValue<K extends DeepKeys<TFormData>>(name: K) {
147
159
  return getBy(this.formState.values, name) ?? null;
148
160
  }
@@ -195,6 +207,12 @@ export class ControllerForm<
195
207
  });
196
208
  }
197
209
 
210
+ public getFormScope(scopeExtra?: {}): IFormScope {
211
+ return objectAssignReactive({}, this.$props.formScope, {
212
+ ...scopeExtra,
213
+ });
214
+ }
215
+
198
216
  public getFieldJsxRenderContext(
199
217
  $$formField: ControllerFormField<TFormData> | undefined,
200
218
  celScope: IFormFieldScope<TFormData>,
@@ -211,6 +229,20 @@ export class ControllerForm<
211
229
  };
212
230
  }
213
231
 
232
+ public getFormJsxRenderContext(
233
+ celScope: IFormScope,
234
+ ): IJsxRenderContextForm<TFormData, TSubmitMeta> {
235
+ return {
236
+ app: this.app,
237
+ ctx: this.ctx,
238
+ $scene: 'form',
239
+ $host: this,
240
+ $celScope: celScope,
241
+ $jsx: this.zovaJsx,
242
+ $$form: this,
243
+ };
244
+ }
245
+
214
246
  public getFieldComponentPropsTop<K extends DeepKeys<TFormData>>(
215
247
  name: K,
216
248
  celScope: IFormFieldScope<TFormData>,
@@ -375,6 +407,15 @@ export class ControllerForm<
375
407
  return typeof renderProvider === 'string' && renderProvider.includes(':formField');
376
408
  }
377
409
 
410
+ private _getFormLayoutErrorFieldCount(node: IResolvedFormLayoutNode | IResolvedFormLayoutTab) {
411
+ if (node.type === 'field') {
412
+ return this.formState.fieldMeta[node.name]?.errors?.length ? 1 : 0;
413
+ }
414
+ return node.children.reduce((count, child) => {
415
+ return count + this._getFormLayoutErrorFieldCount(child);
416
+ }, 0);
417
+ }
418
+
378
419
  private _handleError422(error: Error, cause: ValidationCause = 'submit') {
379
420
  const formApi = this.form;
380
421
 
@@ -33,13 +33,34 @@ export class RenderForm extends BeanRenderBase {
33
33
  return children;
34
34
  }
35
35
 
36
+ private _renderFromBlocks() {
37
+ const blocks = this.$props.blocks;
38
+ if (!blocks || blocks.length === 0) {
39
+ return this._renderSchema();
40
+ }
41
+ const celScope = this.getFormScope();
42
+ const jsxRenderContext = this.getFormJsxRenderContext(celScope);
43
+ const domBlocks: VNode[] = [];
44
+ blocks.forEach((block, index) => {
45
+ const options = Object.assign({ key: index }, block.options);
46
+ const domBlock = this.zovaJsx.render(block.render!, options, celScope, jsxRenderContext);
47
+ if (!domBlock) return;
48
+ if (Array.isArray(domBlock)) {
49
+ domBlocks.push(...domBlock);
50
+ } else {
51
+ domBlocks.push(domBlock);
52
+ }
53
+ });
54
+ return domBlocks;
55
+ }
56
+
36
57
  private _renderBodyInner() {
37
58
  const FormTag = this.$props.formTag;
38
59
  return this.$slotDefault ? (
39
60
  this.$slotDefault(this)
40
61
  ) : (
41
62
  <>
42
- {this._renderSchema()}
63
+ {this.$props.blocks ? this._renderFromBlocks() : this._renderSchema()}
43
64
  {FormTag === 'form' && <button type="submit" style={{ display: 'none' }}></button>}
44
65
  </>
45
66
  );
@@ -48,9 +69,6 @@ export class RenderForm extends BeanRenderBase {
48
69
  private _renderProps() {
49
70
  const FormTag = this.$props.formTag;
50
71
  const props: any = {};
51
- if (this.$props.inline) {
52
- props.class = 'inline';
53
- }
54
72
  if (FormTag === 'form') {
55
73
  props.onSubmit = (e: SubmitEvent) => {
56
74
  if (this.$props.onFormSubmit) {
@@ -1,5 +1,6 @@
1
1
  import { isNil } from '@cabloy/utils';
2
- import { useField, useStore } from '@tanstack/vue-form';
2
+ import { useField } from '@tanstack/vue-form';
3
+ import { useSelector } from '@tanstack/vue-store';
3
4
  import { markRaw } from 'vue';
4
5
  import z from 'zod';
5
6
  import { BeanControllerBase, deepEqual, IComponentOptions, Use } from 'zova';
@@ -139,7 +140,7 @@ export class ControllerFormField<TParentData extends {} = {}> extends BeanContro
139
140
  private _createField() {
140
141
  const options = this._getFormFieldOptions();
141
142
  const field = markRaw(useField(options as any)) as any;
142
- const fieldState = useStore(field.api.store, state => state) as any;
143
+ const fieldState = useSelector(field.api.store, state => state) as any;
143
144
  return { api: field.api, state: fieldState };
144
145
  }
145
146
 
@@ -0,0 +1,203 @@
1
+ import type {
2
+ IFormLayout,
3
+ IFormLayoutField,
4
+ IFormLayoutNode,
5
+ IFormLayoutTab,
6
+ IFormLayoutTabs,
7
+ ISchemaObjectExtensionField,
8
+ } from 'zova-module-a-openapi';
9
+
10
+ import type {
11
+ IFormLayoutDiagnostic,
12
+ IResolvedFormLayout,
13
+ IResolvedFormLayoutField,
14
+ IResolvedFormLayoutGroup,
15
+ IResolvedFormLayoutNode,
16
+ IResolvedFormLayoutSection,
17
+ IResolvedFormLayoutTab,
18
+ IResolvedFormLayoutTabs,
19
+ } from '../types/formLayout.js';
20
+
21
+ export function resolveFormLayout(
22
+ layout: IFormLayout,
23
+ properties: ISchemaObjectExtensionField[] | undefined,
24
+ ): IResolvedFormLayout {
25
+ const propertyNames = new Set(
26
+ properties
27
+ ?.filter(item => item.rest?.visible !== false)
28
+ .map(item => item.key)
29
+ .filter(Boolean) as string[],
30
+ );
31
+ const fieldNames = new Set<string>();
32
+ const nodeIds = new Set<string>();
33
+ const diagnostics: IFormLayoutDiagnostic[] = [];
34
+ const fieldTabPaths: IResolvedFormLayout['fieldTabPaths'] = {};
35
+ const children = layout.children
36
+ .map((node, index) =>
37
+ resolveNode(
38
+ node,
39
+ [index],
40
+ [],
41
+ propertyNames,
42
+ fieldNames,
43
+ nodeIds,
44
+ diagnostics,
45
+ fieldTabPaths,
46
+ ),
47
+ )
48
+ .filter(Boolean) as IResolvedFormLayoutNode[];
49
+ for (const property of properties ?? []) {
50
+ const name = property.key;
51
+ if (!name || !propertyNames.has(name) || fieldNames.has(name)) continue;
52
+ fieldNames.add(name);
53
+ children.push({ type: 'field', name });
54
+ }
55
+ return { children, fieldTabPaths, diagnostics };
56
+ }
57
+
58
+ function resolveNode(
59
+ node: IFormLayoutNode,
60
+ indexPath: number[],
61
+ tabPath: IResolvedFormLayout['fieldTabPaths'][string],
62
+ propertyNames: Set<string>,
63
+ fieldNames: Set<string>,
64
+ nodeIds: Set<string>,
65
+ diagnostics: IFormLayoutDiagnostic[],
66
+ fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
67
+ ): IResolvedFormLayoutNode | undefined {
68
+ if (node.type === 'field') {
69
+ return resolveField(node, tabPath, propertyNames, fieldNames, diagnostics, fieldTabPaths);
70
+ }
71
+ if (node.type === 'tabs') {
72
+ return resolveTabs(
73
+ node,
74
+ indexPath,
75
+ propertyNames,
76
+ fieldNames,
77
+ nodeIds,
78
+ diagnostics,
79
+ fieldTabPaths,
80
+ );
81
+ }
82
+ const id = resolveId(node, indexPath);
83
+ if (!registerId(id, nodeIds, diagnostics)) return;
84
+ if (node.type === 'section') {
85
+ const children = node.children
86
+ .map(item =>
87
+ resolveField(item, tabPath, propertyNames, fieldNames, diagnostics, fieldTabPaths),
88
+ )
89
+ .filter(Boolean) as IResolvedFormLayoutField[];
90
+ return children.length ? { ...node, id, children } : undefined;
91
+ }
92
+ const children = node.children
93
+ .map((item, index) =>
94
+ resolveNode(
95
+ item,
96
+ [...indexPath, index],
97
+ tabPath,
98
+ propertyNames,
99
+ fieldNames,
100
+ nodeIds,
101
+ diagnostics,
102
+ fieldTabPaths,
103
+ ),
104
+ )
105
+ .filter(Boolean) as Array<
106
+ IResolvedFormLayoutField | IResolvedFormLayoutGroup | IResolvedFormLayoutSection
107
+ >;
108
+ return children.length ? { ...node, id, children } : undefined;
109
+ }
110
+
111
+ function resolveTabs(
112
+ node: IFormLayoutTabs,
113
+ indexPath: number[],
114
+ propertyNames: Set<string>,
115
+ fieldNames: Set<string>,
116
+ nodeIds: Set<string>,
117
+ diagnostics: IFormLayoutDiagnostic[],
118
+ fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
119
+ ): IResolvedFormLayoutTabs | undefined {
120
+ const id = resolveId(node, indexPath);
121
+ if (!registerId(id, nodeIds, diagnostics)) return;
122
+ const children = node.children
123
+ .map((tab, index) =>
124
+ resolveTab(
125
+ tab,
126
+ [...indexPath, index],
127
+ id,
128
+ propertyNames,
129
+ fieldNames,
130
+ nodeIds,
131
+ diagnostics,
132
+ fieldTabPaths,
133
+ ),
134
+ )
135
+ .filter(Boolean) as IResolvedFormLayoutTab[];
136
+ return children.length ? { ...node, id, children } : undefined;
137
+ }
138
+
139
+ function resolveTab(
140
+ node: IFormLayoutTab,
141
+ indexPath: number[],
142
+ tabsId: string,
143
+ propertyNames: Set<string>,
144
+ fieldNames: Set<string>,
145
+ nodeIds: Set<string>,
146
+ diagnostics: IFormLayoutDiagnostic[],
147
+ fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
148
+ ): IResolvedFormLayoutTab | undefined {
149
+ const id = resolveId(node, indexPath);
150
+ if (!registerId(id, nodeIds, diagnostics)) return;
151
+ const tabPath = [{ tabsId, tabId: id }];
152
+ const children = node.children
153
+ .map((item, index) =>
154
+ resolveNode(
155
+ item,
156
+ [...indexPath, index],
157
+ tabPath,
158
+ propertyNames,
159
+ fieldNames,
160
+ nodeIds,
161
+ diagnostics,
162
+ fieldTabPaths,
163
+ ),
164
+ )
165
+ .filter(Boolean) as Array<
166
+ IResolvedFormLayoutField | IResolvedFormLayoutGroup | IResolvedFormLayoutSection
167
+ >;
168
+ return children.length ? { ...node, id, children } : undefined;
169
+ }
170
+
171
+ function resolveField(
172
+ node: IFormLayoutField,
173
+ tabPath: IResolvedFormLayout['fieldTabPaths'][string],
174
+ propertyNames: Set<string>,
175
+ fieldNames: Set<string>,
176
+ diagnostics: IFormLayoutDiagnostic[],
177
+ fieldTabPaths: IResolvedFormLayout['fieldTabPaths'],
178
+ ): IResolvedFormLayoutField | undefined {
179
+ if (!propertyNames.has(node.name)) {
180
+ diagnostics.push({ type: 'unknownField', value: node.name });
181
+ return;
182
+ }
183
+ if (fieldNames.has(node.name)) {
184
+ diagnostics.push({ type: 'duplicateField', value: node.name });
185
+ return;
186
+ }
187
+ fieldNames.add(node.name);
188
+ fieldTabPaths[node.name] = tabPath;
189
+ return node;
190
+ }
191
+
192
+ function resolveId(node: { type: string; id?: string }, indexPath: number[]) {
193
+ return node.id || `${node.type}-${indexPath.join('-')}`;
194
+ }
195
+
196
+ function registerId(id: string, ids: Set<string>, diagnostics: IFormLayoutDiagnostic[]) {
197
+ if (!ids.has(id)) {
198
+ ids.add(id);
199
+ return true;
200
+ }
201
+ diagnostics.push({ type: 'duplicateId', value: id });
202
+ return false;
203
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './beanControllerFormBase.js';
2
2
  export * from './beanControllerPageFormBase.js';
3
3
  export * from './utils.js';
4
+ export * from './formLayout.js';