cabloy 5.1.148 → 5.1.149

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 (36) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/package.json +1 -1
  4. package/repo-docs/.vitepress/config.mjs +4 -0
  5. package/repo-docs/backend/markdown-guide.md +1 -0
  6. package/repo-docs/backend/relations-guide.md +1 -0
  7. package/repo-docs/frontend/form-layout-guide.md +15 -1
  8. package/repo-docs/frontend/markdown-guide.md +1 -0
  9. package/repo-docs/fullstack/edition-collaboration-differences.md +1 -0
  10. package/repo-docs/fullstack/one-to-one-companion-resource-guide.md +447 -0
  11. package/repo-e2e/specs/cabloy-basic.spec.ts +176 -1
  12. package/vona/src/suite/a-training/modules/training-student/package.json +1 -0
  13. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +177 -103
  14. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.index.ts +1 -0
  15. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.version.ts +19 -1
  16. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +3 -0
  17. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +3 -0
  18. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +21 -2
  19. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +7 -1
  20. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +21 -2
  21. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +21 -2
  22. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -3
  23. package/vona/src/suite/a-training/modules/training-student/src/entity/studentContent.tsx +36 -0
  24. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +6 -0
  25. package/vona/src/suite/a-training/modules/training-student/src/model/studentContent.ts +22 -0
  26. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +63 -6
  27. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +117 -7
  28. package/zova/packages-zova/zova/package.json +2 -2
  29. package/zova/pnpm-lock.yaml +4 -0
  30. package/zova/src/suite/a-training/modules/training-student/package.json +3 -1
  31. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +40 -40
  32. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +358 -337
  33. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +18 -7
  34. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  35. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +13 -5
  36. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -10,7 +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
- export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions {}
13
+ const studentContentFormField = $makeMetadata(
14
+ ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
15
+ ZovaRender.field('basic-markdown:formFieldMarkdown'),
16
+ );
17
+
18
+ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions<'studentContentForm'> {}
14
19
 
15
20
  @Dto<IDtoOptionsStudentCreate>({
16
21
  blocks: [
@@ -43,6 +48,16 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions {}
43
48
  },
44
49
  ],
45
50
  },
51
+ {
52
+ type: 'group',
53
+ title: $locale('StudentContent'),
54
+ children: [
55
+ {
56
+ type: 'section',
57
+ children: [{ type: 'field', name: 'studentContentForm' }],
58
+ },
59
+ ],
60
+ },
46
61
  ],
47
62
  },
48
63
  {
@@ -78,6 +93,7 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions {}
78
93
  }),
79
94
  ],
80
95
  fields: {
96
+ studentContentForm: studentContentFormField,
81
97
  trainingRecords: $makeMetadata(
82
98
  v.title($locale('TrainingRecords')),
83
99
  ZovaRender.order(6),
@@ -87,7 +103,10 @@ export interface IDtoOptionsStudentCreate extends IDecoratorDtoOptions {}
87
103
  },
88
104
  })
89
105
  export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
90
- include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
106
+ include: {
107
+ studentContentForm: true,
108
+ trainingRecords: { dtoClass: DtoDetailRecordMutate },
109
+ },
91
110
  }) {
92
111
  @Api.field(ZovaRender.visible(false), v.optional(), v.array(DtoDetailRecordResItem))
93
112
  _trainingRecords?: DtoDetailRecordResItem[];
@@ -11,8 +11,14 @@ export interface IDtoOptionsStudentSummary extends IDecoratorDtoOptions {}
11
11
 
12
12
  @Dto<IDtoOptionsStudentSummary>()
13
13
  export class DtoStudentSummary extends $Dto.get(() => ModelStudent, {
14
- columns: ['id', 'name', 'mobile', 'level', 'description'],
14
+ columns: ['id', 'name', 'mobile', 'level'],
15
15
  }) {
16
+ @Api.field(v.title($locale('Description')), v.optional())
17
+ description?: string;
18
+
19
+ @Api.field(v.title($locale('DescriptionHtml')), v.optional())
20
+ descriptionHtml?: string;
21
+
16
22
  @Api.field(v.title($locale('LevelTitle')))
17
23
  levelTitle: string;
18
24
 
@@ -10,7 +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
- export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions {}
13
+ const studentContentFormField = $makeMetadata(
14
+ ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
15
+ ZovaRender.field('basic-markdown:formFieldMarkdown'),
16
+ );
17
+
18
+ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions<'studentContentForm'> {}
14
19
 
15
20
  @Dto<IDtoOptionsStudentUpdate>({
16
21
  blocks: [
@@ -43,6 +48,16 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions {}
43
48
  },
44
49
  ],
45
50
  },
51
+ {
52
+ type: 'group',
53
+ title: $locale('StudentContent'),
54
+ children: [
55
+ {
56
+ type: 'section',
57
+ children: [{ type: 'field', name: 'studentContentForm' }],
58
+ },
59
+ ],
60
+ },
46
61
  ],
47
62
  },
