@pilotiq/pilotiq 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
 
2
- > @pilotiq/pilotiq@0.16.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
2
+ > @pilotiq/pilotiq@0.18.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
3
3
  > tsc -p tsconfig.build.json && pnpm run copy-assets
4
4
 
5
5
 
6
- > @pilotiq/pilotiq@0.16.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
6
+ > @pilotiq/pilotiq@0.18.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
7
7
  > mkdir -p dist/styles && cp -R src/styles/*.css dist/styles/
8
8
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @pilotiq/pilotiq
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b8c1bc: feat(pilotiq): extract `onProviderSynced(provider, fn)` helper for the seed-on-synced collab lifecycle pattern
8
+
9
+ Adapter packages that bind to a collab room (Tiptap-backed editors, the CodeMirror collab adapter) all need the same choreography on mount: if the provider's already streamed in the initial room state, run the seed callback now; otherwise register `provider.once('synced', fn)` and clean up via `provider.off?.('synced', fn)`. That gate was implemented separately in 4 renderers (`CollabTextRenderer`, `MarkdownEditor`, `TiptapEditor` in `@pilotiq/tiptap`; `CollabCodeMirrorEditor` in `@pilotiq/codemirror`).
10
+
11
+ This change extracts the pattern into a single helper in `@pilotiq/pilotiq/react` so future bug fixes in the gate logic (StrictMode double-fire, missing-off-method providers, etc.) fix in one place and so adapters from outside this monorepo can adopt the same pattern with one import.
12
+
13
+ **New public surface on `@pilotiq/pilotiq/react`:**
14
+
15
+ - `onProviderSynced(provider, fn): () => void` — runs `fn` synchronously if `provider.synced`, otherwise registers `provider.once('synced', fn)`. Returns a cleanup that safely unregisters via `try { provider.off?.('synced', fn) } catch {}`. Null/undefined provider returns a no-op cleanup.
16
+ - `SyncedProviderLike` — structural type with `synced?: boolean`, `once?(event: 'synced', fn): void`, `off?(event: 'synced', fn): void`. No yjs / y-websocket peer dep — callers cast their concrete provider via `provider as SyncedProviderLike`.
17
+
18
+ **Adapter package changes (patch-grade):**
19
+
20
+ - `@pilotiq/tiptap`: `CollabTextRenderer`, `MarkdownEditor`, and `TiptapEditor` each replace their ~10-line gate block with `return onProviderSynced(provider, trySeed)` (still inside the existing `useEffect`).
21
+ - `@pilotiq/codemirror`: `CollabCodeMirrorEditor` stores the cleanup and invokes it alongside `view.destroy()` inside the mount effect's combined cleanup.
22
+
23
+ Behavior is unchanged — no double-fire risk, no missed-cleanup risk, no API changes for callers of any of the affected renderers.
24
+
25
+ Test coverage: 6 new unit tests in `packages/pilotiq/src/react/onProviderSynced.test.ts` cover synced-now, defer-until-synced, cleanup-before-synced, null provider, off-throws, and provider-missing-once/off.
26
+
27
+ ## 0.17.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 1559a62: CodeEditorField now binds to `y-codemirror.next` when a `<RecordCollabRoom>` is mounted up-tree (parallel to `@pilotiq/tiptap`'s collab plain-text path). Each `CodeEditorField` opens a doc-root `Y.Text` keyed by either the bare field name (top-level) or `${arrayName}.${rowId}.${fieldName}` (Repeater / Builder row leaves). Opt out per-field with `.collab(false)`.
32
+
33
+ Adds optional peer deps `y-codemirror.next ^0.3` + `yjs ^13` on `@pilotiq/codemirror` (under `peerDependenciesMeta.optional` — panels without `@pilotiq-pro/collab` installed continue to work as before).
34
+
35
+ Also re-exports `useRowCoords`, `RowCoordsContext`, `parseRowFieldPath`, and `ParsedRowFieldPath` from `@pilotiq/pilotiq/react` so adapter packages (codemirror today, others later) can compose row-field collab keys consistently.
36
+
37
+ **Relationship-row code editors:** `y-codemirror.next` binds against `Y.Text`, not `Y.XmlFragment`. `@pilotiq-pro/collab`'s `rowArrayBinding.renameRow` rekeys both share types alongside one another (`applyDelta(toDelta())` for `Y.Text`, `child.clone()` for `Y.XmlFragment`), so on PK-switch (UUID → DB PK after first save) a row-leaf `CodeEditorField`'s text content carries over to the new composite key on peer B without falling back to the DB column. Trade-off is rename-by-recreate (fresh CRDT identity → discards concurrent-edit history on the renamed row's code-editor leaves), same posture as the `Y.XmlFragment` branch. Requires `@pilotiq-pro/collab` ≥ the patch that ships this rekey (`pilotiq-pro@5fae624`, 2026-05-17).
38
+
3
39
  ## 0.16.0
4
40
 
5
41
  ### Minor Changes
@@ -34,4 +34,31 @@ export interface CollabRoom {
34
34
  export declare const CollabRoomContext: import("react").Context<CollabRoom | null>;
35
35
  /** Read the active collab room for the surrounding record, or `null`. */
