Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sanity): support viewing and editing document versions #7251

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dev/test-studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ const defaultWorkspace = {
icon: SanityMonogram,
// eslint-disable-next-line camelcase
__internal_serverDocumentActions: {
enabled: true,
// TODO: Switched off because Actions API doesn't support versions (yet).
enabled: false,
},
scheduledPublishing: {
enabled: true,
Expand Down
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt if we rename this to localPerspective?

Suggested change
export function usePerspective(selectedPerspective?: string): PerspectiveValue {
export function usePerspective(localPerspective?: string): PerspectiveValue {

I like the idea of making it possible to update the local perspective instead of the global one, when you are looking at a local one.
Maybe we could update this hook to require a callback when using a localPerspective, and trigger that callback in the setPerspective function.

This could be a follow up

interface LocalPerspectiveOptions {
  localPerspective: string
  handlePerspectiveChange: (slug: string) => void
}
export function usePerspective(options: LocalPerspectiveOptions | undefined): PerspectiveValue {
  const router = useRouter()
  const {data: bundles} = useBundles()
  const perspective = options?.localPerspective ?? router.stickyParams.perspective

  const setPerspective = (slug: string) => {
    if (options?.handlePerspectiveChange) {
      options.handlePerspectiveChange(slug)
      return // Don't navigate if we're handling the change locally
    }
    if (slug === 'drafts') {
      router.navigateStickyParam('perspective', '')
    } else {
      router.navigateStickyParam('perspective', `bundle.${slug}`)
    }
  }

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

  return {
    setPerspective,
    selectedPerspective: perspective,
    perspectiveBundle: selectedBundle || LATEST,
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Adding this parameter was a bandaid, and I did plan to come back with some more elegant solution. I think your suggestion is definitely much better.

I also wonder whether requiring the local override at all is a bit of a code smell 🤔.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not too happy with this solution either, I was trying to think of a way to remove this, I think it would require a refactor in the bundleMenu given is the only one that needs to work in two cases (local and global).

I was thinking on the option to add another hook in structure to handle only the "pane" perspective and maybe we could rename this one to useGlobalPerspective

They will have a very similar behavior and could expose the same actions, but each one will handle the actions in a different way.

Anyways, I think it could be a follow up and this shouldn't block your changes, seeing that change alone will be easier to identify the best approach.

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,20 +79,27 @@ 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)
juice49 marked this conversation as resolved.
Show resolved Hide resolved
const schemaType = useSchema().get(documentType)
const currentUser = useCurrentUser()

const {name: workspaceName, dataset, projectId} = useWorkspace()

const documentValue = useMemo(() => {
return editState.draft || editState.published
}, [editState.draft, editState.published])
return editState.version || editState.draft || editState.published
}, [editState.version, editState.draft, editState.published])

const documentRevisionId = useMemo(() => documentValue?._rev, [documentValue])

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) {
pedrobonamin marked this conversation as resolved.
Show resolved Hide resolved
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
Loading
Loading