@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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +6 -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 +16 -0
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
- package/dist/react/FormCollabBindingRegistry.js.map +1 -1
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +8 -1
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/fields/BuilderInput.d.ts.map +1 -1
- package/dist/react/fields/BuilderInput.js +64 -40
- package/dist/react/fields/BuilderInput.js.map +1 -1
- package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
- package/dist/react/fields/RepeaterInput.js +78 -43
- package/dist/react/fields/RepeaterInput.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/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 +17 -0
- package/src/react/FormStateContext.tsx +8 -1
- package/src/react/fields/BuilderInput.tsx +53 -29
- package/src/react/fields/RepeaterInput.tsx +66 -32
- package/src/react/fields/repeaterReconcile.test.ts +114 -0
- package/src/react/fields/repeaterReconcile.ts +104 -0
- 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;
|
|
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
package/src/pageData/helpers.ts
CHANGED
|
@@ -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
|
/**
|
package/src/pageData.test.ts
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
let
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
398
|
-
setRows
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
433
|
-
setRows(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
})
|