48
63
  {
@@ -75,6 +90,7 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions {}
75
90
  }),
76
91
  ],
77
92
  fields: {
93
+ studentContentForm: studentContentFormField,
78
94
  trainingRecords: $makeMetadata(
79
95
  v.title($locale('TrainingRecords')),
80
96
  ZovaRender.order(6),
@@ -83,7 +99,10 @@ export interface IDtoOptionsStudentUpdate extends IDecoratorDtoOptions {}
83
99
  },
84
100
  })
85
101
  export class DtoStudentUpdate extends $Dto.update(() => ModelStudent, {
86
- include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
102
+ include: {
103
+ studentContentForm: true,
104
+ trainingRecords: { dtoClass: DtoDetailRecordMutate },
105
+ },
87
106
  }) {
88
107
  @Api.field(ZovaRender.visible(false), v.optional(), v.array(DtoDetailRecordResItem))
89
108
  _trainingRecords?: DtoDetailRecordResItem[];
@@ -11,7 +11,12 @@ import { ModelStudent } from '../model/student.ts';
11
11
  import { DtoDetailRecordResItem } from './detailRecordResItem.tsx';
12
12
  import { DtoDetailRecordView } from './detailRecordView.tsx';
13
13
 
14
- export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
14
+ const studentContentFormField = $makeMetadata(
15
+ ZovaRender.fieldSource('studentContentForm.descriptionMarkdown'),
16
+ ZovaRender.field('basic-markdown:formFieldMarkdown'),
17
+ );
18
+
19
+ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions<'studentContentForm'> {}
15
20
 
16
21
  @Dto<IDtoOptionsStudentView>({
17
22
  blocks: [
@@ -44,6 +49,16 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
44
49
  },
45
50
  ],
46
51
  },
52
+ {
53
+ type: 'group',
54
+ title: $locale('StudentContent'),
55
+ children: [
56
+ {
57
+ type: 'section',
58
+ children: [{ type: 'field', name: 'studentContentForm' }],
59
+ },
60
+ ],
61
+ },
47
62
  ],
48
63
  },
49
64
  {
@@ -73,6 +88,7 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
73
88
  }),
74
89
  ],
75
90
  fields: {
91
+ studentContentForm: studentContentFormField,
76
92
  trainingRecords: $makeMetadata(
77
93
  v.title($locale('TrainingRecords')),
78
94
  ZovaRender.order(6),
@@ -81,7 +97,10 @@ export interface IDtoOptionsStudentView extends IDecoratorDtoOptions {}
81
97
  },
82
98
  })
