@skopon-cool/form-sdk 0.1.9 → 0.2.1
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 +24 -6
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/skoponCaseSelect.d.ts.map +1 -1
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/catalog/skoponSelectLoading.test.d.ts +2 -0
- package/dist/catalog/skoponSelectLoading.test.d.ts.map +1 -0
- package/dist/catalog/useSkoponBoundField.d.ts +2 -0
- package/dist/catalog/useSkoponBoundField.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponFormProvider.d.ts +11 -0
- package/dist/components/SkoponFormProvider.d.ts.map +1 -0
- package/dist/components/SkoponFormRenderer.d.ts +2 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +981 -857
- package/dist/theme/antd-theme.d.ts +7 -0
- package/dist/theme/antd-theme.d.ts.map +1 -0
- package/dist/theme/brand-colors.d.ts +19 -0
- package/dist/theme/brand-colors.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/adapter/a2uiAdapter.ts +2 -3
- package/src/adapter/formSchema.test.ts +5 -1
- package/src/adapter/formSchema.ts +3 -1
- package/src/catalog/a2uiCustomCatalog.tsx +15 -1
- package/src/catalog/skoponCaseSelect.tsx +2 -1
- package/src/catalog/skoponResumeSelect.tsx +1 -0
- package/src/catalog/skoponSelectLoading.test.tsx +182 -0
- package/src/catalog/useSkoponBoundField.test.ts +14 -1
- package/src/catalog/useSkoponBoundField.ts +14 -0
- package/src/components/AskUserFormCard.tsx +20 -12
- package/src/components/SkoponA2uiStreamRenderer.tsx +5 -2
- package/src/components/SkoponFormProvider.tsx +23 -0
- package/src/components/SkoponFormRenderer.tsx +17 -2
- package/src/index.ts +6 -0
- package/src/styles/index.css +4 -3
- package/src/styles/tokens.css +224 -0
- package/src/theme/antd-theme.ts +93 -0
- package/src/theme/brand-colors.ts +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"antd-theme.d.ts","sourceRoot":"","sources":["../../src/theme/antd-theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAA;AAGvC;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,WAqFvB,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 品牌墨绿(Jade)唯一 hex 源
|
|
3
|
+
* CSS 侧通过 Radix --jade-* / 语义变量 --color-* 引用;Ant Design 从此文件同步
|
|
4
|
+
* 规范:front/design/ui/UI规范.md §2.2、§2.5
|
|
5
|
+
*/
|
|
6
|
+
export declare const brandColors: {
|
|
7
|
+
readonly jade9: "#29a383";
|
|
8
|
+
readonly jade10: "#26997b";
|
|
9
|
+
readonly jade11: "#208368";
|
|
10
|
+
readonly jade12: "#1d3b31";
|
|
11
|
+
/** 链接、边框、Logo 图形、实心底(与 primarySolid 同色) */
|
|
12
|
+
readonly primary: "#208368";
|
|
13
|
+
/** Primary 实心按钮、Sider 胶囊、发布等 type="primary" */
|
|
14
|
+
readonly primarySolid: "#208368";
|
|
15
|
+
readonly primaryHover: "#26997b";
|
|
16
|
+
readonly primaryActive: "#1d3b31";
|
|
17
|
+
readonly onPrimary: "#ffffff";
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=brand-colors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand-colors.d.ts","sourceRoot":"","sources":["../../src/theme/brand-colors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;IAKtB,2CAA2C;;IAE3C,+CAA+C;;;;;CAKvC,CAAA"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1
|
|
6
|
+
"version": "0.2.1",
|
|
7
7
|
"description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": [
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"prepublishOnly": "npm run build && npm test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@radix-ui/colors": "^3.0.0",
|
|
34
35
|
"dayjs": "^1.11.13",
|
|
35
36
|
"zod": "^3.24.0"
|
|
36
37
|
},
|
|
@@ -8,9 +8,8 @@ import type {
|
|
|
8
8
|
FormSchema,
|
|
9
9
|
} from '../types/index'
|
|
10
10
|
import { generateId } from './id'
|
|
11
|
-
import { coerceToggleValue } from './formSchema'
|
|
11
|
+
import { coerceToggleValue, syncFormDefinition } from './formSchema'
|
|
12
12
|
import { getMediaUrls, normalizeMediaSize } from './formMedia'
|
|
13
|
-
import { syncFormDefinition } from './formSchema'
|
|
14
13
|
import { getBlockTypeAdapter, getComponentAdapter } from '../blocks/registry'
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -491,7 +490,7 @@ function applyDataModelDefaults(
|
|
|
491
490
|
const raw = dataModel[name]
|
|
492
491
|
if (raw === undefined) return block
|
|
493
492
|
if (block.type === 'toggle') {
|
|
494
|
-
return { ...block, defaultValue: raw
|
|
493
|
+
return { ...block, defaultValue: coerceToggleValue(raw) }
|
|
495
494
|
}
|
|
496
495
|
return { ...block, defaultValue: raw as FormBlock['defaultValue'] }
|
|
497
496
|
})
|
|
@@ -5,11 +5,15 @@ import { describe, it, expect } from 'vitest'
|
|
|
5
5
|
import { coerceToggleValue, normalizeFormDefinition } from './formSchema'
|
|
6
6
|
|
|
7
7
|
describe('coerceToggleValue', () => {
|
|
8
|
-
it('accepts boolean and
|
|
8
|
+
it('accepts boolean, string, and numeric truthy/falsy values', () => {
|
|
9
9
|
expect(coerceToggleValue(true)).toBe(true)
|
|
10
10
|
expect(coerceToggleValue('true')).toBe(true)
|
|
11
|
+
expect(coerceToggleValue(1)).toBe(true)
|
|
12
|
+
expect(coerceToggleValue('1')).toBe(true)
|
|
11
13
|
expect(coerceToggleValue(false)).toBe(false)
|
|
12
14
|
expect(coerceToggleValue('false')).toBe(false)
|
|
15
|
+
expect(coerceToggleValue(0)).toBe(false)
|
|
16
|
+
expect(coerceToggleValue('0')).toBe(false)
|
|
13
17
|
expect(coerceToggleValue(undefined)).toBe(false)
|
|
14
18
|
})
|
|
15
19
|
})
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
const DEFAULT_MEDIA_URL_MAX = 10
|
|
15
15
|
|
|
16
16
|
export function coerceToggleValue(raw: unknown): boolean {
|
|
17
|
-
|
|
17
|
+
if (raw === true || raw === 'true' || raw === 1 || raw === '1') return true
|
|
18
|
+
if (raw === false || raw === 'false' || raw === 0 || raw === '0') return false
|
|
19
|
+
return false
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function normalizeFileUploadCount(raw: unknown, fallback = 1): number {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import type
|
|
2
|
+
import { useEffect, type ReactNode } from 'react'
|
|
3
3
|
import { Catalog, type ComponentContext } from '@a2ui/web_core/v0_9'
|
|
4
4
|
import {
|
|
5
5
|
basicCatalog,
|
|
@@ -35,6 +35,7 @@ import { useA2uiPreviewMode } from './a2uiPreviewContext'
|
|
|
35
35
|
import {
|
|
36
36
|
asOptionalString,
|
|
37
37
|
asStringArray,
|
|
38
|
+
normalizeSelectStoredValue,
|
|
38
39
|
readBoundFieldValue,
|
|
39
40
|
useSkoponBoundField,
|
|
40
41
|
} from './useSkoponBoundField'
|
|
@@ -247,6 +248,11 @@ function SkoponSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
247
248
|
const options = readOptions(props.options)
|
|
248
249
|
const isMultiple = readString(props.mode) === 'multiple'
|
|
249
250
|
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
const normalized = normalizeSelectStoredValue(value, isMultiple)
|
|
253
|
+
if (normalized !== null) setValue(normalized)
|
|
254
|
+
}, [isMultiple, value, setValue])
|
|
255
|
+
|
|
250
256
|
return (
|
|
251
257
|
<div className="form-block-preview">
|
|
252
258
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
@@ -289,6 +295,13 @@ function ChoicePickerPreview({ context }: { context: ComponentContext }) {
|
|
|
289
295
|
: isMultiple
|
|
290
296
|
? 'checkbox'
|
|
291
297
|
: 'radio'
|
|
298
|
+
const isMultiplePicker =
|
|
299
|
+
effectiveType === 'multiselect' || effectiveType === 'checkbox'
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
const normalized = normalizeSelectStoredValue(value, isMultiplePicker)
|
|
303
|
+
if (normalized !== null) setValue(normalized)
|
|
304
|
+
}, [isMultiplePicker, value, setValue])
|
|
292
305
|
|
|
293
306
|
return (
|
|
294
307
|
<div className="form-block-preview">
|
|
@@ -557,6 +570,7 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
557
570
|
允许:{acceptSummary ?? '全部类型'}
|
|
558
571
|
{minCount > 0 ? `;至少 ${minCount} 个文件` : ''}
|
|
559
572
|
{hasMaxLimit ? `;最多 ${maxCount} 个文件` : ''}
|
|
573
|
+
;当前仅收集文件名(不含文件内容,需宿主另行上传)
|
|
560
574
|
{belowMinCount && interactive ? `(当前 ${selectedNames.length} 个,未达下限)` : ''}
|
|
561
575
|
</Typography.Text>
|
|
562
576
|
</div>
|
|
@@ -90,7 +90,7 @@ function CaseCard({
|
|
|
90
90
|
onClick={onToggle}
|
|
91
91
|
>
|
|
92
92
|
<div className="skopon-resume-select-card-avatar">
|
|
93
|
-
<FileTextOutlined style={{ fontSize: 24, color: 'var(--color-primary, #
|
|
93
|
+
<FileTextOutlined style={{ fontSize: 24, color: 'var(--color-primary, #208368)' }} />
|
|
94
94
|
</div>
|
|
95
95
|
<div className="skopon-resume-select-card-body">
|
|
96
96
|
<div className="skopon-resume-select-card-name">{item.name}</div>
|
|
@@ -152,6 +152,7 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
|
|
|
152
152
|
setItems([])
|
|
153
153
|
setTotal(0)
|
|
154
154
|
setFetchError('')
|
|
155
|
+
setLoading(false)
|
|
155
156
|
return
|
|
156
157
|
}
|
|
157
158
|
setLoading(true)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('@a2ui/web_core/v0_9/basic_catalog', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import('@a2ui/web_core/v0_9/basic_catalog')>()
|
|
6
|
+
return { ...actual, injectBasicCatalogStyles: vi.fn() }
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
vi.mock('@a2ui/react/styles', () => ({
|
|
10
|
+
injectStyles: vi.fn(),
|
|
11
|
+
removeStyles: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
import { createElement } from 'react'
|
|
15
|
+
import { render, cleanup, waitFor } from '@testing-library/react'
|
|
16
|
+
import { blocksToA2ui } from '../adapter/a2uiAdapter'
|
|
17
|
+
import SkoponFormRenderer from '../components/SkoponFormRenderer'
|
|
18
|
+
import type { CaseSearchFn } from './caseSearchContext'
|
|
19
|
+
import type { ResumeSearchFn } from './resumeSearchContext'
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
class ResizeObserverMock {
|
|
23
|
+
observe() {}
|
|
24
|
+
unobserve() {}
|
|
25
|
+
disconnect() {}
|
|
26
|
+
}
|
|
27
|
+
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function buildCaseSurface(flowUniqueIds: string[]) {
|
|
31
|
+
return blocksToA2ui(
|
|
32
|
+
{
|
|
33
|
+
title: '',
|
|
34
|
+
description: '',
|
|
35
|
+
blocks: [
|
|
36
|
+
{
|
|
37
|
+
id: 'b-case',
|
|
38
|
+
type: 'case_multiselect',
|
|
39
|
+
name: 'cases',
|
|
40
|
+
label: '案例',
|
|
41
|
+
caseFilter: { flowUniqueIds, pageSize: 20 },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{ includeHeader: false },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildResumeSurface() {
|
|
50
|
+
return blocksToA2ui(
|
|
51
|
+
{
|
|
52
|
+
title: '',
|
|
53
|
+
description: '',
|
|
54
|
+
blocks: [
|
|
55
|
+
{
|
|
56
|
+
id: 'b-resume',
|
|
57
|
+
type: 'resume_multiselect',
|
|
58
|
+
name: 'resumes',
|
|
59
|
+
label: '简历',
|
|
60
|
+
resumeFilter: { names: [], agentUniqueIds: [], resumeUniqueIds: [], pageSize: 20 },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{ includeHeader: false },
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('case/resume select loading state', () => {
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
cleanup()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('clears case loading when caseSearch is removed while fetch is in flight', async () => {
|
|
74
|
+
let resolveSearch!: (value: Awaited<ReturnType<CaseSearchFn>>) => void
|
|
75
|
+
const caseSearch = vi.fn(
|
|
76
|
+
() =>
|
|
77
|
+
new Promise<Awaited<ReturnType<CaseSearchFn>>>((resolve) => {
|
|
78
|
+
resolveSearch = resolve
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const doc = buildCaseSurface(['flow-1'])
|
|
83
|
+
const { rerender, queryByText } = render(
|
|
84
|
+
createElement(SkoponFormRenderer, {
|
|
85
|
+
doc,
|
|
86
|
+
surfaceId: 'case-loading-test',
|
|
87
|
+
caseSearch,
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(queryByText('加载案例…')).toBeTruthy()
|
|
92
|
+
|
|
93
|
+
rerender(
|
|
94
|
+
createElement(SkoponFormRenderer, {
|
|
95
|
+
doc,
|
|
96
|
+
surfaceId: 'case-loading-test',
|
|
97
|
+
caseSearch: null,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(queryByText('加载案例…')).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
expect(queryByText('未配置案例搜索服务')).toBeTruthy()
|
|
105
|
+
|
|
106
|
+
resolveSearch({ list: [], total: 0, page: 1, pageSize: 20 })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('clears case loading when filter becomes invalid while fetch is in flight', async () => {
|
|
110
|
+
let resolveSearch!: (value: Awaited<ReturnType<CaseSearchFn>>) => void
|
|
111
|
+
const caseSearch = vi.fn(
|
|
112
|
+
() =>
|
|
113
|
+
new Promise<Awaited<ReturnType<CaseSearchFn>>>((resolve) => {
|
|
114
|
+
resolveSearch = resolve
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const validDoc = buildCaseSurface(['flow-1'])
|
|
119
|
+
const invalidDoc = buildCaseSurface([])
|
|
120
|
+
|
|
121
|
+
const { rerender, queryByText } = render(
|
|
122
|
+
createElement(SkoponFormRenderer, {
|
|
123
|
+
doc: validDoc,
|
|
124
|
+
surfaceId: 'case-filter-test',
|
|
125
|
+
caseSearch,
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(queryByText('加载案例…')).toBeTruthy()
|
|
130
|
+
|
|
131
|
+
rerender(
|
|
132
|
+
createElement(SkoponFormRenderer, {
|
|
133
|
+
doc: invalidDoc,
|
|
134
|
+
surfaceId: 'case-filter-test',
|
|
135
|
+
caseSearch,
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(queryByText('加载案例…')).toBeNull()
|
|
141
|
+
})
|
|
142
|
+
expect(queryByText('未配置案例列表筛选条件')).toBeTruthy()
|
|
143
|
+
|
|
144
|
+
resolveSearch({ list: [], total: 0, page: 1, pageSize: 20 })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('clears resume loading when resumeSearch is removed while fetch is in flight', async () => {
|
|
148
|
+
let resolveSearch!: (value: Awaited<ReturnType<ResumeSearchFn>>) => void
|
|
149
|
+
const resumeSearch = vi.fn(
|
|
150
|
+
() =>
|
|
151
|
+
new Promise<Awaited<ReturnType<ResumeSearchFn>>>((resolve) => {
|
|
152
|
+
resolveSearch = resolve
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const doc = buildResumeSurface()
|
|
157
|
+
const { rerender, queryByText } = render(
|
|
158
|
+
createElement(SkoponFormRenderer, {
|
|
159
|
+
doc,
|
|
160
|
+
surfaceId: 'resume-loading-test',
|
|
161
|
+
resumeSearch,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
expect(queryByText('加载简历…')).toBeTruthy()
|
|
166
|
+
|
|
167
|
+
rerender(
|
|
168
|
+
createElement(SkoponFormRenderer, {
|
|
169
|
+
doc,
|
|
170
|
+
surfaceId: 'resume-loading-test',
|
|
171
|
+
resumeSearch: null,
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(queryByText('加载简历…')).toBeNull()
|
|
177
|
+
})
|
|
178
|
+
expect(queryByText('未配置简历搜索服务')).toBeTruthy()
|
|
179
|
+
|
|
180
|
+
resolveSearch({ list: [], total: 0, page: 1, pageSize: 20 })
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect } from 'vitest'
|
|
5
5
|
import type { ComponentContext } from '@a2ui/web_core/v0_9'
|
|
6
|
-
import { asStringArray, clampStringArraySelection, readBoundFieldValue, toggleStringArraySelection } from './useSkoponBoundField'
|
|
6
|
+
import { asStringArray, clampStringArraySelection, normalizeSelectStoredValue, readBoundFieldValue, toggleStringArraySelection } from './useSkoponBoundField'
|
|
7
7
|
|
|
8
8
|
function createMockContext(initial: unknown = []): {
|
|
9
9
|
context: ComponentContext
|
|
@@ -29,6 +29,19 @@ function createMockContext(initial: unknown = []): {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
describe('normalizeSelectStoredValue', () => {
|
|
33
|
+
it('coerces string to array in multiple mode', () => {
|
|
34
|
+
expect(normalizeSelectStoredValue('a', true)).toEqual(['a'])
|
|
35
|
+
expect(normalizeSelectStoredValue(['a', 'b'], true)).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('coerces array to string in single mode', () => {
|
|
39
|
+
expect(normalizeSelectStoredValue(['a', 'b'], false)).toBe('a')
|
|
40
|
+
expect(normalizeSelectStoredValue([], false)).toBe('')
|
|
41
|
+
expect(normalizeSelectStoredValue('a', false)).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
32
45
|
describe('readBoundFieldValue', () => {
|
|
33
46
|
it('reads current bound value from dataContext', () => {
|
|
34
47
|
const { context } = createMockContext(['a.pdf'])
|
|
@@ -65,6 +65,20 @@ export function asStringArray(value: unknown): string[] {
|
|
|
65
65
|
return []
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** 单选/多选模式切换时规范化 stored value;无需变更时返回 null。 */
|
|
69
|
+
export function normalizeSelectStoredValue(value: unknown, isMultiple: boolean): unknown | null {
|
|
70
|
+
if (isMultiple) {
|
|
71
|
+
if (Array.isArray(value)) return null
|
|
72
|
+
return asStringArray(value)
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(value)) {
|
|
75
|
+
return value.length > 0 ? String(value[0]) : ''
|
|
76
|
+
}
|
|
77
|
+
if (value === undefined || value === null) return null
|
|
78
|
+
if (typeof value === 'string' || typeof value === 'number') return null
|
|
79
|
+
return ''
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
export function readOptionalSelectCount(value: unknown): number | undefined {
|
|
69
83
|
if (value === undefined || value === null) return undefined
|
|
70
84
|
const num = typeof value === 'number' ? value : Number(value)
|
|
@@ -16,6 +16,7 @@ import type { CaseSearchFn } from '../catalog/caseSearchContext'
|
|
|
16
16
|
import type { ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
17
17
|
import SkoponFormRenderer, { type SkoponFormRendererRef } from './SkoponFormRenderer'
|
|
18
18
|
import CurlSubmitBlock from './CurlSubmitBlock'
|
|
19
|
+
import SkoponFormProvider from './SkoponFormProvider'
|
|
19
20
|
|
|
20
21
|
function formatSubmitErrorMessage(status: number, body: unknown): string {
|
|
21
22
|
if (body && typeof body === 'object' && body !== null && 'message' in body) {
|
|
@@ -154,21 +155,25 @@ export default function AskUserFormCard({
|
|
|
154
155
|
|
|
155
156
|
if (loading) {
|
|
156
157
|
return (
|
|
157
|
-
<
|
|
158
|
-
<
|
|
159
|
-
|
|
158
|
+
<SkoponFormProvider>
|
|
159
|
+
<div className="ask-user-form-card">
|
|
160
|
+
<Spin size="small" />
|
|
161
|
+
</div>
|
|
162
|
+
</SkoponFormProvider>
|
|
160
163
|
)
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
if (!payloadDef || renderBlocks.length === 0) {
|
|
164
167
|
return (
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
<SkoponFormProvider>
|
|
169
|
+
<CurlSubmitBlock
|
|
170
|
+
payload={payload}
|
|
171
|
+
callbackUrl={callbackUrl}
|
|
172
|
+
copyMode="submitFields"
|
|
173
|
+
unpublishedFormId={formDisabled && resolvedFormUniqueId ? resolvedFormUniqueId : undefined}
|
|
174
|
+
onNotify={onNotify}
|
|
175
|
+
/>
|
|
176
|
+
</SkoponFormProvider>
|
|
172
177
|
)
|
|
173
178
|
}
|
|
174
179
|
|
|
@@ -210,7 +215,8 @@ export default function AskUserFormCard({
|
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
return (
|
|
213
|
-
<
|
|
218
|
+
<SkoponFormProvider>
|
|
219
|
+
<div className="ask-user-form-card">
|
|
214
220
|
{formDisabled && resolvedFormUniqueId ? (
|
|
215
221
|
<div className="ask-user-form-card-status">
|
|
216
222
|
<Tooltip title={resolvedFormUniqueId}>
|
|
@@ -224,6 +230,7 @@ export default function AskUserFormCard({
|
|
|
224
230
|
surfaceId={surfaceId}
|
|
225
231
|
fieldNames={fieldNames}
|
|
226
232
|
interactive
|
|
233
|
+
wrapProvider={false}
|
|
227
234
|
resumeSearch={resumeSearch}
|
|
228
235
|
caseSearch={caseSearch}
|
|
229
236
|
/>
|
|
@@ -241,6 +248,7 @@ export default function AskUserFormCard({
|
|
|
241
248
|
</Button>
|
|
242
249
|
</Tooltip>
|
|
243
250
|
</div>
|
|
244
|
-
|
|
251
|
+
</div>
|
|
252
|
+
</SkoponFormProvider>
|
|
245
253
|
)
|
|
246
254
|
}
|
|
@@ -7,6 +7,7 @@ import { buildSkoponCatalog } from '../catalog/a2uiCustomCatalog'
|
|
|
7
7
|
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
8
8
|
import { CaseSearchProvider, type CaseSearchFn } from '../catalog/caseSearchContext'
|
|
9
9
|
import { ResumeSearchProvider, type ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
10
|
+
import SkoponFormProvider from './SkoponFormProvider'
|
|
10
11
|
|
|
11
12
|
export interface SkoponA2uiStreamRendererProps {
|
|
12
13
|
/** 增量追加的 A2UI v0.9 消息 */
|
|
@@ -177,10 +178,11 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
177
178
|
}
|
|
178
179
|
}, [messages, surfaceId])
|
|
179
180
|
|
|
180
|
-
if (surfaces.length === 0) return
|
|
181
|
+
if (surfaces.length === 0) return <SkoponFormProvider>{emptyHint}</SkoponFormProvider>
|
|
181
182
|
|
|
182
183
|
return (
|
|
183
|
-
<
|
|
184
|
+
<SkoponFormProvider>
|
|
185
|
+
<ResumeSearchProvider resumeSearch={resumeSearch}>
|
|
184
186
|
<CaseSearchProvider caseSearch={caseSearch}>
|
|
185
187
|
<A2uiPreviewModeProvider interactive={interactive}>
|
|
186
188
|
<div className="a2ui-surface a2ui-container">
|
|
@@ -191,5 +193,6 @@ export default function SkoponA2uiStreamRenderer({
|
|
|
191
193
|
</A2uiPreviewModeProvider>
|
|
192
194
|
</CaseSearchProvider>
|
|
193
195
|
</ResumeSearchProvider>
|
|
196
|
+
</SkoponFormProvider>
|
|
194
197
|
)
|
|
195
198
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigProvider, type ThemeConfig } from 'antd'
|
|
2
|
+
import type { Locale } from 'antd/es/locale'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { antdTheme as skoponAntdTheme } from '../theme/antd-theme'
|
|
5
|
+
|
|
6
|
+
export interface SkoponFormProviderProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
locale?: Locale
|
|
9
|
+
/** 默认 Skopon 墨绿主题;传入可覆盖 */
|
|
10
|
+
theme?: ThemeConfig
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function SkoponFormProvider({
|
|
14
|
+
children,
|
|
15
|
+
locale,
|
|
16
|
+
theme,
|
|
17
|
+
}: SkoponFormProviderProps) {
|
|
18
|
+
return (
|
|
19
|
+
<ConfigProvider theme={theme ?? skoponAntdTheme} locale={locale}>
|
|
20
|
+
{children}
|
|
21
|
+
</ConfigProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -17,6 +17,7 @@ import { SKOPON_CATALOG_ID, buildSkoponCatalog } from '../catalog/a2uiCustomCata
|
|
|
17
17
|
import { A2uiPreviewModeProvider } from '../catalog/a2uiPreviewContext'
|
|
18
18
|
import { CaseSearchProvider, type CaseSearchFn } from '../catalog/caseSearchContext'
|
|
19
19
|
import { ResumeSearchProvider, type ResumeSearchFn } from '../catalog/resumeSearchContext'
|
|
20
|
+
import SkoponFormProvider from './SkoponFormProvider'
|
|
20
21
|
|
|
21
22
|
export interface SkoponFormRendererRef {
|
|
22
23
|
getValues: (fieldNames?: string[]) => Record<string, unknown>
|
|
@@ -33,6 +34,8 @@ export interface SkoponFormRendererProps {
|
|
|
33
34
|
resumeSearch?: ResumeSearchFn | null
|
|
34
35
|
/** 案例单选组件:注入搜索函数 */
|
|
35
36
|
caseSearch?: CaseSearchFn | null
|
|
37
|
+
/** 外层已包裹 SkoponFormProvider 时可设为 false,避免重复 ConfigProvider */
|
|
38
|
+
wrapProvider?: boolean
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererProps>(
|
|
@@ -45,6 +48,7 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
45
48
|
fieldNames = [],
|
|
46
49
|
resumeSearch = null,
|
|
47
50
|
caseSearch = null,
|
|
51
|
+
wrapProvider = true,
|
|
48
52
|
},
|
|
49
53
|
ref,
|
|
50
54
|
) {
|
|
@@ -104,9 +108,18 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
104
108
|
[surfaceId],
|
|
105
109
|
)
|
|
106
110
|
|
|
107
|
-
if (surfaces.length === 0)
|
|
111
|
+
if (surfaces.length === 0) {
|
|
112
|
+
const empty = (
|
|
113
|
+
<ResumeSearchProvider resumeSearch={resumeSearch}>
|
|
114
|
+
<CaseSearchProvider caseSearch={caseSearch}>
|
|
115
|
+
<A2uiPreviewModeProvider interactive={interactive}>{emptyHint}</A2uiPreviewModeProvider>
|
|
116
|
+
</CaseSearchProvider>
|
|
117
|
+
</ResumeSearchProvider>
|
|
118
|
+
)
|
|
119
|
+
return wrapProvider ? <SkoponFormProvider>{empty}</SkoponFormProvider> : empty
|
|
120
|
+
}
|
|
108
121
|
|
|
109
|
-
|
|
122
|
+
const content = (
|
|
110
123
|
<ResumeSearchProvider resumeSearch={resumeSearch}>
|
|
111
124
|
<CaseSearchProvider caseSearch={caseSearch}>
|
|
112
125
|
<A2uiPreviewModeProvider interactive={interactive}>
|
|
@@ -119,6 +132,8 @@ const SkoponFormRenderer = forwardRef<SkoponFormRendererRef, SkoponFormRendererP
|
|
|
119
132
|
</CaseSearchProvider>
|
|
120
133
|
</ResumeSearchProvider>
|
|
121
134
|
)
|
|
135
|
+
|
|
136
|
+
return wrapProvider ? <SkoponFormProvider>{content}</SkoponFormProvider> : content
|
|
122
137
|
},
|
|
123
138
|
)
|
|
124
139
|
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,12 @@ export type { AskUserFormCardProps } from './components/AskUserFormCard'
|
|
|
80
80
|
export { default as CurlSubmitBlock } from './components/CurlSubmitBlock'
|
|
81
81
|
export type { CurlSubmitBlockProps } from './components/CurlSubmitBlock'
|
|
82
82
|
|
|
83
|
+
export { default as SkoponFormProvider } from './components/SkoponFormProvider'
|
|
84
|
+
export type { SkoponFormProviderProps } from './components/SkoponFormProvider'
|
|
85
|
+
|
|
86
|
+
export { antdTheme as skoponAntdTheme } from './theme/antd-theme'
|
|
87
|
+
export { brandColors as skoponBrandColors } from './theme/brand-colors'
|
|
88
|
+
|
|
83
89
|
|
|
84
90
|
export {
|
|
85
91
|
ResumeSearchProvider,
|
package/src/styles/index.css
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@import './tokens.css';
|
|
1
2
|
@import './a2ui-preview.css';
|
|
2
3
|
|
|
3
4
|
/* form-block-preview(与 admin form-tally 预览对齐的子集) */
|
|
@@ -250,11 +251,11 @@
|
|
|
250
251
|
min-width: 220px;
|
|
251
252
|
}
|
|
252
253
|
.skopon-resume-select-card:hover:not(:disabled) {
|
|
253
|
-
border-color: var(--color-primary, #
|
|
254
|
+
border-color: var(--color-primary, #208368);
|
|
254
255
|
}
|
|
255
256
|
.skopon-resume-select-card--selected {
|
|
256
|
-
border-color: var(--color-primary, #
|
|
257
|
-
background: color-mix(in srgb, var(--color-primary, #
|
|
257
|
+
border-color: var(--color-primary, #208368);
|
|
258
|
+
background: color-mix(in srgb, var(--color-primary, #208368) 8%, transparent);
|
|
258
259
|
}
|
|
259
260
|
.skopon-resume-select-card:disabled {
|
|
260
261
|
cursor: not-allowed;
|