@skopon-cool/form-sdk 0.1.7 → 0.1.9

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.
@@ -72,10 +72,18 @@ export interface FormBlock {
72
72
  defaultValue?: string | string[] | boolean;
73
73
  /** 简历多选:是否显示「换一批」 */
74
74
  resumeEnableRefresh?: boolean;
75
+ /** 简历多选:最少选中数量,配置值 ≥ 1 */
76
+ resumeMinCount?: number;
77
+ /** 简历多选:最多选中数量,配置值 ≥ 1 */
78
+ resumeMaxCount?: number;
75
79
  /** 简历多选:列表筛选条件(持久化进 block json) */
76
80
  resumeFilter?: FormResumeFilter;
77
81
  /** 案例单选:是否显示「换一批」 */
78
82
  caseEnableRefresh?: boolean;
83
+ /** 案例多选:最少选中数量,配置值 ≥ 1 */
84
+ caseMinCount?: number;
85
+ /** 案例多选:最多选中数量,配置值 ≥ 1 */
86
+ caseMaxCount?: number;
79
87
  /** 案例单选:列表筛选条件(持久化进 block json) */
80
88
  caseFilter?: FormCaseFilter;
81
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.7",
6
+ "version": "0.1.9",
7
7
  "description": "Skopon form rendering SDK (A2UI + form_definition) with submit helpers",
8
8
  "type": "module",
9
9
  "sideEffects": [
@@ -27,7 +27,8 @@
27
27
  "scripts": {
28
28
  "build": "vite build && tsc -p tsconfig.build.json",
29
29
  "test": "vitest run",
30
- "test:watch": "vitest"
30
+ "test:watch": "vitest",
31
+ "prepublishOnly": "npm run build && npm test"
31
32
  },
32
33
  "dependencies": {
33
34
  "dayjs": "^1.11.13",
@@ -23,6 +23,12 @@ function normalizeFileUploadCount(raw: unknown, fallback = 1): number {
23
23
  return Math.max(0, Math.floor(num))
24
24
  }
25
25
 
26
+ function normalizeMultiselectCount(raw: unknown, fallback = 1): number {
27
+ const num = typeof raw === 'number' ? raw : Number(raw)
28
+ if (!Number.isFinite(num)) return fallback
29
+ return Math.max(1, Math.floor(num))
30
+ }
31
+
26
32
  function normalizeDefaultValue(
27
33
  type: FormBlockType,
28
34
  raw: FormBlock['defaultValue'],
@@ -129,10 +135,26 @@ function normalizeBlock(raw: FormBlock): FormBlock {
129
135
  : undefined,
130
136
  resumeEnableRefresh:
131
137
  type === 'resume_multiselect' ? migrated.resumeEnableRefresh !== false : undefined,
138
+ resumeMinCount:
139
+ type === 'resume_multiselect' && migrated.resumeMinCount != null
140
+ ? normalizeMultiselectCount(migrated.resumeMinCount, 1)
141
+ : undefined,
142
+ resumeMaxCount:
143
+ type === 'resume_multiselect' && migrated.resumeMaxCount != null
144
+ ? normalizeMultiselectCount(migrated.resumeMaxCount, 1)
145
+ : undefined,
132
146
  resumeFilter:
133
147
  type === 'resume_multiselect' ? normalizeResumeFilter(migrated.resumeFilter) : undefined,
134
148
  caseEnableRefresh:
135
149
  type === 'case_multiselect' ? migrated.caseEnableRefresh !== false : undefined,
150
+ caseMinCount:
151
+ type === 'case_multiselect' && migrated.caseMinCount != null
152
+ ? normalizeMultiselectCount(migrated.caseMinCount, 1)
153
+ : undefined,
154
+ caseMaxCount:
155
+ type === 'case_multiselect' && migrated.caseMaxCount != null
156
+ ? normalizeMultiselectCount(migrated.caseMaxCount, 1)
157
+ : undefined,
136
158
  caseFilter:
137
159
  type === 'case_multiselect' ? normalizeCaseFilter(migrated.caseFilter) : undefined,
138
160
  }
@@ -203,6 +225,8 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
203
225
 
204
226
  if (type === 'resume_multiselect') {
205
227
  block.resumeEnableRefresh = true
228
+ block.resumeMinCount = 1
229
+ block.resumeMaxCount = 1
206
230
  block.resumeFilter = {
207
231
  names: [],
208
232
  agentUniqueIds: [],
@@ -214,6 +238,8 @@ export function createEmptyBlock(type: FormBlockType): FormBlock {
214
238
 
215
239
  if (type === 'case_multiselect') {
216
240
  block.caseEnableRefresh = true
241
+ block.caseMinCount = 1
242
+ block.caseMaxCount = 1
217
243
  block.caseFilter = { flowUniqueIds: [], pageSize: 20 }
218
244
  block.defaultValue = []
219
245
  }
@@ -39,6 +39,13 @@ function readCaseFilterFromNode(cf: Record<string, unknown>) {
39
39
  }
40
40
  }
41
41
 
42
+ function readOptionalCount(raw: unknown): number | undefined {
43
+ if (raw === undefined || raw === null) return undefined
44
+ const num = typeof raw === 'number' ? raw : Number(raw)
45
+ if (!Number.isFinite(num) || num < 1) return undefined
46
+ return Math.floor(num)
47
+ }
48
+
42
49
  export function caseMultiselectToComponent(
43
50
  block: FormBlock,
44
51
  ctx: BlockAdapterContext,
@@ -59,6 +66,8 @@ export function caseMultiselectToComponent(
59
66
  flowUniqueIds: block.caseFilter?.flowUniqueIds ?? [],
60
67
  pageSize: block.caseFilter?.pageSize ?? 20,
61
68
  },
69
+ ...(block.caseMinCount !== undefined ? { minCount: block.caseMinCount } : {}),
70
+ ...(block.caseMaxCount !== undefined ? { maxCount: block.caseMaxCount } : {}),
62
71
  value: { path },
63
72
  })
64
73
  }
@@ -84,6 +93,10 @@ export function caseMultiselectFromComponent(
84
93
  if (node.caseFilter && typeof node.caseFilter === 'object') {
85
94
  block.caseFilter = readCaseFilterFromNode(node.caseFilter as Record<string, unknown>)
86
95
  }
96
+ const minCount = readOptionalCount(node.minCount)
97
+ if (minCount !== undefined) block.caseMinCount = minCount
98
+ const maxCount = readOptionalCount(node.maxCount)
99
+ if (maxCount !== undefined) block.caseMaxCount = maxCount
87
100
  return block
88
101
  }
89
102
 
@@ -5,6 +5,13 @@ function asLiteral(value: unknown): string | undefined {
5
5
  return typeof value === 'string' ? value : undefined
6
6
  }
7
7
 
8
+ function readOptionalCount(raw: unknown): number | undefined {
9
+ if (raw === undefined || raw === null) return undefined
10
+ const num = typeof raw === 'number' ? raw : Number(raw)
11
+ if (!Number.isFinite(num) || num < 1) return undefined
12
+ return Math.floor(num)
13
+ }
14
+
8
15
  export function resumeMultiselectToComponent(
9
16
  block: FormBlock,
10
17
  ctx: BlockAdapterContext,
@@ -23,6 +30,8 @@ export function resumeMultiselectToComponent(
23
30
  resumeUniqueIds: block.resumeFilter?.resumeUniqueIds ?? [],
24
31
  pageSize: block.resumeFilter?.pageSize ?? 20,
25
32
  },
33
+ ...(block.resumeMinCount !== undefined ? { minCount: block.resumeMinCount } : {}),
34
+ ...(block.resumeMaxCount !== undefined ? { maxCount: block.resumeMaxCount } : {}),
26
35
  value: { path },
27
36
  })
28
37
  }
@@ -51,6 +60,10 @@ export function resumeMultiselectFromComponent(
51
60
  pageSize: typeof rf.pageSize === 'number' ? rf.pageSize : 20,
52
61
  }
53
62
  }
63
+ const minCount = readOptionalCount(node.minCount)
64
+ if (minCount !== undefined) block.resumeMinCount = minCount
65
+ const maxCount = readOptionalCount(node.maxCount)
66
+ if (maxCount !== undefined) block.resumeMaxCount = maxCount
54
67
  return block
55
68
  }
