@pilotiq/pilotiq 0.11.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 +21 -0
- package/dist/react/AppShell.d.ts +1 -1
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +7 -1
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/CollabTextRendererRegistry.d.ts +75 -0
- package/dist/react/CollabTextRendererRegistry.d.ts.map +1 -0
- package/dist/react/CollabTextRendererRegistry.js +18 -0
- package/dist/react/CollabTextRendererRegistry.js.map +1 -0
- package/dist/react/CurrentUserContext.d.ts +39 -0
- package/dist/react/CurrentUserContext.d.ts.map +1 -0
- package/dist/react/CurrentUserContext.js +27 -0
- package/dist/react/CurrentUserContext.js.map +1 -0
- package/dist/react/FormCollabBindingRegistry.d.ts +17 -84
- 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 +70 -101
- 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 +111 -164
- 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 +3 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/package.json +5 -5
- package/src/react/AppShell.tsx +11 -1
- package/src/react/CollabTextRendererRegistry.ts +84 -0
- package/src/react/CurrentUserContext.tsx +50 -0
- package/src/react/FormCollabBindingRegistry.ts +17 -77
- 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 +125 -95
- package/src/react/fields/RepeaterInput.tsx +41 -16
- package/src/react/fields/TextLikeInput.tsx +147 -181
- package/src/react/formStateHelpers.test.ts +0 -99
- package/src/react/formStateHelpers.ts +0 -83
- package/src/react/index.ts +12 -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
|
@@ -6,9 +6,12 @@ import {
|
|
|
6
6
|
CodeIcon, PaperclipIcon, Loader2Icon,
|
|
7
7
|
} from 'lucide-react'
|
|
8
8
|
import { useFieldState } from '../FormStateContext.js'
|
|
9
|
+
import { useCollabRoom } from '../CollabRoomContext.js'
|
|
10
|
+
import { getCollabTextRenderer, type CollabTextRenderer } from '../CollabTextRendererRegistry.js'
|
|
11
|
+
import { useRowCoords } from '../RowCoordsContext.js'
|
|
12
|
+
import { parseRowFieldPath } from '../formStateHelpers.js'
|
|
9
13
|
import { useToast } from '../Toaster.js'
|
|
10
14
|
import { Button } from '../ui/button.js'
|
|
11
|
-
import { computeDelta, preserveCursor } from './textDelta.js'
|
|
12
15
|
|
|
13
16
|
type ToolbarButton =
|
|
14
17
|
| 'bold' | 'italic' | 'strike' | 'link'
|
|
@@ -46,92 +49,59 @@ export function MarkdownInput({
|
|
|
46
49
|
uploadUrl: string | undefined
|
|
47
50
|
}): React.ReactElement {
|
|
48
51
|
const fs = useFieldState(name)
|
|
52
|
+
const room = useCollabRoom()
|
|
53
|
+
const collabRenderer = getCollabTextRenderer()
|
|
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.
|
|
62
|
+
//
|
|
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
|
|
67
|
+
// are write-mode-only on the native path; collab users type markdown
|
|
68
|
+
// syntax directly (`**bold**`, `## heading`). The preview tab keeps
|
|
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) {
|
|
80
|
+
return (
|
|
81
|
+
<MarkdownCollabInput
|
|
82
|
+
Renderer={collabRenderer}
|
|
83
|
+
fragmentKey={fragmentKey}
|
|
84
|
+
hiddenInputName={name}
|
|
85
|
+
defaultValue={defaultValue}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
{...(placeholder !== undefined ? { placeholder } : {})}
|
|
88
|
+
{...(minHeight !== undefined ? { minHeight } : {})}
|
|
89
|
+
{...(maxHeight !== undefined ? { maxHeight } : {})}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
49
94
|
const { notify } = useToast()
|
|
50
95
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
51
|
-
// Phase F.6 — IME composition gate. Set between `compositionstart` /
|
|
52
|
-
// `compositionend`; the textarea's onChange skips `applyDelta` while
|
|
53
|
-
// composing so intermediate chars don't emit ops. Lives at the
|
|
54
|
-
// component scope so the onChange and composition handlers share it.
|
|
55
|
-
const isComposingRef = useRef<boolean>(false)
|
|
56
96
|
|
|
57
97
|
const initial = useMemo(() => stringValue(defaultValue), [])
|
|
58
98
|
const [localValue, setLocalValue] = useState<string>(initial)
|
|
59
99
|
const [tab, setTab] = useState<'write' | 'preview'>('write')
|
|
60
100
|
const [busy, setBusy] = useState(false)
|
|
61
101
|
|
|
62
|
-
|
|
63
|
-
// a `TextBinding`, the textarea is bound to a `Y.Text` and edits emit
|
|
64
|
-
// `TextDelta`s. Mirrors the architecture in `TextLikeInput.tsx` but
|
|
65
|
-
// wired in-line because MarkdownInput has its own toolbar + Preview
|
|
66
|
-
// tab that also need to flow through the binding.
|
|
67
|
-
const binding = fs.textBinding
|
|
68
|
-
const [boundValue, setBoundValue] = useState<string>(() => binding?.read() ?? initial)
|
|
69
|
-
const boundValueRef = useRef<string>(boundValue)
|
|
70
|
-
useEffect(() => { boundValueRef.current = boundValue }, [boundValue])
|
|
71
|
-
|
|
72
|
-
// On binding swap: read current Y.Text state. If non-empty, lift it
|
|
73
|
-
// into local + form-map state. If empty (no peer has typed yet), leave
|
|
74
|
-
// the SSR-default-derived `boundValue` showing — first edit will
|
|
75
|
-
// emit a replace-from-empty delta that atomically populates Y.Text.
|
|
76
|
-
// No client-side seed: Y.Text isn't safe to seed under concurrent
|
|
77
|
-
// first-mounters (see @pilotiq-pro/collab `formCollabBinding.ts`).
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (!binding) return
|
|
80
|
-
const next = binding.read()
|
|
81
|
-
if (next.length > 0) {
|
|
82
|
-
setBoundValue(next)
|
|
83
|
-
boundValueRef.current = next
|
|
84
|
-
fs.setValue(next)
|
|
85
|
-
}
|
|
86
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
-
}, [binding])
|
|
88
|
-
|
|
89
|
-
// Subscribe to remote changes. Local-echoes are filtered by the
|
|
90
|
-
// `next === prev` guard. Cursor preserved via the same heuristic
|
|
91
|
-
// used in `TextLikeInput.BoundTextInput`.
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (!binding) return
|
|
94
|
-
return binding.observe((next) => {
|
|
95
|
-
const prev = boundValueRef.current
|
|
96
|
-
if (next === prev) return
|
|
97
|
-
const ta = textareaRef.current
|
|
98
|
-
const cursor = ta?.selectionStart ?? next.length
|
|
99
|
-
const restored = preserveCursor(prev, next, cursor)
|
|
100
|
-
setBoundValue(next)
|
|
101
|
-
boundValueRef.current = next
|
|
102
|
-
fs.setValue(next)
|
|
103
|
-
requestAnimationFrame(() => {
|
|
104
|
-
if (!ta) return
|
|
105
|
-
if (document.activeElement !== ta) return
|
|
106
|
-
try { ta.setSelectionRange(restored, restored) } catch { /* defensive */ }
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
-
}, [binding])
|
|
111
|
-
|
|
112
|
-
const value = binding
|
|
113
|
-
? boundValue
|
|
114
|
-
: (fs.controlled ? stringValue(fs.value) : localValue)
|
|
102
|
+
const value = fs.controlled ? stringValue(fs.value) : localValue
|
|
115
103
|
|
|
116
104
|
const setValue = (next: string): void => {
|
|
117
|
-
if (binding) {
|
|
118
|
-
// Compute against current Y.Text contents (not the local ref) so:
|
|
119
|
-
// - first edit against empty Y.Text → `insert@0 <whole>` atomic
|
|
120
|
-
// populate (no separate seed op needed);
|
|
121
|
-
// - after a remote-applied update or server-resolve replace, the
|
|
122
|
-
// delta reflects the actual current shared state, not stale
|
|
123
|
-
// local bookkeeping.
|
|
124
|
-
const before = binding.read()
|
|
125
|
-
if (next !== before) {
|
|
126
|
-
const delta = computeDelta(before, next)
|
|
127
|
-
if (delta) binding.applyDelta(delta)
|
|
128
|
-
setBoundValue(next)
|
|
129
|
-
boundValueRef.current = next
|
|
130
|
-
}
|
|
131
|
-
fs.setValue(next)
|
|
132
|
-
fs.triggerLive(next)
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
105
|
if (fs.controlled) { fs.setValue(next); fs.triggerLive(next) }
|
|
136
106
|
else { setLocalValue(next); fs.triggerLive(next) }
|
|
137
107
|
}
|
|
@@ -364,24 +334,7 @@ export function MarkdownInput({
|
|
|
364
334
|
{...(fs.controlled
|
|
365
335
|
? {
|
|
366
336
|
value,
|
|
367
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
368
|
-
// Phase F.6 — when the binding is active and the user
|
|
369
|
-
// is mid-IME, paint locally and hold the delta until
|
|
370
|
-
// compositionend so we never emit ops for the
|
|
371
|
-
// intermediate composing chars.
|
|
372
|
-
if (binding && isComposingRef.current) {
|
|
373
|
-
setBoundValue(e.target.value)
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
setValue(e.target.value)
|
|
377
|
-
},
|
|
378
|
-
...(binding ? {
|
|
379
|
-
onCompositionStart: () => { isComposingRef.current = true },
|
|
380
|
-
onCompositionEnd: (e: React.CompositionEvent<HTMLTextAreaElement>) => {
|
|
381
|
-
isComposingRef.current = false
|
|
382
|
-
setValue(e.currentTarget.value)
|
|
383
|
-
},
|
|
384
|
-
} : {}),
|
|
337
|
+
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value),
|
|
385
338
|
}
|
|
386
339
|
: { defaultValue: initial, onChange: (e) => setLocalValue(e.target.value) })}
|
|
387
340
|
onPaste={onPaste}
|
|
@@ -422,6 +375,83 @@ function TabButton({ active, onClick, children }: {
|
|
|
422
375
|
)
|
|
423
376
|
}
|
|
424
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Phase B follow-up — collab-aware markdown editor. Mounts the registered
|
|
380
|
+
* Tiptap-backed plain-text renderer for the Write pane and reuses the
|
|
381
|
+
* existing `marked` pipeline for Preview. No toolbar, no Cmd-shortcuts, no
|
|
382
|
+
* paste-image upload — those features depend on textarea-DOM splicing that
|
|
383
|
+
* doesn't translate to Tiptap's selection model. The cursor-bug fix is the
|
|
384
|
+
* load-bearing change; markdown-syntax authors keep typing as before.
|
|
385
|
+
*/
|
|
386
|
+
function MarkdownCollabInput({
|
|
387
|
+
Renderer, fragmentKey, hiddenInputName, defaultValue, disabled, placeholder, minHeight, maxHeight,
|
|
388
|
+
}: {
|
|
389
|
+
Renderer: CollabTextRenderer
|
|
390
|
+
fragmentKey: string
|
|
391
|
+
hiddenInputName: string
|
|
392
|
+
defaultValue: unknown
|
|
393
|
+
disabled: boolean
|
|
394
|
+
placeholder?: string
|
|
395
|
+
minHeight?: string
|
|
396
|
+
maxHeight?: string
|
|
397
|
+
}): React.ReactElement {
|
|
398
|
+
const fs = useFieldState(hiddenInputName)
|
|
399
|
+
const initial = useMemo(() => stringValue(defaultValue), [])
|
|
400
|
+
const [text, setText] = useState<string>(initial)
|
|
401
|
+
const [tab, setTab] = useState<'write' | 'preview'>('write')
|
|
402
|
+
const textRef = useRef(text)
|
|
403
|
+
useEffect(() => { textRef.current = text }, [text])
|
|
404
|
+
|
|
405
|
+
const handleChange = (next: string): void => {
|
|
406
|
+
setText(next)
|
|
407
|
+
if (fs.controlled) fs.setValue(next)
|
|
408
|
+
fs.triggerLive(next)
|
|
409
|
+
}
|
|
410
|
+
const handleBlur = (): void => { /* fire-and-forget — live trigger already ran on change */ }
|
|
411
|
+
|
|
412
|
+
const previewHtml = useMemo(
|
|
413
|
+
() => tab === 'preview' ? marked.parse(text, { gfm: true, breaks: false, async: false }) as string : '',
|
|
414
|
+
[tab, text],
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
const wrapperStyle: React.CSSProperties = {}
|
|
418
|
+
if (minHeight) wrapperStyle.minHeight = minHeight
|
|
419
|
+
if (maxHeight) wrapperStyle.maxHeight = maxHeight
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<div className="flex flex-col rounded-md border bg-background">
|
|
423
|
+
<div className="flex items-center border-b px-2 py-1">
|
|
424
|
+
<TabButton active={tab === 'write'} onClick={() => setTab('write')}>Write</TabButton>
|
|
425
|
+
<TabButton active={tab === 'preview'} onClick={() => setTab('preview')}>Preview</TabButton>
|
|
426
|
+
</div>
|
|
427
|
+
{tab === 'write' ? (
|
|
428
|
+
<div style={wrapperStyle} className="overflow-auto">
|
|
429
|
+
<input type="hidden" name={hiddenInputName} value={text} />
|
|
430
|
+
<Renderer
|
|
431
|
+
name={fragmentKey}
|
|
432
|
+
multiline={true}
|
|
433
|
+
defaultValue={initial}
|
|
434
|
+
{...(placeholder !== undefined ? { placeholder } : {})}
|
|
435
|
+
disabled={disabled}
|
|
436
|
+
onChange={handleChange}
|
|
437
|
+
onBlur={handleBlur}
|
|
438
|
+
className="w-full bg-transparent px-3 py-2 text-sm font-mono leading-relaxed outline-none disabled:opacity-50 whitespace-pre-wrap break-words"
|
|
439
|
+
/>
|
|
440
|
+
</div>
|
|
441
|
+
) : (
|
|
442
|
+
<>
|
|
443
|
+
<input type="hidden" name={hiddenInputName} value={text} readOnly />
|
|
444
|
+
<div
|
|
445
|
+
className="prose prose-sm dark:prose-invert max-w-none px-3 py-2"
|
|
446
|
+
style={wrapperStyle}
|
|
447
|
+
dangerouslySetInnerHTML={{ __html: previewHtml || '<p class="text-muted-foreground italic">Nothing to preview</p>' }}
|
|
448
|
+
/>
|
|
449
|
+
</>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
425
455
|
function stringValue(v: unknown): string {
|
|
426
456
|
if (v === undefined || v === null) return ''
|
|
427
457
|
if (typeof v === 'string') return v
|
|
@@ -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
|
|