cabloy 5.1.148 → 5.1.150

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 (95) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -1
  3. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +1 -0
  4. package/CHANGELOG.md +33 -0
  5. package/CLAUDE.md +1 -0
  6. package/package.json +1 -1
  7. package/repo-docs/.vitepress/config.mjs +4 -0
  8. package/repo-docs/backend/dto-guide.md +7 -6
  9. package/repo-docs/backend/dto-infer-generation.md +25 -5
  10. package/repo-docs/backend/markdown-guide.md +1 -0
  11. package/repo-docs/backend/relations-guide.md +1 -0
  12. package/repo-docs/frontend/a-model-under-the-hood.md +38 -0
  13. package/repo-docs/frontend/a-openapi-under-the-hood.md +13 -2
  14. package/repo-docs/frontend/form-layout-guide.md +39 -8
  15. package/repo-docs/frontend/markdown-guide.md +1 -0
  16. package/repo-docs/frontend/model-resource-best-practices.md +2 -0
  17. package/repo-docs/frontend/model-resource-cookbook.md +7 -1
  18. package/repo-docs/frontend/use-state-data-best-practices.md +44 -0
  19. package/repo-docs/frontend/zova-form-source-reading-map.md +26 -9
  20. package/repo-docs/frontend/zova-form-under-the-hood.md +11 -0
  21. package/repo-docs/fullstack/edition-collaboration-differences.md +1 -0
  22. package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +447 -0
  23. package/repo-e2e/specs/a-commerce.spec.ts +40 -0
  24. package/repo-e2e/specs/cabloy-basic.spec.ts +176 -1
  25. package/vona/packages-cli/cli/package.json +1 -1
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +2 -2
  27. package/vona/packages-cli/cli-set-api/package.json +1 -1
  28. package/vona/pnpm-lock.yaml +83 -75
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +108 -108
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +12 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/sku.ts +19 -1
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +53 -4
  33. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +148 -148
  34. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAdjust.tsx +37 -3
  35. package/vona/src/suite/a-training/modules/training-student/package.json +1 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +177 -103
  37. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.index.ts +1 -0
  38. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.version.ts +19 -1
  39. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  40. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  41. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +22 -2
  42. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +7 -1
  43. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +22 -2
  44. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +25 -3
  45. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -3
  46. package/vona/src/suite/a-training/modules/training-student/src/entity/studentContent.tsx +36 -0
  47. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +6 -0
  48. package/vona/src/suite/a-training/modules/training-student/src/model/studentContent.ts +22 -0
  49. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +43 -14
  50. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +137 -8
  51. package/zova/packages-zova/zova/package.json +2 -2
  52. package/zova/pnpm-lock.yaml +6 -2
  53. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  54. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +6 -1
  55. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionAdjustStock.ts +34 -0
  56. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +81 -6
  57. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeStockBalance.ts +32 -0
  58. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +44 -44
  59. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +356 -338
  60. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeStockBalance.ts +17 -0
  61. package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionAdjustStock.tsx +35 -0
  62. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionAdjustStock/controller.tsx +138 -0
  63. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +3 -0
  64. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +3 -0
  65. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/stockBalance.ts +36 -0
  66. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +4 -4
  67. package/zova/src/suite/a-training/modules/training-student/package.json +3 -1
  68. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +44 -44
  69. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +360 -339
  70. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +19 -8
  71. package/zova/src/suite/cabloy-basic/modules/basic-app/src/bean/behavior.appModal.tsx +2 -0
  72. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  73. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +2 -1
  74. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/model/resource.types.ts +11 -0
  75. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  76. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  77. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +69 -12
  78. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/formLayout.test.ts +157 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +3 -1
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +11 -13
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +9 -7
  83. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -12
  84. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryPersister.ts +17 -0
  85. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/queryRefetch.ts +34 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +27 -3
  87. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryPersister.test.ts +115 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/queryRefetch.test.ts +84 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.ts +27 -5
  91. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +2 -2
  92. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +4 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/schema.ts +2 -5
  94. package/zova/src/suite-vendor/a-zova/modules/a-openapi/test/lib/schema.test.ts +179 -0
  95. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -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,7 +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
