@skopon-cool/form-sdk 0.4.6 → 0.4.8

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.
@@ -25,8 +25,10 @@ export declare function intersectPayloadBlocksWithForm(payloadDef: PayloadBlocks
25
25
  export declare function extractExtraBlockValues(extraBlocks: FormBlock[]): Record<string, unknown>;
26
26
  /** payload 是否含至少一个具 name 的 input 块(可用于卡片 fallback) */
27
27
  export declare function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): boolean;
28
- /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + input) */
28
+ /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + 非 hidden input) */
29
29
  export declare function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[];
30
- /** payload 中所有 input 块 field name(fallback 提交用) */
30
+ /** payload 中所有 input 块 field name(fallback 提交用,含 hidden) */
31
31
  export declare function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[];
32
+ /** payload 中 hidden text 的默认提交值(fallback 不上屏时仍需写入 answer) */
33
+ export declare function getPayloadHiddenBlockValues(payloadDef: PayloadBlocksDefinition): Record<string, unknown>;
32
34
  //# sourceMappingURL=intersectPayloadBlocksWithForm.d.ts.map
@@ -1 +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,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,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;AAuED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAmBvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CA+B3B;AA+BD,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
+ {"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,GAAG;IAC3F;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAA;CACxB,CAAA;AAED,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;AA8ED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAmBvF;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,uBAAuB,EACnC,cAAc,EAAE,UAAU,GAAG,SAAS,GACrC,yBAAyB,CA+B3B;AA+BD,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,mEAAmE;AACnE,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,uBAAuB,GAAG,SAAS,EAAE,CAO3F;AAED,4DAA4D;AAC5D,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,uBAAuB,GAAG,MAAM,EAAE,CAIvF;AAED,6DAA6D;AAC7D,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,uBAAuB,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.6",
6
+ "version": "0.4.8",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -88,6 +88,31 @@ describe('a2uiAdapter blocks <-> surface', () => {
88
88
  expect(imgNode?.mediaSize).toBe('large')
89
89
  })
90
90
 
91
+ it('blocksToA2ui omits hidden text controls but keeps dataModel defaults', () => {
92
+ const doc = blocksToA2ui(
93
+ {
94
+ title: '',
95
+ description: '',
96
+ blocks: [
97
+ { id: 'b-title', type: 'text', name: 'title', label: '标题', defaultValue: 'shown' },
98
+ {
99
+ id: 'b-hint',
100
+ type: 'text',
101
+ name: 'agent_hint',
102
+ label: '提示',
103
+ defaultValue: 'secret',
104
+ hidden: true,
105
+ },
106
+ ],
107
+ },
108
+ { includeHeader: false },
109
+ )
110
+ expect(doc.components.some((c) => c.id === 'b-hint')).toBe(false)
111
+ expect(doc.components.some((c) => c.id === 'b-title')).toBe(true)
112
+ expect(doc.dataModel?.agent_hint).toBe('secret')
113
+ expect(doc.dataModel?.title).toBe('shown')
114
+ })
115
+
91
116
  it('blocksToA2ui preserves select vs multiselect picker types', () => {
92
117
  const doc = blocksToA2ui(
93
118
  {
@@ -147,6 +147,20 @@ function blockToComponent(block: FormBlock): {
147
147
  return { node: { id, component: 'Text', text: label, variant: 'body' } }
148
148
 
149
149
  case 'text':
150
+ // 隐藏 text:不生成可见控件,仍写入 dataModel 默认值供提交
151
+ if (block.hidden) {
152
+ return name
153
+ ? { node: null, dataKey: name, dataValue: defaultValueForBlock(block) }
154
+ : { node: null }
155
+ }
156
+ return withData({
157
+ id,
158
+ component: 'TextField',
159
+ label,
160
+ variant: 'shortText',
161
+ ...(block.placeholder ? { placeholder: block.placeholder } : {}),
162
+ value: { path },
163
+ })
150
164
  case 'tel':
151
165
  case 'url':
152
166
  return withData({
@@ -312,10 +326,11 @@ export function blocksToA2ui(
312
326
 
313
327
  for (const block of definition.blocks ?? []) {
314
328
  const { node, dataKey, dataValue } = blockToComponent(block)
329
+ // hidden 等无节点块仍可写入 dataModel,保证「不展示仍提交」
330
+ if (dataKey) dataModel[dataKey] = dataValue
315
331
  if (!node) continue
316
332
  components.push(node)
317
333
  childIds.push(node.id)
318
- if (dataKey) dataModel[dataKey] = dataValue
319
334
  }
320
335
 
321
336
  components.unshift({ id: ROOT_ID, component: 'Column', children: childIds })
@@ -14,6 +14,20 @@ function readString(value: unknown): string {
14
14
  return typeof value === 'string' ? value : ''
15
15
  }
16
16
 
17
+ /** 与 formSchema.normalizeFlowUniqueIds 对齐:兼容数组 / JSON 数组字符串。 */
18
+ function readFlowUniqueIds(raw: unknown): string[] {
19
+ if (Array.isArray(raw)) return raw.map(String).filter(Boolean)
20
+ if (typeof raw === 'string' && raw.trim()) {
21
+ try {
22
+ const parsed: unknown = JSON.parse(raw)
23
+ if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean)
24
+ } catch {
25
+ /* ignore */
26
+ }
27
+ }
28
+ return []
29
+ }
30
+
17
31
  function readCaseFilter(raw: unknown): FormCaseFilter {
18
32
  if (!raw || typeof raw !== 'object') {
19
33
  return { flowUniqueIds: [], pageSize: 20 }
@@ -40,9 +54,7 @@ function readCaseFilter(raw: unknown): FormCaseFilter {
40
54
  typeof rec.category === 'string' && rec.category.trim()
41
55
  ? rec.category.trim()
42
56
  : undefined
43
- const flowUniqueIds = Array.isArray(rec.flowUniqueIds)
44
- ? rec.flowUniqueIds.map(String).filter(Boolean)
45
- : []
57
+ const flowUniqueIds = readFlowUniqueIds(rec.flowUniqueIds)
46
58
  return { agentKind, agentUniqueId, flowType, category, flowUniqueIds, pageSize }
47
59
  }
48
60
 
@@ -13,7 +13,7 @@ import {
13
13
  } from '../submit/buildAnswerAgentBody'
14
14
  import {
15
15
  extractExtraBlockValues,
16
- getPayloadInputFieldNames,
16
+ getPayloadHiddenBlockValues,
17
17
  getPayloadRenderableBlocks,
18
18
  intersectPayloadBlocksWithForm,
19
19
  parsePayloadBlocksJson,
@@ -195,18 +195,29 @@ function AskUserFormCardBody({
195
195
  return []
196
196
  }, [intersection, usePayloadFallback, payloadDef])
197
197
 
198
- const extraValues = useMemo(
199
- () => (usePayloadFallback ? {} : extractExtraBlockValues(intersection?.extraBlocks ?? [])),
200
- [usePayloadFallback, intersection],
198
+ const extraValues = useMemo(() => {
199
+ if (usePayloadFallback && payloadDef) {
200
+ // fallback:hidden 不上屏,提交仍带默认值
201
+ return getPayloadHiddenBlockValues(payloadDef)
202
+ }
203
+ const fromExtra = extractExtraBlockValues(intersection?.extraBlocks ?? [])
204
+ // 交集命中但 surface 可能为空(仅 hidden)时,兜底写入默认值
205
+ const fromHiddenMatched = extractExtraBlockValues(
206
+ (intersection?.matchedBlocks ?? []).filter(
207
+ (block) => block.type === 'text' && block.hidden === true,
208
+ ),
209
+ )
210
+ return { ...fromHiddenMatched, ...fromExtra }
211
+ }, [usePayloadFallback, payloadDef, intersection])
212
+
213
+ const fieldNames = useMemo(
214
+ () =>
215
+ renderBlocks
216
+ .filter((block) => block.name?.trim())
217
+ .map((block) => block.name!.trim()),
218
+ [renderBlocks],
201
219
  )
202
220
 
203
- const fieldNames = useMemo(() => {
204
- if (usePayloadFallback && payloadDef) return getPayloadInputFieldNames(payloadDef)
205
- return renderBlocks
206
- .filter((block) => block.name?.trim())
207
- .map((block) => block.name!.trim())
208
- }, [usePayloadFallback, payloadDef, renderBlocks])
209
-
210
221
  const surfaceTitle = intersection?.title ?? payloadDef?.title ?? ''
211
222
  const surfaceDescription = intersection?.description ?? payloadDef?.description ?? ''
212
223
 
@@ -313,12 +324,13 @@ function AskUserFormCardBody({
313
324
  setSubmitting(true)
314
325
  try {
315
326
  const cardValues = rendererRef.current?.getValues(fieldNames) ?? {}
316
- const validationError = validateFormSubmitValues(renderBlocks, cardValues)
327
+ // extra/hidden 后合并:getValues 对未上屏字段可能为 undefined,不能覆盖默认值
328
+ const answer = { ...cardValues, ...extraValues }
329
+ const validationError = validateFormSubmitValues(renderBlocks, answer)
317
330
  if (validationError) {
318
331
  onNotifyRef.current?.('error', validationError.message)
319
332
  return
320
333
  }
321
- const answer = { ...cardValues, ...extraValues }
322
334
 
323
335
  if (submitMode === 'post') {
324
336
  const url = (callbackUrl ?? '').trim()
@@ -343,10 +355,20 @@ function AskUserFormCardBody({
343
355
  return
344
356
  }
345
357
 
358
+ // form 必须是交集后的有效 schema(含 ask 动态 caseFilter),
359
+ // 不能传 vt_forms 原始 formDef,否则 flowUniqueIds 等会被编辑器默认冲掉。
360
+ const effectiveForm =
361
+ renderBlocks.length > 0
362
+ ? {
363
+ title: surfaceTitle,
364
+ description: surfaceDescription,
365
+ blocks: renderBlocks,
366
+ }
367
+ : (payloadDef ?? undefined)
346
368
  const body = buildAnswerAgentBody({
347
369
  context: resolvedAnswerAgentContext,
348
370
  payload,
349
- form: formDef ?? payloadDef,
371
+ form: effectiveForm,
350
372
  answer,
351
373
  })
352
374
  const agentError = validateAnswerAgentBody(body)
@@ -376,6 +398,8 @@ function AskUserFormCardBody({
376
398
  submitting,
377
399
  fieldNames,
378
400
  renderBlocks,
401
+ surfaceTitle,
402
+ surfaceDescription,
379
403
  extraValues,
380
404
  reportSnapshot,
381
405
  runtime,
@@ -384,7 +408,6 @@ function AskUserFormCardBody({
384
408
  resolvedSubmitHeaders,
385
409
  resolvedAnswerAgentContext,
386
410
  payload,
387
- formDef,
388
411
  payloadDef,
389
412
  ])
390
413
 
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  } from './submit/buildCurlStatement'
66
66
  export {
67
67
  extractExtraBlockValues,
68
+ getPayloadHiddenBlockValues,
68
69
  getPayloadInputFieldNames,
69
70
  getPayloadRenderableBlocks,
70
71
  intersectPayloadBlocksWithForm,
@@ -21,6 +21,10 @@ export interface PayloadBlocksIntersection {
21
21
 
22
22
  const FORM_ANCHORED_KEYS = new Set(['type', 'name', 'id'])
23
23
 
24
+ function isHiddenTextBlock(block: FormBlock): boolean {
25
+ return block.type === 'text' && block.hidden === true
26
+ }
27
+
24
28
  function normalizePayloadBlock(block: FormBlock): FormBlock {
25
29
  return normalizeFormDefinition({
26
30
  title: '',
@@ -50,7 +54,10 @@ function buildRenderBlocks(
50
54
  const name = payloadBlock.name?.trim()
51
55
  if (!name) continue
52
56
  const matched = matchedByName.get(name)
53
- if (matched) renderBlocks.push(matched)
57
+ if (!matched) continue
58
+ // hidden text 不上屏;提交值由 AskUser 从 matchedBlocks 提取
59
+ if (isHiddenTextBlock(matched)) continue
60
+ renderBlocks.push(matched)
54
61
  }
55
62
  return renderBlocks
56
63
  }
@@ -198,17 +205,33 @@ export function payloadHasInputBlocks(payloadDef: PayloadBlocksDefinition): bool
198
205
  )
199
206
  }
200
207
 
201
- /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + input) */
208
+ /** 交集为空时 fallback:payload 中可上屏的 blocks(layout + 非 hidden input) */
202
209
  export function getPayloadRenderableBlocks(payloadDef: PayloadBlocksDefinition): FormBlock[] {
203
210
  return payloadDef.blocks.filter((block) => {
204
211
  if (isLayoutBlockType(block.type)) return true
205
- return isInputBlockType(block.type) && Boolean(block.name?.trim())
212
+ if (!isInputBlockType(block.type) || !block.name?.trim()) return false
213
+ // hidden text 不上屏;提交值由 getPayloadHiddenBlockValues / dataModel 补齐
214
+ return !isHiddenTextBlock(block)
206
215
  })
207
216
  }
208
217
 
209
- /** payload 中所有 input 块 field name(fallback 提交用) */
218
+ /** payload 中所有 input 块 field name(fallback 提交用,含 hidden) */
210
219
  export function getPayloadInputFieldNames(payloadDef: PayloadBlocksDefinition): string[] {
211
220
  return payloadDef.blocks
212
221
  .filter((block) => isInputBlockType(block.type) && block.name?.trim())
213
222
  .map((block) => block.name!.trim())
214
223
  }
224
+
225
+ /** payload 中 hidden text 的默认提交值(fallback 不上屏时仍需写入 answer) */
226
+ export function getPayloadHiddenBlockValues(
227
+ payloadDef: PayloadBlocksDefinition,
228
+ ): Record<string, unknown> {
229
+ const values: Record<string, unknown> = {}
230
+ for (const block of payloadDef.blocks) {
231
+ if (!isHiddenTextBlock(block)) continue
232
+ const name = block.name?.trim()
233
+ if (!name) continue
234
+ values[name] = blockValueFromDefault(block)
235
+ }
236
+ return values
237
+ }
@@ -11,6 +11,7 @@ import {
11
11
  import { buildAnswerAgentBody, validateAnswerAgentBody } from '../submit/buildAnswerAgentBody'
12
12
  import {
13
13
  extractExtraBlockValues,
14
+ getPayloadHiddenBlockValues,
14
15
  getPayloadInputFieldNames,
15
16
  getPayloadRenderableBlocks,
16
17
  intersectPayloadBlocksWithForm,
@@ -427,6 +428,59 @@ describe('payload fallback helpers', () => {
427
428
  expect(blocks.map((b) => b.id)).toEqual(['title', 'q1'])
428
429
  })
429
430
 
431
+ it('excludes hidden text from renderable blocks but keeps submit defaults', () => {
432
+ const withHidden = parsePayloadBlocksJson({
433
+ title: '',
434
+ blocks: [
435
+ { id: 'title', type: 'heading', label: '问卷调查' },
436
+ { id: 'q1', type: 'radio', name: 'occupation', label: '职业', options: [] },
437
+ {
438
+ id: 'hint',
439
+ type: 'text',
440
+ name: 'agent_hint',
441
+ label: '提示',
442
+ defaultValue: 'keep-me',
443
+ hidden: true,
444
+ },
445
+ ],
446
+ })!
447
+ expect(getPayloadRenderableBlocks(withHidden).map((b) => b.id)).toEqual(['title', 'q1'])
448
+ expect(getPayloadHiddenBlockValues(withHidden)).toEqual({ agent_hint: 'keep-me' })
449
+ expect(getPayloadInputFieldNames(withHidden)).toEqual(['occupation', 'agent_hint'])
450
+ })
451
+
452
+ it('omits matched hidden text from renderBlocks', () => {
453
+ const formWithHint: FormSchema = {
454
+ title: 'Form',
455
+ description: '',
456
+ blocks: [
457
+ { id: '1', type: 'text', name: 'video_title', label: 'Title' },
458
+ { id: '2', type: 'text', name: 'agent_hint', label: 'Hint', hidden: true },
459
+ ],
460
+ jsonSchema: {},
461
+ }
462
+ const payload = parsePayloadBlocksJson({
463
+ title: 'Ask',
464
+ blocks: [
465
+ { id: 'p1', type: 'text', name: 'video_title', label: '视频标题' },
466
+ {
467
+ id: 'p2',
468
+ type: 'text',
469
+ name: 'agent_hint',
470
+ label: '提示',
471
+ defaultValue: 'hint',
472
+ hidden: true,
473
+ },
474
+ ],
475
+ })!
476
+ const result = intersectPayloadBlocksWithForm(payload, formWithHint)
477
+ expect(result.matchedBlocks.map((b) => b.name)).toEqual(['video_title', 'agent_hint'])
478
+ expect(result.renderBlocks.map((b) => b.name)).toEqual(['video_title'])
479
+ expect(extractExtraBlockValues(result.matchedBlocks.filter((b) => b.hidden))).toEqual({
480
+ agent_hint: 'hint',
481
+ })
482
+ })
483
+
430
484
  it('returns input field names for submit', () => {
431
485
  expect(getPayloadInputFieldNames(surveyPayload)).toEqual(['occupation'])
432
486
  })