@skopon-cool/form-sdk 0.1.1 → 0.1.3
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 +47 -11
- 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/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- 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 +3 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -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.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +941 -706
- 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/package.json +11 -6
- package/src/adapter/a2uiAdapter.test.ts +68 -0
- package/src/adapter/a2uiAdapter.ts +7 -3
- 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 +5 -1
- package/src/catalog/a2uiCustomCatalog.tsx +28 -5
- 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 +136 -58
- package/src/components/SkoponA2uiStreamRenderer.test.ts +62 -0
- package/src/components/SkoponA2uiStreamRenderer.test.tsx +79 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +96 -15
- package/src/components/SkoponFormRenderer.tsx +10 -7
- package/src/index.ts +11 -2
- package/src/styles/index.css +5 -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/dist/submit/intersectPayloadWithForm.d.ts +0 -17
- package/dist/submit/intersectPayloadWithForm.d.ts.map +0 -1
- package/src/submit/intersectPayloadWithForm.ts +0 -54
|
@@ -11,3 +11,52 @@ export function buildCurlStatement(payload: unknown, callbackUrl?: string | null
|
|
|
11
11
|
` -d '${escapeSingleQuotes(body)}'`,
|
|
12
12
|
].join('\n')
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
function formatJsonBodyLine(key: string, value: unknown, trailingComma: boolean): string {
|
|
16
|
+
return ` ${JSON.stringify(key)}: ${JSON.stringify(value)}${trailingComma ? ',' : ''}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 构建含行注释的 JSON body:extra 段标注「额外字段(未在卡片展示)」 */
|
|
20
|
+
export function buildAskUserCurlBodyJson(
|
|
21
|
+
cardValues: Record<string, unknown>,
|
|
22
|
+
extraValues: Record<string, unknown>,
|
|
23
|
+
): string {
|
|
24
|
+
const lines: string[] = ['{']
|
|
25
|
+
const cardEntries = Object.entries(cardValues)
|
|
26
|
+
const extraEntries = Object.entries(extraValues)
|
|
27
|
+
|
|
28
|
+
cardEntries.forEach(([key, value], index) => {
|
|
29
|
+
const hasMore = index < cardEntries.length - 1 || extraEntries.length > 0
|
|
30
|
+
lines.push(formatJsonBodyLine(key, value, hasMore))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (extraEntries.length > 0) {
|
|
34
|
+
lines.push(' // 额外字段(未在卡片展示)')
|
|
35
|
+
extraEntries.forEach(([key, value], index) => {
|
|
36
|
+
lines.push(formatJsonBodyLine(key, value, index < extraEntries.length - 1))
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push('}')
|
|
41
|
+
return lines.join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BuildAskUserCurlStatementOptions {
|
|
45
|
+
cardValues: Record<string, unknown>
|
|
46
|
+
extraValues?: Record<string, unknown>
|
|
47
|
+
callbackUrl?: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildAskUserCurlStatement({
|
|
51
|
+
cardValues,
|
|
52
|
+
extraValues = {},
|
|
53
|
+
callbackUrl,
|
|
54
|
+
}: BuildAskUserCurlStatementOptions): string {
|
|
55
|
+
const url = (callbackUrl ?? '').trim() || '<callback_url>'
|
|
56
|
+
const body = buildAskUserCurlBodyJson(cardValues, extraValues)
|
|
57
|
+
return [
|
|
58
|
+
`curl -X POST '${url}' \\`,
|
|
59
|
+
` -H 'Content-Type: application/json' \\`,
|
|
60
|
+
` -d '${escapeSingleQuotes(body)}'`,
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { normalizeFormDefinition } from '../adapter/formSchema'
|
|
2
|
+
import type { FormBlock, FormSchema } from '../types/index'
|
|
3
|
+
import { isInputBlockType, isLayoutBlockType } from '../types/index'
|
|
4
|
+
|
|
5
|
+
export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' | 'blocks'>
|
|
6
|
+
|
|
7
|
+
export interface PayloadBlocksIntersection {
|
|
8
|
+
matchedBlocks: FormBlock[]
|
|
9
|
+
extraBlocks: FormBlock[]
|
|
10
|
+
/** 按 payload 顺序:layout + matched input(不含 extra input) */
|
|
11
|
+
renderBlocks: FormBlock[]
|
|
12
|
+
title: string
|
|
13
|
+
description: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePayloadBlock(block: FormBlock): FormBlock {
|
|
17
|
+
return normalizeFormDefinition({
|
|
18
|
+
title: '',
|
|
19
|
+
description: '',
|
|
20
|
+
blocks: [block],
|
|
21
|
+
jsonSchema: {},
|
|
22
|
+
}).blocks[0]!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildRenderBlocks(
|
|
26
|
+
payloadDef: PayloadBlocksDefinition,
|
|
27
|
+
matchedBlocks: FormBlock[],
|
|
28
|
+
): FormBlock[] {
|
|
29
|
+
const matchedByName = new Map<string, FormBlock>()
|
|
30
|
+
for (const block of matchedBlocks) {
|
|
31
|
+
const name = block.name?.trim()
|
|
32
|
+
if (name) matchedByName.set(name, block)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const renderBlocks: FormBlock[] = []
|
|
36
|
+
for (const payloadBlock of payloadDef.blocks) {
|
|
37
|
+
if (isLayoutBlockType(payloadBlock.type)) {
|
|
38
|
+
renderBlocks.push(normalizePayloadBlock(payloadBlock))
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (!isInputBlockType(payloadBlock.type)) continue
|
|
42
|
+
const name = payloadBlock.name?.trim()
|
|
43
|
+
if (!name) continue
|
|
44
|
+
const matched = matchedByName.get(name)
|
|
45
|
+
if (matched) renderBlocks.push(matched)
|
|
46
|
+
}
|
|
47
|
+
return renderBlocks
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 交集块以 form 定义为准,仅从 payload 合并运行时 defaultValue。 */
|
|
51
|
+
function mergeBlock(formBlock: FormBlock, payloadBlock: FormBlock): FormBlock {
|
|
52
|
+
const merged: FormBlock = {
|
|
53
|
+
...formBlock,
|
|
54
|
+
name: formBlock.name?.trim() || payloadBlock.name,
|
|
55
|
+
}
|
|
56
|
+
if (payloadBlock.defaultValue !== undefined) {
|
|
57
|
+
merged.defaultValue = payloadBlock.defaultValue
|
|
58
|
+
}
|
|
59
|
+
return normalizeFormDefinition({
|
|
60
|
+
title: '',
|
|
61
|
+
description: '',
|
|
62
|
+
blocks: [merged],
|
|
63
|
+
jsonSchema: {},
|
|
64
|
+
}).blocks[0]!
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 解析 ask_user payload 为 blocksJson 形(title / description / blocks) */
|
|
68
|
+
export function parsePayloadBlocksJson(payload: unknown): PayloadBlocksDefinition | null {
|
|
69
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null
|
|
70
|
+
const obj = payload as Record<string, unknown>
|
|
71
|
+
if (!Array.isArray(obj.blocks)) return null
|
|
72
|
+
return normalizeFormDefinition({
|
|
73
|
+
title: typeof obj.title === 'string' ? obj.title : '',
|
|
74
|
+
description: typeof obj.description === 'string' ? obj.description : '',
|
|
75
|
+
blocks: obj.blocks as FormBlock[],
|
|
76
|
+
jsonSchema: {},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* payload.blocks 与 vt_forms 按 input 块 name 取交集用于渲染;
|
|
82
|
+
* payload 中 form 不存在的块归入 extraBlocks(不渲染,提交时写入 curl)。
|
|
83
|
+
*/
|
|
84
|
+
export function intersectPayloadBlocksWithForm(
|
|
85
|
+
payloadDef: PayloadBlocksDefinition,
|
|
86
|
+
formDefinition: FormSchema | undefined,
|
|
87
|
+
): PayloadBlocksIntersection {
|
|
88
|
+
const formByName = new Map<string, FormBlock>()
|
|
89
|
+
for (const block of formDefinition?.blocks ?? []) {
|
|
90
|
+
if (isInputBlockType(block.type) && block.name?.trim()) {
|
|
91
|
+
formByName.set(block.name.trim(), block)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const matchedBlocks: FormBlock[] = []
|
|
96
|
+
const extraBlocks: FormBlock[] = []
|
|
97
|
+
|
|
98
|
+
for (const payloadBlock of payloadDef.blocks) {
|
|
99
|
+
if (!isInputBlockType(payloadBlock.type)) continue
|
|
100
|
+
const name = payloadBlock.name?.trim()
|
|
101
|
+
if (!name) continue
|
|
102
|
+
const formBlock = formByName.get(name)
|
|
103
|
+
if (formBlock) {
|
|
104
|
+
matchedBlocks.push(mergeBlock(formBlock, payloadBlock))
|
|
105
|
+
} else {
|
|
106
|
+
extraBlocks.push(
|
|
107
|
+
normalizeFormDefinition({
|
|
108
|
+
title: '',
|
|
109
|
+
description: '',
|
|
110
|
+
blocks: [payloadBlock],
|
|
111
|
+
jsonSchema: {},
|
|
112
|
+
}).blocks[0]!,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
matchedBlocks,
|
|
119
|
+
extraBlocks,
|
|
120
|
+
renderBlocks: buildRenderBlocks(payloadDef, matchedBlocks),
|
|
121
|
+
title: payloadDef.title?.trim() || formDefinition?.title?.trim() || '',
|
|
122
|
+
description: payloadDef.description?.trim() || formDefinition?.description?.trim() || '',
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function blockValueFromDefault(block: FormBlock): unknown {
|
|
127
|
+
const { type, defaultValue } = block
|
|
128
|
+
if (defaultValue !== undefined) {
|
|
129
|
+
if (type === 'toggle') return defaultValue === true || defaultValue === 'true'
|
|
130
|
+
if (type === 'multiselect' || type === 'checkbox') {
|
|
131
|
+
if (Array.isArray(defaultValue)) return defaultValue.map(String)
|
|
132
|
+
if (typeof defaultValue === 'string' && defaultValue) return [defaultValue]
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
return defaultValue
|
|
136
|
+
}
|
|
137
|
+
if (type === 'toggle') return false
|
|
138
|
+
if (type === 'multiselect' || type === 'checkbox') return []
|
|
139
|
+
if (type === 'number') return null
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 从 extraBlocks 提取提交值(优先 defaultValue) */
|
|
144
|
+
export function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown> {
|
|
145
|
+
const values: Record<string, unknown> = {}
|
|
146
|
+
for (const block of extraBlocks) {
|
|
147
|
+
if (!isInputBlockType(block.type)) continue
|
|
148
|
+
const name = block.name?.trim()
|
|
149
|
+
if (!name) continue
|
|
150
|
+
values[name] = blockValueFromDefault(block)
|
|
151
|
+
}
|
|
152
|
+
return values
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
|
|
156
|
+
export function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean {
|
|
157
|
+
return payloadDef.blocks.some(
|
|
158
|
+
(block) => isInputBlockType(block.type) && Boolean(block.name?.trim()),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 交集为空时 fallback:payload 中可上屏的 blocks(layout + input) */
|
|
163
|
+
export function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[] {
|
|
164
|
+
return payloadDef.blocks.filter((block) => {
|
|
165
|
+
if (isLayoutBlockType(block.type)) return true
|
|
166
|
+
return isInputBlockType(block.type) && Boolean(block.name?.trim())
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** payload 中所有 input 块 field name(fallback 提交用) */
|
|
171
|
+
export function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[] {
|
|
172
|
+
return payloadDef.blocks
|
|
173
|
+
.filter((block) => isInputBlockType(block.type) && block.name?.trim())
|
|
174
|
+
.map((block) => block.name!.trim())
|
|
175
|
+
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
1
4
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
|
|
5
|
+
import {
|
|
6
|
+
buildAskUserCurlBodyJson,
|
|
7
|
+
buildAskUserCurlStatement,
|
|
8
|
+
buildCurlStatement,
|
|
9
|
+
} from '../submit/buildCurlStatement'
|
|
10
|
+
import {
|
|
11
|
+
extractExtraBlockValues,
|
|
12
|
+
getPayloadInputFieldNames,
|
|
13
|
+
getPayloadRenderableBlocks,
|
|
14
|
+
intersectPayloadBlocksWithForm,
|
|
15
|
+
parsePayloadBlocksJson,
|
|
16
|
+
payloadHasInputBlocks,
|
|
17
|
+
} from '../submit/intersectPayloadBlocksWithForm'
|
|
4
18
|
import { submitFormJson } from '../submit/submitFormJson'
|
|
5
19
|
import type { FormSchema } from '../types/index'
|
|
6
20
|
|
|
@@ -16,9 +30,44 @@ describe('buildCurlStatement', () => {
|
|
|
16
30
|
})
|
|
17
31
|
})
|
|
18
32
|
|
|
19
|
-
describe('
|
|
33
|
+
describe('buildAskUserCurlStatement', () => {
|
|
34
|
+
it('includes extra fields with comment after card values', () => {
|
|
35
|
+
const body = buildAskUserCurlBodyJson(
|
|
36
|
+
{ video_title: 'hello' },
|
|
37
|
+
{ agent_hint: '请尽快确认' },
|
|
38
|
+
)
|
|
39
|
+
expect(body).toContain('"video_title": "hello"')
|
|
40
|
+
expect(body).toContain('// 额外字段(未在卡片展示)')
|
|
41
|
+
expect(body).toContain('"agent_hint": "请尽快确认"')
|
|
42
|
+
|
|
43
|
+
const curl = buildAskUserCurlStatement({
|
|
44
|
+
cardValues: { video_title: 'hello' },
|
|
45
|
+
extraValues: { agent_hint: '请尽快确认' },
|
|
46
|
+
callbackUrl: 'https://example.com/hook',
|
|
47
|
+
})
|
|
48
|
+
expect(curl).toContain('// 额外字段(未在卡片展示)')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('parsePayloadBlocksJson', () => {
|
|
53
|
+
it('parses blocksJson payload', () => {
|
|
54
|
+
const parsed = parsePayloadBlocksJson({
|
|
55
|
+
title: 't',
|
|
56
|
+
blocks: [{ id: '1', type: 'text', name: 'video_title', label: 'Title' }],
|
|
57
|
+
})
|
|
58
|
+
expect(parsed?.blocks).toHaveLength(1)
|
|
59
|
+
expect(parsed?.blocks[0]?.name).toBe('video_title')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns null for non-blocksJson', () => {
|
|
63
|
+
expect(parsePayloadBlocksJson({ video_title: { type: 'text' } })).toBeNull()
|
|
64
|
+
expect(parsePayloadBlocksJson(null)).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('intersectPayloadBlocksWithForm', () => {
|
|
20
69
|
const formDef: FormSchema = {
|
|
21
|
-
title: '
|
|
70
|
+
title: 'Form',
|
|
22
71
|
description: '',
|
|
23
72
|
blocks: [
|
|
24
73
|
{ id: '1', type: 'text', name: 'video_title', label: 'Title' },
|
|
@@ -27,13 +76,124 @@ describe('intersectPayloadWithForm', () => {
|
|
|
27
76
|
jsonSchema: {},
|
|
28
77
|
}
|
|
29
78
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
79
|
+
const payloadDef = parsePayloadBlocksJson({
|
|
80
|
+
title: 'Ask',
|
|
81
|
+
blocks: [
|
|
82
|
+
{ id: 'p1', type: 'text', name: 'video_title', label: '视频标题' },
|
|
83
|
+
{ id: 'p2', type: 'text', name: 'agent_hint', label: '提示', defaultValue: 'hint' },
|
|
84
|
+
],
|
|
85
|
+
})!
|
|
86
|
+
|
|
87
|
+
it('matches payload blocks to form by name', () => {
|
|
88
|
+
const result = intersectPayloadBlocksWithForm(payloadDef, formDef)
|
|
89
|
+
expect(result.matchedBlocks.map((b) => b.name)).toEqual(['video_title'])
|
|
90
|
+
expect(result.extraBlocks.map((b) => b.name)).toEqual(['agent_hint'])
|
|
91
|
+
expect(result.title).toBe('Ask')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('extracts extra block default values', () => {
|
|
95
|
+
const result = intersectPayloadBlocksWithForm(payloadDef, formDef)
|
|
96
|
+
expect(extractExtraBlockValues(result.extraBlocks)).toEqual({ agent_hint: 'hint' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('keeps form block type and schema when payload type differs', () => {
|
|
100
|
+
const selectForm: FormSchema = {
|
|
101
|
+
title: 'Form',
|
|
102
|
+
description: '',
|
|
103
|
+
blocks: [
|
|
104
|
+
{
|
|
105
|
+
id: '1',
|
|
106
|
+
type: 'select',
|
|
107
|
+
name: 'choice',
|
|
108
|
+
label: 'Form Label',
|
|
109
|
+
options: [
|
|
110
|
+
{ label: 'A', value: 'a' },
|
|
111
|
+
{ label: 'B', value: 'b' },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
jsonSchema: {},
|
|
116
|
+
}
|
|
117
|
+
const mismatchedPayload = parsePayloadBlocksJson({
|
|
118
|
+
title: 'Ask',
|
|
119
|
+
blocks: [
|
|
120
|
+
{
|
|
121
|
+
id: 'p1',
|
|
122
|
+
type: 'text',
|
|
123
|
+
name: 'choice',
|
|
124
|
+
label: 'Payload Label',
|
|
125
|
+
defaultValue: 'a',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
})!
|
|
129
|
+
const result = intersectPayloadBlocksWithForm(mismatchedPayload, selectForm)
|
|
130
|
+
expect(result.matchedBlocks).toHaveLength(1)
|
|
131
|
+
expect(result.matchedBlocks[0]?.type).toBe('select')
|
|
132
|
+
expect(result.matchedBlocks[0]?.label).toBe('Form Label')
|
|
133
|
+
expect(result.matchedBlocks[0]?.options).toEqual([
|
|
134
|
+
{ label: 'A', value: 'a' },
|
|
135
|
+
{ label: 'B', value: 'b' },
|
|
136
|
+
])
|
|
137
|
+
expect(result.matchedBlocks[0]?.defaultValue).toBe('a')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('preserves payload layout blocks in renderBlocks on partial intersection', () => {
|
|
141
|
+
const partialPayload = parsePayloadBlocksJson({
|
|
142
|
+
title: 'Ask',
|
|
143
|
+
blocks: [
|
|
144
|
+
{ id: 'h1', type: 'heading', label: '请填写以下信息' },
|
|
145
|
+
{ id: 'p1', type: 'text', name: 'video_title', label: '视频标题' },
|
|
146
|
+
{ id: 'p2', type: 'text', name: 'agent_hint', label: '提示', defaultValue: 'hint' },
|
|
147
|
+
{ id: 'para1', type: 'paragraph', label: '提交后不可修改' },
|
|
148
|
+
],
|
|
149
|
+
})!
|
|
150
|
+
const result = intersectPayloadBlocksWithForm(partialPayload, formDef)
|
|
35
151
|
expect(result.matchedBlocks.map((b) => b.name)).toEqual(['video_title'])
|
|
36
|
-
expect(
|
|
152
|
+
expect(result.extraBlocks.map((b) => b.name)).toEqual(['agent_hint'])
|
|
153
|
+
expect(result.renderBlocks.map((b) => b.type)).toEqual([
|
|
154
|
+
'heading',
|
|
155
|
+
'text',
|
|
156
|
+
'paragraph',
|
|
157
|
+
])
|
|
158
|
+
expect(result.renderBlocks.map((b) => b.label)).toEqual([
|
|
159
|
+
'请填写以下信息',
|
|
160
|
+
'Title',
|
|
161
|
+
'提交后不可修改',
|
|
162
|
+
])
|
|
163
|
+
expect(result.renderBlocks.some((b) => b.type === 'heading' && b.label === '请填写以下信息')).toBe(
|
|
164
|
+
true,
|
|
165
|
+
)
|
|
166
|
+
expect(result.extraBlocks.some((b) => b.type === 'heading')).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('payload fallback helpers', () => {
|
|
171
|
+
const surveyPayload = parsePayloadBlocksJson({
|
|
172
|
+
title: '',
|
|
173
|
+
blocks: [
|
|
174
|
+
{ id: 'title', type: 'heading', label: '问卷调查' },
|
|
175
|
+
{ id: 'q1', type: 'radio', name: 'occupation', label: '职业', options: [] },
|
|
176
|
+
],
|
|
177
|
+
})!
|
|
178
|
+
|
|
179
|
+
it('detects input blocks for fallback', () => {
|
|
180
|
+
expect(payloadHasInputBlocks(surveyPayload)).toBe(true)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('returns layout and input blocks for render', () => {
|
|
184
|
+
const blocks = getPayloadRenderableBlocks(surveyPayload)
|
|
185
|
+
expect(blocks.map((b) => b.id)).toEqual(['title', 'q1'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('returns input field names for submit', () => {
|
|
189
|
+
expect(getPayloadInputFieldNames(surveyPayload)).toEqual(['occupation'])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('intersection empty when formDef missing but payload still renderable', () => {
|
|
193
|
+
const result = intersectPayloadBlocksWithForm(surveyPayload, undefined)
|
|
194
|
+
expect(result.matchedBlocks).toHaveLength(0)
|
|
195
|
+
expect(payloadHasInputBlocks(surveyPayload)).toBe(true)
|
|
196
|
+
expect(getPayloadRenderableBlocks(surveyPayload).length).toBeGreaterThan(0)
|
|
37
197
|
})
|
|
38
198
|
})
|
|
39
199
|
|
|
@@ -46,5 +46,24 @@ export async function submitFormJson(
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function copyTextToClipboard(text: string): Promise<void> {
|
|
49
|
-
|
|
49
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
50
|
+
await navigator.clipboard.writeText(text)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (typeof document === 'undefined') {
|
|
54
|
+
throw new Error('clipboard unavailable')
|
|
55
|
+
}
|
|
56
|
+
const textarea = document.createElement('textarea')
|
|
57
|
+
textarea.value = text
|
|
58
|
+
textarea.setAttribute('readonly', '')
|
|
59
|
+
textarea.style.position = 'fixed'
|
|
60
|
+
textarea.style.left = '-9999px'
|
|
61
|
+
document.body.appendChild(textarea)
|
|
62
|
+
textarea.select()
|
|
63
|
+
try {
|
|
64
|
+
const ok = document.execCommand('copy')
|
|
65
|
+
if (!ok) throw new Error('execCommand copy failed')
|
|
66
|
+
} finally {
|
|
67
|
+
document.body.removeChild(textarea)
|
|
68
|
+
}
|
|
50
69
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { FormBlock, FormSchema } from '../types/index';
|
|
2
|
-
/** payload 单个字段的元数据约定(见 docs/ask/ payload.json) */
|
|
3
|
-
export interface AskUserPayloadField {
|
|
4
|
-
用途描述?: string;
|
|
5
|
-
字段类型?: string;
|
|
6
|
-
字段格式?: string;
|
|
7
|
-
字段枚举?: string;
|
|
8
|
-
[key: string]: unknown;
|
|
9
|
-
}
|
|
10
|
-
export type AskUserPayload = Record<string, AskUserPayloadField | unknown>;
|
|
11
|
-
export interface PayloadFormIntersection {
|
|
12
|
-
matchedBlocks: FormBlock[];
|
|
13
|
-
remainderPayload: AskUserPayload;
|
|
14
|
-
}
|
|
15
|
-
export declare function getPayloadKeys(payload: unknown): string[];
|
|
16
|
-
export declare function intersectPayloadWithForm(payload: unknown, formDefinition: FormSchema | undefined): PayloadFormIntersection;
|
|
17
|
-
//# sourceMappingURL=intersectPayloadWithForm.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"intersectPayloadWithForm.d.ts","sourceRoot":"","sources":["../../src/submit/intersectPayloadWithForm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAG3D,mDAAmD;AACnD,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,CAAA;AAE1E,MAAM,WAAW,uBAAuB;IACtC,aAAa,EAAE,SAAS,EAAE,CAAA;IAC1B,gBAAgB,EAAE,cAAc,CAAA;CACjC;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAGzD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,uBAAuB,CA0BzB"}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { FormBlock, FormSchema } from '../types/index'
|
|
2
|
-
import { isInputBlockType } from '../types/index'
|
|
3
|
-
|
|
4
|
-
/** payload 单个字段的元数据约定(见 docs/ask/ payload.json) */
|
|
5
|
-
export interface AskUserPayloadField {
|
|
6
|
-
用途描述?: string
|
|
7
|
-
字段类型?: string
|
|
8
|
-
字段格式?: string
|
|
9
|
-
字段枚举?: string
|
|
10
|
-
[key: string]: unknown
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type AskUserPayload = Record<string, AskUserPayloadField | unknown>
|
|
14
|
-
|
|
15
|
-
export interface PayloadFormIntersection {
|
|
16
|
-
matchedBlocks: FormBlock[]
|
|
17
|
-
remainderPayload: AskUserPayload
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function getPayloadKeys(payload: unknown): string[] {
|
|
21
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return []
|
|
22
|
-
return Object.keys(payload as Record<string, unknown>)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function intersectPayloadWithForm(
|
|
26
|
-
payload: unknown,
|
|
27
|
-
formDefinition: FormSchema | undefined,
|
|
28
|
-
): PayloadFormIntersection {
|
|
29
|
-
const keys = getPayloadKeys(payload)
|
|
30
|
-
const source = (payload ?? {}) as Record<string, unknown>
|
|
31
|
-
|
|
32
|
-
const blocks = formDefinition?.blocks ?? []
|
|
33
|
-
const inputBlockNames = new Set(
|
|
34
|
-
blocks
|
|
35
|
-
.filter((block) => isInputBlockType(block.type) && block.name?.trim())
|
|
36
|
-
.map((block) => block.name!.trim()),
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
const matchedBlocks = blocks.filter(
|
|
40
|
-
(block) =>
|
|
41
|
-
isInputBlockType(block.type) &&
|
|
42
|
-
block.name?.trim() &&
|
|
43
|
-
keys.includes(block.name.trim()),
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const remainderPayload: AskUserPayload = {}
|
|
47
|
-
for (const key of keys) {
|
|
48
|
-
if (!inputBlockNames.has(key)) {
|
|
49
|
-
remainderPayload[key] = source[key]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { matchedBlocks, remainderPayload }
|
|
54
|
-
}
|