@pilotiq/pilotiq 0.6.1 → 0.7.0
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/.turbo/turbo-build.log +6 -2
- package/CHANGELOG.md +614 -0
- package/CLAUDE.md +6 -5
- package/dist/Column.d.ts +35 -0
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js +41 -0
- package/dist/Column.js.map +1 -1
- package/dist/Page.d.ts +13 -4
- package/dist/Page.d.ts.map +1 -1
- package/dist/Page.js +9 -2
- package/dist/Page.js.map +1 -1
- package/dist/Pilotiq.d.ts +84 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +66 -0
- package/dist/Pilotiq.js.map +1 -1
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +9 -0
- package/dist/Resource.js.map +1 -1
- package/dist/actions/exportFactory.js +1 -1
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/columns/SelectColumn.d.ts +32 -5
- package/dist/columns/SelectColumn.d.ts.map +1 -1
- package/dist/columns/SelectColumn.js +37 -7
- package/dist/columns/SelectColumn.js.map +1 -1
- package/dist/defaultPages.d.ts.map +1 -1
- package/dist/defaultPages.js +3 -0
- package/dist/defaultPages.js.map +1 -1
- package/dist/elements/Form.d.ts +17 -0
- package/dist/elements/Form.d.ts.map +1 -1
- package/dist/elements/Form.js +17 -0
- package/dist/elements/Form.js.map +1 -1
- package/dist/elements/Table.d.ts +26 -0
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +15 -1
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableGroup.d.ts +84 -0
- package/dist/elements/TableGroup.d.ts.map +1 -1
- package/dist/elements/TableGroup.js +103 -0
- package/dist/elements/TableGroup.js.map +1 -1
- package/dist/elements/dispatchForm.d.ts.map +1 -1
- package/dist/elements/dispatchForm.js +36 -6
- package/dist/elements/dispatchForm.js.map +1 -1
- package/dist/elements/dispatchTable.d.ts +12 -0
- package/dist/elements/dispatchTable.d.ts.map +1 -1
- package/dist/elements/dispatchTable.js +104 -29
- package/dist/elements/dispatchTable.js.map +1 -1
- package/dist/fields/Field.d.ts +7 -2
- package/dist/fields/Field.d.ts.map +1 -1
- package/dist/fields/Field.js +8 -3
- package/dist/fields/Field.js.map +1 -1
- package/dist/fields/RepeaterField.d.ts +65 -0
- package/dist/fields/RepeaterField.d.ts.map +1 -1
- package/dist/fields/RepeaterField.js +48 -0
- package/dist/fields/RepeaterField.js.map +1 -1
- package/dist/orm/modelDefaults.d.ts.map +1 -1
- package/dist/orm/modelDefaults.js +19 -0
- package/dist/orm/modelDefaults.js.map +1 -1
- package/dist/pageData.d.ts +20 -0
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +242 -34
- package/dist/pageData.js.map +1 -1
- package/dist/react/AppShell.d.ts +17 -1
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +34 -3
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/PendingSuggestionApplierRegistry.d.ts +34 -0
- package/dist/react/PendingSuggestionApplierRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionApplierRegistry.js +51 -0
- package/dist/react/PendingSuggestionApplierRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts +46 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js +16 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionsContext.d.ts +153 -0
- package/dist/react/PendingSuggestionsContext.d.ts.map +1 -0
- package/dist/react/PendingSuggestionsContext.js +46 -0
- package/dist/react/PendingSuggestionsContext.js.map +1 -0
- package/dist/react/SchemaRenderer.d.ts.map +1 -1
- package/dist/react/SchemaRenderer.js +312 -39
- package/dist/react/SchemaRenderer.js.map +1 -1
- package/dist/react/cells/EditableCell.d.ts +8 -0
- package/dist/react/cells/EditableCell.d.ts.map +1 -1
- package/dist/react/cells/EditableCell.js +6 -2
- package/dist/react/cells/EditableCell.js.map +1 -1
- package/dist/react/fields/CheckboxListInput.d.ts.map +1 -1
- package/dist/react/fields/CheckboxListInput.js +29 -2
- package/dist/react/fields/CheckboxListInput.js.map +1 -1
- package/dist/react/fields/ColorInput.d.ts.map +1 -1
- package/dist/react/fields/ColorInput.js +28 -2
- package/dist/react/fields/ColorInput.js.map +1 -1
- package/dist/react/fields/DateTimeInput.d.ts.map +1 -1
- package/dist/react/fields/DateTimeInput.js +28 -2
- package/dist/react/fields/DateTimeInput.js.map +1 -1
- package/dist/react/fields/FieldShell.d.ts.map +1 -1
- package/dist/react/fields/FieldShell.js +161 -3
- package/dist/react/fields/FieldShell.js.map +1 -1
- package/dist/react/fields/FileUploadInput.d.ts.map +1 -1
- package/dist/react/fields/FileUploadInput.js +27 -2
- package/dist/react/fields/FileUploadInput.js.map +1 -1
- package/dist/react/fields/KeyValueInput.d.ts.map +1 -1
- package/dist/react/fields/KeyValueInput.js +33 -2
- package/dist/react/fields/KeyValueInput.js.map +1 -1
- package/dist/react/fields/RadioInput.d.ts.map +1 -1
- package/dist/react/fields/RadioInput.js +28 -2
- package/dist/react/fields/RadioInput.js.map +1 -1
- package/dist/react/fields/SelectFieldInput.d.ts.map +1 -1
- package/dist/react/fields/SelectFieldInput.js +31 -2
- package/dist/react/fields/SelectFieldInput.js.map +1 -1
- package/dist/react/fields/SliderInput.d.ts.map +1 -1
- package/dist/react/fields/SliderInput.js +26 -2
- package/dist/react/fields/SliderInput.js.map +1 -1
- package/dist/react/fields/TagsInput.d.ts.map +1 -1
- package/dist/react/fields/TagsInput.js +26 -2
- package/dist/react/fields/TagsInput.js.map +1 -1
- package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -1
- package/dist/react/fields/ToggleFieldInput.js +29 -2
- package/dist/react/fields/ToggleFieldInput.js.map +1 -1
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +55 -2
- package/dist/routes.js.map +1 -1
- package/dist/schema/Html.d.ts +2 -2
- package/dist/schema/Html.d.ts.map +1 -1
- package/dist/schema/Html.js +2 -2
- package/dist/schema/Html.js.map +1 -1
- package/dist/schema/Markdown.d.ts +2 -2
- package/dist/schema/Markdown.d.ts.map +1 -1
- package/dist/schema/Markdown.js +2 -2
- package/dist/schema/Markdown.js.map +1 -1
- package/dist/schema/Section.d.ts +16 -0
- package/dist/schema/Section.d.ts.map +1 -1
- package/dist/schema/Section.js +16 -0
- package/dist/schema/Section.js.map +1 -1
- package/dist/schema/Wizard.d.ts +45 -0
- package/dist/schema/Wizard.d.ts.map +1 -1
- package/dist/schema/Wizard.js +50 -0
- package/dist/schema/Wizard.js.map +1 -1
- package/dist/schema/resolveSchema.d.ts +8 -0
- package/dist/schema/resolveSchema.d.ts.map +1 -1
- package/dist/schema/resolveSchema.js +70 -1
- package/dist/schema/resolveSchema.js.map +1 -1
- package/dist/schema/sanitize.d.ts +3 -3
- package/dist/schema/sanitize.d.ts.map +1 -1
- package/dist/schema/sanitize.js +10 -3
- package/dist/schema/sanitize.js.map +1 -1
- package/dist/sessionFilters.d.ts.map +1 -1
- package/dist/sessionFilters.js +12 -1
- package/dist/sessionFilters.js.map +1 -1
- package/dist/styles/file-upload.css +13 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +9 -2
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/Column.test.ts +36 -0
- package/src/Column.ts +54 -0
- package/src/Page.ts +13 -4
- package/src/Pilotiq.ts +109 -0
- package/src/Resource.ts +29 -0
- package/src/actions/exportFactory.ts +1 -1
- package/src/columns/SelectColumn.ts +46 -8
- package/src/columns/editableColumns.test.ts +45 -0
- package/src/defaultPages.ts +3 -0
- package/src/elements/Form.ts +19 -0
- package/src/elements/Table.ts +35 -1
- package/src/elements/TableGroup.test.ts +111 -0
- package/src/elements/TableGroup.ts +135 -0
- package/src/elements/dispatchForm.ts +34 -7
- package/src/elements/dispatchTable.test.ts +267 -0
- package/src/elements/dispatchTable.ts +112 -33
- package/src/fields/Field.test.ts +15 -0
- package/src/fields/Field.ts +8 -3
- package/src/fields/RepeaterField.ts +104 -0
- package/src/fields/RepeaterRelationship.test.ts +173 -0
- package/src/nestedRelationManagerData.test.ts +21 -0
- package/src/orm/modelDefaults.ts +21 -0
- package/src/pageData.ts +267 -47
- package/src/react/AppShell.tsx +55 -4
- package/src/react/PendingSuggestionApplierRegistry.ts +80 -0
- package/src/react/PendingSuggestionOverlayRegistry.ts +54 -0
- package/src/react/PendingSuggestionsContext.tsx +172 -0
- package/src/react/SchemaRenderer.tsx +504 -95
- package/src/react/cells/EditableCell.tsx +11 -2
- package/src/react/fields/CheckboxListInput.tsx +23 -2
- package/src/react/fields/ColorInput.tsx +22 -2
- package/src/react/fields/DateTimeInput.tsx +22 -2
- package/src/react/fields/FieldShell.tsx +167 -3
- package/src/react/fields/FileUploadInput.tsx +21 -2
- package/src/react/fields/KeyValueInput.tsx +32 -2
- package/src/react/fields/RadioInput.tsx +23 -2
- package/src/react/fields/SelectFieldInput.tsx +25 -2
- package/src/react/fields/SliderInput.tsx +20 -2
- package/src/react/fields/TagsInput.tsx +20 -2
- package/src/react/fields/ToggleFieldInput.tsx +23 -2
- package/src/react/index.ts +18 -0
- package/src/relationManagerData.test.ts +451 -2
- package/src/routes.ts +58 -2
- package/src/schema/Html.ts +2 -2
- package/src/schema/Markdown.ts +2 -2
- package/src/schema/Section.ts +17 -0
- package/src/schema/Wizard.ts +67 -0
- package/src/schema/containers.test.ts +90 -0
- package/src/schema/resolveSchema.test.ts +50 -0
- package/src/schema/resolveSchema.ts +79 -1
- package/src/schema/sanitize.ts +13 -4
- package/src/sessionFilters.test.ts +23 -0
- package/src/sessionFilters.ts +11 -1
- package/src/styles/file-upload.css +13 -0
- package/src/vite.ts +9 -2
|
@@ -33,6 +33,11 @@ export interface EditableCellProps {
|
|
|
33
33
|
value: unknown
|
|
34
34
|
/** Disabled = static `disabled()` OR per-row `_cellDisabled[col]`. */
|
|
35
35
|
disabled: boolean
|
|
36
|
+
/** Row-scoped option override stamped by `loadTableRecords` when the
|
|
37
|
+
* column is a `SelectColumn` with a per-row `.options(record => …)`
|
|
38
|
+
* resolver. Wins over `col.selectOptions`; absent when the resolver
|
|
39
|
+
* threw or the column has only static options. */
|
|
40
|
+
rowOptions?: Array<{ value: string; label: string }>
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
interface PatchResultOk {
|
|
@@ -283,9 +288,13 @@ export function CellToggle(props: EditableCellProps): React.ReactElement {
|
|
|
283
288
|
// ─── Select cell ───────────────────────────────────────
|
|
284
289
|
|
|
285
290
|
export function CellSelect(props: EditableCellProps): React.ReactElement {
|
|
286
|
-
const { url, value, disabled, col } = props
|
|
291
|
+
const { url, value, disabled, col, rowOptions } = props
|
|
292
|
+
// Per-row resolver wins over the column's static options. Both can be
|
|
293
|
+
// present: a column may set static options as a fallback that the
|
|
294
|
+
// resolver overrides per row.
|
|
287
295
|
const opts: Array<{ value: string; label: string }> =
|
|
288
|
-
|
|
296
|
+
rowOptions ??
|
|
297
|
+
(Array.isArray(col['selectOptions']) ? col['selectOptions'] as Array<{ value: string; label: string }> : [])
|
|
289
298
|
const nullable = col['selectNullable'] === true
|
|
290
299
|
const showPlaceholderOnce = col['selectablePlaceholder'] !== false // default true (keep showing)
|
|
291
300
|
const confirmMsg = col['confirm'] as string | undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
import { Checkbox } from '../ui/checkbox.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -39,6 +40,26 @@ export function CheckboxListInput({
|
|
|
39
40
|
else { setLocalValue(next); fs.triggerLive(next) }
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// Cross-tree applier — the visible checkboxes are React-controlled
|
|
44
|
+
// (Base UI `<Checkbox checked={…}>`); the per-option hidden mirrors
|
|
45
|
+
// share the `[name]` attribute, so FieldShell's generic applier would
|
|
46
|
+
// overwrite every one of them with the suggestion's stringified value
|
|
47
|
+
// instead of replacing the array. FieldShell skips its generic
|
|
48
|
+
// registration for fieldType === 'checkboxList'.
|
|
49
|
+
const fsRef = useRef(fs)
|
|
50
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
51
|
+
const formId = useContext(FormIdContext) || undefined
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (name.includes('.')) return
|
|
54
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
55
|
+
const next = toArray(suggestion.suggestedValue)
|
|
56
|
+
const cur = fsRef.current
|
|
57
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
58
|
+
else { setLocalValue(next); cur.triggerLive(next) }
|
|
59
|
+
}
|
|
60
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
61
|
+
}, [name, formId])
|
|
62
|
+
|
|
42
63
|
const layout = columns > 1
|
|
43
64
|
? `grid grid-cols-${columns} gap-2`
|
|
44
65
|
: 'flex flex-col gap-2'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
import { Input } from '../ui/input.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -27,6 +28,25 @@ export function ColorInput({
|
|
|
27
28
|
else { setLocalValue(v); fs.triggerLive(v) }
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Cross-tree applier — color/text inputs are React-controlled (`value`,
|
|
32
|
+
// not `defaultValue`), so a DOM-write to the hidden mirror wouldn't
|
|
33
|
+
// reach them. FieldShell skips its generic registration for
|
|
34
|
+
// fieldType === 'color'.
|
|
35
|
+
const fsRef = useRef(fs)
|
|
36
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
37
|
+
const formId = useContext(FormIdContext) || undefined
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (name.includes('.')) return
|
|
40
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
41
|
+
const raw = suggestion.suggestedValue
|
|
42
|
+
const next = typeof raw === 'string' && raw ? raw : '#000000'
|
|
43
|
+
const cur = fsRef.current
|
|
44
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
45
|
+
else { setLocalValue(next); cur.triggerLive(next) }
|
|
46
|
+
}
|
|
47
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
48
|
+
}, [name, formId])
|
|
49
|
+
|
|
30
50
|
return (
|
|
31
51
|
<div className="flex items-center gap-2">
|
|
32
52
|
<input type="hidden" name={name} value={value} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
import { Input } from '../ui/input.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -28,6 +29,25 @@ export function DateTimeInput({
|
|
|
28
29
|
else { setLocalValue(v); fs.triggerLive(v) }
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Cross-tree applier — the visible `<input type="datetime-local">` is
|
|
33
|
+
// React-controlled (`value`, not `defaultValue`), so a DOM-write to
|
|
34
|
+
// it bypasses the controller. FieldShell skips its generic
|
|
35
|
+
// registration for fieldType === 'dateTime'.
|
|
36
|
+
const fsRef = useRef(fs)
|
|
37
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
38
|
+
const formId = useContext(FormIdContext) || undefined
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (name.includes('.')) return
|
|
41
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
42
|
+
const v = suggestion.suggestedValue
|
|
43
|
+
const next = v == null || v === '' ? '' : String(v)
|
|
44
|
+
const cur = fsRef.current
|
|
45
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
46
|
+
else { setLocalValue(next); cur.triggerLive(next) }
|
|
47
|
+
}
|
|
48
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
49
|
+
}, [name, formId])
|
|
50
|
+
|
|
31
51
|
return (
|
|
32
52
|
<Input
|
|
33
53
|
type="datetime-local"
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useRef } from 'react'
|
|
2
2
|
import type { ElementMeta } from '../../schema/Element.js'
|
|
3
3
|
import { getIcon } from '../../icons/registry.js'
|
|
4
|
+
import { usePendingSuggestions, usePendingSuggestionsForField, type PendingSuggestion } from '../PendingSuggestionsContext.js'
|
|
5
|
+
import { getPendingSuggestionOverlay } from '../PendingSuggestionOverlayRegistry.js'
|
|
6
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
7
|
+
import { FormIdContext, useFieldState } from '../FormStateContext.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Field types whose visible state is driven by React (not by a matching
|
|
11
|
+
* `[name]` DOM input). Each registers its own applier inside the field
|
|
12
|
+
* renderer; FieldShell skips its generic DOM-write applier for these so
|
|
13
|
+
* the field-owned applier stays last-write-wins in the registry. Keep in
|
|
14
|
+
* sync with the per-field `useEffect(registerPendingSuggestionApplier…)`
|
|
15
|
+
* blocks under `react/fields/`.
|
|
16
|
+
*/
|
|
17
|
+
const SELF_APPLIER_FIELD_TYPES = new Set<string>([
|
|
18
|
+
'select',
|
|
19
|
+
'toggle',
|
|
20
|
+
'slider',
|
|
21
|
+
'color',
|
|
22
|
+
'keyValue',
|
|
23
|
+
'fileUpload',
|
|
24
|
+
'tagsInput',
|
|
25
|
+
'dateTime',
|
|
26
|
+
'radio',
|
|
27
|
+
'checkboxList',
|
|
28
|
+
])
|
|
4
29
|
|
|
5
30
|
/**
|
|
6
31
|
* Shared chrome around every field input — label + required asterisk +
|
|
@@ -41,6 +66,77 @@ export function FieldShell({ el, name, label, required, children, before, after,
|
|
|
41
66
|
const hiddenLabel = el['hiddenLabel'] === true
|
|
42
67
|
const wrapperAttrs = pickWrapperAttrs(el)
|
|
43
68
|
|
|
69
|
+
// Pending-suggestion overlay (Plan 6/7). RichText fields render the diff
|
|
70
|
+
// inline inside the editor instead — they opt out via the hidden marker
|
|
71
|
+
// below. Other field types pick up the slot whenever a plugin (e.g.
|
|
72
|
+
// `@pilotiq-pro/ai`) has registered a renderer AND there's a matching
|
|
73
|
+
// suggestion in the queue.
|
|
74
|
+
const fieldType = el['fieldType'] as string | undefined
|
|
75
|
+
const isRichText = fieldType === 'richtext'
|
|
76
|
+
// Field types that drive their visible state from React (not from a
|
|
77
|
+
// matching `[name]` DOM input) register their own applier — see
|
|
78
|
+
// `SelectFieldInput` for the canonical example. FieldShell's generic
|
|
79
|
+
// DOM-write applier would silently no-op on these, and (since parent
|
|
80
|
+
// effects run AFTER children) would overwrite the field-owned applier
|
|
81
|
+
// in the registry. Skip registration here so the field-owned applier
|
|
82
|
+
// stays the winner.
|
|
83
|
+
const ownsApplier = fieldType !== undefined && SELF_APPLIER_FIELD_TYPES.has(fieldType)
|
|
84
|
+
const { list: pending, dismiss } = usePendingSuggestionsForField(name)
|
|
85
|
+
const { approve } = usePendingSuggestions()
|
|
86
|
+
const Overlay = isRichText ? null : getPendingSuggestionOverlay()
|
|
87
|
+
const overlaySuggestion = pending[0] ?? null
|
|
88
|
+
// Approve routes through the cross-tree applier registry (Phase 8.5)
|
|
89
|
+
// so field types that own their visible state (Select / Toggle / Slider /
|
|
90
|
+
// Color / etc.) get their registered applier — not the overlay's
|
|
91
|
+
// hardcoded `field.setValue` + DOM-write fallback, which would silently
|
|
92
|
+
// miss the React state of those custom components. Reject just dismisses
|
|
93
|
+
// and restores focus to the input so the user can keep typing without
|
|
94
|
+
// a stray click.
|
|
95
|
+
const onReject = (): void => {
|
|
96
|
+
dismiss(overlaySuggestion!.id)
|
|
97
|
+
if (typeof document === 'undefined') return
|
|
98
|
+
queueMicrotask(() => {
|
|
99
|
+
const el = document.getElementsByName(name)[0]
|
|
100
|
+
if (el instanceof HTMLElement) el.focus()
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
const overlayNode = Overlay && overlaySuggestion ? (
|
|
104
|
+
<Overlay
|
|
105
|
+
suggestion={overlaySuggestion}
|
|
106
|
+
onApprove={() => approve(overlaySuggestion.id)}
|
|
107
|
+
onReject={onReject}
|
|
108
|
+
{...(fieldType !== undefined ? { fieldType } : {})}
|
|
109
|
+
el={el}
|
|
110
|
+
/>
|
|
111
|
+
) : null
|
|
112
|
+
|
|
113
|
+
// Cross-tree applier registration (Phase 8.5). Lets aggregate consumers
|
|
114
|
+
// (e.g. a chat-sidebar pending-pill living outside the form's React
|
|
115
|
+
// tree) reach this field's mutator via
|
|
116
|
+
// `PendingSuggestionApplierRegistry`. Skipped for richtext — the
|
|
117
|
+
// Tiptap bridge registers its own editor-command applier. Skipped for
|
|
118
|
+
// dotted-path fields (Repeater inner rows) since `useFieldState` is
|
|
119
|
+
// a no-op for them; pill-driven approve falls back to `dismiss` which
|
|
120
|
+
// is the right semantics (pill cannot reach into row state).
|
|
121
|
+
const fieldState = useFieldState(name)
|
|
122
|
+
const fieldStateRef = useRef(fieldState)
|
|
123
|
+
useEffect(() => { fieldStateRef.current = fieldState }, [fieldState])
|
|
124
|
+
const formId = useContext(FormIdContext) || undefined
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (isRichText) return
|
|
127
|
+
if (ownsApplier) return
|
|
128
|
+
if (name.includes('.')) return
|
|
129
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
130
|
+
const fs = fieldStateRef.current
|
|
131
|
+
if (fs.controlled) {
|
|
132
|
+
fs.setValue(suggestion.suggestedValue)
|
|
133
|
+
} else {
|
|
134
|
+
applyToUncontrolledInputs(name, suggestion.suggestedValue)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
138
|
+
}, [isRichText, ownsApplier, name, formId])
|
|
139
|
+
|
|
44
140
|
const labelClass = hiddenLabel
|
|
45
141
|
? 'sr-only'
|
|
46
142
|
: 'text-sm font-medium leading-none'
|
|
@@ -64,12 +160,29 @@ export function FieldShell({ el, name, label, required, children, before, after,
|
|
|
64
160
|
)
|
|
65
161
|
: children
|
|
66
162
|
|
|
163
|
+
// When a suggestion is pending we hide the real input visually but
|
|
164
|
+
// keep it in the DOM — both so the applier's DOM-write fallback can
|
|
165
|
+
// resolve `[name="…"]` and so the input doesn't unmount + lose its
|
|
166
|
+
// typed value. The wrapper div is rendered unconditionally (just
|
|
167
|
+
// toggling a class) — switching between bare input and wrapped input
|
|
168
|
+
// would unmount the uncontrolled `<input>`, resetting its value to
|
|
169
|
+
// `defaultValue` and silently undoing the approved write right after
|
|
170
|
+
// the overlay closes.
|
|
171
|
+
const inputBlock = (
|
|
172
|
+
<>
|
|
173
|
+
<div className={overlayNode !== null ? 'hidden' : 'contents'} aria-hidden={overlayNode !== null}>
|
|
174
|
+
{input}
|
|
175
|
+
</div>
|
|
176
|
+
{overlayNode}
|
|
177
|
+
</>
|
|
178
|
+
)
|
|
179
|
+
|
|
67
180
|
if (inline) {
|
|
68
181
|
return (
|
|
69
182
|
<div className="flex items-baseline gap-3" {...wrapperAttrs}>
|
|
70
183
|
{labelEl && <div className="min-w-32 pt-2">{labelEl}</div>}
|
|
71
184
|
<div className="min-w-0 flex-1">
|
|
72
|
-
{
|
|
185
|
+
{inputBlock}
|
|
73
186
|
{helperText && (
|
|
74
187
|
<p className="mt-1 text-xs text-muted-foreground">{helperText}</p>
|
|
75
188
|
)}
|
|
@@ -81,7 +194,7 @@ export function FieldShell({ el, name, label, required, children, before, after,
|
|
|
81
194
|
return (
|
|
82
195
|
<div className="flex flex-col gap-1.5" {...wrapperAttrs}>
|
|
83
196
|
{labelEl}
|
|
84
|
-
{
|
|
197
|
+
{inputBlock}
|
|
85
198
|
{helperText && (
|
|
86
199
|
<p className="text-xs text-muted-foreground">{helperText}</p>
|
|
87
200
|
)}
|
|
@@ -89,6 +202,57 @@ export function FieldShell({ el, name, label, required, children, before, after,
|
|
|
89
202
|
)
|
|
90
203
|
}
|
|
91
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Best-effort DOM apply for forms without a `<FormStateProvider>` (i.e.
|
|
207
|
+
* forms whose fields aren't `live()`). Walks every `[name="…"]` input,
|
|
208
|
+
* uses React's internal value-setter (`Object.getOwnPropertyDescriptor`)
|
|
209
|
+
* so the change is visible to `onChange` handlers + uncontrolled
|
|
210
|
+
* `defaultValue` paths. Coercion is intentionally minimal — `String()`
|
|
211
|
+
* for primitives, `JSON.stringify` for objects/arrays.
|
|
212
|
+
*
|
|
213
|
+
* Used by FieldShell's pending-suggestion applier and exposed for
|
|
214
|
+
* plugins that need the same fallback.
|
|
215
|
+
*/
|
|
216
|
+
function applyToUncontrolledInputs(fieldName: string, value: unknown): void {
|
|
217
|
+
if (typeof document === 'undefined') return
|
|
218
|
+
const stringValue = typeof value === 'string'
|
|
219
|
+
? value
|
|
220
|
+
: typeof value === 'number' || typeof value === 'boolean'
|
|
221
|
+
? String(value)
|
|
222
|
+
: safeStringify(value)
|
|
223
|
+
|
|
224
|
+
const elements = document.getElementsByName(fieldName)
|
|
225
|
+
for (const el of Array.from(elements)) {
|
|
226
|
+
if (el instanceof HTMLInputElement) {
|
|
227
|
+
if (el.type === 'checkbox') {
|
|
228
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked')?.set
|
|
229
|
+
setter?.call(el, Boolean(value))
|
|
230
|
+
} else {
|
|
231
|
+
setNativeValue(HTMLInputElement.prototype, el, stringValue)
|
|
232
|
+
}
|
|
233
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
234
|
+
setNativeValue(HTMLTextAreaElement.prototype, el, stringValue)
|
|
235
|
+
} else if (el instanceof HTMLSelectElement) {
|
|
236
|
+
setNativeValue(HTMLSelectElement.prototype, el, stringValue)
|
|
237
|
+
} else {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
241
|
+
el.dispatchEvent(new Event('change', { bubbles: true }))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function setNativeValue(proto: object, el: HTMLElement, value: string): void {
|
|
246
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
|
|
247
|
+
desc?.set?.call(el, value)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function safeStringify(value: unknown): string {
|
|
251
|
+
if (value === null || value === undefined) return ''
|
|
252
|
+
try { return JSON.stringify(value) ?? '' }
|
|
253
|
+
catch { return '' }
|
|
254
|
+
}
|
|
255
|
+
|
|
92
256
|
/**
|
|
93
257
|
* Merge `extraAttributes` (Filament-parity short name) and
|
|
94
258
|
* `extraFieldWrapperAttributes` (verbose alias) into one record. Latter
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
UploadIcon, XIcon, FileIcon, Loader2Icon,
|
|
4
4
|
GripVerticalIcon, DownloadIcon,
|
|
@@ -7,7 +7,8 @@ import ReactCrop, {
|
|
|
7
7
|
type Crop, type PixelCrop,
|
|
8
8
|
centerCrop, makeAspectCrop, convertToPixelCrop,
|
|
9
9
|
} from 'react-image-crop'
|
|
10
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
10
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
11
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
11
12
|
import { useToast } from '../Toaster.js'
|
|
12
13
|
import { Button } from '../ui/button.js'
|
|
13
14
|
import {
|
|
@@ -104,6 +105,24 @@ export function FileUploadInput({
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// Cross-tree applier — FileUpload state lives in `urls` (React); the
|
|
109
|
+
// hidden mirror input is write-only. FieldShell skips its generic
|
|
110
|
+
// registration for fieldType === 'fileUpload'.
|
|
111
|
+
const fsRef = useRef(fs)
|
|
112
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
113
|
+
const formId = useContext(FormIdContext) || undefined
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (name.includes('.')) return
|
|
116
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
117
|
+
const next = toUrls(suggestion.suggestedValue)
|
|
118
|
+
const stored = multiple ? next : (next[0] ?? null)
|
|
119
|
+
const cur = fsRef.current
|
|
120
|
+
if (cur.controlled) { cur.setValue(stored); cur.triggerLive(stored) }
|
|
121
|
+
else { setLocalUrls(next); cur.triggerLive(stored) }
|
|
122
|
+
}
|
|
123
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
124
|
+
}, [name, formId, multiple])
|
|
125
|
+
|
|
107
126
|
// ── Image editor helpers ──────────────────────────────────────────────────
|
|
108
127
|
|
|
109
128
|
const onImgLoad = (e: React.SyntheticEvent<HTMLImageElement>): void => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Trash2Icon, PlusIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
|
3
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
3
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
4
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
4
5
|
import { Input } from '../ui/input.js'
|
|
5
6
|
import { Button } from '../ui/button.js'
|
|
6
7
|
|
|
@@ -70,6 +71,35 @@ export function KeyValueInput({
|
|
|
70
71
|
const addRow = (): void => {
|
|
71
72
|
setRows([...rows, { id: newId(), key: '', value: '' }])
|
|
72
73
|
}
|
|
74
|
+
// Cross-tree applier — KeyValue rows are React-controlled; the JSON in
|
|
75
|
+
// the hidden input below is a write-only serialization. FieldShell skips
|
|
76
|
+
// its generic registration for fieldType === 'keyValue'.
|
|
77
|
+
const fsRef = useRef(fs)
|
|
78
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
79
|
+
const localRowsRef = useRef(localRows)
|
|
80
|
+
useEffect(() => { localRowsRef.current = localRows }, [localRows])
|
|
81
|
+
const formId = useContext(FormIdContext) || undefined
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (name.includes('.')) return
|
|
84
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
85
|
+
const obj = parseToObject(suggestion.suggestedValue)
|
|
86
|
+
const entries = Object.entries(obj)
|
|
87
|
+
const fallback = localRowsRef.current
|
|
88
|
+
const nextRows: Row[] = entries.length > 0
|
|
89
|
+
? entries.map(([k, v], i) => ({ id: fallback[i]?.id ?? newId(), key: k, value: v }))
|
|
90
|
+
: [{ id: newId(), key: '', value: '' }]
|
|
91
|
+
const cur = fsRef.current
|
|
92
|
+
if (cur.controlled) {
|
|
93
|
+
cur.setValue(obj)
|
|
94
|
+
setLocalRows(nextRows)
|
|
95
|
+
} else {
|
|
96
|
+
setLocalRows(nextRows)
|
|
97
|
+
}
|
|
98
|
+
cur.triggerLive(obj)
|
|
99
|
+
}
|
|
100
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
101
|
+
}, [name, formId])
|
|
102
|
+
|
|
73
103
|
const moveRow = (id: number, dir: -1 | 1): void => {
|
|
74
104
|
const idx = rows.findIndex(r => r.id === id)
|
|
75
105
|
if (idx < 0) return
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Single-choice field rendered as a vertical (or `inline:true` horizontal)
|
|
@@ -24,6 +25,26 @@ export function RadioInput({
|
|
|
24
25
|
if (fs.controlled) { fs.setValue(next); fs.triggerLive(next) }
|
|
25
26
|
else { setLocalValue(next); fs.triggerLive(next) }
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
// Cross-tree applier — the visible radios live under a
|
|
30
|
+
// `${name}__radio`-named group (separate from the `[name]` hidden
|
|
31
|
+
// mirror), and they're React-controlled via `checked={value === o.value}`.
|
|
32
|
+
// FieldShell skips its generic registration for fieldType === 'radio'.
|
|
33
|
+
const fsRef = useRef(fs)
|
|
34
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
35
|
+
const formId = useContext(FormIdContext) || undefined
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (name.includes('.')) return
|
|
38
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
39
|
+
const v = suggestion.suggestedValue
|
|
40
|
+
const next = v == null ? '' : String(v)
|
|
41
|
+
const cur = fsRef.current
|
|
42
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
43
|
+
else { setLocalValue(next); cur.triggerLive(next) }
|
|
44
|
+
}
|
|
45
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
46
|
+
}, [name, formId])
|
|
47
|
+
|
|
27
48
|
const layout = inline ? 'flex flex-row flex-wrap gap-4' : 'flex flex-col gap-2'
|
|
28
49
|
return (
|
|
29
50
|
<div role="radiogroup" className={layout}>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { PlusIcon } from 'lucide-react'
|
|
3
3
|
import type { ElementMeta } from '../../schema/Element.js'
|
|
4
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
4
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
5
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
5
6
|
import { useToast } from '../Toaster.js'
|
|
6
7
|
import { renderFormChild } from '../SchemaRenderer.js'
|
|
7
8
|
import {
|
|
@@ -74,6 +75,28 @@ export function SelectFieldInput({
|
|
|
74
75
|
else { setLocalValue(option.value); fs.triggerLive(option.value) }
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
// Cross-tree applier registration. FieldShell's generic applier writes
|
|
79
|
+
// to the matching `[name]` input via the React prototype-descriptor
|
|
80
|
+
// setter — but Base UI Select isn't driven by the hidden `<input>` in
|
|
81
|
+
// this component, it's driven by `value`/`localValue` React state. So
|
|
82
|
+
// a DOM write moves the hidden input but leaves the visible select
|
|
83
|
+
// unchanged. Register a Select-aware applier that writes to local
|
|
84
|
+
// state instead. FieldShell skips the generic registration for
|
|
85
|
+
// fieldType === 'select' so this one stays the winner.
|
|
86
|
+
const fsRef = useRef(fs)
|
|
87
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
88
|
+
const formId = useContext(FormIdContext) || undefined
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (name.includes('.')) return
|
|
91
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
92
|
+
const next = suggestion.suggestedValue == null ? '' : String(suggestion.suggestedValue)
|
|
93
|
+
const cur = fsRef.current
|
|
94
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
95
|
+
else { setLocalValue(next); cur.triggerLive(next) }
|
|
96
|
+
}
|
|
97
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
98
|
+
}, [name, formId])
|
|
99
|
+
|
|
77
100
|
const showCreateTrigger = createOption !== undefined
|
|
78
101
|
&& typeof createOption.url === 'string'
|
|
79
102
|
&& createOption.url.length > 0
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
import { Slider } from '../ui/slider.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -40,6 +41,23 @@ export function SliderInput({
|
|
|
40
41
|
else { setLocalValue(v); fs.triggerLive(v) }
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Cross-tree applier — Base UI Slider drives via the `value` prop, not
|
|
45
|
+
// the hidden mirror input below. FieldShell skips its generic registration
|
|
46
|
+
// for fieldType === 'slider'.
|
|
47
|
+
const fsRef = useRef(fs)
|
|
48
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
49
|
+
const formId = useContext(FormIdContext) || undefined
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (name.includes('.')) return
|
|
52
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
53
|
+
const v = toNumber(suggestion.suggestedValue)
|
|
54
|
+
const cur = fsRef.current
|
|
55
|
+
if (cur.controlled) { cur.setValue(v); cur.triggerLive(v) }
|
|
56
|
+
else { setLocalValue(v); cur.triggerLive(v) }
|
|
57
|
+
}
|
|
58
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
59
|
+
}, [name, formId])
|
|
60
|
+
|
|
43
61
|
return (
|
|
44
62
|
<div className="flex items-center gap-3">
|
|
45
63
|
<input type="hidden" name={name} value={String(value)} />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { useMemo, useRef, useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { GripVerticalIcon, XIcon } from 'lucide-react'
|
|
3
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
3
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
4
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
4
5
|
import { reorderRows } from './RepeaterInput.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -48,6 +49,23 @@ export function TagsInput({
|
|
|
48
49
|
else { setLocalTags(next); fs.triggerLive(next) }
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Cross-tree applier — chip set lives in React; hidden mirror is a
|
|
53
|
+
// write-only JSON serialization. FieldShell skips its generic
|
|
54
|
+
// registration for fieldType === 'tagsInput'.
|
|
55
|
+
const fsRef = useRef(fs)
|
|
56
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
57
|
+
const formId = useContext(FormIdContext) || undefined
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (name.includes('.')) return
|
|
60
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
61
|
+
const next = toArray(suggestion.suggestedValue)
|
|
62
|
+
const cur = fsRef.current
|
|
63
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
64
|
+
else { setLocalTags(next); cur.triggerLive(next) }
|
|
65
|
+
}
|
|
66
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
67
|
+
}, [name, formId])
|
|
68
|
+
|
|
51
69
|
const canAddMore = maxTags == null || tags.length < maxTags
|
|
52
70
|
|
|
53
71
|
const addTag = (raw: string): void => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { useFieldState } from '../FormStateContext.js'
|
|
1
|
+
import React, { useContext, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFieldState, FormIdContext } from '../FormStateContext.js'
|
|
3
|
+
import { registerPendingSuggestionApplier, type PendingSuggestionApplier } from '../PendingSuggestionApplierRegistry.js'
|
|
3
4
|
import { Switch } from '../ui/switch.js'
|
|
4
5
|
|
|
5
6
|
export function ToggleFieldInput({
|
|
@@ -21,6 +22,26 @@ export function ToggleFieldInput({
|
|
|
21
22
|
if (fs.controlled) { fs.setValue(next); fs.triggerLive(next) }
|
|
22
23
|
else { setLocalChecked(next); fs.triggerLive(next) }
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
// Cross-tree applier — Switch state lives in React, not in the hidden
|
|
27
|
+
// mirror input below. FieldShell's generic DOM-write applier would
|
|
28
|
+
// dispatch a change on the hidden input, but the visible Switch has
|
|
29
|
+
// no listener for it, so the toggle wouldn't flip. FieldShell skips
|
|
30
|
+
// its generic registration for fieldType === 'toggle'.
|
|
31
|
+
const fsRef = useRef(fs)
|
|
32
|
+
useEffect(() => { fsRef.current = fs }, [fs])
|
|
33
|
+
const formId = useContext(FormIdContext) || undefined
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (name.includes('.')) return
|
|
36
|
+
const applier: PendingSuggestionApplier = (suggestion) => {
|
|
37
|
+
const v = suggestion.suggestedValue
|
|
38
|
+
const next = v === true || v === 'true' || v === 1 || v === '1'
|
|
39
|
+
const cur = fsRef.current
|
|
40
|
+
if (cur.controlled) { cur.setValue(next); cur.triggerLive(next) }
|
|
41
|
+
else { setLocalChecked(next); cur.triggerLive(next) }
|
|
42
|
+
}
|
|
43
|
+
return registerPendingSuggestionApplier(formId, name, applier)
|
|
44
|
+
}, [name, formId])
|
|
24
45
|
return (
|
|
25
46
|
<div className="flex items-center gap-2">
|
|
26
47
|
<input type="hidden" name={name} value={checked ? 'true' : 'false'} />
|
package/src/react/index.ts
CHANGED
|
@@ -14,6 +14,24 @@ export {
|
|
|
14
14
|
} from './SchemaRenderer.js'
|
|
15
15
|
export { registerFieldRenderer, getFieldRenderer, type FieldRendererProps } from './registry.js'
|
|
16
16
|
export { registerFieldLabelSlot, getFieldLabelSlot, type FieldLabelSlotProps } from './FieldLabelSlotRegistry.js'
|
|
17
|
+
export {
|
|
18
|
+
registerPendingSuggestionOverlay,
|
|
19
|
+
getPendingSuggestionOverlay,
|
|
20
|
+
type PendingSuggestionOverlayProps,
|
|
21
|
+
} from './PendingSuggestionOverlayRegistry.js'
|
|
22
|
+
export {
|
|
23
|
+
PendingSuggestionsContext,
|
|
24
|
+
usePendingSuggestions,
|
|
25
|
+
usePendingSuggestionsForField,
|
|
26
|
+
type PendingSuggestion,
|
|
27
|
+
type PendingSuggestionOrigin,
|
|
28
|
+
type PendingSuggestionsApi,
|
|
29
|
+
} from './PendingSuggestionsContext.js'
|
|
30
|
+
export {
|
|
31
|
+
registerPendingSuggestionApplier,
|
|
32
|
+
getPendingSuggestionApplier,
|
|
33
|
+
type PendingSuggestionApplier,
|
|
34
|
+
} from './PendingSuggestionApplierRegistry.js'
|
|
17
35
|
export {
|
|
18
36
|
registerWidgetRenderer,
|
|
19
37
|
getWidgetRenderer,
|