56
69
 
@@ -6,7 +6,7 @@ import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
6
6
  import { z } from 'zod'
7
7
  import type { FormCaseFilter } from '../types/index'
8
8
  import { useA2uiPreviewMode } from './a2uiPreviewContext'
9
- import { asStringArray, useSkoponBoundField } from './useSkoponBoundField'
9
+ import { asStringArray, clampStringArraySelection, isCardSelectionDisabled, readOptionalSelectCount, toggleStringArraySelection, useSkoponBoundField } from './useSkoponBoundField'
10
10
  import { type CaseSearchItem, useCaseSearch } from './caseSearchContext'
11
11
 
12
12
  function readString(value: unknown): string {
@@ -64,6 +64,8 @@ const SkoponCaseSelectApi = {
64
64
  help: z.any().optional(),
65
65
  enableRefresh: z.any().optional(),
66
66
  caseFilter: z.any().optional(),
67
+ minCount: z.any().optional(),
68
+ maxCount: z.any().optional(),
67
69
  value: z.any().optional(),
68
70
  })
69
71
  .passthrough(),
@@ -115,6 +117,8 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
115
117
  const placeholder = readString(props.placeholder)
116
118
  const help = readString(props.help)
117
119
  const enableRefresh = props.enableRefresh !== false
120
+ const minCount = readOptionalSelectCount(props.minCount)
121
+ const maxCount = readOptionalSelectCount(props.maxCount)
118
122
  const caseFilter = useMemo(() => readCaseFilter(props.caseFilter), [props.caseFilter])