- export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
15
+ const studentContentField = $makeSchema(
16
+ ZovaRender.fieldSource('content.descriptionMarkdown'),
17
+ ZovaRender.field('basic-markdown:formFieldMarkdown'),
18
+ v.optional(),
19
+ z.string(),
20
+ );
21
+
22
+ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'_descriptionMarkdown'> {}
15
23
 
16
24
  @Dto<IDtoOptionsStudentView>({
17
25
  blocks: [
@@ -44,6 +52,16 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
44
52
  },
45
53
  ],
46
54
  },
55
+ {
56
+ type: 'group',
57
+ title: $locale('StudentContent'),
58
+ children: [
59
+ {
60
+ type: 'section',
61
+ children: [{ type: 'field', name: '_descriptionMarkdown' }],
62
+ },
63
+ ],
64
+ },
47
65
  ],
48
66
  },
49
67
  {
@@ -73,6 +91,7 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
73
91
  }),
74
92
  ],
75
93
  fields: {
94
+ _descriptionMarkdown: studentContentField,
76
95
  trainingRecords: $makeMetadata(
77
96
  v.title($locale('TrainingRecords')),
78
97
  ZovaRender.order(6),
@@ -81,7 +100,10 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
81
100
  },
82
101
  })
83
102
  export class DtoStudentView extends $Dto.get(() => ModelStudent, {
84
- include: { trainingRecords: { dtoClass: DtoDetailRecordView } },
103
+ include: {
104
+ content: { columns: ['descriptionMarkdown'] },
105
+ trainingRecords: { dtoClass: DtoDetailRecordView },
106
+ },
85
107
  }) {
86
108
  @Api.field(
87
109
  ZovaRender.visible(false),
@@ -44,9 +44,6 @@ export class EntityStudent extends EntityBase {
44
44
  )
45
45
  name: string;
46
46
 
47
- @Api.field(v.title($locale('Description')), v.optional(), ZovaRender.order(2))
48
- description?: string;
49
-
50
47
  @Api.field(
51
48
  v.title($locale('Mobile')),
52
49
  v.required(),
@@ -0,0 +1,36 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IDecoratorEntityOptions } from 'vona-module-a-orm';
3
+
4
+ import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
5
+ import { Entity, EntityBase } from 'vona-module-a-orm';
6
+ import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
7
+
8
+ import { $locale } from '../.metadata/locales.ts';
9
+
10
+ export interface IEntityOptionsStudentContent extends IDecoratorEntityOptions {}
11
+
12
+ @Entity<IEntityOptionsStudentContent>('trainingStudentContent', {
13
+ openapi: { title: $locale('StudentContent') },
14
+ fields: {
15
+ id: $makeMetadata(ZovaRender.visible(false)),
16
+ iid: $makeMetadata(ZovaRender.visible(false)),
17
+ deleted: $makeMetadata(ZovaRender.visible(false)),
18
+ createdAt: $makeMetadata(ZovaRender.visible(false)),
19
+ updatedAt: $makeMetadata(ZovaRender.visible(false)),
20
+ },
21
+ })
22
+ export class EntityStudentContent extends EntityBase {
23
+ @Api.field(v.tableIdentity(), ZovaRender.visible(false))
24
+ studentId: TableIdentity;
25
+
26
+ @Api.field(
27
+ v.title($locale('DescriptionMarkdown')),
28
+ v.optional(),
29
+ ZovaRender.order(1),
30
+ ZovaRender.field('basic-markdown:formFieldMarkdown'),
31
+ )
32
+ descriptionMarkdown?: string;
33
+
34
+ @Api.field(v.title($locale('DescriptionHtml')), v.optional(), ZovaRender.visible(false))
35
+ descriptionHtml?: string;
36
+ }
@@ -22,6 +22,12 @@ export interface IModelOptionsStudent extends IDecoratorModelOptions<EntityStude
22
22
  'description',
23
23
  ],
24
24
  }),
