@sanity/sdk-react 2.3.1 → 2.5.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/README.md +575 -4
- package/dist/index.d.ts +260 -4
- package/dist/index.js +227 -47
- package/dist/index.js.map +1 -1
- package/package.json +22 -20
- package/src/_exports/sdk-react.ts +12 -0
- package/src/components/auth/LoginError.tsx +27 -7
- package/src/context/ComlinkTokenRefresh.test.tsx +107 -23
- package/src/hooks/agent/agentActions.test.tsx +78 -0
- package/src/hooks/agent/agentActions.ts +136 -0
- package/src/hooks/agent/useAgentResourceContext.test.tsx +245 -0
- package/src/hooks/agent/useAgentResourceContext.ts +106 -0
- package/src/hooks/client/useClient.test.tsx +42 -0
- package/src/hooks/client/useClient.ts +11 -4
- package/src/hooks/dashboard/types.ts +12 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +239 -0
- package/src/hooks/dashboard/useDispatchIntent.ts +158 -0
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.test.ts +155 -0
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +53 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +124 -9
- package/src/hooks/document/useApplyDocumentActions.ts +75 -4
- package/src/hooks/document/useDocumentPermissions.test.tsx +3 -3
- package/src/hooks/document/useDocumentPermissions.ts +9 -6
- package/src/hooks/projection/useDocumentProjection.ts +14 -3
- package/src/hooks/releases/usePerspective.test.tsx +1 -0
- package/src/hooks/releases/usePerspective.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,584 @@
|
|
|
5
5
|
<h1 align="center">Sanity App SDK (React)</h1>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
React hooks for creating Sanity applications.
|
|
8
|
+
React hooks for creating Sanity applications. Live by default, optimistic updates, multi-project support.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
### 1. Setup (2 min)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx sanity@latest init --template app-quickstart
|
|
18
|
+
cd your-app
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Opens at `https://www.sanity.io/welcome?dev=http%3A%2F%2Flocalhost%3A3333`, proxied through Sanity Dashboard for auth.
|
|
23
|
+
|
|
24
|
+
**Key files:**
|
|
25
|
+
|
|
26
|
+
- `sanity.cli.ts` — configuration options used by the CLI — application metadata, deployment config, etc
|
|
27
|
+
- `src/App.tsx` — Root with `<SanityApp>` provider and project configuration(s)
|
|
28
|
+
- `src/ExampleComponent.tsx` — Your starting point
|
|
29
|
+
|
|
30
|
+
### 2. Project configuration
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import {SanityApp, type SanityConfig} from '@sanity/sdk-react'
|
|
34
|
+
|
|
35
|
+
const config: SanityConfig[] = [
|
|
36
|
+
{projectId: 'abc123', dataset: 'production'},
|
|
37
|
+
{projectId: 'def456', dataset: 'production'}, // multi-project support
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
export function App() {
|
|
41
|
+
return (
|
|
42
|
+
<SanityApp config={config} fallback={<div>Loading...</div>}>
|
|
43
|
+
<YourApp />
|
|
44
|
+
</SanityApp>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Auth is automatic** — Dashboard injects an auth token via iframe. No custom login flow is needed for your application.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Guide
|
|
54
|
+
|
|
55
|
+
### Document Handles
|
|
56
|
+
|
|
57
|
+
Document handles are a core concept for apps built with the App SDK. Document handles are minimal pointers to documents. They consist of the following properties:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
type DocumentHandle = {
|
|
61
|
+
documentId: string
|
|
62
|
+
documentType: string
|
|
63
|
+
projectId?: string // optional if using the default projectId or inside a ResourceProvider
|
|
64
|
+
dataset?: string // optional if using the default dataset or inside a ResourceProvider
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Best practice:** Fetch document handles first → pass them to child components → fetch individual document content from child components.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Hook Reference
|
|
73
|
+
|
|
74
|
+
#### Data Retrieval
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// Get a collection of document handles (structured for infinite scrolling)
|
|
78
|
+
const {data, hasMore, loadMore, isPending, count} = useDocuments({
|
|
79
|
+
documentType: 'article',
|
|
80
|
+
batchSize: 20,
|
|
81
|
+
orderings: [{field: '_updatedAt', direction: 'desc'}],
|
|
82
|
+
filter: 'status == $status', // GROQ filter
|
|
83
|
+
params: {status: 'published'}, // Parameters used for the GROQ filter
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Get a collection of document handles (structured for paginated lists)
|
|
87
|
+
const {data, currentPage, totalPages, nextPage, previousPage} = usePaginatedDocuments({
|
|
88
|
+
documentType: 'article',
|
|
89
|
+
pageSize: 10,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Get content from a single document (live content, optimistic updates when used with useEditDocument)
|
|
93
|
+
const {data: doc} = useDocument(handle)
|
|
94
|
+
const {data: title} = useDocument({...handle, path: 'title'})
|
|
95
|
+
|
|
96
|
+
// Get a projection for an individual document (live content, no optimistic updates)
|
|
97
|
+
const {data} = useDocumentProjection({
|
|
98
|
+
...handle,
|
|
99
|
+
projection: `{ title, "author": author->name, "imageUrl": image.asset->url }`,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Use GROQ directly
|
|
103
|
+
const {data} = useQuery({
|
|
104
|
+
query: `*[_type == "article" && featured == true][0...5]{ title, slug }`,
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Document Manipulation
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// Edit field (emits optimistic updates to useEditDocument listeners, creates a draft automatically)
|
|
112
|
+
const editTitle = useEditDocument({...handle, path: 'title'})
|
|
113
|
+
editTitle('New Title') // fires on every keystroke, debounced internally
|
|
114
|
+
|
|
115
|
+
// Edit a nested path in a document
|
|
116
|
+
const editAuthorName = useEditDocument({...handle, path: 'author.name'})
|
|
117
|
+
|
|
118
|
+
// Document actions
|
|
119
|
+
import {
|
|
120
|
+
useApplyDocumentActions,
|
|
121
|
+
createDocumentHandle,
|
|
122
|
+
publishDocument,
|
|
123
|
+
unpublishDocument,
|
|
124
|
+
deleteDocument,
|
|
125
|
+
createDocument,
|
|
126
|
+
discardDraft,
|
|
127
|
+
} from '@sanity/sdk-react'
|
|
128
|
+
|
|
129
|
+
const apply = useApplyDocumentActions()
|
|
130
|
+
|
|
131
|
+
// Single action
|
|
132
|
+
await apply(publishDocument(handle))
|
|
133
|
+
|
|
134
|
+
// Batch actions
|
|
135
|
+
await apply([publishDocument(handle1), publishDocument(handle2), deleteDocument(handle3)])
|
|
136
|
+
|
|
137
|
+
// Create new document with an optional initial content
|
|
138
|
+
const newHandle = createDocumentHandle({
|
|
139
|
+
documentId: crypto.randomUUID(),
|
|
140
|
+
documentType: 'article',
|
|
141
|
+
})
|
|
142
|
+
await apply(createDocument(newHandle, {title: 'Untitled', status: 'draft'}))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### Events & Permissions
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// Subscribe to document events
|
|
149
|
+
useDocumentEvent({
|
|
150
|
+
...handle,
|
|
151
|
+
onEvent: (event) => {
|
|
152
|
+
// event.type: 'documentEdited' | 'documentPublished' | 'documentDeleted' | ...
|
|
153
|
+
console.log(event.type, event.documentId)
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Check permissions
|
|
158
|
+
const {data: canEdit} = useDocumentPermissions({
|
|
159
|
+
...handle,
|
|
160
|
+
permission: 'update',
|
|
161
|
+
})
|
|
162
|
+
const {data: canPublish} = useDocumentPermissions({
|
|
163
|
+
...handle,
|
|
164
|
+
permission: 'publish',
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Document Actions
|
|
171
|
+
|
|
172
|
+
The `useApplyDocumentActions` hook is used to perform document lifecycle operations. Actions are created using helper functions and applied through the `apply` function.
|
|
173
|
+
|
|
174
|
+
#### Available Action Creators
|
|
175
|
+
|
|
176
|
+
| Function | Description |
|
|
177
|
+
| ------------------- | ---------------------------------------------- |
|
|
178
|
+
| `createDocument` | Create a new document |
|
|
179
|
+
| `publishDocument` | Publish a draft (copy draft → published) |
|
|
180
|
+
| `unpublishDocument` | Unpublish (delete published, keep draft) |
|
|
181
|
+
| `deleteDocument` | Delete document entirely (draft and published) |
|
|
182
|
+
| `discardDraft` | Discard draft changes, revert to published |
|
|
183
|
+
|
|
184
|
+
#### Creating Documents
|
|
185
|
+
|
|
186
|
+
To create a document, you must:
|
|
187
|
+
|
|
188
|
+
1. Generate your own document ID (using `crypto.randomUUID()`)
|
|
189
|
+
2. Create a document handle with `createDocumentHandle`
|
|
190
|
+
3. Apply the `createDocument` action using the document handle, along with optional initial content
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import {useApplyDocumentActions, createDocumentHandle, createDocument} from '@sanity/sdk-react'
|
|
194
|
+
|
|
195
|
+
function CreateArticleButton() {
|
|
196
|
+
const apply = useApplyDocumentActions()
|
|
197
|
+
|
|
198
|
+
const handleCreateArticle = () => {
|
|
199
|
+
const newId = crypto.randomUUID()
|
|
200
|
+
const handle = createDocumentHandle({
|
|
201
|
+
documentId: newId,
|
|
202
|
+
documentType: 'article',
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
apply(
|
|
206
|
+
createDocument(handle, {
|
|
207
|
+
title: 'New Article',
|
|
208
|
+
status: 'draft',
|
|
209
|
+
author: {_type: 'reference', _ref: 'author-123'},
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
// Navigate to the new document
|
|
214
|
+
navigate(`/articles/${newId}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return <button onClick={handleCreateArticle}>Create Article</button>
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Publishing Documents
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import {useApplyDocumentActions, publishDocument, useDocument} from '@sanity/sdk-react'
|
|
225
|
+
|
|
226
|
+
function PublishButton({handle}: {handle: DocumentHandle}) {
|
|
227
|
+
const apply = useApplyDocumentActions()
|
|
228
|
+
const {data: doc} = useDocument(handle)
|
|
229
|
+
|
|
230
|
+
// Check if document has unpublished changes (is a draft)
|
|
231
|
+
const isDraft = doc?._id?.startsWith('drafts.')
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<button disabled={!isDraft} onClick={() => apply(publishDocument(handle))}>
|
|
235
|
+
Publish
|
|
236
|
+
</button>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Deleting Documents
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import {useApplyDocumentActions, deleteDocument} from '@sanity/sdk-react'
|
|
245
|
+
|
|
246
|
+
function DeleteButton({handle}: {handle: DocumentHandle}) {
|
|
247
|
+
const apply = useApplyDocumentActions()
|
|
248
|
+
|
|
249
|
+
const handleDelete = () => {
|
|
250
|
+
if (confirm('Are you sure?')) {
|
|
251
|
+
apply(deleteDocument(handle))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return <button onClick={handleDelete}>Delete</button>
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Batch Operations
|
|
260
|
+
|
|
261
|
+
Apply multiple actions as a single transaction:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
const apply = useApplyDocumentActions()
|
|
265
|
+
|
|
266
|
+
// Create and immediately publish
|
|
267
|
+
const newHandle = createDocumentHandle({
|
|
268
|
+
documentId: crypto.randomUUID(),
|
|
269
|
+
documentType: 'article',
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
apply([createDocument(newHandle, {title: 'Breaking News'}), publishDocument(newHandle)])
|
|
273
|
+
|
|
274
|
+
// Publish multiple documents at once
|
|
275
|
+
apply([publishDocument(handle1), publishDocument(handle2), publishDocument(handle3)])
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Suspense Pattern
|
|
281
|
+
|
|
282
|
+
All hooks that get or write data use React Suspense. Wrap all your components that fetch data with a Suspense boundary to avoid unnecessary re-renders:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
function App() {
|
|
286
|
+
return (
|
|
287
|
+
<Suspense fallback={<Skeleton />}>
|
|
288
|
+
<ArticleList />
|
|
289
|
+
</Suspense>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ArticleList() {
|
|
294
|
+
const {data: articles} = useDocuments({documentType: 'article'})
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<ul>
|
|
298
|
+
{articles.map((handle) => (
|
|
299
|
+
{/* Wrap each list item in its own Suspense boundary to prevent full list re-renders when one item updates */}
|
|
300
|
+
<Suspense key={handle.documentId} fallback={<li>Loading...</li>}>
|
|
301
|
+
<ArticleItem handle={handle} />
|
|
302
|
+
</Suspense>
|
|
303
|
+
))}
|
|
304
|
+
</ul>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### Draft/Published Model
|
|
312
|
+
|
|
313
|
+
Sanity has two document states:
|
|
314
|
+
|
|
315
|
+
- **Published:** `_id: "abc123"` — live, public
|
|
316
|
+
- **Draft:** `_id: "drafts.abc123"` — working copy
|
|
317
|
+
|
|
318
|
+
The SDK handles updating the document state automatically:
|
|
319
|
+
|
|
320
|
+
- `useDocument()` returns draft if exists, else published
|
|
321
|
+
- `useEditDocument()` creates draft on first edit (automatic)
|
|
322
|
+
- `publishDocument()` copies draft → published, deletes draft
|
|
323
|
+
- `discardDraft()` deletes draft, reverts to published
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Real-Time Behavior
|
|
328
|
+
|
|
329
|
+
#### Live by Default
|
|
330
|
+
|
|
331
|
+
- Document changes from other users appear instantly
|
|
332
|
+
- No polling, uses Sanity's listener API
|
|
333
|
+
- Optimistic updates for local edits appear before the server confirms the updates
|
|
334
|
+
|
|
335
|
+
#### Re-render Triggers
|
|
336
|
+
|
|
337
|
+
Any mutation to a subscribed document (even fields you don't display) will trigger a re-render. Use `useDocumentProjection()` for read-only displays to minimize re-renders.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### Multi-Project Access
|
|
342
|
+
|
|
343
|
+
#### Specify Source in Handle
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
const handle: DocumentHandle = {
|
|
347
|
+
documentId: 'xyz',
|
|
348
|
+
documentType: 'product',
|
|
349
|
+
projectId: 'project-a',
|
|
350
|
+
dataset: 'production',
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Use Resource Provider Context
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// App.tsx
|
|
358
|
+
import {ResourceProvider} from '@sanity/sdk-react'
|
|
359
|
+
|
|
360
|
+
import {ProductCard} from './ProductCard'
|
|
361
|
+
|
|
362
|
+
export function WrappedProductCard() {
|
|
363
|
+
return (
|
|
364
|
+
<ResourceProvider projectId="project-a" dataset="production">
|
|
365
|
+
<ProductCard productId="xyz" />
|
|
366
|
+
</ResourceProvider>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ProductCard.tsx
|
|
371
|
+
import {useProjectId, useDataset} from '@sanity/sdk-react'
|
|
372
|
+
|
|
373
|
+
function ProductCard({productId}: {productId: string}) {
|
|
374
|
+
const projectId = useProjectId() // "project-a" from nearest configured ResourceProvider
|
|
375
|
+
const dataset = useDataset() // "production" from nearest configured ResourceProvider
|
|
376
|
+
// ...
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
### TypeScript & TypeGen
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Generate types from your schema
|
|
386
|
+
npx sanity typegen generate
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import type {Article} from './sanity.types'
|
|
391
|
+
|
|
392
|
+
const {data} = useDocument<Article>(handle)
|
|
393
|
+
// data is typed as Article
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
### Deployment
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
npx sanity deploy
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Add the resulting app ID to the `deployment` section of your `sanity.config.ts` file: `{deployment: { appId: "appbc1234", ... } }`.
|
|
405
|
+
|
|
406
|
+
App appears in Sanity Dashboard alongside Studios. Requires `sanity.sdk.applications.deploy` permission.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
### UI Options
|
|
411
|
+
|
|
412
|
+
SDK is headless. Common choices:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# Sanity UI (matches Studio aesthetic)
|
|
416
|
+
npm install @sanity/ui @sanity/icons styled-components
|
|
417
|
+
|
|
418
|
+
# Tailwind
|
|
419
|
+
npm install tailwindcss @tailwindcss/vite
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### Tailwind Setup
|
|
423
|
+
|
|
424
|
+
Tailwind requires a few extra steps since the App SDK uses Vite internally.
|
|
425
|
+
|
|
426
|
+
1. **Install dependencies:**
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
npm install tailwindcss @tailwindcss/vite
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
2. **Configure the Vite plugin in `sanity.cli.ts`:**
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import {defineCliConfig} from 'sanity/cli'
|
|
436
|
+
|
|
437
|
+
export default defineCliConfig({
|
|
438
|
+
app: {
|
|
439
|
+
organizationId: 'your-org-id',
|
|
440
|
+
entry: './src/App.tsx',
|
|
441
|
+
},
|
|
442
|
+
vite: async (viteConfig) => {
|
|
443
|
+
const {default: tailwindcss} = await import('@tailwindcss/vite')
|
|
444
|
+
return {
|
|
445
|
+
...viteConfig,
|
|
446
|
+
plugins: [...viteConfig.plugins, tailwindcss()],
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
3. **Import Tailwind in your CSS (e.g., `src/App.css`):**
|
|
453
|
+
|
|
454
|
+
```css
|
|
455
|
+
@import 'tailwindcss';
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
4. **Import the CSS in your app:**
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
// src/App.tsx
|
|
462
|
+
import './App.css'
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Now you can use Tailwind classes in your components.
|
|
466
|
+
|
|
467
|
+
#### Portable Text Editor
|
|
468
|
+
|
|
469
|
+
Use `@portabletext/plugin-sdk-value` to connect a Portable Text Editor with a Sanity document field. It provides two-way sync, real-time collaboration, and optimistic updates.
|
|
470
|
+
|
|
471
|
+
1. **Install dependencies:**
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
npm install @portabletext/editor @portabletext/plugin-sdk-value
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
2. **Use in a component:**
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import {defineSchema, EditorProvider, PortableTextEditable} from '@portabletext/editor'
|
|
481
|
+
import {SDKValuePlugin} from '@portabletext/plugin-sdk-value'
|
|
482
|
+
|
|
483
|
+
function MyEditor({documentId}: {documentId: string}) {
|
|
484
|
+
return (
|
|
485
|
+
<EditorProvider initialConfig={{schemaDefinition: defineSchema({})}}>
|
|
486
|
+
<PortableTextEditable />
|
|
487
|
+
<SDKValuePlugin documentId={documentId} documentType="article" path="content" />
|
|
488
|
+
</EditorProvider>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
##### SDKValuePlugin Props
|
|
494
|
+
|
|
495
|
+
| Prop | Type | Description |
|
|
496
|
+
| -------------- | ------------------- | ----------------------------------------- |
|
|
497
|
+
| `documentId` | `string` | The document ID |
|
|
498
|
+
| `documentType` | `string` | The document type |
|
|
499
|
+
| `path` | `string` | JSONMatch path to the Portable Text field |
|
|
500
|
+
| `dataset` | `string` (optional) | Dataset name if different from default |
|
|
501
|
+
| `projectId` | `string` (optional) | Project ID if different from default |
|
|
502
|
+
|
|
503
|
+
**The plugin handles:**
|
|
504
|
+
|
|
505
|
+
- Two-way sync between editor and document
|
|
506
|
+
- Real-time updates from other users
|
|
507
|
+
- Optimistic updates for smooth UX
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
### Common Patterns
|
|
512
|
+
|
|
513
|
+
#### Editable List Item
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
function EditableTitle({handle}: {handle: DocumentHandle}) {
|
|
517
|
+
const {data: title} = useDocument({...handle, path: 'title'})
|
|
518
|
+
const editTitle = useEditDocument({...handle, path: 'title'})
|
|
519
|
+
|
|
520
|
+
return <input value={title ?? ''} onChange={(e) => editTitle(e.target.value)} />
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### Publish Button with Permission Check
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
function PublishButton({handle}: {handle: DocumentHandle}) {
|
|
528
|
+
const {data: canPublish} = useDocumentPermissions({
|
|
529
|
+
...handle,
|
|
530
|
+
permission: 'publish',
|
|
531
|
+
})
|
|
532
|
+
const apply = useApplyDocumentActions()
|
|
533
|
+
|
|
534
|
+
if (!canPublish) return null
|
|
535
|
+
|
|
536
|
+
return <button onClick={() => apply(publishDocument(handle))}>Publish</button>
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### Document Status Indicator
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
function DocStatus({handle}: {handle: DocumentHandle}) {
|
|
544
|
+
const {data: published} = useDocumentProjection({
|
|
545
|
+
documentId: handle.documentId, // without drafts. prefix
|
|
546
|
+
documentType: handle.documentType,
|
|
547
|
+
projection: '{ _updatedAt }',
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
const {data: draft} = useDocumentProjection({
|
|
551
|
+
documentId: `drafts.${handle.documentId}`,
|
|
552
|
+
documentType: handle.documentType,
|
|
553
|
+
projection: '{ _updatedAt }',
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
if (draft && published) return <span>Modified</span>
|
|
557
|
+
if (draft) return <span>Draft</span>
|
|
558
|
+
if (published) return <span>Published</span>
|
|
559
|
+
return <span>New</span>
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Quick Reference
|
|
566
|
+
|
|
567
|
+
| Task | Hook/Function |
|
|
568
|
+
| --------------------- | ------------------------------------------- |
|
|
569
|
+
| List documents | `useDocuments`, `usePaginatedDocuments` |
|
|
570
|
+
| Read document | `useDocument`, `useDocumentProjection` |
|
|
571
|
+
| Edit field | `useEditDocument` |
|
|
572
|
+
| Publish/Delete/Create | `useApplyDocumentActions` + action creators |
|
|
573
|
+
| GROQ query | `useQuery` |
|
|
574
|
+
| Check permissions | `useDocumentPermissions` |
|
|
575
|
+
| Listen to changes | `useDocumentEvent` |
|
|
576
|
+
|
|
577
|
+
---
|
|
9
578
|
|
|
10
579
|
## Documentation
|
|
11
580
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
581
|
+
- **[Sanity Docs](https://sanity.io/docs/app-sdk)** — Conceptual overview, quickstart guide, and step-by-step walkthrough
|
|
582
|
+
- **[App SDK Reference](https://reference.sanity.io/_sanity/sdk-react)** — In-depth API documentation
|
|
583
|
+
- **[SDK Explorer](https://sdk-explorer.sanity.io)** — Example implementations
|
|
584
|
+
- **[Migration Guide](./guides/0-Migration-Guide.md)** — Upgrading from previous versions
|
|
585
|
+
- **[Learn Course](https://www.sanity.io/learn/course/build-content-apps-with-sanity-app-sdk)** — Interactive video tutorial
|
|
15
586
|
|
|
16
587
|
## License
|
|
17
588
|
|