@skopon-cool/form-sdk 0.1.1 → 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 +49 -11
- package/dist/adapter/a2uiAdapter.d.ts +0 -1
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formFileAccept.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts +1 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/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/catalog/useSkoponBoundField.d.ts +2 -0
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/client/formClient.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +10 -2
- 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/SkoponA2uiStreamRenderer.test.d.ts +2 -0
- package/dist/components/SkoponA2uiStreamRenderer.test.d.ts.map +1 -0
- 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 +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1390 -710
- package/dist/submit/buildCurlStatement.d.ts +8 -0
- package/dist/submit/buildCurlStatement.d.ts.map +1 -1
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts +26 -0
- package/dist/submit/intersectPayloadBlocksWithForm.d.ts.map +1 -0
- package/dist/submit/submitFormJson.d.ts.map +1 -1
- package/dist/types/index.d.ts +26 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/adapter/a2uiAdapter.test.ts +116 -0
- package/src/adapter/a2uiAdapter.ts +48 -4
- package/src/adapter/formFileAccept.test.ts +53 -0
- package/src/adapter/formFileAccept.ts +11 -2
- package/src/adapter/formSchema.test.ts +35 -0
- package/src/adapter/formSchema.ts +70 -3
- 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 +34 -5
- 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/catalog/textFieldPreview.test.tsx +1 -1
- package/src/catalog/useSkoponBoundField.test.ts +62 -0
- package/src/catalog/useSkoponBoundField.ts +10 -1
- package/src/client/formClient.test.ts +83 -0
- package/src/client/formClient.ts +10 -2
- package/src/components/AskUserFormCard.tsx +146 -58
- package/src/components/SkoponA2uiStreamRenderer.test.ts +78 -0
- package/src/components/SkoponA2uiStreamRenderer.test.tsx +103 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +141 -23
- package/src/components/SkoponFormRenderer.tsx +42 -17
- package/src/index.ts +34 -2
- package/src/styles/index.css +65 -0
- package/src/submit/buildCurlStatement.ts +49 -0
- package/src/submit/intersectPayloadBlocksWithForm.ts +175 -0
- package/src/submit/submit.test.ts +170 -10
- package/src/submit/submitFormJson.ts +20 -1
- package/src/types/index.ts +30 -0
- package/dist/submit/intersectPayloadWithForm.d.ts +0 -17
- package/dist/submit/intersectPayloadWithForm.d.ts.map +0 -1
- package/src/submit/intersectPayloadWithForm.ts +0 -54
|
@@ -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
|
+
}
|
|
@@ -20,16 +20,22 @@ import {
|
|
|
20
20
|
} from 'antd'
|
|
21
21
|
import dayjs, { type Dayjs } from 'dayjs'
|
|
22
22
|
import type { FormFilePlaceholderIcon, FormMediaSize } from '../types/index'
|
|
23
|
+
import { coerceToggleValue } from '../adapter/formSchema'
|
|
23
24
|
import {
|
|
24
25
|
buildFileAcceptAttribute,
|
|
25
26
|
formatFileAcceptSummary,
|
|
26
27
|
} from '../adapter/formFileAccept'
|
|
27
28
|
import { buildMediaListClassName, normalizeMediaSize } from '../adapter/formMedia'
|
|
29
|
+
import {
|
|
30
|
+
getRegisteredCatalogComponentNames,
|
|
31
|
+
getRegisteredCatalogComponents,
|
|
32
|
+
} from '../blocks/registry'
|
|
28
33
|
import FilePlaceholderIcon from '../icons/FilePlaceholderIcon'
|
|
29
34
|
import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
30
35
|
import {
|
|
31
36
|
asOptionalString,
|
|
32
37
|
asStringArray,
|
|
38
|
+
readBoundFieldValue,
|
|
33
39
|
useSkoponBoundField,
|
|
34
40
|
} from './useSkoponBoundField'
|
|
35
41
|
|
|
@@ -51,6 +57,7 @@ const SKOPON_COMPONENT_NAMES = new Set([
|
|
|
51
57
|
'FileUpload',
|
|
52
58
|
'TextField',
|
|
53
59
|
'Text',
|
|
60
|
+
...getRegisteredCatalogComponentNames(),
|
|
54
61
|
])
|
|
55
62
|
|
|
56
63
|
const NON_MARKDOWN_TEXT_VARIANTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'caption'])
|
|
@@ -359,7 +366,7 @@ function ToggleSwitchPreview({ context }: { context: ComponentContext }) {
|
|
|
359
366
|
<Switch
|
|
360
367
|
disabled={!interactive}
|
|
361
368
|
className="form-block-preview-control"
|
|
362
|
-
checked={value
|
|
369
|
+
checked={coerceToggleValue(value)}
|
|
363
370
|
onChange={(checked) => setValue(checked)}
|
|
364
371
|
/>
|
|
365
372
|
</div>
|
|
@@ -495,14 +502,18 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
495
502
|
)
|
|
496
503
|
? (props.filePlaceholderIcon as FormFilePlaceholderIcon)
|
|
497
504
|
: 'document'
|
|
505
|
+
const minCount = typeof props.minCount === 'number' ? Math.max(0, props.minCount) : 0
|
|
498
506
|
const maxCount = typeof props.maxCount === 'number' ? props.maxCount : 1
|
|
507
|
+
const hasMaxLimit = maxCount > 0
|
|
499
508
|
const selectedNames = asStringArray(value)
|
|
509
|
+
const atMaxLimit = hasMaxLimit && selectedNames.length >= maxCount
|
|
510
|
+
const belowMinCount = selectedNames.length < minCount
|
|
500
511
|
|
|
501
512
|
return (
|
|
502
513
|
<div className="form-block-preview">
|
|
503
514
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
504
515
|
<Upload.Dragger
|
|
505
|
-
disabled={!interactive}
|
|
516
|
+
disabled={!interactive || atMaxLimit}
|
|
506
517
|
accept={buildFileAcceptAttribute(fileAcceptTypes, fileAcceptExtensions)}
|
|
507
518
|
className="form-file-upload-preview"
|
|
508
519
|
showUploadList={interactive && selectedNames.length > 0}
|
|
@@ -517,12 +528,24 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
517
528
|
}
|
|
518
529
|
beforeUpload={(file) => {
|
|
519
530
|
if (!interactive) return Upload.LIST_IGNORE
|
|
520
|
-
const
|
|
531
|
+
const currentNames = asStringArray(readBoundFieldValue(context))
|
|
532
|
+
if (hasMaxLimit && currentNames.length >= maxCount) return Upload.LIST_IGNORE
|
|
533
|
+
const next = hasMaxLimit
|
|
534
|
+
? [...currentNames, file.name].slice(0, maxCount)
|
|
535
|
+
: [...currentNames, file.name]
|
|
521
536
|
setValue(next)
|
|
522
537
|
return false
|
|
523
538
|
}}
|
|
524
539
|
onRemove={(file) => {
|
|
525
|
-
|
|
540
|
+
const currentNames = asStringArray(readBoundFieldValue(context))
|
|
541
|
+
const indexMatch = /^(\d+)-/.exec(file.uid ?? '')
|
|
542
|
+
const index = indexMatch ? Number(indexMatch[1]) : -1
|
|
543
|
+
const next =
|
|
544
|
+
index >= 0 && index < currentNames.length
|
|
545
|
+
? currentNames.filter((_, i) => i !== index)
|
|
546
|
+
: currentNames.filter((name) => name !== file.name)
|
|
547
|
+
if (next.length < minCount) return false
|
|
548
|
+
setValue(next)
|
|
526
549
|
}}
|
|
527
550
|
>
|
|
528
551
|
<p className="form-file-upload-preview-icon">
|
|
@@ -532,6 +555,9 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
532
555
|
</Upload.Dragger>
|
|
533
556
|
<Typography.Text type="secondary" className="form-block-preview-help">
|
|
534
557
|
允许:{acceptSummary ?? '全部类型'}
|
|
558
|
+
{minCount > 0 ? `;至少 ${minCount} 个文件` : ''}
|
|
559
|
+
{hasMaxLimit ? `;最多 ${maxCount} 个文件` : ''}
|
|
560
|
+
{belowMinCount && interactive ? `(当前 ${selectedNames.length} 个,未达下限)` : ''}
|
|
535
561
|
</Typography.Text>
|
|
536
562
|
</div>
|
|
537
563
|
)
|
|
@@ -567,13 +593,15 @@ function TextFieldPreview({ context }: { context: ComponentContext }) {
|
|
|
567
593
|
/>
|
|
568
594
|
)
|
|
569
595
|
} else if (variant === 'number') {
|
|
596
|
+
const numericValue =
|
|
597
|
+
stringValue && Number.isFinite(Number(stringValue)) ? Number(stringValue) : undefined
|
|
570
598
|
control = (
|
|
571
599
|
<InputNumber
|
|
572
600
|
disabled={disabled}
|
|
573
601
|
className={controlClassName}
|
|
574
602
|
style={{ width: '100%' }}
|
|
575
603
|
placeholder={placeholder || '数字'}
|
|
576
|
-
value={
|
|
604
|
+
value={numericValue}
|
|
577
605
|
onChange={(next) => setValue(next == null ? '' : String(next))}
|
|
578
606
|
/>
|
|
579
607
|
)
|
|
@@ -668,6 +696,7 @@ export function buildSkoponCatalog(): Catalog<ReactComponentImplementation> {
|
|
|
668
696
|
FileUploadImpl,
|
|
669
697
|
TextFieldImpl,
|
|
670
698
|
TextImpl,
|
|
699
|
+
...getRegisteredCatalogComponents(),
|
|
671
700
|
]
|
|
672
701
|
const functions = [...basicCatalog.functions.values()]
|
|
673
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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import type { FormResumeFilter } from '../types/index'
|
|
3
|
+
|
|
4
|
+
export interface ResumeSearchItem {
|
|
5
|
+
resumeUniqueId: string
|
|
6
|
+
resumeId?: number
|
|
7
|
+
agentId?: number
|
|
8
|
+
name: string
|
|
9
|
+
avatarUrl?: string | null
|
|
10
|
+
description?: string
|
|
11
|
+
satisfaction?: number
|
|
12
|
+
published?: number
|
|
13
|
+
worksCount?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResumeSearchParams {
|
|
17
|
+
filter: FormResumeFilter
|
|
18
|
+
page: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResumeSearchResult {
|
|
22
|
+
list: ResumeSearchItem[]
|
|
23
|
+
total: number
|
|
24
|
+
page: number
|
|
25
|
+
pageSize: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ResumeSearchFn = (params: ResumeSearchParams) => Promise<ResumeSearchResult>
|
|
29
|
+
|
|
30
|
+
const ResumeSearchContext = createContext<ResumeSearchFn | null>(null)
|
|
31
|
+
|
|
32
|
+
export function ResumeSearchProvider({
|
|
33
|
+
resumeSearch,
|
|
34
|
+
children,
|
|
35
|
+
}: {
|
|
36
|
+
resumeSearch: ResumeSearchFn | null | undefined
|
|
37
|
+
children: ReactNode
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<ResumeSearchContext.Provider value={resumeSearch ?? null}>
|
|
41
|
+
{children}
|
|
42
|
+
</ResumeSearchContext.Provider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useResumeSearch(): ResumeSearchFn | null {
|
|
47
|
+
return useContext(ResumeSearchContext)
|
|
48
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { Button, Spin, Typography } from 'antd'
|
|
3
|
+
import { FileTextOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
4
|
+
import type { ComponentContext } from '@a2ui/web_core/v0_9'
|
|
5
|
+
import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import type { FormCaseFilter } from '../types/index'
|
|
8
|
+
import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
9
|
+
import { asOptionalString, useSkoponBoundField } from './useSkoponBoundField'
|
|
10
|
+
import { type CaseSearchItem, useCaseSearch } from './caseSearchContext'
|
|
11
|
+
|
|
12
|
+
function readString(value: unknown): string {
|
|
13
|
+
return typeof value === 'string' ? value : ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readCaseFilter(raw: unknown): FormCaseFilter {
|
|
17
|
+
if (!raw || typeof raw !== 'object') {
|
|
18
|
+
return { pageSize: 20 }
|
|
19
|
+
}
|
|
20
|
+
const rec = raw as Record<string, unknown>
|
|
21
|
+
const flowIdRaw = typeof rec.flowId === 'number' ? rec.flowId : Number(rec.flowId)
|
|
22
|
+
const flowId =
|
|
23
|
+
Number.isFinite(flowIdRaw) && flowIdRaw > 0 ? Math.floor(flowIdRaw) : undefined
|
|
24
|
+
const pageSizeRaw = typeof rec.pageSize === 'number' ? rec.pageSize : Number(rec.pageSize)
|
|
25
|
+
const pageSize =
|
|
26
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
27
|
+
? Math.floor(pageSizeRaw)
|
|
28
|
+
: 20
|
|
29
|
+
return { flowId, pageSize }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function filterKey(filter: FormCaseFilter): string {
|
|
33
|
+
return JSON.stringify(filter)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SkoponCaseSelectApi = {
|
|
37
|
+
name: 'SkoponCaseSelect',
|
|
38
|
+
schema: z
|
|
39
|
+
.object({
|
|
40
|
+
label: z.any().optional(),
|
|
41
|
+
placeholder: z.any().optional(),
|
|
42
|
+
help: z.any().optional(),
|
|
43
|
+
enableRefresh: z.any().optional(),
|
|
44
|
+
caseFilter: z.any().optional(),
|
|
45
|
+
value: z.any().optional(),
|
|
46
|
+
})
|
|
47
|
+
.passthrough(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function CaseCard({
|
|
51
|
+
item,
|
|
52
|
+
selected,
|
|
53
|
+
disabled,
|
|
54
|
+
onToggle,
|
|
55
|
+
}: {
|
|
56
|
+
item: CaseSearchItem
|
|
57
|
+
selected: boolean
|
|
58
|
+
disabled: boolean
|
|
59
|
+
onToggle: () => void
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className={`skopon-resume-select-card${selected ? ' skopon-resume-select-card--selected' : ''}`}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
onClick={onToggle}
|
|
67
|
+
>
|
|
68
|
+
<div className="skopon-resume-select-card-avatar">
|
|
69
|
+
<FileTextOutlined style={{ fontSize: 24, color: 'var(--color-primary, #1677ff)' }} />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="skopon-resume-select-card-body">
|
|
72
|
+
<div className="skopon-resume-select-card-name">{item.name}</div>
|
|
73
|
+
{item.description ? (
|
|
74
|
+
<div className="skopon-resume-select-card-desc">{item.description}</div>
|
|
75
|
+
) : null}
|
|
76
|
+
<div className="skopon-resume-select-card-meta">
|
|
77
|
+
{item.caseUniqueId}
|
|
78
|
+
{item.platform ? ` · ${item.platform}` : ''}
|
|
79
|
+
{item.link ? ` · ${item.link}` : ''}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</button>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
|
|
87
|
+
const { interactive } = useA2uiPreviewMode()
|
|
88
|
+
const caseSearch = useCaseSearch()
|
|
89
|
+
const { value, setValue } = useSkoponBoundField(context)
|
|
90
|
+
const props = context.componentModel.properties as Record<string, unknown>
|
|
91
|
+
|
|
92
|
+
const label = readString(props.label)
|
|
93
|
+
const placeholder = readString(props.placeholder)
|
|
94
|
+
const help = readString(props.help)
|
|
95
|
+
const enableRefresh = props.enableRefresh !== false
|
|
96
|
+
const caseFilter = useMemo(() => readCaseFilter(props.caseFilter), [props.caseFilter])
|
|
97
|
+
const filterSig = filterKey(caseFilter)
|
|
98
|
+
|
|
99
|
+
const [page, setPage] = useState(1)
|
|
100
|
+
const [loading, setLoading] = useState(false)
|
|
101
|
+
const [fetchError, setFetchError] = useState('')
|
|
102
|
+
const [items, setItems] = useState<CaseSearchItem[]>([])
|
|
103
|
+
const [total, setTotal] = useState(0)
|
|
104
|
+
const [pageSize, setPageSize] = useState(caseFilter.pageSize ?? 20)
|
|
105
|
+
const requestSeqRef = useRef(0)
|
|
106
|
+
|
|
107
|
+
const selectedId = asOptionalString(value) ?? ''
|
|
108
|
+
|
|
109
|
+
const emptyHint = useMemo(() => {
|
|
110
|
+
if (!caseSearch) return '未配置案例搜索服务'
|
|
111
|
+
if (!caseFilter.flowId) return '未配置 Flow 筛选条件'
|
|
112
|
+
if (fetchError) return fetchError
|
|
113
|
+
return placeholder || '暂无匹配的案例'
|
|
114
|
+
}, [caseSearch, caseFilter.flowId, fetchError, placeholder])
|
|
115
|
+
|
|
116
|
+
const fetchPage = useCallback(
|
|
117
|
+
async (targetPage: number) => {
|
|
118
|
+
const filter = readCaseFilter(JSON.parse(filterSig) as FormCaseFilter)
|
|
119
|
+
const seq = ++requestSeqRef.current
|
|
120
|
+
if (!caseSearch || !filter.flowId) {
|
|
121
|
+
setItems([])
|
|
122
|
+
setTotal(0)
|
|
123
|
+
setFetchError('')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
setLoading(true)
|
|
127
|
+
setFetchError('')
|
|
128
|
+
try {
|
|
129
|
+
const result = await caseSearch({ filter, page: targetPage })
|
|
130
|
+
if (seq !== requestSeqRef.current) return
|
|
131
|
+
setItems(result.list)
|
|
132
|
+
setTotal(result.total)
|
|
133
|
+
setPage(result.page)
|
|
134
|
+
setPageSize(result.pageSize)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (seq !== requestSeqRef.current) return
|
|
137
|
+
setItems([])
|
|
138
|
+
setTotal(0)
|
|
139
|
+
setFetchError(err instanceof Error ? err.message : '加载案例列表失败')
|
|
140
|
+
} finally {
|
|
141
|
+
if (seq === requestSeqRef.current) setLoading(false)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
[caseSearch, filterSig],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
setPage(1)
|
|
149
|
+
void fetchPage(1)
|
|
150
|
+
}, [filterSig, fetchPage])
|
|
151
|
+
|
|
152
|
+
const toggleSelect = (caseUniqueId: string) => {
|
|
153
|
+
if (!interactive) return
|
|
154
|
+
setValue(selectedId === caseUniqueId ? '' : caseUniqueId)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleRefresh = () => {
|
|
158
|
+
const filter = readCaseFilter(JSON.parse(filterSig) as FormCaseFilter)
|
|
159
|
+
const effectivePageSize = pageSize || filter.pageSize || 20
|
|
160
|
+
const totalPages = Math.max(1, Math.ceil(total / effectivePageSize))
|
|
161
|
+
const nextPage = page >= totalPages ? 1 : page + 1
|
|
162
|
+
void fetchPage(nextPage)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className="form-block-preview skopon-resume-select">
|
|
167
|
+
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
168
|
+
{loading && items.length === 0 ? (
|
|
169
|
+
<div className="skopon-resume-select-status">
|
|
170
|
+
<Spin size="small" /> 加载案例…
|
|
171
|
+
</div>
|
|
172
|
+
) : null}
|
|
173
|
+
{!loading && items.length === 0 ? (
|
|
174
|
+
<div className="skopon-resume-select-empty">{emptyHint}</div>
|
|
175
|
+
) : null}
|
|
176
|
+
{items.length > 0 ? (
|
|
177
|
+
<div className="skopon-resume-select-list">
|
|
178
|
+
{items.map((item) => (
|
|
179
|
+
<CaseCard
|
|
180
|
+
key={item.caseUniqueId}
|
|
181
|
+
item={item}
|
|
182
|
+
selected={selectedId === item.caseUniqueId}
|
|
183
|
+
disabled={!interactive}
|
|
184
|
+
onToggle={() => toggleSelect(item.caseUniqueId)}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
) : null}
|
|
189
|
+
{enableRefresh && caseSearch && caseFilter.flowId ? (
|
|
190
|
+
<div className="skopon-resume-select-actions">
|
|
191
|
+
<Button
|
|
192
|
+
type="default"
|
|
193
|
+
size="small"
|
|
194
|
+
icon={<ReloadOutlined />}
|
|
195
|
+
loading={loading}
|
|
196
|
+
disabled={!interactive}
|
|
197
|
+
onClick={handleRefresh}
|
|
198
|
+
>
|
|
199
|
+
换一批
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
203
|
+
{help ? (
|
|
204
|
+
<Typography.Text type="secondary" className="form-block-preview-help">
|
|
205
|
+
{help}
|
|
206
|
+
</Typography.Text>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const SkoponCaseSelectImpl = createBinderlessComponentImplementation(
|
|
213
|
+
SkoponCaseSelectApi,
|
|
214
|
+
SkoponCaseSelectPreview,
|
|
215
|
+
)
|