@pilotiq/pilotiq 0.13.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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +6 -0
  3. package/dist/pageData/helpers.d.ts +16 -0
  4. package/dist/pageData/helpers.d.ts.map +1 -1
  5. package/dist/pageData/helpers.js +61 -1
  6. package/dist/pageData/helpers.js.map +1 -1
  7. package/dist/pageData.d.ts +1 -1
  8. package/dist/pageData.d.ts.map +1 -1
  9. package/dist/pageData.js +1 -1
  10. package/dist/pageData.js.map +1 -1
  11. package/dist/react/FormCollabBindingRegistry.d.ts +16 -0
  12. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
  13. package/dist/react/FormCollabBindingRegistry.js.map +1 -1
  14. package/dist/react/FormStateContext.d.ts.map +1 -1
  15. package/dist/react/FormStateContext.js +8 -1
  16. package/dist/react/FormStateContext.js.map +1 -1
  17. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  18. package/dist/react/fields/BuilderInput.js +64 -40
  19. package/dist/react/fields/BuilderInput.js.map +1 -1
  20. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  21. package/dist/react/fields/RepeaterInput.js +78 -43
  22. package/dist/react/fields/RepeaterInput.js.map +1 -1
  23. package/dist/react/fields/repeaterReconcile.d.ts +66 -0
  24. package/dist/react/fields/repeaterReconcile.d.ts.map +1 -0
  25. package/dist/react/fields/repeaterReconcile.js +96 -0
  26. package/dist/react/fields/repeaterReconcile.js.map +1 -0
  27. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -1
  28. package/dist/react/schemaRenderer/form/FormRenderer.js +10 -0
  29. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/pageData/helpers.ts +55 -1
  32. package/src/pageData.test.ts +67 -0
  33. package/src/pageData.ts +1 -0
  34. package/src/react/FormCollabBindingRegistry.ts +17 -0
  35. package/src/react/FormStateContext.tsx +8 -1
  36. package/src/react/fields/BuilderInput.tsx +53 -29
  37. package/src/react/fields/RepeaterInput.tsx +66 -32
  38. package/src/react/fields/repeaterReconcile.test.ts +114 -0
  39. package/src/react/fields/repeaterReconcile.ts +104 -0
  40. package/src/react/schemaRenderer/form/FormRenderer.tsx +10 -0
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FormRenderer.d.ts","sourceRoot":"","sources":["../../../../src/react/schemaRenderer/form/FormRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2B,MAAM,OAAO,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAW7D,KAAK,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;AAExE;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,aAAa,GACd,EAAE;IACD,EAAE,EAAE,WAAW,CAAA;IACf,aAAa,EAAE,aAAa,CAAA;CAC7B,2CAiJA;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAU,WAAW,EAC1B,KAAK,EAAU,MAAM,EACrB,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACvC,aAAa,EAAE,aAAa,GAC3B,KAAK,CAAC,SAAS,CAejB"}
1
+ {"version":3,"file":"FormRenderer.d.ts","sourceRoot":"","sources":["../../../../src/react/schemaRenderer/form/FormRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2B,MAAM,OAAO,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAY7D,KAAK,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;AAExE;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,aAAa,GACd,EAAE;IACD,EAAE,EAAE,WAAW,CAAA;IACf,aAAa,EAAE,aAAa,CAAA;CAC7B,2CA0JA;AAyBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAU,WAAW,EAC1B,KAAK,EAAU,MAAM,EACrB,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,MAAM,EAAS,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EACvC,aAAa,EAAE,aAAa,GAC3B,KAAK,CAAC,SAAS,CAejB"}
@@ -5,6 +5,7 @@ import { useCollabRoom } from '../../CollabRoomContext.js';
5
5
  import { getFormCollabBinding } from '../../FormCollabBindingRegistry.js';
6
6
  import { useNavigate } from '../../navigate.js';
7
7
  import { useToast } from '../../Toaster.js';
8
+ import { markSubmitForReconcile } from '../../fields/repeaterReconcile.js';
8
9
  import { renderField } from './renderField.js';
9
10
  /**
10
11
  * Top-level `<form>` element. Owns:
@@ -91,6 +92,15 @@ export function FormRenderer({ el, renderElement, }) {
91
92
  return;
92
93
  }
93
94
  // Success — drain notifications and SPA-navigate to the redirect.
95
+ //
96
+ // Before navigating, mark this tab for the relationship-backed
97
+ // Repeater/Builder PK-switch reconciler. The next mount of any
98
+ // child Repeater/Builder under this formId will run a one-shot
99
+ // CRDT reconcile to drop orphan UUIDs whose rows just persisted
100
+ // under a fresh DB PK. See
101
+ // `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`
102
+ // (Phase A).
103
+ markSubmitForReconcile(formId);
94
104
  const notifs = data.notifications;
95
105
  if (notifs && notifs.length > 0)
96
106
  for (const n of notifs)
@@ -1 +1 @@
1
- {"version":3,"file":"FormRenderer.js","sourceRoot":"","sources":["../../../../src/react/schemaRenderer/form/FormRenderer.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FormRenderer.js","sourceRoot":"","sources":["../../../../src/react/schemaRenderer/form/FormRenderer.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/pilotiq",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "View-based admin panel for RudderJS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -388,7 +388,61 @@ export async function applyFillPipeline<R>(
388
388
  const after = form.getMutateFormDataAfterFill()
389
389
  if (after) values = await after(values, { values, record })
390
390
 
391
- return values
391
+ return normalizeArrayFieldStrings(form, values)
392
+ }
393
+
394
+ /**
395
+ * Walk the form for Repeater / Builder field names whose value on `values`
396
+ * is a JSON string (the shape a `String?` column produces when records are
397
+ * seeded outside the pilotiq save path — raw SQL, migrations, imports).
398
+ * Pilotiq's save path stringifies these arrays via `coerceFormValues`, but
399
+ * the load path doesn't auto-parse — so a fresh `String?` column lands here
400
+ * as a string, then `resolveRepeaterRows` sees `Array.isArray(string) === false`
401
+ * and falls through to empty rows on first paint. The collab path is
402
+ * symmetric: the string lands in the form-data Y.Map, `migrateLegacyArrays`
403
+ * sees `Array.isArray(string) === false`, and silently skips migration.
404
+ *
405
+ * Parse defensively — anything that doesn't deserialize to an array is left
406
+ * verbatim so a stray non-JSON string doesn't get clobbered. Non-string
407
+ * values pass through untouched (already-array, null, undefined, number).
408
+ */
409
+ export function normalizeArrayFieldStrings<R>(
410
+ form: Form<R>,
411
+ values: Record<string, unknown>,
412
+ ): Record<string, unknown> {
413
+ const arrayFieldNames = collectArrayFieldNames(form.getChildren() ?? [])
414
+ if (arrayFieldNames.length === 0) return values
415
+ let out: Record<string, unknown> | null = null
416
+ for (const name of arrayFieldNames) {
417
+ const raw = values[name]
418
+ if (typeof raw !== 'string') continue
419
+ let parsed: unknown
420
+ try { parsed = JSON.parse(raw) } catch { continue }
421
+ if (!Array.isArray(parsed)) continue
422
+ if (!out) out = { ...values }
423
+ out[name] = parsed
424
+ }
425
+ return out ?? values
426
+ }
427
+
428
+ /** Walk the form's children for top-level Repeater + Builder field names.
429
+ * Stops at array-row boundaries — nested Repeater/Builder fields live
430
+ * inside their parent's inner schema and aren't top-level form fields. */
431
+ function collectArrayFieldNames(elements: ReadonlyArray<Element>): string[] {
432
+ const out: string[] = []
433
+ const walk = (els: ReadonlyArray<Element>): void => {
434
+ for (const el of els) {
435
+ if (isRepeaterField(el) || isBuilderField(el)) {
436
+ const name = (el as RepeaterField | BuilderField).name
437
+ if (name) out.push(name)
438
+ continue
439
+ }
440
+ const children = el.getChildren()
441
+ if (children && children.length > 0) walk(children)
442
+ }
443
+ }
444
+ walk(elements)
445
+ return out
392
446
  }
393
447
 
394
448
  /**
@@ -83,6 +83,73 @@ describe('applyFillPipeline', () => {
83
83
  const values = await applyFillPipeline(form, { id: 1 })
84
84
  assert.deepEqual(values, { id: 1, async: true })
85
85
  })
86
+
87
+ it('parses JSON-string values on Repeater slots into arrays', async () => {
88
+ const form = Form.make().schema([
89
+ TextField.make('title'),
90
+ Repeater.make('metadata').schema([TextField.make('heading')]),
91
+ ])
92
+ const record = {
93
+ id: 1,
94
+ title: 'Hello',
95
+ metadata: '[{"__id":"row-1","heading":"a"},{"__id":"row-2","heading":"b"}]',
96
+ }
97
+ const values = await applyFillPipeline(form, record)
98
+ assert.deepEqual(values['metadata'], [
99
+ { __id: 'row-1', heading: 'a' },
100
+ { __id: 'row-2', heading: 'b' },
101
+ ])
102
+ assert.equal(values['title'], 'Hello')
103
+ })
104
+
105
+ it('parses JSON-string values on Builder slots into arrays', async () => {
106
+ const form = Form.make().schema([
107
+ Builder.make('content').blocks([
108
+ Block.make('heading').schema([TextField.make('text')]),
109
+ ]),
110
+ ])
111
+ const record = {
112
+ content: '[{"__id":"row-1","type":"heading","data":{"text":"hi"}}]',
113
+ }
114
+ const values = await applyFillPipeline(form, record)
115
+ assert.deepEqual(values['content'], [
116
+ { __id: 'row-1', type: 'heading', data: { text: 'hi' } },
117
+ ])
118
+ })
119
+
120
+ it('leaves non-JSON strings on array-field slots untouched', async () => {
121
+ const form = Form.make().schema([
122
+ Repeater.make('tags').schema([TextField.make('label')]),
123
+ ])
124
+ const record = { tags: 'not-json' }
125
+ const values = await applyFillPipeline(form, record)
126
+ assert.equal(values['tags'], 'not-json')
127
+ })
128
+
129
+ it('leaves JSON strings that deserialize to non-arrays untouched', async () => {
130
+ const form = Form.make().schema([
131
+ Repeater.make('tags').schema([TextField.make('label')]),
132
+ ])
133
+ const record = { tags: '{"not":"an-array"}' }
134
+ const values = await applyFillPipeline(form, record)
135
+ assert.equal(values['tags'], '{"not":"an-array"}')
136
+ })
137
+
138
+ it('passes through already-parsed arrays unchanged', async () => {
139
+ const form = Form.make().schema([
140
+ Repeater.make('metadata').schema([TextField.make('heading')]),
141
+ ])
142
+ const rows = [{ __id: 'row-1', heading: 'a' }]
143
+ const values = await applyFillPipeline(form, { metadata: rows })
144
+ assert.equal(values['metadata'], rows)
145
+ })
146
+
147
+ it('ignores top-level non-array fields whose value happens to be a JSON-string', async () => {
148
+ const form = Form.make().schema([TextField.make('title')])
149
+ const record = { title: '[1,2,3]' }
150
+ const values = await applyFillPipeline(form, record)
151
+ assert.equal(values['title'], '[1,2,3]')
152
+ })
86
153
  })
87
154
 
88
155
  describe('resolveActiveTab', () => {
package/src/pageData.ts CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  applyRelationshipBuilderFill,
28
28
  applyRelationshipRepeaterFill,
29
29
  callPageSchema,
30
+ normalizeArrayFieldStrings,
30
31
  resolveServerDataElements,
31
32
  tagActionDispatch,
32
33
  tagCellEditUrls,
@@ -101,6 +101,18 @@ export interface FormCollabBinding {
101
101
  * renderer's optimistic update converge).
102
102
  */
103
103
  subscribeRows?(arrayName: string, fn: (event: RowsEvent) => void): () => void
104
+
105
+ /**
106
+ * Snapshot the current row id order for an array. Unlike `subscribeRows`,
107
+ * which only fires on delta events, this returns whatever is in the CRDT
108
+ * right now — used by the renderer's mount-time reconciler to detect
109
+ * orphaned rows (CRDT carries a UUID forward after the parent save
110
+ * switched the row's `__id` to a DB PK; see
111
+ * `docs/plans/repeater-relationship-pk-switch.md` Phase A).
112
+ *
113
+ * Empty array when the binding has no state for `arrayName` yet.
114
+ */
115
+ getRowOrder?(arrayName: string): string[]
104
116
  }
