@seed-ship/mcp-ui-solid 4.2.2 → 4.3.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/CHANGELOG.md +49 -0
- package/README.md +13 -8
- package/dist/components/FormFieldRenderer.cjs +132 -52
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.js +133 -53
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/dist/components/FormRenderer.cjs +25 -6
- package/dist/components/FormRenderer.cjs.map +1 -1
- package/dist/components/FormRenderer.d.ts.map +1 -1
- package/dist/components/FormRenderer.js +25 -6
- package/dist/components/FormRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +583 -280
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts +2 -0
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +583 -280
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/services/validation.cjs +8 -0
- package/dist/services/validation.cjs.map +1 -1
- package/dist/services/validation.d.ts.map +1 -1
- package/dist/services/validation.js +8 -0
- package/dist/services/validation.js.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +8 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/components/FormFieldRenderer.tsx +89 -2
- package/src/components/FormRenderer.tsx +12 -0
- package/src/components/ScratchpadPanel.tsx +174 -1
- package/src/services/validation.ts +10 -0
- package/src/types/index.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -275,6 +275,14 @@ export interface FormFieldParams {
|
|
|
275
275
|
source?: PrefillSource;
|
|
276
276
|
/** If true, field is visually muted (but still editable on click/focus). */
|
|
277
277
|
muted?: boolean;
|
|
278
|
+
/** How to handle prefill for autocomplete fields.
|
|
279
|
+
* - "exact" (default): prefill is the final value (code), use as-is
|
|
280
|
+
* - "resolve": prefill is a display name, call apiUrl to resolve to valueField */
|
|
281
|
+
prefillMode?: 'exact' | 'resolve';
|
|
282
|
+
/** Regex pattern the submitted value MUST match. Prevents submit if invalid. */
|
|
283
|
+
valueFormat?: string;
|
|
284
|
+
/** Human-readable hint shown when valueFormat validation fails. */
|
|
285
|
+
valueFormatHint?: string;
|
|
278
286
|
minLength?: number;
|
|
279
287
|
maxLength?: number;
|
|
280
288
|
pattern?: string;
|
package/package.json
CHANGED
|
@@ -500,6 +500,7 @@ const AutocompleteField: Component<{
|
|
|
500
500
|
const [suggestions, setSuggestions] = createSignal<Array<{ label: string; value: string }>>([])
|
|
501
501
|
const [isOpen, setIsOpen] = createSignal(false)
|
|
502
502
|
const [selectedLabels, setSelectedLabels] = createSignal<Map<string, string>>(new Map())
|
|
503
|
+
const [resolving, setResolving] = createSignal(false)
|
|
503
504
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
504
505
|
|
|
505
506
|
const isMultiple = () => props.field.multiple === true
|
|
@@ -535,6 +536,72 @@ const AutocompleteField: Component<{
|
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
538
|
|
|
539
|
+
// Proposal 1: prefillMode "resolve" — call apiUrl to resolve display name → code
|
|
540
|
+
const resolvePrefill = async (prefillValues: string[]) => {
|
|
541
|
+
if (!props.field.apiUrl || !props.field.searchParam) return
|
|
542
|
+
setResolving(true)
|
|
543
|
+
const labelField = props.field.labelField || 'label'
|
|
544
|
+
const valueField = props.field.valueField || 'value'
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const resolvedValues: string[] = []
|
|
548
|
+
for (const pv of prefillValues) {
|
|
549
|
+
const params = new URLSearchParams({ [props.field.searchParam]: pv, limit: '1' })
|
|
550
|
+
if (props.field.extraParams) {
|
|
551
|
+
for (const [k, v] of Object.entries(props.field.extraParams)) params.set(k, v)
|
|
552
|
+
}
|
|
553
|
+
const res = await fetch(`${props.field.apiUrl}?${params}`)
|
|
554
|
+
if (!res.ok) { resolvedValues.push(pv); continue }
|
|
555
|
+
const data = await res.json()
|
|
556
|
+
const items = Array.isArray(data) ? data : data.results || data.features || []
|
|
557
|
+
if (items.length > 0) {
|
|
558
|
+
const code = String(items[0][valueField] || pv)
|
|
559
|
+
const label = items[0][labelField] || pv
|
|
560
|
+
resolvedValues.push(code)
|
|
561
|
+
setSelectedLabels((prev) => new Map(prev).set(code, label))
|
|
562
|
+
} else {
|
|
563
|
+
resolvedValues.push(pv)
|
|
564
|
+
setSelectedLabels((prev) => new Map(prev).set(pv, pv))
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (isMultiple()) {
|
|
568
|
+
props.onChange(resolvedValues)
|
|
569
|
+
} else {
|
|
570
|
+
props.onChange(resolvedValues[0] || '')
|
|
571
|
+
const label = selectedLabels().get(resolvedValues[0])
|
|
572
|
+
if (label) setQuery(label)
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
// Fallback: use raw prefill values
|
|
576
|
+
} finally {
|
|
577
|
+
setResolving(false)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// On mount: handle prefill
|
|
582
|
+
createEffect(() => {
|
|
583
|
+
const prefill = props.field.prefill
|
|
584
|
+
if (!prefill) return
|
|
585
|
+
const values = Array.isArray(prefill) ? prefill : [prefill]
|
|
586
|
+
if (values.length === 0) return
|
|
587
|
+
|
|
588
|
+
if (props.field.prefillMode === 'resolve') {
|
|
589
|
+
// Proposal 1: resolve display names to codes via API
|
|
590
|
+
resolvePrefill(values)
|
|
591
|
+
} else {
|
|
592
|
+
// Proposal 2 + 6: exact mode — prefill is a code, show displayHint or label as tag
|
|
593
|
+
for (const v of values) {
|
|
594
|
+
if (!selectedLabels().has(v)) {
|
|
595
|
+
setSelectedLabels((prev) => new Map(prev).set(v, props.field.displayHint || v))
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (!isMultiple() && values[0]) {
|
|
599
|
+
const label = props.field.displayHint || values[0]
|
|
600
|
+
setQuery(label)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
|
|
538
605
|
const handleInput = (value: string) => {
|
|
539
606
|
setQuery(value)
|
|
540
607
|
// Only clear the stored value if user is actively changing the text
|
|
@@ -567,6 +634,7 @@ const AutocompleteField: Component<{
|
|
|
567
634
|
setSuggestions([])
|
|
568
635
|
setIsOpen(false)
|
|
569
636
|
} else {
|
|
637
|
+
// Proposal 6: always store valueField (item.value), display labelField
|
|
570
638
|
props.onChange(item.value)
|
|
571
639
|
setSelectedLabels((prev) => new Map(prev).set(item.value, item.label))
|
|
572
640
|
setQuery(item.label)
|
|
@@ -575,6 +643,17 @@ const AutocompleteField: Component<{
|
|
|
575
643
|
}
|
|
576
644
|
}
|
|
577
645
|
|
|
646
|
+
// Proposal 6: on blur without selection, auto-resolve typed text to first API result
|
|
647
|
+
const handleBlur = () => {
|
|
648
|
+
setTimeout(() => {
|
|
649
|
+
setIsOpen(false)
|
|
650
|
+
// If user typed but didn't select, and field has valueField, resolve first match
|
|
651
|
+
if (!isMultiple() && query() && !props.value && props.field.valueField && suggestions().length > 0) {
|
|
652
|
+
selectSuggestion(suggestions()[0])
|
|
653
|
+
}
|
|
654
|
+
}, 200)
|
|
655
|
+
}
|
|
656
|
+
|
|
578
657
|
const removeChip = (val: string) => {
|
|
579
658
|
props.onChange(selectedValues().filter((v) => v !== val))
|
|
580
659
|
setSelectedLabels((prev) => { const m = new Map(prev); m.delete(val); return m })
|
|
@@ -586,6 +665,14 @@ const AutocompleteField: Component<{
|
|
|
586
665
|
|
|
587
666
|
return (
|
|
588
667
|
<div class="relative">
|
|
668
|
+
{/* Resolving indicator */}
|
|
669
|
+
<Show when={resolving()}>
|
|
670
|
+
<div class="flex items-center gap-1 mb-1 text-xs text-gray-400">
|
|
671
|
+
<span class="animate-spin h-3 w-3 border border-gray-400 border-t-transparent rounded-full" />
|
|
672
|
+
Resolving...
|
|
673
|
+
</div>
|
|
674
|
+
</Show>
|
|
675
|
+
|
|
589
676
|
{/* Multi chips */}
|
|
590
677
|
<Show when={isMultiple() && selectedValues().length > 0}>
|
|
591
678
|
<div class="flex flex-wrap gap-1 mb-1">
|
|
@@ -612,11 +699,11 @@ const AutocompleteField: Component<{
|
|
|
612
699
|
value={query()}
|
|
613
700
|
onInput={(e) => handleInput(e.currentTarget.value)}
|
|
614
701
|
onFocus={() => { if (suggestions().length) setIsOpen(true) }}
|
|
615
|
-
onBlur={
|
|
702
|
+
onBlur={handleBlur}
|
|
616
703
|
placeholder={isMultiple() && selectedValues().length
|
|
617
704
|
? 'Add more...'
|
|
618
705
|
: props.field.placeholder}
|
|
619
|
-
disabled={props.disabled}
|
|
706
|
+
disabled={props.disabled || resolving()}
|
|
620
707
|
class={props.baseClass}
|
|
621
708
|
autocomplete="off"
|
|
622
709
|
/>
|
|
@@ -234,6 +234,18 @@ export const FormRenderer: Component<FormRendererProps> = (props) => {
|
|
|
234
234
|
</Show>
|
|
235
235
|
|
|
236
236
|
<form id={`form-${props.component.id}`} onSubmit={handleSubmit} noValidate>
|
|
237
|
+
{/* Proposal 3: prefill summary */}
|
|
238
|
+
<Show when={params().fields.some((f) => f.prefill != null)}>
|
|
239
|
+
{(() => {
|
|
240
|
+
const prefilled = params().fields.filter((f) => f.prefill != null).length
|
|
241
|
+
const total = params().fields.length
|
|
242
|
+
return (
|
|
243
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
244
|
+
{prefilled} champ{prefilled > 1 ? 's' : ''} pré-rempli{prefilled > 1 ? 's' : ''} sur {total}
|
|
245
|
+
</p>
|
|
246
|
+
)
|
|
247
|
+
})()}
|
|
248
|
+
</Show>
|
|
237
249
|
<div class={layoutClass()}>
|
|
238
250
|
<For each={params().fields}>
|
|
239
251
|
{(field) => (
|
|
@@ -36,6 +36,8 @@ export interface ScratchpadPanelProps {
|
|
|
36
36
|
debug?: boolean
|
|
37
37
|
/** Show mini debug overlay */
|
|
38
38
|
debugOverlay?: boolean
|
|
39
|
+
/** Show collapsible debug trace panel under forms */
|
|
40
|
+
debugTrace?: boolean
|
|
39
41
|
closable?: boolean
|
|
40
42
|
autoCloseDelay?: number
|
|
41
43
|
collapsible?: boolean
|
|
@@ -243,6 +245,7 @@ export const ScratchpadPanel: Component<ScratchpadPanelProps> = (props) => {
|
|
|
243
245
|
onAction={handleAction}
|
|
244
246
|
onSectionEdit={props.onSectionEdit}
|
|
245
247
|
onSubmit={props.onSubmit}
|
|
248
|
+
debugTrace={props.debugTrace}
|
|
246
249
|
/>
|
|
247
250
|
)}
|
|
248
251
|
</For>
|
|
@@ -373,6 +376,7 @@ const SectionRenderer: Component<{
|
|
|
373
376
|
onAction?: (action: string, data?: unknown) => void
|
|
374
377
|
onSectionEdit?: (sectionId: string, content: unknown) => void
|
|
375
378
|
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
379
|
+
debugTrace?: boolean
|
|
376
380
|
}> = (props) => {
|
|
377
381
|
return (
|
|
378
382
|
<div class="px-4 py-3 animate-[slideDown_0.2s_ease-out]">
|
|
@@ -383,7 +387,7 @@ const SectionRenderer: Component<{
|
|
|
383
387
|
<Match when={props.section.type === 'message'}><p class="text-sm text-gray-700 dark:text-gray-300">{String(props.section.content)}</p></Match>
|
|
384
388
|
<Match when={props.section.type === 'action'}><ActionSection content={parseContent(props.section.content)} onAction={props.onAction} /></Match>
|
|
385
389
|
<Match when={props.section.type === 'steps'}><EnrichedStepsSection content={parseContent(props.section.content)} onAction={props.onAction} onFilterChange={props.onFilterChange} /></Match>
|
|
386
|
-
<Match when={props.section.type === 'form'}><EmbeddedFormSection content={parseContent(props.section.content)} sectionId={props.section.id} onAction={props.onAction} onSubmit={props.onSubmit} /></Match>
|
|
390
|
+
<Match when={props.section.type === 'form'}><EmbeddedFormSection content={parseContent(props.section.content)} sectionId={props.section.id} onAction={props.onAction} onSubmit={props.onSubmit} debugTrace={props.debugTrace} /></Match>
|
|
387
391
|
<Match when={props.section.type === 'understanding'}><UnderstandingSection content={parseContent(props.section.content)} /></Match>
|
|
388
392
|
<Match when={props.section.type === 'feedback'}><FeedbackSection content={parseContent(props.section.content)} onAction={props.onAction} /></Match>
|
|
389
393
|
<Match when={props.section.type === 'prompt'}><PromptSection content={parseContent(props.section.content)} onAction={props.onAction} /></Match>
|
|
@@ -524,6 +528,7 @@ const EmbeddedFormSection: Component<{
|
|
|
524
528
|
sectionId: string
|
|
525
529
|
onAction?: (action: string, data?: unknown) => void
|
|
526
530
|
onSubmit?: (sectionId: string, values: Record<string, unknown>) => void
|
|
531
|
+
debugTrace?: boolean
|
|
527
532
|
}> = (props) => {
|
|
528
533
|
const [dynamicOptions, setDynamicOptions] = createSignal<Record<string, Array<{ label: string; value: string }>>>({})
|
|
529
534
|
|
|
@@ -631,6 +636,9 @@ const EmbeddedFormSection: Component<{
|
|
|
631
636
|
return dynOpts ? { ...field, options: dynOpts } as FormFieldParams : field as FormFieldParams
|
|
632
637
|
}
|
|
633
638
|
|
|
639
|
+
// Debug trace: track submitted values
|
|
640
|
+
const [submittedValues, setSubmittedValues] = createSignal<Record<string, any> | null>(null)
|
|
641
|
+
|
|
634
642
|
const handleSubmit = (e: Event) => {
|
|
635
643
|
e.preventDefault()
|
|
636
644
|
|
|
@@ -644,6 +652,8 @@ const EmbeddedFormSection: Component<{
|
|
|
644
652
|
.filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0))
|
|
645
653
|
)
|
|
646
654
|
|
|
655
|
+
setSubmittedValues(values)
|
|
656
|
+
|
|
647
657
|
// DX1 Etape 7: form submit log
|
|
648
658
|
console.info(`%c[MCP-UI] Form submitted%c section=${props.sectionId} fields=${Object.keys(values).join(',')}`, 'color: #8b5cf6; font-weight: bold', 'color: inherit')
|
|
649
659
|
|
|
@@ -654,8 +664,43 @@ const EmbeddedFormSection: Component<{
|
|
|
654
664
|
}
|
|
655
665
|
}
|
|
656
666
|
|
|
667
|
+
// Proposal 3: prefill confidence summary
|
|
668
|
+
const prefillSummary = () => {
|
|
669
|
+
const fields = config().fields
|
|
670
|
+
const total = fields.length
|
|
671
|
+
const prefilled = fields.filter((f: any) => f.prefill != null).length
|
|
672
|
+
return { total, prefilled }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Proposal 4: auto-submit toast mode — compact view when ALL fields prefilled
|
|
676
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
677
|
+
const allFieldsPrefilled = () => {
|
|
678
|
+
const fields = config().fields
|
|
679
|
+
return fields.length > 0 && fields.every((f: any) => f.prefill != null)
|
|
680
|
+
}
|
|
681
|
+
const showToast = () => allFieldsPrefilled() && !userInteracted() && !expanded() && countdown() != null
|
|
682
|
+
|
|
657
683
|
return (
|
|
684
|
+
<>
|
|
685
|
+
<Show when={!showToast()} fallback={
|
|
686
|
+
<div class="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-sm">
|
|
687
|
+
<span class="flex-1 text-blue-800 dark:text-blue-200 font-medium">
|
|
688
|
+
{config().fields.map((f: any) => f.displayHint || f.prefill).join(', ')}
|
|
689
|
+
</span>
|
|
690
|
+
<span class="text-blue-600 dark:text-blue-300">{countdown()}s...</span>
|
|
691
|
+
<button type="button" onClick={() => { setExpanded(true); cancelCountdown(); setUserInteracted(true) }}
|
|
692
|
+
class="text-blue-600 dark:text-blue-400 underline text-xs">Modifier</button>
|
|
693
|
+
<button type="button" onClick={() => { cancelCountdown(); setUserInteracted(true) }}
|
|
694
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">×</button>
|
|
695
|
+
</div>
|
|
696
|
+
}>
|
|
658
697
|
<form id={`scratchpad-form-${props.sectionId}`} onSubmit={handleSubmit} class="flex flex-col gap-3">
|
|
698
|
+
{/* Proposal 3: prefill summary */}
|
|
699
|
+
<Show when={prefillSummary().prefilled > 0}>
|
|
700
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
701
|
+
{prefillSummary().prefilled} champ{prefillSummary().prefilled > 1 ? 's' : ''} pré-rempli{prefillSummary().prefilled > 1 ? 's' : ''} sur {prefillSummary().total}
|
|
702
|
+
</p>
|
|
703
|
+
</Show>
|
|
659
704
|
<For each={config().fields}>
|
|
660
705
|
{(field) => (
|
|
661
706
|
<FormFieldRenderer
|
|
@@ -686,6 +731,134 @@ const EmbeddedFormSection: Component<{
|
|
|
686
731
|
</button>
|
|
687
732
|
</div>
|
|
688
733
|
</form>
|
|
734
|
+
</Show>
|
|
735
|
+
|
|
736
|
+
{/* Debug trace panel */}
|
|
737
|
+
<Show when={props.debugTrace}>
|
|
738
|
+
<FormDebugTrace
|
|
739
|
+
fields={config().fields}
|
|
740
|
+
formData={formData()}
|
|
741
|
+
submittedValues={submittedValues()}
|
|
742
|
+
autoSubmitDelay={config().autoSubmitDelay}
|
|
743
|
+
userInteracted={userInteracted()}
|
|
744
|
+
rawContent={props.content}
|
|
745
|
+
/>
|
|
746
|
+
</Show>
|
|
747
|
+
</>
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ─── Form Debug Trace Panel ─────────────────────────────────
|
|
752
|
+
|
|
753
|
+
const FormDebugTrace: Component<{
|
|
754
|
+
fields: any[]
|
|
755
|
+
formData: Record<string, any>
|
|
756
|
+
submittedValues: Record<string, any> | null
|
|
757
|
+
autoSubmitDelay?: number
|
|
758
|
+
userInteracted: boolean
|
|
759
|
+
rawContent: unknown
|
|
760
|
+
}> = (props) => {
|
|
761
|
+
const [open, setOpen] = createSignal(false)
|
|
762
|
+
const [showRaw, setShowRaw] = createSignal(false)
|
|
763
|
+
|
|
764
|
+
const prefilledCount = () => props.fields.filter((f: any) => f.prefill != null).length
|
|
765
|
+
const requiredFields = () => props.fields.filter((f: any) => f.required)
|
|
766
|
+
const missingRequired = () => requiredFields().filter((f: any) => f.prefill == null)
|
|
767
|
+
|
|
768
|
+
const autoSubmitReason = () => {
|
|
769
|
+
if (!props.autoSubmitDelay) return 'no autoSubmitDelay configured'
|
|
770
|
+
if (missingRequired().length > 0) return `${missingRequired().length} required field(s) without prefill`
|
|
771
|
+
if (props.userInteracted) return 'user interacted — cancelled'
|
|
772
|
+
return 'all conditions met'
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Server-side _debug data
|
|
776
|
+
const serverDebug = () => (props.rawContent as any)?._debug
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<div class="mt-2 border border-gray-200 dark:border-gray-700 rounded-md text-xs font-mono">
|
|
780
|
+
<button
|
|
781
|
+
type="button"
|
|
782
|
+
onClick={() => setOpen(!open())}
|
|
783
|
+
class="w-full px-3 py-1.5 text-left text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-1"
|
|
784
|
+
>
|
|
785
|
+
<span>{open() ? '\u25BE' : '\u25B8'}</span>
|
|
786
|
+
Debug trace ({prefilledCount()}/{props.fields.length} prefilled)
|
|
787
|
+
</button>
|
|
788
|
+
<Show when={open()}>
|
|
789
|
+
<div class="px-3 pb-3 space-y-2 text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700 pt-2">
|
|
790
|
+
<div>Fields: {props.fields.length} total, {prefilledCount()} prefilled</div>
|
|
791
|
+
|
|
792
|
+
<For each={props.fields}>
|
|
793
|
+
{(field: any) => (
|
|
794
|
+
<div class="pl-2 border-l-2 border-gray-200 dark:border-gray-600 space-y-0.5">
|
|
795
|
+
<div class="text-gray-800 dark:text-gray-200 font-medium">{field.name}:</div>
|
|
796
|
+
<Show when={field.prefill != null}>
|
|
797
|
+
<div> prefill: {JSON.stringify(field.prefill)}</div>
|
|
798
|
+
</Show>
|
|
799
|
+
<Show when={!field.prefill}>
|
|
800
|
+
<div class="text-amber-500"> prefill: (none)</div>
|
|
801
|
+
</Show>
|
|
802
|
+
<Show when={field.source}><div> source: {field.source}</div></Show>
|
|
803
|
+
<Show when={field.displayHint}><div> displayHint: "{field.displayHint}"</div></Show>
|
|
804
|
+
<Show when={field.muted}><div> muted: true</div></Show>
|
|
805
|
+
<Show when={field.prefillMode}><div> prefillMode: {field.prefillMode}</div></Show>
|
|
806
|
+
<Show when={field.valueFormat}><div> valueFormat: /{field.valueFormat}/</div></Show>
|
|
807
|
+
<Show when={props.submittedValues}>
|
|
808
|
+
{(() => {
|
|
809
|
+
const v = props.submittedValues![field.name]
|
|
810
|
+
const hasValue = v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)
|
|
811
|
+
return (
|
|
812
|
+
<div class={hasValue ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}>
|
|
813
|
+
{'\u2192'} submitted: {hasValue ? `${JSON.stringify(v)} \u2713` : '(empty)'}
|
|
814
|
+
</div>
|
|
815
|
+
)
|
|
816
|
+
})()}
|
|
817
|
+
</Show>
|
|
818
|
+
</div>
|
|
819
|
+
)}
|
|
820
|
+
</For>
|
|
821
|
+
|
|
822
|
+
<div class="pt-1 border-t border-gray-200 dark:border-gray-600">
|
|
823
|
+
autoSubmit: {props.autoSubmitDelay ? 'true' : 'false'}
|
|
824
|
+
{props.autoSubmitDelay ? ` (${props.autoSubmitDelay}ms)` : ''}
|
|
825
|
+
<div> reason: {autoSubmitReason()}</div>
|
|
826
|
+
</div>
|
|
827
|
+
|
|
828
|
+
<Show when={serverDebug()}>
|
|
829
|
+
<div class="pt-1 border-t border-gray-200 dark:border-gray-600">
|
|
830
|
+
<div class="font-medium text-gray-800 dark:text-gray-200">Server _debug:</div>
|
|
831
|
+
<Show when={serverDebug()?.resolvers}>
|
|
832
|
+
<For each={serverDebug().resolvers}>
|
|
833
|
+
{(r: any) => (
|
|
834
|
+
<div class="pl-2">
|
|
835
|
+
{r.field}: {r.resolver} "{r.input}" {'\u2192'} "{r.output}" ({r.ms}ms)
|
|
836
|
+
</div>
|
|
837
|
+
)}
|
|
838
|
+
</For>
|
|
839
|
+
</Show>
|
|
840
|
+
<Show when={serverDebug()?.routing}>
|
|
841
|
+
<div class="pl-2">routing: {serverDebug().routing.topic} via {serverDebug().routing.method} ({serverDebug().routing.ms}ms)</div>
|
|
842
|
+
</Show>
|
|
843
|
+
<Show when={serverDebug()?.missingFields}>
|
|
844
|
+
<div class="pl-2 text-amber-500">missing: {serverDebug().missingFields.join(', ')}</div>
|
|
845
|
+
</Show>
|
|
846
|
+
</div>
|
|
847
|
+
</Show>
|
|
848
|
+
|
|
849
|
+
<div class="pt-1 border-t border-gray-200 dark:border-gray-600">
|
|
850
|
+
<button type="button" onClick={() => setShowRaw(!showRaw())} class="text-blue-500 hover:text-blue-700 underline">
|
|
851
|
+
{showRaw() ? 'Hide' : 'Show'} raw SSE payload
|
|
852
|
+
</button>
|
|
853
|
+
<Show when={showRaw()}>
|
|
854
|
+
<pre class="mt-1 p-2 bg-gray-50 dark:bg-gray-900 rounded max-h-48 overflow-auto text-[10px]">
|
|
855
|
+
{JSON.stringify(props.rawContent, null, 2)}
|
|
856
|
+
</pre>
|
|
857
|
+
</Show>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
</Show>
|
|
861
|
+
</div>
|
|
689
862
|
)
|
|
690
863
|
}
|
|
691
864
|
|
|
@@ -939,6 +939,16 @@ export function validateFieldValue(
|
|
|
939
939
|
break
|
|
940
940
|
}
|
|
941
941
|
|
|
942
|
+
// valueFormat validation (v4.3.0) — runs after type-specific checks
|
|
943
|
+
if (field.valueFormat && value !== undefined && value !== null && value !== '') {
|
|
944
|
+
const vals = Array.isArray(value) ? value : [String(value)]
|
|
945
|
+
for (const v of vals) {
|
|
946
|
+
if (!new RegExp(field.valueFormat).test(v)) {
|
|
947
|
+
return { valid: false, error: field.valueFormatHint || `Invalid format (expected: ${field.valueFormat})` }
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
942
952
|
return { valid: true }
|
|
943
953
|
}
|
|
944
954
|
|
package/src/types/index.ts
CHANGED
|
@@ -345,6 +345,14 @@ export interface FormFieldParams {
|
|
|
345
345
|
source?: PrefillSource
|
|
346
346
|
/** If true, field is visually muted (but still editable on click/focus). */
|
|
347
347
|
muted?: boolean
|
|
348
|
+
/** How to handle prefill for autocomplete fields.
|
|
349
|
+
* - "exact" (default): prefill is the final value (code), use as-is
|
|
350
|
+
* - "resolve": prefill is a display name, call apiUrl to resolve to valueField */
|
|
351
|
+
prefillMode?: 'exact' | 'resolve'
|
|
352
|
+
/** Regex pattern the submitted value MUST match. Prevents submit if invalid. */
|
|
353
|
+
valueFormat?: string
|
|
354
|
+
/** Human-readable hint shown when valueFormat validation fails. */
|
|
355
|
+
valueFormatHint?: string
|
|
348
356
|
|
|
349
357
|
// Text/textarea specific
|
|
350
358
|
minLength?: number
|