36
36
  export declare function useCollabRoom(): CollabRoom | null;
37
+ /**
38
+ * Minimal structural shape every collab provider exposes for the
39
+ * "initial room state has streamed in" signal. Kept structural so callers
40
+ * (`@pilotiq/tiptap`, `@pilotiq/codemirror`, future adapters) can pass
41
+ * `provider as unknown as SyncedProviderLike` without taking a hard peer
42
+ * dep on yjs / y-websocket / y-webrtc.
43
+ */
44
+ export interface SyncedProviderLike {
45
+ synced?: boolean;
46
+ once?(event: 'synced', fn: () => void): void;
47
+ off?(event: 'synced', fn: () => void): void;
48
+ }
49
+ /**
50
+ * Run `fn` once the collab provider's initial room state has streamed in.
51
+ * If the provider is already synced, `fn` fires synchronously; otherwise
52
+ * it's registered via `provider.once('synced', fn)`. The returned cleanup
53
+ * unregisters the once handler safely (idempotent + try/catch) so callers
54
+ * can wire it directly into a React effect's cleanup return.
55
+ *
56
+ * Useful for the brand-new-record seed pattern: editors mounting against
57
+ * a freshly-created record want to push the SSR-rendered default into
58
+ * the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
59
+ * the user types. Race caveat: two peers simultaneously mounting against
60
+ * a brand-new record can both see `length === 0` and both seed —
61
+ * accepted today across every adapter's seed path.
62
+ */
63
+ export declare function onProviderSynced(provider: SyncedProviderLike | null | undefined, fn: () => void): () => void;
37
64
  //# sourceMappingURL=CollabRoomContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CollabRoomContext.d.ts","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,IAAI,EAAO,OAAO,CAAA;IAClB,4DAA4D;IAC5D,QAAQ,EAAG,OAAO,CAAA;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAG,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,4CAAyC,CAAA;AAEvE,yEAAyE;AACzE,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"CollabRoomContext.d.ts","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,IAAI,EAAO,OAAO,CAAA;IAClB,4DAA4D;IAC5D,QAAQ,EAAG,OAAO,CAAA;IAClB,+EAA+E;IAC/E,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAG,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,4CAAyC,CAAA;AAEvE,yEAAyE;AACzE,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC5C,GAAG,CAAC,CAAC,KAAK,EAAG,QAAQ,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAC7C;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,EAC/C,EAAE,EAAQ,MAAM,IAAI,GACnB,MAAM,IAAI,CAUZ"}
@@ -9,4 +9,34 @@ export const CollabRoomContext = createContext(null);
9
9
  export function useCollabRoom() {
10
10
  return useContext(CollabRoomContext);
11
11
  }