119
123
  const filterSig = filterKey(caseFilter)
120
124
 
@@ -128,6 +132,11 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
128
132
 
129
133
  const selectedIds = asStringArray(value)
130
134
 
135
+ useEffect(() => {
136
+ const clamped = clampStringArraySelection(selectedIds, maxCount)
137
+ if (clamped !== null) setValue(clamped)
138
+ }, [maxCount, selectedIds, setValue])
139
+
131
140
  const emptyHint = useMemo(() => {
132
141
  if (!caseSearch) return '未配置案例搜索服务'
133
142
  if (!isCaseFilterReady(caseFilter)) return '未配置案例列表筛选条件'
@@ -173,9 +182,13 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
173
182
 
174
183
  const toggleSelect = (caseUniqueId: string) => {
175
184
  if (!interactive) return
176
- const next = selectedIds.includes(caseUniqueId)
177
- ? selectedIds.filter((id) => id !== caseUniqueId)
178
- : [...selectedIds, caseUniqueId]
185
+ const next = toggleStringArraySelection(
186
+ selectedIds,
187
+ caseUniqueId,
188
+ minCount,
189
+ maxCount,
190
+ )
191
+ if (next === null) return
179
192
  setValue(next)
180
193
  }
181
194
 
@@ -205,7 +218,13 @@ function SkoponCaseSelectPreview({ context }: { context: ComponentContext }) {
205
218
  key={item.caseUniqueId}
206
219
  item={item}
207
220
  selected={selectedIds.includes(item.caseUniqueId)}
208
- disabled={!interactive}
221
+ disabled={isCardSelectionDisabled(
222
+ selectedIds,
223
+ item.caseUniqueId,
224
+ interactive,
225
+ minCount,
226
+ maxCount,
227
+ )}
209
228
  onToggle={() => toggleSelect(item.caseUniqueId)}
210
229
  />
211
230
  ))}
@@ -6,7 +6,7 @@ import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'
6
6
  import { z } from 'zod'
7
7
  import type { FormResumeFilter } from '../types/index'
8
8
  import { useA2uiPreviewMode } from './a2uiPreviewContext'
