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(core): add versions to references and status icons #7690

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 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
Copy link
Contributor

Choose a reason for hiding this comment

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

The simplification of this makes me so happy 🥹

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
Loading