12
+ const NOOP_CLEANUP = () => { };
13
+ /**
14
+ * Run `fn` once the collab provider's initial room state has streamed in.
15
+ * If the provider is already synced, `fn` fires synchronously; otherwise
16
+ * it's registered via `provider.once('synced', fn)`. The returned cleanup
17
+ * unregisters the once handler safely (idempotent + try/catch) so callers
18
+ * can wire it directly into a React effect's cleanup return.
19
+ *
20
+ * Useful for the brand-new-record seed pattern: editors mounting against
21
+ * a freshly-created record want to push the SSR-rendered default into
22
+ * the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
23
+ * the user types. Race caveat: two peers simultaneously mounting against
24
+ * a brand-new record can both see `length === 0` and both seed —
25
+ * accepted today across every adapter's seed path.
26
+ */
27
+ export function onProviderSynced(provider, fn) {
28
+ if (!provider)
29
+ return NOOP_CLEANUP;
30
+ if (provider.synced) {
31
+ fn();
32
+ return NOOP_CLEANUP;
33
+ }
34
+ provider.once?.('synced', fn);
35
+ return () => {
36
+ try {
37
+ provider.off?.('synced', fn);
38
+ }
39
+ catch { /* ignore */ }
40
+ };
41
+ }
12
42
  //# sourceMappingURL=CollabRoomContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CollabRoomContext.js","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AA+BjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAoB,IAAI,CAAC,CAAA;AAEvE,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAA;AACtC,CAAC"}
