Skip to content
Draft
88 changes: 60 additions & 28 deletions apps/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/* eslint-disable no-console */
import {
createSubscriptionRequest,
registerSubscription,
unregisterSubscription,
WorkerStatus,
} from '@sanity/sdk'
import {SanityApp, SanityConfig, useFrameConnection} from '@sanity/sdk-react'
import {Spinner, ThemeProvider} from '@sanity/ui'
import {buildTheme} from '@sanity/ui/theme'
import {type JSX, Suspense, useState, useEffect, useRef, useCallback} from 'react'
import {registerSubscription, unregisterSubscription, createSubscriptionRequest} from '@sanity/sdk'
import {type JSX, Suspense, useCallback, useEffect, useRef, useState} from 'react'

const theme = buildTheme({})

Expand All @@ -26,7 +32,7 @@ type QueryRequestMessage = {
type: 'dashboard/v1/query/request'
data: {
queryId: string
queryOptions: any
queryOptions: unknown
requestId: string
}
}
Expand All @@ -49,58 +55,69 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject<HTMLIFrameEle
const connectionRef = useRef<(() => void) | null>(null)

// Stable status handler
const handleStatus = useCallback((status: string) => {
setConnectionStatus(status)
console.log('[Dashboard] Connection status:', status)
const handleStatus = useCallback((workerStatus: WorkerStatus) => {
setConnectionStatus(workerStatus)
console.log('[Dashboard] Connection status:', workerStatus)
}, [])

// Stable message handler
const handleQueryRequest = useCallback(async (data: any) => {
const handleQueryRequest = useCallback(async (data: unknown) => {
console.log('[Dashboard] Received query request:', data)


// Type assert the data to the expected structure
const queryData = data as {
queryOptions: {
projectId: string
dataset: string
query: string
params?: Record<string, unknown>
}
requestId: string
}

try {
console.log('[Dashboard] Translating query request to SharedWorker subscription request')
// Create a subscription request from the incoming query data
const subscription = createSubscriptionRequest({
storeName: 'query',
projectId: data.queryOptions.projectId,
dataset: data.queryOptions.dataset,
projectId: queryData.queryOptions.projectId,
dataset: queryData.queryOptions.dataset,
params: {
query: data.queryOptions.query,
options: data.queryOptions.params || {},
query: queryData.queryOptions.query,
options: queryData.queryOptions.params || {},
},
appId: 'dashboard-app',
})

console.log('[Dashboard] Creating subscription for query:', subscription)

// Register the subscription with the SharedWorker (it will handle deduplication)
const subscriptionId = await registerSubscription(subscription)
console.log('[Dashboard] Subscription registered with ID:', subscriptionId)
// Register the subscription with the SharedWorker
// The SharedWorker will execute the query and return the result
const response = await registerSubscription(subscription)
console.log('[Dashboard] Received query response from SharedWorker:', response)

// Return the subscription ID and any initial data
// The SharedWorker now returns the actual query result along with the subscription ID
return {
requestId: data.requestId,
subscriptionId,
data: {message: 'Query subscription created successfully'},
requestId: queryData.requestId,
subscriptionId: response.subscriptionId || response, // Handle both old and new format
data: response.result || response, // Return the actual query result
cached: response.cached || false,
}
} catch (error) {
console.error('[Dashboard] Error handling query request:', error)
return {
requestId: data.requestId,
requestId: queryData.requestId,
error: error instanceof Error ? error.message : String(error),
subscriptionId: null,
}
}
}, [])

const {connect} = useFrameConnection<
QueryResponseMessage,
QueryRequestMessage
>({
const {connect} = useFrameConnection<QueryResponseMessage, QueryRequestMessage>({
name: 'dashboard',
connectTo: 'sdk-app',
targetOrigin: '*',
onStatus: handleStatus,
onStatus: (workerStatus) => handleStatus(workerStatus as WorkerStatus),
heartbeat: false, // Disable heartbeat to reduce cycling
onMessage: {
'dashboard/v1/query/request': handleQueryRequest,
Expand Down Expand Up @@ -132,7 +149,6 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject<HTMLIFrameEle
const iframe = iframeRef.current
if (iframe) {
iframe.addEventListener('load', handleIframeLoad)

// If iframe is already loaded, connect immediately
if (iframe.contentDocument?.readyState === 'complete') {
handleIframeLoad()
Expand All @@ -146,10 +162,12 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject<HTMLIFrameEle
iframe.removeEventListener('load', handleIframeLoad)
}
}
}, [connect])

// Return empty cleanup function when no iframe
return () => {}
}, [connect, iframeRef])

const testSubscription = async () => {
// eslint-disable-next-line no-console
console.log('testSubscription')
try {
setStatus('Testing subscription...')
Expand Down Expand Up @@ -217,6 +235,8 @@ function SharedWorkerTest({iframeRef}: {iframeRef: React.RefObject<HTMLIFrameEle
}

export default function App(): JSX.Element {
const iframeRef = useRef<HTMLIFrameElement>(null)

return (
<ThemeProvider theme={theme}>
<SanityApp fallback={<Spinner />} config={devConfigs}>
Expand All @@ -227,16 +247,28 @@ export default function App(): JSX.Element {
inset: 0,
display: 'flex',
flexDirection: 'column',
height: '100vh',
width: '100vw',
}}
>
<SharedWorkerTest iframeRef={iframeRef} />
<iframe
ref={iframeRef}
title="sdk-app"
src="http://localhost:3341/"
style={{
flex: 1,
border: 'none',
width: '100%',
height: '100%',
minHeight: '400px',
backgroundColor: '#f5f5f5', // Add background to see if iframe is loading
}}
onLoad={() => {
console.log('[Dashboard] Iframe loaded successfully')
}}
onError={(e) => {
console.error('[Dashboard] Iframe failed to load:', e)
}}
/>
</div>
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export default defineConfig(({mode}) => {
'@sanity/sdk-react': resolve(import.meta.dirname, '../../packages/react/src/_exports'),
},
},
worker: {
format: 'es',
},
define: {
'import.meta.env.VITE_IS_E2E': JSON.stringify(isE2E),
'import.meta.env.VITE_E2E_PROJECT_ID': JSON.stringify(
Expand Down
60 changes: 32 additions & 28 deletions apps/kitchensink-react/e2e/multi-resource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ test.describe('Multi Resource Route', () => {
process.env['SDK_E2E_DATASET_0'], // First dataset
)

// Create a player document in dataset 1
// Create a movie document in dataset 1
const {
documentIds: [playerId],
documentIds: [movieId],
} = await createDocuments(
[
{
_type: 'player',
name: 'Test Player Multi Resource',
slackUserId: 'U1234567890',
_type: 'movie',
title: 'Test Movie Multi Resource',
tmdb_id: 123456,
release_date: '2021-01-01',
hosted_poster_path: 'https://example.com/poster.jpg',
},
],
{asDraft: false}, // Create as published document
Expand All @@ -45,16 +47,16 @@ test.describe('Multi Resource Route', () => {

// Wait for both document cards to be visible
await expect(pageContext.getByTestId(/^author-document-/)).toBeVisible()
await expect(pageContext.getByTestId(/^player-document-/)).toBeVisible()
await expect(pageContext.getByTestId(/^movie-document-/)).toBeVisible()

// Verify author document content is displayed
await expect(pageContext.getByTestId('author-name-display')).toHaveText(
'Test Author Multi Resource',
)

// Verify player document content is displayed
await expect(pageContext.getByTestId('player-name-display')).toHaveText(
'Test Player Multi Resource',
// Verify movie document content is displayed
await expect(pageContext.getByTestId('movie-name-display')).toHaveText(
'Test Movie Multi Resource',
)

// Test author projection data
Expand All @@ -67,12 +69,14 @@ test.describe('Multi Resource Route', () => {
'Best Code Award',
)

// Test player projection data
await expect(pageContext.getByTestId('player-projection-name')).toContainText(
'Test Player Multi Resource',
// Test movie projection data
await expect(pageContext.getByTestId('movie-projection-name')).toContainText(
'Test Movie Multi Resource',
)
await expect(pageContext.getByTestId('player-projection-slack-id')).toContainText('U1234567890')
await expect(pageContext.getByTestId('player-projection-has-slack')).toContainText('Yes')
await expect(pageContext.getByTestId('movie-projection-release-date')).toContainText(
'2021-01-01',
)
await expect(pageContext.getByTestId('movie-projection-has-poster')).toContainText('Yes')

// Test editing the author document
const authorNameInput = pageContext.getByTestId('author-name-input')
Expand All @@ -87,26 +91,26 @@ test.describe('Multi Resource Route', () => {
'Updated Author Name',
)

// Test editing the player document
const playerNameInput = pageContext.getByTestId('player-name-input')
await playerNameInput.fill('Updated Player Name')
await playerNameInput.press('Enter')
// Test editing the movie document
const movieNameInput = pageContext.getByTestId('movie-name-input')
await movieNameInput.fill('Updated Movie Name')
await movieNameInput.press('Enter')

// Verify the change is reflected in the display
await expect(pageContext.getByTestId('player-name-display')).toHaveText('Updated Player Name')
await expect(pageContext.getByTestId('movie-name-display')).toHaveText('Updated Movie Name')

// Verify the change is also reflected in the projection
await expect(pageContext.getByTestId('player-projection-name')).toContainText(
'Updated Player Name',
await expect(pageContext.getByTestId('movie-projection-name')).toContainText(
'Updated Movie Name',
)

// Test that external changes are reflected (simulating real-time updates)
const authorClient = getClient(process.env['SDK_E2E_DATASET_0'])
await authorClient.patch(`drafts.${authorId}`).set({name: 'Externally Updated Author'}).commit()

// Test external change for player
const playerClient = getClient(process.env['SDK_E2E_DATASET_1'])
await playerClient.patch(`drafts.${playerId}`).set({name: 'Externally Updated Player'}).commit()
// Test external change for movie
const movieClient = getClient(process.env['SDK_E2E_DATASET_1'])
await movieClient.patch(`drafts.${movieId}`).set({title: 'Externally Updated Movie'}).commit()

// Verify external change is reflected
await expect(async () => {
Expand All @@ -118,10 +122,10 @@ test.describe('Multi Resource Route', () => {

// Verify external change is reflected
await expect(async () => {
const playerDisplay = await pageContext.getByTestId('player-name-display').textContent()
const playerProjection = await pageContext.getByTestId('player-projection-name').textContent()
expect(playerDisplay).toBe('Externally Updated Player')
expect(playerProjection).toContain('Externally Updated Player')
const movieDisplay = await pageContext.getByTestId('movie-name-display').textContent()
const movieProjection = await pageContext.getByTestId('movie-projection-name').textContent()
expect(movieDisplay).toBe('Externally Updated Movie')
expect(movieProjection).toContain('Externally Updated Movie')
}).toPass({timeout: 5000})
})
})
2 changes: 1 addition & 1 deletion apps/kitchensink-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"lint": "eslint .",
"paramour": "npx paramour --config=./src/css/css.config.js --output=./src/css/paramour.css",
"preview": "vite preview",
"schema:extract": "sanity schema extract --workspace ppsg7ml5-test --path schema.ppsg7ml5.test.json && sanity schema extract --workspace d45jg133-production --path schema.d45jg133.production.json",
"schema:extract": "sanity schema extract --workspace ppsg7ml5-test --path schema.ppsg7ml5.test.json && sanity schema extract --workspace vo1ysemo-production --path schema.vo1ysemo.production.json",
"test:e2e": "playwright test",
"tsc": "tsc --noEmit",
"types:generate": "./node_modules/@sanity/cli/bin/sanity typegen generate"
Expand Down
4 changes: 2 additions & 2 deletions apps/kitchensink-react/sanity-typegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"schemaId": "ppsg7ml5.test"
},
{
"schemaPath": "./schema.d45jg133.production.json",
"schemaId": "d45jg133.production"
"schemaPath": "./schema.vo1ysemo.production.json",
"schemaId": "vo1ysemo.production"
}
],
"overloadClientMethods": false
Expand Down
4 changes: 2 additions & 2 deletions apps/kitchensink-react/sanity.cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
app: {
organizationId: 'oSyH1iET5',
organizationId: 'oblZgbTFj',
entry: './src/App.tsx',
id: 'cbhuuqhkp6sevhpreoh0so8s',
id: 'wkyoigmzawwnnwx458zgoh46',
},
})
Loading
Loading