@skopon-cool/form-sdk 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/adapter/a2uiAdapter.d.ts +0 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/case_singleselect/adapter.d.ts +10 -0
- package/dist/blocks/case_singleselect/adapter.d.ts.map +1 -0
- package/dist/blocks/case_singleselect/catalog.d.ts +2 -0
- package/dist/blocks/case_singleselect/catalog.d.ts.map +1 -0
- package/dist/blocks/case_singleselect/index.d.ts +3 -0
- package/dist/blocks/case_singleselect/index.d.ts.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts +10 -0
- package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts +2 -0
- package/dist/blocks/resume_multiselect/catalog.d.ts.map +1 -0
- package/dist/blocks/resume_multiselect/index.d.ts +3 -0
- package/dist/blocks/resume_multiselect/index.d.ts.map +1 -0
- package/dist/blocks/types.d.ts +27 -0
- package/dist/blocks/types.d.ts.map +1 -0
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/caseSearchContext.d.ts +28 -0
- package/dist/catalog/caseSearchContext.d.ts.map +1 -0
- package/dist/catalog/resumeSearchContext.d.ts +30 -0
- package/dist/catalog/resumeSearchContext.d.ts.map +1 -0
- package/dist/catalog/skoponCaseSelect.d.ts +2 -0
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -0
- package/dist/catalog/skoponResumeSelect.d.ts +2 -0
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -0
- package/dist/components/AskUserFormCard.d.ts +7 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +7 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +6 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1290 -845
- package/dist/types/index.d.ts +26 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +48 -0
- package/src/adapter/a2uiAdapter.ts +41 -1
- package/src/adapter/formSchema.ts +65 -2
- package/src/blocks/case_singleselect/adapter.ts +74 -0
- package/src/blocks/case_singleselect/catalog.ts +1 -0
- package/src/blocks/case_singleselect/index.ts +14 -0
- package/src/blocks/registry.ts +34 -0
- package/src/blocks/resume_multiselect/adapter.ts +57 -0
- package/src/blocks/resume_multiselect/catalog.ts +1 -0
- package/src/blocks/resume_multiselect/index.ts +14 -0
- package/src/blocks/types.ts +34 -0
- package/src/catalog/a2uiCustomCatalog.tsx +6 -0
- package/src/catalog/caseSearchContext.tsx +46 -0
- package/src/catalog/resumeSearchContext.tsx +48 -0
- package/src/catalog/skoponCaseSelect.tsx +215 -0
- package/src/catalog/skoponResumeSelect.tsx +227 -0
- package/src/components/AskUserFormCard.tsx +10 -0
- package/src/components/SkoponA2uiStreamRenderer.test.ts +18 -2
- package/src/components/SkoponA2uiStreamRenderer.test.tsx +26 -2
- package/src/components/SkoponA2uiStreamRenderer.tsx +60 -23
- package/src/components/SkoponFormRenderer.tsx +32 -10
- package/src/index.ts +23 -0
- package/src/styles/index.css +60 -0
- package/src/types/index.ts +30 -0
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export type A2uiBinding<T> = T | {
|
|
5
5
|
path: string;
|
|
6
6
|
};
|
|
7
|
-
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect';
|
|
7
|
+
export type A2uiComponentName = 'Column' | 'Row' | 'Text' | 'Image' | 'Video' | 'AudioPlayer' | 'TextField' | 'CheckBox' | 'ChoicePicker' | 'DateTimeInput' | 'FileUpload' | 'SkoponMedia' | 'SkoponSelect' | 'SkoponResumeSelect' | 'SkoponCaseSelect';
|
|
8
8
|
export interface A2uiComponentNode {
|
|
9
9
|
id: string;
|
|
10
10
|
component: A2uiComponentName | string;
|
|
@@ -21,7 +21,7 @@ export interface A2uiSurfaceDoc {
|
|
|
21
21
|
}
|
|
22
22
|
export declare const A2UI_PROTOCOL_VERSION: "v0.9";
|
|
23
23
|
export type FormJsonSchema = Record<string, unknown>;
|
|
24
|
-
export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'image' | 'video' | 'audio';
|
|
24
|
+
export type FormBlockType = 'heading' | 'paragraph' | 'text' | 'textarea' | 'email' | 'number' | 'select' | 'multiselect' | 'radio' | 'checkbox' | 'toggle' | 'tel' | 'url' | 'datetime' | 'time' | 'file' | 'resume_multiselect' | 'case_singleselect' | 'image' | 'video' | 'audio';
|
|
25
25
|
export type FormMediaSize = 'huge' | 'large' | 'medium' | 'small' | 'icon';
|
|
26
26
|
export type FormFilePlaceholderIcon = 'video' | 'audio' | 'image' | 'file' | 'spreadsheet' | 'document';
|
|
27
27
|
export declare const FORM_MEDIA_SIZES: FormMediaSize[];
|
|
@@ -29,6 +29,22 @@ export interface FormBlockOption {
|
|
|
29
29
|
value: string;
|
|
30
30
|
label: string;
|
|
31
31
|
}
|
|
32
|
+
/** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
|
|
33
|
+
export interface FormResumeFilter {
|
|
34
|
+
names?: string[];
|
|
35
|
+
agentUniqueIds?: string[];
|
|
36
|
+
resumeUniqueIds?: string[];
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
}
|
|
39
|
+
/** 案例单选组件:渲染时用于 /univ/case/list 的筛选条件 */
|
|
40
|
+
export interface FormCaseFilter {
|
|
41
|
+
/** Agent 类型(编辑器辅助,运行时不用) */
|
|
42
|
+
agentKind?: string;
|
|
43
|
+
/** Agent unique_id(编辑器辅助,运行时不用) */
|
|
44
|
+
agentUniqueId?: string;
|
|
45
|
+
flowId?: number;
|
|
46
|
+
pageSize?: number;
|
|
47
|
+
}
|
|
32
48
|
export interface FormBlock {
|
|
33
49
|
id: string;
|
|
34
50
|
type: FormBlockType;
|
|
@@ -47,6 +63,14 @@ export interface FormBlock {
|
|
|
47
63
|
fileMinCount?: number;
|
|
48
64
|
fileMaxCount?: number;
|
|
49
65
|
defaultValue?: string | string[] | boolean;
|
|
66
|
+
/** 简历多选:是否显示「换一批」 */
|
|
67
|
+
resumeEnableRefresh?: boolean;
|
|
68
|
+
/** 简历多选:列表筛选条件(持久化进 block json) */
|
|
69
|
+
resumeFilter?: FormResumeFilter;
|
|
70
|
+
/** 案例单选:是否显示「换一批」 */
|
|
71
|
+
caseEnableRefresh?: boolean;
|
|
72
|
+
/** 案例单选:列表筛选条件(持久化进 block json) */
|
|
73
|
+
caseFilter?: FormCaseFilter;
|
|
50
74
|
}
|
|
51
75
|
export interface FormSchema {
|
|
52
76
|
title?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,MAAM,MAAM,iBAAiB,GACzB,QAAQ,GACR,KAAK,GACL,MAAM,GACN,OAAO,GACP,OAAO,GACP,aAAa,GACb,WAAW,GACX,UAAU,GACV,cAAc,GACd,eAAe,GACf,YAAY,GACZ,aAAa,GACb,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -238,4 +238,52 @@ describe('a2uiAdapter blocks <-> surface', () => {
|
|
|
238
238
|
const withHeader = blocksToA2ui(makeDefinition())
|
|
239
239
|
expect((withHeader.components.find((c) => c.id === 'root')?.children as string[]).length).toBe(12)
|
|
240
240
|
})
|
|
241
|
+
|
|
242
|
+
it('round-trips case_singleselect through blocksToA2ui and a2uiToBlocks', () => {
|
|
243
|
+
const doc = blocksToA2ui(
|
|
244
|
+
{
|
|
245
|
+
title: '',
|
|
246
|
+
description: '',
|
|
247
|
+
blocks: [
|
|
248
|
+
{
|
|
249
|
+
id: 'b-case',
|
|
250
|
+
type: 'case_singleselect',
|
|
251
|
+
name: 'selected_case',
|
|
252
|
+
label: '选择案例',
|
|
253
|
+
placeholder: '暂无案例',
|
|
254
|
+
help: '请选择一个案例',
|
|
255
|
+
caseEnableRefresh: true,
|
|
256
|
+
caseFilter: {
|
|
257
|
+
agentKind: 'public-user',
|
|
258
|
+
agentUniqueId: 'agent-1',
|
|
259
|
+
flowId: 42,
|
|
260
|
+
pageSize: 10,
|
|
261
|
+
},
|
|
262
|
+
defaultValue: 'case-abc',
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{ includeHeader: false },
|
|
267
|
+
)
|
|
268
|
+
const node = doc.components.find((c) => c.id === 'b-case')
|
|
269
|
+
expect(node?.component).toBe('SkoponCaseSelect')
|
|
270
|
+
expect(node?.enableRefresh).toBe(true)
|
|
271
|
+
expect(node?.caseFilter).toEqual({
|
|
272
|
+
agentKind: 'public-user',
|
|
273
|
+
agentUniqueId: 'agent-1',
|
|
274
|
+
flowId: 42,
|
|
275
|
+
pageSize: 10,
|
|
276
|
+
})
|
|
277
|
+
expect(doc.dataModel?.selected_case).toBe('case-abc')
|
|
278
|
+
|
|
279
|
+
const back = a2uiToBlocks(doc)
|
|
280
|
+
const block = back.blocks.find((b) => b.name === 'selected_case')
|
|
281
|
+
expect(block?.type).toBe('case_singleselect')
|
|
282
|
+
expect(block?.caseEnableRefresh).toBe(true)
|
|
283
|
+
expect(block?.caseFilter?.flowId).toBe(42)
|
|
284
|
+
expect(block?.caseFilter?.pageSize).toBe(10)
|
|
285
|
+
expect(block?.caseFilter?.agentKind).toBe('public-user')
|
|
286
|
+
expect(block?.caseFilter?.agentUniqueId).toBe('agent-1')
|
|
287
|
+
expect(block?.defaultValue).toBe('case-abc')
|
|
288
|
+
})
|
|
241
289
|
})
|
|
@@ -11,6 +11,7 @@ import { generateId } from './id'
|
|
|
11
11
|
import { coerceToggleValue } from './formSchema'
|
|
12
12
|
import { getMediaUrls, normalizeMediaSize } from './formMedia'
|
|
13
13
|
import { syncFormDefinition } from './formSchema'
|
|
14
|
+
import { getBlockTypeAdapter, getComponentAdapter } from '../blocks/registry'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A2UI surface <-> FormBlock 双向适配器。
|
|
@@ -99,14 +100,20 @@ function defaultValueForBlock(block: FormBlock): unknown {
|
|
|
99
100
|
return coerceToggleValue(block.defaultValue)
|
|
100
101
|
case 'multiselect':
|
|
101
102
|
case 'checkbox':
|
|
103
|
+
case 'resume_multiselect':
|
|
102
104
|
if (Array.isArray(block.defaultValue)) return block.defaultValue
|
|
103
105
|
if (typeof block.defaultValue === 'string' && block.defaultValue) return [block.defaultValue]
|
|
104
106
|
return []
|
|
105
107
|
case 'select':
|
|
106
108
|
case 'radio':
|
|
109
|
+
case 'case_singleselect':
|
|
107
110
|
if (Array.isArray(block.defaultValue)) return block.defaultValue[0] ?? ''
|
|
108
111
|
if (typeof block.defaultValue === 'string') return block.defaultValue
|
|
109
112
|
return ''
|
|
113
|
+
case 'number':
|
|
114
|
+
if (typeof block.defaultValue === 'number') return block.defaultValue
|
|
115
|
+
if (typeof block.defaultValue === 'string' && block.defaultValue) return block.defaultValue
|
|
116
|
+
return null
|
|
110
117
|
default:
|
|
111
118
|
if (typeof block.defaultValue === 'string') return block.defaultValue
|
|
112
119
|
return ''
|
|
@@ -131,6 +138,12 @@ function blockToComponent(block: FormBlock): {
|
|
|
131
138
|
dataValue?: unknown
|
|
132
139
|
} => (name ? { node, dataKey: name, dataValue: defaultValueForBlock(block) } : { node })
|
|
133
140
|
|
|
141
|
+
const adapterCtx = { id, label, name, path, withData }
|
|
142
|
+
const typeAdapter = getBlockTypeAdapter(block.type)
|
|
143
|
+
if (typeAdapter) {
|
|
144
|
+
return typeAdapter.toComponent(block, adapterCtx)
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
switch (block.type) {
|
|
135
148
|
case 'heading':
|
|
136
149
|
return { node: { id, component: 'Text', text: label, variant: 'h3' } }
|
|
@@ -351,6 +364,11 @@ function componentToBlock(node: A2uiComponentNode): FormBlock | null {
|
|
|
351
364
|
|
|
352
365
|
const base = (type: FormBlockType): FormBlock => ({ id, type, label })
|
|
353
366
|
|
|
367
|
+
const componentAdapter = getComponentAdapter(component)
|
|
368
|
+
if (componentAdapter) {
|
|
369
|
+
return componentAdapter.fromComponent(node, base, { asLiteral, nameFromValue })
|
|
370
|
+
}
|
|
371
|
+
|
|
354
372
|
switch (component) {
|
|
355
373
|
case 'Text': {
|
|
356
374
|
const variant = String(node.variant ?? 'body')
|
|
@@ -465,6 +483,23 @@ function collectOrder(
|
|
|
465
483
|
}
|
|
466
484
|
|
|
467
485
|
/** A2UI surface 文档 -> FormSchema(已 sync 出 jsonSchema) */
|
|
486
|
+
function applyDataModelDefaults(
|
|
487
|
+
blocks: FormBlock[],
|
|
488
|
+
dataModel: Record<string, unknown> | undefined,
|
|
489
|
+
): FormBlock[] {
|
|
490
|
+
if (!dataModel) return blocks
|
|
491
|
+
return blocks.map((block) => {
|
|
492
|
+
const name = block.name?.trim()
|
|
493
|
+
if (!name || !(name in dataModel)) return block
|
|
494
|
+
const raw = dataModel[name]
|
|
495
|
+
if (raw === undefined) return block
|
|
496
|
+
if (block.type === 'toggle') {
|
|
497
|
+
return { ...block, defaultValue: raw === true || raw === 'true' }
|
|
498
|
+
}
|
|
499
|
+
return { ...block, defaultValue: raw as FormBlock['defaultValue'] }
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
468
503
|
export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema {
|
|
469
504
|
if (!doc || !Array.isArray(doc.components)) {
|
|
470
505
|
return syncFormDefinition({ title: '', description: '', blocks: [], jsonSchema: {} })
|
|
@@ -490,7 +525,12 @@ export function a2uiToBlocks(doc: A2uiSurfaceDoc | null | undefined): FormSchema
|
|
|
490
525
|
if (block) blocks.push(block)
|
|
491
526
|
}
|
|
492
527
|
|
|
493
|
-
return syncFormDefinition({
|
|
528
|
+
return syncFormDefinition({
|
|
529
|
+
title,
|
|
530
|
+
description,
|
|
531
|
+
blocks: applyDataModelDefaults(blocks, doc.dataModel),
|
|
532
|
+
jsonSchema: {},
|
|
533
|
+
})
|
|
494
534
|
}
|
|
495
535
|
|
|
496
536
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormBlock, FormBlockType, FormSchema } from '../types/index'
|
|
1
|
+
import type { FormBlock, FormBlockType, FormCaseFilter, FormResumeFilter, FormSchema } from '../types/index'
|
|
2
2
|
import { isInputBlockType, isMediaBlockType } from '../types/index'
|
|
3
3
|
import { generateId } from './id'
|
|
4
4
|
import {
|
|
@@ -29,7 +29,7 @@ function normalizeDefaultValue(
|
|
|
29
29
|
): FormBlock['defaultValue'] {
|
|
30
30
|
if (raw === undefined) return undefined
|
|
31
31
|
if (type === 'toggle') return coerceToggleValue(raw)
|
|
32
|
-
if (type === 'multiselect' || type === 'checkbox') {
|
|
32
|
+
if (type === 'multiselect' || type === 'checkbox' || type === 'resume_multiselect') {
|
|
33
33
|
if (Array.isArray(raw)) return raw.map(String)
|
|
34
34
|
if (typeof raw === 'string' && raw) return [raw]
|
|
35
35
|
return []
|
|
@@ -39,6 +39,44 @@ function normalizeDefaultValue(
|
|
|
39
39
|
return String(raw)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function normalizeCaseFilter(raw: FormCaseFilter | undefined): FormCaseFilter {
|
|
43
|
+
const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
|
|
44
|
+
const pageSize =
|
|
45
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
46
|
+
? Math.floor(pageSizeRaw)
|
|
47
|
+
: 20
|
|
48
|
+
const flowIdRaw = typeof raw?.flowId === 'number' ? raw.flowId : Number(raw?.flowId)
|
|
49
|
+
const flowId =
|
|
50
|
+
Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined
|
|
51
|
+
const agentKind =
|
|
52
|
+
typeof raw?.agentKind === 'string' && raw.agentKind.trim()
|
|
53
|
+
? raw.agentKind.trim()
|
|
54
|
+
: undefined
|
|
55
|
+
const agentUniqueId =
|
|
56
|
+
typeof raw?.agentUniqueId === 'string' && raw.agentUniqueId.trim()
|
|
57
|
+
? raw.agentUniqueId.trim()
|
|
58
|
+
: undefined
|
|
59
|
+
return { agentKind, agentUniqueId, flowId, pageSize }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFilter {
|
|
63
|
+
const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
|
|
64
|
+
const pageSize =
|
|
65
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
66
|
+
? Math.floor(pageSizeRaw)
|
|
67
|
+
: 20
|
|
68
|
+
return {
|
|
69
|
+
names: Array.isArray(raw?.names) ? raw!.names.map(String).filter(Boolean) : [],
|
|
70
|
+
agentUniqueIds: Array.isArray(raw?.agentUniqueIds)
|
|
71
|
+
? raw!.agentUniqueIds.map(String).filter(Boolean)
|
|
72
|
+
: [],
|
|
73
|
+
resumeUniqueIds: Array.isArray(raw?.resumeUniqueIds)
|
|
74
|
+
? raw!.resumeUniqueIds.map(String).filter(Boolean)
|
|
75
|
+
: [],
|
|
76
|
+
pageSize,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
function normalizeBlock(raw: FormBlock): FormBlock {
|
|
43
81
|
const fileAcceptTypes = normalizeFileAcceptTypes(raw.fileAcceptTypes)
|
|
44
82
|
const type = raw.type
|
|
@@ -69,6 +107,14 @@ function normalizeBlock(raw: FormBlock): FormBlock {
|
|
|
69
107
|
label: opt.label?.trim() || `选项 ${index + 1}`,
|
|
70
108
|
}))
|
|
71
109
|
: undefined,
|
|
110
|
+
resumeEnableRefresh:
|
|
111
|
+
type === 'resume_multiselect' ? raw.resumeEnableRefresh !== false : undefined,
|
|
112
|
+
resumeFilter:
|
|
113
|
+
type === 'resume_multiselect' ? normalizeResumeFilter(raw.resumeFilter) : undefined,
|
|
114
|
+
caseEnableRefresh:
|
|
115
|
+
type === 'case_singleselect' ? raw.caseEnableRefresh !== false : undefined,
|
|
116
|
+
caseFilter:
|
|
117
|
+
type === 'case_singleselect' ? normalizeCaseFilter(raw.caseFilter) : undefined,
|
|
72
118
|
}
|
|
73
119
|
}
|
|
74
120
|
|
|
@@ -135,6 +181,23 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
|
|
|
135
181
|
block.fileMaxCount = 1
|
|
136
182
|
}
|
|
137
183
|
|
|
184
|
+
if (type === 'resume_multiselect') {
|
|
185
|
+
block.resumeEnableRefresh = true
|
|
186
|
+
block.resumeFilter = {
|
|
187
|
+
names: [],
|
|
188
|
+
agentUniqueIds: [],
|
|
189
|
+
resumeUniqueIds: [],
|
|
190
|
+
pageSize: 20,
|
|
191
|
+
}
|
|
192
|
+
block.defaultValue = []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (type === 'case_singleselect') {
|
|
196
|
+
block.caseEnableRefresh = true
|
|
197
|
+
block.caseFilter = { pageSize: 20 }
|
|
198
|
+
block.defaultValue = ''
|
|
199
|
+
}
|
|
200
|
+
|
|
138
201
|
return block
|
|
139
202
|
}
|
|
140
203
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { A2uiComponentNode, FormBlock } from '../../types/index'
|
|
2
|
+
import type { BlockAdapterContext, BlockToComponentResult } from '../types'
|
|
3
|
+
|
|
4
|
+
function asLiteral(value: unknown): string | undefined {
|
|
5
|
+
return typeof value === 'string' ? value : undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function caseSingleselectToComponent(
|
|
9
|
+
block: FormBlock,
|
|
10
|
+
ctx: BlockAdapterContext,
|
|
11
|
+
): BlockToComponentResult {
|
|
12
|
+
const { id, label, path, withData } = ctx
|
|
13
|
+
return withData({
|
|
14
|
+
id,
|
|
15
|
+
component: 'SkoponCaseSelect',
|
|
16
|
+
label,
|
|
17
|
+
placeholder: block.placeholder ?? '',
|
|
18
|
+
help: block.help ?? '',
|
|
19
|
+
enableRefresh: block.caseEnableRefresh !== false,
|
|
20
|
+
caseFilter: {
|
|
21
|
+
agentKind: block.caseFilter?.agentKind,
|
|
22
|
+
agentUniqueId: block.caseFilter?.agentUniqueId,
|
|
23
|
+
flowId: block.caseFilter?.flowId,
|
|
24
|
+
pageSize: block.caseFilter?.pageSize ?? 20,
|
|
25
|
+
},
|
|
26
|
+
value: { path },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function caseSingleselectFromComponent(
|
|
31
|
+
node: A2uiComponentNode,
|
|
32
|
+
base: (type: 'case_singleselect') => FormBlock,
|
|
33
|
+
helpers: {
|
|
34
|
+
asLiteral: (value: unknown) => string | undefined
|
|
35
|
+
nameFromValue: (value: unknown) => string
|
|
36
|
+
},
|
|
37
|
+
): FormBlock {
|
|
38
|
+
const name = helpers.nameFromValue(node.value)
|
|
39
|
+
const block: FormBlock = {
|
|
40
|
+
...base('case_singleselect'),
|
|
41
|
+
name,
|
|
42
|
+
caseEnableRefresh: node.enableRefresh !== false,
|
|
43
|
+
}
|
|
44
|
+
const placeholder = helpers.asLiteral(node.placeholder)
|
|
45
|
+
if (placeholder) block.placeholder = placeholder
|
|
46
|
+
const help = helpers.asLiteral(node.help)
|
|
47
|
+
if (help) block.help = help
|
|
48
|
+
if (node.caseFilter && typeof node.caseFilter === 'object') {
|
|
49
|
+
const cf = node.caseFilter as Record<string, unknown>
|
|
50
|
+
const flowIdRaw = typeof cf.flowId === 'number' ? cf.flowId : Number(cf.flowId)
|
|
51
|
+
const pageSizeRaw = typeof cf.pageSize === 'number' ? cf.pageSize : Number(cf.pageSize)
|
|
52
|
+
const agentKind =
|
|
53
|
+
typeof cf.agentKind === 'string' && cf.agentKind.trim()
|
|
54
|
+
? cf.agentKind.trim()
|
|
55
|
+
: undefined
|
|
56
|
+
const agentUniqueId =
|
|
57
|
+
typeof cf.agentUniqueId === 'string' && cf.agentUniqueId.trim()
|
|
58
|
+
? cf.agentUniqueId.trim()
|
|
59
|
+
: undefined
|
|
60
|
+
block.caseFilter = {
|
|
61
|
+
agentKind,
|
|
62
|
+
agentUniqueId,
|
|
63
|
+
flowId:
|
|
64
|
+
Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined,
|
|
65
|
+
pageSize:
|
|
66
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
67
|
+
? Math.floor(pageSizeRaw)
|
|
68
|
+
: 20,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return block
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { asLiteral }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SkoponCaseSelectImpl } from '../../catalog/skoponCaseSelect'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BlockAdapterPlugin } from '../types'
|
|
2
|
+
import {
|
|
3
|
+
caseSingleselectFromComponent,
|
|
4
|
+
caseSingleselectToComponent,
|
|
5
|
+
} from './adapter'
|
|
6
|
+
import { SkoponCaseSelectImpl } from './catalog'
|
|
7
|
+
|
|
8
|
+
export const caseSingleselectAdapter: BlockAdapterPlugin = {
|
|
9
|
+
type: 'case_singleselect',
|
|
10
|
+
componentName: 'SkoponCaseSelect',
|
|
11
|
+
toComponent: caseSingleselectToComponent,
|
|
12
|
+
fromComponent: caseSingleselectFromComponent,
|
|
13
|
+
catalogComponents: [SkoponCaseSelectImpl],
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FormBlockType } from '../types/index'
|
|
2
|
+
import type { BlockAdapterPlugin } from './types'
|
|
3
|
+
import { caseSingleselectAdapter } from './case_singleselect'
|
|
4
|
+
import { resumeMultiselectAdapter } from './resume_multiselect'
|
|
5
|
+
|
|
6
|
+
const adapters: BlockAdapterPlugin[] = [resumeMultiselectAdapter, caseSingleselectAdapter]
|
|
7
|
+
|
|
8
|
+
const adapterByType = new Map<FormBlockType, BlockAdapterPlugin>(
|
|
9
|
+
adapters.map((adapter) => [adapter.type, adapter]),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const adapterByComponent = new Map<string, BlockAdapterPlugin>(
|
|
13
|
+
adapters.map((adapter) => [adapter.componentName, adapter]),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
export function getBlockTypeAdapter(type: FormBlockType): BlockAdapterPlugin | undefined {
|
|
17
|
+
return adapterByType.get(type)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getComponentAdapter(componentName: string): BlockAdapterPlugin | undefined {
|
|
21
|
+
return adapterByComponent.get(componentName)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAllBlockAdapters(): BlockAdapterPlugin[] {
|
|
25
|
+
return adapters
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getRegisteredCatalogComponents() {
|
|
29
|
+
return adapters.flatMap((adapter) => adapter.catalogComponents)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getRegisteredCatalogComponentNames(): string[] {
|
|
33
|
+
return adapters.map((adapter) => adapter.componentName)
|
|
34
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { A2uiComponentNode, FormBlock } from '../../types/index'
|
|
2
|
+
import type { BlockAdapterContext, BlockToComponentResult } from '../types'
|
|
3
|
+
|
|
4
|
+
function asLiteral(value: unknown): string | undefined {
|
|
5
|
+
return typeof value === 'string' ? value : undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resumeMultiselectToComponent(
|
|
9
|
+
block: FormBlock,
|
|
10
|
+
ctx: BlockAdapterContext,
|
|
11
|
+
): BlockToComponentResult {
|
|
12
|
+
const { id, label, path, withData } = ctx
|
|
13
|
+
return withData({
|
|
14
|
+
id,
|
|
15
|
+
component: 'SkoponResumeSelect',
|
|
16
|
+
label,
|
|
17
|
+
placeholder: block.placeholder ?? '',
|
|
18
|
+
help: block.help ?? '',
|
|
19
|
+
enableRefresh: block.resumeEnableRefresh !== false,
|
|
20
|
+
resumeFilter: {
|
|
21
|
+
names: block.resumeFilter?.names ?? [],
|
|
22
|
+
agentUniqueIds: block.resumeFilter?.agentUniqueIds ?? [],
|
|
23
|
+
resumeUniqueIds: block.resumeFilter?.resumeUniqueIds ?? [],
|
|
24
|
+
pageSize: block.resumeFilter?.pageSize ?? 20,
|
|
25
|
+
},
|
|
26
|
+
value: { path },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resumeMultiselectFromComponent(
|
|
31
|
+
node: A2uiComponentNode,
|
|
32
|
+
base: (type: 'resume_multiselect') => FormBlock,
|
|
33
|
+
helpers: { asLiteral: (value: unknown) => string | undefined; nameFromValue: (value: unknown) => string },
|
|
34
|
+
): FormBlock {
|
|
35
|
+
const name = helpers.nameFromValue(node.value)
|
|
36
|
+
const block: FormBlock = {
|
|
37
|
+
...base('resume_multiselect'),
|
|
38
|
+
name,
|
|
39
|
+
resumeEnableRefresh: node.enableRefresh !== false,
|
|
40
|
+
}
|
|
41
|
+
const placeholder = helpers.asLiteral(node.placeholder)
|
|
42
|
+
if (placeholder) block.placeholder = placeholder
|
|
43
|
+
const help = helpers.asLiteral(node.help)
|
|
44
|
+
if (help) block.help = help
|
|
45
|
+
if (node.resumeFilter && typeof node.resumeFilter === 'object') {
|
|
46
|
+
const rf = node.resumeFilter as Record<string, unknown>
|
|
47
|
+
block.resumeFilter = {
|
|
48
|
+
names: Array.isArray(rf.names) ? rf.names.map(String) : [],
|
|
49
|
+
agentUniqueIds: Array.isArray(rf.agentUniqueIds) ? rf.agentUniqueIds.map(String) : [],
|
|
50
|
+
resumeUniqueIds: Array.isArray(rf.resumeUniqueIds) ? rf.resumeUniqueIds.map(String) : [],
|
|
51
|
+
pageSize: typeof rf.pageSize === 'number' ? rf.pageSize : 20,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return block
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { asLiteral }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SkoponResumeSelectImpl } from '../../catalog/skoponResumeSelect'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BlockAdapterPlugin } from '../types'
|
|
2
|
+
import {
|
|
3
|
+
resumeMultiselectFromComponent,
|
|
4
|
+
resumeMultiselectToComponent,
|
|
5
|
+
} from './adapter'
|
|
6
|
+
import { SkoponResumeSelectImpl } from './catalog'
|
|
7
|
+
|
|
8
|
+
export const resumeMultiselectAdapter: BlockAdapterPlugin = {
|
|
9
|
+
type: 'resume_multiselect',
|
|
10
|
+
componentName: 'SkoponResumeSelect',
|
|
11
|
+
toComponent: resumeMultiselectToComponent,
|
|
12
|
+
fromComponent: resumeMultiselectFromComponent,
|
|
13
|
+
catalogComponents: [SkoponResumeSelectImpl],
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
A2uiComponentNode,
|
|
3
|
+
FormBlock,
|
|
4
|
+
FormBlockType,
|
|
5
|
+
} from '../types/index'
|
|
6
|
+
import type { ReactComponentImplementation } from '@a2ui/react/v0_9'
|
|
7
|
+
|
|
8
|
+
export interface BlockAdapterContext {
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
name: string
|
|
12
|
+
path: string | undefined
|
|
13
|
+
withData: (node: A2uiComponentNode) => {
|
|
14
|
+
node: A2uiComponentNode
|
|
15
|
+
dataKey?: string
|
|
16
|
+
dataValue?: unknown
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type BlockToComponentResult =
|
|
21
|
+
| { node: A2uiComponentNode | null }
|
|
22
|
+
| ReturnType<BlockAdapterContext['withData']>
|
|
23
|
+
|
|
24
|
+
export interface BlockAdapterPlugin {
|
|
25
|
+
type: FormBlockType
|
|
26
|
+
componentName: string
|
|
27
|
+
toComponent: (block: FormBlock, ctx: BlockAdapterContext) => BlockToComponentResult
|
|
28
|
+
fromComponent: (
|
|
29
|
+
node: A2uiComponentNode,
|
|
30
|
+
base: (type: FormBlockType) => FormBlock,
|
|
31
|
+
helpers: { asLiteral: (value: unknown) => string | undefined; nameFromValue: (value: unknown) => string },
|
|
32
|
+
) => FormBlock | null
|
|
33
|
+
catalogComponents: ReactComponentImplementation[]
|
|
34
|
+
}
|
|
@@ -26,6 +26,10 @@ import {
|
|
|
26
26
|
formatFileAcceptSummary,
|
|
27
27
|
} from '../adapter/formFileAccept'
|
|
28
28
|
import { buildMediaListClassName, normalizeMediaSize } from '../adapter/formMedia'
|
|
29
|
+
import {
|
|
30
|
+
getRegisteredCatalogComponentNames,
|
|
31
|
+
getRegisteredCatalogComponents,
|
|
32
|
+
} from '../blocks/registry'
|
|
29
33
|
import FilePlaceholderIcon from '../icons/FilePlaceholderIcon'
|
|
30
34
|
import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
31
35
|
import {
|
|
@@ -53,6 +57,7 @@ const SKOPON_COMPONENT_NAMES = new Set([
|
|
|
53
57
|
'FileUpload',
|
|
54
58
|
'TextField',
|
|
55
59
|
'Text',
|
|
60
|
+
...getRegisteredCatalogComponentNames(),
|
|
56
61
|
])
|
|
57
62
|
|
|
58
63
|
const NON_MARKDOWN_TEXT_VARIANTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'caption'])
|
|
@@ -691,6 +696,7 @@ export function buildSkoponCatalog(): Catalog<ReactComponentImplementation> {
|
|
|
691
696
|
FileUploadImpl,
|
|
692
697
|
TextFieldImpl,
|
|
693
698
|
TextImpl,
|
|
699
|
+
...getRegisteredCatalogComponents(),
|
|
694
700
|
]
|
|
695
701
|
const functions = [...basicCatalog.functions.values()]
|
|
696
702
|
return new Catalog(SKOPON_CATALOG_ID, components, functions, basicCatalog.themeSchema)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import type { FormCaseFilter } from '../types/index'
|
|
3
|
+
|
|
4
|
+
export interface CaseSearchItem {
|
|
5
|
+
caseUniqueId: string
|
|
6
|
+
caseId?: number
|
|
7
|
+
flowId?: number
|
|
8
|
+
name: string
|
|
9
|
+
link?: string
|
|
10
|
+
platform?: string | null
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CaseSearchParams {
|
|
15
|
+
filter: FormCaseFilter
|
|
16
|
+
page: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CaseSearchResult {
|
|
20
|
+
list: CaseSearchItem[]
|
|
21
|
+
total: number
|
|
22
|
+
page: number
|
|
23
|
+
pageSize: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CaseSearchFn = (params: CaseSearchParams) => Promise<CaseSearchResult>
|
|
27
|
+
|
|
28
|
+
const CaseSearchContext = createContext<CaseSearchFn | null>(null)
|
|
29
|
+
|
|
30
|
+
export function CaseSearchProvider({
|
|
31
|
+
caseSearch,
|
|
32
|
+
children,
|
|
33
|
+
}: {
|
|
34
|
+
caseSearch: CaseSearchFn | null | undefined
|
|
35
|
+
children: ReactNode
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<CaseSearchContext.Provider value={caseSearch ?? null}>
|
|
39
|
+
{children}
|
|
40
|
+
</CaseSearchContext.Provider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useCaseSearch(): CaseSearchFn | null {
|
|
45
|
+
return useContext(CaseSearchContext)
|
|
46
|
+
}
|