1
+ {"version":3,"file":"CollabRoomContext.js","sourceRoot":"","sources":["../../src/react/CollabRoomContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AA+BjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAoB,IAAI,CAAC,CAAA;AAEvE,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAA;AACtC,CAAC;AAeD,MAAM,YAAY,GAAG,GAAS,EAAE,GAAE,CAAC,CAAA;AAEnC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAA+C,EAC/C,EAAoB;IAEpB,IAAI,CAAC,QAAQ;QAAE,OAAO,YAAY,CAAA;IAClC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,EAAE,EAAE,CAAA;QACJ,OAAO,YAAY,CAAA;IACrB,CAAC;IACD,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAC7B,OAAO,GAAG,EAAE;QACV,IAAI,CAAC;YAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAC7D,CAAC,CAAA;AACH,CAAC"}
@@ -6,7 +6,7 @@ export { registerFieldLabelSlot, getFieldLabelSlot, type FieldLabelSlotProps } f
6
6
  export { registerPendingSuggestionOverlay, getPendingSuggestionOverlay, type PendingSuggestionOverlayProps, } from './PendingSuggestionOverlayRegistry.js';
7
7
  export { PendingSuggestionsContext, usePendingSuggestions, usePendingSuggestionsForField, type PendingSuggestion, type PendingSuggestionOrigin, type PendingSuggestionsApi, } from './PendingSuggestionsContext.js';
8
8
  export { registerPendingSuggestionApplier, getPendingSuggestionApplier, type PendingSuggestionApplier, } from './PendingSuggestionApplierRegistry.js';
9
- export { CollabRoomContext, useCollabRoom, type CollabRoom, } from './CollabRoomContext.js';
9
+ export { CollabRoomContext, useCollabRoom, onProviderSynced, type CollabRoom, type SyncedProviderLike, } from './CollabRoomContext.js';
10
10
  export { registerCollabExtensions, getCollabExtensions, type CollabExtensionFactory, type CollabExtensionFactoryArgs, } from './CollabExtensionFactoryRegistry.js';
11
11
  export { registerCollabTextRenderer, getCollabTextRenderer, type CollabTextRenderer, type CollabTextRendererProps, } from './CollabTextRendererRegistry.js';
12
12
  export { registerMarkdownEditor, getMarkdownEditor, type MarkdownEditor, type MarkdownEditorProps, } from './MarkdownEditorRegistry.js';
@@ -20,7 +20,8 @@ export { CustomPageWrapperGate, type CustomPageWrapperGateProps, type PageCollab
20
20
  export { parseRecordPageUrl, parseRecordEditUrl, type RecordPageIdentity, type RecordPageRole, type RecordEditIdentity, } from './parseRecordEditUrl.js';
21
21
  export { registerWidgetRenderer, getWidgetRenderer, type WidgetRendererProps, } from './widgetRegistry.js';
22
22
  export { FormStateProvider, useFieldState, useFormState, useRowBinding, type FormStateApi, type FormStateProviderProps, type UseFieldStateResult, } from './FormStateContext.js';
23
- export { parseFormDataToNested } from './formStateHelpers.js';
23
+ export { parseFormDataToNested, parseRowFieldPath, type ParsedRowFieldPath } from './formStateHelpers.js';
24
+ export { RowCoordsContext, useRowCoords, type RowCoords } from './RowCoordsContext.js';
24
25
  export { NavigateProvider, useNavigate, type NavigateFn } from './navigate.js';
25
26
  export { ToasterProvider, useToast } from './Toaster.js';
26
27
  export { WidgetDataProvider, useInitialWidgetData, useWidgetData, type WidgetState, type WidgetMetaLike, type WidgetDataProviderProps, } from './WidgetDataContext.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@ export { registerFieldLabelSlot, getFieldLabelSlot } from './FieldLabelSlotRegis
6
6
  export { registerPendingSuggestionOverlay, getPendingSuggestionOverlay, } from './PendingSuggestionOverlayRegistry.js';
7
7
  export { PendingSuggestionsContext, usePendingSuggestions, usePendingSuggestionsForField, } from './PendingSuggestionsContext.js';
8
8
  export { registerPendingSuggestionApplier, getPendingSuggestionApplier, } from './PendingSuggestionApplierRegistry.js';
9
- export { CollabRoomContext, useCollabRoom, } from './CollabRoomContext.js';
9
+ export { CollabRoomContext, useCollabRoom, onProviderSynced, } from './CollabRoomContext.js';
10
10
  export { registerCollabExtensions, getCollabExtensions, } from './CollabExtensionFactoryRegistry.js';
11
11
  export { registerCollabTextRenderer, getCollabTextRenderer, } from './CollabTextRendererRegistry.js';
12
12
  export { registerMarkdownEditor, getMarkdownEditor, } from './MarkdownEditorRegistry.js';
@@ -20,7 +20,8 @@ export { CustomPageWrapperGate, } from './CustomPageWrapperGate.js';
20
20
  export { parseRecordPageUrl, parseRecordEditUrl, } from './parseRecordEditUrl.js';
21
21
  export { registerWidgetRenderer, getWidgetRenderer, } from './widgetRegistry.js';
22
22
  export { FormStateProvider, useFieldState, useFormState, useRowBinding, } from './FormStateContext.js';
23
- export { parseFormDataToNested } from './formStateHelpers.js';
23
+ export { parseFormDataToNested, parseRowFieldPath } from './formStateHelpers.js';
24
+ export { RowCoordsContext, useRowCoords } from './RowCoordsContext.js';
24
25
  export { NavigateProvider, useNavigate } from './navigate.js';
25
26
  export { ToasterProvider, useToast } from './Toaster.js';
26
27
  export { WidgetDataProvider, useInitialWidgetData, useWidgetData, } from './WidgetDataContext.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/pilotiq",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "View-based admin panel for RudderJS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -987,6 +987,101 @@ describe('formStateData (Plan #5)', () => {
987
987
  const formMeta = result.form as { values?: Record<string, unknown> }
988
988
  assert.equal(formMeta.values?.['slug'], 'hello-world')
989
989
  })
990
+
991
+ // Regression lock — reactive `itemHidden` end-to-end. Server-side resolve
992
+ // alone is covered in `RepeaterField.test.ts` / `BuilderField.test.ts`, and
993
+ // the client-side row-gate sync is covered in `syncRowGates.test.ts`. This
994
+ // covers the wire between them: applyStateUpdate of a row-leaf dotted
995
+ // path, then full resolveSchema, with the `itemHidden` rule reading the
996
+ // updated row value. If this regresses, peer A typing into a `live()`
997
+ // inner field would never flip the row's chrome on a real form.
998
+
999
+ it('re-evaluates Repeater itemHidden after a live() inner-leaf cycle', async () => {
1000
+ class TestPage extends Page {
1001
+ static override slug = 'demo'
1002
+ static override schema() {
1003
+ return [Form.make().formId('the-form').schema([
1004
+ Repeater.make('items')
1005
+ .schema([
1006
+ TextField.make('mode').live(),
1007
+ TextField.make('label'),
1008
+ ])
1009
+ .itemHidden(({ values }) => (values as Record<string, unknown>)['mode'] === 'hidden'),
1010
+ ])]
1011
+ }
1012
+ }
1013
+ const panel = Pilotiq.make('T').path('/admin').pages([TestPage])
1014
+
1015
+ // Before: row is visible.
1016
+ const visible = await formStateData(
1017
+ panel,
1018
+ { kind: 'page', pageSlug: 'demo' },
1019
+ { formId: 'the-form', changed: 'items.0.mode', values: { items: [{ mode: 'visible', label: 'one' }] } },
1020
+ )
1021
+ if (visible === null || !visible.ok) throw new Error('expected ok result')
1022
+ const visibleMeta = visible.form as { children: Array<{ rows: Array<{ id: string; hidden?: boolean }> }> }
1023
+ assert.equal(visibleMeta.children[0]?.rows[0]?.hidden, undefined)
1024
+
1025
+ // After: same row, `mode` flipped to `'hidden'` — itemHidden re-evaluates.
1026
+ const hidden = await formStateData(
1027
+ panel,
1028
+ { kind: 'page', pageSlug: 'demo' },
1029
+ { formId: 'the-form', changed: 'items.0.mode', values: { items: [{ mode: 'hidden', label: 'one' }] } },
1030
+ )
1031
+ if (hidden === null || !hidden.ok) throw new Error('expected ok result')
1032
+ const hiddenMeta = hidden.form as { children: Array<{ rows: Array<{ id: string; hidden?: boolean }> }> }
1033
+ assert.equal(hiddenMeta.children[0]?.rows[0]?.hidden, true)
1034
+ // Row id stays stable across the cycle — syncRowGates on the client
1035
+ // matches on `id`, so an unstable id would silently skip the hidden
1036
+ // flip.
1037
+ assert.equal(hiddenMeta.children[0]?.rows[0]?.id, visibleMeta.children[0]?.rows[0]?.id)
1038
+ })
1039
+
1040
+ it('re-evaluates Builder itemHidden after a live() block-leaf cycle', async () => {
1041
+ class TestPage extends Page {
1042
+ static override slug = 'demo'
1043
+ static override schema() {
1044
+ return [Form.make().formId('the-form').schema([
1045
+ Builder.make('content')
1046
+ .blocks([
1047
+ Block.make('heading').schema([
1048
+ TextField.make('text').live(),
1049
+ TextField.make('anchor'),
1050
+ ]),
1051
+ ])
1052
+ .itemHidden(({ values }) => (values as Record<string, unknown>)['text'] === 'skip'),
1053
+ ])]
1054
+ }
1055
+ }
1056
+ const panel = Pilotiq.make('T').path('/admin').pages([TestPage])
1057
+
1058
+ const keep = await formStateData(
1059
+ panel,
1060
+ { kind: 'page', pageSlug: 'demo' },
1061
+ {
1062
+ formId: 'the-form',
1063
+ changed: 'content.0.data.text',
1064
+ values: { content: [{ type: 'heading', data: { text: 'keep', anchor: '' } }] },
1065
+ },
1066
+ )
1067
+ if (keep === null || !keep.ok) throw new Error('expected ok result')
1068
+ const keepMeta = keep.form as { children: Array<{ rows: Array<{ id: string; hidden?: boolean }> }> }
1069
+ assert.equal(keepMeta.children[0]?.rows[0]?.hidden, undefined)
1070
+
1071
+ const skip = await formStateData(
1072
+ panel,
1073
+ { kind: 'page', pageSlug: 'demo' },
1074
+ {
1075
+ formId: 'the-form',
1076
+ changed: 'content.0.data.text',
1077
+ values: { content: [{ type: 'heading', data: { text: 'skip', anchor: '' } }] },
1078
+ },
1079
+ )
1080
+ if (skip === null || !skip.ok) throw new Error('expected ok result')
1081
+ const skipMeta = skip.form as { children: Array<{ rows: Array<{ id: string; hidden?: boolean }> }> }
1082
+ assert.equal(skipMeta.children[0]?.rows[0]?.hidden, true)
1083
+ assert.equal(skipMeta.children[0]?.rows[0]?.id, keepMeta.children[0]?.rows[0]?.id)
1084
+ })
990
1085
  })