9
- import { asStringArray, useSkoponBoundField } from './useSkoponBoundField'
9
+ import { asStringArray, clampStringArraySelection, isCardSelectionDisabled, readOptionalSelectCount, toggleStringArraySelection, useSkoponBoundField } from './useSkoponBoundField'
10
10
  import {
11
11
  type ResumeSearchItem,
12
12
  type ResumeSearchWorkItem,
@@ -50,6 +50,8 @@ const SkoponResumeSelectApi = {
50
50
  help: z.any().optional(),
51
51
  enableRefresh: z.any().optional(),
52
52
  resumeFilter: z.any().optional(),
53
+ minCount: z.any().optional(),
54
+ maxCount: z.any().optional(),
53
55
  value: z.any().optional(),
54
56
  })
55
57
  .passthrough(),
@@ -168,6 +170,8 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
168
170
  const placeholder = readString(props.placeholder)
169
171
  const help = readString(props.help)
170
172
  const enableRefresh = props.enableRefresh !== false
173
+ const minCount = readOptionalSelectCount(props.minCount)
174
+ const maxCount = readOptionalSelectCount(props.maxCount)
171
175
  const resumeFilter = useMemo(() => readResumeFilter(props.resumeFilter), [props.resumeFilter])
172
176
  const filterSig = filterKey(resumeFilter)
173
177
 
@@ -181,6 +185,11 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
181
185
 
182
186
  const selectedIds = asStringArray(value)
183
187
 
188
+ useEffect(() => {
189
+ const clamped = clampStringArraySelection(selectedIds, maxCount)
190
+ if (clamped !== null) setValue(clamped)
191
+ }, [maxCount, selectedIds, setValue])
192
+
184
193
  const fetchPage = useCallback(
185
194
  async (targetPage: number) => {
186
195
  const filter = readResumeFilter(JSON.parse(filterSig) as FormResumeFilter)
@@ -219,9 +228,13 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
219
228
 
220
229
  const toggleSelect = (resumeUniqueId: string) => {
221
230
  if (!interactive) return
222
- const next = selectedIds.includes(resumeUniqueId)
223
- ? selectedIds.filter((id) => id !== resumeUniqueId)
224
- : [...selectedIds, resumeUniqueId]
231
+ const next = toggleStringArraySelection(
232
+ selectedIds,
233
+ resumeUniqueId,
234
+ minCount,
235
+ maxCount,
236
+ )
237
+ if (next === null) return
225
238
  setValue(next)
226
239
  }
227
240
 
@@ -258,7 +271,13 @@ function SkoponResumeSelectPreview({ context }: { context: ComponentContext }) {
258
271
  key={item.resumeUniqueId}
259
272
  item={item}
260
273
  selected={selectedIds.includes(item.resumeUniqueId)}
261
- disabled={!interactive}
274
+ disabled={isCardSelectionDisabled(
275
+ selectedIds,
276
+ item.resumeUniqueId,
277
+ interactive,
278
+ minCount,
279
+ maxCount,
280
+ )}
262
281
  onToggle={() => toggleSelect(item.resumeUniqueId)}
263
282
  />
264
283
  ))}
@@ -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, readBoundFieldValue } from './useSkoponBoundField'
6
+ import { asStringArray, clampStringArraySelection, readBoundFieldValue, toggleStringArraySelection } from './useSkoponBoundField'
7
7
 
8
8
  function createMockContext(initial: unknown = []): {
9
9
  context: ComponentContext
@@ -47,6 +47,31 @@ describe('readBoundFieldValue', () => {
47
47
  })
48
48
  })
49
49
 
