Skip to content

Commit

Permalink
feat(sanity): add diffView plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Jan 9, 2025
1 parent 4e294c8 commit 1d9fd43
Show file tree
Hide file tree
Showing 22 changed files with 1,537 additions and 424 deletions.
1 change: 1 addition & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
"rxjs-mergemap-array": "^0.1.0",
"sanity-diff-patch": "^4.0.0",
"scroll-into-view-if-needed": "^3.0.3",
"scrollmirror": "^1.2.0",
"semver": "^7.3.5",
"shallow-equals": "^1.0.0",
"speakingurl": "^14.0.1",
Expand Down
10 changes: 9 additions & 1 deletion packages/sanity/src/core/config/resolveDefaultPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {comments} from '../comments/plugin'
import {createIntegration} from '../create/createIntegrationPlugin'
import {diffView} from '../diffView/plugin'
import {releases, RELEASES_NAME} from '../releases/plugin'
import {DEFAULT_SCHEDULED_PUBLISH_PLUGIN_OPTIONS} from '../scheduledPublishing/constants'
import {SCHEDULED_PUBLISHING_NAME, scheduledPublishing} from '../scheduledPublishing/plugin'
Expand All @@ -11,7 +12,14 @@ import {
type WorkspaceOptions,
} from './types'

const defaultPlugins = [comments(), tasks(), scheduledPublishing(), createIntegration(), releases()]
const defaultPlugins = [
comments(),
tasks(),
scheduledPublishing(),
createIntegration(),
releases(),
diffView(),
]

