From 159b948742f5aec7bfed8783b48fd14d9d982e21 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 22 Jul 2024 12:33:44 +0100 Subject: [PATCH] feat(sanity): support viewing and editing document versions --- .../tests/formBuilder/utils/TestWrapper.tsx | 69 ++++++++++--------- .../core/bundles/components/BundleMenu.tsx | 7 +- .../components/panes/BundleActions.tsx | 6 +- .../src/core/bundles/hooks/usePerspective.tsx | 15 ++-- .../context/comments/CommentsProvider.tsx | 14 +++- .../documentStatus/DocumentStatus.tsx | 23 +++++-- .../DocumentStatusIndicator.tsx | 12 ++-- .../src/core/form/FormBuilderContext.ts | 1 + .../src/core/form/FormBuilderProvider.tsx | 4 ++ .../inputs/ReferenceInput/ReferenceField.tsx | 3 + .../inputs/ReferenceInput/ReferenceInput.tsx | 5 ++ .../ReferenceInput/ReferencePreview.tsx | 13 +++- .../core/form/inputs/ReferenceInput/types.ts | 2 + .../ReferenceInput/useReferenceInput.tsx | 7 +- .../src/core/form/studio/FormBuilder.tsx | 3 + .../src/core/form/studio/FormProvider.tsx | 3 + .../inputs/client-adapters/reference.ts | 66 ++++++++++++++---- .../inputs/reference/StudioReferenceInput.tsx | 3 + .../sanity/src/core/form/types/fieldProps.ts | 1 + .../src/core/hooks/useConnectionState.ts | 10 ++- .../src/core/hooks/useDocumentOperation.ts | 10 ++- .../sanity/src/core/hooks/useEditState.ts | 7 +- .../sanity/src/core/hooks/useSyncState.ts | 10 ++- .../src/core/hooks/useValidationStatus.ts | 10 ++- .../sanity/src/core/preview/availability.ts | 15 +++- .../core/preview/components/PreviewLoader.tsx | 3 - .../src/core/preview/createPreviewObserver.ts | 2 - .../sanity/src/core/preview/documentPair.ts | 33 +++++++-- .../src/core/preview/documentPreviewStore.ts | 4 +- packages/sanity/src/core/preview/types.ts | 9 +++ .../src/core/preview/useValuePreview.ts | 6 +- .../schedule/ScheduleAction.tsx | 5 +- .../document/document-pair/checkoutPair.ts | 36 +++++++--- .../document/document-pair/editState.ts | 28 +++++++- .../document/document-pair/memoizeKeyGen.ts | 2 +- .../document/document-pair/operationArgs.ts | 18 ++++- .../document-pair/operations/commit.ts | 3 +- .../document-pair/operations/patch.ts | 15 +++- .../document-pair/operations/types.ts | 7 +- .../document/document-pair/remoteSnapshots.ts | 6 +- .../document-pair/serverOperations/patch.ts | 23 ++++++- .../document/document-pair/snapshotPair.ts | 4 +- .../document/document-pair/validation.ts | 6 +- .../store/_legacy/document/document-store.ts | 48 ++++++++----- .../store/_legacy/document/getPairListener.ts | 18 +++-- .../src/core/store/_legacy/document/types.ts | 3 +- .../_legacy/grants/documentPairPermissions.ts | 15 ++-- .../store/_legacy/history/history/Aligner.ts | 4 ++ .../store/_legacy/history/useTimelineStore.ts | 60 +++++++++++++--- .../form/tasksFormBuilder/FormEdit.tsx | 2 + packages/sanity/src/core/util/draftUtils.ts | 22 +++++- .../components/paneItem/PaneItemPreview.tsx | 4 +- .../paneRouter/PaneRouterProvider.tsx | 5 ++ .../structure/components/paneRouter/types.ts | 5 ++ .../structureTool/StructureTitle.tsx | 15 +++- .../structureTool/StructureTool.tsx | 2 +- .../documentActions/DeleteAction.tsx | 12 +++- .../documentActions/DiscardChangesAction.tsx | 4 +- .../documentActions/DuplicateAction.tsx | 6 +- .../documentActions/HistoryRestoreAction.tsx | 10 ++- .../documentActions/PublishAction.tsx | 19 +++-- .../documentActions/UnpublishAction.tsx | 4 +- .../structure/documentBadges/LiveEditBadge.ts | 5 +- .../src/structure/panes/StructureToolPane.tsx | 7 +- .../panes/document/DocumentPaneContext.ts | 5 ++ .../panes/document/DocumentPaneProvider.tsx | 60 +++++++++++----- .../document/comments/CommentsWrapper.tsx | 6 +- .../banners/ReferenceChangedBanner.tsx | 31 +++++++-- .../documentPanel/documentViews/FormView.tsx | 6 +- .../header/DocumentHeaderTitle.tsx | 2 +- .../perspective/DocumentPerspectiveMenu.tsx | 13 ++-- .../DocumentPerspectiveMenu.test.tsx | 25 ++++--- .../document/inspectors/validation/index.ts | 13 +++- .../statusBar/DocumentStatusBarActions.tsx | 13 ++-- .../document/statusBar/DocumentStatusLine.tsx | 2 +- .../structureResolvers/useResolvedPanes.ts | 2 +- .../sanity/test/testUtils/TestProvider.tsx | 36 ++++++---- 77 files changed, 756 insertions(+), 257 deletions(-) diff --git a/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx b/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx index 69f98a55a1c..f0508b4893b 100644 --- a/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx +++ b/packages/sanity/playwright-ct/tests/formBuilder/utils/TestWrapper.tsx @@ -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, @@ -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' @@ -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. @@ -72,37 +77,39 @@ export const TestWrapper = (props: TestWrapperProps): JSX.Element | null => { return ( - - - - - - - - - - {}} - onSetFocus={() => {}} - > - - - - {children} - - - - - - - - - - - - - + + + + + + + + + + + {}} + onSetFocus={() => {}} + > + + + + {children} + + + + + + + + + + + + + + ) } diff --git a/packages/sanity/src/core/bundles/components/BundleMenu.tsx b/packages/sanity/src/core/bundles/components/BundleMenu.tsx index a7e888eefed..c8735d35974 100644 --- a/packages/sanity/src/core/bundles/components/BundleMenu.tsx +++ b/packages/sanity/src/core/bundles/components/BundleMenu.tsx @@ -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' @@ -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) || [] diff --git a/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx b/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx index a8f641eff10..d600557694e 100644 --- a/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx +++ b/packages/sanity/src/core/bundles/components/panes/BundleActions.tsx @@ -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(false) const [isInVersion, setIsInVersion] = useState(false) const toast = useToast() - const {newVersion} = useDocumentOperation(documentId, documentType) + const {newVersion} = useDocumentOperation(documentId, documentType, bundleSlug) const {t} = useTranslation() useEffect(() => { diff --git a/packages/sanity/src/core/bundles/hooks/usePerspective.tsx b/packages/sanity/src/core/bundles/hooks/usePerspective.tsx index 0b812a14689..b8b0733da99 100644 --- a/packages/sanity/src/core/bundles/hooks/usePerspective.tsx +++ b/packages/sanity/src/core/bundles/hooks/usePerspective.tsx @@ -15,12 +15,16 @@ 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', '') @@ -28,16 +32,15 @@ export function usePerspective(): PerspectiveValue { router.navigateStickyParam('perspective', `bundle.${slug}`) } } + const selectedBundle = - router.stickyParams?.perspective && bundles + perspective && bundles ? bundles.find((bundle: Partial) => { - 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 { diff --git a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx index e15c5a4eb0d..901a04cf50e 100644 --- a/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx +++ b/packages/sanity/src/core/comments/context/comments/CommentsProvider.tsx @@ -43,6 +43,7 @@ export interface CommentsProviderProps { children: ReactNode documentId: string documentType: string + perspective?: string type: CommentsType sortOrder: 'asc' | 'desc' @@ -78,20 +79,27 @@ export const CommentsProvider = memo(function CommentsProvider(props: CommentsPr selectedCommentId, isConnecting, onPathOpen, + perspective, } = props const commentsEnabled = useCommentsEnabled() const [status, setStatus] = useState('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() 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]) diff --git a/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx b/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx index 16a5190dbf7..6faa08d66fb 100644 --- a/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx +++ b/packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx @@ -9,6 +9,7 @@ interface DocumentStatusProps { absoluteDate?: boolean draft?: PreviewValue | Partial | null published?: PreviewValue | Partial | null + version?: PreviewValue | Partial | null singleLine?: boolean } @@ -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({ @@ -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, @@ -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 ( - {!publishedDate && ( + {!version && !publishedDate && ( {t('document-status.not-published')} )} - {publishedDate && ( + {!version && publishedDate && ( {t('document-status.published', {date: publishedDate})} diff --git a/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx b/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx index 892f2cc0e57..475844e6364 100644 --- a/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx +++ b/packages/sanity/src/core/components/documentStatusIndicator/DocumentStatusIndicator.tsx @@ -7,6 +7,7 @@ import {styled} from 'styled-components' interface DocumentStatusProps { draft?: PreviewValue | Partial | null published?: PreviewValue | Partial | null + version?: PreviewValue | Partial | null } const Root = styled(Text)` @@ -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 } diff --git a/packages/sanity/src/core/form/FormBuilderContext.ts b/packages/sanity/src/core/form/FormBuilderContext.ts index be5b2f561f6..22c4b6358b0 100644 --- a/packages/sanity/src/core/form/FormBuilderContext.ts +++ b/packages/sanity/src/core/form/FormBuilderContext.ts @@ -58,4 +58,5 @@ export interface FormBuilderContextValue { renderItem: RenderItemCallback renderPreview: RenderPreviewCallback schemaType: ObjectSchemaType + version?: string } diff --git a/packages/sanity/src/core/form/FormBuilderProvider.tsx b/packages/sanity/src/core/form/FormBuilderProvider.tsx index b14969c90a0..50d7fefa30a 100644 --- a/packages/sanity/src/core/form/FormBuilderProvider.tsx +++ b/packages/sanity/src/core/form/FormBuilderProvider.tsx @@ -65,6 +65,7 @@ export interface FormBuilderProviderProps { schemaType: ObjectSchemaType unstable?: Source['form']['unstable'] validation: ValidationMarker[] + version?: string } const missingPatchChannel: PatchChannel = { @@ -113,6 +114,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) { schemaType, unstable, validation, + version, } = props const __internal: FormBuilderContextValue['__internal'] = useMemo( @@ -171,6 +173,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) { renderItem, renderPreview, schemaType, + version, }), [ __internal, @@ -191,6 +194,7 @@ export function FormBuilderProvider(props: FormBuilderProviderProps) { renderItem, renderPreview, schemaType, + version, ], ) diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx index 0fb43121c38..3c11fbc40a0 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx @@ -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' @@ -60,6 +61,7 @@ export function ReferenceField(props: ReferenceFieldProps) { const elementRef = useRef(null) const {schemaType, path, open, inputId, children, inputProps} = props const {readOnly, focused, renderPreview, onChange} = props.inputProps + const {version} = useFormBuilder() const [fieldActionsNodes, setFieldActionNodes] = useState([]) const documentId = usePublishedId() @@ -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 diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx index 2703be12ed6..a254fd43fc2 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceInput.tsx @@ -52,6 +52,7 @@ export function ReferenceInput(props: ReferenceInputProps) { id, onPathFocus, value, + version, renderPreview, path, elementProps, @@ -62,6 +63,7 @@ export function ReferenceInput(props: ReferenceInputProps) { path, schemaType, value, + version, }) const [searchState, setSearchState] = useState(INITIAL_SEARCH_STATE) @@ -187,6 +189,7 @@ export function ReferenceInput(props: ReferenceInputProps) { const renderOption = useCallback( (option: AutocompleteOption) => { + // TODO: Account for checked-out version. const documentId = option.hit.draft?._id || option.hit.published?._id || option.value return ( @@ -205,6 +208,7 @@ export function ReferenceInput(props: ReferenceInputProps) { const renderValue = useCallback(() => { return ( + loadableReferenceInfo.result?.preview.version?.title || loadableReferenceInfo.result?.preview.draft?.title || loadableReferenceInfo.result?.preview.published?.title || '' @@ -212,6 +216,7 @@ export function ReferenceInput(props: ReferenceInputProps) { }, [ loadableReferenceInfo.result?.preview.draft?.title, loadableReferenceInfo.result?.preview.published?.title, + loadableReferenceInfo.result?.preview.version?.title, ]) const handleFocus = useCallback(() => onPathFocus(['_ref']), [onPathFocus]) diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx index 9371f6275a4..c12802a7b2a 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferencePreview.tsx @@ -27,6 +27,7 @@ export function ReferencePreview(props: { const documentPresence = useDocumentPresence(id) const previewId = + preview.version?._id || preview.draft?._id || preview.published?._id || // note: during publish of the referenced document we might have both a missing draft and a missing published version @@ -44,7 +45,7 @@ export function ReferencePreview(props: { [previewId, refType.name], ) - const {draft, published} = preview + const {draft, published, version} = preview const previewProps = useMemo( () => ({ @@ -57,13 +58,17 @@ export function ReferencePreview(props: { )} - + ), layout, schemaType: refType, - tooltip: , + tooltip: , value: previewStub, }), [ @@ -72,10 +77,12 @@ export function ReferencePreview(props: { layout, preview.draft, preview.published, + preview.version, previewStub, published, refType, showTypeLabel, + version, ], ) diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts b/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts index 933afbdb68d..4b8ffe6148f 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/types.ts @@ -18,6 +18,7 @@ export interface ReferenceInfo { preview: { draft: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined published: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined + version: (PreviewValue & {_id: string; _createdAt?: string; _updatedAt?: string}) | undefined } } @@ -76,4 +77,5 @@ export interface ReferenceInputProps onEditReference: (event: EditReferenceEvent) => void getReferenceInfo: (id: string, type: ReferenceSchemaType) => Observable + version?: string } diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx index 5e56965952f..4b9a99d9872 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/useReferenceInput.tsx @@ -28,10 +28,11 @@ interface Options { path: Path schemaType: ReferenceSchemaType value?: Reference + version?: string } export function useReferenceInput(options: Options) { - const {path, schemaType} = options + const {path, schemaType, version} = options const schema = useSchema() const documentPreviewStore = useDocumentPreviewStore() const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} = @@ -113,8 +114,8 @@ export function useReferenceInput(options: Options) { }, [disableNew, initialValueTemplateItems, schemaType.to]) const getReferenceInfo = useCallback( - (id: string) => adapter.getReferenceInfo(documentPreviewStore, id, schemaType), - [documentPreviewStore, schemaType], + (id: string) => adapter.getReferenceInfo(documentPreviewStore, id, schemaType, {version}), + [documentPreviewStore, schemaType, version], ) return { diff --git a/packages/sanity/src/core/form/studio/FormBuilder.tsx b/packages/sanity/src/core/form/studio/FormBuilder.tsx index 98a744aefbc..1d60080dbe2 100644 --- a/packages/sanity/src/core/form/studio/FormBuilder.tsx +++ b/packages/sanity/src/core/form/studio/FormBuilder.tsx @@ -69,6 +69,7 @@ export interface FormBuilderProps schemaType: ObjectSchemaType validation: ValidationMarker[] value: FormDocumentValue | undefined + version?: string } /** @@ -100,6 +101,7 @@ export function FormBuilder(props: FormBuilderProps) { schemaType, validation, value, + version, } = props const handleCollapseField = useCallback( @@ -278,6 +280,7 @@ export function FormBuilder(props: FormBuilderProps) { validation={validation} readOnly={readOnly} schemaType={schemaType} + version={version} > diff --git a/packages/sanity/src/core/form/studio/FormProvider.tsx b/packages/sanity/src/core/form/studio/FormProvider.tsx index 5e1fbc73a29..e43c957105c 100644 --- a/packages/sanity/src/core/form/studio/FormProvider.tsx +++ b/packages/sanity/src/core/form/studio/FormProvider.tsx @@ -55,6 +55,7 @@ export interface FormProviderProps { readOnly?: boolean schemaType: ObjectSchemaType validation: ValidationMarker[] + version?: string } /** @@ -86,6 +87,7 @@ export function FormProvider(props: FormProviderProps) { readOnly, schemaType, validation, + version, } = props const {file, image} = useSource().form @@ -164,6 +166,7 @@ export function FormProvider(props: FormProviderProps) { renderPreview={renderPreview} schemaType={schemaType} validation={validation} + version={version} > {children} diff --git a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts index 23eb962a061..ca90d3bcfb2 100644 --- a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts +++ b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts @@ -31,22 +31,30 @@ export function getReferenceInfo( documentPreviewStore: DocumentPreviewStore, id: string, referenceType: ReferenceSchemaType, + {version}: {version?: string} = {}, ): Observable { - const {publishedId, draftId} = getIdPair(id) + const {publishedId, draftId, versionId} = getIdPair(id, {version}) - const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id) + const pairAvailability$ = documentPreviewStore.unstable_observeDocumentPairAvailability(id, { + version, + }) return pairAvailability$.pipe( switchMap((pairAvailability) => { - if (!pairAvailability.draft.available && !pairAvailability.published.available) { - // combine availability of draft + published + if ( + !pairAvailability.version?.available && + !pairAvailability.draft.available && + !pairAvailability.published.available + ) { + // combine availability of draft + published + version const availability = + pairAvailability.version?.reason === 'PERMISSION_DENIED' || pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND - // short circuit, neither draft nor published is available so no point in trying to get preview + // short circuit, neither draft nor published nor version is available so no point in trying to get preview return of({ id, type: undefined, @@ -54,19 +62,25 @@ export function getReferenceInfo( preview: { draft: undefined, published: undefined, + version: undefined, }, } as const) } const draftRef = {_type: 'reference', _ref: draftId} const publishedRef = {_type: 'reference', _ref: publishedId} + const versionRef = versionId ? {_type: 'reference', _ref: versionId} : undefined const typeName$ = combineLatest([ documentPreviewStore.observeDocumentTypeFromId(draftId), documentPreviewStore.observeDocumentTypeFromId(publishedId), + ...(versionId ? [documentPreviewStore.observeDocumentTypeFromId(versionId)] : []), ]).pipe( - // assume draft + published are always same type - map(([draftTypeName, publishedTypeName]) => draftTypeName || publishedTypeName), + // assume draft + published + version are always same type + map( + ([draftTypeName, publishedTypeName, versionTypeName]) => + versionTypeName || draftTypeName || publishedTypeName, + ), ) return typeName$.pipe( @@ -83,6 +97,7 @@ export function getReferenceInfo( preview: { draft: undefined, published: undefined, + version: undefined, }, } as const) } @@ -98,6 +113,7 @@ export function getReferenceInfo( preview: { draft: undefined, published: undefined, + version: undefined, }, } as const) } @@ -130,21 +146,46 @@ export function getReferenceInfo( startWith(undefined), ) - const value$ = combineLatest([draftPreview$, publishedPreview$]).pipe( - map(([draft, published]) => ({draft, published})), + const versionPreview$ = + versionId && versionRef + ? documentPreviewStore.observePaths(versionRef, previewPaths).pipe( + map((result) => + result + ? { + _id: versionId, + ...prepareForPreview(result, refSchemaType), + } + : undefined, + ), + startWith(undefined), + ) + : undefined + + const value$ = combineLatest([ + draftPreview$, + publishedPreview$, + ...(versionPreview$ ? [versionPreview$] : []), + ]).pipe( + map(([draft, published, versionValue]) => ({ + draft, + published, + ...(versionValue ? {version: versionValue} : {}), + })), ) return value$.pipe( map((value): ReferenceInfo => { const availability = // eslint-disable-next-line no-nested-ternary - pairAvailability.draft.available || pairAvailability.published.available + pairAvailability.version?.available || + pairAvailability.draft.available || + pairAvailability.published.available ? READABLE - : pairAvailability.draft.reason === 'PERMISSION_DENIED' || + : pairAvailability.version?.reason === 'PERMISSION_DENIED' || + pairAvailability.draft.reason === 'PERMISSION_DENIED' || pairAvailability.published.reason === 'PERMISSION_DENIED' ? PERMISSION_DENIED : NOT_FOUND - return { type: typeName, id: publishedId, @@ -152,6 +193,7 @@ export function getReferenceInfo( preview: { draft: isRecord(value.draft) ? value.draft : undefined, published: isRecord(value.published) ? value.published : undefined, + version: isRecord(value.version) ? value.version : undefined, }, } }), diff --git a/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx b/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx index ce26ad5c64f..5b65e387510 100644 --- a/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx +++ b/packages/sanity/src/core/form/studio/inputs/reference/StudioReferenceInput.tsx @@ -25,6 +25,7 @@ import { type EditReferenceEvent, } from '../../../inputs/ReferenceInput/types' import {type ObjectInputProps} from '../../../types' +import {useFormBuilder} from '../../../useFormBuilder' import {useReferenceInputOptions} from '../../contexts' import * as adapter from '../client-adapters/reference' import {resolveUserDefinedFilter} from './resolveUserDefinedFilter' @@ -61,6 +62,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) { const schema = useSchema() const maxFieldDepth = useSearchMaxFieldDepth() const documentPreviewStore = useDocumentPreviewStore() + const {version} = useFormBuilder() const {path, schemaType} = props const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} = useReferenceInputOptions() @@ -192,6 +194,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) { editReferenceLinkComponent={EditReferenceLink} createOptions={createOptions} onEditReference={handleEditReference} + version={version} /> ) } diff --git a/packages/sanity/src/core/form/types/fieldProps.ts b/packages/sanity/src/core/form/types/fieldProps.ts index e530df7bc60..c9095a2141b 100644 --- a/packages/sanity/src/core/form/types/fieldProps.ts +++ b/packages/sanity/src/core/form/types/fieldProps.ts @@ -61,6 +61,7 @@ export interface BaseFieldProps { changed: boolean children: ReactNode renderDefault: (props: FieldProps) => ReactElement + version?: string } /** diff --git a/packages/sanity/src/core/hooks/useConnectionState.ts b/packages/sanity/src/core/hooks/useConnectionState.ts index 2405544535b..b23971a9104 100644 --- a/packages/sanity/src/core/hooks/useConnectionState.ts +++ b/packages/sanity/src/core/hooks/useConnectionState.ts @@ -11,12 +11,16 @@ export type ConnectionState = 'connecting' | 'reconnecting' | 'connected' const INITIAL: ConnectionState = 'connecting' /** @internal */ -export function useConnectionState(publishedDocId: string, docTypeName: string): ConnectionState { +export function useConnectionState( + publishedDocId: string, + docTypeName: string, + {version}: {version?: string} = {}, +): ConnectionState { const documentStore = useDocumentStore() const observable = useMemo( () => - documentStore.pair.documentEvents(publishedDocId, docTypeName).pipe( + documentStore.pair.documentEvents(publishedDocId, docTypeName, version).pipe( map((ev: {type: string}) => ev.type), map((eventType) => eventType !== 'reconnect'), switchMap((isConnected) => @@ -25,7 +29,7 @@ export function useConnectionState(publishedDocId: string, docTypeName: string): startWith(INITIAL as any), distinctUntilChanged(), ), - [docTypeName, documentStore.pair, publishedDocId], + [docTypeName, documentStore.pair, publishedDocId, version], ) return useObservable(observable, INITIAL) } diff --git a/packages/sanity/src/core/hooks/useDocumentOperation.ts b/packages/sanity/src/core/hooks/useDocumentOperation.ts index 4e67302fd06..105de51f1b3 100644 --- a/packages/sanity/src/core/hooks/useDocumentOperation.ts +++ b/packages/sanity/src/core/hooks/useDocumentOperation.ts @@ -4,11 +4,15 @@ import {useObservable} from 'react-rx' import {type OperationsAPI, useDocumentStore} from '../store' /** @internal */ -export function useDocumentOperation(publishedDocId: string, docTypeName: string): OperationsAPI { +export function useDocumentOperation( + publishedDocId: string, + docTypeName: string, + version?: string, +): OperationsAPI { const documentStore = useDocumentStore() const observable = useMemo( - () => documentStore.pair.editOperations(publishedDocId, docTypeName), - [docTypeName, documentStore.pair, publishedDocId], + () => documentStore.pair.editOperations(publishedDocId, docTypeName, version), + [docTypeName, documentStore.pair, publishedDocId, version], ) /** * We know that since the observable has a startWith operator, it will always emit a value diff --git a/packages/sanity/src/core/hooks/useEditState.ts b/packages/sanity/src/core/hooks/useEditState.ts index 6d2650b73cf..771f973f07e 100644 --- a/packages/sanity/src/core/hooks/useEditState.ts +++ b/packages/sanity/src/core/hooks/useEditState.ts @@ -9,12 +9,13 @@ export function useEditState( publishedDocId: string, docTypeName: string, priority: 'default' | 'low' = 'default', + version?: string, ): EditStateFor { const documentStore = useDocumentStore() const observable = useMemo(() => { if (priority === 'low') { - const base = documentStore.pair.editState(publishedDocId, docTypeName).pipe(share()) + const base = documentStore.pair.editState(publishedDocId, docTypeName, version).pipe(share()) return merge( base.pipe(take(1)), @@ -25,8 +26,8 @@ export function useEditState( ) } - return documentStore.pair.editState(publishedDocId, docTypeName) - }, [docTypeName, documentStore.pair, priority, publishedDocId]) + return documentStore.pair.editState(publishedDocId, docTypeName, version) + }, [docTypeName, documentStore.pair, priority, publishedDocId, version]) /** * We know that since the observable has a startWith operator, it will always emit a value * and that's why the non-null assertion is used here diff --git a/packages/sanity/src/core/hooks/useSyncState.ts b/packages/sanity/src/core/hooks/useSyncState.ts index 385d3205a25..65888a19558 100644 --- a/packages/sanity/src/core/hooks/useSyncState.ts +++ b/packages/sanity/src/core/hooks/useSyncState.ts @@ -14,15 +14,19 @@ const SYNCING = {isSyncing: true} const NOT_SYNCING = {isSyncing: false} /** @internal */ -export function useSyncState(publishedDocId: string, documentType: string): SyncState { +export function useSyncState( + publishedDocId: string, + documentType: string, + {version}: {version?: string} = {}, +): SyncState { const documentStore = useDocumentStore() const observable = useMemo( () => documentStore.pair - .consistencyStatus(publishedDocId, documentType) + .consistencyStatus(publishedDocId, documentType, version) .pipe(map((isConsistent) => (isConsistent ? NOT_SYNCING : SYNCING))), - [documentStore.pair, documentType, publishedDocId], + [documentStore.pair, documentType, publishedDocId, version], ) return useObservable>(observable, NOT_SYNCING) } diff --git a/packages/sanity/src/core/hooks/useValidationStatus.ts b/packages/sanity/src/core/hooks/useValidationStatus.ts index 165e059d932..2411b0eabe8 100644 --- a/packages/sanity/src/core/hooks/useValidationStatus.ts +++ b/packages/sanity/src/core/hooks/useValidationStatus.ts @@ -7,12 +7,16 @@ import {type ValidationStatus} from '../validation' const INITIAL: ValidationStatus = {validation: [], isValidating: false} /** @internal */ -export function useValidationStatus(publishedDocId: string, docTypeName: string): ValidationStatus { +export function useValidationStatus( + publishedDocId: string, + docTypeName: string, + version?: string, +): ValidationStatus { const documentStore = useDocumentStore() const observable = useMemo( - () => documentStore.pair.validation(publishedDocId, docTypeName), - [docTypeName, documentStore.pair, publishedDocId], + () => documentStore.pair.validation(publishedDocId, docTypeName, version), + [docTypeName, documentStore.pair, publishedDocId, version], ) return useObservable(observable, INITIAL) } diff --git a/packages/sanity/src/core/preview/availability.ts b/packages/sanity/src/core/preview/availability.ts index d339ff3d7aa..160beee56a6 100644 --- a/packages/sanity/src/core/preview/availability.ts +++ b/packages/sanity/src/core/preview/availability.ts @@ -71,25 +71,36 @@ export function createPreviewAvailabilityObserver( versionedClient: SanityClient, observePaths: ObservePathsFn, ): { - observeDocumentPairAvailability(id: string): Observable + observeDocumentPairAvailability( + id: string, + options?: {version?: string}, + ): Observable } { /** * Returns an observable of metadata for a given drafts model document */ function observeDocumentPairAvailability( id: string, + {version}: {version?: string} = {}, ): Observable { const draftId = getDraftId(id) const publishedId = getPublishedId(id) + const versionId = version ? [version, publishedId].join('.') : undefined return combineLatest([ observeDocumentAvailability(draftId), observeDocumentAvailability(publishedId), + ...(versionId ? [observeDocumentAvailability(versionId)] : []), ]).pipe( distinctUntilChanged(shallowEquals), - map(([draftReadability, publishedReadability]) => { + map(([draftReadability, publishedReadability, versionReadability]) => { return { draft: draftReadability, published: publishedReadability, + ...(versionReadability + ? { + version: versionReadability, + } + : {}), } }), ) diff --git a/packages/sanity/src/core/preview/components/PreviewLoader.tsx b/packages/sanity/src/core/preview/components/PreviewLoader.tsx index a9e03420d9c..414ab966964 100644 --- a/packages/sanity/src/core/preview/components/PreviewLoader.tsx +++ b/packages/sanity/src/core/preview/components/PreviewLoader.tsx @@ -6,7 +6,6 @@ import { useMemo, useState, } from 'react' -import {useRouter} from 'sanity/router' import {type PreviewProps} from '../../components' import {type RenderPreviewCallbackProps} from '../../form' @@ -39,7 +38,6 @@ export function PreviewLoader( ...restProps } = props - const perspective = useRouter().stickyParams.perspective const {t} = useTranslation() const [element, setElement] = useState(null) @@ -53,7 +51,6 @@ export function PreviewLoader( const preview = useValuePreview({ enabled: skipVisibilityCheck || isVisible, schemaType, - perspective, value, }) diff --git a/packages/sanity/src/core/preview/createPreviewObserver.ts b/packages/sanity/src/core/preview/createPreviewObserver.ts index 241bbe7b589..3b3338adef6 100644 --- a/packages/sanity/src/core/preview/createPreviewObserver.ts +++ b/packages/sanity/src/core/preview/createPreviewObserver.ts @@ -34,7 +34,6 @@ export function createPreviewObserver(context: { value: Previewable, type: PreviewableType, options?: { - perspective?: string viewOptions?: PrepareViewOptions apiConfig?: ApiConfig }, @@ -45,7 +44,6 @@ export function createPreviewObserver(context: { value: Previewable, type: PreviewableType, options?: { - perspective?: string viewOptions?: PrepareViewOptions apiConfig?: ApiConfig }, diff --git a/packages/sanity/src/core/preview/documentPair.ts b/packages/sanity/src/core/preview/documentPair.ts index 6c8e682258c..6b5fbabe426 100644 --- a/packages/sanity/src/core/preview/documentPair.ts +++ b/packages/sanity/src/core/preview/documentPair.ts @@ -14,6 +14,7 @@ export function create_preview_documentPair( observePathsDocumentPair: ( id: string, paths: PreviewPath[], + options?: {version?: string}, ) => Observable> } { const {observeDocumentPairAvailability} = createPreviewAvailabilityObserver( @@ -21,17 +22,23 @@ export function create_preview_documentPair( observePaths, ) - const ALWAYS_INCLUDED_SNAPSHOT_PATHS: PreviewPath[] = [['_updatedAt'], ['_createdAt'], ['_type']] + const ALWAYS_INCLUDED_SNAPSHOT_PATHS: PreviewPath[] = [ + ['_updatedAt'], + ['_createdAt'], + ['_type'], + ['_version'], + ] return {observePathsDocumentPair} function observePathsDocumentPair( id: string, paths: PreviewPath[], + {version}: {version?: string} = {}, ): Observable> { - const {draftId, publishedId} = getIdPair(id) + const {draftId, publishedId, versionId} = getIdPair(id, {version}) - return observeDocumentPairAvailability(draftId).pipe( + return observeDocumentPairAvailability(draftId, {version}).pipe( switchMap((availability) => { if (!availability.draft.available && !availability.published.available) { // short circuit, neither draft nor published is available so no point in trying to get a snapshot @@ -46,6 +53,14 @@ export function create_preview_documentPair( availability: availability.published, snapshot: undefined, }, + ...(availability.version + ? { + version: { + availability: availability.version, + snapshot: undefined, + }, + } + : {}), }) } @@ -54,10 +69,12 @@ export function create_preview_documentPair( return combineLatest([ observePaths({_type: 'reference', _ref: draftId}, snapshotPaths), observePaths({_type: 'reference', _ref: publishedId}, snapshotPaths), + ...(version ? [observePaths({_type: 'reference', _ref: versionId}, snapshotPaths)] : []), ]).pipe( - map(([draftSnapshot, publishedSnapshot]) => { + map(([draftSnapshot, publishedSnapshot, versionSnapshot]) => { // note: assume type is always the same const type = + (isRecord(versionSnapshot) && '_type' in versionSnapshot && versionSnapshot._type) || (isRecord(draftSnapshot) && '_type' in draftSnapshot && draftSnapshot._type) || (isRecord(publishedSnapshot) && '_type' in publishedSnapshot && @@ -75,6 +92,14 @@ export function create_preview_documentPair( availability: availability.published, snapshot: publishedSnapshot as T, }, + ...(availability.version + ? { + version: { + availability: availability.version, + snapshot: versionSnapshot as T, + }, + } + : {}), } }), ) diff --git a/packages/sanity/src/core/preview/documentPreviewStore.ts b/packages/sanity/src/core/preview/documentPreviewStore.ts index e1fbd9f4d6c..7f4343819ef 100644 --- a/packages/sanity/src/core/preview/documentPreviewStore.ts +++ b/packages/sanity/src/core/preview/documentPreviewStore.ts @@ -34,7 +34,7 @@ import { export type ObserveForPreviewFn = ( value: Previewable, type: PreviewableType, - options?: {viewOptions?: PrepareViewOptions; apiConfig?: ApiConfig; perspective?: string}, + options?: {viewOptions?: PrepareViewOptions; apiConfig?: ApiConfig}, ) => Observable /** @@ -57,11 +57,13 @@ export interface DocumentPreviewStore { */ unstable_observeDocumentPairAvailability: ( id: string, + options?: {version?: string}, ) => Observable unstable_observePathsDocumentPair: ( id: string, paths: PreviewPath[], + options?: {version?: string}, ) => Observable> /** diff --git a/packages/sanity/src/core/preview/types.ts b/packages/sanity/src/core/preview/types.ts index dbd82de69fa..91ccb78a2a8 100644 --- a/packages/sanity/src/core/preview/types.ts +++ b/packages/sanity/src/core/preview/types.ts @@ -94,6 +94,11 @@ export interface DraftsModelDocumentAvailability { * document readability for the draft document */ draft: DocumentAvailability + + /** + * document readability for the version document + */ + version?: DocumentAvailability } /** @@ -110,6 +115,10 @@ export interface DraftsModelDocument>(() => { if (!enabled || !previewValue || !schemaType) return of(PENDING_STATE) return observeForPreview(previewValue as Previewable, schemaType, { - perspective, viewOptions: {ordering: ordering}, }).pipe( map((event) => ({isLoading: false, value: event.snapshot || undefined})), catchError((error) => of({isLoading: false, error})), ) - }, [enabled, previewValue, schemaType, observeForPreview, perspective, ordering]) + }, [enabled, previewValue, schemaType, observeForPreview, ordering]) return useObservable(observable, INITIAL_STATE) } diff --git a/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx b/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx index fc704e80e2f..67b773ece53 100644 --- a/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx +++ b/packages/sanity/src/core/scheduledPublishing/plugin/documentActions/schedule/ScheduleAction.tsx @@ -44,12 +44,13 @@ const debug = debugWithName('ScheduleAction') * @beta */ export const ScheduleAction = (props: DocumentActionProps): DocumentActionDescription | null => { - const {draft, id, liveEdit, onComplete, published, type} = props - + const {draft, id, liveEdit, onComplete, published, type, bundleSlug} = props const currentUser = useCurrentUser() + const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'publish', }) const {createSchedule} = useScheduleOperation() diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts index 72f788086a5..491c94a42d8 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/checkoutPair.ts @@ -27,7 +27,7 @@ const isMutationEventForDocId = /** * @hidden * @beta */ -export type WithVersion = T & {version: 'published' | 'draft'} +export type WithVersion = T & {version: 'published' | 'draft' | 'version'} /** * @hidden @@ -60,16 +60,17 @@ export interface DocumentVersion { /** * @hidden * @beta */ -export interface Pair { +export type Pair = { /** @internal */ transactionsPendingEvents$: Observable published: DocumentVersion draft: DocumentVersion + version?: DocumentVersion complete: () => void } -function setVersion(version: 'draft' | 'published') { - return (ev: T): T & {version: 'draft' | 'published'} => ({...ev, version}) +function setVersion(version: 'draft' | 'published' | 'version') { + return (ev: T): T & {version: 'draft' | 'published' | 'version'} => ({...ev, version}) } function requireId( @@ -183,7 +184,7 @@ export function checkoutPair( idPair: IdPair, serverActionsEnabled: Observable, ): Pair { - const {publishedId, draftId} = idPair + const {publishedId, draftId, versionId} = idPair const listenerEventsConnector = new Subject() const listenerEvents$ = getPairListener(client, idPair).pipe( @@ -199,6 +200,14 @@ export function checkoutPair( listenerEvents$.pipe(filter(isMutationEventForDocId(draftId))), ) + const version = + typeof versionId === 'undefined' + ? undefined + : createBufferedDocument( + versionId, + listenerEvents$.pipe(filter(isMutationEventForDocId(versionId))), + ) + const published = createBufferedDocument( publishedId, listenerEvents$.pipe(filter(isMutationEventForDocId(publishedId))), @@ -209,7 +218,11 @@ export function checkoutPair( filter((ev): ev is PendingMutationsEvent => ev.type === 'pending'), ) - const commits$ = merge(draft.commitRequest$, published.commitRequest$).pipe( + const commits$ = merge( + draft.commitRequest$, + published.commitRequest$, + version ? version.commitRequest$ : EMPTY, + ).pipe( mergeMap((commitRequest) => serverActionsEnabled.pipe( take(1), @@ -227,13 +240,20 @@ export function checkoutPair( draft: { ...draft, events: merge(commits$, reconnect$, draft.events).pipe(map(setVersion('draft'))), - consistency$: draft.consistency$, remoteSnapshot$: draft.remoteSnapshot$.pipe(map(setVersion('draft'))), }, + ...(typeof version === 'undefined' + ? {} + : { + version: { + ...version, + events: merge(commits$, reconnect$, version.events).pipe(map(setVersion('version'))), + remoteSnapshot$: version.remoteSnapshot$.pipe(map(setVersion('version'))), + }, + }), published: { ...published, events: merge(commits$, reconnect$, published.events).pipe(map(setVersion('published'))), - consistency$: published.consistency$, remoteSnapshot$: published.remoteSnapshot$.pipe(map(setVersion('published'))), }, complete: () => listenerEventsConnector.complete(), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts index adba9e7588f..6abf9c8094b 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/editState.ts @@ -22,8 +22,23 @@ export interface EditStateFor { transactionSyncLock: TransactionSyncLockState | null draft: SanityDocument | null published: SanityDocument | null + version: SanityDocument | null + /** + * Whether live edit is enabled. This may be true for various reasons: + * + * - The schema type has live edit enabled. + * - A version of the document is checked out. + */ liveEdit: boolean + /** + * Whether the schema type has live edit enabled. + */ + liveEditSchemaType: boolean ready: boolean + /** + * When editing a version, the slug of the bundle the document belongs to. + */ + bundleSlug?: string } const LOCKED: TransactionSyncLockState = {enabled: true} const NOT_LOCKED: TransactionSyncLockState = {enabled: false} @@ -39,7 +54,9 @@ export const editState = memoize( idPair: IdPair, typeName: string, ): Observable => { - const liveEdit = isLiveEditEnabled(ctx.schema, typeName) + const liveEditSchemaType = isLiveEditEnabled(ctx.schema, typeName) + const liveEdit = typeof idPair.versionId !== 'undefined' || liveEditSchemaType + return snapshotPair(ctx.client, idPair, typeName, ctx.serverActionsEnabled).pipe( switchMap((versions) => combineLatest([ @@ -49,25 +66,32 @@ export const editState = memoize( map((ev: PendingMutationsEvent) => (ev.phase === 'begin' ? LOCKED : NOT_LOCKED)), startWith(NOT_LOCKED), ), + ...(typeof versions.version === 'undefined' ? [] : [versions.version.snapshots$]), ]), ), - map(([draftSnapshot, publishedSnapshot, transactionSyncLock]) => ({ + map(([draftSnapshot, publishedSnapshot, transactionSyncLock, versionSnapshot]) => ({ id: idPair.publishedId, type: typeName, draft: draftSnapshot, published: publishedSnapshot, + version: typeof idPair.versionId === 'undefined' ? null : versionSnapshot, liveEdit, + liveEditSchemaType, ready: true, transactionSyncLock, + bundleSlug: idPair.versionId?.split('.').at(0), })), startWith({ id: idPair.publishedId, type: typeName, draft: null, published: null, + version: null, liveEdit, + liveEditSchemaType, ready: false, transactionSyncLock: null, + bundleSlug: idPair.versionId?.split('.').at(0), }), publishReplay(1), refCount(), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts index 25ff846510d..258a58408bc 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/memoizeKeyGen.ts @@ -4,5 +4,5 @@ import {type IdPair} from '../types' export function memoizeKeyGen(client: SanityClient, idPair: IdPair, typeName: string) { const config = client.config() - return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${typeName}` + return `${config.dataset ?? ''}-${config.projectId ?? ''}-${idPair.publishedId}-${idPair.versionId ?? ''}-${typeName}` } diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts index de0b0b5ab29..397d308383a 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operationArgs.ts @@ -30,14 +30,28 @@ export const operationArgs = memoize( versions.draft.snapshots$, versions.published.snapshots$, ctx.serverActionsEnabled, + ...(typeof versions.version === 'undefined' ? [] : [versions.version.snapshots$]), ]).pipe( map( - ([draft, published, canUseServerActions]): OperationArgs => ({ + ([draft, published, canUseServerActions, version]): OperationArgs => ({ ...ctx, serverActionsEnabled: canUseServerActions, idPair, typeName, - snapshots: {draft, published}, + snapshots: { + published, + draft, + ...(version + ? { + version, + } + : {}), + }, + ...(versions.version + ? { + version: versions.version, + } + : {}), draft: versions.draft, published: versions.published, }), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts index 570af1eea09..45d90538c20 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/commit.ts @@ -4,7 +4,8 @@ import {type OperationImpl} from './types' export const commit: OperationImpl = { disabled: (): false => false, - execute: ({draft, published}) => { + execute: ({draft, published, version}) => { + version?.commit() draft.commit() published.commit() // note: we might be able to connect with the outgoing commit request stream here diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts index f67b96767ec..bb4740ebef4 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/patch.ts @@ -5,10 +5,23 @@ import {type OperationImpl} from './types' export const patch: OperationImpl<[patches: any[], initialDocument?: Record]> = { disabled: (): false => false, execute: ( - {schema, snapshots, idPair, draft, published, typeName}, + {schema, snapshots, idPair, draft, published, version, typeName}, patches = [], initialDocument, ): void => { + // TODO: This is exactly the same strategy as live-editing. Can we avoid duplication? + if (version) { + // No drafting, so patch and commit the published document + version.mutate([ + version.createIfNotExists({ + _type: typeName, + ...initialDocument, + }), + ...version.patch(patches), + ]) + return + } + if (isLiveEditEnabled(schema, typeName)) { // No drafting, so patch and commit the published document published.mutate([ diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts index 62615742dd5..df823865e5c 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/types.ts @@ -47,8 +47,13 @@ export interface OperationArgs { schema: Schema typeName: string idPair: IdPair - snapshots: {draft: null | SanityDocument; published: null | SanityDocument} + snapshots: { + draft: null | SanityDocument + published: null | SanityDocument + version?: null | SanityDocument + } draft: DocumentVersionSnapshots published: DocumentVersionSnapshots + version?: DocumentVersionSnapshots serverActionsEnabled: boolean } diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts index 91db8f91fde..e280c196263 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/remoteSnapshots.ts @@ -1,5 +1,5 @@ import {type SanityClient} from '@sanity/client' -import {merge, type Observable} from 'rxjs' +import {EMPTY, merge, type Observable} from 'rxjs' import {switchMap} from 'rxjs/operators' import {type IdPair} from '../types' @@ -17,7 +17,9 @@ export const remoteSnapshots = memoize( serverActionsEnabled: Observable, ): Observable => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( - switchMap(({published, draft}) => merge(published.remoteSnapshot$, draft.remoteSnapshot$)), + switchMap(({published, draft, version}) => + merge(published.remoteSnapshot$, draft.remoteSnapshot$, version?.remoteSnapshot$ ?? EMPTY), + ), ) }, memoizeKeyGen, diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts index 4fce51f99a3..99f056a02f6 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/serverOperations/patch.ts @@ -4,10 +4,31 @@ import {isLiveEditEnabled} from '../utils/isLiveEditEnabled' export const patch: OperationImpl<[patches: any[], initialDocument?: Record]> = { disabled: (): false => false, execute: ( - {schema, snapshots, idPair, draft, published, typeName}, + {schema, snapshots, idPair, draft, published, version, typeName}, patches = [], initialDocument, ): void => { + // TODO: Actions API can't edit versions (yet). + // TODO: This is exactly the same strategy as live-editing. Can we avoid duplication? + if (version) { + // No drafting, so patch and commit the published document + const patchMutation = version.patch(patches) + // Note: if the document doesn't exist on the server yet, we need to create it first. We only want to do this if we can't see it locally + // if it's been deleted on the server we want that to become a mutation error when submitting. + const mutations = snapshots.version + ? patchMutation + : [ + version.createIfNotExists({ + _type: typeName, + ...initialDocument, + }), + ] + // No drafting, so patch and commit the published document + version.mutate(mutations) + + return + } + if (isLiveEditEnabled(schema, typeName)) { // No drafting, so patch and commit the published document const patchMutation = published.patch(patches) diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts index 0efc43a5461..4fb0d8cdf08 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/snapshotPair.ts @@ -57,6 +57,7 @@ interface SnapshotPair { transactionsPendingEvents$: Observable draft: DocumentVersionSnapshots published: DocumentVersionSnapshots + version?: DocumentVersionSnapshots } /** @internal */ @@ -68,11 +69,12 @@ export const snapshotPair = memoize( serverActionsEnabled: Observable, ): Observable => { return memoizedPair(client, idPair, typeName, serverActionsEnabled).pipe( - map(({published, draft, transactionsPendingEvents$}): SnapshotPair => { + map(({published, draft, version, transactionsPendingEvents$}): SnapshotPair => { return { transactionsPendingEvents$, published: withSnapshots(published), draft: withSnapshots(draft), + ...(version ? {version: withSnapshots(version)} : {}), } }), publishReplay(1), diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts index 8cf895fe80a..94c0e865b7b 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/validation.ts @@ -32,11 +32,11 @@ export const validation = memoize( i18n: LocaleSource serverActionsEnabled: Observable }, - {draftId, publishedId}: IdPair, + {draftId, publishedId, versionId}: IdPair, typeName: string, ): Observable => { - const document$ = editState(ctx, {draftId, publishedId}, typeName).pipe( - map(({draft, published}) => draft || published), + const document$ = editState(ctx, {draftId, publishedId, versionId}, typeName).pipe( + map(({version, draft, published}) => version || draft || published), throttleTime(DOC_UPDATE_DELAY, asyncScheduler, {trailing: true}), distinctUntilChanged((prev, next) => { if (prev?._rev === next?._rev) { diff --git a/packages/sanity/src/core/store/_legacy/document/document-store.ts b/packages/sanity/src/core/store/_legacy/document/document-store.ts index d40fba36ce4..ea52b3857c9 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-store.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-store.ts @@ -8,7 +8,7 @@ import {type LocaleSource} from '../../../i18n' import {type DocumentPreviewStore} from '../../../preview' import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient' import {type Template} from '../../../templates' -import {getDraftId, isDraftId} from '../../../util' +import {getIdPair, isDraftId} from '../../../util' import {type ValidationStatus} from '../../../validation' import {type HistoryStore} from '../history' import {checkoutPair, type DocumentVersionEvent, type Pair} from './document-pair/checkoutPair' @@ -33,12 +33,12 @@ import {type IdPair} from './types' * @beta */ export type QueryParams = Record -function getIdPairFromPublished(publishedId: string): IdPair { +function getIdPairFromPublished(publishedId: string, version?: string): IdPair { if (isDraftId(publishedId)) { throw new Error('editOpsOf does not expect a draft id.') } - return {publishedId, draftId: getDraftId(publishedId)} + return getIdPair(publishedId, {version}) } /** @@ -59,17 +59,29 @@ export interface DocumentStore { resolveTypeForDocument: (id: string, specifiedType?: string) => Observable pair: { - consistencyStatus: (publishedId: string, type: string) => Observable + consistencyStatus: (publishedId: string, type: string, version?: string) => Observable /** @internal */ - documentEvents: (publishedId: string, type: string) => Observable + documentEvents: ( + publishedId: string, + type: string, + version?: string, + ) => Observable /** @internal */ - editOperations: (publishedId: string, type: string) => Observable - editState: (publishedId: string, type: string) => Observable + editOperations: ( + publishedId: string, + type: string, + version?: string, + ) => Observable + editState: (publishedId: string, type: string, version?: string) => Observable operationEvents: ( publishedId: string, type: string, ) => Observable - validation: (publishedId: string, type: string) => Observable + validation: ( + publishedId: string, + type: string, + version?: string, + ) => Observable } } @@ -133,27 +145,27 @@ export function createDocumentStore({ return resolveTypeForDocument(client, id, specifiedType) }, pair: { - consistencyStatus(publishedId, type) { + consistencyStatus(publishedId, type, version) { return consistencyStatus( ctx.client, - getIdPairFromPublished(publishedId), + getIdPairFromPublished(publishedId, version), type, serverActionsEnabled, ) }, - documentEvents(publishedId, type) { + documentEvents(publishedId, type, version) { return documentEvents( ctx.client, - getIdPairFromPublished(publishedId), + getIdPairFromPublished(publishedId, version), type, serverActionsEnabled, ) }, - editOperations(publishedId, type) { - return editOperations(ctx, getIdPairFromPublished(publishedId), type) + editOperations(publishedId, type, version) { + return editOperations(ctx, getIdPairFromPublished(publishedId, version), type) }, - editState(publishedId, type) { - return editState(ctx, getIdPairFromPublished(publishedId), type) + editState(publishedId, type, version) { + return editState(ctx, getIdPairFromPublished(publishedId, version), type) }, operationEvents(publishedId, type) { return operationEvents({ @@ -174,8 +186,8 @@ export function createDocumentStore({ }), ) }, - validation(publishedId, type) { - return validation(ctx, getIdPairFromPublished(publishedId), type) + validation(publishedId, type, version) { + return validation(ctx, getIdPairFromPublished(publishedId, version), type) }, }, } diff --git a/packages/sanity/src/core/store/_legacy/document/getPairListener.ts b/packages/sanity/src/core/store/_legacy/document/getPairListener.ts index aadbc5384c0..2e77b22203a 100644 --- a/packages/sanity/src/core/store/_legacy/document/getPairListener.ts +++ b/packages/sanity/src/core/store/_legacy/document/getPairListener.ts @@ -16,6 +16,7 @@ import { interface Snapshots { draft: SanityDocument | null published: SanityDocument | null + version: SanityDocument | null } /** @internal */ @@ -64,14 +65,14 @@ export function getPairListener( idPair: IdPair, options: PairListenerOptions = {}, ): Observable { - const {publishedId, draftId} = idPair + const {publishedId, draftId, versionId} = idPair + return defer( () => client.observable.listen( - `*[_id == $publishedId || _id == $draftId]`, + `*[_id in $ids]`, { - publishedId, - draftId, + ids: [publishedId, draftId, versionId].filter((id) => typeof id !== 'undefined'), }, { includeResult: false, @@ -87,6 +88,7 @@ export function getPairListener( concatMap((snapshots) => [ createSnapshotEvent(draftId, snapshots.draft), createSnapshotEvent(publishedId, snapshots.published), + ...(versionId ? [createSnapshotEvent(versionId, snapshots.version)] : []), ]), ) : observableOf(event), @@ -131,11 +133,15 @@ export function getPairListener( function fetchInitialDocumentSnapshots(): Observable { return client.observable - .getDocuments([draftId, publishedId], {tag: 'document.snapshots'}) + .getDocuments( + [publishedId, draftId, versionId].filter((id): id is string => typeof id === 'string'), + {tag: 'document.snapshots'}, + ) .pipe( - map(([draft, published]) => ({ + map(([published, draft, version]) => ({ draft, published, + version, })), ) } diff --git a/packages/sanity/src/core/store/_legacy/document/types.ts b/packages/sanity/src/core/store/_legacy/document/types.ts index 0ad2cf6e0a1..0891e85787f 100644 --- a/packages/sanity/src/core/store/_legacy/document/types.ts +++ b/packages/sanity/src/core/store/_legacy/document/types.ts @@ -34,7 +34,8 @@ export interface PendingMutationsEvent { } /** @internal */ -export interface IdPair { +export type IdPair = { draftId: string publishedId: string + versionId?: string } diff --git a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts index 5c84b123398..a4f4185f489 100644 --- a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts +++ b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts @@ -10,6 +10,7 @@ import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient' import { createHookFromObservableFactory, getDraftId, + getIdPair, getPublishedId, type PartialExcept, } from '../../../util' @@ -168,6 +169,7 @@ export interface DocumentPairPermissionsOptions { grantsStore: GrantsStore id: string type: string + version?: string permission: DocumentPermission serverActionsEnabled: Observable } @@ -187,6 +189,7 @@ export function getDocumentPairPermissions({ permission, type, serverActionsEnabled, + version, }: DocumentPairPermissionsOptions): Observable { // this case was added to fix a crash that would occur if the `schemaType` was // omitted from `S.documentList()` @@ -199,12 +202,7 @@ export function getDocumentPairPermissions({ const liveEdit = Boolean(getSchemaType(schema, type).liveEdit) - return snapshotPair( - client, - {draftId: getDraftId(id), publishedId: getPublishedId(id)}, - type, - serverActionsEnabled, - ).pipe( + return snapshotPair(client, getIdPair(id, {version}), type, serverActionsEnabled).pipe( switchMap((pair) => combineLatest([pair.draft.snapshots$, pair.published.snapshots$]).pipe( map(([draft, published]) => ({draft, published})), @@ -279,6 +277,7 @@ export const useDocumentPairPermissionsFromHookFactory = createHookFromObservabl export function useDocumentPairPermissions({ id, type, + version, permission, client: overrideClient, schema: overrideSchema, @@ -306,8 +305,8 @@ export function useDocumentPairPermissions({ return useDocumentPairPermissionsFromHookFactory( useMemo( - () => ({client, schema, grantsStore, id, permission, type, serverActionsEnabled}), - [client, grantsStore, id, permission, schema, type, serverActionsEnabled], + () => ({client, schema, grantsStore, id, permission, type, serverActionsEnabled, version}), + [client, grantsStore, id, permission, schema, type, serverActionsEnabled, version], ), ) } diff --git a/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts b/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts index b3f0df10b17..d02e6e589f0 100644 --- a/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts +++ b/packages/sanity/src/core/store/_legacy/history/history/Aligner.ts @@ -101,6 +101,10 @@ export class Aligner { } appendRemoteSnapshotEvent(evt: RemoteSnapshotVersionEvent): void { + if (evt.version === 'version') { + return + } + const state = this._states[evt.version] if (evt.type === 'snapshot') { diff --git a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts index 403a755fbb7..1cf5d2b1594 100644 --- a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts +++ b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts @@ -6,14 +6,19 @@ import { catchError, distinctUntilChanged, map, + type Observable, of, type Subscription, + switchMap, tap, } from 'rxjs' import { type Annotation, type Chunk, + DRAFTS_FOLDER, + remoteSnapshots, + type RemoteSnapshotVersionEvent, type SelectionState, type TimelineController, useHistoryStore, @@ -21,7 +26,6 @@ import { } from '../../..' import {useClient} from '../../../hooks' import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient' -import {remoteSnapshots, type RemoteSnapshotVersionEvent} from '../document' import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle' interface UseTimelineControllerOpts { @@ -30,6 +34,7 @@ interface UseTimelineControllerOpts { onError?: (err: Error) => void rev?: string since?: string + version?: string } /** @internal */ @@ -97,6 +102,7 @@ export function useTimelineStore({ onError, rev, since, + version, }: UseTimelineControllerOpts): TimelineStore { const historyStore = useHistoryStore() const snapshotsSubscriptionRef = useRef(null) @@ -111,10 +117,10 @@ export function useTimelineStore({ () => historyStore.getTimelineController({ client, - documentId, + documentId: version ? [version, documentId].join('.') : documentId, documentType, }), - [client, documentId, documentType, historyStore], + [client, documentId, documentType, historyStore, version], ) /** @@ -173,12 +179,22 @@ export function useTimelineStore({ if (!snapshotsSubscriptionRef.current) { snapshotsSubscriptionRef.current = remoteSnapshots( client, - {draftId: `drafts.${documentId}`, publishedId: documentId}, + { + draftId: [DRAFTS_FOLDER, documentId].join('.'), + publishedId: documentId, + ...(version + ? { + versionId: [version, documentId].join('.'), + } + : {}), + }, documentType, serverActionsEnabled, - ).subscribe((ev: RemoteSnapshotVersionEvent) => { - controller.handleRemoteMutation(ev) - }) + ) + .pipe(mapVersion(version)) + .subscribe((ev: RemoteSnapshotVersionEvent) => { + controller.handleRemoteMutation(ev) + }) } return () => { if (snapshotsSubscriptionRef.current) { @@ -186,7 +202,7 @@ export function useTimelineStore({ snapshotsSubscriptionRef.current = null } } - }, [client, controller, documentId, documentType, serverActionsEnabled]) + }, [client, controller, documentId, documentType, serverActionsEnabled, version]) const timelineStore = useMemo(() => { return { @@ -255,3 +271,31 @@ export function useTimelineStore({ return timelineStore } + +/** + * When computing the timeline for a version document, the version id cannot simply be treated as + * the primary document id. This would result in multiple document pairs being checked out with + * different parameters, which causes multiple listeners to be created. + * + * Instead, the timeline store checks out a document pair including the version, and maps the + * emitted version snapshots to published and draft snapshots. This allows the underyling timeline + * controller to be used without modification. + */ +function mapVersion(version?: string) { + return switchMap>((ev) => { + if (version) { + return of( + { + ...ev, + version: 'published', + }, + { + ...ev, + version: 'draft', + }, + ) + } + + return of(ev) + }) +} diff --git a/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx b/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx index 505d5ea9cac..1a5efd92c51 100644 --- a/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx +++ b/packages/sanity/src/core/tasks/components/form/tasksFormBuilder/FormEdit.tsx @@ -5,6 +5,7 @@ import {Box, Card, Flex, Menu, MenuDivider, Stack} from '@sanity/ui' // eslint-disable-next-line camelcase import {getTheme_v2} from '@sanity/ui/theme' import {useCallback} from 'react' +import {useRouter} from 'sanity/router' import {css, styled} from 'styled-components' import {MenuButton, MenuItem, TooltipDelayGroupProvider} from '../../../../../ui-components' @@ -102,6 +103,7 @@ function FormEditInner(props: ObjectInputProps) { const statusField = props.schemaType.fields.find((f) => f.name === 'status') const value = props.value as TaskDocument const currentUser = useCurrentUser() + const router = useRouter() const {t} = useTranslation(tasksLocaleNamespace) const activityData = useActivityLog(value).changes const handleChangeAndSubscribe = useCallback( diff --git a/packages/sanity/src/core/util/draftUtils.ts b/packages/sanity/src/core/util/draftUtils.ts index 2eb756d9aee..c0d8a7955bb 100644 --- a/packages/sanity/src/core/util/draftUtils.ts +++ b/packages/sanity/src/core/util/draftUtils.ts @@ -56,11 +56,27 @@ export function isDraftId(id: string): id is DraftId { return id.startsWith(DRAFTS_PREFIX) } -/** @internal */ -export function getIdPair(id: string): {draftId: DraftId; publishedId: PublishedId} { +/** + * TODO: Improve return type based on presence of `version` option. + * + * @internal + */ +export function getIdPair( + id: string, + {version}: {version?: string} = {}, +): { + draftId: DraftId + publishedId: PublishedId + versionId?: string +} { return { - draftId: getDraftId(id), publishedId: getPublishedId(id), + draftId: getDraftId(id), + ...(version + ? { + versionId: id.startsWith(`${version}.`) ? id : [version, getPublishedId(id)].join('.'), + } + : {}), } } diff --git a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx index 326a9dbd675..e47a13ae481 100644 --- a/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx +++ b/packages/sanity/src/structure/components/paneItem/PaneItemPreview.tsx @@ -77,12 +77,12 @@ export function PaneItemPreview(props: PaneItemPreviewProps) { {presence && presence.length > 0 && } - + ) - const tooltip = + const tooltip = return ( diff --git a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx index 9dc1eaab36d..94356a57cad 100644 --- a/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx +++ b/packages/sanity/src/structure/components/paneRouter/PaneRouterProvider.tsx @@ -24,6 +24,7 @@ export function PaneRouterProvider(props: { params: Record payload: unknown siblingIndex: number + perspective?: string }) { const {children, flatIndex, index, params, payload, siblingIndex} = props const {navigate, navigateIntent, resolvePathFromState} = useRouter() @@ -219,6 +220,9 @@ export function PaneRouterProvider(props: { // Proxied navigation to a given intent. Consider just exposing `router` instead? navigateIntent, + + // Perspective of the current pane + perspective: props.perspective, }), [ flatIndex, @@ -232,6 +236,7 @@ export function PaneRouterProvider(props: { setPayload, createPathWithParams, navigateIntent, + props.perspective, modifyCurrentGroup, lastPane, navigate, diff --git a/packages/sanity/src/structure/components/paneRouter/types.ts b/packages/sanity/src/structure/components/paneRouter/types.ts index a9a364674ce..b153f876a34 100644 --- a/packages/sanity/src/structure/components/paneRouter/types.ts +++ b/packages/sanity/src/structure/components/paneRouter/types.ts @@ -168,4 +168,9 @@ export interface PaneRouterContextValue { params: Record, options?: {replace?: boolean}, ) => void + + /** + * Perspective of the current pane + */ + perspective?: string } diff --git a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx index 426f684e448..ac93b3dc194 100644 --- a/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx +++ b/packages/sanity/src/structure/components/structureTool/StructureTitle.tsx @@ -6,24 +6,35 @@ import { useSchema, useTranslation, } from 'sanity' +import {useRouter} from 'sanity/router' import {LOADING_PANE} from '../../constants' import {structureLocaleNamespace} from '../../i18n' import {type Panes} from '../../structureResolvers' import {type DocumentPaneNode} from '../../types' import {useStructureTool} from '../../useStructureTool' +import {usePaneRouter} from '../paneRouter' interface StructureTitleProps { resolvedPanes: Panes['resolvedPanes'] } +// TODO: Fix state jank when editing different versions inside panes. const DocumentTitle = (props: {documentId: string; documentType: string}) => { const {documentId, documentType} = props - const editState = useEditState(documentId, documentType) + const router = useRouter() + const paneRouter = usePaneRouter() + const perspective = paneRouter.perspective ?? router.stickyParams.perspective + + const bundlePerspective = perspective?.startsWith('bundle.') + ? perspective.split('bundle.').at(1) + : undefined + + const editState = useEditState(documentId, documentType, 'default', bundlePerspective) const schema = useSchema() const {t} = useTranslation(structureLocaleNamespace) const isNewDocument = !editState?.published && !editState?.draft - const documentValue = editState?.draft || editState?.published + const documentValue = editState?.version || editState?.draft || editState?.published const schemaType = schema.get(documentType) as ObjectSchemaType | undefined const {value, isLoading: previewValueIsLoading} = useValuePreview({ diff --git a/packages/sanity/src/structure/components/structureTool/StructureTool.tsx b/packages/sanity/src/structure/components/structureTool/StructureTool.tsx index c695d592000..1cf12900c8b 100644 --- a/packages/sanity/src/structure/components/structureTool/StructureTool.tsx +++ b/packages/sanity/src/structure/components/structureTool/StructureTool.tsx @@ -135,8 +135,8 @@ export const StructureTool = memo(function StructureTool({onPaneChange}: Structu )} -
+ ) }) diff --git a/packages/sanity/src/structure/documentActions/DeleteAction.tsx b/packages/sanity/src/structure/documentActions/DeleteAction.tsx index 2b25570dbc6..fb65cdd9a93 100644 --- a/packages/sanity/src/structure/documentActions/DeleteAction.tsx +++ b/packages/sanity/src/structure/documentActions/DeleteAction.tsx @@ -21,9 +21,16 @@ const DISABLED_REASON_TITLE_KEY = { } /** @internal */ -export const DeleteAction: DocumentActionComponent = ({id, type, draft, onComplete}) => { +export const DeleteAction: DocumentActionComponent = ({ + id, + type, + draft, + onComplete, + bundleSlug, +}) => { const {setIsDeleting: paneSetIsDeleting} = useDocumentPane() - const {delete: deleteOp} = useDocumentOperation(id, type) + + const {delete: deleteOp} = useDocumentOperation(id, type, bundleSlug) const [isDeleting, setIsDeleting] = useState(false) const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false) @@ -49,6 +56,7 @@ export const DeleteAction: DocumentActionComponent = ({id, type, draft, onComple const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'delete', }) diff --git a/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx b/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx index 21a7524c1b4..3c17ce9322d 100644 --- a/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx +++ b/packages/sanity/src/structure/documentActions/DiscardChangesAction.tsx @@ -27,12 +27,14 @@ export const DiscardChangesAction: DocumentActionComponent = ({ published, liveEdit, onComplete, + bundleSlug, }) => { - const {discardChanges} = useDocumentOperation(id, type) + const {discardChanges} = useDocumentOperation(id, type, bundleSlug) const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false) const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'discardDraft', }) const currentUser = useCurrentUser() diff --git a/packages/sanity/src/structure/documentActions/DuplicateAction.tsx b/packages/sanity/src/structure/documentActions/DuplicateAction.tsx index b3f30937d8a..040a8c1ebbc 100644 --- a/packages/sanity/src/structure/documentActions/DuplicateAction.tsx +++ b/packages/sanity/src/structure/documentActions/DuplicateAction.tsx @@ -21,14 +21,16 @@ const DISABLED_REASON_KEY = { } /** @internal */ -export const DuplicateAction: DocumentActionComponent = ({id, type, onComplete}) => { +export const DuplicateAction: DocumentActionComponent = ({id, type, onComplete, bundleSlug}) => { const documentStore = useDocumentStore() - const {duplicate} = useDocumentOperation(id, type) + const {duplicate} = useDocumentOperation(id, type, bundleSlug) const {navigateIntent} = useRouter() const [isDuplicating, setDuplicating] = useState(false) + const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'duplicate', }) diff --git a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx index 7daf4f6848f..332666609c8 100644 --- a/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx +++ b/packages/sanity/src/structure/documentActions/HistoryRestoreAction.tsx @@ -12,8 +12,14 @@ import {useRouter} from 'sanity/router' import {structureLocaleNamespace} from '../i18n' /** @internal */ -export const HistoryRestoreAction: DocumentActionComponent = ({id, type, revision, onComplete}) => { - const {restore} = useDocumentOperation(id, type) +export const HistoryRestoreAction: DocumentActionComponent = ({ + id, + type, + revision, + onComplete, + bundleSlug, +}) => { + const {restore} = useDocumentOperation(id, type, bundleSlug) const event = useDocumentOperationEvent(id, type) const {navigateIntent} = useRouter() const prevEvent = useRef(event) diff --git a/packages/sanity/src/structure/documentActions/PublishAction.tsx b/packages/sanity/src/structure/documentActions/PublishAction.tsx index 83df6415c5f..e4fea76ea59 100644 --- a/packages/sanity/src/structure/documentActions/PublishAction.tsx +++ b/packages/sanity/src/structure/documentActions/PublishAction.tsx @@ -44,16 +44,21 @@ function AlreadyPublished({publishedAt}: {publishedAt: string}) { return {t('action.publish.already-published.tooltip', {timeSincePublished})} } -/** @internal */ +/** + * TODO: Verify how this should work with versions, if it's needed at all. + * + *@internal + */ // eslint-disable-next-line complexity export const PublishAction: DocumentActionComponent = (props) => { - const {id, type, liveEdit, draft, published} = props + const {id, type, liveEdit, draft, published, bundleSlug} = props const [publishState, setPublishState] = useState<'publishing' | 'published' | null>(null) - const {publish} = useDocumentOperation(id, type) - const validationStatus = useValidationStatus(id, type) - const syncState = useSyncState(id, type) + const {publish} = useDocumentOperation(id, type, bundleSlug) + const validationStatus = useValidationStatus(id, type, bundleSlug) + const syncState = useSyncState(id, type, {version: bundleSlug}) const {changesOpen, onHistoryOpen, documentId, documentType} = useDocumentPane() - const editState = useEditState(documentId, documentType) + + const editState = useEditState(documentId, documentType, 'default', bundleSlug) const {t} = useTranslation(structureLocaleNamespace) const revision = (editState?.draft || editState?.published || {})._rev @@ -66,6 +71,7 @@ export const PublishAction: DocumentActionComponent = (props) => { const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'publish', }) @@ -153,6 +159,7 @@ export const PublishAction: DocumentActionComponent = (props) => { ]) return useMemo(() => { + // TODO: Check whether live edit is enabled because we're editing a version. if (liveEdit) { return { tone: 'default', diff --git a/packages/sanity/src/structure/documentActions/UnpublishAction.tsx b/packages/sanity/src/structure/documentActions/UnpublishAction.tsx index d9230db5b8a..6a7201fdd38 100644 --- a/packages/sanity/src/structure/documentActions/UnpublishAction.tsx +++ b/packages/sanity/src/structure/documentActions/UnpublishAction.tsx @@ -26,12 +26,14 @@ export const UnpublishAction: DocumentActionComponent = ({ draft, onComplete, liveEdit, + bundleSlug, }) => { - const {unpublish} = useDocumentOperation(id, type) + const {unpublish} = useDocumentOperation(id, type, bundleSlug) const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false) const [permissions, isPermissionsLoading] = useDocumentPairPermissions({ id, type, + version: bundleSlug, permission: 'unpublish', }) const currentUser = useCurrentUser() diff --git a/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts b/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts index 78ccc9e2aa0..4051dc1b174 100644 --- a/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts +++ b/packages/sanity/src/structure/documentBadges/LiveEditBadge.ts @@ -2,9 +2,10 @@ import {type DocumentBadgeComponent} from 'sanity' /** @internal */ export const LiveEditBadge: DocumentBadgeComponent = (props) => { - const {liveEdit} = props + const {liveEditSchemaType} = props - if (liveEdit) { + if (liveEditSchemaType) { + // TODO: i18n. return { label: 'Live', color: 'danger', diff --git a/packages/sanity/src/structure/panes/StructureToolPane.tsx b/packages/sanity/src/structure/panes/StructureToolPane.tsx index fd47373c13b..053a76e8755 100644 --- a/packages/sanity/src/structure/panes/StructureToolPane.tsx +++ b/packages/sanity/src/structure/panes/StructureToolPane.tsx @@ -1,7 +1,9 @@ import {isEqual} from 'lodash' import {lazy, memo, Suspense} from 'react' +import {useRouter} from 'sanity/router' import {PaneRouterProvider} from '../components/paneRouter' +import {useResolvedPanes} from '../structureResolvers' import {type PaneNode} from '../types' import {LoadingPane} from './loading' import {UnknownPane} from './unknown' @@ -14,7 +16,7 @@ interface StructureToolPaneProps { itemId: string pane: PaneNode paneKey: string - params: Record + params: Record & {perspective?: string} payload: unknown path: string selected: boolean @@ -52,6 +54,8 @@ export const StructureToolPane = memo( } = props const PaneComponent = paneMap[pane.type] || UnknownPane + const {stickyParams} = useRouter() + const {resolvedPanes} = useResolvedPanes() return ( }> { inspectors: inspectorsResolver, }, } = useSource() - const {stickyParams} = useRouter() const presenceStore = usePresenceStore() const paneRouter = usePaneRouter() const setPaneParams = paneRouter.setParams @@ -115,20 +113,37 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { templateParams, }) + const {perspective} = paneRouter + + const bundlePerspective = perspective?.startsWith('bundle.') + ? perspective.split('bundle.').at(1) + : undefined + const initialValue = useUnique(initialValueRaw) - const {patch} = useDocumentOperation(documentId, documentType) + const {patch} = useDocumentOperation(documentId, documentType, bundlePerspective) const schemaType = schema.get(documentType) as ObjectSchemaType | undefined - const editState = useEditState(documentId, documentType) - const {validation: validationRaw} = useValidationStatus(documentId, documentType) - const connectionState = useConnectionState(documentId, documentType) + const editState = useEditState(documentId, documentType, 'default', bundlePerspective) + const {validation: validationRaw} = useValidationStatus( + documentId, + documentType, + bundlePerspective, + ) + const connectionState = useConnectionState(documentId, documentType, {version: bundlePerspective}) const {data: documentVersions} = useDocumentVersions({documentId}) - const perspective = stickyParams.perspective + let value: SanityDocumentLike = initialValue.value + + switch (true) { + case typeof bundlePerspective !== 'undefined': + value = editState.version || editState.draft || value + break + case perspective === 'published': + value = editState.published || editState.draft || value + break + default: + value = editState?.draft || editState?.published || value + } - const value: SanityDocumentLike = - (perspective === 'published' - ? editState.published || editState.draft - : editState?.draft || editState?.published) || initialValue.value const [isDeleting, setIsDeleting] = useState(false) // Resolve document actions @@ -172,6 +187,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { onError: setTimelineError, rev: params.rev, since: params.since, + version: bundlePerspective, }) // Subscribe to external timeline state changes @@ -207,13 +223,13 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { const [presence, setPresence] = useState([]) useEffect(() => { - const subscription = presenceStore.documentPresence(documentId).subscribe((nextPresence) => { + const subscription = presenceStore.documentPresence(value._id).subscribe((nextPresence) => { setPresence(nextPresence) }) return () => { subscription.unsubscribe() } - }, [documentId, presenceStore]) + }, [presenceStore, value._id]) const inspectors: DocumentInspector[] = useMemo( () => inspectorsResolver({documentId, documentType}), @@ -495,6 +511,10 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { }) const isNonExistent = !value?._id + const isNonExistentInBundle = + typeof bundlePerspective !== 'undefined' && !value._id.startsWith(`${bundlePerspective}.`) + const existsInBundle = + typeof bundlePerspective !== 'undefined' && value._id.startsWith(`${bundlePerspective}.`) const readOnly = useMemo(() => { const hasNoPermission = !isPermissionsLoading && !permissions?.granted @@ -502,9 +522,11 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { const createActionDisabled = isNonExistent && !isActionEnabled(schemaType!, 'create') const reconnecting = connectionState === 'reconnecting' const isLocked = editState.transactionSyncLock?.enabled + const isSystemPerspectiveApplied = perspective && typeof bundlePerspective === 'undefined' return ( - !!perspective || + isNonExistentInBundle || + isSystemPerspectiveApplied || !ready || revTime !== null || hasNoPermission || @@ -520,9 +542,11 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { permissions?.granted, schemaType, isNonExistent, + isNonExistentInBundle, connectionState, editState.transactionSyncLock?.enabled, perspective, + bundlePerspective, ready, revTime, isDeleting, @@ -579,14 +603,14 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { presenceStore.setLocation([ { type: 'document', - documentId, + documentId: value._id, path: nextFocusPath, lastActiveAt: new Date().toISOString(), selection: payload?.selection, }, ]) }, - [documentId, presenceStore], + [presenceStore, value._id], ) const updatePresenceThrottled = useMemo( @@ -624,6 +648,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { documentType, documentVersions, editState, + existsInBundle, fieldActions, focusPath, inspector: currentInspector || null, @@ -664,6 +689,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { timelineStore, title, value, + version: bundlePerspective, views, formState, unstable_languageFilter: languageFilter, @@ -673,6 +699,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { actions, activeViewId, badges, + bundlePerspective, changesOpen, closeInspector, collapsedFieldSets, @@ -686,6 +713,7 @@ export const DocumentPaneProvider = memo((props: DocumentPaneProviderProps) => { documentType, documentVersions, editState, + existsInBundle, fieldActions, focusPath, formState, diff --git a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx index fcd234329a1..88fb450afd7 100644 --- a/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx +++ b/packages/sanity/src/structure/panes/document/comments/CommentsWrapper.tsx @@ -5,6 +5,7 @@ import { CommentsProvider, useCommentsEnabled, } from 'sanity' +import {useRouter} from 'sanity/router' import {usePaneRouter} from '../../../components' import {useDocumentPane} from '../useDocumentPane' @@ -37,7 +38,9 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) { const {enabled} = useCommentsEnabled() const {connectionState, onPathOpen, inspector, openInspector} = useDocumentPane() - const {params, setParams, createPathWithParams} = usePaneRouter() + const router = useRouter() + const {params, setParams, createPathWithParams, ...paneRouter} = usePaneRouter() + const perspective = paneRouter.perspective ?? router.stickyParams.perspective const selectedCommentId = params?.comment const paramsRef = useRef(params) @@ -90,6 +93,7 @@ function CommentsProviderWrapper(props: CommentsWrapperProps) { selectedCommentId={selectedCommentId} sortOrder="desc" type="field" + perspective={perspective} > {children} diff --git a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx index ad0ed930a5c..07dbd1b2afb 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/banners/ReferenceChangedBanner.tsx @@ -21,14 +21,19 @@ import {Banner} from './Banner' interface ParentReferenceInfo { loading: boolean result?: { - availability: {draft: DocumentAvailability; published: DocumentAvailability} + availability: { + draft: DocumentAvailability + published: DocumentAvailability + version?: DocumentAvailability + } refValue: string | undefined } } export const ReferenceChangedBanner = memo(() => { const documentPreviewStore = useDocumentPreviewStore() - const {params, groupIndex, routerPanesState, replaceCurrent, BackLink} = usePaneRouter() + const {params, groupIndex, routerPanesState, replaceCurrent, BackLink, perspective} = + usePaneRouter() const routerReferenceId = routerPanesState[groupIndex]?.[0].id const parentGroup = routerPanesState[groupIndex - 1] as RouterPaneGroup | undefined const parentSibling = parentGroup?.[0] @@ -39,6 +44,10 @@ export const ReferenceChangedBanner = memo(() => { }, [params?.parentRefPath]) const {t} = useTranslation(structureLocaleNamespace) + const bundlePerspective = perspective?.startsWith('bundle.') + ? perspective.split('bundle.').at(1) + : undefined + /** * Loads information regarding the reference field of the parent pane. This * is only applicable to child references (aka references-in-place). @@ -74,6 +83,9 @@ export const ReferenceChangedBanner = memo(() => { .unstable_observePathsDocumentPair( publishedId, (keyedSegmentIndex === -1 ? path : path.slice(0, keyedSegmentIndex)) as string[][], + { + version: bundlePerspective, + }, ) .pipe( // this debounce time is needed to prevent flashing banners due to @@ -82,21 +94,28 @@ export const ReferenceChangedBanner = memo(() => { // initially could be stale. debounceTime(750), map( - ({draft, published}): ParentReferenceInfo => ({ + ({draft, published, version}): ParentReferenceInfo => ({ loading: false, result: { availability: { draft: draft.availability, published: published.availability, + ...(version?.availability + ? { + version: version.availability, + } + : {}), }, - refValue: pathGet(draft.snapshot || published.snapshot, parentRefPath) - ?._ref, + refValue: pathGet( + version?.snapshot || draft.snapshot || published.snapshot, + parentRefPath, + )?._ref, }, }), ), ), ) - }, [documentPreviewStore, parentId, parentRefPath]) + }, [bundlePerspective, documentPreviewStore, parentId, parentRefPath]) const referenceInfo = useObservable(referenceInfoObservable, {loading: true}) const handleReloadReference = useCallback(() => { diff --git a/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx b/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx index 6868e2c4166..fbdd62114e1 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/documentViews/FormView.tsx @@ -40,6 +40,7 @@ export const FormView = forwardRef(function FormV editState, documentId, documentType, + version, fieldActions, onChange, validation, @@ -77,7 +78,7 @@ export const FormView = forwardRef(function FormV useEffect(() => { const sub = documentStore.pair - .documentEvents(documentId, documentType) + .documentEvents(documentId, documentType, version) .pipe( tap((event) => { if (event.type === 'mutation') { @@ -94,7 +95,7 @@ export const FormView = forwardRef(function FormV return () => { sub.unsubscribe() } - }, [documentId, documentStore, documentType, patchChannel]) + }, [documentId, documentStore, documentType, patchChannel, version]) const hasRev = Boolean(value?._rev) useEffect(() => { @@ -207,6 +208,7 @@ export const FormView = forwardRef(function FormV // but these should be compatible formState.value as FormDocumentValue } + version={version} /> )} diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx index 701845754ae..9a48a11e5ff 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/DocumentHeaderTitle.tsx @@ -59,7 +59,7 @@ export function DocumentHeaderTitle(): ReactElement { - + ) diff --git a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx index 59fbb4cf51e..e853a10326c 100644 --- a/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx +++ b/packages/sanity/src/structure/panes/document/documentPanel/header/perspective/DocumentPerspectiveMenu.tsx @@ -1,23 +1,23 @@ import {ChevronDownIcon} from '@sanity/icons' import {Box, Button} from '@sanity/ui' import {useCallback} from 'react' -import {BundleBadge, BundleMenu, getBundleSlug, usePerspective} from 'sanity' +import {BundleBadge, BundleMenu, usePerspective} from 'sanity' import {useRouter} from 'sanity/router' import {styled} from 'styled-components' +import {usePaneRouter} from '../../../../../components' import {useDocumentPane} from '../../../useDocumentPane' const BadgeButton = styled(Button)({ cursor: 'pointer', }) -export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Element { - const {documentId} = props - const {currentGlobalBundle} = usePerspective() +export function DocumentPerspectiveMenu(): JSX.Element { + const paneRouter = usePaneRouter() + const {currentGlobalBundle} = usePerspective(paneRouter.perspective) - const existsInBundle = getBundleSlug(documentId) === currentGlobalBundle?.slug + const {documentVersions, existsInBundle} = useDocumentPane() const {title, hue, icon, slug} = currentGlobalBundle - const {documentVersions} = useDocumentPane() const router = useRouter() @@ -46,6 +46,7 @@ export function DocumentPerspectiveMenu(props: {documentId: string}): JSX.Elemen button={