@pilotiq/pilotiq 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +120 -0
- package/dist/react/FormCollabBindingRegistry.d.ts +130 -0
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
- package/dist/react/FormCollabBindingRegistry.js.map +1 -1
- package/dist/react/FormStateContext.d.ts +39 -1
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +126 -33
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/fields/BuilderInput.d.ts.map +1 -1
- package/dist/react/fields/BuilderInput.js +112 -10
- 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 +113 -10
- package/dist/react/fields/RepeaterInput.js.map +1 -1
- package/dist/react/formStateHelpers.d.ts +102 -0
- package/dist/react/formStateHelpers.d.ts.map +1 -1
- package/dist/react/formStateHelpers.js +234 -0
- package/dist/react/formStateHelpers.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
- package/src/react/FormCollabBindingRegistry.ts +129 -0
- package/src/react/FormStateContext.tsx +157 -34
- package/src/react/fields/BuilderInput.tsx +97 -8
- package/src/react/fields/RepeaterInput.tsx +97 -8
- package/src/react/formStateHelpers.test.ts +312 -0
- package/src/react/formStateHelpers.ts +246 -0
- package/src/react/index.ts +3 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @pilotiq/pilotiq@0.
|
|
2
|
+
> @pilotiq/pilotiq@0.11.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.
|
|
6
|
+
> @pilotiq/pilotiq@0.11.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,125 @@
|
|
|
1
1
|
# @pilotiq/pilotiq
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d36902d: feat(pilotiq): F.5a — Repeater/Builder row-identity contract for collab
|
|
8
|
+
|
|
9
|
+
Widens `FormCollabBinding` with five optional row-array methods plus a
|
|
10
|
+
`RowsEvent` type so the upcoming `Y.Array<Y.Map>` impl in
|
|
11
|
+
`@pilotiq-pro/collab` (F.5b) has a stable surface to hook into. Renderer
|
|
12
|
+
side wiring is live in this release — `RepeaterInput` + `BuilderInput`
|
|
13
|
+
already dispatch `add` / `remove` / `reorder` / `subscribe` through the
|
|
14
|
+
binding when one is registered and reconcile remote row events into
|
|
15
|
+
their local state by `__id`. No behaviour change for non-collab forms
|
|
16
|
+
or bindings that pre-date F.5; pre-F.5 bindings keep typechecking
|
|
17
|
+
because every new method is optional.
|
|
18
|
+
|
|
19
|
+
### New public surface
|
|
20
|
+
|
|
21
|
+
- **`useRowBinding(arrayName)`** — returns a `RowBindingApi` pre-bound
|
|
22
|
+
to a Repeater/Builder field name, or `null` when no F.5 binding is
|
|
23
|
+
active (outside a collab room, pre-F.5 plugin, opted out via
|
|
24
|
+
`.collab(false)`, or non-array field).
|
|
25
|
+
- **`RowBindingApi`** — `{ add, remove, reorder, subscribe }`. Each
|
|
26
|
+
method's `arrayName` arg is pre-bound; `subscribe(fn)` returns an
|
|
27
|
+
unsubscribe function for `useEffect` cleanup.
|
|
28
|
+
- **`RowsEvent`** — `add | remove | move` discriminated union with
|
|
29
|
+
`rowId` + indices for the renderer to reconcile against its current
|
|
30
|
+
`rows` state.
|
|
31
|
+
- **`FormCollabBinding.addRow / removeRow / reorderRows / setRow /
|
|
32
|
+
getRowTextBinding / subscribeRows`** — all optional. Bindings opt
|
|
33
|
+
into F.5 by implementing the trio `addRow + removeRow + reorderRows`;
|
|
34
|
+
`subscribeRows` and `setRow` layer on for remote-event + dotted-path
|
|
35
|
+
routing; `getRowTextBinding` is reserved for F.5c (per-row `Y.Text`).
|
|
36
|
+
|
|
37
|
+
### `FormStateProvider` routing
|
|
38
|
+
|
|
39
|
+
- `setValue` and the live-resolve overlay both route through
|
|
40
|
+
`routeBindingWrite` — top-level names go to `binding.set`, row leaves
|
|
41
|
+
(matching `parseRowFieldPath`) go to `binding.setRow` when available.
|
|
42
|
+
Pre-F.5 row leaves continue to stay local-only.
|
|
43
|
+
- The provider walks `formMeta` for top-level Repeater/Builder field
|
|
44
|
+
names at binding mount and builds a per-array `RowBindingApi` map
|
|
45
|
+
exposed via `useRowBinding`.
|
|
46
|
+
|
|
47
|
+
### Known v1 limitations (kept from the F.5 plan)
|
|
48
|
+
|
|
49
|
+
- Nested Repeaters (e.g. `articles.0.comments.0.body`) stay local-only
|
|
50
|
+
— `parseRowFieldPath` returns `null` and the binding never sees them.
|
|
51
|
+
- Server-derived row values now propagate through `setRow` when
|
|
52
|
+
available; without an F.5 binding they continue to be dropped.
|
|
53
|
+
- F.5c (`getRowTextBinding`) — character-level `Y.Text` per row text
|
|
54
|
+
field — lands in a follow-up; row leaves stay on row-level LWW until
|
|
55
|
+
then.
|
|
56
|
+
|
|
57
|
+
- b70cb49: feat(pilotiq): F.5c — per-row Y.Text composition with F.6
|
|
58
|
+
|
|
59
|
+
Wires `useFieldState(dottedName).textBinding` to resolve through
|
|
60
|
+
`FormCollabBinding.getRowTextBinding(arrayName, rowId, fieldName)` so
|
|
61
|
+
Repeater/Builder row text fields ride character-level CRDT when the
|
|
62
|
+
plugin implements F.5c. Previously dotted-name `textBinding` always
|
|
63
|
+
returned `null`; now it returns a stable handle when:
|
|
64
|
+
|
|
65
|
+
- the row's `__id` is already stamped in the values map,
|
|
66
|
+
- the inner field's `fieldType` is in the F.6 allowlist
|
|
67
|
+
(`text / textarea / email / slug / markdown`),
|
|
68
|
+
- the field isn't opted out via `.collab(false)`,
|
|
69
|
+
- the active binding implements `getRowTextBinding`.
|
|
70
|
+
|
|
71
|
+
### Walker
|
|
72
|
+
|
|
73
|
+
A new `collectRowTextLeavesByArray(formMeta)` helper walks each
|
|
74
|
+
Repeater's inner schema + each Builder block's template once at
|
|
75
|
+
binding mount and stashes the per-array text-leaf names on
|
|
76
|
+
`FormStateApi.rowTextLeaves`. Nested Repeater/Builder boundaries stop
|
|
77
|
+
the walk — 5-segment dotted paths remain out of scope.
|
|
78
|
+
|
|
79
|
+
### Renderer surface unchanged
|
|
80
|
+
|
|
81
|
+
`BoundTextInput` already branches on `textBinding != null` from F.6,
|
|
82
|
+
so rows pick up the character-level path automatically once an F.5c-
|
|
83
|
+
capable binding is registered. No new renderer wiring beyond the
|
|
84
|
+
walker + `useFieldState` resolver.
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- 08ab5bb: fix(pilotiq): F.5c row-text integration — stamp row `__id` and walk `template` not `children`
|
|
89
|
+
|
|
90
|
+
Two integration gaps in the just-shipped F.5c per-row Y.Text path made
|
|
91
|
+
character-level CRDT silently fall back to LWW for every Repeater /
|
|
92
|
+
Builder row text leaf. Both green-CI / broken-at-render bugs.
|
|
93
|
+
|
|
94
|
+
### `collectRowTextLeavesByArray` walked the wrong meta key
|
|
95
|
+
|
|
96
|
+
The walker read `meta.children` for the Repeater's inner row schema,
|
|
97
|
+
but `RepeaterField.toMeta()` emits the row schema under `meta.template`
|
|
98
|
+
(`meta.children` is the per-resolved-row child list, not the field-
|
|
99
|
+
level template). Walker always returned empty → `FormStateApi.rowTextLeaves`
|
|
100
|
+
stayed `null` → `useFieldState(dottedName).textBinding` short-circuited
|
|
101
|
+
on every dotted row-leaf name. The unit-test fixture mirrored the same
|
|
102
|
+
wrong shape, so CI passed while the renderer was inert.
|
|
103
|
+
|
|
104
|
+
### `RepeaterInput` / `BuilderInput` never stamped row `__id` in `ctx.values`
|
|
105
|
+
|
|
106
|
+
`resolveRowTextBinding` looks up `rowIdAtIndex(ctx.values, name, i)` which
|
|
107
|
+
reads `values['${name}.${i}.__id']`. The renderer maintained row identity
|
|
108
|
+
in local component state but never mirrored it into `ctx.values`, so the
|
|
109
|
+
lookup returned `null` and the binding chain never fired — even for
|
|
110
|
+
locally-added rows.
|
|
111
|
+
|
|
112
|
+
Both renderers now mirror `rows` into `ctx.values` via a single
|
|
113
|
+
`useEffect` keyed on the rows array. Pre-existing server-seeded rows
|
|
114
|
+
were unaffected because the seed wasn't a renderer concern; only
|
|
115
|
+
locally-added or remote-reconciled rows hit the gap.
|
|
116
|
+
|
|
117
|
+
### Tests
|
|
118
|
+
|
|
119
|
+
`formStateHelpers.test.ts`'s hand-built `repeater()` helper now emits
|
|
120
|
+
`template:` instead of `children:` to match `RepeaterField.toMeta()`.
|
|
121
|
+
Catches future drift between meta producers and walkers.
|
|
122
|
+
|
|
3
123
|
## 0.10.0
|
|
4
124
|
|
|
5
125
|
### Minor Changes
|
|
@@ -89,7 +89,137 @@ export interface FormCollabBinding {
|
|
|
89
89
|
getTextBinding?(name: string): TextBinding | null;
|
|
90
90
|
/** Cleanup hook called when the form unmounts. */
|
|
91
91
|
destroy(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Add a row to a Repeater or Builder field. `rowId` is the stable
|
|
94
|
+
* `__id` the renderer already mints (UUID for new rows / DB PK for
|
|
95
|
+
* relationship-backed rows) — the binding indexes the row's CRDT
|
|
96
|
+
* surface by this id so concurrent inserts from peers both survive.
|
|
97
|
+
*
|
|
98
|
+
* Idempotent — calling with a `rowId` that already exists in the
|
|
99
|
+
* Repeater's array is a no-op. `initial` may carry pre-filled values
|
|
100
|
+
* for the new row (e.g. the inner fields' `default()` from schema
|
|
101
|
+
* resolution); the binding seeds them onto the row's storage under
|
|
102
|
+
* the same idempotent posture as top-level `set` (first writer wins
|
|
103
|
+
* across concurrent first-mounters).
|
|
104
|
+
*
|
|
105
|
+
* Renderer call sites: `RepeaterInput.addRow()` + `BuilderInput.addBlock()`.
|
|
106
|
+
* When absent, F.5 row-level CRDT degrades to v1 behaviour (whole-array
|
|
107
|
+
* LWW under the top-level Y.Map).
|
|
108
|
+
*/
|
|
109
|
+
addRow?(arrayName: string, rowId: string, initial: Record<string, unknown>): void;
|
|
110
|
+
/**
|
|
111
|
+
* Remove a row from a Repeater/Builder field by stable id. Idempotent
|
|
112
|
+
* — a missing `rowId` is a no-op. Triggered by the renderer's row
|
|
113
|
+
* delete button.
|
|
114
|
+
*/
|
|
115
|
+
removeRow?(arrayName: string, rowId: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Apply a new row order. `newOrder` is the full list of row ids in
|
|
118
|
+
* their final positions; the binding computes the minimal CRDT move
|
|
119
|
+
* sequence and applies it inside a single transaction (peers see one
|
|
120
|
+
* coalesced update, not N intermediate states).
|
|
121
|
+
*
|
|
122
|
+
* Called by drag-and-drop / up-down move handlers in
|
|
123
|
+
* `RepeaterInput` + `BuilderInput`.
|
|
124
|
+
*/
|
|
125
|
+
reorderRows?(arrayName: string, newOrder: string[]): void;
|
|
126
|
+
/**
|
|
127
|
+
* Write a single field on a row. Replaces the dotted-path `set` for
|
|
128
|
+
* row leaves (`tags.0.label` → `setRow('tags', rowId, 'label', value)`).
|
|
129
|
+
*
|
|
130
|
+
* Binding routes by allowlist same as top-level `set`: text-shaped
|
|
131
|
+
* fields go through the row's `TextBinding` when a `Y.Text` exists
|
|
132
|
+
* (see `getRowTextBinding`); non-text fields land on the row's
|
|
133
|
+
* scalar field-map under LWW.
|
|
134
|
+
*
|
|
135
|
+
* `FormStateProvider` calls this on every local edit when both a
|
|
136
|
+
* dotted-path name is being written AND `setRow` is implemented;
|
|
137
|
+
* otherwise the v1 skip-on-dot path runs.
|
|
138
|
+
*/
|
|
139
|
+
setRow?(arrayName: string, rowId: string, fieldName: string, value: unknown): void;
|
|
140
|
+
/**
|
|
141
|
+
* Per-row text-CRDT handle. Composes with F.6's `BoundTextInput`:
|
|
142
|
+
* the renderer asks for one of these when a row leaf is text-shaped
|
|
143
|
+
* AND not opted out via `.collab(false)`, then threads it through
|
|
144
|
+
* the existing `TextBinding` plumbing inside the row.
|
|
145
|
+
*
|
|
146
|
+
* Returns `null` for non-text fields, fields opted out via collab,
|
|
147
|
+
* rows not yet present in the binding's index (e.g. before
|
|
148
|
+
* `addRow` has propagated), or when the binding doesn't yet
|
|
149
|
+
* implement per-row text CRDT (deferred to F.5c).
|
|
150
|
+
*/
|
|
151
|
+
getRowTextBinding?(arrayName: string, rowId: string, fieldName: string): TextBinding | null;
|
|
152
|
+
/**
|
|
153
|
+
* Subscribe to row-lifecycle events for a Repeater/Builder array.
|
|
154
|
+
* Fires on remote add / remove / move; the renderer reconciles its
|
|
155
|
+
* `rows` state by `__id`. Local mutations fire too — the renderer
|
|
156
|
+
* deduplicates by checking whether the event's `rowId` matches one
|
|
157
|
+
* it just dispatched itself (the binding's roundtrip + the
|
|
158
|
+
* renderer's optimistic update converge).
|
|
159
|
+
*/
|
|
160
|
+
subscribeRows?(arrayName: string, fn: (event: RowsEvent) => void): () => void;
|
|
92
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Phase F.5 — array-scoped imperative API exposed to Repeater / Builder
|
|
164
|
+
* renderers through `useRowBinding(arrayName)`. Each method's first arg
|
|
165
|
+
* (`arrayName`) is pre-bound by the hook so call sites read naturally.
|
|
166
|
+
*
|
|
167
|
+
* Returned by the hook only when the active binding implements all four
|
|
168
|
+
* F.5 row methods (`addRow + removeRow + reorderRows`); partial impls
|
|
169
|
+
* read as null so renderers can do a single existence check before
|
|
170
|
+
* proceeding. `setRow` is intentionally NOT exposed here — it's invoked
|
|
171
|
+
* implicitly via `FormStateProvider.setValue`'s dotted-path routing,
|
|
172
|
+
* so renderers never need to touch it directly.
|
|
173
|
+
*/
|
|
174
|
+
export interface RowBindingApi {
|
|
175
|
+
/** Register a new row in the array's CRDT surface. `initial` may
|
|
176
|
+
* carry pre-seeded values (e.g. clone of a sibling); pass `{}` (or
|
|
177
|
+
* omit) for empty rows. Idempotent under existing rowId. */
|
|
178
|
+
add(rowId: string, initial?: Record<string, unknown>): void;
|
|
179
|
+
/** Remove the row with the given id. Idempotent for unknown ids. */
|
|
180
|
+
remove(rowId: string): void;
|
|
181
|
+
/** Replace the array's row order. `newOrder` is the full list of row
|
|
182
|
+
* ids in their post-reorder positions; the binding emits the minimal
|
|
183
|
+
* CRDT move sequence in one transaction so peers observe a single
|
|
184
|
+
* coalesced update. */
|
|
185
|
+
reorder(newOrder: string[]): void;
|
|
186
|
+
/** Subscribe to remote row-lifecycle events on this array. Returns
|
|
187
|
+
* an unsubscribe fn the renderer's `useEffect` cleanup hangs on.
|
|
188
|
+
* Local mutations may also surface here (Yjs observers fire on
|
|
189
|
+
* local transactions); the renderer is expected to dedupe by
|
|
190
|
+
* rowId presence in its current state. Optional on the underlying
|
|
191
|
+
* contract — `null` when the binding lacks `subscribeRows`. */
|
|
192
|
+
subscribe(fn: (event: RowsEvent) => void): () => void;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Phase F.5 — row-lifecycle event surfaced through `subscribeRows`.
|
|
196
|
+
*
|
|
197
|
+
* - `add` — a new row was inserted at `index`. `values` carries
|
|
198
|
+
* the row's seeded field values (mirrors `addRow.initial`).
|
|
199
|
+
* - `remove` — a row was removed; `index` is its position at the
|
|
200
|
+
* moment of removal.
|
|
201
|
+
* - `move` — a row shifted positions. `from` and `to` are 0-based
|
|
202
|
+
* indices in the post-reorder layout (i.e. the row at
|
|
203
|
+
* position `from` ended up at `to`).
|
|
204
|
+
*
|
|
205
|
+
* Pilotiq core stays Yjs-free — the binding impl in `@pilotiq-pro/collab`
|
|
206
|
+
* translates `Y.Array<Y.Map>` `observe` deltas into these events.
|
|
207
|
+
*/
|
|
208
|
+
export type RowsEvent = {
|
|
209
|
+
kind: 'add';
|
|
210
|
+
rowId: string;
|
|
211
|
+
index: number;
|
|
212
|
+
values: Record<string, unknown>;
|
|
213
|
+
} | {
|
|
214
|
+
kind: 'remove';
|
|
215
|
+
rowId: string;
|
|
216
|
+
index: number;
|
|
217
|
+
} | {
|
|
218
|
+
kind: 'move';
|
|
219
|
+
rowId: string;
|
|
220
|
+
from: number;
|
|
221
|
+
to: number;
|
|
222
|
+
};
|
|
93
223
|
export interface FormCollabBindingFactoryArgs {
|
|
94
224
|
/** Active collab room — provides `ydoc`, `provider`, `user`. Opaque to pilotiq core. */
|
|
95
225
|
room: CollabRoom;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormCollabBindingRegistry.d.ts","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAG,MAAM,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,IAAQ,MAAM,CAAA;IAClB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC/C,OAAO,IAAK,IAAI,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,GAAG,IAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,uEAAuE;IACvE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACvC,oEAAoE;IACpE,SAAS,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IACtE;;;;kEAI8D;IAC9D,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAA;IACjD,kDAAkD;IAClD,OAAO,IAAO,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"FormCollabBindingRegistry.d.ts","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormCollabBindingRegistry.js","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FormCollabBindingRegistry.js","sourceRoot":"","sources":["../../src/react/FormCollabBindingRegistry.ts"],"names":[],"mappings":"AAwPA,IAAI,QAAQ,GAAoC,IAAI,CAAA;AAEpD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAiC;IACzE,QAAQ,GAAG,OAAO,CAAA;AACpB,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,oBAAoB;IAClC,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ElementMeta } from '../schema/Element.js';
|
|
3
|
-
import { type TextBinding } from './FormCollabBindingRegistry.js';
|
|
3
|
+
import { type RowBindingApi, type TextBinding } from './FormCollabBindingRegistry.js';
|
|
4
4
|
export type FieldStatus = 'idle' | 'pending';
|
|
5
5
|
export interface FormStateApi {
|
|
6
6
|
values: Record<string, unknown>;
|
|
@@ -27,6 +27,29 @@ export interface FormStateApi {
|
|
|
27
27
|
* non-null answers, so a `Map.get()` hit means the binding has opted
|
|
28
28
|
* this field into the character-level path. */
|
|
29
29
|
textBindings: ReadonlyMap<string, TextBinding> | null;
|
|
30
|
+
/** Phase F.5 — per-Repeater/Builder row-array bindings. `null` outside a
|
|
31
|
+
* collab room or when the binding doesn't implement F.5 row methods.
|
|
32
|
+
* Each entry's API methods are pre-bound to the array name so renderers
|
|
33
|
+
* call `.add(rowId, initial)` rather than `binding.addRow(name, …)`. */
|
|
34
|
+
rowBindings: ReadonlyMap<string, RowBindingApi> | null;
|
|
35
|
+
/**
|
|
36
|
+
* Phase F.5c — per-array set of inner-field names that should route
|
|
37
|
+
* through `Y.Text` (character-level CRDT) instead of row-level Y.Map
|
|
38
|
+
* LWW. Built from a single meta walk at binding mount. Read by
|
|
39
|
+
* `useFieldState(dottedName).textBinding` to decide whether to call
|
|
40
|
+
* `getRowTextBinding`. Sparse — only arrays with at least one
|
|
41
|
+
* text-shaped row leaf appear; absence on a key means "no text leaves
|
|
42
|
+
* in this Repeater/Builder".
|
|
43
|
+
*/
|
|
44
|
+
rowTextLeaves: ReadonlyMap<string, ReadonlySet<string>> | null;
|
|
45
|
+
/**
|
|
46
|
+
* Phase F.5c — resolve a per-row `TextBinding`. Pre-bound to the
|
|
47
|
+
* active F.5 binding so consumers don't reach for `bindingRef`
|
|
48
|
+
* directly. Returns `null` when no binding implements F.5c OR the
|
|
49
|
+
* row+field doesn't qualify (renderer caller should fall back to
|
|
50
|
+
* `defaultValue` like a non-collab form).
|
|
51
|
+
*/
|
|
52
|
+
getRowTextBinding: ((arrayName: string, rowId: string, fieldName: string) => TextBinding | null) | null;
|
|
30
53
|
}
|
|
31
54
|
/** Hook for direct access to the form context. Returns `null` outside a
|
|
32
55
|
* `FormStateProvider` (e.g. an action modal, or a form without any live
|
|
@@ -70,6 +93,21 @@ export interface UseFieldStateResult {
|
|
|
70
93
|
* `applyDelta + observe`; null → today's whole-string LWW path. */
|
|
71
94
|
textBinding: TextBinding | null;
|
|
72
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Phase F.5 — return the row-array CRDT API for a Repeater/Builder field.
|
|
98
|
+
* Returns `null` when:
|
|
99
|
+
*
|
|
100
|
+
* - No `FormStateProvider` is mounted (e.g. uncontrolled form path).
|
|
101
|
+
* - No `<RecordCollabRoom>` is up-tree (no binding registered).
|
|
102
|
+
* - The active binding doesn't implement F.5's row methods.
|
|
103
|
+
* - The named field opted out via `.collab(false)` (skipped at meta walk).
|
|
104
|
+
* - The named field isn't a Repeater/Builder.
|
|
105
|
+
*
|
|
106
|
+
* RepeaterInput + BuilderInput call this once per render and proceed
|
|
107
|
+
* with the v1 local-only behaviour when null. The returned API methods
|
|
108
|
+
* are pre-bound to the array name so consumers don't repeat it.
|
|
109
|
+
*/
|
|
110
|
+
export declare function useRowBinding(arrayName: string): RowBindingApi | null;
|
|
73
111
|
/** Per-field accessor. Inside a `FormStateProvider` it returns the controlled
|
|
74
112
|
* value + setter + live trigger; outside, it returns sentinels and callers
|
|
75
113
|
* should fall back to `defaultValue` (uncontrolled inputs). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormStateContext.d.ts","sourceRoot":"","sources":["../../src/react/FormStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"FormStateContext.d.ts","sourceRoot":"","sources":["../../src/react/FormStateContext.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
-
import { collectFieldDefaults, findFieldMeta, parseFormDataToNested, readNestedValue, writeNestedValue, } from './formStateHelpers.js';
|
|
3
|
+
import { collectFieldDefaults, collectRowArrayFieldNames, collectRowTextLeavesByArray, findFieldMeta, parseFormDataToNested, parseRowFieldPath, readNestedValue, routeBindingWrite, rowIdAtIndex, writeNestedValue, } from './formStateHelpers.js';
|
|
4
4
|
import { runJsHandler } from './fieldJsHandler.js';
|
|
5
5
|
import { useToast } from './Toaster.js';
|
|
6
6
|
import { useCollabRoom } from './CollabRoomContext.js';
|
|
@@ -21,16 +21,24 @@ export function useFormState() {
|
|
|
21
21
|
*/
|
|
22
22
|
export const FormIdContext = createContext('');
|
|
23
23
|
/**
|
|
24
|
-
* Phase
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* Phase F.5 — return the row-array CRDT API for a Repeater/Builder field.
|
|
25
|
+
* Returns `null` when:
|
|
26
|
+
*
|
|
27
|
+
* - No `FormStateProvider` is mounted (e.g. uncontrolled form path).
|
|
28
|
+
* - No `<RecordCollabRoom>` is up-tree (no binding registered).
|
|
29
|
+
* - The active binding doesn't implement F.5's row methods.
|
|
30
|
+
* - The named field opted out via `.collab(false)` (skipped at meta walk).
|
|
31
|
+
* - The named field isn't a Repeater/Builder.
|
|
32
|
+
*
|
|
33
|
+
* RepeaterInput + BuilderInput call this once per render and proceed
|
|
34
|
+
* with the v1 local-only behaviour when null. The returned API methods
|
|
35
|
+
* are pre-bound to the array name so consumers don't repeat it.
|
|
30
36
|
*/
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
37
|
+
export function useRowBinding(arrayName) {
|
|
38
|
+
const ctx = useContext(FormStateContext);
|
|
39
|
+
if (!ctx?.rowBindings)
|
|
40
|
+
return null;
|
|
41
|
+
return ctx.rowBindings.get(arrayName) ?? null;
|
|
34
42
|
}
|
|
35
43
|
/** Per-field accessor. Inside a `FormStateProvider` it returns the controlled
|
|
36
44
|
* value + setter + live trigger; outside, it returns sentinels and callers
|
|
@@ -58,13 +66,37 @@ export function useFieldState(name) {
|
|
|
58
66
|
triggerLive: (valueOverride) => ctx.triggerLive(name, valueOverride),
|
|
59
67
|
pending: ctx.fieldStatus(name) === 'pending',
|
|
60
68
|
errors: ctx.errors[name] ?? [],
|
|
61
|
-
// Phase F.6 —
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
69
|
+
// Phase F.6 — top-level text fields resolve from the binding-mount
|
|
70
|
+
// text stash. Phase F.5c — dotted-path row leaves resolve through
|
|
71
|
+
// `getRowTextBinding` when the field is text-shaped AND the row's
|
|
72
|
+
// `__id` is already stamped in the values map. Outside a collab
|
|
73
|
+
// room, for non-text fields, or before `addRow` has settled the
|
|
74
|
+
// row's id, the lookup returns null and `BoundTextInput` falls
|
|
75
|
+
// back to today's uncontrolled-input path.
|
|
76
|
+
textBinding: dotted
|
|
77
|
+
? resolveRowTextBinding(ctx, name)
|
|
78
|
+
: (ctx.textBindings?.get(name) ?? null),
|
|
66
79
|
};
|
|
67
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Phase F.5c — dotted-name `TextBinding` resolver. Returns `null`
|
|
83
|
+
* whenever any precondition fails so the caller's renderer can take a
|
|
84
|
+
* single branch on null vs non-null.
|
|
85
|
+
*/
|
|
86
|
+
function resolveRowTextBinding(ctx, dottedName) {
|
|
87
|
+
if (!ctx.rowTextLeaves || !ctx.getRowTextBinding)
|
|
88
|
+
return null;
|
|
89
|
+
const parsed = parseRowFieldPath(dottedName);
|
|
90
|
+
if (!parsed)
|
|
91
|
+
return null;
|
|
92
|
+
const set = ctx.rowTextLeaves.get(parsed.arrayName);
|
|
93
|
+
if (!set?.has(parsed.fieldName))
|
|
94
|
+
return null;
|
|
95
|
+
const rowId = rowIdAtIndex(ctx.values, parsed.arrayName, parsed.index);
|
|
96
|
+
if (!rowId)
|
|
97
|
+
return null;
|
|
98
|
+
return ctx.getRowTextBinding(parsed.arrayName, rowId, parsed.fieldName);
|
|
99
|
+
}
|
|
68
100
|
/** Provider component for the controlled form path. Holds the values map,
|
|
69
101
|
* the current form meta (replaced wholesale on live POST), and the
|
|
70
102
|
* per-field live trigger. Mounted by `FormRenderer` when the form has a
|
|
@@ -82,6 +114,15 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
82
114
|
// existing `setValuesState` overlay below already triggers one when the
|
|
83
115
|
// room has pre-existing state.
|
|
84
116
|
const [textBindings, setTextBindings] = useState(null);
|
|
117
|
+
// Phase F.5 — per-Repeater/Builder row-array stash. Same lifecycle as
|
|
118
|
+
// `textBindings`: populated on collab mount when the binding implements
|
|
119
|
+
// F.5 row methods, cleared on unmount.
|
|
120
|
+
const [rowBindings, setRowBindings] = useState(null);
|
|
121
|
+
// Phase F.5c — per-array set of text-shaped row leaves + a pre-bound
|
|
122
|
+
// resolver. Both populated at binding mount when the active binding
|
|
123
|
+
// implements `getRowTextBinding`; cleared on unmount.
|
|
124
|
+
const [rowTextLeaves, setRowTextLeaves] = useState(null);
|
|
125
|
+
const [getRowTextBinding, setGetRowTextBinding] = useState(null);
|
|
85
126
|
const { notify } = useToast();
|
|
86
127
|
// Track an incrementing in-flight id so out-of-order responses are dropped.
|
|
87
128
|
// useRef (not useState) so React StrictMode dev double-invokes don't
|
|
@@ -157,6 +198,52 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
157
198
|
if (textStash.size > 0)
|
|
158
199
|
setTextBindings(textStash);
|
|
159
200
|
}
|
|
201
|
+
// Phase F.5 — build a `RowBindingApi` per top-level Repeater/Builder
|
|
202
|
+
// field when the binding implements all three lifecycle methods. The
|
|
203
|
+
// walk reads from formMeta (structural — fields exist regardless of
|
|
204
|
+
// whether the form has any rows yet); the API is then pre-bound to
|
|
205
|
+
// the array name so `RepeaterInput` calls `rb.add(rowId, …)` rather
|
|
206
|
+
// than `binding.addRow(name, rowId, …)`. Partial F.5 impls (e.g. a
|
|
207
|
+
// binding that has addRow but not reorderRows) skip the stash — the
|
|
208
|
+
// contract says all three or nothing. F.5c's per-row text path is
|
|
209
|
+
// exposed separately via `getRowTextBinding` and stays optional.
|
|
210
|
+
if (binding.addRow && binding.removeRow && binding.reorderRows) {
|
|
211
|
+
const { addRow, removeRow, reorderRows, subscribeRows } = binding;
|
|
212
|
+
const arrayNames = collectRowArrayFieldNames(formMetaRef.current);
|
|
213
|
+
if (arrayNames.length > 0) {
|
|
214
|
+
const rowStash = new Map();
|
|
215
|
+
for (const arrayName of arrayNames) {
|
|
216
|
+
rowStash.set(arrayName, {
|
|
217
|
+
add: (rowId, initial = {}) => addRow.call(binding, arrayName, rowId, initial),
|
|
218
|
+
remove: (rowId) => removeRow.call(binding, arrayName, rowId),
|
|
219
|
+
reorder: (newOrder) => reorderRows.call(binding, arrayName, newOrder),
|
|
220
|
+
// Partial F.5 impl: a binding may ship add/remove/reorder
|
|
221
|
+
// without `subscribeRows` (e.g. tests). Substitute a no-op
|
|
222
|
+
// subscription so renderer code stays uniform — the cleanup
|
|
223
|
+
// fn is still called on unmount but no events ever arrive.
|
|
224
|
+
subscribe: subscribeRows
|
|
225
|
+
? (fn) => subscribeRows.call(binding, arrayName, fn)
|
|
226
|
+
: () => () => { },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
setRowBindings(rowStash);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Phase F.5c — capture the per-array text-leaf allowlist + bind the
|
|
233
|
+
// row-text resolver to the active binding. `getRowTextBinding` may
|
|
234
|
+
// be absent on partial F.5 impls; we expose the resolver as null in
|
|
235
|
+
// that case so `useFieldState` short-circuits cleanly.
|
|
236
|
+
if (binding.getRowTextBinding) {
|
|
237
|
+
const leaves = collectRowTextLeavesByArray(formMetaRef.current);
|
|
238
|
+
if (leaves.size > 0) {
|
|
239
|
+
setRowTextLeaves(leaves);
|
|
240
|
+
const bound = binding.getRowTextBinding;
|
|
241
|
+
// useState's functional-updater overload would invoke the
|
|
242
|
+
// stored function during set; wrapping in a fresh closure keeps
|
|
243
|
+
// React's setState path from confusing it for an updater fn.
|
|
244
|
+
setGetRowTextBinding(() => (arrayName, rowId, fieldName) => bound.call(binding, arrayName, rowId, fieldName));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
160
247
|
// Subscribe to remote changes. Local writes ALSO trigger this
|
|
161
248
|
// (Yjs observers fire on local transactions too) — the per-key
|
|
162
249
|
// Object.is short-circuit below collapses them into no-op renders.
|
|
@@ -178,6 +265,9 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
178
265
|
binding.destroy();
|
|
179
266
|
bindingRef.current = null;
|
|
180
267
|
setTextBindings(null);
|
|
268
|
+
setRowBindings(null);
|
|
269
|
+
setRowTextLeaves(null);
|
|
270
|
+
setGetRowTextBinding(null);
|
|
181
271
|
};
|
|
182
272
|
// `valuesRef.current` is intentionally read once at mount — initial
|
|
183
273
|
// values seed the binding; subsequent edits flow through `setValue`
|
|
@@ -239,14 +329,13 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
239
329
|
return prev;
|
|
240
330
|
return { ...prev, [name]: value };
|
|
241
331
|
});
|
|
242
|
-
// Phase F2 — proxy the write through the collab binding when
|
|
243
|
-
// AND the field hasn't opted out via `.collab(false)`.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
332
|
+
// Phase F2 / F.5 — proxy the write through the collab binding when
|
|
333
|
+
// active AND the field hasn't opted out via `.collab(false)`. Top-level
|
|
334
|
+
// fields ride `binding.set`. Row leaves (dotted paths matching
|
|
335
|
+
// `parseRowFieldPath`) route through `binding.setRow` when the
|
|
336
|
+
// binding implements F.5 — otherwise stay local-only (same posture
|
|
337
|
+
// as pre-F.5).
|
|
338
|
+
routeBindingWrite(bindingRef.current, formMetaRef.current, valuesRef.current, name, value);
|
|
250
339
|
// Fire the client-side JS hook synchronously after the state write.
|
|
251
340
|
// Dotted-name fields don't go through here (their setter is a no-op
|
|
252
341
|
// in `useFieldState`); they fire JS via `triggerLive` instead so we
|
|
@@ -317,18 +406,19 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
317
406
|
const serverValues = data.form.values;
|
|
318
407
|
if (serverValues) {
|
|
319
408
|
setValuesState((prev) => ({ ...prev, ...serverValues }));
|
|
320
|
-
// Phase F2 (Q2) — derived fields propagate to peers via the
|
|
321
|
-
// collab binding so every client sees the auto-`slug` / etc.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
409
|
+
// Phase F2 (Q2) / F.5 — derived fields propagate to peers via the
|
|
410
|
+
// collab binding so every client sees the auto-`slug` / etc. without
|
|
411
|
+
// each peer roundtripping the server. Row leaves route through
|
|
412
|
+
// `setRow` when the binding implements F.5; top-level fields ride
|
|
413
|
+
// `set`. The rowId lookup needs the freshest values — merge
|
|
414
|
+
// `valuesRef.current` with the server overlay so a row-id stamped
|
|
415
|
+
// by this very server-resolve response is visible to `rowIdAtIndex`.
|
|
324
416
|
const binding = bindingRef.current;
|
|
325
417
|
if (binding) {
|
|
418
|
+
const lookupValues = { ...valuesRef.current, ...serverValues };
|
|
326
419
|
for (const [k, v] of Object.entries(serverValues)) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (fieldOptsOutOfCollab(data.form, k))
|
|
330
|
-
continue;
|
|
331
|
-
binding.set(k, v);
|
|
420
|
+
// routeBindingWrite handles `.collab(false)` opt-out internally.
|
|
421
|
+
routeBindingWrite(binding, data.form, lookupValues, k, v);
|
|
332
422
|
}
|
|
333
423
|
}
|
|
334
424
|
}
|
|
@@ -406,7 +496,10 @@ export function FormStateProvider({ initialMeta, initialErrors, children, fetchI
|
|
|
406
496
|
inFlight,
|
|
407
497
|
fieldStatus,
|
|
408
498
|
textBindings,
|
|
409
|
-
|
|
499
|
+
rowBindings,
|
|
500
|
+
rowTextLeaves,
|
|
501
|
+
getRowTextBinding,
|
|
502
|
+
}), [values, setValue, triggerLive, errors, applyErrors, formMeta, inFlight, fieldStatus, textBindings, rowBindings, rowTextLeaves, getRowTextBinding]);
|
|
410
503
|
return (_jsx(FormStateContext.Provider, { value: api, children: children }));
|
|
411
504
|
}
|
|
412
505
|
//# sourceMappingURL=FormStateContext.js.map
|