@skopon-cool/form-sdk 0.6.1 → 0.6.3
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 +16 -0
- package/dist/adapter/a2uiAdapter.d.ts.map +1 -1
- package/dist/adapter/formSchema.d.ts +3 -0
- package/dist/adapter/formSchema.d.ts.map +1 -1
- package/dist/blocks/resume_multiselect/adapter.d.ts.map +1 -1
- package/dist/catalog/a2uiCustomCatalog.d.ts.map +1 -1
- package/dist/catalog/contentSearchContext.d.ts +35 -0
- package/dist/catalog/contentSearchContext.d.ts.map +1 -0
- package/dist/catalog/pairResumeContent.d.ts +20 -0
- package/dist/catalog/pairResumeContent.d.ts.map +1 -0
- package/dist/catalog/skoponResumeSelect.d.ts.map +1 -1
- package/dist/client/contentSearchClient.d.ts +36 -0
- package/dist/client/contentSearchClient.d.ts.map +1 -0
- package/dist/client/resumeSearchClient.d.ts.map +1 -1
- package/dist/client/skoponSdkClients.d.ts +4 -0
- package/dist/client/skoponSdkClients.d.ts.map +1 -1
- package/dist/components/AskUserFormCard.d.ts +3 -0
- package/dist/components/AskUserFormCard.d.ts.map +1 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts +4 -1
- package/dist/components/SkoponA2uiStreamRenderer.d.ts.map +1 -1
- package/dist/components/SkoponFormRenderer.d.ts +3 -0
- package/dist/components/SkoponFormRenderer.d.ts.map +1 -1
- package/dist/form-sdk.css +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1855 -1538
- package/dist/types/index.d.ts +29 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapter/a2uiAdapter.test.ts +12 -0
- package/src/adapter/a2uiAdapter.ts +8 -0
- package/src/adapter/formSchema.test.ts +8 -0
- package/src/adapter/formSchema.ts +55 -0
- package/src/blocks/resume_multiselect/adapter.ts +31 -0
- package/src/catalog/a2uiCustomCatalog.tsx +19 -0
- package/src/catalog/contentSearchContext.tsx +53 -0
- package/src/catalog/fileUploadPreview.test.tsx +17 -0
- package/src/catalog/pairResumeContent.test.ts +34 -0
- package/src/catalog/pairResumeContent.ts +55 -0
- package/src/catalog/skoponResumeSelect.tsx +348 -153
- package/src/catalog/skoponSelectLoading.test.tsx +84 -5
- package/src/client/contentSearchClient.test.ts +123 -0
- package/src/client/contentSearchClient.ts +156 -0
- package/src/client/resumeSearchClient.test.ts +5 -0
- package/src/client/resumeSearchClient.ts +5 -0
- package/src/client/skoponSdkClients.ts +9 -0
- package/src/components/AskUserFormCard.tsx +5 -0
- package/src/components/SkoponA2uiStreamRenderer.tsx +15 -9
- package/src/components/SkoponFormRenderer.tsx +20 -12
- package/src/index.ts +16 -0
- package/src/styles/a2ui-preview.css +4 -4
- package/src/styles/index.css +158 -13
- package/src/submit/submit.test.ts +1 -0
- package/src/types/index.ts +30 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -40,12 +40,30 @@ export interface FormBlockCondition {
|
|
|
40
40
|
/** 简历多选组件:渲染时用于 /univ/resume/search 的筛选条件 */
|
|
41
41
|
export interface FormResumeFilter {
|
|
42
42
|
names?: string[];
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* 智能体类型 unique_id。
|
|
45
|
+
* 运行时作为 agent_type:先找出该类型的智能体,再查他们的简历。
|
|
46
|
+
*/
|
|
44
47
|
agentKind?: string;
|
|
45
48
|
agentUniqueIds?: string[];
|
|
49
|
+
/** 智能体标签 unique_id,多值 AND;与 agentKind 一起定位智能体后再查简历 */
|
|
50
|
+
tagUniqueIds?: string[];
|
|
46
51
|
resumeUniqueIds?: string[];
|
|
47
52
|
pageSize?: number;
|
|
48
53
|
}
|
|
54
|
+
/** 内容制作搜索:渲染时用于 /univ/flow/list?owner_scope=content */
|
|
55
|
+
export interface FormContentFilter {
|
|
56
|
+
/** 流程类型 unique_id,默认 content-production */
|
|
57
|
+
flowType?: string;
|
|
58
|
+
/** 流程类别 unique_id */
|
|
59
|
+
category?: string;
|
|
60
|
+
/** 内容标签 unique_id */
|
|
61
|
+
tagUniqueIds?: string[];
|
|
62
|
+
/** 多标签匹配方式;all=全部命中,any=命中任一 */
|
|
63
|
+
tagMatch?: 'all' | 'any';
|
|
64
|
+
/** 每次向内容接口请求的条数,不是一屏展示张数 */
|
|
65
|
+
pageSize?: number;
|
|
66
|
+
}
|
|
49
67
|
/** 案例多选组件:渲染时用于 /univ/case/list 的筛选条件 */
|
|
50
68
|
export interface FormCaseFilter {
|
|
51
69
|
/** Agent 类型(编辑器辅助,运行时不用) */
|
|
@@ -81,6 +99,10 @@ export interface FormBlock {
|
|
|
81
99
|
filePlaceholderIcon?: FormFilePlaceholderIcon;
|
|
82
100
|
fileMinCount?: number;
|
|
83
101
|
fileMaxCount?: number;
|
|
102
|
+
/** 上传素材的示例图,仅展示,不参与提交。 */
|
|
103
|
+
fileExampleUrls?: string[];
|
|
104
|
+
/** 制作模板的逐字段审核标准;由工作流读取,不能使用提交值覆盖。 */
|
|
105
|
+
fileReviewCriteria?: string;
|
|
84
106
|
defaultValue?: string | string[] | boolean;
|
|
85
107
|
/** 派生 JSON Schema 时写入 property.$comment */
|
|
86
108
|
jsonSchemaDescription?: string;
|
|
@@ -94,8 +116,14 @@ export interface FormBlock {
|
|
|
94
116
|
resumeMinCount?: number;
|
|
95
117
|
/** 简历多选:最多选中数量,配置值 ≥ 1 */
|
|
96
118
|
resumeMaxCount?: number;
|
|
119
|
+
/** 简历多选:一轮展示几张,默认 5。不是最多可选数,也不是接口 page_size */
|
|
120
|
+
/** content-only 仅选内容,提交 flow_unique_id;默认仍为简历与内容组合。 */
|
|
121
|
+
resumeDisplayMode?: 'profile-content' | 'content-only';
|
|
122
|
+
resumeDisplayCount?: number;
|
|
97
123
|
/** 简历多选:列表筛选条件(持久化进 block json) */
|
|
98
124
|
resumeFilter?: FormResumeFilter;
|
|
125
|
+
/** 简历多选:内容制作筛选(持久化进 block json) */
|
|
126
|
+
contentFilter?: FormContentFilter;
|
|
99
127
|
/** 案例单选:是否显示「换一批」 */
|
|
100
128
|
caseEnableRefresh?: boolean;
|
|
101
129
|
/** 案例多选:最少选中数量,配置值 ≥ 1 */
|
|
@@ -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,WAAW,GACX,cAAc,GACd,oBAAoB,GACpB,kBAAkB,CAAA;AAEtB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC1E;AAED,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAA;AAEpD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEpD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,WAAW,GACX,MAAM,GACN,UAAU,GACV,OAAO,GACP,QAAQ,GACR,QAAQ,GACR,aAAa,GACb,OAAO,GACP,UAAU,GACV,QAAQ,GACR,KAAK,GACL,KAAK,GACL,UAAU,GACV,MAAM,GACN,MAAM,GACN,oBAAoB,GACpB,kBAAkB;AACpB,4CAA4C;GAC1C,mBAAmB,GACnB,OAAO,GACP,OAAO,GACP,OAAO,GACP,KAAK,CAAA;AAET,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAE1E,MAAM,MAAM,uBAAuB,GAC/B,OAAO,GACP,OAAO,GACP,OAAO,GACP,MAAM,GACN,aAAa,GACb,UAAU,CAAA;AAEd,eAAO,MAAM,gBAAgB,EAAE,aAAa,EAM3C,CAAA;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,UAAU,CAAA;AAEtE,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -44,6 +44,18 @@ function makeDefinition(): Pick<FormSchema, 'title' | 'description' | 'blocks'>
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
describe('a2uiAdapter blocks <-> surface', () => {
|
|
47
|
+
it('preserves material upload examples and review criteria through normalization and A2UI', () => {
|
|
48
|
+
const definition = normalizeFormDefinition({ blocks: [{
|
|
49
|
+
id: 'material', type: 'file', name: 'image01', help: '上传人物',
|
|
50
|
+
fileExampleUrls: [' https://cdn.test/example.jpg ', 'javascript:alert(1)', 'https://cdn.test/example.jpg'],
|
|
51
|
+
fileReviewCriteria: ' 五官清晰 ',
|
|
52
|
+
}] })
|
|
53
|
+
const back = a2uiToBlocks(blocksToA2ui(definition))
|
|
54
|
+
expect(back.blocks[0]).toMatchObject({
|
|
55
|
+
name: 'image01', help: '上传人物', fileReviewCriteria: '五官清晰',
|
|
56
|
+
fileExampleUrls: ['https://cdn.test/example.jpg'],
|
|
57
|
+
})
|
|
58
|
+
})
|
|
47
59
|
it('blocksToA2ui builds a Column root referencing all components', () => {
|
|
48
60
|
const doc = blocksToA2ui(makeDefinition(), { styleId: 'theme-a' })
|
|
49
61
|
const root = doc.components.find((c) => c.id === 'root')
|
|
@@ -276,6 +276,9 @@ function blockToComponent(block: FormBlock): {
|
|
|
276
276
|
id,
|
|
277
277
|
component: 'FileUpload',
|
|
278
278
|
label,
|
|
279
|
+
help: block.help ?? '',
|
|
280
|
+
fileExampleUrls: block.fileExampleUrls ?? [],
|
|
281
|
+
fileReviewCriteria: block.fileReviewCriteria ?? '',
|
|
279
282
|
accept: (block.fileAcceptExtensions ?? []).join(','),
|
|
280
283
|
fileAcceptTypes: block.fileAcceptTypes ?? [],
|
|
281
284
|
filePlaceholderIcon: block.filePlaceholderIcon ?? 'document',
|
|
@@ -448,6 +451,11 @@ function componentToBlock(node: A2uiComponentNode): FormBlock | null {
|
|
|
448
451
|
case 'FileUpload': {
|
|
449
452
|
const accept = asLiteral(node.accept) ?? ''
|
|
450
453
|
const block: FormBlock = { ...base('file'), name }
|
|
454
|
+
block.help = asLiteral(node.help)
|
|
455
|
+
block.fileReviewCriteria = asLiteral(node.fileReviewCriteria)
|
|
456
|
+
if (Array.isArray(node.fileExampleUrls)) {
|
|
457
|
+
block.fileExampleUrls = node.fileExampleUrls.filter((url): url is string => typeof url === 'string')
|
|
458
|
+
}
|
|
451
459
|
if (accept) block.fileAcceptExtensions = accept.split(',').map((s) => s.trim()).filter(Boolean)
|
|
452
460
|
if (Array.isArray(node.fileAcceptTypes)) {
|
|
453
461
|
block.fileAcceptTypes = node.fileAcceptTypes.map(String)
|
|
@@ -188,6 +188,7 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
|
|
|
188
188
|
agentKind: 'management',
|
|
189
189
|
names: [],
|
|
190
190
|
agentUniqueIds: [],
|
|
191
|
+
tagUniqueIds: ['tag-hot'],
|
|
191
192
|
resumeUniqueIds: [],
|
|
192
193
|
pageSize: 20,
|
|
193
194
|
},
|
|
@@ -196,6 +197,13 @@ describe('normalizeFormDefinition preserves needLLM and editor fields', () => {
|
|
|
196
197
|
jsonSchema: {},
|
|
197
198
|
})
|
|
198
199
|
expect(def.blocks[0]?.resumeFilter?.agentKind).toBe('management')
|
|
200
|
+
expect(def.blocks[0]?.resumeFilter?.tagUniqueIds).toEqual(['tag-hot'])
|
|
201
|
+
expect(def.blocks[0]?.resumeDisplayCount).toBe(5)
|
|
202
|
+
expect(def.blocks[0]?.contentFilter).toMatchObject({
|
|
203
|
+
flowType: 'content-production',
|
|
204
|
+
tagUniqueIds: [],
|
|
205
|
+
pageSize: 20,
|
|
206
|
+
})
|
|
199
207
|
})
|
|
200
208
|
})
|
|
201
209
|
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
FormBlock,
|
|
3
3
|
FormBlockType,
|
|
4
4
|
FormCaseFilter,
|
|
5
|
+
FormContentFilter,
|
|
5
6
|
FormResumeFilter,
|
|
6
7
|
FormJsonSchema,
|
|
7
8
|
FormSchema,
|
|
@@ -136,6 +137,9 @@ function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFil
|
|
|
136
137
|
agentUniqueIds: Array.isArray(raw?.agentUniqueIds)
|
|
137
138
|
? raw!.agentUniqueIds.map(String).filter(Boolean)
|
|
138
139
|
: [],
|
|
140
|
+
tagUniqueIds: Array.isArray(raw?.tagUniqueIds)
|
|
141
|
+
? raw!.tagUniqueIds.map(String).filter(Boolean)
|
|
142
|
+
: [],
|
|
139
143
|
resumeUniqueIds: Array.isArray(raw?.resumeUniqueIds)
|
|
140
144
|
? raw!.resumeUniqueIds.map(String).filter(Boolean)
|
|
141
145
|
: [],
|
|
@@ -143,6 +147,35 @@ function normalizeResumeFilter(raw: FormResumeFilter | undefined): FormResumeFil
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
export const DEFAULT_CONTENT_FLOW_TYPE = 'content-production'
|
|
151
|
+
export const DEFAULT_RESUME_DISPLAY_COUNT = 5
|
|
152
|
+
|
|
153
|
+
export function normalizeResumeDisplayCount(raw: unknown): number {
|
|
154
|
+
const num = typeof raw === 'number' ? raw : Number(raw)
|
|
155
|
+
if (!Number.isFinite(num) || num < 1) return DEFAULT_RESUME_DISPLAY_COUNT
|
|
156
|
+
return Math.min(100, Math.floor(num))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeContentFilter(raw: FormContentFilter | undefined): FormContentFilter {
|
|
160
|
+
const pageSizeRaw = typeof raw?.pageSize === 'number' ? raw.pageSize : Number(raw?.pageSize)
|
|
161
|
+
const pageSize =
|
|
162
|
+
Number.isFinite(pageSizeRaw) && pageSizeRaw >= 1 && pageSizeRaw <= 100
|
|
163
|
+
? Math.floor(pageSizeRaw)
|
|
164
|
+
: 20
|
|
165
|
+
const flowType = raw?.flowType?.trim() || DEFAULT_CONTENT_FLOW_TYPE
|
|
166
|
+
const category = raw?.category?.trim() || undefined
|
|
167
|
+
const tagMatch = raw?.tagMatch === 'any' ? 'any' : 'all'
|
|
168
|
+
return {
|
|
169
|
+
flowType,
|
|
170
|
+
...(category ? { category } : {}),
|
|
171
|
+
tagUniqueIds: Array.isArray(raw?.tagUniqueIds)
|
|
172
|
+
? raw.tagUniqueIds.map(String).filter(Boolean)
|
|
173
|
+
: [],
|
|
174
|
+
tagMatch,
|
|
175
|
+
pageSize,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
146
179
|
function migrateLegacyBlock(raw: FormBlock): FormBlock {
|
|
147
180
|
if (raw.type !== 'case_singleselect') return raw
|
|
148
181
|
const defaultValue =
|
|
@@ -180,6 +213,12 @@ function normalizeBlock(raw: FormBlock): FormBlock {
|
|
|
180
213
|
filePlaceholderIcon: normalizeFilePlaceholderIcon(migrated.filePlaceholderIcon),
|
|
181
214
|
fileMinCount: type === 'file' ? normalizeFileUploadCount(migrated.fileMinCount, 1) : undefined,
|
|
182
215
|
fileMaxCount: type === 'file' ? normalizeFileUploadCount(migrated.fileMaxCount, 1) : undefined,
|
|
216
|
+
fileExampleUrls: type === 'file' && Array.isArray(migrated.fileExampleUrls)
|
|
217
|
+
? [...new Set(migrated.fileExampleUrls.filter((url): url is string =>
|
|
218
|
+
typeof url === 'string' && /^https?:\/\//i.test(url.trim()),
|
|
219
|
+
).map((url) => url.trim()))]
|
|
220
|
+
: undefined,
|
|
221
|
+
fileReviewCriteria: type === 'file' ? migrated.fileReviewCriteria?.trim() ?? '' : undefined,
|
|
183
222
|
defaultValue: normalizeDefaultValue(type, migrated.defaultValue),
|
|
184
223
|
jsonSchemaDescription: migrated.jsonSchemaDescription?.trim() ?? '',
|
|
185
224
|
hidden: type === 'text' ? migrated.hidden === true : undefined,
|
|
@@ -204,8 +243,17 @@ function normalizeBlock(raw: FormBlock): FormBlock {
|
|
|
204
243
|
type === 'resume_multiselect' && migrated.resumeMaxCount != null
|
|
205
244
|
? normalizeMultiselectCount(migrated.resumeMaxCount, 1)
|
|
206
245
|
: undefined,
|
|
246
|
+
resumeDisplayMode: type === 'resume_multiselect'
|
|
247
|
+
? (migrated.resumeDisplayMode === 'content-only' ? 'content-only' : 'profile-content')
|
|
248
|
+
: undefined,
|
|
249
|
+
resumeDisplayCount:
|
|
250
|
+
type === 'resume_multiselect'
|
|
251
|
+
? normalizeResumeDisplayCount(migrated.resumeDisplayCount)
|
|
252
|
+
: undefined,
|
|
207
253
|
resumeFilter:
|
|
208
254
|
type === 'resume_multiselect' ? normalizeResumeFilter(migrated.resumeFilter) : undefined,
|
|
255
|
+
contentFilter:
|
|
256
|
+
type === 'resume_multiselect' ? normalizeContentFilter(migrated.contentFilter) : undefined,
|
|
209
257
|
caseEnableRefresh:
|
|
210
258
|
type === 'case_multiselect' ? migrated.caseEnableRefresh !== false : undefined,
|
|
211
259
|
caseMinCount:
|
|
@@ -460,9 +508,16 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
|
|
|
460
508
|
block.resumeEnableRefresh = true
|
|
461
509
|
block.resumeMinCount = 1
|
|
462
510
|
block.resumeMaxCount = 1
|
|
511
|
+
block.resumeDisplayCount = DEFAULT_RESUME_DISPLAY_COUNT
|
|
512
|
+
block.contentFilter = {
|
|
513
|
+
flowType: DEFAULT_CONTENT_FLOW_TYPE,
|
|
514
|
+
tagUniqueIds: [],
|
|
515
|
+
pageSize: 20,
|
|
516
|
+
}
|
|
463
517
|
block.resumeFilter = {
|
|
464
518
|
names: [],
|
|
465
519
|
agentUniqueIds: [],
|
|
520
|
+
tagUniqueIds: [],
|
|
466
521
|
resumeUniqueIds: [],
|
|
467
522
|
pageSize: 20,
|
|
468
523
|
}
|
|
@@ -24,9 +24,22 @@ export function resumeMultiselectToComponent(
|
|
|
24
24
|
placeholder: block.placeholder ?? '',
|
|
25
25
|
help: block.help ?? '',
|
|
26
26
|
enableRefresh: block.resumeEnableRefresh !== false,
|
|
27
|
+
resumeDisplayMode: block.resumeDisplayMode ?? 'profile-content',
|
|
28
|
+
resumeDisplayCount: block.resumeDisplayCount ?? 5,
|
|
29
|
+
contentFilter: {
|
|
30
|
+
flowType: block.contentFilter?.flowType?.trim() || 'content-production',
|
|
31
|
+
...(block.contentFilter?.category?.trim()
|
|
32
|
+
? { category: block.contentFilter.category.trim() }
|
|
33
|
+
: {}),
|
|
34
|
+
tagUniqueIds: block.contentFilter?.tagUniqueIds ?? [],
|
|
35
|
+
tagMatch: block.contentFilter?.tagMatch === 'any' ? 'any' : 'all',
|
|
36
|
+
pageSize: block.contentFilter?.pageSize ?? 20,
|
|
37
|
+
},
|
|
27
38
|
resumeFilter: {
|
|
28
39
|
names: block.resumeFilter?.names ?? [],
|
|
40
|
+
...(block.resumeFilter?.agentKind ? { agentKind: block.resumeFilter.agentKind } : {}),
|
|
29
41
|
agentUniqueIds: block.resumeFilter?.agentUniqueIds ?? [],
|
|
42
|
+
tagUniqueIds: block.resumeFilter?.tagUniqueIds ?? [],
|
|
30
43
|
resumeUniqueIds: block.resumeFilter?.resumeUniqueIds ?? [],
|
|
31
44
|
pageSize: block.resumeFilter?.pageSize ?? 20,
|
|
32
45
|
},
|
|
@@ -46,6 +59,7 @@ export function resumeMultiselectFromComponent(
|
|
|
46
59
|
...base('resume_multiselect'),
|
|
47
60
|
name,
|
|
48
61
|
resumeEnableRefresh: node.enableRefresh !== false,
|
|
62
|
+
resumeDisplayMode: node.resumeDisplayMode === 'content-only' ? 'content-only' : 'profile-content',
|
|
49
63
|
}
|
|
50
64
|
const placeholder = helpers.asLiteral(node.placeholder)
|
|
51
65
|
if (placeholder) block.placeholder = placeholder
|
|
@@ -53,13 +67,30 @@ export function resumeMultiselectFromComponent(
|
|
|
53
67
|
if (help) block.help = help
|
|
54
68
|
if (node.resumeFilter && typeof node.resumeFilter === 'object') {
|
|
55
69
|
const rf = node.resumeFilter as Record<string, unknown>
|
|
70
|
+
const agentKind = helpers.asLiteral(rf.agentKind)?.trim()
|
|
56
71
|
block.resumeFilter = {
|
|
57
72
|
names: Array.isArray(rf.names) ? rf.names.map(String) : [],
|
|
73
|
+
...(agentKind ? { agentKind } : {}),
|
|
58
74
|
agentUniqueIds: Array.isArray(rf.agentUniqueIds) ? rf.agentUniqueIds.map(String) : [],
|
|
75
|
+
tagUniqueIds: Array.isArray(rf.tagUniqueIds) ? rf.tagUniqueIds.map(String).filter(Boolean) : [],
|
|
59
76
|
resumeUniqueIds: Array.isArray(rf.resumeUniqueIds) ? rf.resumeUniqueIds.map(String) : [],
|
|
60
77
|
pageSize: typeof rf.pageSize === 'number' ? rf.pageSize : 20,
|
|
61
78
|
}
|
|
62
79
|
}
|
|
80
|
+
const displayCount = readOptionalCount(node.resumeDisplayCount)
|
|
81
|
+
if (displayCount !== undefined) block.resumeDisplayCount = displayCount
|
|
82
|
+
if (node.contentFilter && typeof node.contentFilter === 'object') {
|
|
83
|
+
const cf = node.contentFilter as Record<string, unknown>
|
|
84
|
+
const flowType = helpers.asLiteral(cf.flowType)?.trim()
|
|
85
|
+
const category = helpers.asLiteral(cf.category)?.trim()
|
|
86
|
+
block.contentFilter = {
|
|
87
|
+
flowType: flowType || 'content-production',
|
|
88
|
+
...(category ? { category } : {}),
|
|
89
|
+
tagUniqueIds: Array.isArray(cf.tagUniqueIds) ? cf.tagUniqueIds.map(String).filter(Boolean) : [],
|
|
90
|
+
tagMatch: cf.tagMatch === 'any' ? 'any' : 'all',
|
|
91
|
+
pageSize: typeof cf.pageSize === 'number' ? cf.pageSize : 20,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
63
94
|
const minCount = readOptionalCount(node.minCount)
|
|
64
95
|
if (minCount !== undefined) block.resumeMinCount = minCount
|
|
65
96
|
const maxCount = readOptionalCount(node.maxCount)
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DatePicker,
|
|
12
12
|
Input,
|
|
13
13
|
InputNumber,
|
|
14
|
+
Image,
|
|
14
15
|
Radio,
|
|
15
16
|
Select,
|
|
16
17
|
Switch,
|
|
@@ -547,6 +548,10 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
547
548
|
const props = context.componentModel.properties as Record<string, unknown>
|
|
548
549
|
const label = readString(props.label)
|
|
549
550
|
const accept = readString(props.accept)
|
|
551
|
+
const help = readString(props.help)
|
|
552
|
+
const reviewCriteria = readString(props.fileReviewCriteria)
|
|
553
|
+
const exampleUrls = asStringArray(props.fileExampleUrls)
|
|
554
|
+
.filter((url) => /^https?:\/\//i.test(url))
|
|
550
555
|
const fileAcceptTypes = Array.isArray(props.fileAcceptTypes)
|
|
551
556
|
? props.fileAcceptTypes.map(String)
|
|
552
557
|
: undefined
|
|
@@ -704,6 +709,20 @@ function FileUploadPreview({ context }: { context: ComponentContext }) {
|
|
|
704
709
|
return (
|
|
705
710
|
<div className="form-block-preview" data-skopon-field={fieldName || undefined}>
|
|
706
711
|
{label ? <div className="form-block-preview-label">{label}</div> : null}
|
|
712
|
+
{help ? <Typography.Paragraph type="secondary">{help}</Typography.Paragraph> : null}
|
|
713
|
+
{reviewCriteria ? <Typography.Paragraph type="secondary">素材要求:{reviewCriteria}</Typography.Paragraph> : null}
|
|
714
|
+
{exampleUrls.length ? (
|
|
715
|
+
<div className="form-file-examples">
|
|
716
|
+
<Typography.Text type="secondary">参考示例(不作为上传素材提交)</Typography.Text>
|
|
717
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', margin: '8px 0' }}>
|
|
718
|
+
<Image.PreviewGroup>
|
|
719
|
+
{exampleUrls.map((url, index) => (
|
|
720
|
+
<Image key={url} src={url} alt={`${label || '素材'}示例 ${index + 1}`} width={120} height={120} style={{ objectFit: 'contain' }} />
|
|
721
|
+
))}
|
|
722
|
+
</Image.PreviewGroup>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
) : null}
|
|
707
726
|
<Upload.Dragger
|
|
708
727
|
disabled={!interactive}
|
|
709
728
|
openFileDialogOnClick={!atMaxLimit}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import type { FormContentFilter } from '../types/index'
|
|
3
|
+
|
|
4
|
+
export interface ContentSearchItem {
|
|
5
|
+
flowUniqueId: string
|
|
6
|
+
flowId?: number
|
|
7
|
+
name: string
|
|
8
|
+
description?: string
|
|
9
|
+
/** 内容方案标签的展示文案 */
|
|
10
|
+
tagLabels?: string[]
|
|
11
|
+
/** 最新案例名;没有案例时用流程名 */
|
|
12
|
+
title: string
|
|
13
|
+
platform?: string | null
|
|
14
|
+
coverUrl?: string | null
|
|
15
|
+
/** 可播放的视频地址;没有视频时为空 */
|
|
16
|
+
mediaUrl?: string | null
|
|
17
|
+
mediaKind?: 'video' | 'image' | null
|
|
18
|
+
link?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ContentSearchParams {
|
|
22
|
+
filter: FormContentFilter
|
|
23
|
+
page: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContentSearchResult {
|
|
27
|
+
list: ContentSearchItem[]
|
|
28
|
+
total: number
|
|
29
|
+
page: number
|
|
30
|
+
pageSize: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ContentSearchFn = (params: ContentSearchParams) => Promise<ContentSearchResult>
|
|
34
|
+
|
|
35
|
+
const ContentSearchContext = createContext<ContentSearchFn | null>(null)
|
|
36
|
+
|
|
37
|
+
export function ContentSearchProvider({
|
|
38
|
+
contentSearch,
|
|
39
|
+
children,
|
|
40
|
+
}: {
|
|
41
|
+
contentSearch: ContentSearchFn | null | undefined
|
|
42
|
+
children: ReactNode
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<ContentSearchContext.Provider value={contentSearch ?? null}>
|
|
46
|
+
{children}
|
|
47
|
+
</ContentSearchContext.Provider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useContentSearch(): ContentSearchFn | null {
|
|
52
|
+
return useContext(ContentSearchContext)
|
|
53
|
+
}
|
|
@@ -56,6 +56,9 @@ function renderFileSurface(options: {
|
|
|
56
56
|
defaultValue?: string | string[]
|
|
57
57
|
legacyRawValue?: string
|
|
58
58
|
onUploadingChange?: (uploading: boolean) => void
|
|
59
|
+
fileExampleUrls?: string[]
|
|
60
|
+
fileReviewCriteria?: string
|
|
61
|
+
help?: string
|
|
59
62
|
} = {}) {
|
|
60
63
|
const doc = blocksToA2ui(
|
|
61
64
|
{
|
|
@@ -70,6 +73,9 @@ function renderFileSurface(options: {
|
|
|
70
73
|
fileMinCount: options.minCount ?? 0,
|
|
71
74
|
fileMaxCount: options.maxCount ?? 1,
|
|
72
75
|
defaultValue: options.defaultValue,
|
|
76
|
+
fileExampleUrls: options.fileExampleUrls,
|
|
77
|
+
fileReviewCriteria: options.fileReviewCriteria,
|
|
78
|
+
help: options.help,
|
|
73
79
|
},
|
|
74
80
|
],
|
|
75
81
|
},
|
|
@@ -94,6 +100,17 @@ function renderFileSurface(options: {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
describe('FileUploadPreview', () => {
|
|
103
|
+
it('shows descriptions, criteria and examples without submitting example URLs', () => {
|
|
104
|
+
const { getByText, getByAltText, ref } = renderFileSurface({
|
|
105
|
+
help: '请上传一张人物图',
|
|
106
|
+
fileReviewCriteria: '五官清晰',
|
|
107
|
+
fileExampleUrls: ['https://cdn.example.com/example.jpg'],
|
|
108
|
+
})
|
|
109
|
+
expect(getByText('请上传一张人物图')).toBeTruthy()
|
|
110
|
+
expect(getByText('素材要求:五官清晰')).toBeTruthy()
|
|
111
|
+
expect(getByAltText('附件示例 1').getAttribute('src')).toBe('https://cdn.example.com/example.jpg')
|
|
112
|
+
expect(ref.current?.getValues()).toEqual({ attachments: [] })
|
|
113
|
+
})
|
|
97
114
|
it('stores successful CDN URLs in user selection order', async () => {
|
|
98
115
|
const first = deferred<{ url: string }>()
|
|
99
116
|
const second = deferred<{ url: string }>()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { advanceCursor, nextPageForWindow, windowIndexes } from './pairResumeContent'
|
|
3
|
+
|
|
4
|
+
describe('resume and content windows', () => {
|
|
5
|
+
it('pairs independent cursors and wraps only the side that is used up', () => {
|
|
6
|
+
let resumeCursor = 0
|
|
7
|
+
let contentCursor = 0
|
|
8
|
+
const displayCount = 3
|
|
9
|
+
const rounds = [
|
|
10
|
+
[[1, 2, 3], [1, 2, 3]],
|
|
11
|
+
[[4, 5, 6], [4, 5, 6]],
|
|
12
|
+
[[7, 8, 9], [7, 8, 9]],
|
|
13
|
+
[[10, 1, 2], [10, 11, 12]],
|
|
14
|
+
]
|
|
15
|
+
for (const [resumes, contents] of rounds) {
|
|
16
|
+
expect(windowIndexes(resumeCursor, displayCount, 10).map((index) => index + 1)).toEqual(resumes)
|
|
17
|
+
expect(windowIndexes(contentCursor, displayCount, 100).map((index) => index + 1)).toEqual(contents)
|
|
18
|
+
resumeCursor = advanceCursor(resumeCursor, displayCount, 10)
|
|
19
|
+
contentCursor = advanceCursor(contentCursor, displayCount, 100)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('fetches the next page only when a window index is past the cache and more pages exist', () => {
|
|
24
|
+
const cached = { itemCount: 20, total: 100, fetchedPage: 1, pageSize: 20 }
|
|
25
|
+
expect(nextPageForWindow(cached, windowIndexes(0, 3, 100))).toBeNull()
|
|
26
|
+
expect(nextPageForWindow(cached, windowIndexes(18, 3, 100))).toBe(2)
|
|
27
|
+
expect(
|
|
28
|
+
nextPageForWindow(
|
|
29
|
+
{ itemCount: 10, total: 10, fetchedPage: 1, pageSize: 20 },
|
|
30
|
+
windowIndexes(9, 3, 10),
|
|
31
|
+
),
|
|
32
|
+
).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const DEFAULT_RESUME_DISPLAY_COUNT = 5
|
|
2
|
+
|
|
3
|
+
export interface PageCacheSnapshot {
|
|
4
|
+
itemCount: number
|
|
5
|
+
total: number
|
|
6
|
+
fetchedPage: number
|
|
7
|
+
pageSize: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeDisplayCount(raw: unknown, fallback = DEFAULT_RESUME_DISPLAY_COUNT): number {
|
|
11
|
+
const num = typeof raw === 'number' ? raw : Number(raw)
|
|
12
|
+
if (!Number.isFinite(num) || num < 1) return fallback
|
|
13
|
+
return Math.min(100, Math.floor(num))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pairKey(resumeUniqueId: string, flowUniqueId: string): string {
|
|
17
|
+
return `${resumeUniqueId}|${flowUniqueId}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hasMorePages(cache: PageCacheSnapshot): boolean {
|
|
21
|
+
if (cache.total <= 0 || cache.pageSize <= 0) return false
|
|
22
|
+
return cache.fetchedPage * cache.pageSize < cache.total
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function advanceCursor(cursor: number, displayCount: number, total: number): number {
|
|
26
|
+
if (total <= 0) return 0
|
|
27
|
+
const step = Math.max(1, displayCount)
|
|
28
|
+
return (((cursor % total) + total) % total + step) % total
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 一轮要展示的下标。总数不够展示条数时只取总数那么多。 */
|
|
32
|
+
export function windowIndexes(cursor: number, displayCount: number, total: number): number[] {
|
|
33
|
+
if (total <= 0 || displayCount <= 0) return []
|
|
34
|
+
const count = Math.min(displayCount, total)
|
|
35
|
+
const start = ((cursor % total) + total) % total
|
|
36
|
+
return Array.from({ length: count }, (_, index) => (start + index) % total)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 窗口里有下标还没进缓存,并且接口还有下一页时,返回要请求的页码。
|
|
41
|
+
* 已经拿完则返回 null,调用方用缓存开头接上。
|
|
42
|
+
*/
|
|
43
|
+
export function nextPageForWindow(cache: PageCacheSnapshot, indexes: number[]): number | null {
|
|
44
|
+
if (indexes.length === 0) return null
|
|
45
|
+
if (!indexes.some((index) => index >= cache.itemCount)) return null
|
|
46
|
+
if (!hasMorePages(cache)) return null
|
|
47
|
+
return cache.fetchedPage + 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function takeWindow<T>(items: T[], cursor: number, displayCount: number, total: number): T[] {
|
|
51
|
+
const indexes = windowIndexes(cursor, displayCount, total)
|
|
52
|
+
if (indexes.length === 0) return []
|
|
53
|
+
if (indexes.some((index) => index >= items.length)) return []
|
|
54
|
+
return indexes.map((index) => items[index])
|
|
55
|
+
}
|