export function getDefaultPlugins(
options: DefaultPluginsWorkspaceOptions,
Expand Down
14 changes: 14 additions & 0 deletions packages/sanity/src/core/diffView/components/DialogLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {styled} from 'styled-components'

export const DialogLayout = styled.div`
--offset-block: 40px;
display: grid;
height: calc(100vh - var(--offset-block));
min-height: 0;
overflow: hidden;
grid-template-areas:
'header header'
'previous-document next-document';
grid-template-columns: 1fr 1fr;
grid-template-rows: min-content minmax(0, 1fr);
`
58 changes: 58 additions & 0 deletions packages/sanity/src/core/diffView/components/DiffView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {type ComponentType, useState} from 'react'

import {Dialog} from '../../../ui-components/dialog/Dialog'
import {type DocumentLayoutProps} from '../../config/types'
import {useCreatePathSyncChannel} from '../hooks/useCreatePathSyncChannel'
import {useDiffViewRouter} from '../hooks/useDiffViewRouter'
import {useDiffViewState} from '../hooks/useDiffViewState'
import {useScrollMirror} from '../hooks/useScrollMirror'
import {VersionModeHeader} from '../versionMode/components/VersionModeHeader'
import {DialogLayout} from './DialogLayout'
import {DiffViewPane} from './DiffViewPane'

export const DiffView: ComponentType<Pick<DocumentLayoutProps, 'documentId'>> = ({documentId}) => {
const {documents, state, mode} = useDiffViewState()
const {exitDiffView} = useDiffViewRouter()
const syncChannel = useCreatePathSyncChannel()
const [previousPaneElement, setPreviousPaneElement] = useState<HTMLElement | null>(null)
const [nextPaneElement, setNextPaneElement] = useState<HTMLElement | null>(null)

useScrollMirror([previousPaneElement, nextPaneElement])

return (
<Dialog
id="diffView"
width="auto"
onClose={exitDiffView}
padding={false}
__unstable_hideCloseButton
>
<DialogLayout>
{mode === 'version' && <VersionModeHeader documentId={documentId} state={state} />}
{state === 'ready' && (
<>
<DiffViewPane
documentType={documents.previous.type}
documentId={documents.previous.id}
role="previous"
ref={setPreviousPaneElement}
scrollElement={previousPaneElement}
syncChannel={syncChannel}
compareDocument={documents.previous}
/>
<DiffViewPane
documentType={documents.next.type}
documentId={documents.next.id}
role="next"
ref={setNextPaneElement}
scrollElement={nextPaneElement}
syncChannel={syncChannel}
// The previous document's edit state is used to calculate the diff inroduced by the next document.
compareDocument={documents.previous}
/>
</>
)}
</DialogLayout>
</Dialog>
)
}
136 changes: 136 additions & 0 deletions packages/sanity/src/core/diffView/components/DiffViewPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {BoundaryElementProvider, Box, Card, DialogProvider, PortalProvider} from '@sanity/ui'
import {noop} from 'lodash'
import {type CSSProperties, forwardRef, useRef, useState} from 'react'
import {ConnectorContext} from 'sanity/_singletons'
import {DocumentPaneProvider, FormViewComponent} from 'sanity/structure'
import {styled} from 'styled-components'

import {ChangeIndicatorsTracker} from '../../changeIndicators/tracker'
import {VirtualizerScrollInstanceProvider} from '../../form/inputs/arrays/ArrayOfObjectsInput/List/VirtualizerScrollInstanceProvider'
import {useEditState} from '../../hooks/useEditState'
import {getPublishedId, getVersionFromId, isDraftId} from '../../util/draftUtils'
import {type PathSyncChannel} from '../types/pathSyncChannel'
import {PathSyncChannelSubscriber} from './PathSyncChannelSubscriber'
import {Scroller} from './Scroller'

const DiffViewPaneLayout = styled(Card)`
position: relative;
grid-area: var(--grid-area);
`

interface DiffViewPaneProps {
documentType: string
documentId: string
role: 'previous' | 'next'
scrollElement: HTMLElement | null
syncChannel: PathSyncChannel
compareDocument: {
type: string
id: string
}
}

// TODO: Switch off comments. Document inspectors cannot currently be shown inside the diff view.
// TODO: Switch off references pane. It should be a hyperlink instead.
export const DiffViewPane = forwardRef<HTMLDivElement, DiffViewPaneProps>(function DiffViewPane(
{role, documentType, documentId, scrollElement, syncChannel, /*compareValue,*/ compareDocument},
ref,
) {
const paneId = ['diffView', role].join('.')

const version: 'draft' | 'published' | string = isDraftId(documentId)
? 'draft'
: (getVersionFromId(documentId) ?? 'published')

const publishedVersionId = getPublishedId(documentId)
const containerElement = useRef<HTMLDivElement | null>(null)
const [portalElement, setPortalElement] = useState<HTMLDivElement | null>(null)
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)

const compareDocumentEditState = useEditState(
getPublishedId(compareDocument.id),
compareDocument.type,
'low',
getVersionFromId(compareDocument.id),
)

const compareValue =
compareDocumentEditState.version ??
compareDocumentEditState.draft ??
compareDocumentEditState.published ??
{}

return (
<ConnectorContext.Provider
value={{
// Only display change indicators in the next document pane.
isEnabled: role === 'next',
// Render the change indicators inertly, because the diff view modal does not currently
// provide a way to display document inspectors.
isInteractive: false,
onOpenReviewChanges: noop,
onSetFocus: noop,
isReviewChangesOpen: false,
}}
>
<ChangeIndicatorsTracker>
<VirtualizerScrollInstanceProvider
scrollElement={scrollElement}
containerElement={containerElement}
>
<BoundaryElementProvider element={boundaryElement}>
<DiffViewPaneLayout
ref={setBoundaryElement}
style={
{
'--grid-area': `${role}-document`,
} as CSSProperties
}
borderLeft={role === 'next'}
>
<Scroller
ref={ref}
style={
{
// The scroll position is synchronised between panes. This style hides the
// scrollbar for all panes except the one displaying the next document.
'--scrollbar-width': role !== 'next' && 'none',
} as CSSProperties
}
>
<DocumentPaneProvider
index={0}
paneKey={paneId}
itemId={paneId}
perspectiveOverride={version}
pane={{
id: paneId,
type: 'document',
// Providing a falsey value allows the title to be computed automatically based
// on the document's values.
title: '',
options: {
type: documentType,
id: publishedVersionId,
},
}}
compareValue={compareValue}
>
<PortalProvider element={portalElement}>
<DialogProvider position="absolute">
<PathSyncChannelSubscriber id={role} syncChannel={syncChannel} />
<Box ref={containerElement}>
<FormViewComponent hidden={false} margins={[0, 0, 0, 0]} />
</Box>
</DialogProvider>
</PortalProvider>
</DocumentPaneProvider>
</Scroller>
<div data-testid="diffView-document-panel-portal" ref={setPortalElement} />
</DiffViewPaneLayout>
</BoundaryElementProvider>
</VirtualizerScrollInstanceProvider>
</ChangeIndicatorsTracker>
</ConnectorContext.Provider>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {type ComponentType} from 'react'

import {usePathSyncChannel} from '../hooks/usePathSyncChannel'
import {type PathSyncChannelProps} from '../types/pathSyncChannel'

export const PathSyncChannelSubscriber: ComponentType<PathSyncChannelProps> = (props) => {
usePathSyncChannel(props)
return undefined
}
11 changes: 11 additions & 0 deletions packages/sanity/src/core/diffView/components/Scroller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {styled} from 'styled-components'

export const Scroller = styled.div`
position: relative;
height: 100%;
overflow: auto;
scroll-behavior: smooth;
scrollbar-width: var(--scrollbar-width);
overscroll-behavior: contain;
will-change: scroll-position;
`
20 changes: 20 additions & 0 deletions packages/sanity/src/core/diffView/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @internal
*/
export const DIFF_VIEW_SEARCH_PARAMETER = 'diffView'

/**
* @internal
*/
export const DIFF_VIEW_PREVIOUS_DOCUMENT_SEARCH_PARAMETER = 'previousDocument'

/**
* @internal
*/
export const DIFF_VIEW_NEXT_DOCUMENT_SEARCH_PARAMETER = 'nextDocument'

/**
* @internal
* TODO: Gets encoded to `%3B`. Consider another character.
*/
export const DIFF_SEARCH_PARAM_SEPERATOR = ';'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {useMemo} from 'react'
import {Subject} from 'rxjs'

import {type PathSyncChannel, type PathSyncState} from '../types/pathSyncChannel'

/**
* @internal
*/
export function useCreatePathSyncChannel(): PathSyncChannel {
return useMemo(() => new Subject<PathSyncState>(), [])
}
93 changes: 93 additions & 0 deletions packages/sanity/src/core/diffView/hooks/useDiffViewRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {fromPairs, toPairs} from 'lodash'
import {useCallback} from 'react'
import {useRouter} from 'sanity/router'

import {
DIFF_SEARCH_PARAM_SEPERATOR,
DIFF_VIEW_NEXT_DOCUMENT_SEARCH_PARAMETER,
DIFF_VIEW_PREVIOUS_DOCUMENT_SEARCH_PARAMETER,
DIFF_VIEW_SEARCH_PARAMETER,
} from '../constants'
import {type DiffViewMode} from '../types/diffViewMode'

type NavigateDiffView = (
options: {
mode?: DiffViewMode
} & Partial<
Record<
'previousDocument' | 'nextDocument',
{
type: string
id: string
}
>
>,
) => void

interface DiffViewRouter {
navigateDiffView: NavigateDiffView
exitDiffView: () => void
}

/**
* @internal
*/
export function useDiffViewRouter(): DiffViewRouter {
const {navigate, state: routerState} = useRouter()

const navigateDiffView = useCallback<NavigateDiffView>(
({mode, previousDocument, nextDocument}) => {
const next = {
...fromPairs(routerState._searchParams),
...(mode
? {
[DIFF_VIEW_SEARCH_PARAMETER]: mode,
}
: {}),
...(previousDocument
? {
[DIFF_VIEW_PREVIOUS_DOCUMENT_SEARCH_PARAMETER]: [
previousDocument.type,
previousDocument.id,
].join(DIFF_SEARCH_PARAM_SEPERATOR),
}
: {}),
...(nextDocument
? {
[DIFF_VIEW_NEXT_DOCUMENT_SEARCH_PARAMETER]: [nextDocument.type, nextDocument.id].join(
DIFF_SEARCH_PARAM_SEPERATOR,
),
}
: {}),
}

// FIXME: Sticky params (e.g. `perspective`) are being lost here SOMETIMES.
// ok here: http://localhost:3333/test/structure/input-debug;objectsDebug;7c71b8b7-8b6c-47a0-a6ca-e691776e8bbc?perspective=rF1jtvjXP
// lost here: http://localhost:3333/test/structure/book;5c80b890-badc-482b-a656-3adcc2ed75f7?perspective=rCj1fA9MW
navigate({
...routerState,
_searchParams: toPairs(next),
})
},
[navigate, routerState],
)

const exitDiffView = useCallback(() => {
navigate({
...routerState,
_searchParams: (routerState._searchParams ?? []).filter(
([key]) =>
![
DIFF_VIEW_SEARCH_PARAMETER,
DIFF_VIEW_PREVIOUS_DOCUMENT_SEARCH_PARAMETER,
DIFF_VIEW_NEXT_DOCUMENT_SEARCH_PARAMETER,
].includes(key),
),
})
}, [navigate, routerState])

return {
navigateDiffView,
exitDiffView,
}
}
Loading

0 comments on commit 1d9fd43

Please sign in to comment.