@skopon-cool/form-sdk 0.1.1 → 0.1.4
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.
- package/README.md +49 -11
- package/dist/adapter/a2uiAdapter.d.ts +0 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formFileAccept.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts +1 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/case_singleselect/adapter.d.ts +10 -0
- package/dist/blocks/case_singleselect/adapter.d.ts.map +1 -0
- package/dist/blocks/case_singleselect/catalog.d.ts +2 -0
- package/dist/blocks/case_singleselect/catalog.d.ts.map +1 -0
- package/dist/blocks/case_singleselect/index.d.ts +3 -0
- package/dist/blocks/case_singleselect/index.d.ts.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts +10 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts +2 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/index.d.ts +3 -0
- package/dist/blocks/resume_multiselect/index.d.ts.map +1 -0
- package/dist/blocks/types.d.ts +27 -0
- package/dist/blocks/types.d.ts.map +1 -0
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/caseSearchContext.d.ts +28 -0
- package/dist/catalog/caseSearchContext.d.ts.map +1 -0
- package/dist/catalog/resumeSearchContext.d.ts +30 -0
- package/dist/catalog/resumeSearchContext.d.ts.map +1 -0
- package/dist/catalog/skoponCaseSelect.d.ts +2 -0
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -0
- package/dist/catalog/skoponResumeSelect.d.ts +2 -0
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -0
- package/dist/catalog/useSkoponBoundField.d.ts +2 -0
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/client/formClient.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +10 -2
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +7 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.test.d.ts +2 -0
- package/dist/components/SkoponA2uiStreamRenderer.test.d.ts.map +1 -0
- package/dist/components/SkoponFormRenderer.d.ts +6 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1390 -710
- package/dist/submit/buildCurlStatement.d.ts +8 -0
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts +26 -0
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -0
- package/dist/submit/submitFormJson.d.ts.map +1 -1
- package/dist/types/index.d.ts +26 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/adapter/a2uiAdapter.test.ts +116 -0
- package/src/adapter/a2uiAdapter.ts +48 -4
- package/src/adapter/formFileAccept.test.ts +53 -0
- package/src/adapter/formFileAccept.ts +11 -2
- package/src/adapter/formSchema.test.ts +35 -0
- package/src/adapter/formSchema.ts +70 -3
- package/src/blocks/case_singleselect/adapter.ts +74 -0
- package/src/blocks/case_singleselect/catalog.ts +1 -0
- package/src/blocks/case_singleselect/index.ts +14 -0
- package/src/blocks/registry.ts +34 -0
- package/src/blocks/resume_multiselect/adapter.ts +57 -0
- package/src/blocks/resume_multiselect/catalog.ts +1 -0
- package/src/blocks/resume_multiselect/index.ts +14 -0
- package/src/blocks/types.ts +34 -0
- package/src/catalog/a2uiCustomCatalog.tsx +34 -5
- package/src/catalog/caseSearchContext.tsx +46 -0
- package/src/catalog/resumeSearchContext.tsx +48 -0
- package/src/catalog/skoponCaseSelect.tsx +215 -0
- package/src/catalog/skoponResumeSelect.tsx +227 -0
- package/src/catalog/textFieldPreview.test.tsx +1 -1
- package/src/catalog/useSkoponBoundField.test.ts +62 -0
- package/src/catalog/useSkoponBoundField.ts +10 -1
- package/src/client/formClient.test.ts +83 -0
- package/src/client/formClient.ts +10 -2
- package/src/components/AskUserFormCard.tsx +146 -58
- package/src/components/SkoponA2uiStreamRenderer.test.ts +78 -0
- package/src/components/SkoponA2uiStreamRenderer.test.tsx +103 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +141 -23
- package/src/components/SkoponFormRenderer.tsx +42 -17
- package/src/index.ts +34 -2
- package/src/styles/index.css +65 -0
- package/src/submit/buildCurlStatement.ts +49 -0
- package/src/submit/intersectPayloadBlocksWithForm.ts +175 -0
- package/src/submit/submit.test.ts +170 -10
- package/src/submit/submitFormJson.ts +20 -1
- package/src/types/index.ts +30 -0
- package/dist/submit/intersectPayloadWithForm.d.ts +0 -17
- package/dist/submit/intersectPayloadWithForm.d.ts.map +0 -1
- package/src/submit/intersectPayloadWithForm.ts +0 -54
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export declare function buildCurlStatement(payload: unknown, callbackUrl?: string | null): string;
|
|
2
|
+
/** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
|
|
3
|
+
export declare function buildAskUserCurlBodyJson(cardValues: Record<string, unknown>, extraValues: Record<string, unknown>): string;
|
|
4
|
+
export interface BuildAskUserCurlStatementOptions {
|
|
5
|
+
cardValues: Record<string, unknown>;
|
|
6
|
+
extraValues?: Record<string, unknown>;
|
|
7
|
+
callbackUrl?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildAskUserCurlStatement({ cardValues, extraValues, callbackUrl, }: BuildAskUserCurlStatementOptions): string;
|
|
2
10
|
//# sourceMappingURL=buildCurlStatement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF"}
|
|
1
|
+
{"version":3,"file":"buildCurlStatement.d.ts","sourceRoot":"","sources":["../../src/submit/buildCurlStatement.ts"],"names":[],"mappings":"AAIA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQxF;AAMD,gDAAgD;AAChD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAmBR;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,yBAAyB,CAAC,EACxC,UAAU,EACV,WAAgB,EAChB,WAAW,GACZ,EAAE,gCAAgC,GAAG,MAAM,CAQ3C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormBlock, FormSchema } from '../types/index';
|
|
2
|
+
export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' | 'blocks'>;
|
|
3
|
+
export interface PayloadBlocksIntersection {
|
|
4
|
+
matchedBlocks: FormBlock[];
|
|
5
|
+
extraBlocks: FormBlock[];
|
|
6
|
+
/** 按 payload 顺序:layout + matched input(不含 extra input) */
|
|
7
|
+
renderBlocks: FormBlock[];
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
/** 解析 ask_user payload 为 blocksJson 形(title / description / blocks) */
|
|
12
|
+
export declare function parsePayloadBlocksJson(payload: unknown): PayloadBlocksDefinition | null;
|
|
13
|
+
/**
|
|
14
|
+
* payload.blocks 与 vt_forms 按 input 块 name 取交集用于渲染;
|
|
15
|
+
* payload 中 form 不存在的块归入 extraBlocks(不渲染,提交时写入 curl)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function intersectPayloadBlocksWithForm(payloadDef: PayloadBlocksDefinition, formDefinition: FormSchema | undefined): PayloadBlocksIntersection;
|
|
18
|
+
/** 从 extraBlocks 提取提交值(优先 defaultValue) */
|
|
19
|
+
export declare function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown>;
|
|
20
|
+
/** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
|
|
21
|
+
export declare function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean;
|
|
22
|
+
/** 交集为空时 fallback:payload 中可上屏的 blocks(layout + input) */
|
|
23
|
+
export declare function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[];
|
|
24
|
+
/** payload 中所有 input 块 field name(fallback 提交用) */
|
|
25
|
+
export declare function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[];
|
|
26
|
+
//# sourceMappingURL=intersectPayloadBlocksWithForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intersectPayloadBlocksWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadBlocksWithForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,QAAQ,CAAC,CAAA;AAE1F,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,0DAA0D;IAC1D,YAAY,EAAE,SAAS,EAAE,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AAqDD,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAUvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CAqC3B;AAmBD,2CAA2C;AAC3C,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzF;AAED,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAIlF;AAED,0DAA0D;AAC1D,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAK3F;AAED,mDAAmD;AACnD,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitFormJson.d.ts","sourceRoot":"","sources":["../../src/submit/submitFormJson.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"submitFormJson.d.ts","sourceRoot":"","sources":["../../src/submit/submitFormJson.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export type A2uiBinding<T> = T | {
|
|
5
5
|
path: string;
|
|
6
6
|
};
|
|
7
|
-
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect';
|
|
7
|
+
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
|
|
8
8
|
export interface A2uiComponentNode {
|
|
9
9
|
id: string;
|
|
10
10
|
component: A2uiComponentName | string;
|
|
@@ -21,7 +21,7 @@ export interface A2uiSurfaceDoc {
|
|
|
21
21
|
}
|
|
22
22
|
export declare const A2UI_PROTOCOL_VERSION: "v0.9";
|
|
23
23
|
export type FormJsonSchema = Record<string, unknown>;
|
|
24
|
-
export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'image' | 'video' | 'audio';
|
|
24
|
+
export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'resume_multiselect' | 'case_singleselect' | 'image' | 'video' | 'audio';
|
|
25
25
|
export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
|
|
26
26
|
export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
|
|
27
27
|
export declare const FORM_MEDIA_SIZES: FormMediaSize[];
|
|
@@ -29,6 +29,22 @@ export interface FormBlockOption {
|
|
|
29
29
|
value: string;
|
|
30
30
|
label: string;
|
|
31
31
|
}
|
|
32
|
+
/** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
|
|
33
|
+
export interface FormResumeFilter {
|
|
34
|
+
names?: string[];
|
|
35
|
+
agentUniqueIds?: string[];
|
|
36
|
+
resumeUniqueIds?: string[];
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
}
|
|
39
|
+
/** 案例单选组件:渲染时用于 /univ/case/list 的筛选条件 */
|
|
40
|
+
export interface FormCaseFilter {
|
|
41
|
+
/** Agent 类型(编辑器辅助,运行时不用) */
|
|
42
|
+
agentKind?: string;
|
|
43
|
+
/** Agent unique_id(编辑器辅助,运行时不用) */
|
|
44
|
+
agentUniqueId?: string;
|
|
45
|
+
flowId?: number;
|
|
46
|
+
pageSize?: number;
|
|
47
|
+
}
|
|
32
48
|
export interface FormBlock {
|
|
33
49
|
id: string;
|
|
34
50
|
type: FormBlockType;
|
|
@@ -47,6 +63,14 @@ export interface FormBlock {
|
|
|
47
63
|
fileMinCount?: number;
|
|
48
64
|
fileMaxCount?: number;
|
|
49
65
|
defaultValue?: string | string[] | boolean;
|
|
66
|
+
/** 简历多选:是否显示「换一批」 */
|
|
67
|
+
resumeEnableRefresh?: boolean;
|
|
68
|
+
/** 简历多选:列表筛选条件(持久化进 block json) */
|
|
69
|
+
resumeFilter?: FormResumeFilter;
|
|
70
|
+
/** 案例单选:是否显示「换一批」 */
|
|
71
|
+
caseEnableRefresh?: boolean;
|
|
72
|
+
/** 案例单选:列表筛选条件(持久化进 block json) */
|
|
73
|
+
caseFilter?: FormCaseFilter;
|
|
50
74
|
}
|
|
51
75
|
export interface FormSchema {
|
|
52
76
|
title?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,KAAK,GACL,MAAM,GACN,OAAO,GACP,OAAO,GACP,aAAa,GACb,WAAW,GACX,UAAU,GACV,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.4",
|
|
7
7
|
"description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": [
|
|
@@ -29,31 +29,36 @@
|
|
|
29
29
|
"test": "vitest run",
|
|
30
30
|
"test:watch": "vitest"
|
|
31
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"dayjs": "^1.11.13",
|
|
34
|
+
"zod": "^3.24.0"
|
|
35
|
+
},
|
|
32
36
|
"peerDependencies": {
|
|
33
|
-
"@ant-design/icons": "^6.0.0",
|
|
34
37
|
"@a2ui/react": "^0.10.1",
|
|
35
38
|
"@a2ui/web_core": "^0.10.2",
|
|
39
|
+
"@ant-design/icons": "^6.0.0",
|
|
36
40
|
"antd": "^6.4.3",
|
|
37
|
-
"dayjs": "^1.11.0",
|
|
38
41
|
"react": "^18.0.0 || ^19.0.0",
|
|
39
42
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"@ant-design/icons": "^6.2.3",
|
|
43
45
|
"@a2ui/react": "^0.10.1",
|
|
44
46
|
"@a2ui/web_core": "^0.10.2",
|
|
47
|
+
"@ant-design/icons": "^6.2.3",
|
|
48
|
+
"@testing-library/react": "^16.3.2",
|
|
45
49
|
"@types/react": "^19.1.2",
|
|
46
50
|
"@types/react-dom": "^19.1.2",
|
|
47
51
|
"@vitejs/plugin-react": "^4.5.2",
|
|
48
52
|
"antd": "^6.4.3",
|
|
49
53
|
"dayjs": "^1.11.13",
|
|
54
|
+
"happy-dom": "^17.6.3",
|
|
55
|
+
"jsdom": "^29.1.1",
|
|
50
56
|
"react": "^19.1.0",
|
|
51
57
|
"react-dom": "^19.1.0",
|
|
52
58
|
"typescript": "~5.8.3",
|
|
53
59
|
"vite": "^6.3.5",
|
|
54
60
|
"vite-plugin-dts": "^4.5.4",
|
|
55
61
|
"vitest": "^3.2.4",
|
|
56
|
-
"
|
|
57
|
-
"@testing-library/react": "^16.3.2"
|
|
62
|
+
"zod": "^3.24.0"
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
1
4
|
import { describe, it, expect } from 'vitest'
|
|
2
5
|
import type { FormSchema } from '../types/index'
|
|
3
6
|
import { a2uiToBlocks, blocksToA2ui, isA2uiSurfaceEmpty, surfaceDocToMessages } from './a2uiAdapter'
|
|
@@ -120,6 +123,71 @@ describe('a2uiAdapter blocks <-> surface', () => {
|
|
|
120
123
|
expect(back.blocks.find((b) => b.name === 'tags')?.type).toBe('multiselect')
|
|
121
124
|
})
|
|
122
125
|
|
|
126
|
+
it('blocksToA2ui stores single-select and radio defaults as strings in dataModel', () => {
|
|
127
|
+
const doc = blocksToA2ui(
|
|
128
|
+
{
|
|
129
|
+
title: '',
|
|
130
|
+
description: '',
|
|
131
|
+
blocks: [
|
|
132
|
+
{
|
|
133
|
+
id: 'b-city',
|
|
134
|
+
type: 'select',
|
|
135
|
+
name: 'city',
|
|
136
|
+
label: '城市',
|
|
137
|
+
defaultValue: 'bj',
|
|
138
|
+
options: [{ label: '北京', value: 'bj' }],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'b-gender',
|
|
142
|
+
type: 'radio',
|
|
143
|
+
name: 'gender',
|
|
144
|
+
label: '性别',
|
|
145
|
+
defaultValue: 'm',
|
|
146
|
+
options: [
|
|
147
|
+
{ label: '男', value: 'm' },
|
|
148
|
+
{ label: '女', value: 'f' },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'b-tags',
|
|
153
|
+
type: 'multiselect',
|
|
154
|
+
name: 'tags',
|
|
155
|
+
label: '标签',
|
|
156
|
+
defaultValue: ['a', 'b'],
|
|
157
|
+
options: [
|
|
158
|
+
{ label: 'A', value: 'a' },
|
|
159
|
+
{ label: 'B', value: 'b' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{ includeHeader: false },
|
|
165
|
+
)
|
|
166
|
+
expect(doc.dataModel?.city).toBe('bj')
|
|
167
|
+
expect(doc.dataModel?.gender).toBe('m')
|
|
168
|
+
expect(doc.dataModel?.tags).toEqual(['a', 'b'])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('blocksToA2ui stores toggle string default "true" as true in dataModel', () => {
|
|
172
|
+
const doc = blocksToA2ui(
|
|
173
|
+
{
|
|
174
|
+
title: '',
|
|
175
|
+
description: '',
|
|
176
|
+
blocks: [
|
|
177
|
+
{
|
|
178
|
+
id: 'b-sub',
|
|
179
|
+
type: 'toggle',
|
|
180
|
+
name: 'subscribe',
|
|
181
|
+
label: '订阅',
|
|
182
|
+
defaultValue: 'true',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{ includeHeader: false },
|
|
187
|
+
)
|
|
188
|
+
expect(doc.dataModel?.subscribe).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
123
191
|
it('round-trips textarea placeholder through blocksToA2ui', () => {
|
|
124
192
|
const doc = blocksToA2ui(
|
|
125
193
|
{
|
|
@@ -170,4 +238,52 @@ describe('a2uiAdapter blocks <-> surface', () => {
|
|
|
170
238
|
const withHeader = blocksToA2ui(makeDefinition())
|
|
171
239
|
expect((withHeader.components.find((c) => c.id === 'root')?.children as string[]).length).toBe(12)
|
|
172
240
|
})
|
|
241
|
+
|
|
242
|
+
it('round-trips case_singleselect through blocksToA2ui and a2uiToBlocks', () => {
|
|
243
|
+
const doc = blocksToA2ui(
|
|
244
|
+
{
|
|
245
|
+
title: '',
|
|
246
|
+
description: '',
|
|
247
|
+
blocks: [
|
|
248
|
+
{
|
|
249
|
+
id: 'b-case',
|
|
250
|
+
type: 'case_singleselect',
|
|
251
|
+
name: 'selected_case',
|
|
252
|
+
label: '选择案例',
|
|
253
|
+
placeholder: '暂无案例',
|
|
254
|
+
help: '请选择一个案例',
|
|
255
|
+
caseEnableRefresh: true,
|
|
256
|
+
caseFilter: {
|
|
257
|
+
agentKind: 'public-user',
|
|
258
|
+
agentUniqueId: 'agent-1',
|
|
259
|
+
flowId: 42,
|
|
260
|
+
pageSize: 10,
|
|
261
|
+
},
|
|
262
|
+
defaultValue: 'case-abc',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{ includeHeader: false },
|
|
267
|
+
)
|
|
268
|
+
const node = doc.components.find((c) => c.id === 'b-case')
|
|
269
|
+
expect(node?.component).toBe('SkoponCaseSelect')
|
|
270
|
+
expect(node?.enableRefresh).toBe(true)
|
|
271
|
+
expect(node?.caseFilter).toEqual({
|
|
272
|
+
agentKind: 'public-user',
|
|
273
|
+
agentUniqueId: 'agent-1',
|
|
274
|
+
flowId: 42,
|
|
275
|
+
pageSize: 10,
|
|
276
|
+
})
|
|
277
|
+
expect(doc.dataModel?.selected_case).toBe('case-abc')
|
|
278
|
+
|
|
279
|
+
const back = a2uiToBlocks(doc)
|
|
280
|
+
const block = back.blocks.find((b) => b.name === 'selected_case')
|
|
281
|
+
expect(block?.type).toBe('case_singleselect')
|
|
282
|
+
expect(block?.caseEnableRefresh).toBe(true)
|
|
283
|
+
expect(block?.caseFilter?.flowId).toBe(42)
|
|
284
|
+
expect(block?.caseFilter?.pageSize).toBe(10)
|
|
285
|
+
expect(block?.caseFilter?.agentKind).toBe('public-user')
|
|
286
|
+
expect(block?.caseFilter?.agentUniqueId).toBe('agent-1')
|
|
287
|
+
expect(block?.defaultValue).toBe('case-abc')
|
|
288
|
+
})
|
|
173
289
|
})
|
|
@@ -8,8 +8,10 @@ import type {
|
|
|
8
8
|
FormSchema,
|
|
9
9
|
} from '../types/index'
|
|
10
10
|
import { generateId } from './id'
|
|
11
|
+
import { coerceToggleValue } from './formSchema'
|
|
11
12
|
import { getMediaUrls, normalizeMediaSize } from './formMedia'
|
|
12
13
|
import { syncFormDefinition } from './formSchema'
|
|
14
|
+
import { getBlockTypeAdapter, getComponentAdapter } from '../blocks/registry'
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* A2UI surface <-> FormBlock 双向适配器。
|
|
@@ -95,14 +97,23 @@ function readMediaBlock(
|
|
|
95
97
|
function defaultValueForBlock(block: FormBlock): unknown {
|
|
96
98
|
switch (block.type) {
|
|
97
99
|
case 'toggle':
|
|
98
|
-
return
|
|
100
|
+
return coerceToggleValue(block.defaultValue)
|
|
99
101
|
case 'multiselect':
|
|
100
102
|
case 'checkbox':
|
|
101
|
-
case '
|
|
102
|
-
case 'radio':
|
|
103
|
+
case 'resume_multiselect':
|
|
103
104
|
if (Array.isArray(block.defaultValue)) return block.defaultValue
|
|
104
105
|
if (typeof block.defaultValue === 'string' && block.defaultValue) return [block.defaultValue]
|
|
105
106
|
return []
|
|
107
|
+
case 'select':
|
|
108
|
+
case 'radio':
|
|
109
|
+
case 'case_singleselect':
|
|
110
|
+
if (Array.isArray(block.defaultValue)) return block.defaultValue[0] ?? ''
|
|
111
|
+
if (typeof block.defaultValue === 'string') return block.defaultValue
|
|
112
|
+
return ''
|
|
113
|
+
case 'number':
|
|
114
|
+
if (typeof block.defaultValue === 'number') return block.defaultValue
|
|
115
|
+
if (typeof block.defaultValue === 'string' && block.defaultValue) return block.defaultValue
|
|
116
|
+
return null
|
|
106
117
|
default:
|
|
107
118
|
if (typeof block.defaultValue === 'string') return block.defaultValue
|
|
108
119
|
return ''
|
|
@@ -127,6 +138,12 @@ function blockToComponent(block: FormBlock): {
|
|
|
127
138
|
dataValue?: unknown
|
|
128
139
|
} => (name ? { node, dataKey: name, dataValue: defaultValueForBlock(block) } : { node })
|
|
129
140
|
|
|
141
|
+
const adapterCtx = { id, label, name, path, withData }
|
|
142
|
+
const typeAdapter = getBlockTypeAdapter(block.type)
|
|
143
|
+
if (typeAdapter) {
|
|
144
|
+
return typeAdapter.toComponent(block, adapterCtx)
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
switch (block.type) {
|
|
131
148
|
case 'heading':
|
|
132
149
|
return { node: { id, component: 'Text', text: label, variant: 'h3' } }
|
|
@@ -347,6 +364,11 @@ function componentToBlock(node: A2uiComponentNode): FormBlock | null {
|
|
|
347
364
|
|
|
348
365
|
const base = (type: FormBlockType): FormBlock => ({ id, type, label })
|
|
349
366
|
|
|
367
|
+
const componentAdapter = getComponentAdapter(component)
|
|
368
|
+
if (componentAdapter) {
|
|
369
|
+
return componentAdapter.fromComponent(node, base, { asLiteral, nameFromValue })
|
|
370
|
+
}
|
|
371
|
+
|
|
350
372
|
switch (component) {
|
|
351
373
|
case 'Text': {
|
|
352
374
|
const variant = String(node.variant ?? 'body')
|
|
@@ -461,6 +483,23 @@ function collectOrder(
|
|
|
461
483
|
}
|
|
462
484
|
|
|
463
485
|
/** A2UI surface 文档 -> FormSchema(已 sync 出 jsonSchema) */
|
|
486
|
+
function applyDataModelDefaults(
|
|
487
|
+
blocks: FormBlock[],
|
|
488
|
+
dataModel: Record<string, unknown> | undefined,
|
|
489
|
+
): FormBlock[] {
|
|
490
|
+
if (!dataModel) return blocks
|
|
491
|
+
return blocks.map((block) => {
|
|
492
|
+
const name = block.name?.trim()
|
|
493
|
+
if (!name || !(name in dataModel)) return block
|
|
494
|
+
const raw = dataModel[name]
|
|
495
|
+
if (raw === undefined) return block
|
|
496
|
+
if (block.type === 'toggle') {
|
|
497
|
+
return { ...block, defaultValue: raw === true || raw === 'true' }
|
|
498
|
+
}
|
|
499
|
+
return { ...block, defaultValue: raw as FormBlock['defaultValue'] }
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
464
503
|
export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema {
|
|
465
504
|
if (!doc || !Array.isArray(doc.components)) {
|
|
466
505
|
return syncFormDefinition({ title: '', description: '', blocks: [], jsonSchema: {} })
|
|
@@ -486,7 +525,12 @@ export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema
|
|
|
486
525
|
if (block) blocks.push(block)
|
|
487
526
|
}
|
|
488
527
|
|
|
489
|
-
return syncFormDefinition({
|
|
528
|
+
return syncFormDefinition({
|
|
529
|
+
title,
|
|
530
|
+
description,
|
|
531
|
+
blocks: applyDataModelDefaults(blocks, doc.dataModel),
|
|
532
|
+
jsonSchema: {},
|
|
533
|
+
})
|
|
490
534
|
}
|
|
491
535
|
|
|
492
536
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
buildFileAcceptAttribute,
|
|
7
|
+
filterFileAcceptExtensionsForTypes,
|
|
8
|
+
formatFileAcceptSummary,
|
|
9
|
+
} from './formFileAccept'
|
|
10
|
+
import { normalizeFormDefinition } from './formSchema'
|
|
11
|
+
|
|
12
|
+
describe('filterFileAcceptExtensionsForTypes without types', () => {
|
|
13
|
+
it('keeps whitelisted extensions when fileAcceptTypes is empty', () => {
|
|
14
|
+
expect(filterFileAcceptExtensionsForTypes(undefined, ['.pdf', '.docx'])).toEqual([
|
|
15
|
+
'.pdf',
|
|
16
|
+
'.docx',
|
|
17
|
+
])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('drops unknown extensions when fileAcceptTypes is empty', () => {
|
|
21
|
+
expect(filterFileAcceptExtensionsForTypes([], ['.pdf', '.exe'])).toEqual(['.pdf'])
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('buildFileAcceptAttribute without types', () => {
|
|
26
|
+
it('preserves extension-only accept attribute', () => {
|
|
27
|
+
expect(buildFileAcceptAttribute(undefined, ['.pdf', '.docx'])).toBe('.pdf,.docx')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('formatFileAcceptSummary shows extension labels without types', () => {
|
|
31
|
+
expect(formatFileAcceptSummary(undefined, ['.pdf'])).toBe('.pdf')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('normalizeFormDefinition file extensions without types', () => {
|
|
36
|
+
it('retains whitelisted extensions on file blocks', () => {
|
|
37
|
+
const def = normalizeFormDefinition({
|
|
38
|
+
title: '',
|
|
39
|
+
description: '',
|
|
40
|
+
blocks: [
|
|
41
|
+
{
|
|
42
|
+
id: 'b1',
|
|
43
|
+
type: 'file',
|
|
44
|
+
name: 'attach',
|
|
45
|
+
label: '附件',
|
|
46
|
+
fileAcceptExtensions: ['.pdf', '.docx'],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
jsonSchema: {},
|
|
50
|
+
})
|
|
51
|
+
expect(def.blocks[0]?.fileAcceptExtensions).toEqual(['.pdf', '.docx'])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -103,6 +103,10 @@ const FILE_EXTENSION_LABELS = Object.fromEntries(
|
|
|
103
103
|
|
|
104
104
|
const FILE_TYPE_VALUE_SET = new Set(FORM_FILE_TYPE_OPTIONS.map((item) => item.value))
|
|
105
105
|
|
|
106
|
+
const KNOWN_FILE_EXTENSION_SET = new Set(
|
|
107
|
+
FORM_FILE_EXTENSION_OPTIONS.map((item) => item.value),
|
|
108
|
+
)
|
|
109
|
+
|
|
106
110
|
function resolveFileAcceptTypeValue(value: string): string | null {
|
|
107
111
|
const trimmed = value.trim()
|
|
108
112
|
if (!trimmed) return null
|
|
@@ -164,10 +168,15 @@ export function filterFileAcceptExtensionsForTypes(
|
|
|
164
168
|
types?: string[],
|
|
165
169
|
extensions?: string[],
|
|
166
170
|
): string[] {
|
|
171
|
+
const normalizedExtensions = normalizeFileAcceptExtensions(extensions)
|
|
172
|
+
const normalizedTypes = normalizeFileAcceptTypes(types)
|
|
173
|
+
if (normalizedTypes.length === 0) {
|
|
174
|
+
return normalizedExtensions.filter((item) => KNOWN_FILE_EXTENSION_SET.has(item))
|
|
175
|
+
}
|
|
167
176
|
const allowed = new Set(
|
|
168
|
-
getFileExtensionOptionsForTypes(
|
|
177
|
+
getFileExtensionOptionsForTypes(normalizedTypes).map((item) => item.value),
|
|
169
178
|
)
|
|
170
|
-
return
|
|
179
|
+
return normalizedExtensions.filter((item) => allowed.has(item))
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
export function buildFileAcceptAttribute(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import { coerceToggleValue, normalizeFormDefinition } from './formSchema'
|
|
6
|
+
|
|
7
|
+
describe('coerceToggleValue', () => {
|
|
8
|
+
it('accepts boolean and string true', () => {
|
|
9
|
+
expect(coerceToggleValue(true)).toBe(true)
|
|
10
|
+
expect(coerceToggleValue('true')).toBe(true)
|
|
11
|
+
expect(coerceToggleValue(false)).toBe(false)
|
|
12
|
+
expect(coerceToggleValue('false')).toBe(false)
|
|
13
|
+
expect(coerceToggleValue(undefined)).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('normalizeFormDefinition toggle default', () => {
|
|
18
|
+
it('normalizes string "true" default to boolean true', () => {
|
|
19
|
+
const def = normalizeFormDefinition({
|
|
20
|
+
title: '',
|
|
21
|
+
description: '',
|
|
22
|
+
blocks: [
|
|
23
|
+
{
|
|
24
|
+
id: 'b1',
|
|
25
|
+
type: 'toggle',
|
|
26
|
+
name: 'subscribe',
|
|
27
|
+
label: '订阅',
|
|
28
|
+
defaultValue: 'true',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
jsonSchema: {},
|
|
32
|
+
})
|
|
33
|
+
expect(def.blocks[0]?.defaultValue).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormBlock, FormBlockType, FormSchema } from '../types/index'
|
|
1
|
+
import type { FormBlock, FormBlockType, FormCaseFilter, FormResumeFilter, FormSchema } from '../types/index'
|
|
2
2
|
import { isInputBlockType, isMediaBlockType } from '../types/index'
|
|
3
3
|
import { generateId } from './id'
|
|
4
4
|
import {
|
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const DEFAULT_MEDIA_URL_MAX = 10
|
|
15
15
|
|
|
16
|
+
export function coerceToggleValue(raw: unknown): boolean {
|
|
17
|
+
return raw === true || raw === 'true'
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
function normalizeFileUploadCount(raw: unknown, fallback = 1): number {
|
|
17
21
|
const num = typeof raw === 'number' ? raw : Number(raw)
|
|
18
22
|
if (!Number.isFinite(num)) return fallback
|
|
@@ -24,8 +28,8 @@ function normalizeDefaultValue(
|
|
|
24
28
|
raw: FormBlock['defaultValue'],
|
|
25
29
|
): FormBlock['defaultValue'] {
|
|
26
30
|
if (raw === undefined) return undefined
|
|
27
|
-
if (type === 'toggle') return raw
|
|
28
|
-
if (type === 'multiselect' || type === 'checkbox') {
|
|
31
|
+
if (type === 'toggle') return coerceToggleValue(raw)
|
|
32
|
+
if (type === 'multiselect' || type === 'checkbox' || type === 'resume_multiselect') {
|
|
29
33
|
if (Array.isArray(raw)) return raw.map(String)
|
|
30
34
|
if (typeof raw === 'string' && raw) return [raw]
|
|
31
35
|
return []
|
|
@@ -35,6 +39,44 @@ function normalizeDefaultValue(
|
|
|
35
39
|
return String(raw)
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
function normalizeCaseFilter(raw: FormCaseFilter | undefined): FormCaseFilter {
|
|
43
|
+
const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
|
|
44
|
+
const pageSize =
|
|
45
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
46
|
+
? Math.floor(pageSizeRaw)
|
|
47
|
+
: 20
|
|
48
|
+
const flowIdRaw = typeof raw?.flowId === 'number' ? raw.flowId : Number(raw?.flowId)
|
|
49
|
+
const flowId =
|
|
50
|
+
Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined
|
|
51
|
+
const agentKind =
|
|
52
|
+
typeof raw?.agentKind === 'string' && raw.agentKind.trim()
|
|
53
|
+
? raw.agentKind.trim()
|
|
54
|
+
: undefined
|
|
55
|
+
const agentUniqueId =
|
|
56
|
+
typeof raw?.agentUniqueId === 'string' && raw.agentUniqueId.trim()
|
|
57
|
+
? raw.agentUniqueId.trim()
|
|
58
|
+
: undefined
|
|
59
|
+
return { agentKind, agentUniqueId, flowId, pageSize }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFilter {
|
|
63
|
+
const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
|
|
64
|
+
const pageSize =
|
|
65
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
66
|
+
? Math.floor(pageSizeRaw)
|
|
67
|
+
: 20
|
|
68
|
+
return {
|
|
69
|
+
names: Array.isArray(raw?.names) ? raw!.names.map(String).filter(Boolean) : [],
|
|
70
|
+
agentUniqueIds: Array.isArray(raw?.agentUniqueIds)
|
|
71
|
+
? raw!.agentUniqueIds.map(String).filter(Boolean)
|
|
72
|
+
: [],
|
|
73
|
+
resumeUniqueIds: Array.isArray(raw?.resumeUniqueIds)
|
|
74
|
+
? raw!.resumeUniqueIds.map(String).filter(Boolean)
|
|
75
|
+
: [],
|
|
76
|
+
pageSize,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
38
80
|
function normalizeBlock(raw: FormBlock): FormBlock {
|
|
39
81
|
const fileAcceptTypes = normalizeFileAcceptTypes(raw.fileAcceptTypes)
|
|
40
82
|
const type = raw.type
|
|
@@ -65,6 +107,14 @@ function normalizeBlock(raw: FormBlock): FormBlock {
|
|
|
65
107
|
label: opt.label?.trim() || `选项 ${index + 1}`,
|
|
66
108
|
}))
|
|
67
109
|
: undefined,
|
|
110
|
+
resumeEnableRefresh:
|
|
111
|
+
type === 'resume_multiselect' ? raw.resumeEnableRefresh !== false : undefined,
|
|
112
|
+
resumeFilter:
|
|
113
|
+
type === 'resume_multiselect' ? normalizeResumeFilter(raw.resumeFilter) : undefined,
|
|
114
|
+
caseEnableRefresh:
|
|
115
|
+
type === 'case_singleselect' ? raw.caseEnableRefresh !== false : undefined,
|
|
116
|
+
caseFilter:
|
|
117
|
+
type === 'case_singleselect' ? normalizeCaseFilter(raw.caseFilter) : undefined,
|
|
68
118
|
}
|
|
69
119
|
}
|
|
70
120
|
|
|
@@ -131,6 +181,23 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
|
|
|
131
181
|
block.fileMaxCount = 1
|
|
132
182
|
}
|
|
133
183
|
|
|
184
|
+
if (type === 'resume_multiselect') {
|
|
185
|
+
block.resumeEnableRefresh = true
|
|
186
|
+
block.resumeFilter = {
|
|
187
|
+
names: [],
|
|
188
|
+
agentUniqueIds: [],
|
|
189
|
+
resumeUniqueIds: [],
|
|
190
|
+
pageSize: 20,
|
|
191
|
+
}
|
|
192
|
+
block.defaultValue = []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (type === 'case_singleselect') {
|
|
196
|
+
block.caseEnableRefresh = true
|
|
197
|
+
block.caseFilter = { pageSize: 20 }
|
|
198
|
+
block.defaultValue = ''
|
|
199
|
+
}
|
|
200
|
+
|
|
134
201
|
return block
|
|
135
202
|
}
|
|
136
203
|
|