83
99
  export class DtoStudentView extends $Dto.get(() => ModelStudent, {
84
- include: { trainingRecords: { dtoClass: DtoDetailRecordView } },
100
+ include: {
101
+ studentContentForm: true,
102
+ trainingRecords: { dtoClass: DtoDetailRecordView },
103
+ },
85
104
  }) {
86
105
  @Api.field(
87
106
  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
+ studentContent: $relation.hasOne('training-student:studentContent', 'studentId', {
26
+ columns: ['id', 'studentId', 'descriptionMarkdown', 'descriptionHtml'],
27
+ }),
28
+ studentContentForm: $relation.hasOne('training-student:studentContent', 'studentId', {
29
+ columns: ['id', 'studentId', 'descriptionMarkdown'],
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> {}
@@ -18,12 +18,27 @@ function getStudentRecordSubjectsInclude(): {
18
18
  return { trainingRecords: { include: { trainingRecordSubjects: true } } };
19
19
  }
20
20
 
21
+ function getStudentContentFormInclude() {
22
+ return { studentContentForm: true };
23
+ }
24
+
25
+ function getStudentContentInclude() {
26
+ return { studentContent: true };
27
+ }
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, {
32
+ const { studentContentForm, ...studentFields } = student as DtoStudentCreate & {
33
+ studentContentForm?: { descriptionMarkdown?: string };
34
+ };
35
+ const created = await this.scope.model.student.insert(studentFields, {
25
36
  include: getStudentRecordSubjectsInclude(),
26
37
  });
38
+ if (studentContentForm !== undefined) {
39
+ await this._updateStudentContent(created.id, studentContentForm.descriptionMarkdown);
40
+ }
41
+ return created;
27
42
  }
28
43
 
29
44
  async select(params?: IQueryParams<ModelStudent>): Promise<DtoStudentSelectRes> {
@@ -32,20 +47,34 @@ export class ServiceStudent extends BeanBase {
32
47
 
33
48
  async view(id: TableIdentity): Promise<DtoStudentView | undefined> {
34
49
  return await this.scope.model.student.getById(id, {
35
- include: getStudentRecordSubjectsInclude(),
50
+ include: {
51
+ ...getStudentRecordSubjectsInclude(),
52
+ ...getStudentContentFormInclude(),
53
+ },
36
54
  });
37
55
  }
38
56
 
39
57
  async update(id: TableIdentity, student: DtoStudentUpdate) {
40
- return await this.scope.model.student.updateById(id, student, {
58
+ const { studentContentForm, ...studentFields } = student as DtoStudentUpdate & {
59
+ studentContentForm?: { descriptionMarkdown?: string };
60
+ };
61
+ const updateResult = await this.scope.model.student.updateById(id, studentFields, {
41
62
  include: getStudentRecordSubjectsInclude(),
42
63
  });
64
+ if (studentContentForm !== undefined) {
65
+ await this._updateStudentContent(id, studentContentForm.descriptionMarkdown);
66
+ }
67
+ return updateResult;
43
68
  }
44
69
 
45
70
  async summary(id: TableIdentity): Promise<DtoStudentSummary | undefined> {
46
- const student = await this.scope.model.student.getById(id);
71
+ const student = await this.scope.model.student.getById(id, {
72
+ include: getStudentContentInclude(),
73
+ });
47
74
  if (!student) return undefined;
48
- const descriptionLength = student.description?.length ?? 0;
75
+ const description = student.studentContent?.descriptionMarkdown;
76
+ const descriptionHtml = student.studentContent?.descriptionHtml;
77
+ const descriptionLength = description?.length ?? 0;
49
78
  const levelTitle = String(student.level);
50
79
  return {
51
80
  id: student.id,
@@ -53,22 +82,50 @@ export class ServiceStudent extends BeanBase {
53
82
  mobile: student.mobile,
54
83
  level: student.level,
55
84
  levelTitle,
56
- description: student.description,
85
+ description,
86
+ descriptionHtml,
57
87
  descriptionLength,
58
88
  summaryText: `${student.name} is in level ${student.level}. Description length: ${descriptionLength}.`,
59
89
  };
60
90
  }
61
91
 
62
92
  async delete(id: TableIdentity) {
93
+ await this.scope.model.studentContent.delete({ studentId: id });
63
94
  return await this.scope.model.student.deleteById(id, {
64
95
  include: getStudentRecordSubjectsInclude(),
65
96
  });
66
97
  }
67
98
 
68
99
  async deleteForce(id: TableIdentity) {
100
+ await this.scope.model.studentContent.delete({ studentId: id }, { disableDeleted: true });
69
101
  return await this.scope.model.student.deleteById(id, {
70
102
  disableDeleted: true,
71
103
  include: getStudentRecordSubjectsInclude(),
72
104
  });
73
105
  }
106
+
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
+ }
130
+ }
74
131
  }
@@ -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
+ assert.deepEqual(
47
+ 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',
57
+ );
39
58
  assert.equal(tabs?.children[1]?.children[0]?.name, 'level');
40
59
  assert.deepEqual(
41
60
  trainingRecordsSection?.children.map(item => item.name),
@@ -93,6 +112,7 @@ describe('student.test.ts', () => {
93
112
  'mobile',
94
113
  'level',
95
114
  'description',
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
+ studentContentForm: { 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.studentContentForm?.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
+ studentContentForm: { 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.studentContentForm?.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.description, 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
@@ -294,11 +332,83 @@ describe('student.test.ts', () => {
294
332
  disableDeleted: true,
295
333
  });
296
334
  assert.equal(studentForce, undefined);
335
+ assert.equal(
336
+ await app
337
+ .scope('training-student')
338
+ .model.studentContent.get({ studentId: studentIdForce }, { disableDeleted: true }),
339
+ undefined,
340
+ );
297
341
  // logout
298
342
  await app.bean.passport.signout();
299
343
  });
300
344
  });
301
345
 
346
+ it('action:student:contentPersistence', async () => {
347
+ await app.bean.executor.mockCtx(async () => {
348
+ await app.bean.passport.signinMock();
349
+ let studentId: string | undefined;
350
+ try {
351
+ studentId = await app.bean.executor.performAction('post', '/training/student', {
352
+ body: {
353
+ name: '__ContentStudent__',
354
+ mobile: '13812345678',
355
+ level: 1,
356
+ },
357
+ });
358
+ assert.equal(
359
+ await app.scope('training-student').model.studentContent.get({ studentId }),
360
+ undefined,
361
+ );
362
+
363
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
364
+ params: { id: studentId },
365
+ body: {
366
+ name: '__ContentStudent__',
367
+ mobile: '13812345678',
368
+ level: 1,
369
+ studentContentForm: {
370
+ descriptionMarkdown: ' ## Created later ',
371
+ descriptionHtml: '<p>Forged HTML</p>',
372
+ },
373
+ },
374
+ });
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
+ );
383
+ assert.notEqual(studentContent?.descriptionHtml, '<p>Forged HTML</p>');
384
+
385
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
386
+ params: { id: studentId },
387
+ body: {
388
+ name: '__ContentStudent__',
389
+ mobile: '13812345678',
390
+ level: 1,
391
+ studentContentForm: { descriptionMarkdown: ' \n\t ' },
392
+ },
393
+ });
394
+ assert.equal(
395
+ await app.scope('training-student').model.studentContent.get({ studentId }),
396
+ undefined,
397
+ );
398
+ } finally {
399
+ if (studentId) {
400
+ await app
401
+ .scope('training-student')
402
+ .model.studentContent.delete({ studentId }, { disableDeleted: true });
403
+ await app
404
+ .scope('training-student')
405
+ .model.student.deleteById(studentId, { disableDeleted: true });
406
+ }
407
+ await app.bean.passport.signout();
408
+ }
409
+ });
410
+ });
411
+
302
412
  it('action:student:systemAdmin', async () => {
303
413
  await app.bean.executor.mockCtx(async () => {
304
414
  await app.bean.passport.signinMock();
@@ -327,7 +437,7 @@ describe('student.test.ts', () => {
327
437
  await app.bean.executor.performAction('post', '/training/student', {
328
438
  body: {
329
439
  name: '__Tom__',
330
- description: 'This is a test',
440
+ studentContentForm: { descriptionMarkdown: 'This is a test' },
331
441
  mobile: '13812345678',
332
442
  level: 4,
333
443
  },
@@ -344,7 +454,7 @@ describe('student.test.ts', () => {
344
454
  await app.bean.executor.performAction('post', '/training/student', {
345
455
  body: {
346
456
  name: '__Tom__',
347
- description: 'This is a test',
457
+ studentContentForm: { descriptionMarkdown: 'This is a test' },
348
458
  level: 1,
349
459
  },
350
460
  });
@@ -360,7 +470,7 @@ describe('student.test.ts', () => {
360
470
  await app.bean.executor.performAction('post', '/training/student', {
361
471
  body: {
362
472
  name: '__Tom__',
363
- description: 'This is a test',
473
+ studentContentForm: { descriptionMarkdown: 'This is a test' },
364
474
  mobile: '1381234567',
365
475
  level: 1,
366
476
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.153",
3
+ "version": "5.1.154",
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.153"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -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
@@ -33,7 +33,9 @@
33
33
  "prepack": "clean-package",
34
34
  "postpack": "clean-package restore"
35
35
  },
36
- "dependencies": {},
36
+ "dependencies": {
37
+ "zova-module-basic-markdown": "^5.0.0"
38
+ },
37
39
  "devDependencies": {
38
40
  "clean-package": "^2.2.0",
39
41
  "rimraf": "^6.1.3"