cabloy 5.1.149 → 5.1.151

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 (122) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/commands/release.md +12 -1
  3. package/.claude/hooks/contract-loop-gate.ts +35 -17
  4. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +4 -3
  5. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +3 -2
  6. package/.claude/skills/cabloy-contract-loop/SKILL.md +4 -3
  7. package/.claude/skills/cabloy-domain-planning/SKILL.md +4 -3
  8. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +5 -3
  9. package/.claude/skills/cabloy-master-detail/SKILL.md +4 -3
  10. package/.claude/skills/cabloy-module-removal/SKILL.md +5 -4
  11. package/.claude/skills/cabloy-resource-field-update/SKILL.md +8 -5
  12. package/.claude/skills/cabloy-spec-execution/SKILL.md +169 -0
  13. package/.claude/skills/cabloy-spec-execution/evals/evals.json +71 -0
  14. package/.claude/skills/cabloy-spec-execution/references/execution-protocol.md +137 -0
  15. package/.claude/skills/cabloy-spec-execution/references/status-and-evidence.md +117 -0
  16. package/.claude/skills/cabloy-spec-generation/SKILL.md +227 -0
  17. package/.claude/skills/cabloy-spec-generation/evals/evals.json +83 -0
  18. package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +101 -0
  19. package/.claude/skills/cabloy-spec-generation/references/repo-specs-document-set.md +267 -0
  20. package/.claude/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +135 -0
  21. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.mjs +350 -0
  22. package/.claude/skills/cabloy-spec-generation/scripts/generate-implementation-charts.test.mjs +202 -0
  23. package/.claude/skills/cabloy-workflow/SKILL.md +21 -10
  24. package/.claude/skills/cabloy-workflow/evals/evals.json +8 -2
  25. package/.claude/skills/cabloy-workflow/references/edition-detection.md +5 -3
  26. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +4 -3
  27. package/CHANGELOG.md +41 -0
  28. package/CLAUDE.md +12 -11
  29. package/package.json +4 -2
  30. package/repo-docs/.vitepress/config.mjs +4 -0
  31. package/repo-docs/ai/docs-skills-rules-mapping.md +6 -6
  32. package/repo-docs/ai/future-skill-roadmap.md +2 -2
  33. package/repo-docs/ai/introduction.md +5 -5
  34. package/repo-docs/ai/playbook-contract-regeneration.md +3 -9
  35. package/repo-docs/ai/repo-guidance.md +5 -4
  36. package/repo-docs/ai/rules-and-config.md +2 -2
  37. package/repo-docs/ai/virtual-decorator-guidance.md +1 -3
  38. package/repo-docs/backend/cache-guide.md +36 -0
  39. package/repo-docs/backend/dto-guide.md +7 -6
  40. package/repo-docs/backend/dto-infer-generation.md +25 -5
  41. package/repo-docs/backend/introduction.md +1 -1
  42. package/repo-docs/backend/migration-and-changes.md +27 -2
  43. package/repo-docs/backend/model-guide.md +1 -1
  44. package/repo-docs/backend/resource-field-update.md +156 -0
  45. package/repo-docs/frontend/a-model-under-the-hood.md +38 -0
  46. package/repo-docs/frontend/a-openapi-under-the-hood.md +13 -2
  47. package/repo-docs/frontend/form-layout-guide.md +26 -9
  48. package/repo-docs/frontend/model-resource-best-practices.md +2 -0
  49. package/repo-docs/frontend/model-resource-cookbook.md +7 -1
  50. package/repo-docs/frontend/router-tabs-introduction.md +2 -4
  51. package/repo-docs/frontend/use-state-data-best-practices.md +44 -0
  52. package/repo-docs/frontend/zova-form-source-reading-map.md +26 -9
  53. package/repo-docs/frontend/zova-form-under-the-hood.md +11 -0
  54. package/repo-docs/fullstack/a-pay-payment-suite.md +1 -1
  55. package/repo-docs/fullstack/contract-loop-playbook.md +4 -0
  56. package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +2 -2
  57. package/repo-docs/reference/package-map.md +1 -1
  58. package/repo-e2e/specs/a-commerce.spec.ts +40 -0
  59. package/scripts/release.ts +48 -1
  60. package/scripts/upgrade.ts +18 -7
  61. package/vona/packages-cli/cli/package.json +1 -1
  62. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
  63. package/vona/packages-cli/cli-set-api/package.json +1 -1
  64. package/vona/pnpm-lock.yaml +83 -75
  65. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
  66. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
  68. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
  71. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +2 -2
  72. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +7 -6
  73. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +1 -1
  74. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +7 -6
  75. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +10 -7
  76. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +4 -4
  77. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +39 -67
  78. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +56 -37
  79. package/zova/packages-zova/zova/package.json +2 -2
  80. package/zova/pnpm-lock.yaml +5 -5
  81. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  82. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
  83. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
  84. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
  85. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
  86. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
  87. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
  88. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
  89. package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
  90. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
  91. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
  92. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
  93. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
  94. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
  95. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +8 -8
  96. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +11 -11
  97. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +2 -2
  98. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
  99. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  100. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
  101. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
  102. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  103. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  104. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +58 -9
  105. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
  106. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  107. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
  108. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
  109. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
  110. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
  111. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
  112. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
  113. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
  114. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
  115. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
  116. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  117. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
  118. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
  119. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
  120. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
  121. package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
  122. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -4,19 +4,47 @@ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