991
1086
 
992
1087
  describe('mentionResolveData (async mention items)', () => {
@@ -40,3 +40,47 @@ export const CollabRoomContext = createContext<CollabRoom | null>(null)
40
40
  export function useCollabRoom(): CollabRoom | null {
41
41
  return useContext(CollabRoomContext)
42
42
  }
43
+
44
+ /**
45
+ * Minimal structural shape every collab provider exposes for the
46
+ * "initial room state has streamed in" signal. Kept structural so callers
47
+ * (`@pilotiq/tiptap`, `@pilotiq/codemirror`, future adapters) can pass
48
+ * `provider as unknown as SyncedProviderLike` without taking a hard peer
49
+ * dep on yjs / y-websocket / y-webrtc.
50
+ */
51
+ export interface SyncedProviderLike {
52
+ synced?: boolean
53
+ once?(event: 'synced', fn: () => void): void
54
+ off?(event: 'synced', fn: () => void): void
55
+ }
56
+
57
+ const NOOP_CLEANUP = (): void => {}
58
+
59
+ /**
60
+ * Run `fn` once the collab provider's initial room state has streamed in.
61
+ * If the provider is already synced, `fn` fires synchronously; otherwise
62
+ * it's registered via `provider.once('synced', fn)`. The returned cleanup
63
+ * unregisters the once handler safely (idempotent + try/catch) so callers
64
+ * can wire it directly into a React effect's cleanup return.
65
+ *
66
+ * Useful for the brand-new-record seed pattern: editors mounting against
67
+ * a freshly-created record want to push the SSR-rendered default into
68
+ * the empty `Y.Text` / `Y.XmlFragment` exactly once after sync, before
69
+ * the user types. Race caveat: two peers simultaneously mounting against
70
+ * a brand-new record can both see `length === 0` and both seed —
71
+ * accepted today across every adapter's seed path.
72
+ */
73
+ export function onProviderSynced(
74
+ provider: SyncedProviderLike | null | undefined,
75
+ fn: () => void,
76
+ ): () => void {
77
+ if (!provider) return NOOP_CLEANUP
78
+ if (provider.synced) {
79
+ fn()
80
+ return NOOP_CLEANUP
81
+ }
82
+ provider.once?.('synced', fn)
83
+ return () => {
84
+ try { provider.off?.('synced', fn) } catch { /* ignore */ }
85
+ }
86
+ }
@@ -35,7 +35,9 @@ export {
35
35
  export {
36
36
  CollabRoomContext,
37
37
  useCollabRoom,
38
+ onProviderSynced,
38
39
  type CollabRoom,
40
+ type SyncedProviderLike,
39
41
  } from './CollabRoomContext.js'
40
42
  export {
41
43
  registerCollabExtensions,
@@ -117,7 +119,8 @@ export {
117
119
  type UseFieldStateResult,
118
120
  } from './FormStateContext.js'
119
121
 
120
- export { parseFormDataToNested } from './formStateHelpers.js'
122
+ export { parseFormDataToNested, parseRowFieldPath, type ParsedRowFieldPath } from './formStateHelpers.js'
123
+ export { RowCoordsContext, useRowCoords, type RowCoords } from './RowCoordsContext.js'
121
124
 
122
125
  export { NavigateProvider, useNavigate, type NavigateFn } from './navigate.js'
123
126
 
@@ -0,0 +1,90 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { onProviderSynced, type SyncedProviderLike } from './CollabRoomContext.js'
5
+
6
+ /** Bare provider double — captures `once` registrations and supports
7
+ * flipping `synced` so we can assert the deferred path. */
8
+ function mockProvider(initialSynced = false): SyncedProviderLike & {
9
+ fire(): void
10
+ registered: number
11
+ unregistered: number
12
+ offThrows: boolean
13
+ } {
14
+ let synced = initialSynced
15
+ let pending: (() => void) | null = null
16
+ const inst = {
17
+ get synced() { return synced },
18
+ set synced(v: boolean) { synced = v },
19
+ once(event: 'synced', fn: () => void) {
20
+ assert.equal(event, 'synced', 'only synced is registered')
21
+ pending = fn
22
+ inst.registered++
23
+ },
24
+ off(event: 'synced', fn: () => void) {
25
+ assert.equal(event, 'synced', 'only synced is unregistered')
26
+ if (inst.offThrows) throw new Error('boom')
27
+ if (pending === fn) pending = null
28
+ inst.unregistered++
29
+ },
30
+ fire() { if (pending) { const f = pending; pending = null; f() } },
31
+ registered: 0,
32
+ unregistered: 0,
33
+ offThrows: false,
34
+ }
35
+ return inst
36
+ }
37
+
38
+ describe('onProviderSynced', () => {
39
+ it('fires fn synchronously when provider is already synced', () => {
40
+ const provider = mockProvider(true)
41
+ let calls = 0
42
+ const cleanup = onProviderSynced(provider, () => { calls++ })
43
+ assert.equal(calls, 1, 'fn ran synchronously')
44
+ assert.equal(provider.registered, 0, 'once never registered')
45
+ cleanup()
46
+ assert.equal(provider.unregistered, 0, 'no-op cleanup did not call off')
47
+ })
48
+
49
+ it('defers fn until synced event when provider not yet synced', () => {
50
+ const provider = mockProvider(false)
51
+ let calls = 0
52
+ const cleanup = onProviderSynced(provider, () => { calls++ })
53
+ assert.equal(calls, 0, 'fn not yet called')
54
+ assert.equal(provider.registered, 1, 'once was registered')
55
+ provider.fire()
56
+ assert.equal(calls, 1, 'fn ran after synced fired')
57
+ cleanup()
58
+ assert.equal(provider.unregistered, 1, 'cleanup called off')
59
+ })
60
+
61
+ it('cleanup unregisters the once handler when called before synced fires', () => {
62
+ const provider = mockProvider(false)
63
+ let calls = 0
64
+ const cleanup = onProviderSynced(provider, () => { calls++ })
65
+ cleanup()
66
+ assert.equal(provider.unregistered, 1, 'off was called')
67
+ provider.fire()
68
+ assert.equal(calls, 0, 'fn never fired after cleanup')
69
+ })
70
+
71
+ it('returns a no-op cleanup when provider is null or undefined', () => {
72
+ const noopCleanup = onProviderSynced(null, () => { throw new Error('unreachable') })
73
+ assert.doesNotThrow(() => noopCleanup())
74
+ const noopCleanup2 = onProviderSynced(undefined, () => { throw new Error('unreachable') })
75
+ assert.doesNotThrow(() => noopCleanup2())
76
+ })
77
+
78
+ it('swallows errors thrown by provider.off so cleanup never breaks an effect', () => {
79
+ const provider = mockProvider(false)
80
+ provider.offThrows = true
81
+ const cleanup = onProviderSynced(provider, () => {})
82
+ assert.doesNotThrow(cleanup, 'cleanup did not propagate the throw')
83
+ })
84
+
85
+ it('gracefully handles a provider missing once/off methods', () => {
86
+ const sparse: SyncedProviderLike = { synced: false }
87
+ const cleanup = onProviderSynced(sparse, () => {})
88
+ assert.doesNotThrow(cleanup, 'cleanup is safe when off is undefined')
89
+ })
90
+ })