@pilotiq/pilotiq 0.12.0 → 0.13.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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +19 -0
- package/dist/pageData/helpers.d.ts +16 -0
- package/dist/pageData/helpers.d.ts.map +1 -1
- package/dist/pageData/helpers.js +61 -1
- package/dist/pageData/helpers.js.map +1 -1
- package/dist/pageData.d.ts +1 -1
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +1 -1
- package/dist/pageData.js.map +1 -1
- package/dist/react/FormCollabBindingRegistry.d.ts +33 -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 +15 -92
- 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 +78 -49
- 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 +104 -60
- 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/fields/repeaterReconcile.d.ts +66 -0
- package/dist/react/fields/repeaterReconcile.d.ts.map +1 -0
- package/dist/react/fields/repeaterReconcile.js +96 -0
- package/dist/react/fields/repeaterReconcile.js.map +1 -0
- 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/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -1
- package/dist/react/schemaRenderer/form/FormRenderer.js +10 -0
- package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/pageData/helpers.ts +55 -1
- package/src/pageData.test.ts +67 -0
- package/src/pageData.ts +1 -0
- package/src/react/FormCollabBindingRegistry.ts +34 -91
- package/src/react/FormStateContext.tsx +14 -126
- package/src/react/RowCoordsContext.tsx +23 -0
- package/src/react/fields/BuilderInput.tsx +75 -39
- package/src/react/fields/MarkdownInput.tsx +42 -129
- package/src/react/fields/RepeaterInput.tsx +107 -48
- package/src/react/fields/TextLikeInput.tsx +67 -225
- package/src/react/fields/repeaterReconcile.test.ts +114 -0
- package/src/react/fields/repeaterReconcile.ts +104 -0
- package/src/react/formStateHelpers.test.ts +0 -99
- package/src/react/formStateHelpers.ts +0 -83
- package/src/react/index.ts +0 -2
- package/src/react/schemaRenderer/form/FormRenderer.tsx +10 -0
- 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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect,
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useFieldState } from '../FormStateContext.js';
|
|
4
4
|
import { useCollabRoom } from '../CollabRoomContext.js';
|
|
5
5
|
import { getCollabTextRenderer } from '../CollabTextRendererRegistry.js';
|
|
6
|
+
import { useRowCoords } from '../RowCoordsContext.js';
|
|
7
|
+
import { parseRowFieldPath } from '../formStateHelpers.js';
|
|
6
8
|
import { Input } from '../ui/input.js';
|
|
7
9
|
import { Textarea } from '../ui/textarea.js';
|
|
8
|
-
import { computeDelta, preserveCursor } from './textDelta.js';
|
|
9
10
|
/**
|
|
10
11
|
* Bridge between controlled (FormStateProvider) and uncontrolled
|
|
11
12
|
* (defaultValue) modes for text-style inputs. When inside a form with
|
|
@@ -13,52 +14,69 @@ import { computeDelta, preserveCursor } from './textDelta.js';
|
|
|
13
14
|
* fires the live trigger on change/blur according to the field's `live`
|
|
14
15
|
* config. Outside a controlled form, falls back to plain `defaultValue`.
|
|
15
16
|
*
|
|
16
|
-
* **
|
|
17
|
-
* is mounted up-tree AND `@pilotiq
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
17
|
+
* **Collab branch — Tiptap-backed `Y.XmlFragment`.** When a
|
|
18
|
+
* `<RecordCollabRoom>` is mounted up-tree AND `@pilotiq/tiptap`'s
|
|
19
|
+
* `registerTiptap()` registered a collab text renderer, the input
|
|
20
|
+
* mounts the Tiptap-backed editor against a `Y.XmlFragment` keyed by
|
|
21
|
+
* either the bare field name (top-level) or
|
|
22
|
+
* `${arrayName}.${rowId}.${fieldName}` (Repeater / Builder row leaves
|
|
23
|
+
* via `useRowCoords()`). Selections anchor to `Y.RelativePosition` via
|
|
24
|
+
* y-prosemirror, so cursors survive both mid-word remote edits and
|
|
25
|
+
* concurrent inserts. Masked fields fall through to the legacy
|
|
26
|
+
* whole-string LWW path (mask + character-level CRDT is incompatible
|
|
27
|
+
* — peers would see raw keystrokes desynced from the rendered mask).
|
|
25
28
|
*/
|
|
26
29
|
export function TextLikeInput({ el, name, common, type, extraProps, multiline, applyMask, }) {
|
|
27
30
|
const fs = useFieldState(name);
|
|
28
31
|
const room = useCollabRoom();
|
|
29
32
|
const collabRenderer = getCollabTextRenderer();
|
|
33
|
+
const rowCoords = useRowCoords();
|
|
30
34
|
const liveCfg = el['live'];
|
|
31
35
|
const liveOpts = (typeof liveCfg === 'object' && liveCfg !== null
|
|
32
36
|
? liveCfg
|
|
33
37
|
: {});
|
|
34
38
|
const onBlurMode = liveOpts.onBlur === true;
|
|
35
39
|
const mask = applyMask ?? identity;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// Masking is mutually exclusive with character-level CRDT (peers would
|
|
41
|
+
// see raw keystrokes diverged from the local mask render); masked
|
|
42
|
+
// fields fall through to LWW. We read the mask from the field meta
|
|
43
|
+
// directly — `applyMask` is a `useCallback`-wrapped fn that's *always*
|
|
44
|
+
// defined (identity when no mask), so its truthiness can't gate the
|
|
45
|
+
// branch.
|
|
42
46
|
const hasMask = typeof el['mask'] === 'string';
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
47
|
+
// Collab branch — Tiptap-backed plain-text editor. Top-level fields
|
|
48
|
+
// use the bare `name` as the fragment-key; Repeater / Builder row
|
|
49
|
+
// leaves compose `${arrayName}.${rowId}.${fieldName}` from
|
|
50
|
+
// `useRowCoords()` so the Y.XmlFragment survives row reorders (keyed
|
|
51
|
+
// by the stable rowId, not the array index). The hidden FormData
|
|
52
|
+
// input keeps the original dotted path so submission lands on the
|
|
53
|
+
// server at the right slot.
|
|
54
|
+
//
|
|
55
|
+
// Dotted paths that don't match a row shape (no rowCoords OR
|
|
56
|
+
// `parseRowFieldPath` returns null — nested row arrays, malformed
|
|
57
|
+
// names) skip the collab path and fall through to the controlled /
|
|
58
|
+
// uncontrolled branches below.
|
|
52
59
|
const fieldCollab = el['collab'];
|
|
60
|
+
const fragmentKey = (() => {
|
|
61
|
+
if (!name.includes('.'))
|
|
62
|
+
return name;
|
|
63
|
+
if (!rowCoords)
|
|
64
|
+
return null;
|
|
65
|
+
const parsed = parseRowFieldPath(name);
|
|
66
|
+
if (!parsed)
|
|
67
|
+
return null;
|
|
68
|
+
if (parsed.arrayName !== rowCoords.arrayName)
|
|
69
|
+
return null;
|
|
70
|
+
if (parsed.index !== rowCoords.rowIndex)
|
|
71
|
+
return null;
|
|
72
|
+
return `${rowCoords.arrayName}.${rowCoords.rowId}.${parsed.fieldName}`;
|
|
73
|
+
})();
|
|
53
74
|
if (room &&
|
|
54
75
|
collabRenderer &&
|
|
55
76
|
fieldCollab !== false &&
|
|
56
77
|
!hasMask &&
|
|
57
|
-
|
|
58
|
-
return (_jsx(CollabTextField, { Renderer: collabRenderer,
|
|
59
|
-
}
|
|
60
|
-
if (fs.textBinding && !hasMask) {
|
|
61
|
-
return (_jsx(BoundTextInput, { binding: fs.textBinding, name: name, triggerLive: fs.triggerLive, onBlurMode: onBlurMode, common: common, extraProps: extraProps, type: type, multiline: multiline }));
|
|
78
|
+
fragmentKey !== null) {
|
|
79
|
+
return (_jsx(CollabTextField, { Renderer: collabRenderer, fragmentKey: fragmentKey, hiddenInputName: name, multiline: multiline, defaultValue: stringValue(common['defaultValue']), ...(common['placeholder'] !== undefined ? { placeholder: String(common['placeholder']) } : {}), disabled: Boolean(common['disabled']), triggerLive: fs.triggerLive, setValue: fs.setValue, controlled: fs.controlled, onBlurMode: onBlurMode }));
|
|
62
80
|
}
|
|
63
81
|
if (fs.controlled) {
|
|
64
82
|
const ctxValue = fs.value !== undefined && fs.value !== null ? String(fs.value) : '';
|
|
@@ -101,162 +119,7 @@ export function TextLikeInput({ el, name, common, type, extraProps, multiline, a
|
|
|
101
119
|
return _jsx(Input, { ...common, type: type, ...extraProps });
|
|
102
120
|
}
|
|
103
121
|
/**
|
|
104
|
-
*
|
|
105
|
-
* because the binding's `Y.Text` is the source of truth (not the
|
|
106
|
-
* form's `values` map). Mirrors every committed value back into the
|
|
107
|
-
* form context via `fs.setValue` so submission / live re-resolve see
|
|
108
|
-
* the latest string.
|
|
109
|
-
*
|
|
110
|
-
* Lifecycle:
|
|
111
|
-
* - Mount: seed local state from `binding.read()`; mirror it into
|
|
112
|
-
* the form's `values` map.
|
|
113
|
-
* - Local edit: compute a `TextDelta` (insert / delete / replace)
|
|
114
|
-
* from the before/after strings and `applyDelta` to the binding.
|
|
115
|
-
* Eagerly update local state in the same React render so the
|
|
116
|
-
* controlled input doesn't lag the keystroke.
|
|
117
|
-
* - Remote edit: `binding.observe` fires with the post-change
|
|
118
|
-
* string; we replace local state and best-effort preserve the
|
|
119
|
-
* local cursor via `preserveCursor`. The local-echo of our own
|
|
120
|
-
* `applyDelta` is collapsed by the value-equality check.
|
|
121
|
-
* - IME composition: `applyDelta` is deferred to `compositionend`
|
|
122
|
-
* so the binding never sees intermediate composing chars (which
|
|
123
|
-
* would emit one delta per keystroke and confuse downstream
|
|
124
|
-
* observers).
|
|
125
|
-
*/
|
|
126
|
-
function BoundTextInput({ binding, name, triggerLive, onBlurMode, common, extraProps, type, multiline, }) {
|
|
127
|
-
const fs = useFieldState(name);
|
|
128
|
-
// SSR-rendered default. Captured once at mount; used as display
|
|
129
|
-
// fallback while the room's `Y.Text` is still empty (the seed race
|
|
130
|
-
// for Y.Text isn't safe across concurrent first-mounters, so no peer
|
|
131
|
-
// populates it client-side — see `@pilotiq-pro/collab` for the
|
|
132
|
-
// rationale). First user edit emits a replace-from-empty delta that
|
|
133
|
-
// atomically lifts the displayed value into the CRDT.
|
|
134
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
|
-
const fallback = useMemo(() => stringValue(fs.value), []);
|
|
136
|
-
const [value, setValueLocal] = useState(() => binding.read() || fallback);
|
|
137
|
-
const valueRef = useRef(value);
|
|
138
|
-
const isComposing = useRef(false);
|
|
139
|
-
const inputRef = useRef(null);
|
|
140
|
-
useEffect(() => { valueRef.current = value; }, [value]);
|
|
141
|
-
// Stable ref to the form-mirror writer so the observer effect below
|
|
142
|
-
// doesn't tear down on every render (fs.setValue is a fresh arrow on
|
|
143
|
-
// every useFieldState call).
|
|
144
|
-
const mirrorRef = useRef(() => { });
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
mirrorRef.current = (v) => { fs.setValue(v); };
|
|
147
|
-
});
|
|
148
|
-
// On mount / binding swap: read the binding's current state. If
|
|
149
|
-
// non-empty (i.e. someone else has already typed), display it and
|
|
150
|
-
// mirror into the form values map. If empty, leave the fallback
|
|
151
|
-
// showing — no client-side seed (see file-header comment).
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
const initial = binding.read();
|
|
154
|
-
if (initial.length > 0) {
|
|
155
|
-
setValueLocal(initial);
|
|
156
|
-
valueRef.current = initial;
|
|
157
|
-
mirrorRef.current(initial);
|
|
158
|
-
}
|
|
159
|
-
}, [binding]);
|
|
160
|
-
// Subscribe to text-CRDT changes. Yjs fires this for BOTH local and
|
|
161
|
-
// remote transactions — local echoes are collapsed by the
|
|
162
|
-
// `next === prev` guard.
|
|
163
|
-
useEffect(() => {
|
|
164
|
-
const unsubscribe = binding.observe((next) => {
|
|
165
|
-
const prev = valueRef.current;
|
|
166
|
-
if (next === prev)
|
|
167
|
-
return;
|
|
168
|
-
const el = inputRef.current;
|
|
169
|
-
const cursor = el?.selectionStart ?? next.length;
|
|
170
|
-
const restored = preserveCursor(prev, next, cursor);
|
|
171
|
-
setValueLocal(next);
|
|
172
|
-
valueRef.current = next;
|
|
173
|
-
mirrorRef.current(next);
|
|
174
|
-
// Defer cursor restore until after React commits. Only reapply
|
|
175
|
-
// when the input is still focused — yanking the selection on a
|
|
176
|
-
// blurred field would steal focus across the page.
|
|
177
|
-
requestAnimationFrame(() => {
|
|
178
|
-
if (!el)
|
|
179
|
-
return;
|
|
180
|
-
if (document.activeElement !== el)
|
|
181
|
-
return;
|
|
182
|
-
try {
|
|
183
|
-
el.setSelectionRange(restored, restored);
|
|
184
|
-
}
|
|
185
|
-
catch { /* setSelectionRange unsupported on some input types — defensive */ }
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
return unsubscribe;
|
|
189
|
-
}, [binding]);
|
|
190
|
-
const commitDelta = useCallback((after) => {
|
|
191
|
-
// Compute the delta against the binding's *current* Y.Text contents
|
|
192
|
-
// — not the renderer's `before` ref. The two can diverge in three
|
|
193
|
-
// cases that all converge correctly under this approach:
|
|
194
|
-
// 1. First edit when Y.Text is empty: delta = `insert@0 <whole>`,
|
|
195
|
-
// which atomically lifts the displayed fallback into the CRDT
|
|
196
|
-
// without a separate seed op.
|
|
197
|
-
// 2. After a remote-applied update: Y.Text holds the peer's value;
|
|
198
|
-
// computing against it avoids "ghost" deltas that re-emit ops
|
|
199
|
-
// against a stale local ref.
|
|
200
|
-
// 3. After a server-resolve `triggerLive` replace: same as (2).
|
|
201
|
-
const before = binding.read();
|
|
202
|
-
if (after === before)
|
|
203
|
-
return;
|
|
204
|
-
const delta = computeDelta(before, after);
|
|
205
|
-
if (!delta)
|
|
206
|
-
return;
|
|
207
|
-
// Pre-stamp `valueRef.current = after` BEFORE `applyDelta`. Y.Text's
|
|
208
|
-
// observe fires synchronously inside `applyDelta` for our own write,
|
|
209
|
-
// so without this the observer would see `prev=before, next=after`
|
|
210
|
-
// and run `preserveCursor` — which is designed for *remote* edits
|
|
211
|
-
// and clobbers the user's caret on local typing (typed '1' at pos 0
|
|
212
|
-
// would jump cursor forward by delta-length and the next keystroke
|
|
213
|
-
// would insert at the wrong index, producing scrambled output).
|
|
214
|
-
// With `valueRef` already at `after`, the observer's `next === prev`
|
|
215
|
-
// short-circuit fires and the cursor is left alone for local echoes.
|
|
216
|
-
valueRef.current = after;
|
|
217
|
-
binding.applyDelta(delta);
|
|
218
|
-
setValueLocal(after);
|
|
219
|
-
mirrorRef.current(after);
|
|
220
|
-
if (!onBlurMode)
|
|
221
|
-
triggerLive(after);
|
|
222
|
-
}, [binding, onBlurMode, triggerLive]);
|
|
223
|
-
const onChange = (e) => {
|
|
224
|
-
if (isComposing.current) {
|
|
225
|
-
// IME mid-composition — paint locally, hold the delta until commit.
|
|
226
|
-
setValueLocal(e.target.value);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
commitDelta(e.target.value);
|
|
230
|
-
};
|
|
231
|
-
const onCompositionStart = () => { isComposing.current = true; };
|
|
232
|
-
const onCompositionEnd = (e) => {
|
|
233
|
-
isComposing.current = false;
|
|
234
|
-
commitDelta(e.currentTarget.value);
|
|
235
|
-
};
|
|
236
|
-
const onBlur = () => {
|
|
237
|
-
if (onBlurMode)
|
|
238
|
-
triggerLive(valueRef.current);
|
|
239
|
-
};
|
|
240
|
-
const setRef = (el) => {
|
|
241
|
-
inputRef.current = el;
|
|
242
|
-
};
|
|
243
|
-
const props = {
|
|
244
|
-
...common,
|
|
245
|
-
...extraProps,
|
|
246
|
-
defaultValue: undefined,
|
|
247
|
-
value,
|
|
248
|
-
onChange,
|
|
249
|
-
onBlur,
|
|
250
|
-
onCompositionStart,
|
|
251
|
-
onCompositionEnd,
|
|
252
|
-
ref: setRef,
|
|
253
|
-
};
|
|
254
|
-
if (multiline)
|
|
255
|
-
return _jsx(Textarea, { ...props });
|
|
256
|
-
return _jsx(Input, { ...props, type: type });
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Phase B — wrapper around the registered Tiptap-backed collab editor.
|
|
122
|
+
* Wrapper around the registered Tiptap-backed collab editor.
|
|
260
123
|
* Owns the local text mirror so the hidden `<input>` always carries the
|
|
261
124
|
* editor's current value for FormData submission. When `FormStateProvider`
|
|
262
125
|
* is mounted up-tree, also mirrors every update into the values map via
|
|
@@ -267,8 +130,15 @@ function BoundTextInput({ binding, name, triggerLive, onBlurMode, common, extraP
|
|
|
267
130
|
* editor handles composition natively and y-prosemirror anchors selections
|
|
268
131
|
* to `Yjs.RelativePosition`, so the cursor survives concurrent + mid-word
|
|
269
132
|
* remote edits without any client-side bookkeeping.
|
|
133
|
+
*
|
|
134
|
+
* `fragmentKey` and `hiddenInputName` diverge for row-text leaves (Phase
|
|
135
|
+
* 1 of collab-row-text-tiptap-backed.md): the renderer's Y.XmlFragment is
|
|
136
|
+
* keyed by `${arrayName}.${rowId}.${fieldName}` so it survives row
|
|
137
|
+
* reorders, while the hidden FormData input keeps the dotted path
|
|
138
|
+
* (`items.0.title`) so submission lands at the right server-side slot.
|
|
139
|
+
* For top-level fields the two are identical.
|
|
270
140
|
*/
|
|
271
|
-
function CollabTextField({ Renderer,
|
|
141
|
+
function CollabTextField({ Renderer, fragmentKey, hiddenInputName, multiline, defaultValue, placeholder, disabled, triggerLive, setValue, controlled, onBlurMode, }) {
|
|
272
142
|
const [text, setText] = useState(defaultValue);
|
|
273
143
|
const textRef = useRef(text);
|
|
274
144
|
useEffect(() => { textRef.current = text; }, [text]);
|
|
@@ -304,7 +174,7 @@ function CollabTextField({ Renderer, name, multiline, defaultValue, placeholder,
|
|
|
304
174
|
const className = multiline
|
|
305
175
|
? 'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm whitespace-pre-wrap break-words'
|
|
306
176
|
: 'flex h-9 w-full items-center rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm whitespace-nowrap overflow-x-clip';
|
|
307
|
-
return (_jsxs(_Fragment, { children: [_jsx("input", { type: "hidden", name:
|
|
177
|
+
return (_jsxs(_Fragment, { children: [_jsx("input", { type: "hidden", name: hiddenInputName, value: text }), _jsx(Renderer, { name: fragmentKey, multiline: multiline, defaultValue: defaultValue, ...(placeholder !== undefined ? { placeholder } : {}), disabled: disabled, onChange: handleChange, onBlur: handleBlur, className: className })] }));
|
|
308
178
|
}
|
|
309
179
|
function identity(v) { return v; }
|
|
310
180
|
function stringValue(v) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextLikeInput.js","sourceRoot":"","sources":["../../../src/react/fields/TextLikeInput.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,WAAW,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"TextLikeInput.js","sourceRoot":"","sources":["../../../src/react/fields/TextLikeInput.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase A of the `Repeater.relationship` PK-switch reconciliation
|
|
3
|
+
* (see `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`).
|
|
4
|
+
*
|
|
5
|
+
* When a parent form submit creates new relationship-backed rows, the
|
|
6
|
+
* server assigns each child a DB primary key — but the row's `__id` in
|
|
7
|
+
* the row CRDT is still the renderer-minted UUID from the local session.
|
|
8
|
+
* After redirect, the submitting tab's pageData carries `__id = String(pk)`
|
|
9
|
+
* while CRDT still has the UUID, so the renderer ends up showing the
|
|
10
|
+
* same row twice (DB PK from initialRows + orphan UUID from CRDT).
|
|
11
|
+
*
|
|
12
|
+
* Phase A fix: the submitting tab marks itself for a one-shot CRDT
|
|
13
|
+
* reconcile on the next mount via a per-formId sessionStorage flag.
|
|
14
|
+
* `RepeaterInput` / `BuilderInput` read the flag on mount and, when set,
|
|
15
|
+
* snapshot the row CRDT after a short settle (waiting for WS sync) and
|
|
16
|
+
* reconcile against `initialRows` — removing orphan CRDT rows not in
|
|
17
|
+
* the form's authoritative data, and adding missing CRDT rows (rare,
|
|
18
|
+
* happens when the row was DB-seeded outside the collab session).
|
|
19
|
+
*
|
|
20
|
+
* The flag is scoped per-tab via sessionStorage, so other peers' tabs
|
|
21
|
+
* never run the reconciler — preserving their in-flight edits.
|
|
22
|
+
*
|
|
23
|
+
* Phase B (server-side rename via the @rudderjs/sync Y.Doc seam) will
|
|
24
|
+
* extend this to other peers without requiring them to reload.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Called by `FormRenderer` on submit success. Records that this tab
|
|
28
|
+
* has just persisted the form, so the next mount of any Repeater /
|
|
29
|
+
* Builder under the same form runs the PK-switch reconciler. No-op
|
|
30
|
+
* when `formId` is empty or `sessionStorage` is unavailable (SSR).
|
|
31
|
+
*/
|
|
32
|
+
export declare function markSubmitForReconcile(formId: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Called by `RepeaterInput` / `BuilderInput` on mount. Returns `true`
|
|
35
|
+
* iff the form was just submitted in this tab AND clears the flag so
|
|
36
|
+
* subsequent mounts no-op. Idempotent across multiple Repeater/Builder
|
|
37
|
+
* fields on the same form — the FIRST reader clears the flag, so
|
|
38
|
+
* siblings see `false`. To avoid that, both fields call this helper at
|
|
39
|
+
* the same mount tick — for v1 we accept the limitation: only the first
|
|
40
|
+
* Repeater on the form runs the reconciler; siblings don't.
|
|
41
|
+
*
|
|
42
|
+
* If a future need surfaces (multiple relationship-backed Repeaters on
|
|
43
|
+
* the same form), switch to a per-field flag keyed by `formId.fieldName`
|
|
44
|
+
* or have the FormRenderer dispatch a custom event instead.
|
|
45
|
+
*/
|
|
46
|
+
export declare function consumeReconcileFlag(formId: string): boolean;
|
|
47
|
+
export interface ReconcileInputs {
|
|
48
|
+
/** Current CRDT row id order (post-WS-sync). */
|
|
49
|
+
current: readonly string[];
|
|
50
|
+
/** Authoritative row id list from server-rendered initialRows. */
|
|
51
|
+
authoritative: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
export interface ReconcilePlan {
|
|
54
|
+
/** Row ids present in CRDT but not in initialRows — orphan UUIDs. */
|
|
55
|
+
toRemove: string[];
|
|
56
|
+
/** Row ids present in initialRows but not in CRDT — DB rows not yet
|
|
57
|
+
* in the room (rare; only happens when DB was seeded outside collab). */
|
|
58
|
+
toAdd: string[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Pure helper: compute the symmetric difference. Exported separately so
|
|
62
|
+
* unit tests don't need a DOM / sessionStorage shim to verify the
|
|
63
|
+
* reconciliation arithmetic.
|
|
64
|
+
*/
|
|
65
|
+
export declare function computeReconcilePlan({ current, authoritative }: ReconcileInputs): ReconcilePlan;
|
|
66
|
+
//# sourceMappingURL=repeaterReconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeaterReconcile.d.ts","sourceRoot":"","sources":["../../../src/react/fields/repeaterReconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAS3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAW5D;AAED,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,OAAO,EAAS,SAAS,MAAM,EAAE,CAAA;IACjC,kEAAkE;IAClE,aAAa,EAAG,SAAS,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB;8EAC0E;IAC1E,KAAK,EAAK,MAAM,EAAE,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,eAAe,GAAG,aAAa,CAQ/F"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase A of the `Repeater.relationship` PK-switch reconciliation
|
|
3
|
+
* (see `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`).
|
|
4
|
+
*
|
|
5
|
+
* When a parent form submit creates new relationship-backed rows, the
|
|
6
|
+
* server assigns each child a DB primary key — but the row's `__id` in
|
|
7
|
+
* the row CRDT is still the renderer-minted UUID from the local session.
|
|
8
|
+
* After redirect, the submitting tab's pageData carries `__id = String(pk)`
|
|
9
|
+
* while CRDT still has the UUID, so the renderer ends up showing the
|
|
10
|
+
* same row twice (DB PK from initialRows + orphan UUID from CRDT).
|
|
11
|
+
*
|
|
12
|
+
* Phase A fix: the submitting tab marks itself for a one-shot CRDT
|
|
13
|
+
* reconcile on the next mount via a per-formId sessionStorage flag.
|
|
14
|
+
* `RepeaterInput` / `BuilderInput` read the flag on mount and, when set,
|
|
15
|
+
* snapshot the row CRDT after a short settle (waiting for WS sync) and
|
|
16
|
+
* reconcile against `initialRows` — removing orphan CRDT rows not in
|
|
17
|
+
* the form's authoritative data, and adding missing CRDT rows (rare,
|
|
18
|
+
* happens when the row was DB-seeded outside the collab session).
|
|
19
|
+
*
|
|
20
|
+
* The flag is scoped per-tab via sessionStorage, so other peers' tabs
|
|
21
|
+
* never run the reconciler — preserving their in-flight edits.
|
|
22
|
+
*
|
|
23
|
+
* Phase B (server-side rename via the @rudderjs/sync Y.Doc seam) will
|
|
24
|
+
* extend this to other peers without requiring them to reload.
|
|
25
|
+
*/
|
|
26
|
+
const STORAGE_PREFIX = 'pilotiq.repeaterReconcile.';
|
|
27
|
+
function storageKey(formId) {
|
|
28
|
+
return STORAGE_PREFIX + formId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Called by `FormRenderer` on submit success. Records that this tab
|
|
32
|
+
* has just persisted the form, so the next mount of any Repeater /
|
|
33
|
+
* Builder under the same form runs the PK-switch reconciler. No-op
|
|
34
|
+
* when `formId` is empty or `sessionStorage` is unavailable (SSR).
|
|
35
|
+
*/
|
|
36
|
+
export function markSubmitForReconcile(formId) {
|
|
37
|
+
if (!formId)
|
|
38
|
+
return;
|
|
39
|
+
if (typeof sessionStorage === 'undefined')
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
sessionStorage.setItem(storageKey(formId), '1');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Quota exceeded / disabled — silently skip. Reconciliation is
|
|
46
|
+
// an optimization, not a correctness requirement.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Called by `RepeaterInput` / `BuilderInput` on mount. Returns `true`
|
|
51
|
+
* iff the form was just submitted in this tab AND clears the flag so
|
|
52
|
+
* subsequent mounts no-op. Idempotent across multiple Repeater/Builder
|
|
53
|
+
* fields on the same form — the FIRST reader clears the flag, so
|
|
54
|
+
* siblings see `false`. To avoid that, both fields call this helper at
|
|
55
|
+
* the same mount tick — for v1 we accept the limitation: only the first
|
|
56
|
+
* Repeater on the form runs the reconciler; siblings don't.
|
|
57
|
+
*
|
|
58
|
+
* If a future need surfaces (multiple relationship-backed Repeaters on
|
|
59
|
+
* the same form), switch to a per-field flag keyed by `formId.fieldName`
|
|
60
|
+
* or have the FormRenderer dispatch a custom event instead.
|
|
61
|
+
*/
|
|
62
|
+
export function consumeReconcileFlag(formId) {
|
|
63
|
+
if (!formId)
|
|
64
|
+
return false;
|
|
65
|
+
if (typeof sessionStorage === 'undefined')
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
const v = sessionStorage.getItem(storageKey(formId));
|
|
69
|
+
if (v !== '1')
|
|
70
|
+
return false;
|
|
71
|
+
sessionStorage.removeItem(storageKey(formId));
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pure helper: compute the symmetric difference. Exported separately so
|
|
80
|
+
* unit tests don't need a DOM / sessionStorage shim to verify the
|
|
81
|
+
* reconciliation arithmetic.
|
|
82
|
+
*/
|
|
83
|
+
export function computeReconcilePlan({ current, authoritative }) {
|
|
84
|
+
const currentSet = new Set(current);
|
|
85
|
+
const authSet = new Set(authoritative);
|
|
86
|
+
const toRemove = [];
|
|
87
|
+
const toAdd = [];
|
|
88
|
+
for (const id of current)
|
|
89
|
+
if (!authSet.has(id))
|
|
90
|
+
toRemove.push(id);
|
|
91
|
+
for (const id of authoritative)
|
|
92
|
+
if (!currentSet.has(id))
|
|
93
|
+
toAdd.push(id);
|
|
94
|
+
return { toRemove, toAdd };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=repeaterReconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeaterReconcile.js","sourceRoot":"","sources":["../../../src/react/fields/repeaterReconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,cAAc,GAAG,4BAA4B,CAAA;AAEnD,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,cAAc,GAAG,MAAM,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,IAAI,CAAC,MAAM;QAAE,OAAM;IACnB,IAAI,OAAO,cAAc,KAAK,WAAW;QAAE,OAAM;IACjD,IAAI,CAAC;QACH,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAA;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;QAC/D,kDAAkD;IACpD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACzB,IAAI,OAAO,cAAc,KAAK,WAAW;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;QACpD,IAAI,CAAC,KAAK,GAAG;YAAE,OAAO,KAAK,CAAA;QAC3B,cAAc,CAAC,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7C,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAiBD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,OAAO,EAAE,aAAa,EAAmB;IAC9E,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;IACnC,MAAM,OAAO,GAAM,IAAI,GAAG,CAAC,aAAa,CAAC,CAAA;IACzC,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,MAAM,KAAK,GAAgB,EAAE,CAAA;IAC7B,KAAK,MAAM,EAAE,IAAI,OAAO;QAAQ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC1E,KAAK,MAAM,EAAE,IAAI,aAAa;QAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AAC5B,CAAC"}
|
|
@@ -128,19 +128,4 @@ export declare function routeBindingWrite(binding: FormCollabBinding | null, for
|
|
|
128
128
|
* the v1 local-state path.
|
|
129
129
|
*/
|
|
130
130
|
export declare function collectRowArrayFieldNames(formMeta: ElementMeta): string[];
|
|
131
|
-
/**
|
|
132
|
-
* Phase F.5c — per-Repeater/Builder set of inner-field names that
|
|
133
|
-
* carry text-shaped leaves eligible for character-level CRDT. Drives
|
|
134
|
-
* `useFieldState(dottedName).textBinding` resolution: only fields in
|
|
135
|
-
* the per-array set go through `binding.getRowTextBinding`; everything
|
|
136
|
-
* else stays on row-level Y.Map LWW.
|
|
137
|
-
*
|
|
138
|
-
* Repeater rows expose their schema directly under `meta.children`;
|
|
139
|
-
* Builder rows nest schemas under `meta.blocks[i].template`. The
|
|
140
|
-
* walker descends through every block's template so a `markdown` leaf
|
|
141
|
-
* inside any block-type lands in the array's allowlist. Nested
|
|
142
|
-
* Repeaters / Builders inside row schemas are out of scope v1 (their
|
|
143
|
-
* dotted paths are 5+ segments and `parseRowFieldPath` rejects them).
|
|
144
|
-
*/
|
|
145
|
-
export declare function collectRowTextLeavesByArray(formMeta: ElementMeta): Map<string, Set<string>>;
|
|
146
131
|
//# sourceMappingURL=formStateHelpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formStateHelpers.d.ts","sourceRoot":"","sources":["../../src/react/formStateHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEvE;;sEAEsE;AACtE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyBnF;AAED;;;;;;;;wCAQwC;AACxC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAQ1F;AAuED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAW3E;AAgCD;;sDAEsD;AACtD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,EAAG,MAAM,EACb,KAAK,EAAE,OAAO,GACb,IAAI,CAEN;AAED;;;0DAG0D;AAC1D,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,MAAM,GACX,OAAO,CAkBT;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAM,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAgBzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAM,MAAM,GAChB,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAU,iBAAiB,GAAG,IAAI,EACzC,QAAQ,EAAS,WAAW,GAAG,SAAS,EACxC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,IAAI,EAAa,MAAM,EACvB,KAAK,EAAY,OAAO,GACvB,IAAI,CAaN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,EAAE,CAuBzE
|
|
1
|
+
{"version":3,"file":"formStateHelpers.d.ts","sourceRoot":"","sources":["../../src/react/formStateHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEvE;;sEAEsE;AACtE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyBnF;AAED;;;;;;;;wCAQwC;AACxC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAQ1F;AAuED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAW3E;AAgCD;;sDAEsD;AACtD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,EAAG,MAAM,EACb,KAAK,EAAE,OAAO,GACb,IAAI,CAEN;AAED;;;0DAG0D;AAC1D,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,MAAM,GACX,OAAO,CAkBT;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAM,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAgBzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAM,MAAM,GAChB,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAU,iBAAiB,GAAG,IAAI,EACzC,QAAQ,EAAS,WAAW,GAAG,SAAS,EACxC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,IAAI,EAAa,MAAM,EACvB,KAAK,EAAY,OAAO,GACvB,IAAI,CAaN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,EAAE,CAuBzE"}
|
|
@@ -370,95 +370,4 @@ export function collectRowArrayFieldNames(formMeta) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
|
-
/**
|
|
374
|
-
* Phase F.5c — text-shaped fieldTypes whose row-leaf values should be
|
|
375
|
-
* routed through `Y.Text` instead of `Y.Map` LWW. Mirrors the same
|
|
376
|
-
* allowlist `@pilotiq-pro/collab`'s top-level binding uses; consumers
|
|
377
|
-
* registering character-level CRDT for additional plain-text-shaped
|
|
378
|
-
* fields update both copies in lockstep until a cross-repo shared
|
|
379
|
-
* constants module exists.
|
|
380
|
-
*/
|
|
381
|
-
const ROW_TEXT_FIELD_TYPES = new Set([
|
|
382
|
-
'text', 'textarea', 'email', 'slug', 'markdown',
|
|
383
|
-
]);
|
|
384
|
-
/**
|
|
385
|
-
* Phase F.5c — per-Repeater/Builder set of inner-field names that
|
|
386
|
-
* carry text-shaped leaves eligible for character-level CRDT. Drives
|
|
387
|
-
* `useFieldState(dottedName).textBinding` resolution: only fields in
|
|
388
|
-
* the per-array set go through `binding.getRowTextBinding`; everything
|
|
389
|
-
* else stays on row-level Y.Map LWW.
|
|
390
|
-
*
|
|
391
|
-
* Repeater rows expose their schema directly under `meta.children`;
|
|
392
|
-
* Builder rows nest schemas under `meta.blocks[i].template`. The
|
|
393
|
-
* walker descends through every block's template so a `markdown` leaf
|
|
394
|
-
* inside any block-type lands in the array's allowlist. Nested
|
|
395
|
-
* Repeaters / Builders inside row schemas are out of scope v1 (their
|
|
396
|
-
* dotted paths are 5+ segments and `parseRowFieldPath` rejects them).
|
|
397
|
-
*/
|
|
398
|
-
export function collectRowTextLeavesByArray(formMeta) {
|
|
399
|
-
const out = new Map();
|
|
400
|
-
walkTop(formMeta);
|
|
401
|
-
return out;
|
|
402
|
-
function walkTop(node) {
|
|
403
|
-
if (node.type === 'field') {
|
|
404
|
-
const fieldType = String(node['fieldType'] ?? '');
|
|
405
|
-
if (fieldType === 'repeater' || fieldType === 'builder') {
|
|
406
|
-
if (node.collab === false)
|
|
407
|
-
return;
|
|
408
|
-
const name = String(node['name'] ?? '');
|
|
409
|
-
if (!name)
|
|
410
|
-
return;
|
|
411
|
-
const set = new Set();
|
|
412
|
-
// Repeater's `toMeta()` emits the row schema under `template` (not
|
|
413
|
-
// `children` — that's per-resolved-row). Builder nests row schemas
|
|
414
|
-
// under `blocks[i].template`. Reading `children` here pre-fix gave
|
|
415
|
-
// every Repeater an empty text-leaf set → row text never CRDT'd.
|
|
416
|
-
if (fieldType === 'repeater')
|
|
417
|
-
walkRow(node.template, set);
|
|
418
|
-
else
|
|
419
|
-
walkBlocks(node.blocks, set);
|
|
420
|
-
if (set.size > 0)
|
|
421
|
-
out.set(name, set);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const children = node.children;
|
|
426
|
-
if (Array.isArray(children)) {
|
|
427
|
-
for (const child of children)
|
|
428
|
-
walkTop(child);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
function walkRow(children, set) {
|
|
432
|
-
if (!Array.isArray(children))
|
|
433
|
-
return;
|
|
434
|
-
for (const child of children)
|
|
435
|
-
walkRowEl(child, set);
|
|
436
|
-
}
|
|
437
|
-
function walkRowEl(node, set) {
|
|
438
|
-
if (node.type === 'field') {
|
|
439
|
-
const fieldType = String(node['fieldType'] ?? '');
|
|
440
|
-
if (fieldType === 'repeater' || fieldType === 'builder')
|
|
441
|
-
return; // nested array
|
|
442
|
-
if (node.collab === false)
|
|
443
|
-
return;
|
|
444
|
-
const name = String(node['name'] ?? '');
|
|
445
|
-
if (name && ROW_TEXT_FIELD_TYPES.has(fieldType))
|
|
446
|
-
set.add(name);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const children = node.children;
|
|
450
|
-
if (Array.isArray(children)) {
|
|
451
|
-
for (const child of children)
|
|
452
|
-
walkRowEl(child, set);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
function walkBlocks(blocks, set) {
|
|
456
|
-
if (!Array.isArray(blocks))
|
|
457
|
-
return;
|
|
458
|
-
for (const block of blocks) {
|
|
459
|
-
const tpl = block.template;
|
|
460
|
-
walkRow(tpl, set);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
373
|
//# sourceMappingURL=formStateHelpers.js.map
|