4
4
  import { Api, v } from 'vona-module-a-openapiutils';
5
5
  import { Dto } from 'vona-module-a-web';
6
6
  import { z } from 'zod';
7
+ import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
7
8
 
8
9
  import { $locale } from '../.metadata/locales.ts';
9
10
 
10
11
  export interface IDtoOptionsStockAdjust extends IDecoratorDtoOptions {}
11
12
 
12
- @Dto<IDtoOptionsStockAdjust>({ openapi: { title: $locale('AdjustStock') } })
13
+ @Dto<IDtoOptionsStockAdjust>({
14
+ openapi: { title: $locale('AdjustStock') },
15
+ blocks: [
16
+ ZovaRender.block('basic-pageentry:blockPageEntry', {
17
+ blocks: [
18
+ ZovaRender.block('basic-pageentry:blockForm', {
19
+ blocks: [
20
+ ZovaRender.block('basic-form:blockFormLayout', {
21
+ formLayout: {
22
+ children: [
23
+ {
24
+ type: 'section',
25
+ layout: 'flow',
26
+ children: [
27
+ { type: 'field', name: 'delta' },
28
+ { type: 'field', name: 'reason' },
29
+ ],
30
+ },
31
+ ],
32
+ },
33
+ }),
34
+ ],
35
+ }),
36
+ ],
37
+ }),
38
+ ],
39
+ })
13
40
  export class DtoStockAdjust {
14
- @Api.field(v.title($locale('SkuId')), v.required(), v.tableIdentity())
41
+ @Api.field(v.title($locale('SkuId')), v.required(), v.tableIdentity(), ZovaRender.visible(false))
15
42
  skuId: TableIdentity;
16
43
 
17
44
  @Api.field(
18
45
  v.title($locale('Delta')),
19
46
  v.required(),
47
+ ZovaRender.field('basic-input:formFieldInput', { type: 'number' }),
20
48
  z
21
49
  .number()
22
50
  .int()
@@ -27,6 +55,12 @@ export class DtoStockAdjust {
27
55
  @Api.field(v.title($locale('Reason')), v.required(), v.min(1), v.max(255))
28
56
  reason: string;
29
57
 
30
- @Api.field(v.title($locale('CorrelationId')), v.required(), v.min(1), v.max(100))
58
+ @Api.field(
59
+ v.title($locale('CorrelationId')),
60
+ v.required(),
61
+ v.min(1),
62
+ v.max(100),
63
+ ZovaRender.visible(false),
64
+ )
31
65
  correlationId: string;
32
66
  }
@@ -116,8 +116,8 @@ declare module 'vona-module-training-student' {
116
116
  export interface IModelOptionsStudent {
117
117
  relations: {
118
118
  trainingRecords: IModelRelationHasMany<'training-record:record', 'studentId', false, 'id'|'name'|'subjectCount'|'totalScore'|'averageScore'|'trainingTime'|'sceneImageIds'|'dossierFileIds'|'description', undefined, undefined, undefined>;
119
- studentContent: IModelRelationHasOne<'training-student:studentContent', 'studentId', false, 'id'|'studentId'|'descriptionMarkdown'|'descriptionHtml'>;
120
- studentContentForm: IModelRelationHasOne<'training-student:studentContent', 'studentId', false, 'id'|'studentId'|'descriptionMarkdown'>;
119
+ content: IModelRelationHasOne<'training-student:studentContent', 'studentId', false, 'id'|'descriptionMarkdown'>;
120
+ contentHtml: IModelRelationHasOne<'training-student:studentContent', 'studentId', false, 'id'|'descriptionHtml'>;
121
121
  };
122
122
  }
123
123
  export interface IModelOptionsStudentContent {
@@ -10,12 +10,12 @@ import { ModelStudent } from '../model/student.ts';
10
10
  import { DtoDetailRecordMutate } from './detailRecordMutate.tsx';
11
11
  import { DtoDetailRecordResItem } from './detailRecordResItem.tsx';
12
12
 
13
- const studentContentFormField = $makeMetadata(
14
- ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
13
+ const studentContentField = $makeMetadata(
14
+ ZovaRender.fieldSource('content.descriptionMarkdown'),
15
15
  ZovaRender.field('basic-markdown:formFieldMarkdown'),
16
16
  );
17
17
 
18
- export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'studentContentForm'> {}
18
+ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'content'> {}
19
19
 
20
20
  @Dto<IDtoOptionsStudentCreate>({
21
21
  blocks: [
@@ -54,7 +54,7 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'studentC
54
54
  children: [
55
55
  {
56
56
  type: 'section',
57
- children: [{ type: 'field', name: 'studentContentForm' }],
57
+ children: [{ type: 'field', name: 'content' }],
58
58
  },
59
59
  ],
60
60
  },
@@ -93,7 +93,7 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'studentC
93
93
  }),
94
94
  ],
95
95
  fields: {
96
- studentContentForm: studentContentFormField,
96
+ content: studentContentField,
97
97
  trainingRecords: $makeMetadata(
98
98
  v.title($locale('TrainingRecords')),
99
99
  ZovaRender.order(6),
@@ -103,8 +103,9 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'studentC
103
103
  },
104
104
  })
105
105
  export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
106
+ columns: ['name', 'mobile', 'imageId', 'level'],
106
107
  include: {
107
- studentContentForm: true,
108
+ content: { columns: ['descriptionMarkdown'] },
108
109
  trainingRecords: { dtoClass: DtoDetailRecordMutate },
109
110
  },
110
111
  }) {
@@ -14,7 +14,7 @@ export class DtoStudentSummary extends $Dto.get(() => ModelStudent, {
14
14
  columns: ['id', 'name', 'mobile', 'level'],
15
15
  }) {
16
16
  @Api.field(v.title($locale('Description')), v.optional())
17
- description?: string;
17
+ descriptionMarkdown?: string;
18
18
 
19
19
  @Api.field(v.title($locale('DescriptionHtml')), v.optional())
20
20
  descriptionHtml?: string;
@@ -10,12 +10,12 @@ import { ModelStudent } from '../model/student.ts';
10
10
  import { DtoDetailRecordMutate } from './detailRecordMutate.tsx';
11
11
  import { DtoDetailRecordResItem } from './detailRecordResItem.tsx';
12
12
 
13
- const studentContentFormField = $makeMetadata(
14
- ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
13
+ const studentContentField = $makeMetadata(
14
+ ZovaRender.fieldSource('content.descriptionMarkdown'),
15
15
  ZovaRender.field('basic-markdown:formFieldMarkdown'),
16
16
  );
17
17
 
18
- export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'studentContentForm'> {}
18
+ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'content'> {}
19
19
 
20
20
  @Dto<IDtoOptionsStudentUpdate>({
21
21
  blocks: [
@@ -54,7 +54,7 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'studentC
54
54
  children: [
55
55
  {
56
56
  type: 'section',
57
- children: [{ type: 'field', name: 'studentContentForm' }],
57
+ children: [{ type: 'field', name: 'content' }],
58
58
  },
59
59
  ],
60
60
  },
@@ -90,7 +90,7 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'studentC
90
90
  }),
91
91
  ],
92
92
  fields: {
93
- studentContentForm: studentContentFormField,
93
+ content: studentContentField,
94
94
  trainingRecords: $makeMetadata(
95
95
  v.title($locale('TrainingRecords')),
96
96
  ZovaRender.order(6),
@@ -99,8 +99,9 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'studentC
99
99
  },
100
100
  })
101
101
  export class DtoStudentUpdate extends $Dto.update(() => ModelStudent, {
102
+ columns: ['name', 'mobile', 'imageId', 'level'],
102
103
  include: {
103
- studentContentForm: true,
104
+ content: { columns: ['descriptionMarkdown'] },
104
105
  trainingRecords: { dtoClass: DtoDetailRecordMutate },
105
106
  },
106
107
  }) {
@@ -1,9 +1,10 @@
1
1
  import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
2
 
3
3
  import { DtoImageView } from 'vona-module-a-image';
4
- import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
4
+ import { $makeMetadata, $makeSchema, Api, v } from 'vona-module-a-openapiutils';
5
5
  import { $Dto } from 'vona-module-a-orm';
6
6
  import { Dto } from 'vona-module-a-web';
7
+ import z from 'zod';
7
8
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
9
 
9
10
  import { $locale } from '../.metadata/locales.ts';
@@ -11,12 +12,14 @@ import { ModelStudent } from '../model/student.ts';
11
12
  import { DtoDetailRecordResItem } from './detailRecordResItem.tsx';
12
13
  import { DtoDetailRecordView } from './detailRecordView.tsx';
13
14
 
14
- const studentContentFormField = $makeMetadata(
15
- ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
15
+ const studentContentField = $makeSchema(
16
+ ZovaRender.fieldSource('content.descriptionMarkdown'),
16
17
  ZovaRender.field('basic-markdown:formFieldMarkdown'),
18
+ v.optional(),
19
+ z.string(),
17
20
  );
18
21
 
19
- export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'studentContentForm'> {}
22
+ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'_descriptionMarkdown'> {}
20
23
 
21
24
  @Dto<IDtoOptionsStudentView>({
22
25
  blocks: [
@@ -55,7 +58,7 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'studentCon
55
58
  children: [
56
59
  {
57
60
  type: 'section',
58
- children: [{ type: 'field', name: 'studentContentForm' }],
61
+ children: [{ type: 'field', name: '_descriptionMarkdown' }],
59
62
  },
60
63
  ],
61
64
  },
@@ -88,7 +91,7 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'studentCon
88
91
  }),
89
92
  ],
90
93
  fields: {
91
- studentContentForm: studentContentFormField,
94
+ _descriptionMarkdown: studentContentField,
92
95
  trainingRecords: $makeMetadata(
93
96
  v.title($locale('TrainingRecords')),
94
97
  ZovaRender.order(6),
@@ -98,7 +101,7 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'studentCon
98
101
  })
99
102
  export class DtoStudentView extends $Dto.get(() => ModelStudent, {
100
103
  include: {
101
- studentContentForm: true,
104
+ content: { columns: ['descriptionMarkdown'] },
102
105
  trainingRecords: { dtoClass: DtoDetailRecordView },
103
106
  },
104
107
  }) {
@@ -22,11 +22,11 @@ export interface IModelOptionsStudent extends IDecoratorModelOptions<EntityStude
22
22
  'description',
23
23
  ],
24
24
  }),
25
- studentContent: $relation.hasOne('training-student:studentContent', 'studentId', {
26
- columns: ['id', 'studentId', 'descriptionMarkdown', 'descriptionHtml'],
25
+ content: $relation.hasOne('training-student:studentContent', 'studentId', {
26
+ columns: ['id', 'descriptionMarkdown'],
27
27
  }),
28
- studentContentForm: $relation.hasOne('training-student:studentContent', 'studentId', {
29
- columns: ['id', 'studentId', 'descriptionMarkdown'],
28
+ contentHtml: $relation.hasOne('training-student:studentContent', 'studentId', {
29
+ columns: ['id', 'descriptionHtml'],
30
30
  }),
31
31
  },
32
32
  })
@@ -12,33 +12,30 @@ import type { DtoStudentView } from '../dto/studentView.tsx';
12
12
  import type { EntityStudent } from '../entity/student.tsx';
13
13
  import type { ModelStudent } from '../model/student.ts';
14
14
 
15
- function getStudentRecordSubjectsInclude(): {
15
+ type StudentContentInput = {
16
+ descriptionMarkdown?: string;
17
+ };
18
+
19
+ function getStudentRelationsInclude(): {
20
+ content: true;
16
21
  trainingRecords: { include: { trainingRecordSubjects: true } };
17
22
  } {
18
- return { trainingRecords: { include: { trainingRecordSubjects: true } } };
19
- }
20
-
21
- function getStudentContentFormInclude() {
22
- return { studentContentForm: true };
23
- }
24
-
25
- function getStudentContentInclude() {
26
- return { studentContent: true };
23
+ return {
24
+ content: true,
25
+ trainingRecords: { include: { trainingRecordSubjects: true } },
26
+ };
27
27
  }
28
28
 
29
29
  @Service()
30
30
  export class ServiceStudent extends BeanBase {
31
31
  async create(student: DtoStudentCreate): Promise<EntityStudent> {
32
- const { studentContentForm, ...studentFields } = student as DtoStudentCreate & {
33
- studentContentForm?: { descriptionMarkdown?: string };
34
- };
35
- const created = await this.scope.model.student.insert(studentFields, {
36
- include: getStudentRecordSubjectsInclude(),
37
- });
38
- if (studentContentForm !== undefined) {
39
- await this._updateStudentContent(created.id, studentContentForm.descriptionMarkdown);
40
- }
41
- return created;
32
+ return await this.scope.model.student.insert(
33
+ {
34
+ ...student,
35
+ content: this._prepareContent(student.content),
36
+ },
37
+ { include: getStudentRelationsInclude() },
38
+ );
42
39
  }
43
40
 
44
41
  async select(params?: IQueryParams<ModelStudent>): Promise<DtoStudentSelectRes> {
@@ -47,34 +44,28 @@ export class ServiceStudent extends BeanBase {
47
44
 
48
45
  async view(id: TableIdentity): Promise<DtoStudentView | undefined> {
49
46
  return await this.scope.model.student.getById(id, {
50
- include: {
51
- ...getStudentRecordSubjectsInclude(),
52
- ...getStudentContentFormInclude(),
53
- },
47
+ include: getStudentRelationsInclude(),
54
48
  });
55
49
  }
56
50
 
57
51
  async update(id: TableIdentity, student: DtoStudentUpdate) {
58
- const { studentContentForm, ...studentFields } = student as DtoStudentUpdate & {
59
- studentContentForm?: { descriptionMarkdown?: string };
60
- };
61
- const updateResult = await this.scope.model.student.updateById(id, studentFields, {
62
- include: getStudentRecordSubjectsInclude(),
63
- });
64
- if (studentContentForm !== undefined) {
65
- await this._updateStudentContent(id, studentContentForm.descriptionMarkdown);
66
- }
67
- return updateResult;
52
+ const content = Object.hasOwn(student, 'content')
53
+ ? this._prepareContent(student.content)
54
+ : undefined;
55
+ return await this.scope.model.student.updateById(
56
+ id,
57
+ content === undefined ? student : { ...student, content },
58
+ { include: getStudentRelationsInclude() },
59
+ );
68
60
  }
69
61
 
70
62
  async summary(id: TableIdentity): Promise<DtoStudentSummary | undefined> {
71
63
  const student = await this.scope.model.student.getById(id, {
72
- include: getStudentContentInclude(),
64
+ include: { content: true, contentHtml: true },
73
65
  });
74
66
  if (!student) return undefined;
75
- const description = student.studentContent?.descriptionMarkdown;
76
- const descriptionHtml = student.studentContent?.descriptionHtml;
77
- const descriptionLength = description?.length ?? 0;
67
+ const descriptionMarkdown = student.content?.descriptionMarkdown;
68
+ const descriptionLength = descriptionMarkdown?.length ?? 0;
78
69
  const levelTitle = String(student.level);
79
70
  return {
80
71
  id: student.id,
@@ -82,50 +73,31 @@ export class ServiceStudent extends BeanBase {
82
73
  mobile: student.mobile,
83
74
  level: student.level,
84
75
  levelTitle,
85
- description,
86
- descriptionHtml,
76
+ descriptionMarkdown,
77
+ descriptionHtml: student.contentHtml?.descriptionHtml,
87
78
  descriptionLength,
88
79
  summaryText: `${student.name} is in level ${student.level}. Description length: ${descriptionLength}.`,
89
80
  };
90
81
  }
91
82
 
92
83
  async delete(id: TableIdentity) {
93
- await this.scope.model.studentContent.delete({ studentId: id });
94
84
  return await this.scope.model.student.deleteById(id, {
95
- include: getStudentRecordSubjectsInclude(),
85
+ include: getStudentRelationsInclude(),
96
86
  });
97
87
  }
98
88
 
99
89
  async deleteForce(id: TableIdentity) {
100
- await this.scope.model.studentContent.delete({ studentId: id }, { disableDeleted: true });
101
90
  return await this.scope.model.student.deleteById(id, {
102
91
  disableDeleted: true,
103
- include: getStudentRecordSubjectsInclude(),
92
+ include: getStudentRelationsInclude(),
104
93
  });
105
94
  }
106
95
 
107
- private async _updateStudentContent(
108
- studentId: TableIdentity,
109
- descriptionMarkdown?: string,
110
- ): Promise<void> {
111
- const markdown = descriptionMarkdown?.trim();
112
- const studentContent = await this.scope.model.studentContent.get({ studentId });
113
- if (!markdown) {
114
- if (studentContent) await this.scope.model.studentContent.deleteById(studentContent.id);
115
- return;
116
- }
117
- const descriptionHtml = this.bean.markdown.renderHtml(markdown);
118
- if (studentContent) {
119
- await this.scope.model.studentContent.updateById(studentContent.id, {
120
- descriptionMarkdown: markdown,
121
- descriptionHtml,
122
- });
123
- } else {
124
- await this.scope.model.studentContent.insert({
125
- studentId,
126
- descriptionMarkdown: markdown,
127
- descriptionHtml,
128
- });
129
- }
96
+ private _prepareContent(content: StudentContentInput | undefined) {
97
+ const descriptionMarkdown = content?.descriptionMarkdown ?? '';
98
+ return {
99
+ descriptionMarkdown,
100
+ descriptionHtml: this.bean.markdown.renderHtml(descriptionMarkdown),
101
+ };
130
102
  }
131
103
  }
@@ -43,18 +43,18 @@ describe('student.test.ts', () => {
43
43
  profileSection?.children.map(item => item.name),
44
44
  ['name', 'mobile', 'imageId'],
45
45
  );
46
+ const descriptionFieldName =
47
+ DtoClass === DtoStudentView ? '_descriptionMarkdown' : 'content';
46
48
  assert.deepEqual(
47
49
  studentContentSection?.children.map(item => item.name),
48
- ['studentContentForm'],
49
- );
50
- assert.equal(
51
- component.properties.studentContentForm.rest.fieldSource,
52
- 'studentContentForm.descriptionMarkdown',
53
- );
54
- assert.equal(
55
- component.properties.studentContentForm.rest.form.render,
56
- 'basic-markdown:formFieldMarkdown',
50
+ [descriptionFieldName],
57
51
  );
52
+ const descriptionField = (component as any).properties[descriptionFieldName];
53
+ assert.equal(descriptionField.rest.fieldSource, 'content.descriptionMarkdown');
54
+ assert.equal(descriptionField.rest.form.render, 'basic-markdown:formFieldMarkdown');
55
+ if (DtoClass === DtoStudentView) {
56
+ assert.equal((component as any).required?.includes('_descriptionMarkdown'), false);
57
+ }
58
58
  assert.equal(tabs?.children[1]?.children[0]?.name, 'level');
59
59
  assert.deepEqual(
60
60
  trainingRecordsSection?.children.map(item => item.name),
@@ -111,7 +111,7 @@ describe('student.test.ts', () => {
111
111
  'name',
112
112
  'mobile',
113
113
  'level',
114
- 'description',
114
+ 'descriptionMarkdown',
115
115
  'descriptionHtml',
116
116
  'levelTitle',
117
117
  'descriptionLength',
@@ -144,7 +144,7 @@ describe('student.test.ts', () => {
144
144
  `## Updated profile\n\n${'This is updated Markdown content. '.repeat(10)}`.trim();
145
145
  const data = {
146
146
  name: '__Tom__',
147
- studentContentForm: { descriptionMarkdown: description },
147
+ content: { descriptionMarkdown: description },
148
148
  mobile,
149
149
  level: 1,
150
150
  trainingRecords: [
@@ -207,7 +207,7 @@ describe('student.test.ts', () => {
207
207
  const record = student.trainingRecords?.[0];
208
208
  const recordSubject = record?.trainingRecordSubjects?.[0];
209
209
  assert.equal(student.trainingRecords?.length, 1);
210
- assert.equal(student.studentContentForm?.descriptionMarkdown, description);
210
+ assert.equal(student.content?.descriptionMarkdown, description);
211
211
  assert.equal(record?.name, '__Record__');
212
212
  assert.equal(record?.subjectCount, 1);
213
213
  assert.equal(record?.totalScore, 88);
@@ -219,7 +219,7 @@ describe('student.test.ts', () => {
219
219
  // update
220
220
  const dataUpdate = {
221
221
  name: '__TomNew__',
222
- studentContentForm: { descriptionMarkdown: descriptionUpdate },
222
+ content: { descriptionMarkdown: descriptionUpdate },
223
223
  mobile: mobileUpdate,
224
224
  level: 2,
225
225
  trainingRecords: [
@@ -262,7 +262,7 @@ describe('student.test.ts', () => {
262
262
  assert.equal(student.name, dataUpdate.name);
263
263
  assert.equal(student.level, dataUpdate.level);
264
264
  assert.equal(student.mobile, maskedMobileUpdate);
265
- assert.equal(student.studentContentForm?.descriptionMarkdown, descriptionUpdate);
265
+ assert.equal(student.content?.descriptionMarkdown, descriptionUpdate);
266
266
  assert.equal(student.trainingRecords?.length, 1);
267
267
  assert.equal(updatedRecord?.name, '__RecordNew__');
268
268
  assert.equal(updatedRecord?.subjectCount, 2);
@@ -299,7 +299,7 @@ describe('student.test.ts', () => {
299
299
  assert.equal(summary.name, dataUpdate.name);
300
300
  assert.equal(summary.mobile, maskedMobileUpdate);
301
301
  assert.equal(summary.level, dataUpdate.level);
302
- assert.equal(summary.description, descriptionUpdate);
302
+ assert.equal(summary.descriptionMarkdown, descriptionUpdate);
303
303
  assert.equal(summary.descriptionHtml, app.bean.markdown.renderHtml(descriptionUpdate));
304
304
  assert.equal(summary.descriptionLength, descriptionUpdate.length);
305
305
  assert.equal(typeof summary.levelTitle, 'string');
@@ -309,6 +309,14 @@ describe('student.test.ts', () => {
309
309
  params: { id: student.id },
310
310
  });
311
311
  assert.equal(deleteRes, null);
312
+ const studentContentDeleted = await app
313
+ .scope('training-student')
314
+ .model.studentContent.get({ studentId }, { disableDeleted: true });
315
+ assert.equal(studentContentDeleted?.deleted, true);
316
+ assert.equal(
317
+ await app.scope('training-student').model.studentContent.get({ studentId }),
318
+ undefined,
319
+ );
312
320
  // findOne
313
321
  student = await app.bean.executor.performAction('get', '/training/student/:id', {
314
322
  params: { id: student.id },
@@ -355,45 +363,56 @@ describe('student.test.ts', () => {
355
363
  level: 1,
356
364
  },
357
365
  });
358
- assert.equal(
359
- await app.scope('training-student').model.studentContent.get({ studentId }),
360
- undefined,
361
- );
366
+ const studentContentModel = app.scope('training-student').model.studentContent;
367
+ const createdContent = await studentContentModel.get({ studentId });
368
+ assert.equal(createdContent?.descriptionMarkdown, '');
369
+ assert.equal(createdContent?.descriptionHtml, '');
362
370
 
363
371
  await app.bean.executor.performAction('patch', '/training/student/:id', {
364
372
  params: { id: studentId },
365
373
  body: {
366
- name: '__ContentStudent__',
374
+ name: '__ContentStudentUpdated__',
367
375
  mobile: '13812345678',
368
376
  level: 1,
369
- studentContentForm: {
370
- descriptionMarkdown: ' ## Created later ',
377
+ },
378
+ });
379
+ const preservedContent = await studentContentModel.get({ studentId });
380
+ assert.equal(preservedContent?.descriptionMarkdown, '');
381
+ assert.equal(preservedContent?.descriptionHtml, '');
382
+
383
+ const markdown = ' ## Created later ';
384
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
385
+ params: { id: studentId },
386
+ body: {
387
+ name: '__ContentStudentUpdated__',
388
+ mobile: '13812345678',
389
+ level: 1,
390
+ content: {
391
+ descriptionMarkdown: markdown,
371
392
  descriptionHtml: '<p>Forged HTML</p>',
372
393
  },
373
394
  },
374
395
  });
375
- const studentContent = await app
376
- .scope('training-student')
377
- .model.studentContent.get({ studentId });
378
- assert.equal(studentContent?.descriptionMarkdown, '## Created later');
379
- assert.equal(
380
- studentContent?.descriptionHtml,
381
- app.bean.markdown.renderHtml('## Created later'),
382
- );
396
+ const studentContent = await studentContentModel.get({ studentId });
397
+ assert.equal(studentContent?.descriptionMarkdown, markdown);
398
+ assert.equal(studentContent?.descriptionHtml, app.bean.markdown.renderHtml(markdown));
383
399
  assert.notEqual(studentContent?.descriptionHtml, '<p>Forged HTML</p>');
384
400
 
401
+ const whitespaceMarkdown = ' \n\t ';
385
402
  await app.bean.executor.performAction('patch', '/training/student/:id', {
386
403
  params: { id: studentId },
387
404
  body: {
388
- name: '__ContentStudent__',
405
+ name: '__ContentStudentUpdated__',
389
406
  mobile: '13812345678',
390
407
  level: 1,
391
- studentContentForm: { descriptionMarkdown: ' \n\t ' },
408
+ content: { descriptionMarkdown: whitespaceMarkdown },
392
409
  },
393
410
  });
411
+ const whitespaceContent = await studentContentModel.get({ studentId });
412
+ assert.equal(whitespaceContent?.descriptionMarkdown, whitespaceMarkdown);
394
413
  assert.equal(
395
- await app.scope('training-student').model.studentContent.get({ studentId }),
396
- undefined,
414
+ whitespaceContent?.descriptionHtml,
415
+ app.bean.markdown.renderHtml(whitespaceMarkdown),
397
416
  );
398
417
  } finally {
399
418
  if (studentId) {
@@ -437,7 +456,7 @@ describe('student.test.ts', () => {
437
456
  await app.bean.executor.performAction('post', '/training/student', {
438
457
  body: {
439
458
  name: '__Tom__',
440
- studentContentForm: { descriptionMarkdown: 'This is a test' },
459
+ content: { descriptionMarkdown: 'This is a test' },
441
460
  mobile: '13812345678',
442
461
  level: 4,
443
462
  },
@@ -454,7 +473,7 @@ describe('student.test.ts', () => {
454
473
  await app.bean.executor.performAction('post', '/training/student', {
455
474
  body: {
456
475
  name: '__Tom__',
457
- studentContentForm: { descriptionMarkdown: 'This is a test' },
476
+ content: { descriptionMarkdown: 'This is a test' },
458
477
  level: 1,
459
478
  },
460
479
  });
@@ -470,7 +489,7 @@ describe('student.test.ts', () => {
470
489
  await app.bean.executor.performAction('post', '/training/student', {
471
490
  body: {
472
491
  name: '__Tom__',
473
- studentContentForm: { descriptionMarkdown: 'This is a test' },
492
+ content: { descriptionMarkdown: 'This is a test' },
474
493
  mobile: '1381234567',
475
494
  level: 1,
476
495
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.154",
3
+ "version": "5.1.155",
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.153"
49
+ "zova-suite-a-zova": "^5.1.154"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -870,7 +870,7 @@ importers:
870
870
  specifier: ^5.1.88
871
871
  version: link:../zova-core
872
872
  zova-suite-a-zova:
873
- specifier: ^5.1.152
873
+ specifier: ^5.1.154
874
874
  version: link:../../src/suite-vendor/a-zova
875
875
  devDependencies:
876
876
  clean-package:
@@ -977,7 +977,7 @@ importers:
977
977
  specifier: ^5.0.1
978
978
  version: link:modules/a-rbac
979
979
  zova-module-rest-resource:
980
- specifier: ^5.1.46
980
+ specifier: ^5.1.47
981
981
  version: link:modules/rest-resource
982
982
 
983
983
  src/suite-vendor/a-cabloy/modules/a-rbac:
@@ -1076,7 +1076,7 @@ importers:
1076
1076
  specifier: ^5.1.25
1077
1077
  version: link:modules/a-fetch
1078
1078
  zova-module-a-form:
1079
- specifier: ^5.1.48
1079
+ specifier: ^5.1.50
1080
1080
  version: link:modules/a-form
1081
1081
  zova-module-a-icon:
1082
1082
  specifier: ^5.1.27
@@ -1091,10 +1091,10 @@ importers:
1091
1091
  specifier: ^5.1.22
1092
1092
  version: link:modules/a-meta
1093
1093
  zova-module-a-model:
1094
- specifier: ^5.1.35
1094
+ specifier: ^5.1.36
1095
1095
  version: link:modules/a-model
1096
1096
  zova-module-a-openapi:
1097
- specifier: ^5.1.50
1097
+ specifier: ^5.1.51
1098
1098
  version: link:modules/a-openapi
1099
1099
  zova-module-a-router:
1100
1100
  specifier: ^5.1.34