create-questpie 2.0.2 → 2.0.4
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/dist/index.mjs +244 -30
- package/package.json +1 -1
- package/skills/questpie/AGENTS.md +310 -103
- package/skills/questpie/SKILL.md +196 -84
- package/skills/questpie/coverage.json +213 -0
- package/skills/questpie/references/auth.md +119 -4
- package/skills/questpie/references/business-logic.md +126 -56
- package/skills/questpie/references/crud-api.md +231 -29
- package/skills/questpie/references/data-modeling.md +26 -6
- package/skills/questpie/references/extend.md +98 -7
- package/skills/questpie/references/field-types.md +14 -2
- package/skills/questpie/references/infrastructure-adapters.md +207 -32
- package/skills/questpie/references/mcp.md +147 -0
- package/skills/questpie/references/multi-tenancy.md +1 -2
- package/skills/questpie/references/production.md +218 -53
- package/skills/questpie/references/quickstart.md +31 -18
- package/skills/questpie/references/rules.md +140 -13
- package/skills/questpie/references/sandbox.md +110 -0
- package/skills/questpie/references/tanstack-query.md +34 -11
- package/skills/questpie/references/type-inference.md +167 -0
- package/skills/questpie/references/workflows.md +155 -0
- package/skills/questpie-admin/AGENTS.md +141 -68
- package/skills/questpie-admin/SKILL.md +96 -63
- package/skills/questpie-admin/references/blocks.md +28 -4
- package/skills/questpie-admin/references/custom-ui.md +1 -1
- package/skills/questpie-admin/references/views.md +21 -5
- package/templates/tanstack-start/AGENTS.md +15 -8
- package/templates/tanstack-start/CLAUDE.md +12 -5
- package/templates/tanstack-start/README.md +7 -6
- package/templates/tanstack-start/package.json +1 -0
- package/templates/tanstack-start/src/lib/query-client.ts +10 -1
- package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
- package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
- package/templates/tanstack-start/src/routes/admin/$.tsx +12 -1
- package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
- package/templates/tanstack-start/src/routes/api/$.ts +1 -2
- package/templates/tanstack-start/vite.config.ts +2 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: questpie-core/workflows
|
|
3
|
+
description:
|
|
4
|
+
QUESTPIE durable workflows long-running business processes replay steps sleep waitForEvent invoke compensation cron admin UI workflow service
|
|
5
|
+
- questpie-core
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Durable Workflows
|
|
9
|
+
|
|
10
|
+
Use `@questpie/workflows` when business logic spans multiple steps, waits on time or external events, needs retry-safe side effects, or should survive process restarts.
|
|
11
|
+
|
|
12
|
+
## Install And Register
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @questpie/workflows
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts title="modules.ts"
|
|
19
|
+
import { workflowsModule } from "@questpie/workflows/modules/workflows";
|
|
20
|
+
export default [workflowsModule] as const;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`workflowsModule` carries its codegen plugin. Do not also add `workflowsPlugin()` to `questpie.config.ts` unless you are doing a custom module setup that deliberately omits `workflowsModule`.
|
|
24
|
+
|
|
25
|
+
Runtime options (route access rule — default admin-only — and execution-lease settings) go in plugin-discovered `config/workflows.ts` using the `workflowsConfig()` factory from `@questpie/workflows/server`.
|
|
26
|
+
|
|
27
|
+
For admin UI pages/widgets, register the client module:
|
|
28
|
+
|
|
29
|
+
```ts title="questpie/admin/modules.ts"
|
|
30
|
+
import adminClientModule from "@questpie/admin/client-module";
|
|
31
|
+
import { workflowsClientModule } from "@questpie/workflows/client/modules/workflows";
|
|
32
|
+
export default {
|
|
33
|
+
name: "app-admin" as const,
|
|
34
|
+
views: { ...adminClientModule.views, ...workflowsClientModule.views },
|
|
35
|
+
components: {
|
|
36
|
+
...adminClientModule.components,
|
|
37
|
+
...workflowsClientModule.components,
|
|
38
|
+
},
|
|
39
|
+
fields: { ...adminClientModule.fields, ...workflowsClientModule.fields },
|
|
40
|
+
pages: { ...adminClientModule.pages, ...workflowsClientModule.pages },
|
|
41
|
+
widgets: { ...adminClientModule.widgets, ...workflowsClientModule.widgets },
|
|
42
|
+
blocks: { ...adminClientModule.blocks, ...workflowsClientModule.blocks },
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Define A Workflow
|
|
47
|
+
|
|
48
|
+
Put workflow definitions in `workflows/*.ts`:
|
|
49
|
+
|
|
50
|
+
```ts title="workflows/production-order.ts"
|
|
51
|
+
import { workflow } from "@questpie/workflows";
|
|
52
|
+
import { z } from "zod";
|
|
53
|
+
|
|
54
|
+
export default workflow({
|
|
55
|
+
name: "production-order",
|
|
56
|
+
schema: z.object({
|
|
57
|
+
orderId: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
timeout: "7d",
|
|
60
|
+
handler: async ({ input, step, ctx, log }) => {
|
|
61
|
+
const order = await step.run("load-order", async () => {
|
|
62
|
+
return ctx.collections.productionOrders.findOne({
|
|
63
|
+
where: { id: input.orderId },
|
|
64
|
+
with: { toy: true },
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
if (!order) throw new Error("Production order not found");
|
|
68
|
+
|
|
69
|
+
await step.run("reserve-materials", async () => {
|
|
70
|
+
await ctx.queue.recalculateMaterialPlan.publish({
|
|
71
|
+
orderId: input.orderId,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await step.waitForEvent("materials-ready", {
|
|
76
|
+
event: "materials.available",
|
|
77
|
+
match: { orderId: input.orderId },
|
|
78
|
+
timeout: "2d",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await step.run("notify-scheduled", async () => {
|
|
82
|
+
await ctx.email.sendTemplate({
|
|
83
|
+
template: "productionScheduled",
|
|
84
|
+
input: { orderId: input.orderId },
|
|
85
|
+
to: order.ownerEmail,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
log.info("Production order workflow completed");
|
|
90
|
+
return { status: "scheduled" };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Run codegen after adding workflow files:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bun questpie generate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Trigger And Signal
|
|
102
|
+
|
|
103
|
+
Use injected `workflows` from route, job, hook, or service context:
|
|
104
|
+
|
|
105
|
+
```ts title="routes/start-production.ts"
|
|
106
|
+
import { route } from "questpie/services";
|
|
107
|
+
import { z } from "zod";
|
|
108
|
+
|
|
109
|
+
export default route()
|
|
110
|
+
.post()
|
|
111
|
+
.schema(z.object({ orderId: z.string() }))
|
|
112
|
+
.handler(async ({ input, workflows }) => {
|
|
113
|
+
const result = await workflows.trigger("production-order", {
|
|
114
|
+
orderId: input.orderId,
|
|
115
|
+
});
|
|
116
|
+
return { instanceId: result.instanceId };
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Signal waiting workflows:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await workflows.sendEvent(
|
|
124
|
+
"materials.available",
|
|
125
|
+
{ receivedAt: new Date().toISOString() },
|
|
126
|
+
{ orderId },
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Cron Workflows
|
|
131
|
+
|
|
132
|
+
Use workflow-level `cron` for recurring long-running processes:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
export default workflow({
|
|
136
|
+
name: "nightly-material-plan",
|
|
137
|
+
schema: z.object({}),
|
|
138
|
+
cron: { schedule: "0 2 * * *", overlap: "skip" },
|
|
139
|
+
handler: async ({ step, ctx }) => {
|
|
140
|
+
await step.run("recalculate", async () => {
|
|
141
|
+
await ctx.queue.recalculateMaterialPlan.publish({});
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
On Node/Bun workers, `app.queue.listen()` runs workflow jobs and maintenance. On Cloudflare Workers, use `cloudflareQueuesAdapter`, export `createCloudflareWorkerHandlers(app)`, and configure a Cron Trigger for workflow maintenance.
|
|
148
|
+
|
|
149
|
+
## Rules
|
|
150
|
+
|
|
151
|
+
- Keep external side effects inside `step.run()` so replay does not repeat them.
|
|
152
|
+
- Use stable step names. Renaming a step changes replay identity.
|
|
153
|
+
- Use idempotency keys when calling external APIs.
|
|
154
|
+
- Use `step.waitForEvent()` for durable waits instead of polling loops.
|
|
155
|
+
- Keep workflow definitions in `workflows/`; do not define them inside route/job files.
|
|
@@ -20,7 +20,8 @@ For the complete admin reference with all topics expanded: `AGENTS.md`
|
|
|
20
20
|
- **@base-ui/react** primitives (NOT @radix-ui)
|
|
21
21
|
- **@iconify/react** with Phosphor icon set (`ph:icon-name`)
|
|
22
22
|
- **sonner** for toasts — `toast.error()`, `toast.success()`
|
|
23
|
-
-
|
|
23
|
+
- QUESTPIE Neutral Design: flat surfaces, soft neutral geometry,
|
|
24
|
+
tokenized radius, and restrained floating shadows
|
|
24
25
|
|
|
25
26
|
## Setup
|
|
26
27
|
|
|
@@ -33,8 +34,7 @@ bun add @questpie/admin
|
|
|
33
34
|
### 2. Runtime Config
|
|
34
35
|
|
|
35
36
|
```ts title="questpie.config.ts"
|
|
36
|
-
import { runtimeConfig } from "questpie";
|
|
37
|
-
|
|
37
|
+
import { runtimeConfig } from "questpie/app";
|
|
38
38
|
export default runtimeConfig({
|
|
39
39
|
app: { url: process.env.APP_URL || "http://localhost:3000" },
|
|
40
40
|
db: { url: process.env.DATABASE_URL },
|
|
@@ -47,7 +47,8 @@ The admin module contributes the codegen plugin automatically. It discovers `con
|
|
|
47
47
|
### 3. Modules
|
|
48
48
|
|
|
49
49
|
```ts title="modules.ts"
|
|
50
|
-
import { adminModule
|
|
50
|
+
import { adminModule } from "@questpie/admin/modules/admin";
|
|
51
|
+
import { auditModule } from "@questpie/admin/modules/audit";
|
|
51
52
|
|
|
52
53
|
export default [adminModule, auditModule] as const;
|
|
53
54
|
```
|
|
@@ -57,6 +58,26 @@ export default [adminModule, auditModule] as const;
|
|
|
57
58
|
| `adminModule` | User collection, auth pages, admin UI |
|
|
58
59
|
| `auditModule` | Audit log collection, timeline widget |
|
|
59
60
|
|
|
61
|
+
### Auth/User Contract - Critical
|
|
62
|
+
|
|
63
|
+
`adminModule` includes the starter auth model and owns the canonical Better Auth `user` collection shape used by admin setup and login guards. That contract includes `user.role` with at least `admin` and `user` values. The built-in setup route checks for `role = "admin"`, and the admin `AuthGuard` expects `session.user.role === "admin"`.
|
|
64
|
+
|
|
65
|
+
Do not create a replacement `collection("user")` from scratch in an app that uses `adminModule`. If the app needs to customize the user admin UI or add fields, merge the starter user collection and extend it:
|
|
66
|
+
|
|
67
|
+
```ts title="collections/user.ts"
|
|
68
|
+
import { starterModule } from "questpie/app";
|
|
69
|
+
import { collection } from "#questpie/factories";
|
|
70
|
+
|
|
71
|
+
export default collection("user")
|
|
72
|
+
.merge(starterModule.collections.user)
|
|
73
|
+
.fields(({ f }) => ({
|
|
74
|
+
// add app-specific user fields here
|
|
75
|
+
internalNotes: f.textarea(),
|
|
76
|
+
}));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
App-specific authorization tables are fine, but they must not replace or remove the admin user contract unless the app also replaces the built-in admin setup route and auth guard.
|
|
80
|
+
|
|
60
81
|
### 4. Admin Config
|
|
61
82
|
|
|
62
83
|
```ts title="config/admin.ts"
|
|
@@ -129,44 +150,31 @@ export default adminConfig({
|
|
|
129
150
|
|
|
130
151
|
The admin uses CSS variables for all theming. Override them in your own CSS file.
|
|
131
152
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
| `--
|
|
147
|
-
| `--
|
|
148
|
-
| `--
|
|
149
|
-
| `--
|
|
150
|
-
| `--radius` | `0px` | Border radius (0 = brutalist) |
|
|
151
|
-
|
|
152
|
-
### Dark Theme (`.dark` class)
|
|
153
|
-
|
|
154
|
-
Dark mode uses the `.dark` class on the root element. Key overrides:
|
|
155
|
-
|
|
156
|
-
| Variable | Dark Value |
|
|
157
|
-
| -------------- | ---------- |
|
|
158
|
-
| `--background` | `#0A0A0A` |
|
|
159
|
-
| `--foreground` | `#FFFFFF` |
|
|
160
|
-
| `--card` | `#111111` |
|
|
161
|
-
| `--border` | `#333333` |
|
|
162
|
-
| `--muted` | `#1A1A1A` |
|
|
153
|
+
The full source of truth is `packages/admin/DESIGN.md`. Key defaults:
|
|
154
|
+
|
|
155
|
+
| Role | Dark | Light |
|
|
156
|
+
| ------------- | --------- | --------- |
|
|
157
|
+
| Background | `#121212` | `#fafafa` |
|
|
158
|
+
| Foreground | `#ececec` | `#1c1c1c` |
|
|
159
|
+
| Card | `#1b1b1b` | `#ffffff` |
|
|
160
|
+
| Surface high | `#2a2a2a` | `#e8e8e8` |
|
|
161
|
+
| Border | `#343434` | `#e2e2e2` |
|
|
162
|
+
| Border subtle | `#262626` | `#ebebeb` |
|
|
163
|
+
| Brand primary | `#b700ff` | `#b700ff` |
|
|
164
|
+
|
|
165
|
+
| Token | Default | Use |
|
|
166
|
+
| ------------------------ | ------- | ------------------------------------------- |
|
|
167
|
+
| `--control-radius-inner` | `8px` | Icon buttons/actions nested inside controls |
|
|
168
|
+
| `--control-radius` | `12px` | Inputs, selects, buttons, compact controls |
|
|
169
|
+
| `--surface-radius` | `14px` | Cards, panels, grouped fields, docs blocks |
|
|
170
|
+
| `--floating-radius` | `14px` | Menus, popovers, dialogs, command panels |
|
|
163
171
|
|
|
164
172
|
### Typography
|
|
165
173
|
|
|
166
174
|
| Variable | Value |
|
|
167
175
|
| ------------- | ------------------------------------------------------------------- |
|
|
168
|
-
| `--font-sans` | `"Geist Variable"` —
|
|
169
|
-
| `--font-mono` | `"JetBrains Mono Variable"` —
|
|
176
|
+
| `--font-sans` | `"Geist Variable"` — UI, prose, headings, labels, navigation |
|
|
177
|
+
| `--font-mono` | `"JetBrains Mono Variable"` — code, file paths, commands, IDs |
|
|
170
178
|
|
|
171
179
|
### Sidebar Variables
|
|
172
180
|
|
|
@@ -184,8 +192,7 @@ Separate tokens for independent sidebar theming: `--sidebar`, `--sidebar-foregro
|
|
|
184
192
|
When collections have `.localized()` fields, the admin shows a locale switcher:
|
|
185
193
|
|
|
186
194
|
```ts title="config/app.ts"
|
|
187
|
-
import { appConfig } from "questpie";
|
|
188
|
-
|
|
195
|
+
import { appConfig } from "questpie/app";
|
|
189
196
|
export default appConfig({
|
|
190
197
|
locale: {
|
|
191
198
|
locales: [
|
|
@@ -213,9 +220,9 @@ The admin renders drag-and-drop upload, image preview, file info, and remove but
|
|
|
213
220
|
|
|
214
221
|
## Live Preview
|
|
215
222
|
|
|
216
|
-
Live Preview
|
|
223
|
+
Live Preview is one system: the existing collection `FormView`, Preview button, `LivePreviewMode`, and frontend iframe. Preserve the normal form lifecycle. Do not introduce a separate visual-edit form API, a second default form view, or a parallel preview surface.
|
|
217
224
|
|
|
218
|
-
|
|
225
|
+
The admin form is authoritative. The iframe mirrors form state through `postMessage`, supports field/block focus, and may request inline scalar edits. Persistence still goes through existing save, autosave, Cmd+S, history, workflow, locks, and actions.
|
|
219
226
|
|
|
220
227
|
### Server Config
|
|
221
228
|
|
|
@@ -239,35 +246,57 @@ export const pages = collection("pages")
|
|
|
239
246
|
});
|
|
240
247
|
```
|
|
241
248
|
|
|
242
|
-
|
|
249
|
+
Preview opens the existing split-screen editor. Patches and refresh/resync messages update the iframe mirror; save/autosave still writes through the form.
|
|
250
|
+
|
|
251
|
+
### Prepare a Frontend Page
|
|
243
252
|
|
|
244
|
-
|
|
253
|
+
Use exported APIs only: `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and `BlockRenderer`.
|
|
245
254
|
|
|
246
|
-
|
|
255
|
+
Checklist:
|
|
256
|
+
|
|
257
|
+
1. Configure `.preview({ url })` on the collection.
|
|
258
|
+
2. Load the same record shape the page normally renders.
|
|
259
|
+
3. For workflow-published pages, public reads use `stage: "published"`; if the public client/HTTP API is exposed, anonymous read access also checks `input?.stage === "published"`. Authorized preview/draft reads can load the working record.
|
|
260
|
+
4. Call `useCollectionPreview({ initialData, onRefresh })` in the page renderer.
|
|
261
|
+
5. Wrap the visual output in `PreviewProvider`.
|
|
262
|
+
6. Render from `preview.data`, not the original loader object.
|
|
263
|
+
7. Wrap editable scalar text with `PreviewField field="..." editable="text" | "textarea"`.
|
|
264
|
+
8. Render block content with `BlockRenderer`; pass `selectedBlockId` and `onBlockClick`.
|
|
265
|
+
9. Keep add/remove/reorder/nesting block operations in the existing block editor.
|
|
247
266
|
|
|
248
267
|
```tsx
|
|
249
268
|
import {
|
|
269
|
+
BlockRenderer,
|
|
250
270
|
PreviewField,
|
|
251
271
|
PreviewProvider,
|
|
252
272
|
useCollectionPreview,
|
|
253
273
|
} from "@questpie/admin/client";
|
|
274
|
+
import admin from "@/questpie/admin/.generated/client";
|
|
254
275
|
|
|
255
|
-
function PagePreview({
|
|
276
|
+
function PagePreview({ page }) {
|
|
256
277
|
const router = useRouter();
|
|
257
278
|
const preview = useCollectionPreview({
|
|
258
|
-
initialData,
|
|
279
|
+
initialData: page,
|
|
259
280
|
onRefresh: () => router.invalidate(),
|
|
260
281
|
});
|
|
261
282
|
|
|
262
283
|
return (
|
|
263
|
-
<PreviewProvider
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
<PreviewProvider preview={preview}>
|
|
285
|
+
<main className={preview.isPreviewMode ? "questpie-preview" : undefined}>
|
|
286
|
+
<PreviewField field="title" editable="text" as="h1">
|
|
287
|
+
{preview.data.title}
|
|
288
|
+
</PreviewField>
|
|
289
|
+
|
|
290
|
+
<BlockRenderer
|
|
291
|
+
content={preview.data.content}
|
|
292
|
+
data={preview.data.content?._data}
|
|
293
|
+
renderers={admin.blocks}
|
|
294
|
+
selectedBlockId={preview.selectedBlockId}
|
|
295
|
+
onBlockClick={
|
|
296
|
+
preview.isPreviewMode ? preview.handleBlockClick : undefined
|
|
297
|
+
}
|
|
298
|
+
/>
|
|
299
|
+
</main>
|
|
271
300
|
</PreviewProvider>
|
|
272
301
|
);
|
|
273
302
|
}
|
|
@@ -275,11 +304,15 @@ function PagePreview({ initialData }) {
|
|
|
275
304
|
|
|
276
305
|
### Key Principles
|
|
277
306
|
|
|
278
|
-
-
|
|
279
|
-
-
|
|
307
|
+
- Keep `FormView`, the Preview button, and `LivePreviewMode`
|
|
308
|
+
- Never add a separate visual-edit form API, a second default form view, or parallel preview API names
|
|
309
|
+
- Preserve save/autosave/Cmd+S/history/workflow/locks/actions
|
|
310
|
+
- `useCollectionPreview` handles preview mode, mirrored data, refresh/resync, and focus state
|
|
280
311
|
- `PreviewProvider` supplies preview context to `PreviewField`
|
|
281
|
-
-
|
|
282
|
-
-
|
|
312
|
+
- `PreviewField` annotates scalar fields and can opt into inline editing with `editable`
|
|
313
|
+
- `BlockRenderer` preserves block IDs and block scopes for block annotations
|
|
314
|
+
- Use `BlockScopeProvider` only for custom/manual block rendering outside `BlockRenderer`
|
|
315
|
+
- Validate all iframe messages before updating form state
|
|
283
316
|
|
|
284
317
|
## History & Versions
|
|
285
318
|
|
|
@@ -560,7 +593,7 @@ Section-level visibility:
|
|
|
560
593
|
{
|
|
561
594
|
type: "section",
|
|
562
595
|
label: { en: "SEO" },
|
|
563
|
-
hidden: ({ data }) => !data.
|
|
596
|
+
hidden: ({ data }) => !data.showSeo,
|
|
564
597
|
fields: [f.metaTitle, f.metaDescription],
|
|
565
598
|
}
|
|
566
599
|
```
|
|
@@ -803,7 +836,7 @@ export const logs = collection("logs")
|
|
|
803
836
|
|
|
804
837
|
## Form Views and Live Preview
|
|
805
838
|
|
|
806
|
-
Form views connect to the Live Preview
|
|
839
|
+
Form views connect to the existing Live Preview system when the collection has `.preview()` configured. Keep `v.collectionForm()` as the form surface; do not introduce a separate visual-edit form API, a second default form view, or parallel preview API names. The form editor remains the source of patches, refreshes, commits, and resyncs.
|
|
807
840
|
|
|
808
841
|
### Enabling Preview on a Collection
|
|
809
842
|
|
|
@@ -827,9 +860,25 @@ export const pages = collection("pages")
|
|
|
827
860
|
### How It Works
|
|
828
861
|
|
|
829
862
|
1. The form view detects `.preview()` config and opens a split-screen layout
|
|
830
|
-
2.
|
|
831
|
-
3.
|
|
832
|
-
4.
|
|
863
|
+
2. The preview iframe mirrors form state with snapshot, patch, refresh, commit, and resync messages
|
|
864
|
+
3. Save/autosave/Cmd+S/history/workflow/locks/actions stay in the form lifecycle
|
|
865
|
+
4. The preview page uses `useCollectionPreview({ initialData, onRefresh })`
|
|
866
|
+
5. `PreviewProvider`, `PreviewField`, and `BlockRenderer` wire field and block annotations
|
|
867
|
+
|
|
868
|
+
### Frontend Preparation Checklist
|
|
869
|
+
|
|
870
|
+
Use this checklist before expecting visual editing to work:
|
|
871
|
+
|
|
872
|
+
1. The collection has `.preview({ url })`.
|
|
873
|
+
2. The page loader returns the complete rendered record shape.
|
|
874
|
+
3. Public workflow reads use `stage: "published"`; if public client/HTTP access is enabled, anonymous read access also requires `input?.stage === "published"`. Preview/draft-mode reads load the working record for authorized editors.
|
|
875
|
+
4. The page renderer calls `useCollectionPreview({ initialData, onRefresh })`.
|
|
876
|
+
5. The rendered page is wrapped in `PreviewProvider preview={preview}`.
|
|
877
|
+
6. UI reads from `preview.data`, not directly from loader data.
|
|
878
|
+
7. Scalar text uses `PreviewField field="..." editable="text" | "textarea"` when inline editing is expected.
|
|
879
|
+
8. Blocks render through `BlockRenderer` with `selectedBlockId` and `onBlockClick`.
|
|
880
|
+
9. `BlockScopeProvider` is only needed for custom/manual block rendering outside `BlockRenderer`.
|
|
881
|
+
10. Add/remove/reorder/nesting block operations stay in the existing block editor.
|
|
833
882
|
|
|
834
883
|
---
|
|
835
884
|
|
|
@@ -1111,11 +1160,35 @@ function PageRenderer({ page }) {
|
|
|
1111
1160
|
|
|
1112
1161
|
## Blocks in Live Preview
|
|
1113
1162
|
|
|
1114
|
-
When a collection has `.preview()` configured, blocks
|
|
1163
|
+
When a collection has `.preview()` configured, blocks participate in the existing Live Preview system through `BlockRenderer` and `PreviewField`. The form remains authoritative; block annotations only mirror values, focus fields, select blocks, or request inline scalar edits.
|
|
1164
|
+
|
|
1165
|
+
### Preferred: BlockRenderer
|
|
1166
|
+
|
|
1167
|
+
Use `BlockRenderer` for normal frontend page rendering. It preserves `data-block-id`, routes block selection, and scopes nested `PreviewField` paths automatically.
|
|
1168
|
+
|
|
1169
|
+
```tsx
|
|
1170
|
+
<BlockRenderer
|
|
1171
|
+
content={preview.data.content}
|
|
1172
|
+
data={preview.data.content?._data}
|
|
1173
|
+
renderers={admin.blocks}
|
|
1174
|
+
selectedBlockId={preview.selectedBlockId}
|
|
1175
|
+
onBlockClick={preview.isPreviewMode ? preview.handleBlockClick : undefined}
|
|
1176
|
+
/>
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
Inside custom block renderers, annotate scalar values with `PreviewField`:
|
|
1180
|
+
|
|
1181
|
+
```tsx
|
|
1182
|
+
<PreviewField field="title" editable="text" as="h2">
|
|
1183
|
+
{values.title}
|
|
1184
|
+
</PreviewField>
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
This resolves to `content._values.{blockId}.title` when rendered inside `BlockRenderer`.
|
|
1115
1188
|
|
|
1116
|
-
### BlockScopeProvider Wrapper
|
|
1189
|
+
### Manual BlockScopeProvider Wrapper
|
|
1117
1190
|
|
|
1118
|
-
Use `BlockScopeProvider`
|
|
1191
|
+
Use `BlockScopeProvider` only when you manually render blocks outside `BlockRenderer`:
|
|
1119
1192
|
|
|
1120
1193
|
```tsx
|
|
1121
1194
|
import { BlockScopeProvider } from "@questpie/admin/client";
|
|
@@ -1134,7 +1207,7 @@ function PageRenderer({ blocks, previewData }) {
|
|
|
1134
1207
|
|
|
1135
1208
|
`PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
|
|
1136
1209
|
|
|
1137
|
-
|
|
1210
|
+
Inline block edits target `_values`, for example `content._values.<blockId>.title`. Tree edits such as add, remove, reorder, and nesting stay in the existing block editor and should trigger refresh or resync when patching is not safe.
|
|
1138
1211
|
|
|
1139
1212
|
---
|
|
1140
1213
|
|
|
@@ -1437,6 +1510,6 @@ toast.error("Failed to save");
|
|
|
1437
1510
|
|
|
1438
1511
|
6. **MEDIUM: Using `@phosphor-icons/react` or `lucide-react`** — use `@iconify/react` with `ph:` prefix for all icons.
|
|
1439
1512
|
|
|
1440
|
-
7. **LOW: Not using shadcn components** — prefer `<Button>`, `<Card>`, `<Input>` from the shadcn component library instead of raw HTML elements. The admin
|
|
1513
|
+
7. **LOW: Not using shadcn components** — prefer `<Button>`, `<Card>`, `<Input>` from the shadcn component library instead of raw HTML elements. The admin follows QUESTPIE Neutral Design.
|
|
1441
1514
|
|
|
1442
1515
|
---
|