create-questpie 2.0.2 → 2.0.3
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/package.json +1 -1
- package/skills/questpie/AGENTS.md +11 -5
- package/skills/questpie/SKILL.md +161 -82
- package/skills/questpie/references/data-modeling.md +4 -0
- package/skills/questpie/references/extend.md +64 -0
- package/skills/questpie/references/quickstart.md +25 -10
- package/skills/questpie/references/rules.md +62 -0
- package/skills/questpie-admin/AGENTS.md +94 -28
- package/skills/questpie-admin/SKILL.md +50 -24
- package/skills/questpie-admin/references/blocks.md +28 -4
- package/skills/questpie-admin/references/views.md +21 -5
- package/templates/tanstack-start/src/lib/query-client.ts +10 -1
- package/templates/tanstack-start/src/routes/admin/$.tsx +12 -1
- package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
package/package.json
CHANGED
|
@@ -295,7 +295,7 @@ export const posts = collection("posts")
|
|
|
295
295
|
title: f.text(255).required().label("Title"),
|
|
296
296
|
slug: f.text(255).required(),
|
|
297
297
|
content: f.richText().localized(),
|
|
298
|
-
status: f.select(["
|
|
298
|
+
status: f.select(["internal", "featured"]).default("internal"),
|
|
299
299
|
author: f.relation("users"),
|
|
300
300
|
tags: f.relation("tags").manyToMany({ through: "post_tags" }),
|
|
301
301
|
cover: f.upload(),
|
|
@@ -376,9 +376,9 @@ export const posts = collection("posts")
|
|
|
376
376
|
description: "It will become visible to all readers.",
|
|
377
377
|
},
|
|
378
378
|
handler: async ({ record, collections }) => {
|
|
379
|
-
await collections.posts.
|
|
379
|
+
await collections.posts.transitionStage({
|
|
380
380
|
id: record.id,
|
|
381
|
-
|
|
381
|
+
stage: "published",
|
|
382
382
|
});
|
|
383
383
|
return { type: "success", toast: { message: "Published!" } };
|
|
384
384
|
},
|
|
@@ -1238,6 +1238,8 @@ collection("pages").options({
|
|
|
1238
1238
|
- `transitionStage({ id, stage: "published" })` → move between workflow stages
|
|
1239
1239
|
- `beforeTransition` / `afterTransition` hooks
|
|
1240
1240
|
|
|
1241
|
+
For publishable pages with workflow enabled, workflow stage is the publication source. Public reads must pass `stage: "published"`. If public client/HTTP access is enabled, anonymous read access should require `input?.stage === "published"` so callers cannot omit `stage` and fetch the working draft. Preview/draft-mode reads may omit `stage` to show the working stage to authorized editors. Do not add duplicate `isPublished` guidance when workflow already controls publishing.
|
|
1242
|
+
|
|
1241
1243
|
### API Usage
|
|
1242
1244
|
|
|
1243
1245
|
```ts
|
|
@@ -2012,6 +2014,10 @@ collection("posts").preview({
|
|
|
2012
2014
|
});
|
|
2013
2015
|
```
|
|
2014
2016
|
|
|
2017
|
+
Live Preview uses the existing admin `FormView`, Preview button, `LivePreviewMode`, and iframe. Do not introduce a separate visual-edit form API, a second default form view, or parallel preview API names. Preserve save, autosave, Cmd+S, history, workflow transitions, locks, and actions in the normal form lifecycle.
|
|
2018
|
+
|
|
2019
|
+
Frontend visual editing needs `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and usually `BlockRenderer`; load the `questpie-admin` skill for the full frontend preparation checklist.
|
|
2020
|
+
|
|
2015
2021
|
### `.actions()` — Server Actions
|
|
2016
2022
|
|
|
2017
2023
|
```ts
|
|
@@ -2028,9 +2034,9 @@ collection("posts").actions(({ a, c, f }) => ({
|
|
|
2028
2034
|
destructive: false,
|
|
2029
2035
|
},
|
|
2030
2036
|
handler: async ({ record, collections }) => {
|
|
2031
|
-
await collections.posts.
|
|
2037
|
+
await collections.posts.transitionStage({
|
|
2032
2038
|
id: record.id,
|
|
2033
|
-
|
|
2039
|
+
stage: "published",
|
|
2034
2040
|
});
|
|
2035
2041
|
return { type: "success", toast: { message: "Published!" } };
|
|
2036
2042
|
},
|
package/skills/questpie/SKILL.md
CHANGED
|
@@ -14,6 +14,7 @@ Server-first TypeScript application framework. Define your data schema once usin
|
|
|
14
14
|
## When to Apply
|
|
15
15
|
|
|
16
16
|
Reference these guidelines when:
|
|
17
|
+
|
|
17
18
|
- Creating or modifying collections, globals, routes, jobs, services, emails, blocks
|
|
18
19
|
- Working with file conventions or codegen pipeline
|
|
19
20
|
- Configuring adapters (queue, search, storage, realtime, email, KV)
|
|
@@ -24,58 +25,120 @@ Reference these guidelines when:
|
|
|
24
25
|
|
|
25
26
|
## Import Paths — Critical
|
|
26
27
|
|
|
27
|
-
| Factory
|
|
28
|
-
|
|
29
|
-
| `collection(name)`
|
|
30
|
-
| `global(name)`
|
|
31
|
-
| `block(name)`
|
|
32
|
-
| `adminConfig({...})`
|
|
33
|
-
| `route()`
|
|
34
|
-
| `job({...})`
|
|
35
|
-
| `service()`
|
|
36
|
-
| `email({...})`
|
|
37
|
-
| `migration({...})`
|
|
38
|
-
| `seed({...})`
|
|
39
|
-
| `runtimeConfig({...})`
|
|
40
|
-
| `appConfig({...})`
|
|
41
|
-
| `authConfig({...})`
|
|
42
|
-
| `createClient<AppConfig>()`
|
|
43
|
-
| `createQuestpieQueryOptions()` | `"@questpie/tanstack-query"` | No
|
|
28
|
+
| Factory | Import From | Needs Codegen? |
|
|
29
|
+
| ------------------------------ | ---------------------------- | -------------- |
|
|
30
|
+
| `collection(name)` | `#questpie/factories` | Yes |
|
|
31
|
+
| `global(name)` | `#questpie/factories` | Yes |
|
|
32
|
+
| `block(name)` | `#questpie/factories` | Yes |
|
|
33
|
+
| `adminConfig({...})` | `#questpie/factories` | Yes |
|
|
34
|
+
| `route()` | `"questpie"` | No |
|
|
35
|
+
| `job({...})` | `"questpie"` | No |
|
|
36
|
+
| `service()` | `"questpie"` | No |
|
|
37
|
+
| `email({...})` | `"questpie"` | No |
|
|
38
|
+
| `migration({...})` | `"questpie"` | No |
|
|
39
|
+
| `seed({...})` | `"questpie"` | No |
|
|
40
|
+
| `runtimeConfig({...})` | `"questpie"` | No |
|
|
41
|
+
| `appConfig({...})` | `"questpie"` | No |
|
|
42
|
+
| `authConfig({...})` | `"questpie"` | No |
|
|
43
|
+
| `createClient<AppConfig>()` | `"questpie/client"` | No |
|
|
44
|
+
| `createQuestpieQueryOptions()` | `"@questpie/tanstack-query"` | No |
|
|
45
|
+
|
|
46
|
+
## Module And Plugin Configuration - Critical
|
|
47
|
+
|
|
48
|
+
Codegen imports `modules.ts` before runtime app creation to extract module-contributed plugins. Any module that contributes a `plugin`, discover patterns, generated factories, categories, views, components, fields, or config factories must be statically discoverable from `modules.ts`.
|
|
49
|
+
|
|
50
|
+
### DO THIS
|
|
51
|
+
|
|
52
|
+
Use a static module and a plugin-discovered config file for runtime options:
|
|
53
|
+
|
|
54
|
+
```ts title="modules.ts"
|
|
55
|
+
import { observabilityModule } from "@questpie/observability/server";
|
|
56
|
+
|
|
57
|
+
export default [observabilityModule] as const;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts title="config/observability.ts"
|
|
61
|
+
import { observabilityConfig } from "@questpie/observability/server";
|
|
62
|
+
|
|
63
|
+
export default observabilityConfig({
|
|
64
|
+
serviceName: "barbershop",
|
|
65
|
+
enabled: process.env.NODE_ENV === "production",
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The module reads config at runtime from `app.state.config.observability`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export const observabilityModule = module({
|
|
73
|
+
name: "questpie-observability",
|
|
74
|
+
plugin: observabilityPlugin(),
|
|
75
|
+
services: {
|
|
76
|
+
observability: service({
|
|
77
|
+
namespace: null,
|
|
78
|
+
lifecycle: "singleton",
|
|
79
|
+
create: ({ app, logger }) =>
|
|
80
|
+
createObservabilityService(app.state.config?.observability, logger),
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### DON'T DO THIS
|
|
87
|
+
|
|
88
|
+
Do not make runtime options the primary API for codegen-aware modules:
|
|
89
|
+
|
|
90
|
+
```ts title="modules.ts"
|
|
91
|
+
export default [
|
|
92
|
+
observabilityModule({
|
|
93
|
+
serviceName: "barbershop",
|
|
94
|
+
}),
|
|
95
|
+
] as const;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Do not conditionally hide codegen-aware modules or plugins behind env/runtime checks:
|
|
99
|
+
|
|
100
|
+
```ts title="modules.ts"
|
|
101
|
+
export default [
|
|
102
|
+
process.env.OTEL_ENABLED ? observabilityModule : undefined,
|
|
103
|
+
].filter(Boolean);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Factory modules are acceptable only for simple runtime-only modules whose plugin identity and codegen contributions do not change. For reusable packages that ship a `CodegenPlugin`, prefer **static module + `config/*.ts` singleton factory**.
|
|
44
107
|
|
|
45
108
|
## Reference Topics
|
|
46
109
|
|
|
47
110
|
### Core
|
|
48
111
|
|
|
49
|
-
| Topic
|
|
50
|
-
|
|
51
|
-
| Quickstart
|
|
52
|
-
| Data Modeling
|
|
53
|
-
| Field Types
|
|
54
|
-
| Rules
|
|
55
|
-
| Business Logic
|
|
56
|
-
| CRUD API
|
|
57
|
-
| Query Operators | `references/query-operators.md` | `where` clause operators by field type
|
|
112
|
+
| Topic | File | Covers |
|
|
113
|
+
| --------------- | ------------------------------- | ------------------------------------------------------------------ |
|
|
114
|
+
| Quickstart | `references/quickstart.md` | Scaffold, configure, codegen, migrate, serve — zero to running app |
|
|
115
|
+
| Data Modeling | `references/data-modeling.md` | Collections, globals, fields, relations, options, localization |
|
|
116
|
+
| Field Types | `references/field-types.md` | All built-in field types with options and operators |
|
|
117
|
+
| Rules | `references/rules.md` | Access control (row/field level), hooks lifecycle, validation |
|
|
118
|
+
| Business Logic | `references/business-logic.md` | Routes, jobs, services, email templates, context injection |
|
|
119
|
+
| CRUD API | `references/crud-api.md` | Server-side `find`, `create`, `update`, `delete`, globals API |
|
|
120
|
+
| Query Operators | `references/query-operators.md` | `where` clause operators by field type |
|
|
58
121
|
|
|
59
122
|
### Infrastructure
|
|
60
123
|
|
|
61
|
-
| Topic
|
|
62
|
-
|
|
63
|
-
| Production | `references/production.md`
|
|
64
|
-
| Auth
|
|
65
|
-
| Adapters
|
|
124
|
+
| Topic | File | Covers |
|
|
125
|
+
| ---------- | --------------------------------------- | ------------------------------------------------------------ |
|
|
126
|
+
| Production | `references/production.md` | Queue, search, realtime, storage, email, KV adapter setup |
|
|
127
|
+
| Auth | `references/auth.md` | Better Auth integration, session, providers, access patterns |
|
|
128
|
+
| Adapters | `references/infrastructure-adapters.md` | All adapter configs: pg-boss, S3, SMTP, pgNotify, Redis |
|
|
66
129
|
|
|
67
130
|
### Extend
|
|
68
131
|
|
|
69
|
-
| Topic
|
|
70
|
-
|
|
71
|
-
| Extend
|
|
72
|
-
| Codegen Plugin API | `references/codegen-plugin-api.md` | Plugin architecture, category declarations, templates
|
|
73
|
-
| Multi-Tenancy
|
|
132
|
+
| Topic | File | Covers |
|
|
133
|
+
| ------------------ | ---------------------------------- | ------------------------------------------------------------ |
|
|
134
|
+
| Extend | `references/extend.md` | Custom modules, fields, operators, adapters, codegen plugins |
|
|
135
|
+
| Codegen Plugin API | `references/codegen-plugin-api.md` | Plugin architecture, category declarations, templates |
|
|
136
|
+
| Multi-Tenancy | `references/multi-tenancy.md` | Scope isolation, workspace filtering, ScopeProvider |
|
|
74
137
|
|
|
75
138
|
### Client
|
|
76
139
|
|
|
77
|
-
| Topic
|
|
78
|
-
|
|
140
|
+
| Topic | File | Covers |
|
|
141
|
+
| -------------- | ------------------------------ | ---------------------------------------------------------------- |
|
|
79
142
|
| TanStack Query | `references/tanstack-query.md` | `q.collections.*`, `q.globals.*`, `q.routes.*`, realtime queries |
|
|
80
143
|
|
|
81
144
|
## Key Patterns — Quick Reference
|
|
@@ -86,23 +149,23 @@ Reference these guidelines when:
|
|
|
86
149
|
import { collection } from "#questpie/factories";
|
|
87
150
|
|
|
88
151
|
export default collection("posts")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
152
|
+
.fields(({ f }) => ({
|
|
153
|
+
title: f.text().required(),
|
|
154
|
+
status: f.select([{ value: "draft", label: "Draft" }]),
|
|
155
|
+
author: f.relation("users").required(),
|
|
156
|
+
}))
|
|
157
|
+
.access({
|
|
158
|
+
read: true,
|
|
159
|
+
create: ({ session }) => !!session,
|
|
160
|
+
update: ({ session, doc }) => doc.authorId === session?.user?.id,
|
|
161
|
+
})
|
|
162
|
+
.hooks({
|
|
163
|
+
beforeChange: async ({ data, operation }) => {
|
|
164
|
+
if (operation === "create") data.slug = slugify(data.title);
|
|
165
|
+
return data;
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
.options({ versioning: true, timestamps: true });
|
|
106
169
|
```
|
|
107
170
|
|
|
108
171
|
### Route
|
|
@@ -112,11 +175,11 @@ import { route } from "questpie";
|
|
|
112
175
|
import z from "zod";
|
|
113
176
|
|
|
114
177
|
export default route()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
178
|
+
.post()
|
|
179
|
+
.schema(z.object({ period: z.enum(["day", "week", "month"]) }))
|
|
180
|
+
.handler(async ({ input, collections }) => {
|
|
181
|
+
return collections.posts.find({ where: { status: "published" } });
|
|
182
|
+
});
|
|
120
183
|
```
|
|
121
184
|
|
|
122
185
|
### Job
|
|
@@ -126,13 +189,15 @@ import { job } from "questpie";
|
|
|
126
189
|
import z from "zod";
|
|
127
190
|
|
|
128
191
|
export default job({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
192
|
+
name: "sendReminder",
|
|
193
|
+
schema: z.object({ userId: z.string() }),
|
|
194
|
+
retryDelay: 5, // seconds (not ms!)
|
|
195
|
+
handler: async ({ payload, email, collections }) => {
|
|
196
|
+
const user = await collections.users.findOne({
|
|
197
|
+
where: { id: payload.userId },
|
|
198
|
+
});
|
|
199
|
+
await email.send("reminder", { to: user.email, data: { name: user.name } });
|
|
200
|
+
},
|
|
136
201
|
});
|
|
137
202
|
```
|
|
138
203
|
|
|
@@ -143,14 +208,17 @@ import { createClient } from "questpie/client";
|
|
|
143
208
|
import type { AppConfig } from "#questpie";
|
|
144
209
|
|
|
145
210
|
const client = createClient<AppConfig>({
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
baseURL:
|
|
212
|
+
typeof window !== "undefined"
|
|
213
|
+
? window.location.origin
|
|
214
|
+
: process.env.APP_URL,
|
|
215
|
+
basePath: "/api",
|
|
148
216
|
});
|
|
149
217
|
|
|
150
218
|
const { docs } = await client.collections.posts.find({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
219
|
+
where: { status: "published" },
|
|
220
|
+
orderBy: { createdAt: "desc" },
|
|
221
|
+
with: { author: true },
|
|
154
222
|
});
|
|
155
223
|
```
|
|
156
224
|
|
|
@@ -164,17 +232,28 @@ await queue.sendReminder.publish({ userId: "abc" });
|
|
|
164
232
|
|
|
165
233
|
## Common Mistakes
|
|
166
234
|
|
|
167
|
-
| Severity | Mistake
|
|
168
|
-
|
|
169
|
-
| CRITICAL | Files in wrong directory
|
|
170
|
-
| CRITICAL | Missing `export default` on convention files
|
|
235
|
+
| Severity | Mistake | Fix |
|
|
236
|
+
| -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
237
|
+
| CRITICAL | Files in wrong directory | Collections in `collections/`, routes in `routes/`, etc. |
|
|
238
|
+
| CRITICAL | Missing `export default` on convention files | Codegen silently ignores files without default export |
|
|
171
239
|
| CRITICAL | Importing route/job/service from `#questpie/factories` | Use `"questpie"` — only collection/global/block/adminConfig use `#questpie/factories` |
|
|
172
|
-
| HIGH
|
|
173
|
-
| HIGH
|
|
174
|
-
| HIGH
|
|
175
|
-
| HIGH
|
|
176
|
-
|
|
|
177
|
-
| MEDIUM
|
|
240
|
+
| HIGH | Forgetting `questpie generate` after adding files | Re-run codegen on any file add/remove in convention dirs |
|
|
241
|
+
| HIGH | Job handler uses `input` instead of `payload` | Jobs destructure `{ payload }`, routes destructure `{ input }` |
|
|
242
|
+
| HIGH | `queue.send("name", data)` | Use `queue.jobName.publish(data)` |
|
|
243
|
+
| HIGH | `beforeCreate` / `afterCreate` hook names | Use `beforeChange` / `afterChange` with `operation === "create"` guard |
|
|
244
|
+
| HIGH | Runtime options in codegen-aware modules | Use static `module({...})` + plugin-discovered `config/*.ts` factory |
|
|
245
|
+
| MEDIUM | Using npm/yarn instead of Bun | QUESTPIE requires Bun as package manager |
|
|
246
|
+
| MEDIUM | Editing `.generated/` files | Never edit — re-run `questpie generate` |
|
|
247
|
+
|
|
248
|
+
## Preview And Workflow Rules
|
|
249
|
+
|
|
250
|
+
- Live Preview uses the existing admin `FormView`, Preview button, `LivePreviewMode`, and iframe. Do not introduce a separate visual-edit form API, a second default form view, or parallel preview API names.
|
|
251
|
+
- Preserve save, autosave, Cmd+S, history, workflow transitions, locks, and actions in the normal form lifecycle.
|
|
252
|
+
- Frontend visual editing needs `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and usually `BlockRenderer`; load the `questpie-admin` skill for the full frontend preparation checklist.
|
|
253
|
+
- For publishable pages with workflow enabled, workflow stage is the publication source. Public frontend reads use `stage: "published"`.
|
|
254
|
+
- If public client/HTTP access is enabled, anonymous read access should require `input?.stage === "published"`; editor/preview reads can omit `stage` only when a session is present.
|
|
255
|
+
- Preview/draft-mode reads may load the working stage for authorized editors.
|
|
256
|
+
- Do not add duplicate `isPublished` guidance when workflow already controls publishing.
|
|
178
257
|
|
|
179
258
|
## Full Compiled Document
|
|
180
259
|
|
|
@@ -94,6 +94,10 @@ import { uniqueIndex } from "drizzle-orm/pg-core";
|
|
|
94
94
|
})
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
Live Preview uses the existing admin `FormView`, Preview button, `LivePreviewMode`, and iframe. Do not introduce a separate visual-edit form API, a second default form view, or parallel preview API names.
|
|
98
|
+
|
|
99
|
+
When workflow is the publication source for pages, public reads use `stage: "published"` and preview/draft-mode reads can load the working stage for authorized editors. Do not add duplicate publication booleans for the same concern.
|
|
100
|
+
|
|
97
101
|
### Access Control
|
|
98
102
|
|
|
99
103
|
```ts
|
|
@@ -86,6 +86,70 @@ export default runtimeConfig({
|
|
|
86
86
|
});
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
Use direct `runtimeConfig({ plugins })` registration only for standalone codegen plugins or custom setups that do not ship a module. Reusable packages should usually attach the plugin to a static module and let codegen extract it from `modules.ts`.
|
|
90
|
+
|
|
91
|
+
### Configurable Codegen-Aware Modules
|
|
92
|
+
|
|
93
|
+
When a package ships a module and a `CodegenPlugin`, keep module identity static and put runtime options in a plugin-discovered config file. Codegen imports `modules.ts` before runtime app creation, so it must be able to see the same module/plugin tree regardless of environment or runtime options.
|
|
94
|
+
|
|
95
|
+
#### DO THIS
|
|
96
|
+
|
|
97
|
+
```ts title="modules.ts"
|
|
98
|
+
import { observabilityModule } from "@questpie/observability/server";
|
|
99
|
+
|
|
100
|
+
export default [observabilityModule] as const;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts title="config/observability.ts"
|
|
104
|
+
import { observabilityConfig } from "@questpie/observability/server";
|
|
105
|
+
|
|
106
|
+
export default observabilityConfig({
|
|
107
|
+
serviceName: "barbershop",
|
|
108
|
+
enabled: process.env.NODE_ENV === "production",
|
|
109
|
+
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ts title="@questpie/observability/server.ts"
|
|
114
|
+
export const observabilityModule = module({
|
|
115
|
+
name: "questpie-observability",
|
|
116
|
+
plugin: observabilityPlugin(),
|
|
117
|
+
services: {
|
|
118
|
+
observability: service({
|
|
119
|
+
namespace: null,
|
|
120
|
+
lifecycle: "singleton",
|
|
121
|
+
create: ({ app, logger }) =>
|
|
122
|
+
createObservabilityService(app.state.config?.observability, logger),
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The plugin contributes `config/observability.ts` as a discover pattern and a typed singleton factory such as `observabilityConfig()`. The service reads the resolved config at runtime from `app.state.config.observability`.
|
|
129
|
+
|
|
130
|
+
#### DON'T DO THIS
|
|
131
|
+
|
|
132
|
+
Do not make runtime options the main API for modules that contribute codegen plugins:
|
|
133
|
+
|
|
134
|
+
```ts title="modules.ts"
|
|
135
|
+
export default [
|
|
136
|
+
observabilityModule({
|
|
137
|
+
serviceName: "barbershop",
|
|
138
|
+
enabled: process.env.NODE_ENV === "production",
|
|
139
|
+
}),
|
|
140
|
+
] as const;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Do not conditionally include codegen-aware modules or plugins:
|
|
144
|
+
|
|
145
|
+
```ts title="modules.ts"
|
|
146
|
+
export default [
|
|
147
|
+
process.env.OTEL_ENABLED ? observabilityModule : undefined,
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Factory modules are acceptable only for simple runtime-only modules whose plugin identity and generated contributions do not change. If the package contributes discover patterns, generated factories, module categories, views, components, fields, or collection/global extensions, use **static module + `config/*.ts` singleton factory**.
|
|
152
|
+
|
|
89
153
|
### Plugin Lifecycle
|
|
90
154
|
|
|
91
155
|
1. **Discovery** -- codegen scans for files matching category patterns and discover patterns
|
|
@@ -494,7 +494,7 @@ bun dev
|
|
|
494
494
|
|
|
495
495
|
## 12. Live Preview (Optional)
|
|
496
496
|
|
|
497
|
-
Add split-screen live preview to any collection with `.preview()`.
|
|
497
|
+
Add split-screen live preview to any collection with `.preview()`. Live Preview uses the existing admin `FormView`, Preview button, `LivePreviewMode`, and iframe. Do not introduce a second default form view or parallel preview API names.
|
|
498
498
|
|
|
499
499
|
### Add Preview to a Collection
|
|
500
500
|
|
|
@@ -518,32 +518,47 @@ export default collection("pages")
|
|
|
518
518
|
|
|
519
519
|
### Add Preview Support to the Frontend Page
|
|
520
520
|
|
|
521
|
+
Frontend checklist:
|
|
522
|
+
|
|
523
|
+
1. Call `useCollectionPreview({ initialData, onRefresh })`.
|
|
524
|
+
2. Wrap the rendered output in `PreviewProvider`.
|
|
525
|
+
3. Render from `preview.data`, not directly from loader data.
|
|
526
|
+
4. Wrap editable scalar text in `PreviewField`.
|
|
527
|
+
5. Render blocks with `BlockRenderer` when the page uses `f.blocks()`.
|
|
528
|
+
|
|
521
529
|
```tsx
|
|
522
530
|
import {
|
|
531
|
+
BlockRenderer,
|
|
523
532
|
PreviewField,
|
|
524
533
|
PreviewProvider,
|
|
525
534
|
useCollectionPreview,
|
|
526
535
|
} from "@questpie/admin/client";
|
|
536
|
+
import admin from "@/questpie/admin/.generated/client";
|
|
527
537
|
|
|
528
|
-
function PageView({
|
|
538
|
+
function PageView({ page }) {
|
|
529
539
|
const router = useRouter();
|
|
530
540
|
const preview = useCollectionPreview({
|
|
531
|
-
initialData,
|
|
541
|
+
initialData: page,
|
|
532
542
|
onRefresh: () => router.invalidate(),
|
|
533
543
|
});
|
|
534
544
|
|
|
535
545
|
return (
|
|
536
|
-
<PreviewProvider
|
|
537
|
-
|
|
538
|
-
focusedField={preview.focusedField}
|
|
539
|
-
onFieldClick={preview.handleFieldClick}
|
|
540
|
-
>
|
|
541
|
-
<PreviewField field="title" as="h1">
|
|
546
|
+
<PreviewProvider preview={preview}>
|
|
547
|
+
<PreviewField field="title" editable="text" as="h1">
|
|
542
548
|
{preview.data.title}
|
|
543
549
|
</PreviewField>
|
|
550
|
+
<BlockRenderer
|
|
551
|
+
content={preview.data.content}
|
|
552
|
+
data={preview.data.content?._data}
|
|
553
|
+
renderers={admin.blocks}
|
|
554
|
+
selectedBlockId={preview.selectedBlockId}
|
|
555
|
+
onBlockClick={
|
|
556
|
+
preview.isPreviewMode ? preview.handleBlockClick : undefined
|
|
557
|
+
}
|
|
558
|
+
/>
|
|
544
559
|
</PreviewProvider>
|
|
545
560
|
);
|
|
546
561
|
}
|
|
547
562
|
```
|
|
548
563
|
|
|
549
|
-
The
|
|
564
|
+
The form remains authoritative. Save, autosave, Cmd+S, history, workflow, locks, and actions stay in the existing form lifecycle.
|
|
@@ -88,6 +88,49 @@ Access functions receive `AppContext` with these properties:
|
|
|
88
88
|
| `session` | Current auth session (null if unauthed) |
|
|
89
89
|
| `db` | Database instance |
|
|
90
90
|
| `collections` | Typed collection API |
|
|
91
|
+
| `request` | Current HTTP `Request` (headers, URL) |
|
|
92
|
+
|
|
93
|
+
Access functions may be async. Use `request` for request-scoped checks such as headers, tenant scope, CAPTCHA tokens, or signed public form tokens:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { ApiError } from "questpie";
|
|
97
|
+
import { isAdminRequest } from "@questpie/admin/shared";
|
|
98
|
+
|
|
99
|
+
type AccessCtx = {
|
|
100
|
+
request?: Request | null;
|
|
101
|
+
session?: { user?: unknown | null } | null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
async function canCreatePublicSubmission({ request, session }: AccessCtx) {
|
|
105
|
+
if (session?.user) return true;
|
|
106
|
+
if (request && isAdminRequest(request)) {
|
|
107
|
+
throw ApiError.unauthorized();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const token = request?.headers.get("x-captcha-token");
|
|
111
|
+
const valid = token ? await verifyCaptchaToken(token) : false;
|
|
112
|
+
if (valid) return true;
|
|
113
|
+
|
|
114
|
+
throw ApiError.forbidden({
|
|
115
|
+
operation: "create",
|
|
116
|
+
resource: "public_submissions",
|
|
117
|
+
reason: "CAPTCHA verification failed",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default collection("public_submissions")
|
|
122
|
+
.fields(({ f }) => ({
|
|
123
|
+
message: f.textarea().required(),
|
|
124
|
+
}))
|
|
125
|
+
.access({
|
|
126
|
+
read: false,
|
|
127
|
+
create: canCreatePublicSubmission,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For public anti-abuse checks, bypass already authenticated users before requiring a CAPTCHA token. Admin-origin requests should not be asked for CAPTCHA either, but remember that `isAdminRequest()` is a caller-intent signal, not authentication; if an admin-origin request reaches this rule without a session, fail it as unauthorized instead of accepting it.
|
|
132
|
+
|
|
133
|
+
Prefer throwing `ApiError.*` from access rules when callers need a specific structured error response. Returning `false` is fine for generic denial, but it produces the default forbidden message.
|
|
91
134
|
|
|
92
135
|
### System Access Mode
|
|
93
136
|
|
|
@@ -322,6 +365,25 @@ Live preview sessions use token-based authentication. When a preview iframe load
|
|
|
322
365
|
- Access rules (`.access()`) still apply to all data fetched during preview, including prefetched relations and block data.
|
|
323
366
|
- Row-level access (AccessWhere) filters are enforced even in preview context — a user cannot preview records they cannot read.
|
|
324
367
|
|
|
368
|
+
### Workflow Published Reads
|
|
369
|
+
|
|
370
|
+
For publishable collections that use workflow stages, do not use `read: true` when public client or HTTP access is available. Gate anonymous reads to the published stage:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
.access({
|
|
374
|
+
read: ({ session, input }) => {
|
|
375
|
+
if (session?.user) return true;
|
|
376
|
+
return input?.stage === "published";
|
|
377
|
+
},
|
|
378
|
+
create: ({ session }) => !!session?.user,
|
|
379
|
+
update: ({ session }) => !!session?.user,
|
|
380
|
+
delete: ({ session }) => !!session?.user,
|
|
381
|
+
transition: ({ session }) => !!session?.user,
|
|
382
|
+
})
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Public frontend code must pass `stage: "published"`. Preview/draft-mode reads may omit `stage` only when the request has an authorized editor session.
|
|
386
|
+
|
|
325
387
|
### System Access and Preview
|
|
326
388
|
|
|
327
389
|
Do not use `accessMode: "system"` to serve preview data. Preview requests should go through normal session-based access, with the preview token resolving to the editor's session. This ensures previewed content respects the same visibility rules as the final published page.
|
|
@@ -213,9 +213,9 @@ The admin renders drag-and-drop upload, image preview, file info, and remove but
|
|
|
213
213
|
|
|
214
214
|
## Live Preview
|
|
215
215
|
|
|
216
|
-
Live Preview
|
|
216
|
+
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
217
|
|
|
218
|
-
|
|
218
|
+
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
219
|
|
|
220
220
|
### Server Config
|
|
221
221
|
|
|
@@ -239,35 +239,57 @@ export const pages = collection("pages")
|
|
|
239
239
|
});
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
Preview opens the existing split-screen editor. Patches and refresh/resync messages update the iframe mirror; save/autosave still writes through the form.
|
|
243
243
|
|
|
244
|
-
### Frontend
|
|
244
|
+
### Prepare a Frontend Page
|
|
245
245
|
|
|
246
|
-
Use `useCollectionPreview
|
|
246
|
+
Use exported APIs only: `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and `BlockRenderer`.
|
|
247
|
+
|
|
248
|
+
Checklist:
|
|
249
|
+
|
|
250
|
+
1. Configure `.preview({ url })` on the collection.
|
|
251
|
+
2. Load the same record shape the page normally renders.
|
|
252
|
+
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.
|
|
253
|
+
4. Call `useCollectionPreview({ initialData, onRefresh })` in the page renderer.
|
|
254
|
+
5. Wrap the visual output in `PreviewProvider`.
|
|
255
|
+
6. Render from `preview.data`, not the original loader object.
|
|
256
|
+
7. Wrap editable scalar text with `PreviewField field="..." editable="text" | "textarea"`.
|
|
257
|
+
8. Render block content with `BlockRenderer`; pass `selectedBlockId` and `onBlockClick`.
|
|
258
|
+
9. Keep add/remove/reorder/nesting block operations in the existing block editor.
|
|
247
259
|
|
|
248
260
|
```tsx
|
|
249
261
|
import {
|
|
262
|
+
BlockRenderer,
|
|
250
263
|
PreviewField,
|
|
251
264
|
PreviewProvider,
|
|
252
265
|
useCollectionPreview,
|
|
253
266
|
} from "@questpie/admin/client";
|
|
267
|
+
import admin from "@/questpie/admin/.generated/client";
|
|
254
268
|
|
|
255
|
-
function PagePreview({
|
|
269
|
+
function PagePreview({ page }) {
|
|
256
270
|
const router = useRouter();
|
|
257
271
|
const preview = useCollectionPreview({
|
|
258
|
-
initialData,
|
|
272
|
+
initialData: page,
|
|
259
273
|
onRefresh: () => router.invalidate(),
|
|
260
274
|
});
|
|
261
275
|
|
|
262
276
|
return (
|
|
263
|
-
<PreviewProvider
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
277
|
+
<PreviewProvider preview={preview}>
|
|
278
|
+
<main className={preview.isPreviewMode ? "questpie-preview" : undefined}>
|
|
279
|
+
<PreviewField field="title" editable="text" as="h1">
|
|
280
|
+
{preview.data.title}
|
|
281
|
+
</PreviewField>
|
|
282
|
+
|
|
283
|
+
<BlockRenderer
|
|
284
|
+
content={preview.data.content}
|
|
285
|
+
data={preview.data.content?._data}
|
|
286
|
+
renderers={admin.blocks}
|
|
287
|
+
selectedBlockId={preview.selectedBlockId}
|
|
288
|
+
onBlockClick={
|
|
289
|
+
preview.isPreviewMode ? preview.handleBlockClick : undefined
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
</main>
|
|
271
293
|
</PreviewProvider>
|
|
272
294
|
);
|
|
273
295
|
}
|
|
@@ -275,11 +297,15 @@ function PagePreview({ initialData }) {
|
|
|
275
297
|
|
|
276
298
|
### Key Principles
|
|
277
299
|
|
|
278
|
-
-
|
|
279
|
-
-
|
|
300
|
+
- Keep `FormView`, the Preview button, and `LivePreviewMode`
|
|
301
|
+
- Never add a separate visual-edit form API, a second default form view, or parallel preview API names
|
|
302
|
+
- Preserve save/autosave/Cmd+S/history/workflow/locks/actions
|
|
303
|
+
- `useCollectionPreview` handles preview mode, mirrored data, refresh/resync, and focus state
|
|
280
304
|
- `PreviewProvider` supplies preview context to `PreviewField`
|
|
281
|
-
-
|
|
282
|
-
-
|
|
305
|
+
- `PreviewField` annotates scalar fields and can opt into inline editing with `editable`
|
|
306
|
+
- `BlockRenderer` preserves block IDs and block scopes for block annotations
|
|
307
|
+
- Use `BlockScopeProvider` only for custom/manual block rendering outside `BlockRenderer`
|
|
308
|
+
- Validate all iframe messages before updating form state
|
|
283
309
|
|
|
284
310
|
## History & Versions
|
|
285
311
|
|
|
@@ -560,7 +586,7 @@ Section-level visibility:
|
|
|
560
586
|
{
|
|
561
587
|
type: "section",
|
|
562
588
|
label: { en: "SEO" },
|
|
563
|
-
hidden: ({ data }) => !data.
|
|
589
|
+
hidden: ({ data }) => !data.showSeo,
|
|
564
590
|
fields: [f.metaTitle, f.metaDescription],
|
|
565
591
|
}
|
|
566
592
|
```
|
|
@@ -803,7 +829,7 @@ export const logs = collection("logs")
|
|
|
803
829
|
|
|
804
830
|
## Form Views and Live Preview
|
|
805
831
|
|
|
806
|
-
Form views connect to the Live Preview
|
|
832
|
+
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
833
|
|
|
808
834
|
### Enabling Preview on a Collection
|
|
809
835
|
|
|
@@ -827,9 +853,25 @@ export const pages = collection("pages")
|
|
|
827
853
|
### How It Works
|
|
828
854
|
|
|
829
855
|
1. The form view detects `.preview()` config and opens a split-screen layout
|
|
830
|
-
2.
|
|
831
|
-
3.
|
|
832
|
-
4.
|
|
856
|
+
2. The preview iframe mirrors form state with snapshot, patch, refresh, commit, and resync messages
|
|
857
|
+
3. Save/autosave/Cmd+S/history/workflow/locks/actions stay in the form lifecycle
|
|
858
|
+
4. The preview page uses `useCollectionPreview({ initialData, onRefresh })`
|
|
859
|
+
5. `PreviewProvider`, `PreviewField`, and `BlockRenderer` wire field and block annotations
|
|
860
|
+
|
|
861
|
+
### Frontend Preparation Checklist
|
|
862
|
+
|
|
863
|
+
Use this checklist before expecting visual editing to work:
|
|
864
|
+
|
|
865
|
+
1. The collection has `.preview({ url })`.
|
|
866
|
+
2. The page loader returns the complete rendered record shape.
|
|
867
|
+
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.
|
|
868
|
+
4. The page renderer calls `useCollectionPreview({ initialData, onRefresh })`.
|
|
869
|
+
5. The rendered page is wrapped in `PreviewProvider preview={preview}`.
|
|
870
|
+
6. UI reads from `preview.data`, not directly from loader data.
|
|
871
|
+
7. Scalar text uses `PreviewField field="..." editable="text" | "textarea"` when inline editing is expected.
|
|
872
|
+
8. Blocks render through `BlockRenderer` with `selectedBlockId` and `onBlockClick`.
|
|
873
|
+
9. `BlockScopeProvider` is only needed for custom/manual block rendering outside `BlockRenderer`.
|
|
874
|
+
10. Add/remove/reorder/nesting block operations stay in the existing block editor.
|
|
833
875
|
|
|
834
876
|
---
|
|
835
877
|
|
|
@@ -1111,11 +1153,35 @@ function PageRenderer({ page }) {
|
|
|
1111
1153
|
|
|
1112
1154
|
## Blocks in Live Preview
|
|
1113
1155
|
|
|
1114
|
-
When a collection has `.preview()` configured, blocks
|
|
1156
|
+
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.
|
|
1157
|
+
|
|
1158
|
+
### Preferred: BlockRenderer
|
|
1159
|
+
|
|
1160
|
+
Use `BlockRenderer` for normal frontend page rendering. It preserves `data-block-id`, routes block selection, and scopes nested `PreviewField` paths automatically.
|
|
1161
|
+
|
|
1162
|
+
```tsx
|
|
1163
|
+
<BlockRenderer
|
|
1164
|
+
content={preview.data.content}
|
|
1165
|
+
data={preview.data.content?._data}
|
|
1166
|
+
renderers={admin.blocks}
|
|
1167
|
+
selectedBlockId={preview.selectedBlockId}
|
|
1168
|
+
onBlockClick={preview.isPreviewMode ? preview.handleBlockClick : undefined}
|
|
1169
|
+
/>
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
Inside custom block renderers, annotate scalar values with `PreviewField`:
|
|
1173
|
+
|
|
1174
|
+
```tsx
|
|
1175
|
+
<PreviewField field="title" editable="text" as="h2">
|
|
1176
|
+
{values.title}
|
|
1177
|
+
</PreviewField>
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
This resolves to `content._values.{blockId}.title` when rendered inside `BlockRenderer`.
|
|
1115
1181
|
|
|
1116
|
-
### BlockScopeProvider Wrapper
|
|
1182
|
+
### Manual BlockScopeProvider Wrapper
|
|
1117
1183
|
|
|
1118
|
-
Use `BlockScopeProvider`
|
|
1184
|
+
Use `BlockScopeProvider` only when you manually render blocks outside `BlockRenderer`:
|
|
1119
1185
|
|
|
1120
1186
|
```tsx
|
|
1121
1187
|
import { BlockScopeProvider } from "@questpie/admin/client";
|
|
@@ -1134,7 +1200,7 @@ function PageRenderer({ blocks, previewData }) {
|
|
|
1134
1200
|
|
|
1135
1201
|
`PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
|
|
1136
1202
|
|
|
1137
|
-
|
|
1203
|
+
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
1204
|
|
|
1139
1205
|
---
|
|
1140
1206
|
|
|
@@ -13,11 +13,11 @@ The QUESTPIE admin panel is a **projection of your server schema** — not the f
|
|
|
13
13
|
|
|
14
14
|
## Reference Topics
|
|
15
15
|
|
|
16
|
-
| Topic
|
|
17
|
-
|
|
18
|
-
| Views
|
|
19
|
-
| Blocks
|
|
20
|
-
| Custom UI | `references/custom-ui.md` | Custom fields, custom views, registries, reactive fields, widgets
|
|
16
|
+
| Topic | File | Covers |
|
|
17
|
+
| --------- | ------------------------- | -------------------------------------------------------------------------------------- |
|
|
18
|
+
| Views | `references/views.md` | List views, form views, dashboard, sidebar, filters, bulk actions, visibility, history |
|
|
19
|
+
| Blocks | `references/blocks.md` | Block definitions, fields, prefetch, renderers, block picker |
|
|
20
|
+
| Custom UI | `references/custom-ui.md` | Custom fields, custom views, registries, reactive fields, widgets |
|
|
21
21
|
|
|
22
22
|
## Full Compiled Document
|
|
23
23
|
|
|
@@ -222,9 +222,9 @@ The admin renders drag-and-drop upload, image preview, file info, and remove but
|
|
|
222
222
|
|
|
223
223
|
## Live Preview
|
|
224
224
|
|
|
225
|
-
Live Preview
|
|
225
|
+
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.
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
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.
|
|
228
228
|
|
|
229
229
|
### Server Config
|
|
230
230
|
|
|
@@ -248,35 +248,57 @@ export const pages = collection("pages")
|
|
|
248
248
|
});
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
Preview opens the existing split-screen editor. Patches and refresh/resync messages update the iframe mirror; save/autosave still writes through the form.
|
|
252
252
|
|
|
253
|
-
### Frontend
|
|
253
|
+
### Prepare a Frontend Page
|
|
254
254
|
|
|
255
|
-
Use `useCollectionPreview
|
|
255
|
+
Use exported APIs only: `useCollectionPreview`, `PreviewProvider`, `PreviewField`, and `BlockRenderer`.
|
|
256
|
+
|
|
257
|
+
Checklist:
|
|
258
|
+
|
|
259
|
+
1. Configure `.preview({ url })` on the collection.
|
|
260
|
+
2. Load the same record shape the page normally renders.
|
|
261
|
+
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.
|
|
262
|
+
4. Call `useCollectionPreview({ initialData, onRefresh })` in the page renderer.
|
|
263
|
+
5. Wrap the visual output in `PreviewProvider`.
|
|
264
|
+
6. Render from `preview.data`, not the original loader object.
|
|
265
|
+
7. Wrap editable scalar text with `PreviewField field="..." editable="text" | "textarea"`.
|
|
266
|
+
8. Render block content with `BlockRenderer`; pass `selectedBlockId` and `onBlockClick`.
|
|
267
|
+
9. Keep add/remove/reorder/nesting block operations in the existing block editor.
|
|
256
268
|
|
|
257
269
|
```tsx
|
|
258
270
|
import {
|
|
271
|
+
BlockRenderer,
|
|
259
272
|
PreviewField,
|
|
260
273
|
PreviewProvider,
|
|
261
274
|
useCollectionPreview,
|
|
262
275
|
} from "@questpie/admin/client";
|
|
276
|
+
import admin from "@/questpie/admin/.generated/client";
|
|
263
277
|
|
|
264
|
-
function PagePreview({
|
|
278
|
+
function PagePreview({ page }) {
|
|
265
279
|
const router = useRouter();
|
|
266
280
|
const preview = useCollectionPreview({
|
|
267
|
-
initialData,
|
|
281
|
+
initialData: page,
|
|
268
282
|
onRefresh: () => router.invalidate(),
|
|
269
283
|
});
|
|
270
284
|
|
|
271
285
|
return (
|
|
272
|
-
<PreviewProvider
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
<PreviewProvider preview={preview}>
|
|
287
|
+
<main className={preview.isPreviewMode ? "questpie-preview" : undefined}>
|
|
288
|
+
<PreviewField field="title" editable="text" as="h1">
|
|
289
|
+
{preview.data.title}
|
|
290
|
+
</PreviewField>
|
|
291
|
+
|
|
292
|
+
<BlockRenderer
|
|
293
|
+
content={preview.data.content}
|
|
294
|
+
data={preview.data.content?._data}
|
|
295
|
+
renderers={admin.blocks}
|
|
296
|
+
selectedBlockId={preview.selectedBlockId}
|
|
297
|
+
onBlockClick={
|
|
298
|
+
preview.isPreviewMode ? preview.handleBlockClick : undefined
|
|
299
|
+
}
|
|
300
|
+
/>
|
|
301
|
+
</main>
|
|
280
302
|
</PreviewProvider>
|
|
281
303
|
);
|
|
282
304
|
}
|
|
@@ -284,11 +306,15 @@ function PagePreview({ initialData }) {
|
|
|
284
306
|
|
|
285
307
|
### Key Principles
|
|
286
308
|
|
|
287
|
-
-
|
|
288
|
-
-
|
|
309
|
+
- Keep `FormView`, the Preview button, and `LivePreviewMode`
|
|
310
|
+
- Never add a separate visual-edit form API, a second default form view, or parallel preview API names
|
|
311
|
+
- Preserve save/autosave/Cmd+S/history/workflow/locks/actions
|
|
312
|
+
- `useCollectionPreview` handles preview mode, mirrored data, refresh/resync, and focus state
|
|
289
313
|
- `PreviewProvider` supplies preview context to `PreviewField`
|
|
290
|
-
-
|
|
291
|
-
-
|
|
314
|
+
- `PreviewField` annotates scalar fields and can opt into inline editing with `editable`
|
|
315
|
+
- `BlockRenderer` preserves block IDs and block scopes for block annotations
|
|
316
|
+
- Use `BlockScopeProvider` only for custom/manual block rendering outside `BlockRenderer`
|
|
317
|
+
- Validate all iframe messages before updating form state
|
|
292
318
|
|
|
293
319
|
## History & Versions
|
|
294
320
|
|
|
@@ -281,11 +281,35 @@ function PageRenderer({ page }) {
|
|
|
281
281
|
|
|
282
282
|
## Blocks in Live Preview
|
|
283
283
|
|
|
284
|
-
When a collection has `.preview()` configured, blocks
|
|
284
|
+
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.
|
|
285
285
|
|
|
286
|
-
###
|
|
286
|
+
### Preferred: BlockRenderer
|
|
287
287
|
|
|
288
|
-
Use `
|
|
288
|
+
Use `BlockRenderer` for normal frontend page rendering. It preserves `data-block-id`, routes block selection, and scopes nested `PreviewField` paths automatically.
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<BlockRenderer
|
|
292
|
+
content={preview.data.content}
|
|
293
|
+
data={preview.data.content?._data}
|
|
294
|
+
renderers={admin.blocks}
|
|
295
|
+
selectedBlockId={preview.selectedBlockId}
|
|
296
|
+
onBlockClick={preview.isPreviewMode ? preview.handleBlockClick : undefined}
|
|
297
|
+
/>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Inside custom block renderers, annotate scalar values with `PreviewField`:
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
<PreviewField field="title" editable="text" as="h2">
|
|
304
|
+
{values.title}
|
|
305
|
+
</PreviewField>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This resolves to `content._values.{blockId}.title` when rendered inside `BlockRenderer`.
|
|
309
|
+
|
|
310
|
+
### Manual BlockScopeProvider Wrapper
|
|
311
|
+
|
|
312
|
+
Use `BlockScopeProvider` only when you manually render blocks outside `BlockRenderer`:
|
|
289
313
|
|
|
290
314
|
```tsx
|
|
291
315
|
import { BlockScopeProvider } from "@questpie/admin/client";
|
|
@@ -304,4 +328,4 @@ function PageRenderer({ blocks, previewData }) {
|
|
|
304
328
|
|
|
305
329
|
`PreviewField` components inside the provider resolve paths like `content._values.{blockId}.title`.
|
|
306
330
|
|
|
307
|
-
|
|
331
|
+
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.
|
|
@@ -161,7 +161,7 @@ Section-level visibility:
|
|
|
161
161
|
{
|
|
162
162
|
type: "section",
|
|
163
163
|
label: { en: "SEO" },
|
|
164
|
-
hidden: ({ data }) => !data.
|
|
164
|
+
hidden: ({ data }) => !data.showSeo,
|
|
165
165
|
fields: [f.metaTitle, f.metaDescription],
|
|
166
166
|
}
|
|
167
167
|
```
|
|
@@ -404,7 +404,7 @@ export const logs = collection("logs")
|
|
|
404
404
|
|
|
405
405
|
## Form Views and Live Preview
|
|
406
406
|
|
|
407
|
-
Form views connect to the Live Preview
|
|
407
|
+
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.
|
|
408
408
|
|
|
409
409
|
### Enabling Preview on a Collection
|
|
410
410
|
|
|
@@ -428,6 +428,22 @@ export const pages = collection("pages")
|
|
|
428
428
|
### How It Works
|
|
429
429
|
|
|
430
430
|
1. The form view detects `.preview()` config and opens a split-screen layout
|
|
431
|
-
2.
|
|
432
|
-
3.
|
|
433
|
-
4.
|
|
431
|
+
2. The preview iframe mirrors form state with snapshot, patch, refresh, commit, and resync messages
|
|
432
|
+
3. Save/autosave/Cmd+S/history/workflow/locks/actions stay in the form lifecycle
|
|
433
|
+
4. The preview page uses `useCollectionPreview({ initialData, onRefresh })`
|
|
434
|
+
5. `PreviewProvider`, `PreviewField`, and `BlockRenderer` wire field and block annotations
|
|
435
|
+
|
|
436
|
+
### Frontend Preparation Checklist
|
|
437
|
+
|
|
438
|
+
Use this checklist before expecting visual editing to work:
|
|
439
|
+
|
|
440
|
+
1. The collection has `.preview({ url })`.
|
|
441
|
+
2. The page loader returns the complete rendered record shape.
|
|
442
|
+
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.
|
|
443
|
+
4. The page renderer calls `useCollectionPreview({ initialData, onRefresh })`.
|
|
444
|
+
5. The rendered page is wrapped in `PreviewProvider preview={preview}`.
|
|
445
|
+
6. UI reads from `preview.data`, not directly from loader data.
|
|
446
|
+
7. Scalar text uses `PreviewField field="..." editable="text" | "textarea"` when inline editing is expected.
|
|
447
|
+
8. Blocks render through `BlockRenderer` with `selectedBlockId` and `onBlockClick`.
|
|
448
|
+
9. `BlockScopeProvider` is only needed for custom/manual block rendering outside `BlockRenderer`.
|
|
449
|
+
10. Add/remove/reorder/nesting block operations stay in the existing block editor.
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { QueryClient } from "@tanstack/react-query";
|
|
2
2
|
|
|
3
|
+
const ONE_MINUTE = 60 * 1000;
|
|
4
|
+
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
|
5
|
+
|
|
3
6
|
export const queryClient = new QueryClient({
|
|
4
7
|
defaultOptions: {
|
|
5
8
|
queries: {
|
|
6
|
-
staleTime:
|
|
9
|
+
staleTime: ONE_MINUTE,
|
|
10
|
+
gcTime: FIVE_MINUTES,
|
|
11
|
+
refetchOnWindowFocus: false,
|
|
12
|
+
retry: 1,
|
|
13
|
+
},
|
|
14
|
+
mutations: {
|
|
15
|
+
retry: 0,
|
|
7
16
|
},
|
|
8
17
|
},
|
|
9
18
|
});
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
2
|
+
import { useMemo } from "react";
|
|
2
3
|
|
|
3
4
|
import { AdminRouter } from "@questpie/admin/client";
|
|
4
5
|
|
|
6
|
+
function createAdminNavigate(navigate: ReturnType<typeof useNavigate>) {
|
|
7
|
+
return (path: string) => {
|
|
8
|
+
void navigate({ to: path });
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
function AdminCatchAll() {
|
|
6
13
|
const navigate = useNavigate();
|
|
7
14
|
const params = Route.useParams();
|
|
8
15
|
const splat = params._splat as string;
|
|
16
|
+
const handleNavigate = useMemo(
|
|
17
|
+
() => createAdminNavigate(navigate),
|
|
18
|
+
[navigate],
|
|
19
|
+
);
|
|
9
20
|
const segments = splat ? splat.split("/").filter(Boolean) : [];
|
|
10
21
|
|
|
11
22
|
return (
|
|
12
23
|
<AdminRouter
|
|
13
24
|
segments={segments}
|
|
14
|
-
navigate={
|
|
25
|
+
navigate={handleNavigate}
|
|
15
26
|
basePath="/admin"
|
|
16
27
|
/>
|
|
17
28
|
);
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
2
|
+
import { useMemo } from "react";
|
|
2
3
|
|
|
3
4
|
import { AdminRouter } from "@questpie/admin/client";
|
|
4
5
|
|
|
6
|
+
function createAdminNavigate(navigate: ReturnType<typeof useNavigate>) {
|
|
7
|
+
return (path: string) => {
|
|
8
|
+
void navigate({ to: path });
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
function AdminDashboard() {
|
|
6
13
|
const navigate = useNavigate();
|
|
14
|
+
const handleNavigate = useMemo(
|
|
15
|
+
() => createAdminNavigate(navigate),
|
|
16
|
+
[navigate],
|
|
17
|
+
);
|
|
7
18
|
|
|
8
19
|
return (
|
|
9
|
-
<AdminRouter
|
|
10
|
-
segments={[]}
|
|
11
|
-
navigate={(path) => navigate({ to: path })}
|
|
12
|
-
basePath="/admin"
|
|
13
|
-
/>
|
|
20
|
+
<AdminRouter segments={[]} navigate={handleNavigate} basePath="/admin" />
|
|
14
21
|
);
|
|
15
22
|
}
|
|
16
23
|
|