Skip to content

Commit

Permalink
feat(core): add versions to references and status icons (#7690)
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin authored Oct 29, 2024
1 parent 76894c1 commit ccdb82f
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 203 deletions.
3 changes: 3 additions & 0 deletions packages/@sanity/types/src/schema/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface PrepareViewOptions {

/** @public */
export interface PreviewValue {
_id?: string
_createdAt?: string
_updatedAt?: string
title?: string
subtitle?: string
description?: string
Expand Down
158 changes: 75 additions & 83 deletions packages/sanity/src/core/components/documentStatus/DocumentStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import {type PreviewValue, type SanityDocument} from '@sanity/types'
import {Flex, Text} from '@sanity/ui'
import {type BadgeTone, Flex, Text} from '@sanity/ui'
import {useMemo} from 'react'
import {styled} from 'styled-components'
import {useReleases} from 'sanity'

import {useDateTimeFormat, useRelativeTime} from '../../hooks'
import {useRelativeTime} from '../../hooks'
import {useTranslation} from '../../i18n'
import {type VersionsRecord} from '../../preview/utils/getPreviewStateObservable'
import {type CurrentPerspective} from '../../releases'
import {PerspectiveBadge} from '../perspective/PerspectiveBadge'
import {getReleaseTone, ReleaseAvatar} from '../../releases'

interface DocumentStatusProps {
absoluteDate?: boolean
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
// eslint-disable-next-line
versions?: VersionsRecord
versions?: VersionsRecord | Record<string, {snapshot: DocumentStatusProps['draft']}>
singleLine?: boolean
currentGlobalBundle?: CurrentPerspective
}

const StyledText = styled(Text)`
white-space: nowrap;
`

/**
* Displays document status indicating both last published and edited dates in either relative (the default)
* or absolute formats.
Expand All @@ -34,86 +25,87 @@ const StyledText = styled(Text)`
*
* @internal
*/
export function DocumentStatus({
absoluteDate,
draft,
published,
version,
singleLine,
currentGlobalBundle,
}: DocumentStatusProps) {
export function DocumentStatus({draft, published, versions, singleLine}: DocumentStatusProps) {
const {data: releases} = useReleases()
const versionsList = useMemo(() => Object.entries(versions ?? {}), [versions])
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({
dateStyle: 'medium',
timeStyle: 'short',
})

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,
useTemporalPhrase: true,
})
const publishedUpdatedTimeAgo = useRelativeTime(publishedUpdatedAt || '', {
minimal: true,
useTemporalPhrase: true,
})
const versionUpdatedTimeAgo = useRelativeTime(versionUpdatedAt || '', {
minimal: true,
useTemporalPhrase: true,
})

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

const title = currentGlobalBundle?.metadata?.title

const documentStatus = useMemo(() => {
if (published && '_id' in published) {
return 'published'
} else if (version && '_id' in version) {
return 'version'
}

return 'draft'
}, [published, version])

return (
<Flex
align={singleLine ? 'center' : 'flex-start'}
data-testid="pane-footer-document-status"
direction={singleLine ? 'row' : 'column'}
gap={2}
gap={3}
wrap="nowrap"
>
{version && currentGlobalBundle && (
<PerspectiveBadge releaseTitle={title} documentStatus={documentStatus} />
{published && (
<VersionStatus
title={t('release.chip.published')}
mode="published"
timestamp={published._updatedAt}
tone={'positive'}
/>
)}

{!version && !publishedDate && (
<StyledText size={1} weight="medium">
{t('document-status.not-published')}
</StyledText>
)}
{!version && publishedDate && (
<StyledText size={1} weight="medium">
{t('document-status.published', {date: publishedDate})}
</StyledText>
)}
{updatedDate && (
<StyledText muted size={1} wrap="nowrap">
{t('document-status.edited', {date: updatedDate})}
</StyledText>
{draft && (
<VersionStatus
title={t('release.chip.draft')}
mode="draft"
timestamp={draft._updatedAt}
tone="caution"
/>
)}
{versionsList.map(([versionName, {snapshot}]) => {
const release = releases?.find((r) => r.name === versionName)
return (
<VersionStatus
key={versionName}
mode={snapshot?._updatedAt === snapshot?._createdAt ? 'created' : 'edited'}
title={release?.metadata.title || versionName}
timestamp={snapshot?._updatedAt}
tone={release ? getReleaseTone(release) : 'default'}
/>
)
})}
</Flex>
)
}

type Mode = 'edited' | 'created' | 'draft' | 'published'

const labels: Record<Mode, string> = {
draft: 'document-status.edited',
published: 'document-status.date',
edited: 'document-status.edited',
created: 'document-status.created',
}

const VersionStatus = ({
title,
timestamp,
mode,
tone,
}: {
title: string
mode: Mode
timestamp?: string
tone: BadgeTone
}) => {
const {t} = useTranslation()

const relativeTime = useRelativeTime(timestamp || '', {
minimal: true,
useTemporalPhrase: true,
})

return (
<Flex align="center" gap={2}>
<ReleaseAvatar tone={tone} padding={0} />
<Text size={1}>
{title}{' '}
<span style={{color: 'var(--card-muted-fg-color)'}}>
{t(labels[mode], {date: relativeTime})}
</span>
</Text>
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,92 @@
import {DotIcon} from '@sanity/icons'
import {type PreviewValue, type SanityDocument} from '@sanity/types'
import {Text} from '@sanity/ui'
import {useMemo} from 'react'
import {Flex, Text} from '@sanity/ui'
import {type ComponentType, useMemo} from 'react'
import {styled} from 'styled-components'

import {type VersionsRecord} from '../../preview/utils/getPreviewStateObservable'
import {useReleases} from '../../store/release/useReleases'

interface DocumentStatusProps {
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
// eslint-disable-next-line
versions?: VersionsRecord
versions: VersionsRecord | undefined
}

const Root = styled(Text)`
&[data-status='edited'] {
--card-icon-color: var(--card-badge-caution-dot-color);
}
&[data-status='unpublished'] {
&[data-status='not-published'] {
--card-icon-color: var(--card-badge-default-dot-color);
opacity: 0.5 !important;
}
&[data-status='draft'] {
--card-icon-color: var(--card-badge-caution-dot-color);
}
&[data-status='asap'] {
--card-icon-color: var(--card-badge-critical-dot-color);
}
&[data-status='undecided'] {
--card-icon-color: var(--card-badge-explore-dot-color);
}
&[data-status='scheduled'] {
--card-icon-color: var(--card-badge-primary-dot-color);
}
`

type Status = 'not-published' | 'draft' | 'asap' | 'scheduled' | 'undecided'

/**
* Renders a dot indicating the current document status.
*
* - 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 or for version documents.
*
* @internal
*/
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, $version])

// Return null if the document is:
// - Published without edits
// - Neither published or without edits (this shouldn't be possible)
// - A version
if ((!$draft && !$published) || (!$draft && $published) || $version) {
return null
}
export function DocumentStatusIndicator({draft, published, versions}: DocumentStatusProps) {
const {data: releases} = useReleases()
const versionsList = useMemo(
() =>
versions
? Object.keys(versions).map((versionName) => {
const release = releases?.find((r) => r.name === versionName)
return release?.metadata.releaseType
})
: [],
[releases, versions],
)

const indicators: {
status: Status
show: boolean
}[] = [
{
status: draft && !published ? 'not-published' : 'draft',
show: Boolean(draft),
},
{
status: 'asap',
show: versionsList.includes('asap'),
},
{
status: 'scheduled',
show: versionsList.includes('scheduled'),
},
{
status: 'undecided',
show: versionsList.includes('undecided'),
},
]

// TODO: Remove debug `status[0]` output.
return (
<Root data-status={status} size={1}>
<DotIcon />
</Root>
<Flex>
{indicators
.filter(({show}) => show)
.map(({status}) => (
<Dot key={status} status={status} />
))}
</Flex>
)
}

const Dot: ComponentType<{status: Status}> = ({status}) => (
<Root data-status={status} size={1}>
<DotIcon />
</Root>
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ export function ReferencePreview(props: {
[previewId, refType.name],
)

const {draft, published, version} = preview

const previewProps = useMemo(
() => ({
children: (
Expand All @@ -61,28 +59,31 @@ export function ReferencePreview(props: {
<DocumentStatusIndicator
draft={preview.draft}
published={preview.published}
version={preview.version}
versions={preview.versions}
/>
</Inline>
</Box>
),
layout,
schemaType: refType,
tooltip: <DocumentStatus draft={draft} published={published} version={version} />,
tooltip: (
<DocumentStatus
draft={preview.draft}
published={preview.published}
versions={preview.versions}
/>
),
value: previewStub,
}),
[
documentPresence,
draft,
layout,
preview.draft,
preview.published,
preview.version,
preview.versions,
previewStub,
published,
refType,
showTypeLabel,
version,
],
)

Expand Down
2 changes: 2 additions & 0 deletions packages/sanity/src/core/form/inputs/ReferenceInput/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {type ComponentType, type ReactNode} from 'react'
import {type Observable} from 'rxjs'

import {type DocumentAvailability} from '../../../preview'
import {type VersionsRecord} from '../../../preview/utils/getPreviewStateObservable'
import {type ObjectInputProps} from '../../types'

export type PreviewDocumentValue = PreviewValue & {
Expand All @@ -25,6 +26,7 @@ export interface ReferenceInfo {
draft: PreviewDocumentValue | undefined
published: PreviewDocumentValue | undefined
version: PreviewDocumentValue | undefined
versions: VersionsRecord
}
}

Expand Down
Loading

0 comments on commit ccdb82f

Please sign in to comment.