50
+ describe('toggleStringArraySelection', () => {
51
+ it('blocks append when at maxCount', () => {
52
+ expect(toggleStringArraySelection(['a'], 'b', undefined, 1)).toBeNull()
53
+ })
54
+
55
+ it('blocks deselect when at minCount', () => {
56
+ expect(toggleStringArraySelection(['a'], 'a', 1, undefined)).toBeNull()
57
+ })
58
+
59
+ it('allows toggle within bounds', () => {
60
+ expect(toggleStringArraySelection([], 'a', 1, 2)).toEqual(['a'])
61
+ expect(toggleStringArraySelection(['a', 'b'], 'a', 1, 3)).toEqual(['b'])
62
+ })
63
+ })
64
+
65
+ describe('clampStringArraySelection', () => {
66
+ it('returns null when within maxCount', () => {
67
+ expect(clampStringArraySelection(['a'], 2)).toBeNull()
68
+ })
69
+
70
+ it('trims to maxCount when over limit', () => {
71
+ expect(clampStringArraySelection(['a', 'b', 'c'], 2)).toEqual(['a', 'b'])
72
+ })
73
+ })
74
+
50
75
  describe('file upload remove by uid index', () => {
51
76
  it('removes only the file at the uid index when names duplicate', () => {
52
77
  const currentNames = ['dup.txt', 'dup.txt', 'other.txt']
@@ -64,3 +64,49 @@ export function asStringArray(value: unknown): string[] {
64
64
  if (typeof value === 'string' && value) return [value]
65
65
  return []
66
66
  }
67
+
68
+ export function readOptionalSelectCount(value: unknown): number | undefined {
69
+ if (value === undefined || value === null) return undefined
70
+ const num = typeof value === 'number' ? value : Number(value)
71
+ if (!Number.isFinite(num) || num < 1) return undefined
72
+ return Math.floor(num)
73
+ }
74
+
75
+ export function toggleStringArraySelection(
76
+ selectedIds: string[],
77
+ itemId: string,
78
+ minCount: number | undefined,
79
+ maxCount: number | undefined,
80
+ ): string[] | null {
81
+ const isSelected = selectedIds.includes(itemId)
82
+ if (isSelected) {
83
+ const next = selectedIds.filter((id) => id !== itemId)
84
+ if (minCount !== undefined && next.length < minCount) return null
85
+ return next
86
+ }
87
+ if (maxCount !== undefined && selectedIds.length >= maxCount) return null
88
+ return [...selectedIds, itemId]
89
+ }
90
+
91
+ export function isCardSelectionDisabled(
92
+ selectedIds: string[],
93
+ itemId: string,
94
+ interactive: boolean,
95
+ minCount: number | undefined,
96
+ maxCount: number | undefined,
97
+ ): boolean {
98
+ if (!interactive) return true
99
+ const isSelected = selectedIds.includes(itemId)
100
+ if (!isSelected && maxCount !== undefined && selectedIds.length >= maxCount) return true
101
+ if (isSelected && minCount !== undefined && selectedIds.length <= minCount) return true
102
+ return false
103
+ }
104
+
105
+ /** 超出 maxCount 时返回裁剪后的数组,否则返回 null(无需变更)。 */
106
+ export function clampStringArraySelection(
107
+ selectedIds: string[],
108
+ maxCount: number | undefined,
109
+ ): string[] | null {
110
+ if (maxCount === undefined || selectedIds.length <= maxCount) return null
111
+ return selectedIds.slice(0, maxCount)
112
+ }
@@ -129,10 +129,18 @@ export interface FormBlock {
129
129
  defaultValue?: string | string[] | boolean
130
130
  /** 简历多选:是否显示「换一批」 */
131
131
  resumeEnableRefresh?: boolean
132
+ /** 简历多选:最少选中数量,配置值 ≥ 1 */
133
+ resumeMinCount?: number
134
+ /** 简历多选:最多选中数量,配置值 ≥ 1 */
135
+ resumeMaxCount?: number
132
136
  /** 简历多选:列表筛选条件(持久化进 block json) */
133
137
  resumeFilter?: FormResumeFilter
134
138
  /** 案例单选:是否显示「换一批」 */
135
139
  caseEnableRefresh?: boolean
140
+ /** 案例多选:最少选中数量,配置值 ≥ 1 */
141
+ caseMinCount?: number
142
+ /** 案例多选:最多选中数量,配置值 ≥ 1 */
143
+ caseMaxCount?: number
136
144
  /** 案例单选:列表筛选条件(持久化进 block json) */
137
145
  caseFilter?: FormCaseFilter
138
146
  }