25
+ content: $relation.hasOne('training-student:studentContent', 'studentId', {
26
+ columns: ['id', 'descriptionMarkdown'],
27
+ }),
28
+ contentHtml: $relation.hasOne('training-student:studentContent', 'studentId', {
29
+ columns: ['id', 'descriptionHtml'],
30
+ }),
25
31
  },
26
32
  })
27
33
  export class ModelStudent extends BeanModelBase<EntityStudent> {}
@@ -0,0 +1,22 @@
1
+ import type { IDecoratorModelOptions } from 'vona-module-a-orm';
2
+
3
+ import { $relation, BeanModelBase, Model } from 'vona-module-a-orm';
4
+
5
+ import { EntityStudentContent } from '../entity/studentContent.tsx';
6
+
7
+ export interface IModelOptionsStudentContent extends IDecoratorModelOptions<EntityStudentContent> {}
8
+
9
+ @Model<IModelOptionsStudentContent>({
10
+ entity: EntityStudentContent,
11
+ relations: {
12
+ student: $relation.belongsTo(
13
+ 'training-student:studentContent',
14
+ 'training-student:student',
15
+ 'studentId',
16
+ ),
17
+ },
18
+ cache: {
19
+ modelsClear: 'training-student:student',
20
+ },
21
+ })
22
+ export class ModelStudentContent extends BeanModelBase<EntityStudentContent> {}
@@ -12,18 +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 } } };
23
+ return {
24
+ content: true,
25
+ trainingRecords: { include: { trainingRecordSubjects: true } },
26
+ };
19
27
  }
20
28
 
21
29
  @Service()
