@pilotiq/pilotiq 0.6.1 → 0.7.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 +6 -2
- package/CHANGELOG.md +614 -0
- package/CLAUDE.md +6 -5
- package/dist/Column.d.ts +35 -0
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js +41 -0
- package/dist/Column.js.map +1 -1
- package/dist/Page.d.ts +13 -4
- package/dist/Page.d.ts.map +1 -1
- package/dist/Page.js +9 -2
- package/dist/Page.js.map +1 -1
- package/dist/Pilotiq.d.ts +84 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +66 -0
- package/dist/Pilotiq.js.map +1 -1
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +9 -0
- package/dist/Resource.js.map +1 -1
- package/dist/actions/exportFactory.js +1 -1
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/columns/SelectColumn.d.ts +32 -5
- package/dist/columns/SelectColumn.d.ts.map +1 -1
- package/dist/columns/SelectColumn.js +37 -7
- package/dist/columns/SelectColumn.js.map +1 -1
- package/dist/defaultPages.d.ts.map +1 -1
- package/dist/defaultPages.js +3 -0
- package/dist/defaultPages.js.map +1 -1
- package/dist/elements/Form.d.ts +17 -0
- package/dist/elements/Form.d.ts.map +1 -1
- package/dist/elements/Form.js +17 -0
- package/dist/elements/Form.js.map +1 -1
- package/dist/elements/Table.d.ts +26 -0
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +15 -1
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableGroup.d.ts +84 -0
- package/dist/elements/TableGroup.d.ts.map +1 -1
- package/dist/elements/TableGroup.js +103 -0
- package/dist/elements/TableGroup.js.map +1 -1
- package/dist/elements/dispatchForm.d.ts.map +1 -1
- package/dist/elements/dispatchForm.js +36 -6
- package/dist/elements/dispatchForm.js.map +1 -1
- package/dist/elements/dispatchTable.d.ts +12 -0
- package/dist/elements/dispatchTable.d.ts.map +1 -1
- package/dist/elements/dispatchTable.js +104 -29
- package/dist/elements/dispatchTable.js.map +1 -1
- package/dist/fields/Field.d.ts +7 -2
- package/dist/fields/Field.d.ts.map +1 -1
- package/dist/fields/Field.js +8 -3
- package/dist/fields/Field.js.map +1 -1
- package/dist/fields/RepeaterField.d.ts +65 -0
- package/dist/fields/RepeaterField.d.ts.map +1 -1
- package/dist/fields/RepeaterField.js +48 -0
- package/dist/fields/RepeaterField.js.map +1 -1
- package/dist/orm/modelDefaults.d.ts.map +1 -1
- package/dist/orm/modelDefaults.js +19 -0
- package/dist/orm/modelDefaults.js.map +1 -1
- package/dist/pageData.d.ts +20 -0
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +242 -34
- package/dist/pageData.js.map +1 -1
- package/dist/react/AppShell.d.ts +17 -1
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +34 -3
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/PendingSuggestionApplierRegistry.d.ts +34 -0
- package/dist/react/PendingSuggestionApplierRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionApplierRegistry.js +51 -0
- package/dist/react/PendingSuggestionApplierRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts +46 -0
- package/dist/react/PendingSuggestionOverlayRegistry.d.ts.map +1 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js +16 -0
- package/dist/react/PendingSuggestionOverlayRegistry.js.map +1 -0
- package/dist/react/PendingSuggestionsContext.d.ts +153 -0
- package/dist/react/PendingSuggestionsContext.d.ts.map +1 -0
- package/dist/react/PendingSuggestionsContext.js +46 -0
- package/dist/react/PendingSuggestionsContext.js.map +1 -0
- package/dist/react/SchemaRenderer.d.ts.map +1 -1
- package/dist/react/SchemaRenderer.js +312 -39
- package/dist/react/SchemaRenderer.js.map +1 -1
- package/dist/react/cells/EditableCell.d.ts +8 -0
- package/dist/react/cells/EditableCell.d.ts.map +1 -1
- package/dist/react/cells/EditableCell.js +6 -2
- package/dist/react/cells/EditableCell.js.map +1 -1
- package/dist/react/fields/CheckboxListInput.d.ts.map +1 -1
- package/dist/react/fields/CheckboxListInput.js +29 -2
- package/dist/react/fields/CheckboxListInput.js.map +1 -1
- package/dist/react/fields/ColorInput.d.ts.map +1 -1
- package/dist/react/fields/ColorInput.js +28 -2
- package/dist/react/fields/ColorInput.js.map +1 -1
- package/dist/react/fields/DateTimeInput.d.ts.map +1 -1
- package/dist/react/fields/DateTimeInput.js +28 -2
- package/dist/react/fields/DateTimeInput.js.map +1 -1
- package/dist/react/fields/FieldShell.d.ts.map +1 -1
- package/dist/react/fields/FieldShell.js +161 -3
- package/dist/react/fields/FieldShell.js.map +1 -1
- package/dist/react/fields/FileUploadInput.d.ts.map +1 -1
- package/dist/react/fields/FileUploadInput.js +27 -2
- package/dist/react/fields/FileUploadInput.js.map +1 -1
- package/dist/react/fields/KeyValueInput.d.ts.map +1 -1
- package/dist/react/fields/KeyValueInput.js +33 -2
- package/dist/react/fields/KeyValueInput.js.map +1 -1
- package/dist/react/fields/RadioInput.d.ts.map +1 -1
- package/dist/react/fields/RadioInput.js +28 -2
- package/dist/react/fields/RadioInput.js.map +1 -1
- package/dist/react/fields/SelectFieldInput.d.ts.map +1 -1
- package/dist/react/fields/SelectFieldInput.js +31 -2
- package/dist/react/fields/SelectFieldInput.js.map +1 -1
- package/dist/react/fields/SliderInput.d.ts.map +1 -1
- package/dist/react/fields/SliderInput.js +26 -2
- package/dist/react/fields/SliderInput.js.map +1 -1
- package/dist/react/fields/TagsInput.d.ts.map +1 -1
- package/dist/react/fields/TagsInput.js +26 -2
- package/dist/react/fields/TagsInput.js.map +1 -1
- package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -1
- package/dist/react/fields/ToggleFieldInput.js +29 -2
- package/dist/react/fields/ToggleFieldInput.js.map +1 -1
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +55 -2
- package/dist/routes.js.map +1 -1
- package/dist/schema/Html.d.ts +2 -2
- package/dist/schema/Html.d.ts.map +1 -1
- package/dist/schema/Html.js +2 -2
- package/dist/schema/Html.js.map +1 -1
- package/dist/schema/Markdown.d.ts +2 -2
- package/dist/schema/Markdown.d.ts.map +1 -1
- package/dist/schema/Markdown.js +2 -2
- package/dist/schema/Markdown.js.map +1 -1
- package/dist/schema/Section.d.ts +16 -0
- package/dist/schema/Section.d.ts.map +1 -1
- package/dist/schema/Section.js +16 -0
- package/dist/schema/Section.js.map +1 -1
- package/dist/schema/Wizard.d.ts +45 -0
- package/dist/schema/Wizard.d.ts.map +1 -1
- package/dist/schema/Wizard.js +50 -0
- package/dist/schema/Wizard.js.map +1 -1
- package/dist/schema/resolveSchema.d.ts +8 -0
- package/dist/schema/resolveSchema.d.ts.map +1 -1
- package/dist/schema/resolveSchema.js +70 -1
- package/dist/schema/resolveSchema.js.map +1 -1
- package/dist/schema/sanitize.d.ts +3 -3
- package/dist/schema/sanitize.d.ts.map +1 -1
- package/dist/schema/sanitize.js +10 -3
- package/dist/schema/sanitize.js.map +1 -1
- package/dist/sessionFilters.d.ts.map +1 -1
- package/dist/sessionFilters.js +12 -1
- package/dist/sessionFilters.js.map +1 -1
- package/dist/styles/file-upload.css +13 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +9 -2
- package/dist/vite.js.map +1 -1
- package/package.json +6 -4
- package/src/Column.test.ts +36 -0
- package/src/Column.ts +54 -0
- package/src/Page.ts +13 -4
- package/src/Pilotiq.ts +109 -0
- package/src/Resource.ts +29 -0
- package/src/actions/exportFactory.ts +1 -1
- package/src/columns/SelectColumn.ts +46 -8
- package/src/columns/editableColumns.test.ts +45 -0
- package/src/defaultPages.ts +3 -0
- package/src/elements/Form.ts +19 -0
- package/src/elements/Table.ts +35 -1
- package/src/elements/TableGroup.test.ts +111 -0
- package/src/elements/TableGroup.ts +135 -0
- package/src/elements/dispatchForm.ts +34 -7
- package/src/elements/dispatchTable.test.ts +267 -0
- package/src/elements/dispatchTable.ts +112 -33
- package/src/fields/Field.test.ts +15 -0
- package/src/fields/Field.ts +8 -3
- package/src/fields/RepeaterField.ts +104 -0
- package/src/fields/RepeaterRelationship.test.ts +173 -0
- package/src/nestedRelationManagerData.test.ts +21 -0
- package/src/orm/modelDefaults.ts +21 -0
- package/src/pageData.ts +267 -47
- package/src/react/AppShell.tsx +55 -4
- package/src/react/PendingSuggestionApplierRegistry.ts +80 -0
- package/src/react/PendingSuggestionOverlayRegistry.ts +54 -0
- package/src/react/PendingSuggestionsContext.tsx +172 -0
- package/src/react/SchemaRenderer.tsx +504 -95
- package/src/react/cells/EditableCell.tsx +11 -2
- package/src/react/fields/CheckboxListInput.tsx +23 -2
- package/src/react/fields/ColorInput.tsx +22 -2
- package/src/react/fields/DateTimeInput.tsx +22 -2
- package/src/react/fields/FieldShell.tsx +167 -3
- package/src/react/fields/FileUploadInput.tsx +21 -2
- package/src/react/fields/KeyValueInput.tsx +32 -2
- package/src/react/fields/RadioInput.tsx +23 -2
- package/src/react/fields/SelectFieldInput.tsx +25 -2
- package/src/react/fields/SliderInput.tsx +20 -2
- package/src/react/fields/TagsInput.tsx +20 -2
- package/src/react/fields/ToggleFieldInput.tsx +23 -2
- package/src/react/index.ts +18 -0
- package/src/relationManagerData.test.ts +451 -2
- package/src/routes.ts +58 -2
- package/src/schema/Html.ts +2 -2
- package/src/schema/Markdown.ts +2 -2
- package/src/schema/Section.ts +17 -0
- package/src/schema/Wizard.ts +67 -0
- package/src/schema/containers.test.ts +90 -0
- package/src/schema/resolveSchema.test.ts +50 -0
- package/src/schema/resolveSchema.ts +79 -1
- package/src/schema/sanitize.ts +13 -4
- package/src/sessionFilters.test.ts +23 -0
- package/src/sessionFilters.ts +11 -1
- package/src/styles/file-upload.css +13 -0
- package/src/vite.ts +9 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
> @pilotiq/pilotiq@0.
|
|
3
|
-
> tsc -p tsconfig.build.json
|
|
2
|
+
> @pilotiq/pilotiq@0.7.0 build /home/runner/work/pilotiq/pilotiq/packages/pilotiq
|
|
3
|
+
> tsc -p tsconfig.build.json && pnpm run copy-assets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
> @pilotiq/pilotiq@0.7.0 copy-assets /home/runner/work/pilotiq/pilotiq/packages/pilotiq
|
|
7
|
+
> mkdir -p dist/styles && cp -R src/styles/*.css dist/styles/
|
|
4
8
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,619 @@
|
|
|
1
1
|
# @pilotiq/pilotiq
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b6dffde: feat(columns): Column.toggleable() user-visibility chrome
|
|
8
|
+
|
|
9
|
+
`Column.toggleable()` lets users show / hide individual columns from a
|
|
10
|
+
new toolbar **Columns** dropdown. Preference persists per-table to
|
|
11
|
+
`localStorage` (key `pilotiq.table.<currentPath>.columns.<col>`), so the
|
|
12
|
+
choice sticks across reloads + SPA navigations. Pass `{ initiallyHidden:
|
|
13
|
+
true }` to start the column off-screen — useful for technical / debug
|
|
14
|
+
columns that the typical viewer doesn't need.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
Resource.table = (t) =>
|
|
18
|
+
t.columns([
|
|
19
|
+
TextColumn.make("name"),
|
|
20
|
+
TextColumn.make("email").toggleable(),
|
|
21
|
+
TextColumn.make("internalId").toggleable({ initiallyHidden: true }),
|
|
22
|
+
]);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The dropdown trigger renders next to the existing Filters / Sort
|
|
26
|
+
controls; non-toggleable columns always render and never appear in the
|
|
27
|
+
dropdown. Hidden state is purely presentational — the column's data
|
|
28
|
+
still loads from the server so sorts / filters that reference a hidden
|
|
29
|
+
column keep working, and a re-toggle paints fresh values without a
|
|
30
|
+
roundtrip. Toggling multiple columns in one open: the dropdown stays
|
|
31
|
+
open between clicks (`closeOnClick={false}`).
|
|
32
|
+
|
|
33
|
+
`visibleColumns = columns.filter(c => !hidden.has(c.name))` flows
|
|
34
|
+
through the TableHead loop, body cells loop, per-group + footer summary
|
|
35
|
+
rows, and the empty-state colSpan.
|
|
36
|
+
|
|
37
|
+
The `toggleable` key is sparse on the wire — only set when a column
|
|
38
|
+
opts in.
|
|
39
|
+
|
|
40
|
+
- 8845b90: feat(core): `@pilotiq/pilotiq/styles/file-upload.css` subpath
|
|
41
|
+
|
|
42
|
+
`FileUploadField`'s image-cropping UI ships its own stylesheet via the
|
|
43
|
+
`react-image-crop` package — a declared dep of `@pilotiq/pilotiq`.
|
|
44
|
+
Consumers no longer need to declare `react-image-crop` themselves;
|
|
45
|
+
import the new subpath from your app's Tailwind / global stylesheet:
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
@import "@pilotiq/pilotiq/styles/file-upload.css";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The CSS file re-imports `react-image-crop/dist/ReactCrop.css`; the
|
|
52
|
+
@import resolves through pilotiq's own `node_modules`, so the consumer
|
|
53
|
+
side doesn't need a direct dep declaration. Mirrors the same pattern
|
|
54
|
+
as other UI peer deps that pilotiq ships through subpaths.
|
|
55
|
+
|
|
56
|
+
**Build side:** `pnpm build` now copies `src/styles/*.css` to
|
|
57
|
+
`dist/styles/` via a new `copy-assets` script. Watch-mode (`pnpm dev`)
|
|
58
|
+
runs the copy once at startup; per-CSS-edit re-copies aren't wired
|
|
59
|
+
(unusual in dev — the CSS file is essentially static).
|
|
60
|
+
|
|
61
|
+
- 2c441b7: feat(core): `Form.inlineLabel()` / `Section.inlineLabel()` cascade
|
|
62
|
+
|
|
63
|
+
Set `inlineLabel` once at the top of a form (or any section) and every
|
|
64
|
+
descendant `Field` inherits it instead of repeating `.inlineLabel()`
|
|
65
|
+
on each one. Per-field calls still win.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
Form.make()
|
|
69
|
+
.inlineLabel()
|
|
70
|
+
.schema([
|
|
71
|
+
TextField.make("name"), // → inlineLabel: true
|
|
72
|
+
TextField.make("email"), // → inlineLabel: true
|
|
73
|
+
TextField.make("bio").inlineLabel(false), // explicit → label-above
|
|
74
|
+
Section.make("Address")
|
|
75
|
+
.inlineLabel(false)
|
|
76
|
+
.schema([
|
|
77
|
+
TextField.make("street"), // subtree resets → label-above
|
|
78
|
+
TextField.make("city"), // → label-above
|
|
79
|
+
]),
|
|
80
|
+
]);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Resolution chain (most-specific wins):**
|
|
84
|
+
|
|
85
|
+
1. Field-level `Field.inlineLabel(true|false)` — explicit setting on the
|
|
86
|
+
field itself.
|
|
87
|
+
2. Nearest ancestor `Section` with `.inlineLabel(true|false)` — overrides
|
|
88
|
+
any outer container for its subtree.
|
|
89
|
+
3. Outer `Form.inlineLabel(true|false)` — applies to the whole form.
|
|
90
|
+
4. Default — label-above.
|
|
91
|
+
|
|
92
|
+
**Implementation:**
|
|
93
|
+
|
|
94
|
+
- `RenderContext.inlineLabelDefault?: boolean` — pushed by
|
|
95
|
+
`resolveSchema.deriveChildContext` when a `Form` or `Section` calls
|
|
96
|
+
`.inlineLabel(...)`. Children inherit until another container resets
|
|
97
|
+
the flag.
|
|
98
|
+
- `Field._inlineLabel` widened from `boolean` (default `false`) to
|
|
99
|
+
`boolean | undefined`. `Field.buildMeta(ctx)` reads
|
|
100
|
+
`this._inlineLabel ?? ctx.inlineLabelDefault` to decide whether to
|
|
101
|
+
emit the meta key. No public-API change — the setter is unchanged
|
|
102
|
+
(`inlineLabel(v = true)`).
|
|
103
|
+
- New `Form.inlineLabel(v = true)` + `Form.getInlineLabel()` and the
|
|
104
|
+
parallel `Section.inlineLabel(v = true)` + `Section.getInlineLabel()`.
|
|
105
|
+
|
|
106
|
+
**No wire-shape change.** The on-the-wire `FieldMeta.inlineLabel` is
|
|
107
|
+
still emitted with `true` only — the cascade is server-side.
|
|
108
|
+
|
|
109
|
+
Closes the "Schema-wide `inlineLabel()` cascading default on
|
|
110
|
+
Form/Section. Easy but no consumer ask." item from the field
|
|
111
|
+
micro-additions audit (`docs/plans/admin-gap-audit.md`).
|
|
112
|
+
|
|
113
|
+
- ae1450e: feat(core): `Pilotiq.layoutProvider(C)` — plugin-mounted layout-root providers
|
|
114
|
+
|
|
115
|
+
Adds an open-core registry where plugins can register React provider
|
|
116
|
+
components that wrap the panel's `<AppShell>` children at the layout
|
|
117
|
+
root. Removes the per-app requirement that consumers manually wrap
|
|
118
|
+
their `pages/+Layout.tsx` to make plugin contexts available outside
|
|
119
|
+
specific component slots.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// In a plugin's register(panel) step:
|
|
123
|
+
panel.layoutProvider(({ children, basePath }) => (
|
|
124
|
+
<AiUiProvider panelPath={basePath}>{children}</AiUiProvider>
|
|
125
|
+
));
|
|
126
|
+
|
|
127
|
+
// or bulk:
|
|
128
|
+
panel.layoutProviders([Provider1, Provider2]);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Provider components receive `{ children, basePath? }` props.
|
|
132
|
+
Registration order is preserved — the first-registered provider sits
|
|
133
|
+
OUTERMOST (closest to the layout root); the last sits INNERMOST
|
|
134
|
+
(closest to the page tree). Use this when one provider depends on
|
|
135
|
+
another being in scope: register the producer first.
|
|
136
|
+
|
|
137
|
+
**Mirrors the `panel.rightPanel(...)` pattern** — Vite plugin
|
|
138
|
+
harvests the live component refs into `_components.ts` (alongside
|
|
139
|
+
`componentRegistry` + `rightPanelRegistry`) as `layoutProviderRegistry`,
|
|
140
|
+
the auto-gen `+Layout.tsx` template threads it as
|
|
141
|
+
`<AppShell layoutProviderRegistry={...}>`, and `AppShell` folds the
|
|
142
|
+
registry around its rendered tree from last to first so the first
|
|
143
|
+
provider ends up outermost. Empty / unset → no wrapping happens.
|
|
144
|
+
|
|
145
|
+
The first consumer is `@pilotiq-pro/ai` (≥ next minor), which uses
|
|
146
|
+
this to auto-mount `<AiUiProvider>` so the cross-package
|
|
147
|
+
`PendingSuggestionsContext` queue and `<AiClientToolBindings>`
|
|
148
|
+
handlers reach the form tree without a per-app `+Layout.tsx` edit.
|
|
149
|
+
Apps on this version of pilotiq core can drop the manual `<AiUiProvider>`
|
|
150
|
+
wrap they were carrying as a load-bearing requirement.
|
|
151
|
+
|
|
152
|
+
- e1a79f6: feat(core+tiptap): cross-tree applier registry — Approve from anywhere
|
|
153
|
+
|
|
154
|
+
Phase 8.5 of the AI UX polish plan. Adds an open-core registry that
|
|
155
|
+
lets aggregate consumers — chat-sidebar pending-pills, bulk-action
|
|
156
|
+
menus, future "AI inbox" surfaces — apply a `PendingSuggestion` to its
|
|
157
|
+
target field without sharing the form's React tree.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { registerPendingSuggestionApplier } from "@pilotiq/pilotiq/react";
|
|
161
|
+
|
|
162
|
+
// Renderer-side (auto-wired by FieldShell + Tiptap bridge):
|
|
163
|
+
useEffect(
|
|
164
|
+
() =>
|
|
165
|
+
registerPendingSuggestionApplier(formId, fieldName, (suggestion) => {
|
|
166
|
+
/* apply to this field's underlying input or editor */
|
|
167
|
+
}),
|
|
168
|
+
[formId, fieldName]
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Core (`@pilotiq/pilotiq`)**:
|
|
173
|
+
|
|
174
|
+
- New module `react/PendingSuggestionApplierRegistry.ts` — module-level
|
|
175
|
+
Map keyed by `(formId, fieldName)` (`formId` defaults to `'*'` for
|
|
176
|
+
global form scope; form-scoped registrations always win over the
|
|
177
|
+
wildcard for the same field). Exposes `registerPendingSuggestionApplier`
|
|
178
|
+
(returns unregister fn for `useEffect` cleanup) and
|
|
179
|
+
`getPendingSuggestionApplier`.
|
|
180
|
+
- `PendingSuggestionsApi` extended with `approve(id)` and
|
|
181
|
+
`approveAll(filter?)` — resolves the suggestion's `(formId,
|
|
182
|
+
fieldName)` against the registry, runs the applier, then dismisses.
|
|
183
|
+
Falls through to plain `dismiss` when no applier is registered or
|
|
184
|
+
the applier throws (so a busted applier doesn't strand entries).
|
|
185
|
+
Default no-op context implements both as plain dismiss.
|
|
186
|
+
- `<FieldShell>` auto-registers a generic applier on mount for every
|
|
187
|
+
non-richtext, non-dotted-path field. Applier uses
|
|
188
|
+
`useFieldState.setValue` for controlled (live) forms and a DOM
|
|
189
|
+
fallback (React's internal value setter via
|
|
190
|
+
`Object.getOwnPropertyDescriptor(proto, 'value').set`) for
|
|
191
|
+
uncontrolled forms. Cleanup on unmount.
|
|
192
|
+
|
|
193
|
+
**Tiptap (`@pilotiq/tiptap`)**:
|
|
194
|
+
|
|
195
|
+
- `useAiSuggestionBridge` registers a richtext-aware applier that
|
|
196
|
+
calls `editor.chain().focus().approveAiSuggestion(id).run()` —
|
|
197
|
+
same path the inline chip click takes. The transaction listener
|
|
198
|
+
already mirrors the editor-side dismissal back to context, so a
|
|
199
|
+
pill-driven Approve flows: pill → applier → editor command →
|
|
200
|
+
editor `onTransaction` → context `dismiss`.
|
|
201
|
+
|
|
202
|
+
The registry is generic — not AI-specific. Future field-mutation
|
|
203
|
+
extensions (form-recovery, undo stacks, bulk imports) can register
|
|
204
|
+
through the same seam.
|
|
205
|
+
|
|
206
|
+
Default no-op context still ships, so trees without a real provider
|
|
207
|
+
mounted (e.g. headless tests, marketing-site previews) see no behavior
|
|
208
|
+
change.
|
|
209
|
+
|
|
210
|
+
- df85886: feat(core): `PendingSuggestion.origin` for cross-surface filtering
|
|
211
|
+
|
|
212
|
+
Widen the `PendingSuggestion` type with an optional `origin` block so
|
|
213
|
+
aggregate UIs (pending-pills, overlays, etc.) can filter the shared
|
|
214
|
+
panel-wide queue down to the surface that produced each entry. Backward
|
|
215
|
+
compatible — existing producers that don't stamp `origin` keep working;
|
|
216
|
+
consumers that don't read it see the same flat queue they always did.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
export interface PendingSuggestionOrigin {
|
|
220
|
+
surface: "sidebar" | "popover" | "field-action";
|
|
221
|
+
runId?: string;
|
|
222
|
+
agentSlug?: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface PendingSuggestion {
|
|
226
|
+
// …existing fields…
|
|
227
|
+
origin?: PendingSuggestionOrigin;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Plugin packages (`@pilotiq-pro/ai`) stamp `origin` when they push from a
|
|
232
|
+
known surface — the popover-chat scopes its `<PendingSuggestionsPill>`
|
|
233
|
+
filter to `o => o?.runId === currentRunId` so it only surfaces its own
|
|
234
|
+
session's output, even when sidebar-originated suggestions are still
|
|
235
|
+
visible in the same panel-wide queue.
|
|
236
|
+
|
|
237
|
+
No wire-shape break, no consumer code required.
|
|
238
|
+
|
|
239
|
+
- 56a6f62: feat(core+tiptap): PendingSuggestionsContext seam + RichTextField AI bridge
|
|
240
|
+
|
|
241
|
+
Adds a cross-package, plugin-fillable queue of suggested field-value
|
|
242
|
+
changes that any field renderer can subscribe to. Open-core seam — core
|
|
243
|
+
defines the shape + provider, plugins like `@pilotiq-pro/ai` ship the
|
|
244
|
+
real implementation.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { usePendingSuggestionsForField } from "@pilotiq/pilotiq/react";
|
|
248
|
+
|
|
249
|
+
const { list, dismiss } = usePendingSuggestionsForField("body");
|
|
250
|
+
// ↑ filtered to suggestions targeting this field+formId
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**`@pilotiq/pilotiq` exports** (`@pilotiq/pilotiq/react`):
|
|
254
|
+
|
|
255
|
+
- `PendingSuggestion` — `{ id, fieldName, formId?, currentValue,
|
|
256
|
+
suggestedValue, source?, createdAt, meta? }`. The `meta` bag carries
|
|
257
|
+
field-type-specific extras (e.g. `editorRange: { from, to }` for
|
|
258
|
+
`richtext`).
|
|
259
|
+
- `PendingSuggestionsApi` — `{ list, push, dismiss, dismissAll }`. Core
|
|
260
|
+
ships a no-op default context so trees without a real provider never
|
|
261
|
+
throw.
|
|
262
|
+
- `PendingSuggestionsContext`, `usePendingSuggestions()`,
|
|
263
|
+
`usePendingSuggestionsForField(name, formId?)` — the subscription
|
|
264
|
+
surface.
|
|
265
|
+
- `registerPendingSuggestionOverlay(C)` — mirrors
|
|
266
|
+
`registerFieldLabelSlot()`. A plugin registers a single component
|
|
267
|
+
(`{ suggestion, onApprove, onReject }` props) that `<FieldShell>`
|
|
268
|
+
mounts below the input whenever a matching pending suggestion exists.
|
|
269
|
+
Skipped on `richtext` fields (those render the diff inline via the
|
|
270
|
+
Tiptap extension).
|
|
271
|
+
|
|
272
|
+
**`@pilotiq/tiptap` `RichTextField` bridge**:
|
|
273
|
+
|
|
274
|
+
The Tiptap renderer now subscribes to the queue and mirrors entries
|
|
275
|
+
into its `AiSuggestionExtension`. Producers push a `PendingSuggestion`
|
|
276
|
+
with `meta.editorRange = { from, to }` and a string `suggestedValue`;
|
|
277
|
+
the bridge calls `editor.commands.addAiSuggestion(...)` so the inline
|
|
278
|
+
diff + Approve / Reject chips appear. When the user clicks a chip,
|
|
279
|
+
the editor command runs (mutating the doc on Approve, leaving it on
|
|
280
|
+
Reject) and the bridge mirrors the removal back to the queue via
|
|
281
|
+
`dismiss(id)` so other surfaces (chat-sidebar pill, FieldShell
|
|
282
|
+
overlay registered by another plugin) clear in lock-step.
|
|
283
|
+
|
|
284
|
+
The bridge is no-op when no provider is mounted — pilotiq core ships
|
|
285
|
+
the default no-op context, so consumers without `@pilotiq-pro/ai` see
|
|
286
|
+
no behavior change.
|
|
287
|
+
|
|
288
|
+
Pure helpers + types are public; the bridge hook
|
|
289
|
+
`useAiSuggestionBridge` is exported from `@pilotiq/tiptap` for advanced
|
|
290
|
+
producers that want to drive their own editor instances.
|
|
291
|
+
|
|
292
|
+
- e791f65: feat(core): per-tab `canX` gating on `RelationTabs`
|
|
293
|
+
|
|
294
|
+
The record sub-navigation strip (`[View, Edit, …managers]`) now runs the
|
|
295
|
+
matching authorization predicate for each tab and drops entries the
|
|
296
|
+
user can't reach. The routes always enforced — this is presentation
|
|
297
|
+
polish so the chrome doesn't promise a link that 403s on click.
|
|
298
|
+
|
|
299
|
+
**Gates evaluated per tab:**
|
|
300
|
+
|
|
301
|
+
- `__view` → `R.canView(user, parentRecord)`
|
|
302
|
+
- `__edit` → `R.canEdit(user, parentRecord)`
|
|
303
|
+
- manager → `safeManagerPolicy(M, 'canViewAny', Related, user,
|
|
304
|
+
parentRecord)` (falls through to the related Resource's
|
|
305
|
+
`canViewAny` when the manager hasn't overridden — same shape as
|
|
306
|
+
everywhere else)
|
|
307
|
+
|
|
308
|
+
Throwing predicate fails closed (tab hidden). Record-aware predicates
|
|
309
|
+
short-circuit to "visible" when the record-load failed (so the route's
|
|
310
|
+
own gate surfaces the 404/403, not a silent hide).
|
|
311
|
+
|
|
312
|
+
**Empty-strip collapse:** if every gated tab drops, `buildRelationTabs`
|
|
313
|
+
returns `undefined` and the strip is omitted entirely (consistent with
|
|
314
|
+
the existing "no managers registered" branch). The depth-2
|
|
315
|
+
`buildNestedRelationTabs` mirrors the shape — sibling nested manager
|
|
316
|
+
tabs gate on `safeManagerPolicy(N, 'canViewAny', Related, user,
|
|
317
|
+
child1Record)`; the back-link `__view` stays unconditional since the
|
|
318
|
+
user already passed `M.canViewAny` to reach that page; if all sibling
|
|
319
|
+
tabs drop the depth-2 strip is omitted (back-link alone isn't useful
|
|
320
|
+
sub-nav).
|
|
321
|
+
|
|
322
|
+
**No public API change.** Tab gating runs inside the existing
|
|
323
|
+
`buildRelationTabs` / `buildNestedRelationTabs` helpers — both private
|
|
324
|
+
to `pageData.ts`. Their callers (`resourceEditData` / `resourceViewData`
|
|
325
|
+
/ relation data builders / nested relation data builders) already had
|
|
326
|
+
`user` and `parentRecord` (or `child1`) in scope so threading is a
|
|
327
|
+
one-line change at each site.
|
|
328
|
+
|
|
329
|
+
7 tests added (6 depth-1 + 1 depth-2).
|
|
330
|
+
|
|
331
|
+
- cce4f52: feat(repeater): afterCreate / afterUpdate / afterDelete hooks for relationship-mode
|
|
332
|
+
|
|
333
|
+
`Repeater.relationship(...)` gains three per-row lifecycle hooks that
|
|
334
|
+
fire from `persistRelationshipRows` after each child operation:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
RepeaterField.make("attachments")
|
|
338
|
+
.relationship("attachments")
|
|
339
|
+
.schema([TextField.make("filename")])
|
|
340
|
+
.afterCreate(async (record, ctx) => {
|
|
341
|
+
/* ... */
|
|
342
|
+
})
|
|
343
|
+
.afterUpdate(async (record, ctx) => {
|
|
344
|
+
/* ... */
|
|
345
|
+
})
|
|
346
|
+
.afterDelete(async (removed, ctx) => {
|
|
347
|
+
if (ctx.mode === "hasMany" || ctx.mode === "morphMany") {
|
|
348
|
+
// child record was physically deleted
|
|
349
|
+
}
|
|
350
|
+
// For M2M only the pivot row was detached; the child may still exist.
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The handler receives the persisted child record and a `RepeaterRowContext`
|
|
355
|
+
carrying:
|
|
356
|
+
|
|
357
|
+
- `parent` — post-save parent record.
|
|
358
|
+
- `parentId` — `parent[primaryKey]`.
|
|
359
|
+
- `field` — the Repeater field's `name`.
|
|
360
|
+
- `index` — 0-based row index in the submitted set; `-1` for `afterDelete`.
|
|
361
|
+
- `mode` — the resolved `RepeaterRelationMode` (`'hasMany' | 'morphMany'
|
|
362
|
+
| 'belongsToMany' | 'morphToMany' | 'morphedByMany'`).
|
|
363
|
+
|
|
364
|
+
Each setter is config-time guarded: calling on a Repeater that hasn't
|
|
365
|
+
declared `relationship(...)` throws with a clear message (mirrors the
|
|
366
|
+
existing `orderColumn() / pivotColumns()` guards). Throwing handlers
|
|
367
|
+
propagate and stop the rest of the persist diff — earlier rows have
|
|
368
|
+
already saved (v1 isn't transactional).
|
|
369
|
+
|
|
370
|
+
- bd8229e: feat(core): `Resource.pages().record` — custom record sub-pages auto-mounted on the sub-nav strip
|
|
371
|
+
|
|
372
|
+
Declare custom pages that live under a single record. Each sub-page
|
|
373
|
+
gets its own URL (`${resourceBase}/:id/${subPageSlug}`), its own tab in
|
|
374
|
+
the record `RelationTabs` strip, receives the loaded record on
|
|
375
|
+
`ctx.record`, and runs its own `canAccess(user, record)` gate.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
class ActivityPage extends Page {
|
|
379
|
+
static override slug = "activity";
|
|
380
|
+
static override label = "Activity";
|
|
381
|
+
static override schema(ctx) {
|
|
382
|
+
return [
|
|
383
|
+
Heading.make(`Activity for ${(ctx.record as { name?: string })?.name}`),
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
// Optional record-aware gate.
|
|
387
|
+
static override async canAccess(user, record) {
|
|
388
|
+
return (
|
|
389
|
+
(record as { ownerId: string })?.ownerId ===
|
|
390
|
+
(user as { id: string })?.id
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
class UserResource extends Resource {
|
|
396
|
+
static override slug = "users";
|
|
397
|
+
static override pages() {
|
|
398
|
+
return {
|
|
399
|
+
record: {
|
|
400
|
+
activity: ActivityPage,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Wiring:**
|
|
408
|
+
|
|
409
|
+
- `ResourcePages.record?: Record<string, typeof Page>` widening — keeps
|
|
410
|
+
the four standard roles (`index / create / edit / view`) cleanly
|
|
411
|
+
typed; the `record` slot signals "these are per-record sub-pages."
|
|
412
|
+
- `Resource.getRecordPages()` accessor (sugar over
|
|
413
|
+
`resolvePages().record ?? {}`).
|
|
414
|
+
- `PageMode` widened with `'record'`.
|
|
415
|
+
- `Page.canAccess(user, record?)` signature widened — second optional
|
|
416
|
+
arg, back-compat with existing custom-page subclasses that wrote
|
|
417
|
+
`canAccess(user)`.
|
|
418
|
+
- Routes: `GET ${resourceBase}/:id/${subPageSlug}` per registered
|
|
419
|
+
sub-page. The Vike `relation-list` route + `dispatchPageData` share
|
|
420
|
+
the URL slot — relation managers tried first, record sub-pages
|
|
421
|
+
second. Boot validation prevents slug collisions.
|
|
422
|
+
- New `resourceRecordPageData(pilotiq, slug, recordId, subPageSlug,
|
|
423
|
+
req)` builder mirrors `resourceViewData`'s shape.
|
|
424
|
+
- `RelationTabs` strip inserts a tab per sub-page between `__edit` and
|
|
425
|
+
the managers, gated on `SubPage.canAccess(user, record)`. Strip now
|
|
426
|
+
also mounts when ONLY sub-pages exist (no relation managers needed).
|
|
427
|
+
|
|
428
|
+
**Boot validation:**
|
|
429
|
+
|
|
430
|
+
Sub-page slugs must match `[A-Za-z0-9_-]+` and must not collide with:
|
|
431
|
+
|
|
432
|
+
- Reserved relation-manager tokens (`edit`, `delete`, `restore`,
|
|
433
|
+
`force-delete`, `_form`, `_action`, `_search`, `_uploads`,
|
|
434
|
+
`_attach`, `_detach`, `_bulk-detach`).
|
|
435
|
+
- Any of the resource's relation-manager `relationship` slugs.
|
|
436
|
+
|
|
437
|
+
Boot fails with a clear error message — silent 404 at request time is
|
|
438
|
+
much harder to debug than a config-time throw.
|
|
439
|
+
|
|
440
|
+
**v1 limits:** depth-1 only (sub-pages live under `Resource`, not
|
|
441
|
+
under `RelationManager`); no automatic sidebar surface (sub-pages are
|
|
442
|
+
per-record); no tab badges on record sub-pages.
|
|
443
|
+
|
|
444
|
+
Plan + guide: `docs/plans/resource-record-sub-pages.md`,
|
|
445
|
+
`docs/guide/record-sub-pages.md`.
|
|
446
|
+
|
|
447
|
+
- 2f42dcd: feat(columns): SelectColumn.options(record => …) per-row resolver
|
|
448
|
+
|
|
449
|
+
`SelectColumn.options()` now accepts a function form alongside the
|
|
450
|
+
existing static `{ key: label }` / `[{ value, label }]` shapes. The
|
|
451
|
+
resolver receives the raw record and may return a Promise; runs once
|
|
452
|
+
per visible row in `loadTableRecords` (gated behind the existing
|
|
453
|
+
`canEdit` hook so hidden cells skip the resolver cost).
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
SelectColumn.make("assigneeId").options(async (row) => {
|
|
457
|
+
const team = await Team.find(row.teamId);
|
|
458
|
+
return team.members.map((m) => ({ value: String(m.id), label: m.name }));
|
|
459
|
+
});
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The resolved per-row option list is stamped on `row._cellSelectOptions[col.name]`;
|
|
463
|
+
the renderer's `<CellSelect>` reads it as `props.rowOptions` and falls
|
|
464
|
+
back to the column's static `selectOptions` when unset. Resolvers run
|
|
465
|
+
in parallel across columns within a row. A throwing resolver leaves
|
|
466
|
+
the slot unset on that row only — others still stamp, and the cell
|
|
467
|
+
falls back to the static fallback list so one bad row doesn't break
|
|
468
|
+
the whole table.
|
|
469
|
+
|
|
470
|
+
- d7dbc80: feat(core): `TableGroup.scopeQueryByKey()` — click-a-group-heading-to-drill-in
|
|
471
|
+
|
|
472
|
+
Click a banded group's heading to drill the table into just that group's
|
|
473
|
+
rows. The banded layout disappears for that render, a "Drilled into
|
|
474
|
+
<Label>: <Value>" chip mounts above the table with an × to clear, and
|
|
475
|
+
the query has already been narrowed server-side via the registered scoper.
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
Table.make()
|
|
479
|
+
.groups([
|
|
480
|
+
TableGroup.make("status")
|
|
481
|
+
.label("Status")
|
|
482
|
+
.scopeQueryByKey((q, key) => q.where("status", "=", key)),
|
|
483
|
+
])
|
|
484
|
+
.defaultGroup("status");
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Three new methods on `TableGroup`:**
|
|
488
|
+
|
|
489
|
+
- `scopeQueryByKey(fn)` — query scoper applied when the user clicks a
|
|
490
|
+
heading. Receives `(q, key)` and returns the narrowed query. **Default
|
|
491
|
+
(no override):** exact-match `(q, key) => q.where(column, '=', key)`.
|
|
492
|
+
Date groups (`.date()`) install a whole-day range default instead —
|
|
493
|
+
`(q, key) => q.where(col, '>=', '${key} 00:00:00').where(col, '<=', '${key} 23:59:59')`.
|
|
494
|
+
Auto-arms `.scopable(true)`.
|
|
495
|
+
- `getKeyFromRecordUsing(fn)` — override the per-record bucket key
|
|
496
|
+
resolver. Returned string round-trips through `?<prefix>groupKey=` and
|
|
497
|
+
lands as the second arg of `scopeQueryByKey`. Default = raw column
|
|
498
|
+
value cast to string (or the `YYYY-MM-DD` bucket when `.date()` is on).
|
|
499
|
+
Auto-arms `.scopable(true)`.
|
|
500
|
+
- `scopable(v = true)` — explicit opt-in toggle for the clickable
|
|
501
|
+
heading affordance. Use `.scopable(false)` to opt back out after a
|
|
502
|
+
setter has auto-armed it.
|
|
503
|
+
|
|
504
|
+
**URL state:** dedicated `?groupKey=<value>` key, prefix-aware via
|
|
505
|
+
`Table.queryStringIdentifier`. Pairs with `?group=<col>`. Clicking a
|
|
506
|
+
heading resets `?page` to 1 server-side so drill-in always lands on the
|
|
507
|
+
first page of the bucket. The × chip clears `?groupKey=` and restores
|
|
508
|
+
the banded view.
|
|
509
|
+
|
|
510
|
+
**Renderer:** group heading text wraps in a real `<a href>` when
|
|
511
|
+
`scopable` is true (cmd-click / right-click "open in new tab" works);
|
|
512
|
+
plain left-click SPA-navs via `useNavigate()`. The collapsible chevron
|
|
513
|
+
(when `.collapsible()` is also set) stays separate so users can fold
|
|
514
|
+
the group without drilling in.
|
|
515
|
+
|
|
516
|
+
**Persistence:** `<prefix>groupKey` is excluded from
|
|
517
|
+
`persistFiltersInSession`'s persisted slice (parallel to `<prefix>page`)
|
|
518
|
+
— drill-in is page-state, not filter-state. Bare-URL visits return to
|
|
519
|
+
the banded view; the user's last drill-in URL is shareable but not
|
|
520
|
+
auto-restored on revisit.
|
|
521
|
+
|
|
522
|
+
**Composition:**
|
|
523
|
+
|
|
524
|
+
- Chains on top of filters / `TrashedFilter` / active tab query — runs
|
|
525
|
+
after all of them via `ctx.groupScope` in the model adapter.
|
|
526
|
+
- Suppresses per-group summaries (`groupSummaries`) for the drilled-in
|
|
527
|
+
render; the global `tfoot` summary still computes over the visible
|
|
528
|
+
bucket.
|
|
529
|
+
- Composes with `queryStringIdentifier` — keys parse as
|
|
530
|
+
`<id>_groupKey` alongside `<id>_group`.
|
|
531
|
+
- Works on `RelationManager` tables — `modelRelationTableRecords`
|
|
532
|
+
reads the same `ctx.groupScope`.
|
|
533
|
+
|
|
534
|
+
**v1 limits:** one key at a time (multi-select drill-in deferred);
|
|
535
|
+
drill-in URLs survive bookmarking but not session-persistence; date
|
|
536
|
+
range default is whole-day (sub-day buckets need a custom scoper).
|
|
537
|
+
|
|
538
|
+
Plan: `docs/plans/table-group-scope-query-by-key.md`.
|
|
539
|
+
|
|
540
|
+
- 8d92594: feat(wizard): nav-button customizers + URL-state persistence
|
|
541
|
+
|
|
542
|
+
`Wizard.submitAction(a => …) / .nextAction(...) / .previousAction(...)`
|
|
543
|
+
let consumers customize the chrome of the built-in nav buttons. The
|
|
544
|
+
customizer receives a framework-built default `Action` (Submit / Next /
|
|
545
|
+
Back) and returns a customized clone (or a fresh `Action` outright);
|
|
546
|
+
chrome (label / icon / color / size / outlined / iconOnly / tooltip /
|
|
547
|
+
disabled rules) carries through to the rendered button while click
|
|
548
|
+
behavior stays hardwired to advance / recede / submit-form.
|
|
549
|
+
|
|
550
|
+
`submitAction` is the opt-in case: by default the wizard renders a hint
|
|
551
|
+
pointing at the surrounding form's Save button. Setting `submitAction`
|
|
552
|
+
mounts a real `<button type="submit">` inside the wizard chrome on the
|
|
553
|
+
final step, making the wizard self-contained — pair with
|
|
554
|
+
`CreatePage.getFormActions(R) → []` to suppress the page-level Save when
|
|
555
|
+
you don't want two submits on the same page.
|
|
556
|
+
|
|
557
|
+
`Wizard.persistStepInQueryString(key='step' | true | false)` mirrors the
|
|
558
|
+
active step to the URL as `?<key>=N` (1-based for human-friendly URLs)
|
|
559
|
+
via `history.replaceState` — purely client-side state sync with no SSR
|
|
560
|
+
re-fetch. URL wins over localStorage on initial mount so deep-linking
|
|
561
|
+
to a specific step works. Multi-wizard pages should use distinct keys
|
|
562
|
+
to avoid collisions on the same query string.
|
|
563
|
+
|
|
564
|
+
### Patch Changes
|
|
565
|
+
|
|
566
|
+
- 425cf50: fix(core): register field-owned AI appliers on every React-driven input
|
|
567
|
+
|
|
568
|
+
Same hidden-input bug as `SelectField`, swept across nine more field
|
|
569
|
+
types. Each of these renders a `<input type="hidden" name={name}>`
|
|
570
|
+
mirror for native form submit but drives the visible widget from React
|
|
571
|
+
state — `FieldShell`'s generic applier writes to the hidden input and
|
|
572
|
+
dispatches `change`, but the widget has no listener wired to it, so AI
|
|
573
|
+
Review-mode Approve (and any other `PendingSuggestionApplierRegistry`
|
|
574
|
+
caller) silently no-ops.
|
|
575
|
+
|
|
576
|
+
Fixed by registering a field-owned applier inside each component and
|
|
577
|
+
adding the field's `fieldType` to the central
|
|
578
|
+
`SELF_APPLIER_FIELD_TYPES` set in `FieldShell.tsx` (single source of
|
|
579
|
+
truth — `FieldShell` skips its generic registration so the field's
|
|
580
|
+
applier stays last-write-wins):
|
|
581
|
+
|
|
582
|
+
- `ToggleFieldInput` — `'toggle'`; coerces to boolean
|
|
583
|
+
- `SliderInput` — `'slider'`; coerces to number (clamps to `min` on NaN)
|
|
584
|
+
- `ColorInput` — `'color'`; falls back to `#000000` for null/empty
|
|
585
|
+
- `KeyValueInput` — `'keyValue'`; rebuilds rows from the suggestion
|
|
586
|
+
object (preserves existing row IDs by index for input-focus stability)
|
|
587
|
+
- `FileUploadInput` — `'fileUpload'`; routes through `toUrls()`;
|
|
588
|
+
honors `multiple` (single-file persists `urls[0] ?? null`)
|
|
589
|
+
- `TagsInput` — `'tagsInput'`; routes through the existing `toArray()`
|
|
590
|
+
parser (tolerates `string[]`, JSON-encoded, single string)
|
|
591
|
+
- `DateTimeInput` — `'dateTime'`; coerces null/empty to `''`
|
|
592
|
+
- `RadioInput` — `'radio'`; coerces null to `''`
|
|
593
|
+
- `CheckboxListInput` — `'checkboxList'`; routes through the local
|
|
594
|
+
`toArray()` (also fixes a pre-existing latent corruption: per-option
|
|
595
|
+
hidden mirrors share the `[name]` attribute, so the generic applier
|
|
596
|
+
would have stamped every one with the same stringified value
|
|
597
|
+
instead of replacing the array)
|
|
598
|
+
|
|
599
|
+
All appliers follow the canonical `SelectFieldInput` shape:
|
|
600
|
+
`useRef(fs)` to hold latest field-state across re-registrations,
|
|
601
|
+
dotted-path skip (Repeater rows are inaccessible from outside the
|
|
602
|
+
form's React tree), and a controlled/uncontrolled split that mirrors
|
|
603
|
+
each component's existing `setValue` path.
|
|
604
|
+
|
|
605
|
+
After this sweep, AI Review-mode Approve correctly updates the visible
|
|
606
|
+
widget on every Filament-parity field type. Custom field renderers
|
|
607
|
+
that drive their state from React still need to follow the same
|
|
608
|
+
pattern — register inside the component, add `fieldType` to the
|
|
609
|
+
shared set.
|
|
610
|
+
|
|
611
|
+
## 0.6.2
|
|
612
|
+
|
|
613
|
+
### Patch Changes
|
|
614
|
+
|
|
615
|
+
- 27a8472: Lazy-import `sanitize-html` so the client bundle no longer pulls PostCSS and its Node-built-in shims. Eliminates the `browser-external` console warnings (`fs`, `path`, `url`, `source-map-js`) that surfaced on apps using the `Markdown` / `Html` display primes or `TextColumn` rich-display. Sanitization still runs server-side at meta-build time; the wire shape is unchanged.
|
|
616
|
+
|
|
3
617
|
## 0.6.1
|
|
4
618
|
|
|
5
619
|
### Patch Changes
|