@pilotiq/pilotiq 0.12.0 → 0.13.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 +2 -2
- package/CHANGELOG.md +13 -0
- package/dist/react/FormCollabBindingRegistry.d.ts +17 -98
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
- package/dist/react/FormCollabBindingRegistry.js.map +1 -1
- package/dist/react/FormStateContext.d.ts +1 -35
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +7 -91
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/RowCoordsContext.d.ts +19 -0
- package/dist/react/RowCoordsContext.d.ts.map +1 -0
- package/dist/react/RowCoordsContext.js +6 -0
- package/dist/react/RowCoordsContext.js.map +1 -0
- package/dist/react/fields/BuilderInput.d.ts.map +1 -1
- package/dist/react/fields/BuilderInput.js +14 -9
- package/dist/react/fields/BuilderInput.js.map +1 -1
- package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
- package/dist/react/fields/MarkdownInput.js +35 -125
- package/dist/react/fields/MarkdownInput.js.map +1 -1
- package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
- package/dist/react/fields/RepeaterInput.js +26 -17
- package/dist/react/fields/RepeaterInput.js.map +1 -1
- package/dist/react/fields/TextLikeInput.d.ts +11 -9
- package/dist/react/fields/TextLikeInput.d.ts.map +1 -1
- package/dist/react/fields/TextLikeInput.js +59 -189
- package/dist/react/fields/TextLikeInput.js.map +1 -1
- package/dist/react/formStateHelpers.d.ts +0 -15
- package/dist/react/formStateHelpers.d.ts.map +1 -1
- package/dist/react/formStateHelpers.js +0 -91
- package/dist/react/formStateHelpers.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/react/FormCollabBindingRegistry.ts +17 -91
- package/src/react/FormStateContext.tsx +6 -125
- package/src/react/RowCoordsContext.tsx +23 -0
- package/src/react/fields/BuilderInput.tsx +22 -10
- package/src/react/fields/MarkdownInput.tsx +42 -129
- package/src/react/fields/RepeaterInput.tsx +41 -16
- package/src/react/fields/TextLikeInput.tsx +67 -225
- package/src/react/formStateHelpers.test.ts +0 -99
- package/src/react/formStateHelpers.ts +0 -83
- package/src/react/index.ts +0 -2
- package/dist/react/fields/textDelta.d.ts +0 -44
- package/dist/react/fields/textDelta.d.ts.map +0 -1
- package/dist/react/fields/textDelta.js +0 -80
- package/dist/react/fields/textDelta.js.map +0 -1
- package/src/react/fields/textDelta.test.ts +0 -141
- package/src/react/fields/textDelta.ts +0 -86
|
@@ -5,6 +5,7 @@ import { Button } from '../ui/button.js'
|
|
|
5
5
|
import { SchemaRenderer } from '../SchemaRenderer.js'
|
|
6
6
|
import { FormIdContext, useFormState, useRowBinding } from '../FormStateContext.js'
|
|
7
7
|
import { findFieldMeta } from '../formStateHelpers.js'
|
|
8
|
+
import { RowCoordsContext } from '../RowCoordsContext.js'
|
|
8
9
|
import { useIconFor } from '../icon-context.js'
|
|
9
10
|
import { reorderRows, ExtraActionStrip, buildGridContainer } from './RepeaterInput.js'
|
|
10
11
|
import { syncRowGates } from './syncRowGates.js'
|
|
@@ -191,11 +192,9 @@ export function BuilderInput({
|
|
|
191
192
|
// the first event — without it, the picker dropdown choice doesn't
|
|
192
193
|
// propagate until the user makes their first inner-field edit.
|
|
193
194
|
const rowBinding = useRowBinding(name)
|
|
194
|
-
//
|
|
195
|
-
// row
|
|
196
|
-
//
|
|
197
|
-
// Without this stamp the F.5c per-row Y.Text path stays null on Builder
|
|
198
|
-
// and inner text fields never sync. Mirrors the same fix in RepeaterInput.
|
|
195
|
+
// Mirror row identities into the form's values map so dotted row-leaf
|
|
196
|
+
// consumers can resolve the row's `__id` via `rowIdAtIndex(ctx.values,
|
|
197
|
+
// name, i)`. Mirrors the same plumbing in RepeaterInput.
|
|
199
198
|
const formStateForIds = useFormState()
|
|
200
199
|
const ctxSetValue = formStateForIds?.setValue
|
|
201
200
|
useEffect(() => {
|
|
@@ -870,6 +869,15 @@ function BuilderRow({
|
|
|
870
869
|
() => row.children.map(c => prefixFieldNames(c, dataPrefix)),
|
|
871
870
|
[row.children, dataPrefix],
|
|
872
871
|
)
|
|
872
|
+
// Row coords for dotted-path text leaves under this row — composes
|
|
873
|
+
// fragment-key `${arrayName}.${rowId}.${fieldName}` (Phase 1 of
|
|
874
|
+
// collab-row-text-tiptap-backed.md). `parseRowFieldPath` strips the
|
|
875
|
+
// Builder-specific `data` segment, so the coords use the array name +
|
|
876
|
+
// the row's stable id without referencing the dialect.
|
|
877
|
+
const rowCoords = useMemo(
|
|
878
|
+
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
879
|
+
[name, index, row.id],
|
|
880
|
+
)
|
|
873
881
|
|
|
874
882
|
const RowIcon = useIconFor(showIcons ? block?.icon : undefined)
|
|
875
883
|
const blockLabel = block?.label ?? row.type ?? 'Block'
|
|
@@ -878,11 +886,13 @@ function BuilderRow({
|
|
|
878
886
|
|
|
879
887
|
if (row.hidden) {
|
|
880
888
|
return (
|
|
881
|
-
<
|
|
882
|
-
<
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
889
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
890
|
+
<div style={{ display: 'none' }} data-pilotiq-builder-row="hidden">
|
|
891
|
+
<input type="hidden" name={`${name}.${index}.__id`} value={row.id} readOnly />
|
|
892
|
+
<input type="hidden" name={`${name}.${index}.type`} value={row.type} readOnly />
|
|
893
|
+
<SchemaRenderer elements={namespaced} />
|
|
894
|
+
</div>
|
|
895
|
+
</RowCoordsContext.Provider>
|
|
886
896
|
)
|
|
887
897
|
}
|
|
888
898
|
|
|
@@ -921,6 +931,7 @@ function BuilderRow({
|
|
|
921
931
|
const innerColumns = block.columns && block.columns > 1 ? block.columns : 1
|
|
922
932
|
|
|
923
933
|
return (
|
|
934
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
924
935
|
<div
|
|
925
936
|
className={`rounded-md border bg-card transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
926
937
|
data-pilotiq-builder-row=""
|
|
@@ -999,6 +1010,7 @@ function BuilderRow({
|
|
|
999
1010
|
: <SchemaRenderer elements={namespaced} />}
|
|
1000
1011
|
</div>
|
|
1001
1012
|
</div>
|
|
1013
|
+
</RowCoordsContext.Provider>
|
|
1002
1014
|
)
|
|
1003
1015
|
}
|
|
1004
1016
|
|
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
import { useFieldState } from '../FormStateContext.js'
|
|
9
9
|
import { useCollabRoom } from '../CollabRoomContext.js'
|
|
10
10
|
import { getCollabTextRenderer, type CollabTextRenderer } from '../CollabTextRendererRegistry.js'
|
|
11
|
+
import { useRowCoords } from '../RowCoordsContext.js'
|
|
12
|
+
import { parseRowFieldPath } from '../formStateHelpers.js'
|
|
11
13
|
import { useToast } from '../Toaster.js'
|
|
12
14
|
import { Button } from '../ui/button.js'
|
|
13
|
-
import { computeDelta, preserveCursor } from './textDelta.js'
|
|
14
15
|
|
|
15
16
|
type ToolbarButton =
|
|
16
17
|
| 'bold' | 'italic' | 'strike' | 'link'
|
|
@@ -50,24 +51,37 @@ export function MarkdownInput({
|
|
|
50
51
|
const fs = useFieldState(name)
|
|
51
52
|
const room = useCollabRoom()
|
|
52
53
|
const collabRenderer = getCollabTextRenderer()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// when collab is on.
|
|
56
|
-
//
|
|
57
|
-
//
|
|
54
|
+
const rowCoords = useRowCoords()
|
|
55
|
+
|
|
56
|
+
// Tiptap-backed plain-text editor for markdown source when collab is on.
|
|
57
|
+
// Same architectural fix as `TextLikeInput`'s `CollabTextField`:
|
|
58
|
+
// y-prosemirror's `RelativePosition` cursor anchoring against a
|
|
59
|
+
// `Y.XmlFragment` replaces whole-string LWW. Row leaves get the
|
|
60
|
+
// composite-key transform via `useRowCoords()` + `parseRowFieldPath`
|
|
61
|
+
// (same shape as TextLikeInput) so the fragment survives row reorders.
|
|
58
62
|
//
|
|
59
|
-
// Tradeoff: the markdown toolbar + Cmd-shortcuts + paste-image upload
|
|
60
|
-
// operate on a `<textarea>`'s DOM selection — they don't have a
|
|
61
|
-
// reach into the Tiptap editor's selection without exposing the
|
|
62
|
-
// instance, which would widen the renderer seam.
|
|
63
|
+
// Tradeoff: the markdown toolbar + Cmd-shortcuts + paste-image upload
|
|
64
|
+
// all operate on a `<textarea>`'s DOM selection — they don't have a
|
|
65
|
+
// way to reach into the Tiptap editor's selection without exposing the
|
|
66
|
+
// editor instance, which would widen the renderer seam. Those features
|
|
63
67
|
// are write-mode-only on the native path; collab users type markdown
|
|
64
68
|
// syntax directly (`**bold**`, `## heading`). The preview tab keeps
|
|
65
|
-
// working since
|
|
66
|
-
|
|
69
|
+
// working since `MarkdownCollabInput` maintains a local mirror.
|
|
70
|
+
const fragmentKey: string | null = (() => {
|
|
71
|
+
if (!name.includes('.')) return name
|
|
72
|
+
if (!rowCoords) return null
|
|
73
|
+
const parsed = parseRowFieldPath(name)
|
|
74
|
+
if (!parsed) return null
|
|
75
|
+
if (parsed.arrayName !== rowCoords.arrayName) return null
|
|
76
|
+
if (parsed.index !== rowCoords.rowIndex) return null
|
|
77
|
+
return `${rowCoords.arrayName}.${rowCoords.rowId}.${parsed.fieldName}`
|
|
78
|
+
})()
|
|
79
|
+
if (room && collabRenderer && fragmentKey !== null) {
|
|
67
80
|
return (
|
|
68
81
|
<MarkdownCollabInput
|
|
69
82
|
Renderer={collabRenderer}
|
|
70
|
-
|
|
83
|
+
fragmentKey={fragmentKey}
|
|
84
|
+
hiddenInputName={name}
|
|
71
85
|
defaultValue={defaultValue}
|
|
72
86
|
disabled={disabled}
|
|
73
87
|
{...(placeholder !== undefined ? { placeholder } : {})}
|
|
@@ -79,100 +93,15 @@ export function MarkdownInput({
|
|
|
79
93
|
|
|
80
94
|
const { notify } = useToast()
|
|
81
95
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
82
|
-
// Phase F.6 — IME composition gate. Set between `compositionstart` /
|
|
83
|
-
// `compositionend`; the textarea's onChange skips `applyDelta` while
|
|
84
|
-
// composing so intermediate chars don't emit ops. Lives at the
|
|
85
|
-
// component scope so the onChange and composition handlers share it.
|
|
86
|
-
const isComposingRef = useRef<boolean>(false)
|
|
87
96
|
|
|
88
97
|
const initial = useMemo(() => stringValue(defaultValue), [])
|
|
89
98
|
const [localValue, setLocalValue] = useState<string>(initial)
|
|
90
99
|
const [tab, setTab] = useState<'write' | 'preview'>('write')
|
|
91
100
|
const [busy, setBusy] = useState(false)
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
// a `TextBinding`, the textarea is bound to a `Y.Text` and edits emit
|
|
95
|
-
// `TextDelta`s. Mirrors the architecture in `TextLikeInput.tsx` but
|
|
96
|
-
// wired in-line because MarkdownInput has its own toolbar + Preview
|
|
97
|
-
// tab that also need to flow through the binding.
|
|
98
|
-
const binding = fs.textBinding
|
|
99
|
-
const [boundValue, setBoundValue] = useState<string>(() => binding?.read() ?? initial)
|
|
100
|
-
const boundValueRef = useRef<string>(boundValue)
|
|
101
|
-
useEffect(() => { boundValueRef.current = boundValue }, [boundValue])
|
|
102
|
-
|
|
103
|
-
// On binding swap: read current Y.Text state. If non-empty, lift it
|
|
104
|
-
// into local + form-map state. If empty (no peer has typed yet), leave
|
|
105
|
-
// the SSR-default-derived `boundValue` showing — first edit will
|
|
106
|
-
// emit a replace-from-empty delta that atomically populates Y.Text.
|
|
107
|
-
// No client-side seed: Y.Text isn't safe to seed under concurrent
|
|
108
|
-
// first-mounters (see @pilotiq-pro/collab `formCollabBinding.ts`).
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!binding) return
|
|
111
|
-
const next = binding.read()
|
|
112
|
-
if (next.length > 0) {
|
|
113
|
-
setBoundValue(next)
|
|
114
|
-
boundValueRef.current = next
|
|
115
|
-
fs.setValue(next)
|
|
116
|
-
}
|
|
117
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
-
}, [binding])
|
|
119
|
-
|
|
120
|
-
// Subscribe to remote changes. Local-echoes are filtered by the
|
|
121
|
-
// `next === prev` guard. Cursor preserved via the same heuristic
|
|
122
|
-
// used in `TextLikeInput.BoundTextInput`.
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!binding) return
|
|
125
|
-
return binding.observe((next) => {
|
|
126
|
-
const prev = boundValueRef.current
|
|
127
|
-
if (next === prev) return
|
|
128
|
-
const ta = textareaRef.current
|
|
129
|
-
const cursor = ta?.selectionStart ?? next.length
|
|
130
|
-
const restored = preserveCursor(prev, next, cursor)
|
|
131
|
-
setBoundValue(next)
|
|
132
|
-
boundValueRef.current = next
|
|
133
|
-
fs.setValue(next)
|
|
134
|
-
requestAnimationFrame(() => {
|
|
135
|
-
if (!ta) return
|
|
136
|
-
if (document.activeElement !== ta) return
|
|
137
|
-
try { ta.setSelectionRange(restored, restored) } catch { /* defensive */ }
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
141
|
-
}, [binding])
|
|
142
|
-
|
|
143
|
-
const value = binding
|
|
144
|
-
? boundValue
|
|
145
|
-
: (fs.controlled ? stringValue(fs.value) : localValue)
|
|
102
|
+
const value = fs.controlled ? stringValue(fs.value) : localValue
|
|
146
103
|
|
|
147
104
|
const setValue = (next: string): void => {
|
|
148
|
-
if (binding) {
|
|
149
|
-
// Compute against current Y.Text contents (not the local ref) so:
|
|
150
|
-
// - first edit against empty Y.Text → `insert@0 <whole>` atomic
|
|
151
|
-
// populate (no separate seed op needed);
|
|
152
|
-
// - after a remote-applied update or server-resolve replace, the
|
|
153
|
-
// delta reflects the actual current shared state, not stale
|
|
154
|
-
// local bookkeeping.
|
|
155
|
-
const before = binding.read()
|
|
156
|
-
if (next !== before) {
|
|
157
|
-
const delta = computeDelta(before, next)
|
|
158
|
-
// Pre-stamp `boundValueRef.current = next` BEFORE `applyDelta`.
|
|
159
|
-
// Y.Text's `observe` fires synchronously inside `applyDelta` for
|
|
160
|
-
// our own write; without this the observer would see
|
|
161
|
-
// `prev=before, next=after` and run `preserveCursor` — designed
|
|
162
|
-
// for *remote* edits — which clobbers the user's caret on local
|
|
163
|
-
// typing (scrambled output on mid-string inserts). With
|
|
164
|
-
// `boundValueRef` already at `next`, the observer's
|
|
165
|
-
// `next === prev` short-circuit fires and the cursor is left
|
|
166
|
-
// alone for local echoes. Mirror of the same fix in
|
|
167
|
-
// `BoundTextInput.commitDelta`.
|
|
168
|
-
boundValueRef.current = next
|
|
169
|
-
if (delta) binding.applyDelta(delta)
|
|
170
|
-
setBoundValue(next)
|
|
171
|
-
}
|
|
172
|
-
fs.setValue(next)
|
|
173
|
-
fs.triggerLive(next)
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
105
|
if (fs.controlled) { fs.setValue(next); fs.triggerLive(next) }
|
|
177
106
|
else { setLocalValue(next); fs.triggerLive(next) }
|
|
178
107
|
}
|
|
@@ -405,24 +334,7 @@ export function MarkdownInput({
|
|
|
405
334
|
{...(fs.controlled
|
|
406
335
|
? {
|
|
407
336
|
value,
|
|
408
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
409
|
-
// Phase F.6 — when the binding is active and the user
|
|
410
|
-
// is mid-IME, paint locally and hold the delta until
|
|
411
|
-
// compositionend so we never emit ops for the
|
|
412
|
-
// intermediate composing chars.
|
|
413
|
-
if (binding && isComposingRef.current) {
|
|
414
|
-
setBoundValue(e.target.value)
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
setValue(e.target.value)
|
|
418
|
-
},
|
|
419
|
-
...(binding ? {
|
|
420
|
-
onCompositionStart: () => { isComposingRef.current = true },
|
|
421
|
-
onCompositionEnd: (e: React.CompositionEvent<HTMLTextAreaElement>) => {
|
|
422
|
-
isComposingRef.current = false
|
|
423
|
-
setValue(e.currentTarget.value)
|
|
424
|
-
},
|
|
425
|
-
} : {}),
|
|
337
|
+
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value),
|
|
426
338
|
}
|
|
427
339
|
: { defaultValue: initial, onChange: (e) => setLocalValue(e.target.value) })}
|
|
428
340
|
onPaste={onPaste}
|
|
@@ -472,17 +384,18 @@ function TabButton({ active, onClick, children }: {
|
|
|
472
384
|
* load-bearing change; markdown-syntax authors keep typing as before.
|
|
473
385
|
*/
|
|
474
386
|
function MarkdownCollabInput({
|
|
475
|
-
Renderer,
|
|
387
|
+
Renderer, fragmentKey, hiddenInputName, defaultValue, disabled, placeholder, minHeight, maxHeight,
|
|
476
388
|
}: {
|
|
477
|
-
Renderer:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
389
|
+
Renderer: CollabTextRenderer
|
|
390
|
+
fragmentKey: string
|
|
391
|
+
hiddenInputName: string
|
|
392
|
+
defaultValue: unknown
|
|
393
|
+
disabled: boolean
|
|
394
|
+
placeholder?: string
|
|
395
|
+
minHeight?: string
|
|
396
|
+
maxHeight?: string
|
|
484
397
|
}): React.ReactElement {
|
|
485
|
-
const fs = useFieldState(
|
|
398
|
+
const fs = useFieldState(hiddenInputName)
|
|
486
399
|
const initial = useMemo(() => stringValue(defaultValue), [])
|
|
487
400
|
const [text, setText] = useState<string>(initial)
|
|
488
401
|
const [tab, setTab] = useState<'write' | 'preview'>('write')
|
|
@@ -513,9 +426,9 @@ function MarkdownCollabInput({
|
|
|
513
426
|
</div>
|
|
514
427
|
{tab === 'write' ? (
|
|
515
428
|
<div style={wrapperStyle} className="overflow-auto">
|
|
516
|
-
<input type="hidden" name={
|
|
429
|
+
<input type="hidden" name={hiddenInputName} value={text} />
|
|
517
430
|
<Renderer
|
|
518
|
-
name={
|
|
431
|
+
name={fragmentKey}
|
|
519
432
|
multiline={true}
|
|
520
433
|
defaultValue={initial}
|
|
521
434
|
{...(placeholder !== undefined ? { placeholder } : {})}
|
|
@@ -527,7 +440,7 @@ function MarkdownCollabInput({
|
|
|
527
440
|
</div>
|
|
528
441
|
) : (
|
|
529
442
|
<>
|
|
530
|
-
<input type="hidden" name={
|
|
443
|
+
<input type="hidden" name={hiddenInputName} value={text} readOnly />
|
|
531
444
|
<div
|
|
532
445
|
className="prose prose-sm dark:prose-invert max-w-none px-3 py-2"
|
|
533
446
|
style={wrapperStyle}
|
|
@@ -5,6 +5,7 @@ import { Button } from '../ui/button.js'
|
|
|
5
5
|
import { SchemaRenderer, dispatchHandlerAction } from '../SchemaRenderer.js'
|
|
6
6
|
import { FormIdContext, useFormState, useRowBinding } from '../FormStateContext.js'
|
|
7
7
|
import { findFieldMeta } from '../formStateHelpers.js'
|
|
8
|
+
import { RowCoordsContext } from '../RowCoordsContext.js'
|
|
8
9
|
import { useNavigate } from '../navigate.js'
|
|
9
10
|
import { useToast } from '../Toaster.js'
|
|
10
11
|
import type { RowButtonsMeta } from '../../fields/RowButton.js'
|
|
@@ -243,16 +244,14 @@ export function RepeaterInput({
|
|
|
243
244
|
// it when present so peers see the same lifecycle events; absent =
|
|
244
245
|
// today's local-only behaviour, unchanged.
|
|
245
246
|
const rowBinding = useRowBinding(name)
|
|
246
|
-
//
|
|
247
|
-
// row
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// `
|
|
253
|
-
//
|
|
254
|
-
// `formStateForIds` mirrors `formState` below; we read via `useFormState()`
|
|
255
|
-
// here too instead of forward-referencing the later binding.
|
|
247
|
+
// Mirror row identities into the form's values map so dotted row-leaf
|
|
248
|
+
// consumers can resolve the row's `__id` via `rowIdAtIndex(ctx.values,
|
|
249
|
+
// name, i)`. Setting a `__id` key routes through `routeBindingWrite` →
|
|
250
|
+
// `parseRowFieldPath` which filters `__id` → no-op on the binding side,
|
|
251
|
+
// so the only effect is a row entry landing in `valuesState`.
|
|
252
|
+
// `formStateForIds` mirrors `formState` below; we read via
|
|
253
|
+
// `useFormState()` here too instead of forward-referencing the later
|
|
254
|
+
// binding.
|
|
256
255
|
const formStateForIds = useFormState()
|
|
257
256
|
const ctxSetValue = formStateForIds?.setValue
|
|
258
257
|
useEffect(() => {
|
|
@@ -749,16 +748,25 @@ function RepeaterRow({
|
|
|
749
748
|
[row.children, prefix],
|
|
750
749
|
)
|
|
751
750
|
const headerLabel = row.itemLabel ?? `Item ${index + 1}`
|
|
751
|
+
// Row coords for dotted-path text leaves — composes fragment-key
|
|
752
|
+
// `${arrayName}.${rowId}.${fieldName}` for the Tiptap-backed collab
|
|
753
|
+
// renderer (see collab-row-text-tiptap-backed.md Phase 1).
|
|
754
|
+
const rowCoords = useMemo(
|
|
755
|
+
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
756
|
+
[name, index, row.id],
|
|
757
|
+
)
|
|
752
758
|
|
|
753
759
|
// Hidden rows: render only the inputs (and __id) inside a display:none
|
|
754
760
|
// wrapper so their values round-trip through FormData on submit. No
|
|
755
761
|
// chrome, no drag wiring, no labels — `itemHidden` is purely UX.
|
|
756
762
|
if (row.hidden) {
|
|
757
763
|
return (
|
|
758
|
-
<
|
|
759
|
-
<
|
|
760
|
-
|
|
761
|
-
|
|
764
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
765
|
+
<div style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
|
|
766
|
+
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
767
|
+
<SchemaRenderer elements={namespaced} />
|
|
768
|
+
</div>
|
|
769
|
+
</RowCoordsContext.Provider>
|
|
762
770
|
)
|
|
763
771
|
}
|
|
764
772
|
|
|
@@ -795,6 +803,7 @@ function RepeaterRow({
|
|
|
795
803
|
// wrapping in a class that hides the FieldShell's label slot.
|
|
796
804
|
if (simple) {
|
|
797
805
|
return (
|
|
806
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
798
807
|
<div
|
|
799
808
|
className={`flex items-center gap-2 transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
800
809
|
data-pilotiq-repeater-row="simple"
|
|
@@ -830,10 +839,12 @@ function RepeaterRow({
|
|
|
830
839
|
/>
|
|
831
840
|
)}
|
|
832
841
|
</div>
|
|
842
|
+
</RowCoordsContext.Provider>
|
|
833
843
|
)
|
|
834
844
|
}
|
|
835
845
|
|
|
836
846
|
return (
|
|
847
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
837
848
|
<div
|
|
838
849
|
className={`rounded-md border bg-card transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
839
850
|
data-pilotiq-repeater-row=""
|
|
@@ -910,6 +921,7 @@ function RepeaterRow({
|
|
|
910
921
|
: <SchemaRenderer elements={namespaced} />}
|
|
911
922
|
</div>
|
|
912
923
|
</div>
|
|
924
|
+
</RowCoordsContext.Provider>
|
|
913
925
|
)
|
|
914
926
|
}
|
|
915
927
|
|
|
@@ -1168,6 +1180,10 @@ function RepeaterTableRow({
|
|
|
1168
1180
|
() => row.children.map(c => prefixFieldNames(c, prefix)),
|
|
1169
1181
|
[row.children, prefix],
|
|
1170
1182
|
)
|
|
1183
|
+
const rowCoords = useMemo(
|
|
1184
|
+
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
1185
|
+
[name, index, row.id],
|
|
1186
|
+
)
|
|
1171
1187
|
|
|
1172
1188
|
if (row.hidden) {
|
|
1173
1189
|
// Render the hidden envelope as a single full-span cell so column
|
|
@@ -1175,11 +1191,18 @@ function RepeaterTableRow({
|
|
|
1175
1191
|
// still in the form's submit. Using `<tr style="display:none">`
|
|
1176
1192
|
// would warn under React strict-mode in Firefox; the wrapping cell
|
|
1177
1193
|
// keeps the markup HTML-valid.
|
|
1194
|
+
//
|
|
1195
|
+
// The provider wraps the cell rather than the `<tr>` because React's
|
|
1196
|
+
// table-row whitelisting only accepts `<th>/<td>` children, not a
|
|
1197
|
+
// context provider; the provider is a no-DOM wrapper so it sits
|
|
1198
|
+
// inside the cell fine.
|
|
1178
1199
|
return (
|
|
1179
1200
|
<tr style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
|
|
1180
1201
|
<td colSpan={columns.length + 1}>
|
|
1181
|
-
<
|
|
1182
|
-
|
|
1202
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
1203
|
+
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
1204
|
+
<SchemaRenderer elements={namespaced} />
|
|
1205
|
+
</RowCoordsContext.Provider>
|
|
1183
1206
|
</td>
|
|
1184
1207
|
</tr>
|
|
1185
1208
|
)
|
|
@@ -1209,6 +1232,7 @@ function RepeaterTableRow({
|
|
|
1209
1232
|
: {}
|
|
1210
1233
|
|
|
1211
1234
|
return (
|
|
1235
|
+
<RowCoordsContext.Provider value={rowCoords}>
|
|
1212
1236
|
<tr
|
|
1213
1237
|
className={`border-t align-top ${isDragging ? 'opacity-50' : ''}`}
|
|
1214
1238
|
data-pilotiq-repeater-row=""
|
|
@@ -1265,6 +1289,7 @@ function RepeaterTableRow({
|
|
|
1265
1289
|
</div>
|
|
1266
1290
|
</td>
|
|
1267
1291
|
</tr>
|
|
1292
|
+
</RowCoordsContext.Provider>
|
|
1268
1293
|
)
|
|
1269
1294
|
}
|
|
1270
1295
|
|