22
30
  export class ServiceStudent extends BeanBase {
23
31
  async create(student: DtoStudentCreate): Promise<EntityStudent> {
24
- return await this.scope.model.student.insert(student, {
25
- include: getStudentRecordSubjectsInclude(),
26
- });
32
+ return await this.scope.model.student.insert(
33
+ {
34
+ ...student,
35
+ content: this._prepareContent(student.content),
36
+ },
37
+ { include: getStudentRelationsInclude() },
38
+ );
27
39
  }
28
40
 
29
41
  async select(params?: IQueryParams<ModelStudent>): Promise<DtoStudentSelectRes> {
@@ -32,20 +44,28 @@ export class ServiceStudent extends BeanBase {
32
44
 
33
45
  async view(id: TableIdentity): Promise<DtoStudentView | undefined> {
34
46
  return await this.scope.model.student.getById(id, {
35
- include: getStudentRecordSubjectsInclude(),
47
+ include: getStudentRelationsInclude(),
36
48
  });
37
49
  }
38
50
 
39
51
  async update(id: TableIdentity, student: DtoStudentUpdate) {
40
- return await this.scope.model.student.updateById(id, student, {
41
- include: getStudentRecordSubjectsInclude(),
42
- });
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
+ );
43
60
  }
44
61
 
45
62
  async summary(id: TableIdentity): Promise<DtoStudentSummary | undefined> {
46
- const student = await this.scope.model.student.getById(id);
63
+ const student = await this.scope.model.student.getById(id, {
64
+ include: { content: true, contentHtml: true },
65
+ });
47
66
  if (!student) return undefined;
48
- const descriptionLength = student.description?.length ?? 0;
67
+ const descriptionMarkdown = student.content?.descriptionMarkdown;
68
+ const descriptionLength = descriptionMarkdown?.length ?? 0;
49
69
  const levelTitle = String(student.level);
50
70
  return {
51
71
  id: student.id,
@@ -53,7 +73,8 @@ export class ServiceStudent extends BeanBase {
53
73
  mobile: student.mobile,
54
74
  level: student.level,
55
75
  levelTitle,
56
- description: student.description,
76
+ descriptionMarkdown,
77
+ descriptionHtml: student.contentHtml?.descriptionHtml,
57
78
  descriptionLength,
58
79
  summaryText: `${student.name} is in level ${student.level}. Description length: ${descriptionLength}.`,
59
80
  };
@@ -61,14 +82,22 @@ export class ServiceStudent extends BeanBase {
61
82
 
62
83
  async delete(id: TableIdentity) {
63
84
  return await this.scope.model.student.deleteById(id, {
64
- include: getStudentRecordSubjectsInclude(),
85
+ include: getStudentRelationsInclude(),
65
86
  });
66
87
  }
67
88
 
68
89
  async deleteForce(id: TableIdentity) {
69
90
  return await this.scope.model.student.deleteById(id, {
70
91
  disableDeleted: true,
71
- include: getStudentRecordSubjectsInclude(),
92
+ include: getStudentRelationsInclude(),
72
93
  });
73
94
  }
95
+
96
+ private _prepareContent(content: StudentContentInput | undefined) {
97
+ const descriptionMarkdown = content?.descriptionMarkdown ?? '';
98
+ return {
99
+ descriptionMarkdown,
100
+ descriptionHtml: this.bean.markdown.renderHtml(descriptionMarkdown),
101
+ };
102
+ }
74
103
  }
@@ -25,17 +25,36 @@ describe('student.test.ts', () => {
25
25
  const formLayout =
26
26
  blocks?.[0]?.options?.blocks?.[0]?.options?.blocks?.[0]?.options?.formLayout;
27
27
  const tabs = formLayout?.children[0];
28
- const profileSection = tabs?.children[0]?.children[0]?.children[0];
28
+ const groups = tabs?.children[0]?.children;
29
+ const profileSection = groups?.[0]?.children[0];
30
+ const studentContentSection = groups?.[1]?.children[0];
29
31
  const trainingRecordsSection = tabs?.children[1]?.children[1];
30
32
  assert.equal(tabs?.type, 'tabs');
31
33
  assert.equal(tabs?.id, undefined);
32
34
  assert.equal(tabs?.children[1]?.type, 'tab');
33
35
  assert.equal(tabs?.children[1]?.id, undefined);
36
+ assert.equal(groups?.length, 2);
37
+ assert.deepEqual(
38
+ groups?.map(item => item.type),
39
+ ['group', 'group'],
40
+ );
34
41
  assert.deepEqual(profileSection?.columns, { default: 1, md: 2 });
35
42
  assert.deepEqual(
36
43
  profileSection?.children.map(item => item.name),
37
44
  ['name', 'mobile', 'imageId'],
38
45
  );
46
+ const descriptionFieldName =
47
+ DtoClass === DtoStudentView ? '_descriptionMarkdown' : 'content';
48
+ assert.deepEqual(
49
+ studentContentSection?.children.map(item => item.name),
50
+ [descriptionFieldName],
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
+ }
39
58
  assert.equal(tabs?.children[1]?.children[0]?.name, 'level');
40
59
  assert.deepEqual(
41
60
  trainingRecordsSection?.children.map(item => item.name),
@@ -92,7 +111,8 @@ describe('student.test.ts', () => {
92
111
  'name',
93
112
  'mobile',
94
113
  'level',
95
- 'description',
114
+ 'descriptionMarkdown',
115
+ 'descriptionHtml',
96
116
  'levelTitle',
97
117
  'descriptionLength',
98
118
  'summaryText',
@@ -118,9 +138,13 @@ describe('student.test.ts', () => {
118
138
  const maskedMobileUpdate = '139****4321';
119
139
  const trainingTime = new Date('2026-03-10T08:00:00.000Z');
120
140
  const trainingTimeUpdate = new Date('2026-04-18T13:20:00.000Z');
141
+ const description =
142
+ `# Student profile\n\n${'This is a long Markdown description. '.repeat(10)}`.trim();
143
+ const descriptionUpdate =
144
+ `## Updated profile\n\n${'This is updated Markdown content. '.repeat(10)}`.trim();
121
145
  const data = {
122
146
  name: '__Tom__',
123
- description: 'This is a test',
147
+ content: { descriptionMarkdown: description },
124
148
  mobile,
125
149
  level: 1,
126
150
  trainingRecords: [
@@ -157,6 +181,7 @@ describe('student.test.ts', () => {
157
181
  assert.equal(!!studentItem, true);
158
182
  assert.equal(studentItem!.level, data.level);
159
183
  assert.equal(studentItem!.mobile, maskedMobile);
184
+ assert.equal((studentItem as any).description, undefined);
160
185
  // findMany: level filter
161
186
  const selectResByLevel: DtoStudentSelectRes = await app.bean.executor.performAction(
162
187
  'get',
@@ -182,6 +207,7 @@ describe('student.test.ts', () => {
182
207
  const record = student.trainingRecords?.[0];
183
208
  const recordSubject = record?.trainingRecordSubjects?.[0];
184
209
  assert.equal(student.trainingRecords?.length, 1);
210
+ assert.equal(student.content?.descriptionMarkdown, description);
185
211
  assert.equal(record?.name, '__Record__');
186
212
  assert.equal(record?.subjectCount, 1);
187
213
  assert.equal(record?.totalScore, 88);
@@ -193,7 +219,7 @@ describe('student.test.ts', () => {
193
219
  // update
194
220
  const dataUpdate = {
195
221
  name: '__TomNew__',
196
- description: 'This is a test',
222
+ content: { descriptionMarkdown: descriptionUpdate },
197
223
  mobile: mobileUpdate,
198
224
  level: 2,
199
225
  trainingRecords: [
@@ -236,6 +262,7 @@ describe('student.test.ts', () => {
236
262
  assert.equal(student.name, dataUpdate.name);
237
263
  assert.equal(student.level, dataUpdate.level);
238
264
  assert.equal(student.mobile, maskedMobileUpdate);
265
+ assert.equal(student.content?.descriptionMarkdown, descriptionUpdate);
239
266
  assert.equal(student.trainingRecords?.length, 1);
240
267
  assert.equal(updatedRecord?.name, '__RecordNew__');
241
268
  assert.equal(updatedRecord?.subjectCount, 2);
@@ -254,6 +281,15 @@ describe('student.test.ts', () => {
254
281
  disableDeleted: true,
255
282
  });
256
283
  assert.equal(studentRaw!.mobile, mobileUpdate);
284
+ assert.equal((studentRaw as any).description, undefined);
285
+ const studentContent = await app
286
+ .scope('training-student')
287
+ .model.studentContent.get({ studentId });
288
+ assert.equal(studentContent?.descriptionMarkdown, descriptionUpdate);
289
+ assert.equal(
290
+ studentContent?.descriptionHtml,
291
+ app.bean.markdown.renderHtml(descriptionUpdate),
292
+ );
257
293
  // summary
258
294
  const summary: DtoStudentSummary = await app.bean.executor.performAction(
259
295
  'get',
@@ -263,7 +299,9 @@ describe('student.test.ts', () => {
263
299
  assert.equal(summary.name, dataUpdate.name);
264
300
  assert.equal(summary.mobile, maskedMobileUpdate);
265
301
  assert.equal(summary.level, dataUpdate.level);
266
- assert.equal(summary.descriptionLength, dataUpdate.description?.length);
302
+ assert.equal(summary.descriptionMarkdown, descriptionUpdate);
303
+ assert.equal(summary.descriptionHtml, app.bean.markdown.renderHtml(descriptionUpdate));
304
+ assert.equal(summary.descriptionLength, descriptionUpdate.length);
267
305
  assert.equal(typeof summary.levelTitle, 'string');
268
306
  assert.equal(typeof summary.summaryText, 'string');
269
307
  // delete
@@ -271,6 +309,14 @@ describe('student.test.ts', () => {
271
309
  params: { id: student.id },
272
310
  });
273
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
+ );
274
320
  // findOne
275
321
  student = await app.bean.executor.performAction('get', '/training/student/:id', {
276
322
  params: { id: student.id },
@@ -294,11 +340,94 @@ describe('student.test.ts', () => {
294
340
  disableDeleted: true,
295
341
  });
296
342
  assert.equal(studentForce, undefined);
343
+ assert.equal(
344
+ await app
345
+ .scope('training-student')
346
+ .model.studentContent.get({ studentId: studentIdForce }, { disableDeleted: true }),
347
+ undefined,
348
+ );
297
349
  // logout
298
350
  await app.bean.passport.signout();
299
351
  });
300
352
  });
301
353
 
354
+ it('action:student:contentPersistence', async () => {
355
+ await app.bean.executor.mockCtx(async () => {
356
+ await app.bean.passport.signinMock();
357
+ let studentId: string | undefined;
358
+ try {
359
+ studentId = await app.bean.executor.performAction('post', '/training/student', {
360
+ body: {
361
+ name: '__ContentStudent__',
362
+ mobile: '13812345678',
363
+ level: 1,
364
+ },
365
+ });
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, '');
370
+
371
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
372
+ params: { id: studentId },
373
+ body: {
374
+ name: '__ContentStudentUpdated__',
375
+ mobile: '13812345678',
376
+ level: 1,
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,
392
+ descriptionHtml: '<p>Forged HTML</p>',
393
+ },
394
+ },
395
+ });
396
+ const studentContent = await studentContentModel.get({ studentId });
397
+ assert.equal(studentContent?.descriptionMarkdown, markdown);
398
+ assert.equal(studentContent?.descriptionHtml, app.bean.markdown.renderHtml(markdown));
399
+ assert.notEqual(studentContent?.descriptionHtml, '<p>Forged HTML</p>');
400
+
401
+ const whitespaceMarkdown = ' \n\t ';
402
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
403
+ params: { id: studentId },
404
+ body: {
405
+ name: '__ContentStudentUpdated__',
406
+ mobile: '13812345678',
407
+ level: 1,
408
+ content: { descriptionMarkdown: whitespaceMarkdown },
409
+ },
410
+ });
411
+ const whitespaceContent = await studentContentModel.get({ studentId });
412
+ assert.equal(whitespaceContent?.descriptionMarkdown, whitespaceMarkdown);
413
+ assert.equal(
414
+ whitespaceContent?.descriptionHtml,
415
+ app.bean.markdown.renderHtml(whitespaceMarkdown),
416
+ );
417
+ } finally {
418
+ if (studentId) {
419
+ await app
420
+ .scope('training-student')
421
+ .model.studentContent.delete({ studentId }, { disableDeleted: true });
422
+ await app
423
+ .scope('training-student')
424
+ .model.student.deleteById(studentId, { disableDeleted: true });
425
+ }
426
+ await app.bean.passport.signout();
427
+ }
428
+ });
429
+ });
430
+
302
431
  it('action:student:systemAdmin', async () => {
303
432
  await app.bean.executor.mockCtx(async () => {
304
433
  await app.bean.passport.signinMock();
@@ -327,7 +456,7 @@ describe('student.test.ts', () => {
327
456
  await app.bean.executor.performAction('post', '/training/student', {
328
457
  body: {
329
458
  name: '__Tom__',
330
- description: 'This is a test',
459
+ content: { descriptionMarkdown: 'This is a test' },
331
460
  mobile: '13812345678',
332
461
  level: 4,
333
462
  },
@@ -344,7 +473,7 @@ describe('student.test.ts', () => {
344
473
  await app.bean.executor.performAction('post', '/training/student', {
345
474
  body: {
346
475
  name: '__Tom__',
347
- description: 'This is a test',
476
+ content: { descriptionMarkdown: 'This is a test' },
348
477
  level: 1,
349
478
  },
350
479
  });
@@ -360,7 +489,7 @@ describe('student.test.ts', () => {
360
489
  await app.bean.executor.performAction('post', '/training/student', {
361
490
  body: {
362
491
  name: '__Tom__',
363
- description: 'This is a test',
492
+ content: { descriptionMarkdown: 'This is a test' },
364
493
  mobile: '1381234567',
365
494
  level: 1,
366
495
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.153",
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.152"
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.153
874
874
  version: link:../../src/suite-vendor/a-zova
875
875
  devDependencies:
876
876
  clean-package:
@@ -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.49
1080
1080
  version: link:modules/a-form
1081
1081
  zova-module-a-icon:
1082
1082
  specifier: ^5.1.27
@@ -1804,6 +1804,10 @@ importers:
1804
1804
  version: link:modules/training-student
1805
1805
 
1806
1806
  src/suite/a-training/modules/training-student:
1807
+ dependencies:
1808
+ zova-module-basic-markdown:
1809
+ specifier: ^5.0.0
1810
+ version: link:../../../cabloy-basic/modules/basic-markdown
1807
1811
  devDependencies:
1808
1812
  clean-package:
1809
1813
  specifier: ^2.2.0
@@ -11,6 +11,7 @@ export default function (): ZovaOpenapiConfigModule {
11
11
  'CommerceTradeCart_clear',
12
12
  'CommerceTradeCheckout_paymentMethods',
13
13
  'CommerceTradeCheckout_create',
14
+ 'CommerceTradeStockBalance_adjustStock',
14
15
  'CommerceTradeOrder_select',
15
16
  'CommerceTradeOrder_view',
16
17
  'CommerceTradeOrder_ship',
@@ -52,5 +52,10 @@
52
52
  "exports.\\..import"
53
53
  ]
54
54
  },
55
- "title": "commerce-trade"
55
+ "title": "commerce-trade",
56
+ "zovaModule": {
57
+ "dependencies": {
58
+ "basic-app": "5.0.0"
59
+ }
60
+ }
56
61
  }
@@ -0,0 +1,34 @@
1
+ import type { TypeControllerInnerProps } from 'zova';
2
+
3
+ import { defineComponent } from 'vue';
4
+ import { prepareComponentOptions, useController } from 'zova';
5
+
6
+ import type { ControllerTableCellActionAdjustStockProps } from '../../component/tableCellActionAdjustStock/controller.jsx';
7
+
8
+ import { ControllerTableCellActionAdjustStock } from '../../component/tableCellActionAdjustStock/controller.jsx';
9
+ export type ZTableCellActionAdjustStockProps = {
10
+ controllerRef?: (ref: ControllerTableCellActionAdjustStock) => void;
11
+ } & ControllerTableCellActionAdjustStockProps;
12
+
13
+ type ControllerInnerProps = TypeControllerInnerProps<
14
+ ControllerTableCellActionAdjustStockProps,
15
+ keyof typeof ControllerTableCellActionAdjustStock.$propsDefault
16
+ >;
17
+ declare module 'zova-module-commerce-trade' {
18
+ export interface ControllerTableCellActionAdjustStock {
19
+ $props: ControllerInnerProps;
20
+ }
21
+ }
22
+
23
+ export const ZTableCellActionAdjustStock = defineComponent(
24
+ (_props: ZTableCellActionAdjustStockProps) => {
25
+ useController(ControllerTableCellActionAdjustStock, undefined, undefined);
26
+ return () => {};
27
+ },
28
+ prepareComponentOptions(),
29
+ );
30
+ declare module 'zova-module-a-bean' {
31
+ export interface IVonaComponentRecord {
32
+ 'commerce-trade:tableCellActionAdjustStock': ControllerTableCellActionAdjustStockProps;
33
+ }
34
+ }