diff --git a/apps/kitchensink-react/e2e/document-projection.spec.ts b/apps/kitchensink-react/e2e/document-projection.spec.ts new file mode 100644 index 000000000..36c7cda3f --- /dev/null +++ b/apps/kitchensink-react/e2e/document-projection.spec.ts @@ -0,0 +1,147 @@ +import {expect, test} from '@repo/e2e' + +test.describe('Document Projection', () => { + test('can switch between different projection types and view data', async ({ + page, + getClient, + createDocuments, + getPageContext, + }) => { + const client = getClient() + + // Create a book document first (needed for favoriteBooks reference) + const { + documentIds: [bookId], + } = await createDocuments( + [ + { + _type: 'book', + title: 'Test Book for Projection', + }, + ], + {asDraft: false}, + ) + + // Create an author document with favoriteBooks and role + const { + documentIds: [authorId], + } = await createDocuments( + [ + { + _type: 'author', + name: 'Test Author Projection', + favoriteBooks: [{_type: 'reference', _ref: bookId}], + role: 'Senior Developer', + }, + ], + {asDraft: false}, + ) + + // Update the author to reference itself as bestFriend + await client + .patch(authorId) + .set({bestFriend: {_type: 'reference', _ref: authorId}}) + .commit() + + // Navigate to the document projection route + await page.goto('./document-projection') + + // Get the page context for iframe/page detection + const pageContext = await getPageContext(page) + + // Wait for the table to be visible + const table = pageContext.getByTestId('projection-table') + await table.waitFor() + await expect(table).toBeVisible() + + // Wait for the author row to be visible + const authorRow = pageContext.getByTestId(`author-row-${authorId}`) + await authorRow.waitFor() + await expect(authorRow).toBeVisible() + + // Test 1: Favorite Books Projection (default) + const favoriteBooksButton = pageContext.getByRole('button', {name: 'Favorite Books'}) + await expect(favoriteBooksButton).toBeVisible() + + // Verify favorite books projection data is displayed + const nameCell = pageContext.getByTestId(`projection-name-${authorId}`) + await expect(nameCell).toContainText('Test Author Projection') + + const favoriteBooksCell = pageContext.getByTestId(`projection-favorite-books-${authorId}`) + await expect(favoriteBooksCell).toBeVisible() + // Should show the book title or "No favorite books" + const favoriteBooksText = await favoriteBooksCell.textContent() + expect(favoriteBooksText).toBeTruthy() + + // Test 2: Switch to Best Friend Projection + const bestFriendButton = pageContext.getByRole('button', {name: 'Best Friend'}) + await bestFriendButton.click() + + // Wait for the projection to update + await expect(async () => { + const bestFriendCell = pageContext.getByTestId(`projection-best-friend-${authorId}`) + await expect(bestFriendCell).toBeVisible() + }).toPass({timeout: 5000}) + + // Verify best friend projection data is displayed + const bestFriendCell = pageContext.getByTestId(`projection-best-friend-${authorId}`) + await expect(bestFriendCell).toBeVisible() + + const roleCell = pageContext.getByTestId(`projection-role-${authorId}`) + await expect(roleCell).toBeVisible() + await expect(roleCell).toContainText('Senior Developer') + + // Verify favorite books cell is no longer visible + await expect(favoriteBooksCell).not.toBeVisible() + + // Test 3: Switch to Book Count (Groq Helper) Projection + const bookCountButton = pageContext.getByRole('button', {name: 'Book Count'}) + await bookCountButton.click() + + // Wait for the projection to update + await expect(async () => { + const bookCountCell = pageContext.getByTestId(`projection-book-count-${authorId}`) + await expect(bookCountCell).toBeVisible() + }).toPass({timeout: 5000}) + + // Verify book count projection data is displayed + const bookCountCell = pageContext.getByTestId(`projection-book-count-${authorId}`) + await expect(bookCountCell).toBeVisible() + // Should show "1 books" or "0 books" + const bookCountText = await bookCountCell.textContent() + expect(bookCountText).toMatch(/\d+ books/) + + const hasBooksCell = pageContext.getByTestId(`projection-has-books-${authorId}`) + await expect(hasBooksCell).toBeVisible() + // Should show "Yes" or "No" + const hasBooksText = await hasBooksCell.textContent() + expect(['Yes', 'No']).toContain(hasBooksText) + + // Verify best friend and role cells are no longer visible + await expect(bestFriendCell).not.toBeVisible() + await expect(roleCell).not.toBeVisible() + + // Test 4: Switch back to Favorite Books Projection + await favoriteBooksButton.click() + + // Wait for the projection to update + await expect(async () => { + const favoriteBooksCellAgain = pageContext.getByTestId( + `projection-favorite-books-${authorId}`, + ) + await expect(favoriteBooksCellAgain).toBeVisible() + }).toPass({timeout: 5000}) + + // Verify favorite books projection is displayed again + const favoriteBooksCellAgain = pageContext.getByTestId(`projection-favorite-books-${authorId}`) + await expect(favoriteBooksCellAgain).toBeVisible() + + // Verify book count cells are no longer visible + await expect(bookCountCell).not.toBeVisible() + await expect(hasBooksCell).not.toBeVisible() + + // Verify the name cell is still visible (should always be visible) + await expect(nameCell).toBeVisible() + await expect(nameCell).toContainText('Test Author Projection') + }) +}) diff --git a/apps/kitchensink-react/src/DocumentCollection/DocumentProjectionRoute.tsx b/apps/kitchensink-react/src/DocumentCollection/DocumentProjectionRoute.tsx index 169e321be..a91064f1e 100644 --- a/apps/kitchensink-react/src/DocumentCollection/DocumentProjectionRoute.tsx +++ b/apps/kitchensink-react/src/DocumentCollection/DocumentProjectionRoute.tsx @@ -1,34 +1,51 @@ import {DocumentHandle, useDocumentProjection, usePaginatedDocuments} from '@sanity/sdk-react' import {Box, Button, Card, Flex, Label, Spinner, Stack, Text, TextInput} from '@sanity/ui' -import {defineProjection} from 'groq' +import groq, {defineProjection} from 'groq' import {JSX, ReactNode, Suspense, useRef, useState} from 'react' import {ErrorBoundary} from 'react-error-boundary' // Import the custom table components import {Table, TD, TH, TR} from '../components/TableElements' +interface PossibleAuthorProjections { + name?: string + favoriteBookTitles: string[] + bestFriend?: { + name?: string + } + role?: string + bookCount?: number + hasBooks?: boolean +} + // Component for displaying projection data with proper error handling function ProjectionData({ docHandle, - useFirstProjection, + projectionType, }: { docHandle: DocumentHandle<'author'> - useFirstProjection: boolean + projectionType: 'favoriteBooks' | 'bestFriend' | 'groqHelper' }) { - const authorProjection = defineProjection(`{ + const projections: Record = { + favoriteBooks: `{ name, "favoriteBookTitles": favoriteBooks[]->{title}.title - }`) - - const bestFriendProjection = defineProjection(`{ + }`, + bestFriend: defineProjection(`{ name, 'bestFriendName': bestFriend->{name}.name, role - }`) + }`), + groqHelper: groq`{ + name, + "bookCount": count(favoriteBooks), + "hasBooks": count(favoriteBooks) > 0 + }`, + } const ref = useRef(null) - const projection = useFirstProjection ? authorProjection : bestFriendProjection - const {data} = useDocumentProjection({ + const projection = projections[projectionType] + const {data} = useDocumentProjection({ ...docHandle, ref, projection, @@ -36,19 +53,32 @@ function ProjectionData({ return ( <> - + {data.name || 'Untitled'} - {'favoriteBookTitles' in data ? ( + {projectionType === 'favoriteBooks' ? ( <> - + {data.favoriteBookTitles?.filter(Boolean).join(', ') || 'No favorite books'} + ) : projectionType === 'bestFriend' ? ( + <> + + {data.bestFriend?.name || 'No best friend'} + + + {data.role || 'No role'} + + ) : ( <> - {data.bestFriendName || 'No best friend'} - {data.role || 'No role'} + + {data.bookCount ?? 0} books + + + {data.hasBooks ? 'Yes' : 'No'} + )} @@ -84,16 +114,16 @@ function ProjectionError({error}: {error: Error}): ReactNode { // Component for displaying a single author row with projection data function AuthorRow({ docHandle, - useFirstProjection, + projectionType, }: { docHandle: DocumentHandle<'author'> - useFirstProjection: boolean + projectionType: 'favoriteBooks' | 'bestFriend' | 'groqHelper' }) { return ( - + }> }> - + @@ -209,7 +239,9 @@ function PaginationControls({ export function DocumentProjectionRoute(): JSX.Element { const [searchTerm, setSearchTerm] = useState('') const [pageSize, setPageSize] = useState(5) - const [useFirstProjection, setUseFirstProjection] = useState(true) + const [projectionType, setProjectionType] = useState< + 'favoriteBooks' | 'bestFriend' | 'groqHelper' + >('favoriteBooks') const { data, @@ -305,41 +337,53 @@ export function DocumentProjectionRoute(): JSX.Element { /> -