@skopon-cool/form-sdk 0.2.4 → 0.3.0
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/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/catalog/useSkoponBoundField.d.ts +16 -1
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/client/authStorage.d.ts +14 -1
- package/dist/client/authStorage.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +8 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/CurlSubmitBlock.d.ts +3 -1
- package/dist/components/CurlSubmitBlock.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +1 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1579 -1107
- package/dist/runtime/formAutoControl.d.ts +15 -0
- package/dist/runtime/formAutoControl.d.ts.map +1 -0
- package/dist/runtime/formRuntimeContext.d.ts +17 -0
- package/dist/runtime/formRuntimeContext.d.ts.map +1 -0
- package/dist/runtime/formRuntimeWs.d.ts +27 -0
- package/dist/runtime/formRuntimeWs.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +20 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/submit/buildCurlStatement.d.ts +4 -3
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/submitAnswerAgent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/catalog/a2uiCustomCatalog.tsx +41 -16
- package/src/catalog/skoponCaseSelect.tsx +10 -4
- package/src/catalog/skoponResumeSelect.tsx +10 -4
- package/src/catalog/useSkoponBoundField.ts +100 -4
- package/src/client/authStorage.test.ts +74 -1
- package/src/client/authStorage.ts +45 -4
- package/src/components/AskUserFormCard.tsx +178 -31
- package/src/components/CurlSubmitBlock.tsx +14 -3
- package/src/components/SkoponFormRenderer.tsx +12 -0
- package/src/index.ts +3 -0
- package/src/runtime/formAutoControl.test.ts +71 -0
- package/src/runtime/formAutoControl.ts +109 -0
- package/src/runtime/formRuntimeContext.tsx +107 -0
- package/src/runtime/formRuntimeWs.ts +158 -0
- package/src/runtime/types.ts +26 -0
- package/src/submit/buildCurlStatement.ts +20 -3
- package/src/submit/submit.test.ts +81 -0
- package/src/submit/submitAnswerAgent.ts +22 -13
|
@@ -10,6 +10,7 @@ export interface SkoponAuthStorageOptions {
|
|
|
10
10
|
export const DEFAULT_AUTH_STORAGE_KEY = 'auth_user'
|
|
11
11
|
export const DEFAULT_AUTH_TOKEN_FIELD = 'token'
|
|
12
12
|
export const DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD = 'answer_agent_callback_url'
|
|
13
|
+
export const DEFAULT_AUTH_REALTIME_WS_URL_FIELD = 'realtime_ws_url'
|
|
13
14
|
/** 与 skopon-admin 登录写入的 mock answer-agent 路径一致 */
|
|
14
15
|
export const DEFAULT_ANSWER_AGENT_CALLBACK_URL = '/api/v1/mock/ask_user_answer_mock'
|
|
15
16
|
|
|
@@ -46,7 +47,10 @@ function readTokenFromSessionJson(
|
|
|
46
47
|
: ''
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* 从宿主登录缓存读取 answer_agent_callback_url。
|
|
52
|
+
* 无 session / 无法读取时返回空串;session 存在但字段缺失或为空时回落到默认 mock 路径(与 skopon-admin 登录迁移一致)。
|
|
53
|
+
*/
|
|
50
54
|
export function resolveAnswerAgentCallbackUrl(
|
|
51
55
|
options: SkoponAuthStorageOptions & {
|
|
52
56
|
answerAgentCallbackUrlField?: string
|
|
@@ -58,19 +62,56 @@ export function resolveAnswerAgentCallbackUrl(
|
|
|
58
62
|
options.answerAgentCallbackUrlField ?? DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD
|
|
59
63
|
|
|
60
64
|
const storage = readStorage(storageKind)
|
|
61
|
-
if (!storage) return
|
|
65
|
+
if (!storage) return ''
|
|
62
66
|
|
|
63
67
|
const raw = storage.getItem(storageKey)
|
|
64
|
-
if (!raw) return
|
|
68
|
+
if (!raw) return ''
|
|
65
69
|
|
|
66
70
|
const parsed = readSessionJson(raw)
|
|
67
|
-
if (!parsed) return
|
|
71
|
+
if (!parsed) return ''
|
|
68
72
|
|
|
69
73
|
const url = parsed[field]
|
|
70
74
|
const trimmed = typeof url === 'string' ? url.trim() : ''
|
|
71
75
|
return trimmed || DEFAULT_ANSWER_AGENT_CALLBACK_URL
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
/**
|
|
79
|
+
* 从宿主登录缓存读取 realtime_ws_url。
|
|
80
|
+
* 无 session / 无字段 / 空串时返回 ''(不建连);SDK 不负责写入该字段。
|
|
81
|
+
*/
|
|
82
|
+
export function resolveRealtimeWsUrl(
|
|
83
|
+
options: SkoponAuthStorageOptions & {
|
|
84
|
+
realtimeWsUrlField?: string
|
|
85
|
+
} = {},
|
|
86
|
+
): string {
|
|
87
|
+
const storageKind = options.storage ?? 'sessionStorage'
|
|
88
|
+
const storageKey = options.storageKey ?? DEFAULT_AUTH_STORAGE_KEY
|
|
89
|
+
const field = options.realtimeWsUrlField ?? DEFAULT_AUTH_REALTIME_WS_URL_FIELD
|
|
90
|
+
|
|
91
|
+
const storage = readStorage(storageKind)
|
|
92
|
+
if (!storage) return ''
|
|
93
|
+
|
|
94
|
+
const raw = storage.getItem(storageKey)
|
|
95
|
+
if (!raw) return ''
|
|
96
|
+
|
|
97
|
+
const parsed = readSessionJson(raw)
|
|
98
|
+
if (!parsed) return ''
|
|
99
|
+
|
|
100
|
+
const url = parsed[field]
|
|
101
|
+
return typeof url === 'string' ? url.trim() : ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 合并 session 鉴权头与宿主显式 headers;显式 headers 优先。 */
|
|
105
|
+
export function resolveSubmitAuthHeaders(
|
|
106
|
+
explicit?: Record<string, string>,
|
|
107
|
+
authStorage?: SkoponAuthStorageOptions,
|
|
108
|
+
): Record<string, string> {
|
|
109
|
+
return {
|
|
110
|
+
...resolveSkoponAuthHeaders(authStorage),
|
|
111
|
+
...(explicit ?? {}),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
74
115
|
/** 从宿主登录缓存读取 Bearer token,返回 Authorization 头(无 token 时返回空对象)。 */
|
|
75
116
|
export function resolveSkoponAuthHeaders(
|
|
76
117
|
options: SkoponAuthStorageOptions = {},
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Button, Spin, Tag, Tooltip } from 'antd'
|
|
3
3
|
import type { FormDetailResult, FormSchema, SubmitMode } from '../types/index'
|
|
4
|
+
import {
|
|
5
|
+
resolveRealtimeWsUrl,
|
|
6
|
+
resolveSubmitAuthHeaders,
|
|
7
|
+
} from '../client/authStorage'
|
|
4
8
|
import { buildAskUserSurface } from '../adapter/resolveSurface'
|
|
5
9
|
import {
|
|
6
10
|
buildAnswerAgentBody,
|
|
@@ -22,6 +26,10 @@ import {
|
|
|
22
26
|
import { validateFormSubmitValues } from '../submit/validateFormSubmit'
|
|
23
27
|
import type { CaseSearchFn } from '../catalog/caseSearchContext'
|
|
24
28
|
import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
29
|
+
import { FormRuntimeProvider, useFormRuntime } from '../runtime/formRuntimeContext'
|
|
30
|
+
import { FormRuntimeWsClient } from '../runtime/formRuntimeWs'
|
|
31
|
+
import { executeFormAutoControl } from '../runtime/formAutoControl'
|
|
32
|
+
import type { FormAutoControlEventData } from '../runtime/types'
|
|
25
33
|
import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
|
|
26
34
|
import CurlSubmitBlock from './CurlSubmitBlock'
|
|
27
35
|
import SkoponFormProvider from './SkoponFormProvider'
|
|
@@ -43,9 +51,16 @@ export interface AskUserFormCardProps {
|
|
|
43
51
|
payload: unknown
|
|
44
52
|
/** 可选;无则跳过拉取 vt_forms,仅用 payload blocks fallback */
|
|
45
53
|
formUniqueId?: string | null
|
|
54
|
+
/**
|
|
55
|
+
* ask_source_unique_id(Flow/QA 业务 id)。
|
|
56
|
+
* 有值且 sessionStorage.auth_user.realtime_ws_url 存在、且渲染出表单控件时按卡建连。
|
|
57
|
+
*/
|
|
58
|
+
askUserMessageId?: string | null
|
|
46
59
|
callbackUrl?: string | null
|
|
47
60
|
submitMode?: SubmitMode
|
|
48
61
|
answerAgentContext?: AskUserAnswerAgentContext
|
|
62
|
+
/** 显式提交 headers,与 session 鉴权合并;显式优先 */
|
|
63
|
+
submitHeaders?: Record<string, string>
|
|
49
64
|
/** 需 useCallback 稳定引用,避免重复拉取 */
|
|
50
65
|
fetchFormDetail: (ref: { formUniqueId: string }) => Promise<FormDetailResult>
|
|
51
66
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
@@ -55,27 +70,44 @@ export interface AskUserFormCardProps {
|
|
|
55
70
|
caseSearch?: CaseSearchFn | null
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
interface AskUserFormCardBodyProps extends AskUserFormCardProps {
|
|
74
|
+
onSurfaceReadyChange: (ready: boolean) => void
|
|
75
|
+
onAutoControlReady: (handler: ((data: FormAutoControlEventData) => void) | null) => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function AskUserFormCardBody({
|
|
59
79
|
payload,
|
|
60
80
|
formUniqueId,
|
|
81
|
+
askUserMessageId,
|
|
61
82
|
callbackUrl,
|
|
62
83
|
submitMode = 'curl',
|
|
63
84
|
answerAgentContext,
|
|
85
|
+
submitHeaders,
|
|
64
86
|
fetchFormDetail,
|
|
65
87
|
onNotify,
|
|
66
88
|
resumeSearch = null,
|
|
67
89
|
caseSearch = null,
|
|
68
|
-
|
|
90
|
+
onSurfaceReadyChange,
|
|
91
|
+
onAutoControlReady,
|
|
92
|
+
}: AskUserFormCardBodyProps) {
|
|
69
93
|
const [formDef, setFormDef] = useState<FormSchema | null>(null)
|
|
70
94
|
const [formDisabled, setFormDisabled] = useState(false)
|
|
71
95
|
const [loading, setLoading] = useState(true)
|
|
72
96
|
const [submitting, setSubmitting] = useState(false)
|
|
73
97
|
const rendererRef = useRef<SkoponFormRendererRef>(null)
|
|
98
|
+
const cardRootRef = useRef<HTMLDivElement>(null)
|
|
74
99
|
const mountedRef = useRef(true)
|
|
75
100
|
const onNotifyRef = useRef(onNotify)
|
|
76
101
|
onNotifyRef.current = onNotify
|
|
102
|
+
const runtime = useFormRuntime()
|
|
77
103
|
|
|
78
104
|
const resolvedFormUniqueId = formUniqueId?.trim() ?? ''
|
|
105
|
+
const resolvedAskUserMessageId = askUserMessageId?.trim() ?? ''
|
|
106
|
+
|
|
107
|
+
const resolvedSubmitHeaders = useMemo(
|
|
108
|
+
() => resolveSubmitAuthHeaders(submitHeaders),
|
|
109
|
+
[submitHeaders],
|
|
110
|
+
)
|
|
79
111
|
|
|
80
112
|
const resolvedAnswerAgentContext = useMemo(
|
|
81
113
|
(): AskUserAnswerAgentContext => ({
|
|
@@ -171,31 +203,14 @@ export default function AskUserFormCard({
|
|
|
171
203
|
? `ask-user-${resolvedFormUniqueId}`
|
|
172
204
|
: 'ask-user-payload'
|
|
173
205
|
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<SkoponFormProvider>
|
|
177
|
-
<div className="ask-user-form-card">
|
|
178
|
-
<Spin size="small" />
|
|
179
|
-
</div>
|
|
180
|
-
</SkoponFormProvider>
|
|
181
|
-
)
|
|
182
|
-
}
|
|
206
|
+
const hasFormSurface = Boolean(!loading && payloadDef && renderBlocks.length > 0 && surfaceDoc)
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
payload={payload}
|
|
189
|
-
callbackUrl={callbackUrl}
|
|
190
|
-
answerAgentContext={resolvedAnswerAgentContext}
|
|
191
|
-
unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
|
|
192
|
-
onNotify={onNotify}
|
|
193
|
-
/>
|
|
194
|
-
</SkoponFormProvider>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
onSurfaceReadyChange(hasFormSurface)
|
|
210
|
+
return () => onSurfaceReadyChange(false)
|
|
211
|
+
}, [hasFormSurface, onSurfaceReadyChange])
|
|
197
212
|
|
|
198
|
-
|
|
213
|
+
const handleSubmit = useCallback(async () => {
|
|
199
214
|
if (submitting) return
|
|
200
215
|
setSubmitting(true)
|
|
201
216
|
try {
|
|
@@ -207,13 +222,21 @@ export default function AskUserFormCard({
|
|
|
207
222
|
}
|
|
208
223
|
const answer = { ...cardValues, ...extraValues }
|
|
209
224
|
|
|
225
|
+
runtime.reportData(answer)
|
|
226
|
+
runtime.reportEvent({
|
|
227
|
+
action: 'submit',
|
|
228
|
+
component: 'SubmitButton',
|
|
229
|
+
})
|
|
230
|
+
|
|
210
231
|
if (submitMode === 'post') {
|
|
211
232
|
const url = (callbackUrl ?? '').trim()
|
|
212
233
|
if (!url) {
|
|
213
234
|
onNotifyRef.current?.('error', 'callback_url 为空,无法提交')
|
|
214
235
|
return
|
|
215
236
|
}
|
|
216
|
-
const result = await submitFormJson(url, answer
|
|
237
|
+
const result = await submitFormJson(url, answer, {
|
|
238
|
+
headers: resolvedSubmitHeaders,
|
|
239
|
+
})
|
|
217
240
|
if (!mountedRef.current) return
|
|
218
241
|
if (result.ok) {
|
|
219
242
|
onNotifyRef.current?.('success', '提交成功')
|
|
@@ -229,7 +252,9 @@ export default function AskUserFormCard({
|
|
|
229
252
|
form: formDef ?? payloadDef,
|
|
230
253
|
answer,
|
|
231
254
|
})
|
|
232
|
-
const result = await submitAnswerAgent(body
|
|
255
|
+
const result = await submitAnswerAgent(body, {
|
|
256
|
+
headers: resolvedSubmitHeaders,
|
|
257
|
+
})
|
|
233
258
|
if (!mountedRef.current) return
|
|
234
259
|
const notify = formatAnswerAgentSubmitMessage(result)
|
|
235
260
|
onNotifyRef.current?.(notify.type, notify.message)
|
|
@@ -240,11 +265,79 @@ export default function AskUserFormCard({
|
|
|
240
265
|
} finally {
|
|
241
266
|
if (mountedRef.current) setSubmitting(false)
|
|
242
267
|
}
|
|
268
|
+
}, [
|
|
269
|
+
submitting,
|
|
270
|
+
fieldNames,
|
|
271
|
+
renderBlocks,
|
|
272
|
+
extraValues,
|
|
273
|
+
runtime,
|
|
274
|
+
submitMode,
|
|
275
|
+
callbackUrl,
|
|
276
|
+
resolvedSubmitHeaders,
|
|
277
|
+
resolvedAnswerAgentContext,
|
|
278
|
+
payload,
|
|
279
|
+
formDef,
|
|
280
|
+
payloadDef,
|
|
281
|
+
])
|
|
282
|
+
|
|
283
|
+
const handleSubmitRef = useRef(handleSubmit)
|
|
284
|
+
handleSubmitRef.current = handleSubmit
|
|
285
|
+
|
|
286
|
+
const applyAutoControl = useCallback(
|
|
287
|
+
(data: FormAutoControlEventData) => {
|
|
288
|
+
executeFormAutoControl(
|
|
289
|
+
data,
|
|
290
|
+
{
|
|
291
|
+
setValues: (values) => rendererRef.current?.setValues(values),
|
|
292
|
+
getValues: (names) => rendererRef.current?.getValues(names) ?? {},
|
|
293
|
+
submit: () => handleSubmitRef.current(),
|
|
294
|
+
rootElement: cardRootRef.current,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
reportEvent: runtime.reportEvent,
|
|
298
|
+
runAutoControl: runtime.runAutoControl,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
},
|
|
302
|
+
[runtime],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!hasFormSurface) {
|
|
307
|
+
onAutoControlReady(null)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
onAutoControlReady(applyAutoControl)
|
|
311
|
+
return () => onAutoControlReady(null)
|
|
312
|
+
}, [hasFormSurface, applyAutoControl, onAutoControlReady])
|
|
313
|
+
|
|
314
|
+
if (loading) {
|
|
315
|
+
return (
|
|
316
|
+
<div className="ask-user-form-card">
|
|
317
|
+
<Spin size="small" />
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!payloadDef || renderBlocks.length === 0) {
|
|
323
|
+
return (
|
|
324
|
+
<CurlSubmitBlock
|
|
325
|
+
payload={payload}
|
|
326
|
+
callbackUrl={callbackUrl}
|
|
327
|
+
answerAgentContext={resolvedAnswerAgentContext}
|
|
328
|
+
submitHeaders={submitHeaders}
|
|
329
|
+
unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
|
|
330
|
+
onNotify={onNotify}
|
|
331
|
+
/>
|
|
332
|
+
)
|
|
243
333
|
}
|
|
244
334
|
|
|
245
335
|
return (
|
|
246
|
-
<
|
|
247
|
-
|
|
336
|
+
<div
|
|
337
|
+
className="ask-user-form-card"
|
|
338
|
+
ref={cardRootRef}
|
|
339
|
+
data-ask-user-message-id={resolvedAskUserMessageId || undefined}
|
|
340
|
+
>
|
|
248
341
|
{formDisabled && resolvedFormUniqueId ? (
|
|
249
342
|
<div className="ask-user-form-card-status">
|
|
250
343
|
<Tooltip title={resolvedFormUniqueId}>
|
|
@@ -282,7 +375,61 @@ export default function AskUserFormCard({
|
|
|
282
375
|
</Button>
|
|
283
376
|
</Tooltip>
|
|
284
377
|
</div>
|
|
285
|
-
|
|
378
|
+
</div>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export default function AskUserFormCard(props: AskUserFormCardProps) {
|
|
383
|
+
const resolvedAskUserMessageId = props.askUserMessageId?.trim() ?? ''
|
|
384
|
+
const realtimeWsUrl = useMemo(() => resolveRealtimeWsUrl(), [])
|
|
385
|
+
const [surfaceReady, setSurfaceReady] = useState(false)
|
|
386
|
+
const [wsClient, setWsClient] = useState<FormRuntimeWsClient | null>(null)
|
|
387
|
+
const autoControlHandlerRef = useRef<((data: FormAutoControlEventData) => void) | null>(null)
|
|
388
|
+
|
|
389
|
+
const onSurfaceReadyChange = useCallback((ready: boolean) => {
|
|
390
|
+
setSurfaceReady(ready)
|
|
391
|
+
}, [])
|
|
392
|
+
|
|
393
|
+
const onAutoControlReady = useCallback(
|
|
394
|
+
(handler: ((data: FormAutoControlEventData) => void) | null) => {
|
|
395
|
+
autoControlHandlerRef.current = handler
|
|
396
|
+
},
|
|
397
|
+
[],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const shouldConnect = Boolean(
|
|
401
|
+
realtimeWsUrl && resolvedAskUserMessageId && surfaceReady,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
if (!shouldConnect || !realtimeWsUrl || !resolvedAskUserMessageId) {
|
|
406
|
+
setWsClient(null)
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
const client = new FormRuntimeWsClient({
|
|
410
|
+
url: realtimeWsUrl,
|
|
411
|
+
askUserMessageId: resolvedAskUserMessageId,
|
|
412
|
+
onAutoControl: (data) => {
|
|
413
|
+
autoControlHandlerRef.current?.(data)
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
client.connect()
|
|
417
|
+
setWsClient(client)
|
|
418
|
+
return () => {
|
|
419
|
+
client.close()
|
|
420
|
+
setWsClient(null)
|
|
421
|
+
}
|
|
422
|
+
}, [shouldConnect, realtimeWsUrl, resolvedAskUserMessageId])
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<SkoponFormProvider>
|
|
426
|
+
<FormRuntimeProvider client={wsClient}>
|
|
427
|
+
<AskUserFormCardBody
|
|
428
|
+
{...props}
|
|
429
|
+
onSurfaceReadyChange={onSurfaceReadyChange}
|
|
430
|
+
onAutoControlReady={onAutoControlReady}
|
|
431
|
+
/>
|
|
432
|
+
</FormRuntimeProvider>
|
|
286
433
|
</SkoponFormProvider>
|
|
287
434
|
)
|
|
288
435
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Button, Tag, Tooltip, Typography } from 'antd'
|
|
3
|
+
import { resolveSubmitAuthHeaders } from '../client/authStorage'
|
|
3
4
|
import { buildCurlStatement } from '../submit/buildCurlStatement'
|
|
4
5
|
import {
|
|
5
6
|
buildAnswerAgentBody,
|
|
@@ -25,6 +26,8 @@ export interface CurlSubmitBlockProps {
|
|
|
25
26
|
/** display:复制与显示一致;submitFields:复制向 callback 提交返回字段的 curl */
|
|
26
27
|
copyMode?: CurlSubmitBlockCopyMode
|
|
27
28
|
answerAgentContext?: AskUserAnswerAgentContext
|
|
29
|
+
/** 显式提交 headers,与 session 鉴权合并;显式优先 */
|
|
30
|
+
submitHeaders?: Record<string, string>
|
|
28
31
|
onNotify?: (type: 'success' | 'error', message: string) => void
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -44,6 +47,7 @@ export default function CurlSubmitBlock({
|
|
|
44
47
|
unpublishedFormId,
|
|
45
48
|
incompleteFormId,
|
|
46
49
|
answerAgentContext,
|
|
50
|
+
submitHeaders,
|
|
47
51
|
onNotify,
|
|
48
52
|
}: CurlSubmitBlockProps) {
|
|
49
53
|
const [submitting, setSubmitting] = useState(false)
|
|
@@ -56,9 +60,14 @@ export default function CurlSubmitBlock({
|
|
|
56
60
|
}
|
|
57
61
|
}, [])
|
|
58
62
|
|
|
63
|
+
const resolvedSubmitHeaders = useMemo(
|
|
64
|
+
() => resolveSubmitAuthHeaders(submitHeaders),
|
|
65
|
+
[submitHeaders],
|
|
66
|
+
)
|
|
67
|
+
|
|
59
68
|
const displayCurl = useMemo(
|
|
60
|
-
() => buildCurlStatement(payload, callbackUrl),
|
|
61
|
-
[payload, callbackUrl],
|
|
69
|
+
() => buildCurlStatement(payload, callbackUrl, resolvedSubmitHeaders),
|
|
70
|
+
[payload, callbackUrl, resolvedSubmitHeaders],
|
|
62
71
|
)
|
|
63
72
|
|
|
64
73
|
const resolvedContext = useMemo(
|
|
@@ -79,7 +88,9 @@ export default function CurlSubmitBlock({
|
|
|
79
88
|
payload,
|
|
80
89
|
answer,
|
|
81
90
|
})
|
|
82
|
-
const result = await submitAnswerAgent(body
|
|
91
|
+
const result = await submitAnswerAgent(body, {
|
|
92
|
+
headers: resolvedSubmitHeaders,
|
|
93
|
+
})
|
|
83
94
|
if (!mountedRef.current) return
|
|
84
95
|
const notify = formatAnswerAgentSubmitMessage(result)
|
|
85
96
|
onNotify?.(notify.type, notify.message)
|
|
@@ -21,6 +21,7 @@ import SkoponFormProvider from './SkoponFormProvider'
|
|
|
21
21
|
|
|
22
22
|
export interface SkoponFormRendererRef {
|
|
23
23
|
getValues: (fieldNames?: string[]) => Record<string, unknown>
|
|
24
|
+
setValues: (values: Record<string, unknown>) => void
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface SkoponFormRendererProps {
|
|
@@ -104,6 +105,17 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
104
105
|
const resolvedNames = (names ?? fieldNamesRef.current).map((n) => n.trim()).filter(Boolean)
|
|
105
106
|
return extractSurfaceValues(processor, surfaceId, resolvedNames)
|
|
106
107
|
},
|
|
108
|
+
setValues(values) {
|
|
109
|
+
const processor = processorRef.current
|
|
110
|
+
if (!processor) return
|
|
111
|
+
const surface = processor.model.surfacesMap.get(surfaceId)
|
|
112
|
+
if (!surface) return
|
|
113
|
+
for (const [name, value] of Object.entries(values)) {
|
|
114
|
+
const trimmed = name.trim()
|
|
115
|
+
if (!trimmed) continue
|
|
116
|
+
surface.dataModel.set(`/${trimmed}`, value)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
107
119
|
}),
|
|
108
120
|
[surfaceId],
|
|
109
121
|
)
|
package/src/index.ts
CHANGED
|
@@ -79,11 +79,14 @@ export {
|
|
|
79
79
|
} from './submit/submitAnswerAgent'
|
|
80
80
|
export {
|
|
81
81
|
resolveAnswerAgentCallbackUrl,
|
|
82
|
+
resolveRealtimeWsUrl,
|
|
82
83
|
resolveSkoponAuthHeaders,
|
|
84
|
+
resolveSubmitAuthHeaders,
|
|
83
85
|
resolveClientHeaders,
|
|
84
86
|
DEFAULT_AUTH_STORAGE_KEY,
|
|
85
87
|
DEFAULT_AUTH_TOKEN_FIELD,
|
|
86
88
|
DEFAULT_AUTH_ANSWER_AGENT_CALLBACK_URL_FIELD,
|
|
89
|
+
DEFAULT_AUTH_REALTIME_WS_URL_FIELD,
|
|
87
90
|
DEFAULT_ANSWER_AGENT_CALLBACK_URL,
|
|
88
91
|
type SkoponAuthStorageOptions,
|
|
89
92
|
type ClientHeadersOptions,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
5
|
+
import { executeFormAutoControl } from './formAutoControl'
|
|
6
|
+
import type { FormAutoControlEventData } from './types'
|
|
7
|
+
|
|
8
|
+
describe('executeFormAutoControl', () => {
|
|
9
|
+
it('writes values and reports bac change event', () => {
|
|
10
|
+
const setValues = vi.fn()
|
|
11
|
+
const reportEvent = vi.fn()
|
|
12
|
+
const runAutoControl = vi.fn((fn: () => unknown) => fn())
|
|
13
|
+
const data: FormAutoControlEventData = {
|
|
14
|
+
action: 'change',
|
|
15
|
+
field: 'name',
|
|
16
|
+
value: 'Alice',
|
|
17
|
+
component: 'TextField',
|
|
18
|
+
}
|
|
19
|
+
executeFormAutoControl(
|
|
20
|
+
data,
|
|
21
|
+
{
|
|
22
|
+
setValues,
|
|
23
|
+
getValues: () => ({}),
|
|
24
|
+
submit: vi.fn(),
|
|
25
|
+
rootElement: null,
|
|
26
|
+
},
|
|
27
|
+
{ reportEvent, runAutoControl },
|
|
28
|
+
)
|
|
29
|
+
expect(runAutoControl).toHaveBeenCalled()
|
|
30
|
+
expect(setValues).toHaveBeenCalledWith({ name: 'Alice' })
|
|
31
|
+
expect(reportEvent).toHaveBeenCalledWith({
|
|
32
|
+
action: 'change',
|
|
33
|
+
component: 'TextField',
|
|
34
|
+
field: 'name',
|
|
35
|
+
value: 'Alice',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('ignores change without field', () => {
|
|
40
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
41
|
+
const setValues = vi.fn()
|
|
42
|
+
executeFormAutoControl(
|
|
43
|
+
{ action: 'change', value: 'x' },
|
|
44
|
+
{
|
|
45
|
+
setValues,
|
|
46
|
+
getValues: () => ({}),
|
|
47
|
+
submit: vi.fn(),
|
|
48
|
+
rootElement: null,
|
|
49
|
+
},
|
|
50
|
+
{ reportEvent: vi.fn(), runAutoControl: (fn) => fn() },
|
|
51
|
+
)
|
|
52
|
+
expect(setValues).not.toHaveBeenCalled()
|
|
53
|
+
expect(warn).toHaveBeenCalled()
|
|
54
|
+
warn.mockRestore()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('invokes submit for submit action', () => {
|
|
58
|
+
const submit = vi.fn()
|
|
59
|
+
executeFormAutoControl(
|
|
60
|
+
{ action: 'submit' },
|
|
61
|
+
{
|
|
62
|
+
setValues: vi.fn(),
|
|
63
|
+
getValues: () => ({}),
|
|
64
|
+
submit,
|
|
65
|
+
rootElement: null,
|
|
66
|
+
},
|
|
67
|
+
{ reportEvent: vi.fn(), runAutoControl: (fn) => fn() },
|
|
68
|
+
)
|
|
69
|
+
expect(submit).toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { FormAutoControlEventData, FormRuntimeEventData } from './types'
|
|
2
|
+
|
|
3
|
+
export interface FormAutoControlTargets {
|
|
4
|
+
setValues: (values: Record<string, unknown>) => void
|
|
5
|
+
getValues: (fieldNames?: string[]) => Record<string, unknown>
|
|
6
|
+
submit: () => void | Promise<void>
|
|
7
|
+
/** 卡片根节点,用于按 data-skopon-field 定位 DOM */
|
|
8
|
+
rootElement: HTMLElement | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FormAutoControlHelpers {
|
|
12
|
+
reportEvent: (event: FormRuntimeEventData) => void
|
|
13
|
+
runAutoControl: <T>(fn: () => T) => T
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeAttrSelector(value: string): string {
|
|
17
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
18
|
+
return CSS.escape(value)
|
|
19
|
+
}
|
|
20
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function focusField(root: HTMLElement | null, field: string): boolean {
|
|
24
|
+
if (!root) return false
|
|
25
|
+
const container = root.querySelector(`[data-skopon-field="${escapeAttrSelector(field)}"]`)
|
|
26
|
+
if (!(container instanceof HTMLElement)) return false
|
|
27
|
+
const focusable =
|
|
28
|
+
container.querySelector<HTMLElement>(
|
|
29
|
+
'input, textarea, select, button, [tabindex]:not([tabindex="-1"])',
|
|
30
|
+
) ?? container
|
|
31
|
+
focusable.focus()
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clickField(root: HTMLElement | null, field: string): boolean {
|
|
36
|
+
if (!root) return false
|
|
37
|
+
const container = root.querySelector(`[data-skopon-field="${escapeAttrSelector(field)}"]`)
|
|
38
|
+
if (!(container instanceof HTMLElement)) return false
|
|
39
|
+
const clickable =
|
|
40
|
+
container.querySelector<HTMLElement>(
|
|
41
|
+
'button, [role="button"], .skopon-resume-select-card, input, label',
|
|
42
|
+
) ?? container
|
|
43
|
+
clickable.click()
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 执行下行 form_auto_control_event;缺参或未知 action 时 warn 并忽略。 */
|
|
48
|
+
export function executeFormAutoControl(
|
|
49
|
+
data: FormAutoControlEventData,
|
|
50
|
+
targets: FormAutoControlTargets,
|
|
51
|
+
helpers: FormAutoControlHelpers,
|
|
52
|
+
): void {
|
|
53
|
+
const action = data.action
|
|
54
|
+
if (
|
|
55
|
+
action !== 'change' &&
|
|
56
|
+
action !== 'select' &&
|
|
57
|
+
action !== 'focus' &&
|
|
58
|
+
action !== 'click' &&
|
|
59
|
+
action !== 'submit'
|
|
60
|
+
) {
|
|
61
|
+
console.warn('[FormAutoControl] unknown action', action)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
helpers.runAutoControl(() => {
|
|
66
|
+
if (action === 'submit') {
|
|
67
|
+
void targets.submit()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const field = typeof data.field === 'string' ? data.field.trim() : ''
|
|
72
|
+
if (!field) {
|
|
73
|
+
console.warn('[FormAutoControl] missing field for action', action)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const component = data.component?.trim() || 'Unknown'
|
|
78
|
+
|
|
79
|
+
if (action === 'change' || action === 'select') {
|
|
80
|
+
if (data.value === undefined) {
|
|
81
|
+
console.warn('[FormAutoControl] missing value for action', action)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
targets.setValues({ [field]: data.value })
|
|
85
|
+
helpers.reportEvent({
|
|
86
|
+
action,
|
|
87
|
+
component,
|
|
88
|
+
field,
|
|
89
|
+
value: data.value,
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (action === 'focus') {
|
|
95
|
+
focusField(targets.rootElement, field)
|
|
96
|
+
helpers.reportEvent({
|
|
97
|
+
action: 'focus',
|
|
98
|
+
component,
|
|
99
|
+
field,
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// click:由组件 onClick 打点回传(autoControlDepth 生效)
|
|
105
|
+
if (!clickField(targets.rootElement, field)) {
|
|
106
|
+
console.warn('[FormAutoControl] click target not found', field)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|