Skip to content

Commit a1a8e01

Browse files
committed
feat(document): update document store to accept sources
1 parent b4a3333 commit a1a8e01

19 files changed

+798
-165
lines changed

apps/kitchensink-react/src/DocumentCollection/DocumentEditorRoute.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ import {
2929
TextInput,
3030
Tooltip,
3131
} from '@sanity/ui'
32-
import {JsonData, JsonEditor} from 'json-edit-react'
3332
import {type JSX, useEffect, useState} from 'react'
3433

34+
import {JsonDocumentEditor} from '../components/JsonDocumentEditor'
3535
import {devConfigs, e2eConfigs} from '../sanityConfigs'
3636

3737
function DocumentEditor({docHandle}: {docHandle: DocumentHandle<'author'>}) {
@@ -50,7 +50,6 @@ function DocumentEditor({docHandle}: {docHandle: DocumentHandle<'author'>}) {
5050
const setName = useEditDocument({...docHandle, path: 'name'})
5151

5252
const {data: document} = useDocument(docHandle)
53-
const setDocument = useEditDocument(docHandle)
5453

5554
return (
5655
<Box padding={4}>
@@ -208,9 +207,12 @@ function DocumentEditor({docHandle}: {docHandle: DocumentHandle<'author'>}) {
208207
<Box style={{display: 'none'}} data-testid="document-content">
209208
{JSON.stringify(document)}
210209
</Box>
211-
<Box style={{minHeight: '400px'}}>
212-
<JsonEditor data={document} setData={setDocument as (data: JsonData) => void} />
213-
</Box>
210+
<JsonDocumentEditor
211+
documentHandle={docHandle}
212+
minHeight="400px"
213+
wrapInCard={false}
214+
showSyncStatus={false}
215+
/>
214216
</>
215217
)}
216218
</Stack>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
type DocumentHandle,
3+
useDocument,
4+
useDocumentSyncStatus,
5+
useEditDocument,
6+
} from '@sanity/sdk-react'
7+
import {Badge, Box, Card, Stack, Text} from '@sanity/ui'
8+
import {type JsonData, JsonEditor} from 'json-edit-react'
9+
import React from 'react'
10+
import {ErrorBoundary} from 'react-error-boundary'
11+
12+
interface JsonDocumentEditorProps {
13+
/** Document handle for the document being edited */
14+
documentHandle: Pick<DocumentHandle, 'documentId' | 'documentType'>
15+
16+
/** Optional minimum height (defaults to 400px) */
17+
minHeight?: string
18+
19+
/** Optional maximum height for scrolling */
20+
maxHeight?: string
21+
22+
/** Show sync status indicator (defaults to true) */
23+
showSyncStatus?: boolean
24+
25+
/** Optional custom styles for the editor container */
26+
containerStyle?: React.CSSProperties
27+
28+
/** Optional card wrapper (defaults to true) */
29+
wrapInCard?: boolean
30+
}
31+
32+
function ErrorFallback({error}: {error: Error}) {
33+
return (
34+
<Card tone="critical" padding={4}>
35+
<Stack space={3}>
36+
<Text weight="semibold">Failed to load editor</Text>
37+
<Text size={1}>{error.message}</Text>
38+
</Stack>
39+
</Card>
40+
)
41+
}
42+
43+
export function JsonDocumentEditor({
44+
documentHandle,
45+
minHeight = '400px',
46+
maxHeight,
47+
showSyncStatus = true,
48+
containerStyle,
49+
wrapInCard = true,
50+
}: JsonDocumentEditorProps): React.JSX.Element {
51+
const {data: document} = useDocument(documentHandle)
52+
const editDocument = useEditDocument(documentHandle)
53+
const synced = useDocumentSyncStatus(documentHandle)
54+
55+
const editorContent = (
56+
<Stack space={3}>
57+
{showSyncStatus && (
58+
<Box>
59+
{synced ? (
60+
<Badge tone="positive">✓ Synced</Badge>
61+
) : (
62+
<Badge tone="default">⟳ Syncing…</Badge>
63+
)}
64+
</Box>
65+
)}
66+
<Box
67+
style={{
68+
minHeight,
69+
maxHeight,
70+
overflow: maxHeight ? 'auto' : undefined,
71+
border: '1px solid var(--card-border-color)',
72+
borderRadius: '4px',
73+
padding: '16px',
74+
backgroundColor: 'var(--card-bg-color)',
75+
...containerStyle,
76+
}}
77+
>
78+
<JsonEditor data={document || {}} setData={editDocument as (data: JsonData) => void} />
79+
</Box>
80+
</Stack>
81+
)
82+
83+
const content = wrapInCard ? (
84+
<Card padding={4} style={{minHeight}}>
85+
{editorContent}
86+
</Card>
87+
) : (
88+
editorContent
89+
)
90+
91+
return <ErrorBoundary FallbackComponent={ErrorFallback}>{content}</ErrorBoundary>
92+
}

apps/kitchensink-react/src/routes/MediaLibraryRoute.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,45 @@
1-
import {useDocumentProjection, useQuery} from '@sanity/sdk-react'
2-
import {Card, Spinner, Text} from '@sanity/ui'
1+
import {useDocumentProjection, useDocumentSyncStatus, useQuery} from '@sanity/sdk-react'
2+
import {Box, Button, Card, Dialog, Flex, Spinner, Text} from '@sanity/ui'
33
import {type JSX, Suspense, useState} from 'react'
44
import {SanityDocument} from 'sanity'
55

6+
import {JsonDocumentEditor} from '../components/JsonDocumentEditor'
7+
8+
// Modal dialog for editing media assets
9+
function MediaAssetEditorDialog({
10+
assetId,
11+
open,
12+
onClose,
13+
}: {
14+
assetId: string
15+
open: boolean
16+
onClose: () => void
17+
}) {
18+
const docHandle = {
19+
documentType: 'sanity.asset',
20+
documentId: assetId,
21+
sourceName: 'media-library',
22+
}
23+
const synced = useDocumentSyncStatus(docHandle)
24+
25+
return (
26+
<Dialog
27+
header={`Edit Asset: ${assetId}`}
28+
id="media-asset-editor"
29+
onClose={onClose}
30+
open={open}
31+
width={2}
32+
>
33+
<Box padding={4}>
34+
<JsonDocumentEditor documentHandle={docHandle} minHeight="500px" maxHeight="70vh" />
35+
<Flex justify="flex-end" gap={2} marginTop={4}>
36+
<Button text={synced ? 'Close' : 'Syncing...'} onClick={onClose} tone="primary" />
37+
</Flex>
38+
</Box>
39+
</Dialog>
40+
)
41+
}
42+
643
// Component to display projection data for a specific asset
744
function AssetProjection({assetId}: {assetId: string}) {
845
const {data: projectionData} = useDocumentProjection<{
@@ -45,6 +82,7 @@ function AssetProjection({assetId}: {assetId: string}) {
4582
export function MediaLibraryRoute(): JSX.Element {
4683
const [query] = useState('*[_type == "sanity.asset"][0...10] | order(_id desc)')
4784
const [isLoading] = useState(false)
85+
const [editingAssetId, setEditingAssetId] = useState<string | null>(null)
4886

4987
const {data, isPending} = useQuery<SanityDocument[]>({
5088
query,
@@ -91,11 +129,28 @@ export function MediaLibraryRoute(): JSX.Element {
91129

92130
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}>
93131
<Card padding={3} style={{backgroundColor: '#1a1a1a'}}>
94-
<div style={{display: 'flex', alignItems: 'center', marginBottom: '1rem'}}>
95-
<Text size={1} weight="medium" style={{color: '#fff'}}>
96-
useQuery Results:
97-
</Text>
98-
{(isPending || isLoading) && <Spinner style={{marginLeft: '0.5rem'}} />}
132+
<div
133+
style={{
134+
display: 'flex',
135+
alignItems: 'center',
136+
justifyContent: 'space-between',
137+
marginBottom: '1rem',
138+
}}
139+
>
140+
<div style={{display: 'flex', alignItems: 'center'}}>
141+
<Text size={1} weight="medium" style={{color: '#fff'}}>
142+
useQuery Results:
143+
</Text>
144+
{(isPending || isLoading) && <Spinner style={{marginLeft: '0.5rem'}} />}
145+
</div>
146+
{firstAssetId && (
147+
<Button
148+
text="Edit First Asset"
149+
tone="primary"
150+
fontSize={1}
151+
onClick={() => setEditingAssetId(firstAssetId)}
152+
/>
153+
)}
99154
</div>
100155

101156
<pre
@@ -127,6 +182,15 @@ export function MediaLibraryRoute(): JSX.Element {
127182
</Suspense>
128183
)}
129184
</div>
185+
186+
{/* Editor Dialog */}
187+
{editingAssetId && (
188+
<MediaAssetEditorDialog
189+
assetId={editingAssetId}
190+
open={!!editingAssetId}
191+
onClose={() => setEditingAssetId(null)}
192+
/>
193+
)}
130194
</div>
131195
)
132196
}

packages/core/src/document/applyDocumentActions.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {type SanityDocument} from '@sanity/types'
33
import {Subject} from 'rxjs'
44
import {describe, expect, it} from 'vitest'
55

6-
import {bindActionByDataset} from '../store/createActionBinder'
6+
import {bindActionBySource} from '../store/createActionBinder'
77
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
88
import {} from '../store/createStateSourceAction'
99
import {createStoreState, type StoreState} from '../store/createStoreState'
@@ -14,7 +14,7 @@ import {type AppliedTransaction, type OutgoingTransaction} from './reducers'
1414

1515
vi.mock('../store/createActionBinder', async (importOriginal) => ({
1616
...(await importOriginal<typeof import('../store/createActionBinder')>()),
17-
bindActionByDataset: vi.fn(),
17+
bindActionBySource: vi.fn(),
1818
}))
1919

2020
type TestState = Pick<
@@ -48,9 +48,9 @@ describe('applyDocumentActions', () => {
4848
}
4949
state = createStoreState(initialState)
5050
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
51-
const key = {name: 'p.d', projectId: 'p', dataset: 'd'}
51+
const key = {name: 'p.d', source: {projectId: 'p', dataset: 'd'}}
5252

53-
vi.mocked(bindActionByDataset).mockImplementation(
53+
vi.mocked(bindActionBySource).mockImplementation(
5454
(_storeDef, action) => (instanceParam: SanityInstance, options) =>
5555
action({instance: instanceParam, state, key}, options),
5656
)
@@ -75,6 +75,7 @@ describe('applyDocumentActions', () => {
7575
const applyPromise = applyDocumentActions(instance, {
7676
actions: [action],
7777
transactionId: 'txn-success',
78+
source: {projectId: 'p', dataset: 'd'},
7879
})
7980

8081
const appliedTx: AppliedTransaction = {
@@ -132,6 +133,7 @@ describe('applyDocumentActions', () => {
132133
const applyPromise = applyDocumentActions(instance, {
133134
actions: [action],
134135
transactionId: 'txn-error',
136+
source: {projectId: 'p', dataset: 'd'},
135137
})
136138

137139
const errorEvent: DocumentEvent = {
@@ -165,6 +167,7 @@ describe('applyDocumentActions', () => {
165167
const applyPromise = applyDocumentActions(childInstance, {
166168
actions: [action],
167169
transactionId: 'txn-child-match',
170+
source: {projectId: 'p', dataset: 'd'},
168171
})
169172

170173
// Simulate an applied transaction on the parent's instance

packages/core/src/document/applyDocumentActions.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import {type SanityClient} from '@sanity/client'
22
import {type SanityDocument} from 'groq'
33
import {distinctUntilChanged, filter, first, firstValueFrom, map, race} from 'rxjs'
44

5-
import {bindActionByDataset} from '../store/createActionBinder'
5+
import {type DocumentSource} from '../config/sanityConfig'
6+
import {bindActionBySource} from '../store/createActionBinder'
67
import {type SanityInstance} from '../store/createSanityInstance'
78
import {type StoreContext} from '../store/defineStore'
89
import {type DocumentAction} from './actions'
@@ -29,6 +30,11 @@ export interface ApplyDocumentActionsOptions {
2930
*/
3031
actions: DocumentAction[]
3132

33+
/**
34+
* The source to which the documents being acted on belong.
35+
*/
36+
source?: DocumentSource
37+
3238
/**
3339
* Optionally provide an ID to be used as this transaction ID
3440
*/
@@ -61,7 +67,7 @@ export function applyDocumentActions(
6167
return boundApplyDocumentActions(...args)
6268
}
6369

64-
const boundApplyDocumentActions = bindActionByDataset(documentStore, _applyDocumentActions)
70+
const boundApplyDocumentActions = bindActionBySource(documentStore, _applyDocumentActions)
6571

6672
/** @internal */
6773
async function _applyDocumentActions(

0 commit comments

Comments
 (0)