105
117
 
106
118
  /**
@@ -134,6 +146,11 @@ export interface RowBindingApi {
134
146
  * rowId presence in its current state. Optional on the underlying
135
147
  * contract — `null` when the binding lacks `subscribeRows`. */
136
148
  subscribe(fn: (event: RowsEvent) => void): () => void
149
+ /** Snapshot the array's current row id order. Empty array when the
150
+ * binding has no state yet OR when the underlying binding doesn't
151
+ * implement `getRowOrder` (in which case mount-time reconciliation
152
+ * no-ops). */
153
+ current(): string[]
137
154
  }
138
155
 
139
156
  /**
@@ -265,7 +265,7 @@ export function FormStateProvider({
265
265
  // binding that has addRow but not reorderRows) skip the stash — the
266
266
  // contract says all three or nothing.
267
267
  if (binding.addRow && binding.removeRow && binding.reorderRows) {
268
- const { addRow, removeRow, reorderRows, subscribeRows } = binding
268
+ const { addRow, removeRow, reorderRows, subscribeRows, getRowOrder } = binding
269
269
  const arrayNames = collectRowArrayFieldNames(formMetaRef.current)
270
270
  if (arrayNames.length > 0) {
271
271
  const rowStash = new Map<string, RowBindingApi>()
@@ -281,6 +281,13 @@ export function FormStateProvider({
281
281
  subscribe: subscribeRows
282
282
  ? (fn) => subscribeRows.call(binding, arrayName, fn)
283
283
  : () => () => {},
284
+ // `getRowOrder` is optional — bindings that ship row CRUD
285
+ // without a snapshot read (test stubs, older plugins) get a
286
+ // `[]` substitute. The renderer's reconciler treats empty as
287
+ // "no orphans known" and no-ops, which is the safest fallback.
288
+ current: getRowOrder
289
+ ? () => getRowOrder.call(binding, arrayName)
290
+ : () => [],
284
291
  })
285
292
  }
286
293
  setRowBindings(rowStash)
@@ -9,6 +9,7 @@ import { RowCoordsContext } from '../RowCoordsContext.js'
9
9
  import { useIconFor } from '../icon-context.js'
10
10
  import { reorderRows, ExtraActionStrip, buildGridContainer } from './RepeaterInput.js'
11
11
  import { syncRowGates } from './syncRowGates.js'
12
+ import { consumeReconcileFlag, computeReconcilePlan } from './repeaterReconcile.js'
12
13
  import type { RowButtonsMeta } from '../../fields/RowButton.js'
13
14
  import {
14
15
  RowChromeIconButton,
@@ -252,6 +253,31 @@ export function BuilderInput({
252
253
  })
253
254
  })
254
255
  }, [rowBinding, blocksByName, meta.blocks])
256
+
257
+ // Phase A reconciliation for `Builder.relationship` PK-switch — mirrors
258
+ // the effect in `RepeaterInput`. See its comment + the plan doc:
259
+ // `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`.
260
+ useEffect(() => {
261
+ if (!rowBinding) return
262
+ if (!consumeReconcileFlag(formId)) return
263
+ const timer = setTimeout(() => {
264
+ const plan = computeReconcilePlan({
265
+ current: rowBinding.current(),
266
+ authoritative: initialRows.map(r => r.id),
267
+ })
268
+ for (const id of plan.toRemove) rowBinding.remove(id)
269
+ // For Builder, the row carries a block `type` discriminator; seed
270
+ // it on the add path so peers' picker dropdowns see the right
271
+ // block (matches the existing add path in `addBlock`). The block
272
+ // type comes from initialRows when the row is server-rendered.
273
+ for (const id of plan.toAdd) {
274
+ const row = initialRows.find(r => r.id === id)
275
+ rowBinding.add(id, row?.type ? { type: row.type } : {})
276
+ }
277
+ }, 1500)
278
+ return () => clearTimeout(timer)
279
+ // eslint-disable-next-line react-hooks/exhaustive-deps
280
+ }, [rowBinding, formId])
255
281
  const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() =>
256
282
  accordion ? {} : initSeedCollapsed(initialRows, formId, name, defaultCollapsed, collapsible),
257
283
  )
@@ -361,26 +387,23 @@ export function BuilderInput({
361
387
  }
362
388
 
363
389
  const moveRow = (id: string, dir: -1 | 1): void => {
364
- let newOrder: string[] | null = null
365
- setRows(prev => {
366
- const idx = prev.findIndex(r => r.id === id)
367
- if (idx < 0) return prev
368
- let next: RowState[]
369
- if (dir === -1) {
370
- let target = idx - 1
371
- while (target >= 0 && prev[target]?.hidden) target--
372
- if (target < 0) return prev
373
- next = reorderRows(prev, idx, target)
374
- } else {
375
- let target = idx + 1
376
- while (target < prev.length && prev[target]?.hidden) target++
377
- if (target >= prev.length) return prev
378
- next = reorderRows(prev, idx, target + 1)
379
- }
380
- if (next !== prev) newOrder = next.map(r => r.id)
381
- return next
382
- })
383
- if (newOrder !== null) rowBinding?.reorder(newOrder)
390
+ const idx = rows.findIndex(r => r.id === id)
391
+ if (idx < 0) return
392
+ let next: RowState[]
393
+ if (dir === -1) {
394
+ let target = idx - 1
395
+ while (target >= 0 && rows[target]?.hidden) target--
396
+ if (target < 0) return
397
+ next = reorderRows(rows, idx, target)
398
+ } else {
399
+ let target = idx + 1
400
+ while (target < rows.length && rows[target]?.hidden) target++
401
+ if (target >= rows.length) return
402
+ next = reorderRows(rows, idx, target + 1)
403
+ }
404
+ if (next === rows) return
405
+ setRows(next)
406
+ rowBinding?.reorder(next.map(r => r.id))
384
407
  }
385
408
 
386
409
  // ── DnD state (skipped when buttonsOnly) ────────────────
@@ -394,15 +417,16 @@ export function BuilderInput({
394
417
  } = useRowReorderDnd({
395
418
  enabled: dndEnabled,
396
419
  onDrop: (fromId, at) => {
397
- let newOrder: string[] | null = null
398
- setRows(prev => {
399
- const fromIdx = prev.findIndex(r => r.id === fromId)
400
- if (fromIdx < 0) return prev
401
- const next = reorderRows(prev, fromIdx, at)
402
- if (next !== prev) newOrder = next.map(r => r.id)
403
- return next
404
- })
405
- if (newOrder !== null) rowBinding?.reorder(newOrder)
420
+ // See RepeaterInput's matching onDrop comment — closure-mutation
421
+ // through setRows's updater is unreliable when other state updates
422
+ // are batched (useRowReorderDnd nulls dragId/dropAt right before
423
+ // calling this).
424
+ const fromIdx = rows.findIndex(r => r.id === fromId)
425
+ if (fromIdx < 0) return
426
+ const next = reorderRows(rows, fromIdx, at)
427
+ if (next === rows) return
428
+ setRows(next)
429
+ rowBinding?.reorder(next.map(r => r.id))
406
430
  },
407
431
  })
408
432
 
@@ -21,6 +21,7 @@ import {
21
21
  DEFAULT_DELETE,
22
22
  } from './rowChromeButton.js'
23
23
  import { syncRowGates } from './syncRowGates.js'
24
+ import { consumeReconcileFlag, computeReconcilePlan } from './repeaterReconcile.js'
24
25
  import {
25
26
  generateRowId, makeAccordionStorage, makeCollapsedStorage,
26
27
  } from './rowState.js'
@@ -304,6 +305,37 @@ export function RepeaterInput({
304
305
  })
305
306
  })
306
307
  }, [rowBinding, meta.template])
308
+
309
+ // Phase A reconciliation for `Repeater.relationship` PK-switch — when
310
+ // the surrounding form just submitted in this tab AND we're inside a
311
+ // collab room with a row binding, snapshot the CRDT order after a
312
+ // short settle (long enough for WS sync to deliver any persisted
313
+ // state) and reconcile against `initialRows`. Drops orphan UUIDs
314
+ // whose rows just persisted under a fresh DB PK; idempotent + no-op
315
+ // for non-relationship Repeaters where `__id` stays UUID across
316
+ // save+reload. Plan:
317
+ // `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`.
318
+ useEffect(() => {
319
+ if (!rowBinding) return
320
+ if (!consumeReconcileFlag(formId)) return
321
+ // Give WS sync time to deliver any persisted rows before reading
322
+ // current(). 1500ms is conservative; typical sync settles in <300ms.
323
+ // The reconciler is one-shot per submit, so we accept the brief
324
+ // visual flicker over a tighter timer that might fire pre-sync.
325
+ const timer = setTimeout(() => {
326
+ const plan = computeReconcilePlan({
327
+ current: rowBinding.current(),
328
+ authoritative: initialRows.map(r => r.id),
329
+ })
330
+ for (const id of plan.toRemove) rowBinding.remove(id)
331
+ for (const id of plan.toAdd) rowBinding.add(id, {})
332
+ }, 1500)
333
+ return () => clearTimeout(timer)
334
+ // initialRows is a stable useMemo([]) ref so it's safe to omit. We
335
+ // intentionally key only on rowBinding + formId — the reconciler is
336
+ // tied to the submit lifecycle, not to row-state changes.
337
+ // eslint-disable-next-line react-hooks/exhaustive-deps
338
+ }, [rowBinding, formId])
307
339
  const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() =>
308
340
  accordion ? {} : initSeedCollapsed(initialRows, formId, name, defaultCollapsed, collapsible),
309
341
  )
@@ -394,29 +426,26 @@ export function RepeaterInput({
394
426
  }
395
427
 
396
428
  const moveRow = (id: string, dir: -1 | 1): void => {
397
- let newOrder: string[] | null = null
398
- setRows(prev => {
399
- const idx = prev.findIndex(r => r.id === id)
400
- if (idx < 0) return prev
401
- // Skip past hidden neighbours so reorder operates between visible
402
- // rows. Hidden rows hold their absolute slot — the visible row hops
403
- // over them.
404
- let next: RowState[]
405
- if (dir === -1) {
406
- let target = idx - 1
407
- while (target >= 0 && prev[target]?.hidden) target--
408
- if (target < 0) return prev
409
- next = reorderRows(prev, idx, target)
410
- } else {
411
- let target = idx + 1
412
- while (target < prev.length && prev[target]?.hidden) target++
413
- if (target >= prev.length) return prev
414
- next = reorderRows(prev, idx, target + 1)
415
- }
416
- if (next !== prev) newOrder = next.map(r => r.id)
417
- return next
418
- })
419
- if (newOrder !== null) rowBinding?.reorder(newOrder)
429
+ const idx = rows.findIndex(r => r.id === id)
430
+ if (idx < 0) return
431
+ // Skip past hidden neighbours so reorder operates between visible
432
+ // rows. Hidden rows hold their absolute slot — the visible row hops
433
+ // over them.
434
+ let next: RowState[]
435
+ if (dir === -1) {
436
+ let target = idx - 1
437
+ while (target >= 0 && rows[target]?.hidden) target--
438
+ if (target < 0) return
439
+ next = reorderRows(rows, idx, target)
440
+ } else {
441
+ let target = idx + 1
442
+ while (target < rows.length && rows[target]?.hidden) target++
443
+ if (target >= rows.length) return
444
+ next = reorderRows(rows, idx, target + 1)
445
+ }
446
+ if (next === rows) return
447
+ setRows(next)
448
+ rowBinding?.reorder(next.map(r => r.id))
420
449
  }
421
450
 
422
451
  // ── DnD state ───────────────────────────────────────────
@@ -429,15 +458,20 @@ export function RepeaterInput({
429
458
  } = useRowReorderDnd({
430
459
  enabled: reorderable && !disabled,
431
460
  onDrop: (fromId, at) => {
432
- let newOrder: string[] | null = null
433
- setRows(prev => {
434
- const fromIdx = prev.findIndex(r => r.id === fromId)
435
- if (fromIdx < 0) return prev
436
- const next = reorderRows(prev, fromIdx, at)
437
- if (next !== prev) newOrder = next.map(r => r.id)
438
- return next
439
- })
440
- if (newOrder !== null) rowBinding?.reorder(newOrder)
461
+ // Compute next from the current `rows` directly. The previous
462
+ // setRows(updater) + closure-mutation pattern relied on React
463
+ // running the updater synchronously inside setState — which only
464
+ // happens when no other update is queued. `useRowReorderDnd`'s
465
+ // handleDrop sets dragId/dropAt to null right before calling
466
+ // this callback, so the updater runs in commit phase and the
467
+ // outer `newOrder` stayed null past the `if` check, silently
468
+ // skipping the rowBinding.reorder broadcast.
469
+ const fromIdx = rows.findIndex(r => r.id === fromId)
470
+ if (fromIdx < 0) return
471
+ const next = reorderRows(rows, fromIdx, at)
472
+ if (next === rows) return
473
+ setRows(next)
474
+ rowBinding?.reorder(next.map(r => r.id))
441
475
  },
442
476
  })
443
477
 
@@ -0,0 +1,114 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ computeReconcilePlan,
6
+ markSubmitForReconcile,
7
+ consumeReconcileFlag,
8
+ } from './repeaterReconcile.js'
9
+
10
+ describe('computeReconcilePlan', () => {
11
+ it('returns empty plan when current and authoritative match', () => {
12
+ const plan = computeReconcilePlan({
13
+ current: ['a', 'b', 'c'],
14
+ authoritative: ['a', 'b', 'c'],
15
+ })
16
+ assert.deepEqual(plan.toRemove, [])
17
+ assert.deepEqual(plan.toAdd, [])
18
+ })
19
+
20
+ it('flags orphan CRDT rows as toRemove (PK-switch happy path)', () => {
21
+ // Submitting tab reloaded — server returned the new DB PK; CRDT
22
+ // still carries the renderer-minted UUID from the just-saved row.
23
+ const plan = computeReconcilePlan({
24
+ current: ['uuid-foo', '42'],
25
+ authoritative: ['42'],
26
+ })
27
+ assert.deepEqual(plan.toRemove, ['uuid-foo'])
28
+ assert.deepEqual(plan.toAdd, [])
29
+ })
30
+
31
+ it('flags missing CRDT rows as toAdd (raw-SQL-seeded record)', () => {
32
+ // First peer to open a record whose DB rows weren't seeded into the
33
+ // Y.Doc (no `seedRowArraysFromRecord` coverage for relationship-
34
+ // backed fields). Reconciler ensures CRDT mirrors initialRows.
35
+ const plan = computeReconcilePlan({
36
+ current: [],
37
+ authoritative: ['42', '43'],
38
+ })
39
+ assert.deepEqual(plan.toRemove, [])
40
+ assert.deepEqual(plan.toAdd, ['42', '43'])
41
+ })
42
+
43
+ it('handles both directions in a single pass', () => {
44
+ const plan = computeReconcilePlan({
45
+ current: ['uuid-foo', 'uuid-bar', '42'],
46
+ authoritative: ['42', '43'],
47
+ })
48
+ assert.deepEqual(plan.toRemove, ['uuid-foo', 'uuid-bar'])
49
+ assert.deepEqual(plan.toAdd, ['43'])
50
+ })
51
+
52
+ it('preserves order from inputs in toRemove / toAdd', () => {
53
+ const plan = computeReconcilePlan({
54
+ current: ['z', 'a', 'm'],
55
+ authoritative: ['a', 'b', 'c'],
56
+ })
57
+ // toRemove walks current in order; toAdd walks authoritative in order.
58
+ // Order-stability matters because reconciler applies them sequentially
59
+ // and we want deterministic test snapshots.
60
+ assert.deepEqual(plan.toRemove, ['z', 'm'])
61
+ assert.deepEqual(plan.toAdd, ['b', 'c'])
62
+ })
63
+ })
64
+
65
+ describe('markSubmitForReconcile / consumeReconcileFlag', () => {
66
+ // Minimal in-memory sessionStorage stub — Node lacks one, and we
67
+ // want to avoid bringing in jsdom for a flag-roundtrip test.
68
+ const realSessionStorage = (globalThis as { sessionStorage?: Storage }).sessionStorage
69
+ const store: Map<string, string> = new Map()
70
+
71
+ before(() => {
72
+ ;(globalThis as { sessionStorage?: Storage }).sessionStorage = {
73
+ get length() { return store.size },
74
+ key: (i: number) => Array.from(store.keys())[i] ?? null,
75
+ getItem: (k: string) => store.has(k) ? store.get(k)! : null,
76
+ setItem: (k: string, v: string) => { store.set(k, v) },
77
+ removeItem: (k: string) => { store.delete(k) },
78
+ clear: () => { store.clear() },
79
+ } as Storage
80
+ })
81
+
82
+ after(() => {
83
+ if (realSessionStorage === undefined) {
84
+ delete (globalThis as { sessionStorage?: Storage }).sessionStorage
85
+ } else {
86
+ (globalThis as { sessionStorage?: Storage }).sessionStorage = realSessionStorage
87
+ }
88
+ })
89
+
90
+ beforeEach(() => { store.clear() })
91
+
92
+ it('returns false when no flag has been set', () => {
93
+ assert.equal(consumeReconcileFlag('form-1'), false)
94
+ })
95
+
96
+ it('round-trips a flag and clears on first consume', () => {
97
+ markSubmitForReconcile('form-1')
98
+ assert.equal(consumeReconcileFlag('form-1'), true)
99
+ // Second read: flag was cleared on the first consume.
100
+ assert.equal(consumeReconcileFlag('form-1'), false)
101
+ })
102
+
103
+ it('scopes the flag per formId', () => {
104
+ markSubmitForReconcile('form-1')
105
+ assert.equal(consumeReconcileFlag('form-2'), false)
106
+ assert.equal(consumeReconcileFlag('form-1'), true)
107
+ })
108
+
109
+ it('no-ops on empty formId (mark and consume both)', () => {
110
+ markSubmitForReconcile('')
111
+ assert.equal(store.size, 0)
112
+ assert.equal(consumeReconcileFlag(''), false)
113
+ })
114
+ })