@skopon-cool/form-sdk 0.6.2 → 0.6.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/dist/adapter/a2uiAdapter.d.ts +1 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formAssetOptions.d.ts +3 -0
- package/dist/adapter/formAssetOptions.d.ts.map +1 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/adapter/resolveSurface.d.ts +1 -1
- package/dist/adapter/resolveSurface.d.ts.map +1 -1
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +2 -0
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2018 -1797
- package/dist/submit/buildAnswerAgentBody.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts +11 -2
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -1
- package/dist/types/index.d.ts +39 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +16 -0
- package/src/adapter/a2uiAdapter.ts +66 -2
- package/src/adapter/formAssetOptions.ts +19 -0
- package/src/adapter/formSchema.ts +60 -0
- package/src/adapter/resolveSurface.ts +4 -1
- package/src/catalog/a2uiCustomCatalog.tsx +216 -13
- package/src/catalog/fileUploadPreview.test.tsx +73 -0
- package/src/catalog/moduleTabs.test.tsx +59 -0
- package/src/components/AskUserFormCard.tsx +28 -9
- package/src/components/AskUserFormCard.upload.test.tsx +66 -0
- package/src/icons/FilePlaceholderIcon.tsx +2 -2
- package/src/index.ts +2 -0
- package/src/styles/index.css +295 -0
- package/src/submit/buildAnswerAgentBody.ts +2 -0
- package/src/submit/intersectPayloadBlocksWithForm.ts +52 -3
- package/src/submit/submit.test.ts +54 -0
- package/src/types/index.ts +41 -0
|
@@ -39,6 +39,29 @@ beforeEach(() => {
|
|
|
39
39
|
afterEach(cleanup)
|
|
40
40
|
|
|
41
41
|
describe('AskUserFormCard file upload guard', () => {
|
|
42
|
+
it('freezes settled cards on the message snapshot without fetching latest definition', async () => {
|
|
43
|
+
const fetchFormDetail = vi.fn()
|
|
44
|
+
const snapshotBlock = {
|
|
45
|
+
id: 'snapshot-title',
|
|
46
|
+
type: 'text' as const,
|
|
47
|
+
name: 'title',
|
|
48
|
+
label: '历史标题',
|
|
49
|
+
defaultValue: '已提交内容',
|
|
50
|
+
}
|
|
51
|
+
const view = render(
|
|
52
|
+
createElement(AskUserFormCard, {
|
|
53
|
+
payload: { title: '历史表单', description: '', blocks: [snapshotBlock] },
|
|
54
|
+
formUniqueId: 'form-versioned',
|
|
55
|
+
formDefinitionPolicy: 'snapshot',
|
|
56
|
+
callbackUrl: null,
|
|
57
|
+
fetchFormDetail,
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await waitFor(() => expect(view.getByText('历史标题')).toBeTruthy())
|
|
62
|
+
expect(fetchFormDetail).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
42
65
|
it('disables submit until every file upload settles', async () => {
|
|
43
66
|
let resolveUpload!: (value: { url: string }) => void
|
|
44
67
|
uploadMock.mockReturnValue(
|
|
@@ -89,3 +112,46 @@ describe('AskUserFormCard file upload guard', () => {
|
|
|
89
112
|
await waitFor(() => expect(submit.disabled).toBe(false))
|
|
90
113
|
})
|
|
91
114
|
})
|
|
115
|
+
|
|
116
|
+
it('generic material schemas render every runtime position even when only some keys match the stored form', async () => {
|
|
117
|
+
const field = { id: 'portrait', type: 'file' as const, name: 'portrait', label: '人物', moduleId: 'people' }
|
|
118
|
+
const view = render(createElement(AskUserFormCard, {
|
|
119
|
+
payload: { schema: 'skopon.material-form/v2', title: '通用素材收集', modules: [{ id: 'people', label: '人物 Tab' }], blocks: [field, { ...field, id: 'outfit', name: 'outfit', label: '服装' }] },
|
|
120
|
+
formUniqueId: 'generic-form', callbackUrl: '/callback',
|
|
121
|
+
fetchFormDetail: async () => ({ formUniqueId: 'generic-form', disabled: false, formDefinition: normalizeFormDefinition({ title: '旧标题', blocks: [field], jsonSchema: {} }) }),
|
|
122
|
+
}))
|
|
123
|
+
await waitFor(() => expect(view.getByText('服装')).toBeTruthy())
|
|
124
|
+
expect(view.getByText('人物')).toBeTruthy()
|
|
125
|
+
expect(view.getByText('人物 Tab')).toBeTruthy()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('keeps runtime material source modes when hibot snapshot omits the schema marker', async () => {
|
|
129
|
+
const module = { id: 'materials', label: '素材' }
|
|
130
|
+
const payloadBlocks = [
|
|
131
|
+
{ id: 'upload', name: 'upload', type: 'file' as const, label: '物料', moduleId: 'materials', fileSourceMode: 'upload_required' as const },
|
|
132
|
+
{ id: 'asset', name: 'asset', type: 'file' as const, label: '素材', moduleId: 'materials', fileSourceMode: 'asset_only' as const },
|
|
133
|
+
{ id: 'mixed', name: 'mixed', type: 'file' as const, label: '物料或素材', moduleId: 'materials', fileSourceMode: 'upload_or_asset' as const },
|
|
134
|
+
]
|
|
135
|
+
const storedBlocks = payloadBlocks.map(({ fileSourceMode: _mode, ...block }) => block)
|
|
136
|
+
const formDefinition = normalizeFormDefinition({
|
|
137
|
+
title: '存量表单', modules: [module], blocks: storedBlocks, jsonSchema: {},
|
|
138
|
+
})
|
|
139
|
+
const view = render(createElement(AskUserFormCard, {
|
|
140
|
+
payload: { title: '本次素材', modules: [module], blocks: payloadBlocks },
|
|
141
|
+
formUniqueId: 'generic-form', callbackUrl: '/callback',
|
|
142
|
+
fetchFormDetail: async () => ({ formUniqueId: 'generic-form', disabled: false, formDefinition }),
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(view.container.querySelectorAll('.skopon-compact-upload')).toHaveLength(3))
|
|
146
|
+
const cards = view.container.querySelectorAll('.skopon-compact-upload')
|
|
147
|
+
const text = (index: number) => (cards[index].textContent || '').replace(/\s/g, '')
|
|
148
|
+
expect(text(0)).toContain('物料·仅上传')
|
|
149
|
+
expect(text(0)).toContain('上传')
|
|
150
|
+
expect(text(0)).not.toContain('选择素材')
|
|
151
|
+
expect(text(1)).toContain('素材·仅选择')
|
|
152
|
+
expect(text(1)).toContain('选择素材')
|
|
153
|
+
expect(text(1)).not.toContain('上传')
|
|
154
|
+
expect(text(2)).toContain('物料或素材')
|
|
155
|
+
expect(text(2)).toContain('上传')
|
|
156
|
+
expect(text(2)).toContain('选择素材')
|
|
157
|
+
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FileExcelOutlined,
|
|
3
|
+
FileImageOutlined,
|
|
3
4
|
FileOutlined,
|
|
4
5
|
FileTextOutlined,
|
|
5
|
-
PictureOutlined,
|
|
6
6
|
SoundOutlined,
|
|
7
7
|
VideoCameraOutlined,
|
|
8
8
|
} from '@ant-design/icons'
|
|
@@ -11,7 +11,7 @@ import type { FormFilePlaceholderIcon } from '../types/index'
|
|
|
11
11
|
const PLACEHOLDER_ICON_COMPONENTS = {
|
|
12
12
|
video: VideoCameraOutlined,
|
|
13
13
|
audio: SoundOutlined,
|
|
14
|
-
image:
|
|
14
|
+
image: FileImageOutlined,
|
|
15
15
|
file: FileOutlined,
|
|
16
16
|
spreadsheet: FileExcelOutlined,
|
|
17
17
|
document: FileTextOutlined,
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
A2uiComponentNode,
|
|
5
5
|
A2uiSurfaceDoc,
|
|
6
6
|
FormBlock,
|
|
7
|
+
FormAssetOption,
|
|
7
8
|
FormBlockCondition,
|
|
8
9
|
FormBlockOption,
|
|
9
10
|
FormBlockType,
|
|
@@ -15,6 +16,7 @@ export type {
|
|
|
15
16
|
FormFilePlaceholderIcon,
|
|
16
17
|
FormJsonSchema,
|
|
17
18
|
FormMediaSize,
|
|
19
|
+
FormModule,
|
|
18
20
|
FormResumeFilter,
|
|
19
21
|
FormSchema,
|
|
20
22
|
SubmitMode,
|
package/src/styles/index.css
CHANGED
|
@@ -230,6 +230,210 @@
|
|
|
230
230
|
justify-content: flex-end;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
.skopon-module-panel {
|
|
234
|
+
min-width: 0;
|
|
235
|
+
padding: var(--space-3) var(--space-2) var(--space-2);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.skopon-module-panel__description {
|
|
239
|
+
margin: 0 0 var(--space-3) !important;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.skopon-module-panel__grid {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
245
|
+
gap: var(--space-3);
|
|
246
|
+
align-items: start;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.skopon-module-panel__field {
|
|
250
|
+
min-width: 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.skopon-module-panel__field .form-block-preview {
|
|
254
|
+
padding: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.skopon-compact-upload {
|
|
258
|
+
display: grid;
|
|
259
|
+
grid-template-columns: 86px minmax(0, 1fr);
|
|
260
|
+
gap: 13px;
|
|
261
|
+
align-items: center;
|
|
262
|
+
min-width: 0;
|
|
263
|
+
padding: 12px;
|
|
264
|
+
border: 1px solid var(--color-border-subtle);
|
|
265
|
+
border-radius: 12px;
|
|
266
|
+
background: var(--color-surface);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.skopon-compact-upload--failed {
|
|
270
|
+
border-color: color-mix(in srgb, var(--color-error, #cf3030) 45%, var(--color-border-subtle));
|
|
271
|
+
background: color-mix(in srgb, var(--color-error, #cf3030) 7%, var(--color-surface));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.skopon-compact-upload__thumb {
|
|
275
|
+
position: relative;
|
|
276
|
+
display: grid;
|
|
277
|
+
place-items: center;
|
|
278
|
+
width: 86px;
|
|
279
|
+
height: 92px;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
border-radius: 9px;
|
|
282
|
+
color: var(--color-text-secondary);
|
|
283
|
+
background: linear-gradient(145deg, var(--color-bg-subtle), var(--color-border-subtle));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.skopon-compact-upload__thumb img {
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
object-fit: cover;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.skopon-compact-upload__state-mark {
|
|
293
|
+
position: absolute;
|
|
294
|
+
right: 6px;
|
|
295
|
+
bottom: 6px;
|
|
296
|
+
display: grid;
|
|
297
|
+
place-items: center;
|
|
298
|
+
width: 22px;
|
|
299
|
+
height: 22px;
|
|
300
|
+
border-radius: 999px;
|
|
301
|
+
color: var(--color-text-secondary);
|
|
302
|
+
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
|
|
303
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.14);
|
|
304
|
+
font-size: 13px;
|
|
305
|
+
font-weight: 700;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.skopon-compact-upload--passed .skopon-compact-upload__state-mark,
|
|
309
|
+
.skopon-compact-upload--passed .skopon-compact-upload__status {
|
|
310
|
+
color: var(--color-success, #087a48);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.skopon-compact-upload--failed .skopon-compact-upload__state-mark,
|
|
314
|
+
.skopon-compact-upload--failed .skopon-compact-upload__status,
|
|
315
|
+
.skopon-compact-upload__error {
|
|
316
|
+
color: var(--color-error, #cf3030);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.skopon-compact-upload__meta {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-direction: column;
|
|
322
|
+
align-items: flex-start;
|
|
323
|
+
gap: 5px;
|
|
324
|
+
min-width: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.skopon-compact-upload__meta > strong {
|
|
328
|
+
font-size: var(--font-size-sm);
|
|
329
|
+
font-weight: var(--font-weight-medium);
|
|
330
|
+
line-height: 1.4;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.skopon-compact-upload__status,
|
|
334
|
+
.skopon-compact-upload__error {
|
|
335
|
+
max-width: 100%;
|
|
336
|
+
overflow: hidden;
|
|
337
|
+
color: var(--color-text-muted);
|
|
338
|
+
font-size: var(--font-size-xs);
|
|
339
|
+
line-height: 1.4;
|
|
340
|
+
text-overflow: ellipsis;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.skopon-compact-upload__source {
|
|
344
|
+
display: inline-flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
min-height: 20px;
|
|
347
|
+
padding: 1px 7px;
|
|
348
|
+
border: 1px solid var(--color-border-subtle);
|
|
349
|
+
border-radius: 999px;
|
|
350
|
+
color: var(--color-text-secondary);
|
|
351
|
+
background: var(--color-bg-subtle);
|
|
352
|
+
font-size: 11px;
|
|
353
|
+
line-height: 1.4;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.skopon-compact-upload__source--asset_only {
|
|
357
|
+
color: var(--color-primary);
|
|
358
|
+
border-color: color-mix(in srgb, var(--color-primary) 24%, var(--color-border-subtle));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.skopon-compact-upload__source--upload_or_asset {
|
|
362
|
+
color: var(--color-text);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.skopon-compact-upload__meta .ant-btn {
|
|
366
|
+
min-height: 30px;
|
|
367
|
+
padding-inline: 11px;
|
|
368
|
+
border-radius: 8px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.skopon-module-panel__guide {
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
justify-content: space-between;
|
|
375
|
+
gap: var(--space-4);
|
|
376
|
+
margin-top: 14px;
|
|
377
|
+
padding: 13px 0 0;
|
|
378
|
+
border-top: 1px solid var(--color-border-subtle);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.skopon-module-panel__requirements .ant-typography {
|
|
382
|
+
margin-bottom: 0;
|
|
383
|
+
white-space: pre-line;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.skopon-module-panel__example-list {
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-wrap: wrap;
|
|
389
|
+
gap: 6px;
|
|
390
|
+
margin-top: 4px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.skopon-module-panel__example-list .ant-image,
|
|
394
|
+
.skopon-module-panel__example-list .ant-image-img {
|
|
395
|
+
width: 42px !important;
|
|
396
|
+
height: 42px !important;
|
|
397
|
+
border-radius: 7px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.a2ui-surface.a2ui-container .a2ui-tabs-header.a2ui-tab-button {
|
|
401
|
+
min-height: 44px;
|
|
402
|
+
padding: 0 14px !important;
|
|
403
|
+
border-radius: 0 !important;
|
|
404
|
+
border-bottom: 2px solid transparent !important;
|
|
405
|
+
background: transparent !important;
|
|
406
|
+
color: var(--color-text-muted) !important;
|
|
407
|
+
font-size: var(--font-size-sm);
|
|
408
|
+
font-weight: 400;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.a2ui-surface.a2ui-container .a2ui-tabs-header.a2ui-tab-button.active {
|
|
412
|
+
border-bottom-color: var(--color-text) !important;
|
|
413
|
+
color: var(--color-text) !important;
|
|
414
|
+
font-weight: var(--font-weight-medium);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@media (max-width: 640px) {
|
|
418
|
+
.skopon-module-panel__grid {
|
|
419
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.skopon-module-panel__guide {
|
|
423
|
+
align-items: flex-start;
|
|
424
|
+
flex-direction: column;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.skopon-compact-upload {
|
|
428
|
+
grid-template-columns: 72px minmax(0, 1fr);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.skopon-compact-upload__thumb {
|
|
432
|
+
width: 72px;
|
|
433
|
+
height: 82px;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
233
437
|
.ask-user-curl-card {
|
|
234
438
|
display: flex;
|
|
235
439
|
flex-direction: column;
|
|
@@ -617,3 +821,94 @@
|
|
|
617
821
|
padding: 12px 0;
|
|
618
822
|
color: var(--color-text-secondary, rgba(0, 0, 0, 0.45));
|
|
619
823
|
}
|
|
824
|
+
|
|
825
|
+
.skopon-file-source-actions {
|
|
826
|
+
display: flex;
|
|
827
|
+
flex-wrap: wrap;
|
|
828
|
+
gap: 8px;
|
|
829
|
+
margin: 8px 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.skopon-asset-picker {
|
|
833
|
+
display: grid;
|
|
834
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
835
|
+
gap: 12px;
|
|
836
|
+
max-height: min(68vh, 620px);
|
|
837
|
+
overflow: auto;
|
|
838
|
+
padding: 2px;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.skopon-asset-picker__card {
|
|
842
|
+
display: flex;
|
|
843
|
+
flex-direction: column;
|
|
844
|
+
gap: 9px;
|
|
845
|
+
min-width: 0;
|
|
846
|
+
padding: 10px;
|
|
847
|
+
border: 1px solid #e5e7eb;
|
|
848
|
+
border-radius: 10px;
|
|
849
|
+
background: #fff;
|
|
850
|
+
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.skopon-asset-picker__card:hover {
|
|
854
|
+
border-color: #91caff;
|
|
855
|
+
box-shadow: 0 2px 10px rgba(22, 119, 255, 0.1);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.skopon-asset-picker__media {
|
|
859
|
+
display: flex;
|
|
860
|
+
align-items: center;
|
|
861
|
+
justify-content: center;
|
|
862
|
+
width: 100%;
|
|
863
|
+
height: 150px;
|
|
864
|
+
overflow: hidden;
|
|
865
|
+
border-radius: 6px;
|
|
866
|
+
background: #f5f7fa;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.skopon-asset-picker__media .ant-image,
|
|
870
|
+
.skopon-asset-picker__media .ant-image-img {
|
|
871
|
+
display: block;
|
|
872
|
+
width: 100%;
|
|
873
|
+
height: 100%;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.skopon-asset-picker__media .ant-image-img {
|
|
877
|
+
object-fit: contain;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.skopon-asset-picker__placeholder {
|
|
881
|
+
display: flex;
|
|
882
|
+
justify-content: center;
|
|
883
|
+
align-items: center;
|
|
884
|
+
width: 100%;
|
|
885
|
+
height: 100%;
|
|
886
|
+
color: #8c8c8c;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.skopon-asset-picker__name {
|
|
890
|
+
overflow-wrap: anywhere;
|
|
891
|
+
min-height: 22px;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
@media (max-width: 600px) {
|
|
895
|
+
.skopon-asset-picker {
|
|
896
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
897
|
+
gap: 8px;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.skopon-asset-picker__card {
|
|
901
|
+
padding: 8px;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.skopon-asset-picker__media {
|
|
905
|
+
height: 120px;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.skopon-selected-assets__row {
|
|
910
|
+
display: flex;
|
|
911
|
+
align-items: center;
|
|
912
|
+
justify-content: space-between;
|
|
913
|
+
gap: 8px;
|
|
914
|
+
}
|
|
@@ -24,8 +24,10 @@ function toBlocksJsonShape(
|
|
|
24
24
|
return source
|
|
25
25
|
}
|
|
26
26
|
return {
|
|
27
|
+
...(obj.schema === 'skopon.material-form/v2' ? { schema: obj.schema } : {}),
|
|
27
28
|
title: typeof obj.title === 'string' ? obj.title : '',
|
|
28
29
|
description: typeof obj.description === 'string' ? obj.description : '',
|
|
30
|
+
modules: Array.isArray(obj.modules) ? obj.modules : undefined,
|
|
29
31
|
blocks: obj.blocks,
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -3,7 +3,8 @@ import { hasRenderableFormOptions } from '../adapter/formOptions'
|
|
|
3
3
|
import type { FormBlock, FormSchema } from '../types/index'
|
|
4
4
|
import { isDisplayBlockType, isInputBlockType } from '../types/index'
|
|
5
5
|
|
|
6
|
-
export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' | 'blocks'> & {
|
|
6
|
+
export type PayloadBlocksDefinition = Pick<FormSchema, 'title' | 'description' | 'modules' | 'blocks'> & {
|
|
7
|
+
schema?: 'skopon.material-form/v2'
|
|
7
8
|
/**
|
|
8
9
|
* ask 原始 blocks(normalize 前)。交集合并按此键集覆盖 form,
|
|
9
10
|
* 避免 normalize 填入的空默认值误盖表单样式。
|
|
@@ -18,9 +19,24 @@ export interface PayloadBlocksIntersection {
|
|
|
18
19
|
renderBlocks: FormBlock[]
|
|
19
20
|
title: string
|
|
20
21
|
description: string
|
|
22
|
+
modules?: FormSchema['modules']
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const FORM_ANCHORED_KEYS = new Set(['type', 'name', 'id'])
|
|
26
|
+
const RUNTIME_SNAPSHOT_KEYS = new Set([
|
|
27
|
+
'defaultValue',
|
|
28
|
+
'fileReviewStatus',
|
|
29
|
+
'fileReviewMessage',
|
|
30
|
+
'fileRejectedUrls',
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
export interface IntersectPayloadBlocksOptions {
|
|
34
|
+
/**
|
|
35
|
+
* snapshot:消息快照中的显式配置优先(兼容旧行为);
|
|
36
|
+
* latest:最新表单负责布局/文案/校验,只保留快照里的填写与审核运行态。
|
|
37
|
+
*/
|
|
38
|
+
definitionPolicy?: 'snapshot' | 'latest'
|
|
39
|
+
}
|
|
24
40
|
|
|
25
41
|
function isHiddenTextBlock(block: FormBlock): boolean {
|
|
26
42
|
return block.type === 'text' && block.hidden === true
|
|
@@ -94,7 +110,27 @@ function mergeBlock(formBlock: FormBlock, rawPayloadBlock: FormBlock): FormBlock
|
|
|
94
110
|
merged.type = formBlock.type
|
|
95
111
|
merged.name = formBlock.name?.trim() || rawPayloadBlock.name
|
|
96
112
|
merged.id = formBlock.id
|
|
97
|
-
|
|
113
|
+
const normalized = normalizePayloadBlock(merged)
|
|
114
|
+
// 单 block normalize 没有 modules 上下文,会把合法 moduleId 当成悬空引用清掉。
|
|
115
|
+
normalized.moduleId = merged.moduleId
|
|
116
|
+
return normalized
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mergeBlockWithLatestDefinition(
|
|
120
|
+
formBlock: FormBlock,
|
|
121
|
+
rawPayloadBlock: FormBlock,
|
|
122
|
+
): FormBlock {
|
|
123
|
+
const merged: FormBlock = { ...formBlock }
|
|
124
|
+
for (const key of Object.keys(rawPayloadBlock) as (keyof FormBlock)[]) {
|
|
125
|
+
if (!RUNTIME_SNAPSHOT_KEYS.has(key)) continue
|
|
126
|
+
if (!Object.prototype.hasOwnProperty.call(rawPayloadBlock, key)) continue
|
|
127
|
+
const value = rawPayloadBlock[key]
|
|
128
|
+
if (value === undefined) continue
|
|
129
|
+
Object.assign(merged, { [key]: value })
|
|
130
|
+
}
|
|
131
|
+
const normalized = normalizePayloadBlock(merged)
|
|
132
|
+
normalized.moduleId = merged.moduleId
|
|
133
|
+
return normalized
|
|
98
134
|
}
|
|
99
135
|
|
|
100
136
|
/** 解析 ask_user payload 为 blocksJson 形(title / description / blocks) */
|
|
@@ -108,12 +144,15 @@ export function parsePayloadBlocksJson(payload: unknown): PayloadBlocksDefinitio
|
|
|
108
144
|
const normalized = normalizeFormDefinition({
|
|
109
145
|
title: typeof obj.title === 'string' ? obj.title : '',
|
|
110
146
|
description: typeof obj.description === 'string' ? obj.description : '',
|
|
147
|
+
modules: Array.isArray(obj.modules) ? obj.modules as FormSchema['modules'] : undefined,
|
|
111
148
|
blocks: rawBlocks,
|
|
112
149
|
jsonSchema: {},
|
|
113
150
|
})
|
|
114
151
|
return {
|
|
152
|
+
schema: obj.schema === 'skopon.material-form/v2' ? obj.schema : undefined,
|
|
115
153
|
title: normalized.title,
|
|
116
154
|
description: normalized.description,
|
|
155
|
+
modules: normalized.modules,
|
|
117
156
|
blocks: normalized.blocks,
|
|
118
157
|
rawBlocks,
|
|
119
158
|
}
|
|
@@ -126,7 +165,9 @@ export function parsePayloadBlocksJson(payload: unknown): PayloadBlocksDefinitio
|
|
|
126
165
|
export function intersectPayloadBlocksWithForm(
|
|
127
166
|
payloadDef: PayloadBlocksDefinition,
|
|
128
167
|
formDefinition: FormSchema | undefined,
|
|
168
|
+
options?: IntersectPayloadBlocksOptions,
|
|
129
169
|
): PayloadBlocksIntersection {
|
|
170
|
+
const definitionPolicy = options?.definitionPolicy ?? 'snapshot'
|
|
130
171
|
const formByName = new Map<string, FormBlock>()
|
|
131
172
|
for (const block of formDefinition?.blocks ?? []) {
|
|
132
173
|
if (isInputBlockType(block.type) && block.name?.trim()) {
|
|
@@ -144,7 +185,11 @@ export function intersectPayloadBlocksWithForm(
|
|
|
144
185
|
const formBlock = formByName.get(name)
|
|
145
186
|
if (formBlock) {
|
|
146
187
|
const rawPayloadBlock = resolveRawInputBlock(payloadDef, name, payloadBlock)
|
|
147
|
-
matchedBlocks.push(
|
|
188
|
+
matchedBlocks.push(
|
|
189
|
+
definitionPolicy === 'latest'
|
|
190
|
+
? mergeBlockWithLatestDefinition(formBlock, rawPayloadBlock)
|
|
191
|
+
: mergeBlock(formBlock, rawPayloadBlock),
|
|
192
|
+
)
|
|
148
193
|
} else {
|
|
149
194
|
extraBlocks.push(normalizePayloadBlock(payloadBlock))
|
|
150
195
|
}
|
|
@@ -156,6 +201,10 @@ export function intersectPayloadBlocksWithForm(
|
|
|
156
201
|
renderBlocks: buildRenderBlocks(payloadDef, matchedBlocks),
|
|
157
202
|
title: payloadDef.title?.trim() || formDefinition?.title?.trim() || '',
|
|
158
203
|
description: payloadDef.description?.trim() || formDefinition?.description?.trim() || '',
|
|
204
|
+
modules:
|
|
205
|
+
definitionPolicy === 'latest'
|
|
206
|
+
? (formDefinition?.modules?.length ? formDefinition.modules : payloadDef.modules)
|
|
207
|
+
: (payloadDef.modules?.length ? payloadDef.modules : formDefinition?.modules),
|
|
159
208
|
}
|
|
160
209
|
}
|
|
161
210
|
|
|
@@ -262,6 +262,60 @@ describe('intersectPayloadBlocksWithForm', () => {
|
|
|
262
262
|
expect(result.matchedBlocks[0]?.defaultValue).toBe('hello')
|
|
263
263
|
})
|
|
264
264
|
|
|
265
|
+
it('uses latest form presentation while preserving snapshot runtime state', () => {
|
|
266
|
+
const form: FormSchema = {
|
|
267
|
+
title: 'Latest Form',
|
|
268
|
+
description: '',
|
|
269
|
+
modules: [{ id: 'latest-person', label: '人物(新版)' }],
|
|
270
|
+
blocks: [{
|
|
271
|
+
id: 'latest-file',
|
|
272
|
+
type: 'file',
|
|
273
|
+
name: 'image01',
|
|
274
|
+
label: '新版人物图',
|
|
275
|
+
moduleId: 'latest-person',
|
|
276
|
+
help: '新版说明',
|
|
277
|
+
fileReviewCriteria: '新版审核标准',
|
|
278
|
+
fileExampleUrls: ['https://example.com/latest.png'],
|
|
279
|
+
}],
|
|
280
|
+
jsonSchema: {},
|
|
281
|
+
}
|
|
282
|
+
const snapshot = parsePayloadBlocksJson({
|
|
283
|
+
title: '当前任务素材',
|
|
284
|
+
modules: [{ id: 'old-person', label: '人物(旧版)' }],
|
|
285
|
+
blocks: [{
|
|
286
|
+
id: 'old-file',
|
|
287
|
+
type: 'file',
|
|
288
|
+
name: 'image01',
|
|
289
|
+
label: '旧版人物图',
|
|
290
|
+
moduleId: 'old-person',
|
|
291
|
+
help: '旧版说明',
|
|
292
|
+
fileReviewCriteria: '旧版审核标准',
|
|
293
|
+
defaultValue: ['https://example.com/uploaded.png'],
|
|
294
|
+
fileReviewStatus: 'failed',
|
|
295
|
+
fileReviewMessage: '人物模糊',
|
|
296
|
+
fileRejectedUrls: ['https://example.com/rejected.png'],
|
|
297
|
+
}],
|
|
298
|
+
})!
|
|
299
|
+
|
|
300
|
+
const result = intersectPayloadBlocksWithForm(snapshot, form, {
|
|
301
|
+
definitionPolicy: 'latest',
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
expect(result.modules).toEqual(form.modules)
|
|
305
|
+
expect(result.matchedBlocks[0]).toMatchObject({
|
|
306
|
+
id: 'latest-file',
|
|
307
|
+
label: '新版人物图',
|
|
308
|
+
moduleId: 'latest-person',
|
|
309
|
+
help: '新版说明',
|
|
310
|
+
fileReviewCriteria: '新版审核标准',
|
|
311
|
+
fileExampleUrls: ['https://example.com/latest.png'],
|
|
312
|
+
defaultValue: ['https://example.com/uploaded.png'],
|
|
313
|
+
fileReviewStatus: 'failed',
|
|
314
|
+
fileReviewMessage: '人物模糊',
|
|
315
|
+
fileRejectedUrls: ['https://example.com/rejected.png'],
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
265
319
|
it('keeps form caseFilter when ask omits caseFilter key', () => {
|
|
266
320
|
const form: FormSchema = {
|
|
267
321
|
title: 'Form',
|
package/src/types/index.ts
CHANGED
|
@@ -20,6 +20,16 @@ export type A2uiComponentName =
|
|
|
20
20
|
| 'SkoponSelect'
|
|
21
21
|
| 'SkoponResumeSelect'
|
|
22
22
|
| 'SkoponCaseSelect'
|
|
23
|
+
| 'SkoponModulePanel'
|
|
24
|
+
|
|
25
|
+
/** 表单分组模块。运行时以 Tab 展示,要求与示例统一放在模块底部。 */
|
|
26
|
+
export interface FormModule {
|
|
27
|
+
id: string
|
|
28
|
+
label: string
|
|
29
|
+
description?: string
|
|
30
|
+
reviewCriteria?: string
|
|
31
|
+
exampleUrls?: string[]
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
export interface A2uiComponentNode {
|
|
25
35
|
id: string
|
|
@@ -88,6 +98,14 @@ export interface FormBlockOption {
|
|
|
88
98
|
label: string
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
export interface FormAssetOption {
|
|
102
|
+
assetId: string
|
|
103
|
+
url: string
|
|
104
|
+
previewUrl?: string
|
|
105
|
+
label: string
|
|
106
|
+
assetType?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
export type FormConditionOperator = 'eq' | 'ne' | 'empty' | 'notEmpty'
|
|
92
110
|
|
|
93
111
|
export interface FormBlockCondition {
|
|
@@ -161,6 +179,25 @@ export interface FormBlock {
|
|
|
161
179
|
filePlaceholderIcon?: FormFilePlaceholderIcon
|
|
162
180
|
fileMinCount?: number
|
|
163
181
|
fileMaxCount?: number
|
|
182
|
+
/** 上传素材的示例图,仅展示,不参与提交。 */
|
|
183
|
+
fileExampleUrls?: string[]
|
|
184
|
+
/** 制作模板的逐字段审核标准;由工作流读取,不能使用提交值覆盖。 */
|
|
185
|
+
fileReviewCriteria?: string
|
|
186
|
+
/** 是否需要由工作流审核上传内容;素材库已审核素材可以关闭。 */
|
|
187
|
+
fileReviewRequired?: boolean
|
|
188
|
+
/** 物料采集来源模式,由 skopon-admin 环节配置。 */
|
|
189
|
+
fileSourceMode?: 'upload_required' | 'upload_or_asset' | 'asset_only'
|
|
190
|
+
/** 通用收集流从 Footage 为当前素材位查询的候选;仅提交选中的文件 URL。 */
|
|
191
|
+
fileAssetOptions?: FormAssetOption[]
|
|
192
|
+
/** 对应 Footage 同步素材位的限定 key。 */
|
|
193
|
+
assetSlotKey?: string
|
|
194
|
+
assetType?: 'image' | 'video' | 'audio' | 'text'
|
|
195
|
+
/** 运行时素材审核状态;用于返工表单回显,不作为用户提交值。 */
|
|
196
|
+
fileReviewStatus?: 'passed' | 'failed'
|
|
197
|
+
/** 运行时素材审核说明;failed 时通常为不合格原因。 */
|
|
198
|
+
fileReviewMessage?: string
|
|
199
|
+
/** 审核未通过的历史文件,仅作错误态回显,不计入有效提交。 */
|
|
200
|
+
fileRejectedUrls?: string[]
|
|
164
201
|
defaultValue?: string | string[] | boolean
|
|
165
202
|
/** 派生 JSON Schema 时写入 property.$comment */
|
|
166
203
|
jsonSchemaDescription?: string
|
|
@@ -191,11 +228,14 @@ export interface FormBlock {
|
|
|
191
228
|
/** 案例单选:列表筛选条件(持久化进 block json) */
|
|
192
229
|
caseFilter?: FormCaseFilter
|
|
193
230
|
condition?: FormBlockCondition
|
|
231
|
+
/** 所属模块;命中 FormSchema.modules.id 时在对应 Tab 中展示。 */
|
|
232
|
+
moduleId?: string
|
|
194
233
|
}
|
|
195
234
|
|
|
196
235
|
export interface FormSchema {
|
|
197
236
|
title?: string
|
|
198
237
|
description?: string
|
|
238
|
+
modules?: FormModule[]
|
|
199
239
|
blocks: FormBlock[]
|
|
200
240
|
jsonSchema: FormJsonSchema
|
|
201
241
|
}
|
|
@@ -236,6 +276,7 @@ export function supportsDefaultValue(type: FormBlockType): boolean {
|
|
|
236
276
|
export interface FormDefinitionPayload {
|
|
237
277
|
title?: string
|
|
238
278
|
description?: string
|
|
279
|
+
modules?: FormModule[]
|
|
239
280
|
blocks?: FormBlock[]
|
|
240
281
|
a2ui?: A2uiSurfaceDoc
|
|
241
282
|
json_schema?: FormJsonSchema
|