Skip to content

Commit

Permalink
feat(sanity): support viewing and editing document versions
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Aug 12, 2024
1 parent 597ad68 commit e9aa1cd
Show file tree
Hide file tree
Showing 77 changed files with 759 additions and 256 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type SanityClient} from '@sanity/client'
import {Card, LayerProvider, ThemeProvider, ToastProvider} from '@sanity/ui'
import {buildTheme, type RootTheme} from '@sanity/ui/theme'
import {noop} from 'lodash'
import {type ReactNode, Suspense, useEffect, useState} from 'react'
import {
ChangeConnectorRoot,
Expand All @@ -18,6 +19,8 @@ import {
import {Pane, PaneContent, PaneLayout} from 'sanity/structure'
import {styled} from 'styled-components'

import {route} from '../../../../src/router'
import {RouterProvider} from '../../../../src/router/RouterProvider'
import {createMockSanityClient} from '../../../../test/mocks/mockSanityClient'
import {getMockWorkspace} from '../../../../test/testUtils/getMockWorkspaceFromConfig'

Expand All @@ -36,6 +39,8 @@ const StyledChangeConnectorRoot = styled(ChangeConnectorRoot)`
min-width: 0;
`

const router = route.create('/')

/**
* @description This component is used to wrap all tests in the providers it needs to be able to run successfully.
* It provides a mock Sanity client and a mock workspace.
Expand Down Expand Up @@ -72,37 +77,39 @@ export const TestWrapper = (props: TestWrapperProps): JSX.Element | null => {

return (
<Suspense fallback={null}>
<ThemeProvider theme={studioThemeConfig}>
<ToastProvider>
<LayerProvider>
<WorkspaceProvider workspace={mockWorkspace}>
<ResourceCacheProvider>
<SourceProvider source={mockWorkspace.unstable_sources[0]}>
<CopyPasteProvider>
<ColorSchemeProvider>
<UserColorManagerProvider>
<StyledChangeConnectorRoot
isReviewChangesOpen={false}
onOpenReviewChanges={() => {}}
onSetFocus={() => {}}
>
<PaneLayout height="fill">
<Pane id="test-pane">
<PaneContent>
<Card padding={3}>{children}</Card>
</PaneContent>
</Pane>
</PaneLayout>
</StyledChangeConnectorRoot>
</UserColorManagerProvider>
</ColorSchemeProvider>
</CopyPasteProvider>
</SourceProvider>
</ResourceCacheProvider>
</WorkspaceProvider>
</LayerProvider>
</ToastProvider>
</ThemeProvider>
<RouterProvider router={router} state={{}} onNavigate={noop}>
<ThemeProvider theme={studioThemeConfig}>
<ToastProvider>
<LayerProvider>
<WorkspaceProvider workspace={mockWorkspace}>
<ResourceCacheProvider>
<SourceProvider source={mockWorkspace.unstable_sources[0]}>
<CopyPasteProvider>
<ColorSchemeProvider>
<UserColorManagerProvider>
<StyledChangeConnectorRoot
isReviewChangesOpen={false}
onOpenReviewChanges={() => {}}
onSetFocus={() => {}}
>
<PaneLayout height="fill">
<Pane id="test-pane">
<PaneContent>
<Card padding={3}>{children}</Card>
</PaneContent>
</Pane>
</PaneLayout>
</StyledChangeConnectorRoot>
</UserColorManagerProvider>
</ColorSchemeProvider>
</CopyPasteProvider>
</SourceProvider>
</ResourceCacheProvider>
</WorkspaceProvider>
</LayerProvider>
</ToastProvider>
</ThemeProvider>
</RouterProvider>
</Suspense>
)
}
7 changes: 4 additions & 3 deletions packages/sanity/src/core/bundles/components/BundleMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {type ReactElement, useCallback} from 'react'
import {styled} from 'styled-components'

import {type BundleDocument} from '../../store/bundles/types'
import {usePerspective} from '../hooks/usePerspective'
import {usePerspective} from '../hooks'
import {LATEST} from '../util/const'
import {isDraftOrPublished} from '../util/util'
import {BundleBadge} from './BundleBadge'
Expand All @@ -23,14 +23,15 @@ interface BundleListProps {
bundles: BundleDocument[] | null
loading: boolean
actions?: ReactElement
perspective?: string
}

/**
* @internal
*/
export function BundleMenu(props: BundleListProps): JSX.Element {
const {bundles, loading, actions, button} = props
const {currentGlobalBundle, setPerspective} = usePerspective()
const {bundles, loading, actions, button, perspective} = props
const {currentGlobalBundle, setPerspective} = usePerspective(perspective)

const bundlesToDisplay =
bundles?.filter((bundle) => !isDraftOrPublished(bundle.slug) && !bundle.archivedAt) || []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ interface BundleActionsProps {
documentId: string
documentType: string
documentVersions: BundleDocument[] | null
bundleSlug?: string
}

/**
* @internal
*/
export function BundleActions(props: BundleActionsProps): ReactNode {
const {currentGlobalBundle, documentId, documentType, documentVersions} = props
const {currentGlobalBundle, documentId, documentType, documentVersions, bundleSlug} = props
const {slug, title, archivedAt} = currentGlobalBundle
const documentStore = useDocumentStore()

const [creatingVersion, setCreatingVersion] = useState<boolean>(false)
const [isInVersion, setIsInVersion] = useState<boolean>(false)

const toast = useToast()
const {newVersion} = useDocumentOperation(documentId, documentType)
const {newVersion} = useDocumentOperation(documentId, documentType, bundleSlug)
const {t} = useTranslation()

useEffect(() => {
Expand Down
15 changes: 9 additions & 6 deletions packages/sanity/src/core/bundles/hooks/usePerspective.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,32 @@ export interface PerspectiveValue {
}

/**
* TODO: Improve distinction between global and pane perspectives.
*
* @internal
*/
export function usePerspective(): PerspectiveValue {
export function usePerspective(selectedPerspective?: string): PerspectiveValue {
const router = useRouter()
const {data: bundles} = useBundles()
const perspective = selectedPerspective ?? router.stickyParams.perspective

// TODO: Should it be possible to set the perspective within a pane, rather than globally?
const setPerspective = (slug: string | undefined) => {
if (slug === 'drafts') {
router.navigateStickyParam('perspective', '')
} else {
router.navigateStickyParam('perspective', `bundle.${slug}`)
}
}

const selectedBundle =
router.stickyParams?.perspective && bundles
perspective && bundles
? bundles.find((bundle: Partial<BundleDocument>) => {
return (
`bundle.${bundle.slug}`.toLocaleLowerCase() ===
router.stickyParams.perspective?.toLocaleLowerCase()
)
return `bundle.${bundle.slug}`.toLocaleLowerCase() === perspective?.toLocaleLowerCase()
})
: LATEST

// TODO: Improve naming; this may not be global.
const currentGlobalBundle = selectedBundle || LATEST

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface CommentsProviderProps {
children: ReactNode
documentId: string
documentType: string
perspective?: string
type: CommentsType
sortOrder: 'asc' | 'desc'

Expand Down Expand Up @@ -78,12 +79,19 @@ export const CommentsProvider = memo(function CommentsProvider(props: CommentsPr
selectedCommentId,
isConnecting,
onPathOpen,
perspective,
} = props
const commentsEnabled = useCommentsEnabled()
const [status, setStatus] = useState<CommentStatus>('open')
const {client, createAddonDataset, isCreatingDataset} = useAddonDataset()
const publishedId = getPublishedId(documentId)
const editState = useEditState(publishedId, documentType, 'low')

const bundlePerspective = perspective?.startsWith('bundle.')
? perspective.split('bundle.').at(1)
: undefined

// TODO: Allow versions to have separate comments.
const editState = useEditState(publishedId, documentType, 'default', bundlePerspective)
const schemaType = useSchema().get(documentType)
const currentUser = useCurrentUser()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface DocumentStatusProps {
absoluteDate?: boolean
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
singleLine?: boolean
}

Expand All @@ -26,9 +27,16 @@ const StyledText = styled(Text)`
*
* @internal
*/
export function DocumentStatus({absoluteDate, draft, published, singleLine}: DocumentStatusProps) {
export function DocumentStatus({
absoluteDate,
draft,
published,
version,
singleLine,
}: DocumentStatusProps) {
const {t} = useTranslation()
const draftUpdatedAt = draft && '_updatedAt' in draft ? draft._updatedAt : ''
const versionUpdatedAt = version && '_updatedAt' in version ? version._updatedAt : ''
const publishedUpdatedAt = published && '_updatedAt' in published ? published._updatedAt : ''

const intlDateFormat = useDateTimeFormat({
Expand All @@ -39,6 +47,7 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
const draftDateAbsolute = draftUpdatedAt && intlDateFormat.format(new Date(draftUpdatedAt))
const publishedDateAbsolute =
publishedUpdatedAt && intlDateFormat.format(new Date(publishedUpdatedAt))
const versionDateAbsolute = versionUpdatedAt && intlDateFormat.format(new Date(versionUpdatedAt))

const draftUpdatedTimeAgo = useRelativeTime(draftUpdatedAt || '', {
minimal: true,
Expand All @@ -48,9 +57,15 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
minimal: true,
useTemporalPhrase: true,
})
const versionUpdatedTimeAgo = useRelativeTime(versionUpdatedAt || '', {
minimal: true,
useTemporalPhrase: true,
})

const publishedDate = absoluteDate ? publishedDateAbsolute : publishedUpdatedTimeAgo
const updatedDate = absoluteDate ? draftDateAbsolute : draftUpdatedTimeAgo
const updatedDate = absoluteDate
? versionDateAbsolute || draftDateAbsolute
: versionUpdatedTimeAgo || draftUpdatedTimeAgo

return (
<Flex
Expand All @@ -60,12 +75,12 @@ export function DocumentStatus({absoluteDate, draft, published, singleLine}: Doc
gap={2}
wrap="nowrap"
>
{!publishedDate && (
{!version && !publishedDate && (
<StyledText size={1} weight="medium">
{t('document-status.not-published')}
</StyledText>
)}
{publishedDate && (
{!version && publishedDate && (
<StyledText size={1} weight="medium">
{t('document-status.published', {date: publishedDate})}
</StyledText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {styled} from 'styled-components'
interface DocumentStatusProps {
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
}

const Root = styled(Text)`
Expand All @@ -25,23 +26,26 @@ const Root = styled(Text)`
* - Yellow (caution) for published documents with edits
* - Gray (default) for unpublished documents (with or without edits)
*
* No dot will be displayed for published documents without edits.
* No dot will be displayed for published documents without edits or for version documents.
*
* @internal
*/
export function DocumentStatusIndicator({draft, published}: DocumentStatusProps) {
export function DocumentStatusIndicator({draft, published, version}: DocumentStatusProps) {
const $draft = Boolean(draft)
const $published = Boolean(published)
const $version = Boolean(version)

const status = useMemo(() => {
if ($version) return undefined
if ($draft && !$published) return 'unpublished'
return 'edited'
}, [$draft, $published])
}, [$draft, $published, $version])

// Return null if the document is:
// - Published without edits
// - Neither published or without edits (this shouldn't be possible)
if ((!$draft && !$published) || (!$draft && $published)) {
// - A version
if ((!$draft && !$published) || (!$draft && $published) || $version) {
return null
}

Expand Down
1 change: 1 addition & 0 deletions packages/sanity/src/core/form/FormBuilderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ export interface FormBuilderContextValue {
renderItem: RenderItemCallback
renderPreview: RenderPreviewCallback
schemaType: ObjectSchemaType
version?: string
}
4 changes: 4 additions & 0 deletions packages/sanity/src/core/form/FormBuilderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface FormBuilderProviderProps {
schemaType: ObjectSchemaType
unstable?: Source['form']['unstable']
validation: ValidationMarker[]
version?: string
}

const missingPatchChannel: PatchChannel = {
Expand Down Expand Up @@ -113,6 +114,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
schemaType,
unstable,
validation,
version,
} = props

const __internal: FormBuilderContextValue['__internal'] = useMemo(
Expand Down Expand Up @@ -171,6 +173,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
renderItem,
renderPreview,
schemaType,
version,
}),
[
__internal,
Expand All @@ -191,6 +194,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) {
renderItem,
renderPreview,
schemaType,
version,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {useDidUpdate} from '../../hooks/useDidUpdate'
import {useScrollIntoViewOnFocusWithin} from '../../hooks/useScrollIntoViewOnFocusWithin'
import {set, unset} from '../../patch'
import {type ObjectFieldProps, type RenderPreviewCallback} from '../../types'
import {useFormBuilder} from '../../useFormBuilder'
import {PreviewReferenceValue} from './PreviewReferenceValue'
import {ReferenceFinalizeAlertStrip} from './ReferenceFinalizeAlertStrip'
import {ReferenceLinkCard} from './ReferenceLinkCard'
Expand Down Expand Up @@ -60,6 +61,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
const elementRef = useRef<HTMLDivElement | null>(null)
const {schemaType, path, open, inputId, children, inputProps} = props
const {readOnly, focused, renderPreview, onChange} = props.inputProps
const {version} = useFormBuilder()

const [fieldActionsNodes, setFieldActionNodes] = useState<DocumentFieldActionNode[]>([])
const documentId = usePublishedId()
Expand All @@ -72,6 +74,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
path,
schemaType,
value,
version,
})

// this is here to make sure the item is visible if it's being edited behind a modal
Expand Down
Loading

0 comments on commit e9aa1cd

Please sign in to comment.