From fb84570797f73bb2ba2a291514e7731646a91f95 Mon Sep 17 00:00:00 2001 From: Pedro Bonamin <46196328+pedrobonamin@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:55:13 +0200 Subject: [PATCH] feat(corel): Add review changes screen (#7155) * fix(corel): add prepareForPreview in useDocumentPreviewValues * feat(corel): add review changes screen * fix(corel): update ReleaseReview to use new documents history data * feat(corel): add published document listener to review changes screen * feat(core): add new useObserveDocument hook * feat(corel): move active screen state to query params * chore(corel): make documentDiff component agnostic to releases * fix(corel): update document diff is changed check * fix(corel): update useObserveDocument hook, export it as unstable * chore(corel): add tests to release review screen --- .../core/field/diff/components/DiffCard.tsx | 17 +- .../field/diff/components/FieldChange.tsx | 1 + .../field/diff/components/GroupChange.tsx | 3 +- packages/sanity/src/core/preview/index.ts | 1 + .../src/core/preview/useObserveDocument.ts | 36 ++++ .../components/ReleaseDocumentPreview.tsx | 49 +++++ .../tool/__tests__/ReleaseReview.test.tsx | 197 ++++++++++++++++++ .../releases/tool/detail/ReleaseDetail.tsx | 51 ++++- .../releases/tool/detail/ReleaseOverview.tsx | 14 +- .../releases/tool/detail/ReleaseReview.tsx | 29 +++ .../tool/detail/documentTable/DocumentRow.tsx | 35 +--- .../documentTable/useDocumentPreviewValues.ts | 4 +- .../detail/review/DocumentDiff.styled.tsx | 94 +++++++++ .../tool/detail/review/DocumentDiff.tsx | 78 +++++++ .../detail/review/DocumentDiffContainer.tsx | 53 +++++ .../detail/review/DocumentReviewHeader.tsx | 67 ++++++ 16 files changed, 680 insertions(+), 49 deletions(-) create mode 100644 packages/sanity/src/core/preview/useObserveDocument.ts create mode 100644 packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx create mode 100644 packages/sanity/src/core/releases/tool/__tests__/ReleaseReview.test.tsx create mode 100644 packages/sanity/src/core/releases/tool/detail/ReleaseReview.tsx create mode 100644 packages/sanity/src/core/releases/tool/detail/review/DocumentDiff.styled.tsx create mode 100644 packages/sanity/src/core/releases/tool/detail/review/DocumentDiff.tsx create mode 100644 packages/sanity/src/core/releases/tool/detail/review/DocumentDiffContainer.tsx create mode 100644 packages/sanity/src/core/releases/tool/detail/review/DocumentReviewHeader.tsx diff --git a/packages/sanity/src/core/field/diff/components/DiffCard.tsx b/packages/sanity/src/core/field/diff/components/DiffCard.tsx index 8f59383774a..e1e104e054f 100644 --- a/packages/sanity/src/core/field/diff/components/DiffCard.tsx +++ b/packages/sanity/src/core/field/diff/components/DiffCard.tsx @@ -17,13 +17,19 @@ export interface DiffCardProps { tooltip?: {description?: ReactNode} | boolean } -const StyledCard = styled(Card)` +interface StyledCardProps { + $annotationColor: {backgroundColor: string; color: string} +} + +const StyledCard = styled(Card)` --diff-card-radius: ${({theme}) => rem(theme.sanity.radius[2])}; --diff-card-bg-color: ${({theme}) => theme.sanity.color.card.enabled.bg}; max-width: 100%; position: relative; border-radius: var(--diff-card-radius); + background-color: ${({$annotationColor}) => $annotationColor.backgroundColor}; + color: ${({$annotationColor}) => $annotationColor.color}; &:not(del) { text-decoration: none; @@ -100,13 +106,12 @@ export const DiffCard = forwardRef(function DiffCard( as={as} className={className} data-hover={disableHoverEffect || !annotation ? undefined : ''} + data-ui="diff-card" ref={ref} radius={1} - style={{ - ...style, - backgroundColor: color.background, - color: color.text, - }} + // Added annotation color to the card using css to make it possible to override by the ReleaseReview + $annotationColor={{backgroundColor: color.background, color: color.text}} + style={style} > {children} diff --git a/packages/sanity/src/core/field/diff/components/FieldChange.tsx b/packages/sanity/src/core/field/diff/components/FieldChange.tsx index f828bf79794..71f1928a5ff 100644 --- a/packages/sanity/src/core/field/diff/components/FieldChange.tsx +++ b/packages/sanity/src/core/field/diff/components/FieldChange.tsx @@ -93,6 +93,7 @@ export function FieldChange( data-revert-field-hover={revertHovered ? '' : undefined} data-error={change.error ? '' : undefined} data-revert-all-hover + data-ui="field-diff-inspect-wrapper" > {change.error ? ( diff --git a/packages/sanity/src/core/field/diff/components/GroupChange.tsx b/packages/sanity/src/core/field/diff/components/GroupChange.tsx index 7bbe0599724..fc50233eb1a 100644 --- a/packages/sanity/src/core/field/diff/components/GroupChange.tsx +++ b/packages/sanity/src/core/field/diff/components/GroupChange.tsx @@ -77,10 +77,11 @@ export function GroupChange( - + {changes.map((change) => ( ( + documentId: string, + schemaType: ObjectSchemaType, +): { + document: T | null + loading: boolean +} { + const documentPreviewStore = useDocumentPreviewStore() + const observable = useMemo( + () => + documentPreviewStore + .observePaths( + {_id: documentId}, + schemaType.fields.map((field) => [field.name]), + ) + .pipe(map((document) => ({loading: false, document: document as T}))), + [documentId, documentPreviewStore, schemaType.fields], + ) + return useObservable(observable, INITIAL_STATE) +} diff --git a/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx b/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx new file mode 100644 index 00000000000..4cd9b306b13 --- /dev/null +++ b/packages/sanity/src/core/releases/components/ReleaseDocumentPreview.tsx @@ -0,0 +1,49 @@ +import {type PreviewValue} from '@sanity/types' +import {Card} from '@sanity/ui' +import {type ForwardedRef, forwardRef, useMemo} from 'react' +import {IntentLink} from 'sanity/router' + +import {SanityDefaultPreview} from '../../preview/components/SanityDefaultPreview' +import {getPublishedId} from '../../util/draftUtils' + +interface ReleaseDocumentPreviewProps { + documentId: string + documentTypeName: string + releaseName: string + previewValues: PreviewValue + isLoading: boolean +} + +export function ReleaseDocumentPreview({ + documentId, + documentTypeName, + releaseName, + previewValues, + isLoading, +}: ReleaseDocumentPreviewProps) { + const LinkComponent = useMemo( + () => + // eslint-disable-next-line @typescript-eslint/no-shadow + forwardRef(function LinkComponent(linkProps, ref: ForwardedRef) { + return ( + + ) + }), + [documentId, documentTypeName, releaseName], + ) + + return ( + + + + ) +} diff --git a/packages/sanity/src/core/releases/tool/__tests__/ReleaseReview.test.tsx b/packages/sanity/src/core/releases/tool/__tests__/ReleaseReview.test.tsx new file mode 100644 index 00000000000..ffc478abd1a --- /dev/null +++ b/packages/sanity/src/core/releases/tool/__tests__/ReleaseReview.test.tsx @@ -0,0 +1,197 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {render, screen} from '@testing-library/react' + +import {queryByDataUi} from '../../../../../test/setup/customQueries' +import {createWrapper} from '../../../bundles/util/tests/createWrapper' +import {useObserveDocument} from '../../../preview/useObserveDocument' +import {ColorSchemeProvider} from '../../../studio/colorScheme' +import {UserColorManagerProvider} from '../../../user-color/provider' +import {releasesUsEnglishLocaleBundle} from '../../i18n' +import {useDocumentPreviewValues} from '../detail/documentTable/useDocumentPreviewValues' +import {type DocumentHistory} from '../detail/documentTable/useReleaseHistory' +import {ReleaseReview} from '../detail/ReleaseReview' + +const baseDocument = { + name: 'William Faulkner', + role: 'developer', + awards: ['first award', 'second award'], + favoriteBooks: [ + { + _ref: '0b229e82-48e4-4226-a36f-e6b3d874478a', + _type: 'reference', + _key: '23610d43d8e8', + }, + ], + _id: '1f8caa96-4174-4c91-bb40-cbc96a737fcf', + _rev: 'FvEfB9CaLlljeKWNkRBaf5', + _type: 'author', + _createdAt: '', + _updatedAt: '', +} + +const MOCKED_PROPS = { + documents: [ + { + favoriteBooks: [ + { + _ref: '0daffd51-59c3-4dca-a9ee-1c4db54db87e', + _type: 'reference', + _key: '0d3f45004da0', + }, + ], + _version: {}, + bestFriend: { + _ref: '0c9d9135-0d20-44a1-9ec1-54ea41ce84d1', + _type: 'reference', + }, + _rev: 'FvEfB9CaLlljeKWNkQgpz9', + _type: 'author', + role: 'designer', + _createdAt: '2024-07-10T12:10:38Z', + name: 'William Faulkner added', + _id: 'differences.1f8caa96-4174-4c91-bb40-cbc96a737fcf', + _updatedAt: '2024-07-15T10:46:02Z', + }, + ], + release: { + _updatedAt: '2024-07-12T10:39:32Z', + authorId: 'p8xDvUMxC', + _type: 'bundle', + description: 'To test differences in documents', + hue: 'gray', + title: 'Differences', + _createdAt: '2024-07-10T12:09:56Z', + icon: 'cube', + name: 'differences', + _id: 'd3137faf-ece6-44b5-a2b1-1090967f868e', + _rev: 'j9BPWHem9m3oUugvhMXEGV', + } as const, + documentsHistory: new Map([ + [ + 'differences.1f8caa96-4174-4c91-bb40-cbc96a737fcf', + { + history: [], + createdBy: 'p8xDvUMxC', + lastEditedBy: 'p8xDvUMxC', + editors: ['p8xDvUMxC'], + }, + ], + ]), +} + +jest.mock('sanity/router', () => ({ + ...(jest.requireActual('sanity/router') || {}), + IntentLink: jest.fn().mockImplementation((props: any) => {props.children}), + useRouter: jest.fn().mockReturnValue({ + state: {bundleName: 'differences'}, + navigate: jest.fn(), + }), +})) + +jest.mock('../../../preview/useObserveDocument', () => { + return { + useObserveDocument: jest.fn(), + } +}) + +jest.mock('../detail/documentTable/useDocumentPreviewValues', () => { + return { + useDocumentPreviewValues: jest.fn(), + } +}) + +const mockedUseObserveDocument = useObserveDocument as jest.Mock +const mockedUseDocumentPreviewValues = useDocumentPreviewValues as jest.Mock< + typeof useDocumentPreviewValues +> + +const previewValues = { + _id: 'differences.1f8caa96-4174-4c91-bb40-cbc96a737fcf', + _type: 'author', + _createdAt: '2024-07-10T12:10:38Z', + _updatedAt: '2024-07-15T10:46:02Z', + _version: {}, + title: 'William Faulkner differences check 123 asklaks ', + subtitle: 'Designer', +} + +describe('ReleaseReview', () => { + describe('when loading baseDocument', () => { + beforeEach(async () => { + mockedUseObserveDocument.mockReturnValue({ + document: null, + loading: true, + }) + mockedUseDocumentPreviewValues.mockReturnValue({previewValues, isLoading: false}) + const wrapper = await createWrapper({ + resources: [releasesUsEnglishLocaleBundle], + }) + render(, {wrapper}) + }) + it("should show the loader when the base document hasn't loaded", () => { + queryByDataUi(document.body, 'Spinner') + }) + }) + describe('when there is no base document', () => { + beforeEach(async () => { + mockedUseObserveDocument.mockReturnValue({ + document: null, + loading: false, + }) + mockedUseDocumentPreviewValues.mockReturnValue({previewValues, isLoading: false}) + const wrapper = await createWrapper({ + resources: [releasesUsEnglishLocaleBundle], + }) + render(, {wrapper}) + }) + it('should render the new document ui', async () => { + expect(screen.getByText('New document')).toBeInTheDocument() + }) + }) + + describe('when the base document is loaded and there are no changes', () => { + beforeEach(async () => { + mockedUseObserveDocument.mockReturnValue({ + document: MOCKED_PROPS.documents[0], + loading: false, + }) + mockedUseDocumentPreviewValues.mockReturnValue({previewValues, isLoading: false}) + const wrapper = await createWrapper({ + resources: [releasesUsEnglishLocaleBundle], + }) + render(, {wrapper}) + }) + it('should show that there are no changes', async () => { + expect(screen.getByText('No changes')).toBeInTheDocument() + }) + }) + + describe('when the base document is loaded and has changes', () => { + beforeEach(async () => { + mockedUseObserveDocument.mockReturnValue({ + document: baseDocument, + loading: false, + }) + mockedUseDocumentPreviewValues.mockReturnValue({previewValues, isLoading: false}) + const wrapper = await createWrapper({ + resources: [releasesUsEnglishLocaleBundle], + }) + render( + + + + + , + {wrapper}, + ) + }) + it('should should show the changes', async () => { + // Find an ins tag with the text "added" + const element = screen.getByText((content, el) => { + return el?.tagName.toLowerCase() === 'ins' && content === 'added' + }) + + expect(element).toBeInTheDocument() + }) + }) +}) diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx index e52baa1d896..078dca4af54 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseDetail.tsx @@ -1,8 +1,8 @@ import {ArrowLeftIcon, PublishIcon} from '@sanity/icons' import {Box, Card, Container, Flex, Heading, Stack, Text} from '@sanity/ui' -import {useMemo, useState} from 'react' +import {useCallback, useMemo} from 'react' import {LoadingBlock, useClient} from 'sanity' -import {useRouter} from 'sanity/router' +import {type RouterContextValue, useRouter} from 'sanity/router' import {Button} from '../../../../ui-components' import {useListener} from '../../../hooks/useListener' @@ -13,8 +13,10 @@ import {BundleMenuButton} from '../../components/BundleMenuButton/BundleMenuButt import {type ReleasesRouterState} from '../../types/router' import {useReleaseHistory} from './documentTable/useReleaseHistory' import {ReleaseOverview} from './ReleaseOverview' +import {ReleaseReview} from './ReleaseReview' -type Screen = 'overview' | 'review' +const SUPPORTED_SCREENS = ['overview', 'review'] as const +type Screen = (typeof SUPPORTED_SCREENS)[number] const useFetchBundleDocuments = (bundleName: string) => { const client = useClient({apiVersion: API_VERSION}) @@ -22,9 +24,22 @@ const useFetchBundleDocuments = (bundleName: string) => { return useListener({query, client}) } +const getActiveScreen = (router: RouterContextValue): Screen => { + const activeScreen = Object.fromEntries(router.state._searchParams || []).screen as Screen + if ( + typeof activeScreen !== 'string' || + !activeScreen || + !SUPPORTED_SCREENS.includes(activeScreen) + ) { + return 'overview' + } + return activeScreen +} export const ReleaseDetail = () => { const router = useRouter() - const [activeScreen, setActiveScreen] = useState('overview') + + const activeScreen = getActiveScreen(router) + const {bundleName}: ReleasesRouterState = router.state const parsedBundleName = decodeURIComponent(bundleName || '') const {data, loading} = useBundles() @@ -37,6 +52,20 @@ export const ReleaseDetail = () => { const showPublishButton = loading || !bundle?.publishedAt const isPublishButtonDisabled = loading || !bundle || !bundleHasDocuments + const navigateToReview = useCallback(() => { + router.navigate({ + ...router.state, + _searchParams: [['screen', 'review']], + }) + }, [router]) + + const navigateToOverview = useCallback(() => { + router.navigate({ + ...router.state, + _searchParams: [], + }) + }, [router]) + const header = useMemo( () => ( @@ -62,7 +91,7 @@ export const ReleaseDetail = () => {