Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add diff view #7907

Draft
wants to merge 13 commits into
base: corel
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions packages/sanity/src/_singletons/context/ConnectorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ export const ConnectorContext = createContext<ConnectorContextValue>(
isReviewChangesOpen: false,
onOpenReviewChanges: () => undefined,
onSetFocus: () => undefined,
isEnabled: true,
isInteractive: true,
} as ConnectorContextValue,
)
11 changes: 11 additions & 0 deletions packages/sanity/src/core/changeIndicators/ChangeIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import {
memo,
type MouseEvent,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
import deepCompare from 'react-fast-compare'

import {ConnectorContext} from '../../_singletons/context/ConnectorContext'
import {EMPTY_ARRAY} from '../util'
import {ElementWithChangeBar} from './ElementWithChangeBar'
import {useChangeIndicatorsReporter} from './tracker'
Expand All @@ -23,6 +25,7 @@ const ChangeBarWrapper = memo(function ChangeBarWrapper(
hasFocus: boolean
isChanged?: boolean
withHoverEffect?: boolean
isInteractive?: boolean
},
) {
const {
Expand All @@ -34,6 +37,7 @@ const ChangeBarWrapper = memo(function ChangeBarWrapper(
onMouseLeave: onMouseLeaveProp,
path = EMPTY_ARRAY,
withHoverEffect,
isInteractive,
...restProps
} = props
const layer = useLayer()
Expand Down Expand Up @@ -82,6 +86,7 @@ const ChangeBarWrapper = memo(function ChangeBarWrapper(
isChanged={isChanged}
disabled={disabled}
withHoverEffect={withHoverEffect}
isInteractive={isInteractive}
>
{children}
</ElementWithChangeBar>
Expand All @@ -102,6 +107,11 @@ export function ChangeIndicator(
props: ChangeIndicatorProps & Omit<HTMLProps<HTMLDivElement>, 'as'>,
) {
const {children, hasFocus, isChanged, path, withHoverEffect, ...restProps} = props
const {isEnabled, isInteractive} = useContext(ConnectorContext)

if (!isEnabled) {
return children
}

return (
<ChangeBarWrapper
Expand All @@ -110,6 +120,7 @@ export function ChangeIndicator(
hasFocus={hasFocus}
isChanged={isChanged}
withHoverEffect={withHoverEffect}
isInteractive={isInteractive}
>
{children}
</ChangeBarWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export interface ConnectorContextValue {
isReviewChangesOpen: boolean
onOpenReviewChanges: () => void | undefined
onSetFocus: (nextPath: Path) => void | undefined
isEnabled: boolean
isInteractive?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@ export const ChangeBarMarker = styled.div((props) => {
`
})

export const ChangeBarButton = styled.button<{$withHoverEffect?: boolean}>((props) => {
const {$withHoverEffect} = props
export const ChangeBarButton = styled.button<{
$withHoverEffect?: boolean
$isInteractive?: boolean
}>((props) => {
const {$withHoverEffect, $isInteractive} = props

return css`
appearance: none;
Expand All @@ -123,8 +126,13 @@ export const ChangeBarButton = styled.button<{$withHoverEffect?: boolean}>((prop
opacity: 0;
position: absolute;
height: 100%;
cursor: pointer;
pointer-events: all;

${$isInteractive &&
css`
cursor: pointer;
pointer-events: all;
`}

left: calc(-0.25rem + var(--change-bar-offset));
width: calc(1rem - 1px);
transition: opacity ${animationSpeed}ms;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ export function ElementWithChangeBar(props: {
hasFocus?: boolean
isChanged?: boolean
withHoverEffect?: boolean
isInteractive?: boolean
}) {
const {children, disabled, hasFocus, isChanged, withHoverEffect = true} = props
const {
children,
disabled,
hasFocus,
isChanged,
withHoverEffect = true,
isInteractive = true,
} = props

const {onOpenReviewChanges, isReviewChangesOpen} = useContext(ConnectorContext)
const {zIndex} = useLayer()
Expand All @@ -30,19 +38,29 @@ export function ElementWithChangeBar(props: {
disabled || !isChanged ? null : (
<ChangeBar data-testid="change-bar" $zIndex={zIndex}>
<ChangeBarMarker data-testid="change-bar__marker" />
<Tooltip content={t('changes.change-bar.aria-label')} portal>
<Tooltip content={t('changes.change-bar.aria-label')} portal disabled={!isInteractive}>
<ChangeBarButton
aria-label={t('changes.change-bar.aria-label')}
data-testid="change-bar__button"
onClick={isReviewChangesOpen ? undefined : onOpenReviewChanges}
tabIndex={-1}
type="button"
$withHoverEffect={withHoverEffect}
$isInteractive={isInteractive}
/>
</Tooltip>
</ChangeBar>
),
[disabled, isChanged, isReviewChangesOpen, onOpenReviewChanges, t, withHoverEffect, zIndex],
[
disabled,
isChanged,
isInteractive,
isReviewChangesOpen,
onOpenReviewChanges,
t,
withHoverEffect,
zIndex,
],
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface ChangeConnectorRootProps {
isReviewChangesOpen: boolean
onOpenReviewChanges: () => void
onSetFocus: (path: Path) => void
isEnabled?: boolean
}

/** @internal */
Expand All @@ -22,6 +23,7 @@ export function ChangeConnectorRoot({
isReviewChangesOpen,
onOpenReviewChanges,
onSetFocus,
isEnabled = true,
...restProps
}: ChangeConnectorRootProps) {
const [rootElement, setRootElement] = useState<HTMLDivElement | null>()
Expand All @@ -31,8 +33,9 @@ export function ChangeConnectorRoot({
isReviewChangesOpen,
onOpenReviewChanges,
onSetFocus,
isEnabled,
}),
[isReviewChangesOpen, onOpenReviewChanges, onSetFocus],
[isReviewChangesOpen, onOpenReviewChanges, onSetFocus, isEnabled],
)

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/sanity/src/core/hooks/useEditState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function useEditState(
version?: string | undefined,
): EditStateFor {
if (version === 'published' || version === 'draft') {
throw new Error('Version cannot be published or daft')
throw new Error('Version cannot be published or draft')
}
const documentStore = useDocumentStore()

Expand Down
1 change: 1 addition & 0 deletions packages/sanity/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
isReleaseDocument,
isReleaseScheduledOrScheduling,
LATEST,
ReleaseAvatar,
type ReleaseDocument,
RELEASES_INTENT,
useDocumentVersions,
Expand Down
38 changes: 31 additions & 7 deletions packages/sanity/src/core/releases/hooks/usePerspective.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {type ReleaseId} from '@sanity/client'
import {useCallback, useEffect, useMemo} from 'react'
import {useRouter} from 'sanity/router'

import {isSystemBundleName} from '../../util/draftUtils'
import {type ReleaseDocument} from '../store/types'
import {useReleases} from '../store/useReleases'
import {LATEST} from '../util/const'
Expand Down Expand Up @@ -39,22 +40,45 @@ export interface PerspectiveValue {
perspectiveStack: string[]
}

/**
* @internal
*/
export interface PerspectiveOptions {
/**
* The perspective is normally determined by the router. The `perspectiveOverride` prop can be
* used to explicitly set the perspective, overriding the perspective provided by the router.
*/
perspectiveOverride?: string

/**
* The excluded perspective is normally determined by the router. The
* `excludedPerspectivesOverride` prop can be used to explicitly set the excluded perspective,
* overriding the excluded perspective provided by the router.
*/
excludedPerspectivesOverride?: string[]
}

const EMPTY_ARRAY: string[] = []

/**
* @internal
*/
export function usePerspective(): PerspectiveValue {
export function usePerspective({
perspectiveOverride,
excludedPerspectivesOverride,
}: PerspectiveOptions = {}): PerspectiveValue {
const router = useRouter()
const {data: releases, archivedReleases, loading: releasesLoading} = useReleases()
const selectedPerspectiveName = router.stickyParams.perspective as
const selectedPerspectiveName = (perspectiveOverride ?? router.stickyParams.perspective) as
| 'published'
| ReleaseId
| undefined

const excludedPerspectives = useMemo(
() => router.stickyParams.excludedPerspectives?.split(',') || EMPTY_ARRAY,
[router.stickyParams.excludedPerspectives],
() =>
excludedPerspectivesOverride ??
(router.stickyParams.excludedPerspectives?.split(',') || EMPTY_ARRAY),
[excludedPerspectivesOverride, router.stickyParams.excludedPerspectives],
)

const setPerspective = useCallback(
Expand Down Expand Up @@ -134,10 +158,10 @@ export function usePerspective(): PerspectiveValue {
() => ({
selectedPerspective,
selectedPerspectiveName,
selectedReleaseId:
selectedPerspectiveName === 'published' ? undefined : selectedPerspectiveName,
selectedReleaseId: isSystemBundleName(selectedPerspectiveName)
? undefined
: selectedPerspectiveName,
perspectiveStack,

setPerspective,
toggleExcludedPerspective,
isPerspectiveExcluded,
Expand Down
5 changes: 4 additions & 1 deletion packages/sanity/src/core/releases/hooks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ export function getReleasesPerspectiveStack({
releases,
excludedPerspectives,
}: {
selectedPerspectiveName: ReleaseId | undefined | 'published'
selectedPerspectiveName: ReleaseId | undefined | 'published' | 'draft'
releases: ReleaseDocument[]
excludedPerspectives: string[]
}): string[] {
if (!selectedPerspectiveName || selectedPerspectiveName === 'published') {
return []
}
if (selectedPerspectiveName === 'draft') {
return [DRAFTS_FOLDER].filter((name) => !excludedPerspectives.includes(name))
}
const sorted: string[] = sortReleases(releases).map((release) =>
getReleaseIdFromReleaseDocumentId(release._id),
)
Expand Down
2 changes: 0 additions & 2 deletions packages/sanity/src/core/releases/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ const releasesLocaleStrings = {
'action.archive.tooltip': 'Unschedule this release to archive it',
/** Action text for showing the archived releases */
'action.archived': 'Archived',
/** Action text for comparing document versions */
'action.compare-versions': 'Compare versions',
/** Action text for reverting a release by creating a new release */
'action.create-revert-release': 'Stage in new release',
/** Action text for deleting a release */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export default function resolveDocumentActions(
context: DocumentActionsContext,
): Action[] {
const duplicateAction = existingActions.filter(({name}) => name === 'DuplicateAction')

return context.versionType === 'version'
? duplicateAction.concat(DiscardVersionAction).concat(UnpublishVersionAction)
: existingActions
return [
...(context.versionType === 'version'
? duplicateAction.concat(DiscardVersionAction).concat(UnpublishVersionAction)
: existingActions),
]
}
14 changes: 10 additions & 4 deletions packages/sanity/src/core/releases/tool/components/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {Box, Card, Flex, Text} from '@sanity/ui'
import {type ReactNode} from 'react'
import {Box, Card, type CardProps, Flex, Text} from '@sanity/ui'
import {type ComponentType, type ReactNode} from 'react'

export function Chip(props: {avatar?: ReactNode; text: ReactNode; icon?: ReactNode}) {
type Props = {
avatar?: ReactNode
text: ReactNode
icon?: ReactNode
} & Pick<CardProps, 'tone'>

export const Chip: ComponentType<Props> = ({tone, ...props}) => {
const {avatar, text, icon} = props

return (
<Card muted radius="full">
<Card muted radius="full" tone={tone}>
<Flex align={'center'}>
{icon && (
<Box padding={1} marginLeft={1}>
Expand Down
31 changes: 30 additions & 1 deletion packages/sanity/src/core/util/draftUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,38 @@ export function getDraftId(id: string): DraftId {
return isDraftId(id) ? id : ((DRAFTS_PREFIX + id) as DraftId)
}

/** @internal */
export const systemBundles = ['drafts', 'published'] as const

/** @internal */
export type SystemBundle = (typeof systemBundles)[number]

/** @internal */
export function isSystemBundle(maybeSystemBundle: unknown): maybeSystemBundle is SystemBundle {
return systemBundles.includes(maybeSystemBundle as SystemBundle)
}

/** @internal */
const systemBundleNames = ['draft', 'published'] as const

/** @internal */
type SystemBundleName = (typeof systemBundleNames)[number]

/**
* `isSystemBundle` should be preferred, but some parts of the codebase currently use the singular
* "draft" name instead of the plural "drafts".
*
* @internal
*/
export function isSystemBundleName(
maybeSystemBundleName: unknown,
): maybeSystemBundleName is SystemBundleName {
return systemBundleNames.includes(maybeSystemBundleName as SystemBundleName)
}

/** @internal */
export function getVersionId(id: string, version: string): string {
if (version === 'drafts' || version === 'published') {
if (isSystemBundle(version)) {
throw new Error('Version can not be "published" or "drafts"')
}

Expand Down
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);
`
Loading
Loading