Skip to content

Commit

Permalink
feat(sanity): intial context menu for version badges
Browse files Browse the repository at this point in the history
  • Loading branch information
RitaDias committed Oct 16, 2024
1 parent 8e9d1d5 commit 4dd3b7c
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const createVersion: OperationImpl<[baseDocumentId: string], 'NO_NEW_VERS
return snapshots.published || snapshots.draft ? false : 'NO_NEW_VERSION'
},
execute: ({schema, client, snapshots, typeName}, dupeId) => {
const source = snapshots.draft || snapshots.published
const source = snapshots.version || snapshots.draft || snapshots.published

if (!source) {
throw new Error('cannot execute on empty document')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {DotIcon} from '@sanity/icons'
// eslint-disable-next-line no-restricted-imports -- Bundle Button requires more fine-grained styling than studio button
import {Text} from '@sanity/ui'
// eslint-disable-next-line camelcase
import {memo, useCallback} from 'react'
import {getVersionFromId, useDateTimeFormat, usePerspective, useTranslation} from 'sanity'
import {
getVersionFromId,
useBundles,
useDateTimeFormat,
usePerspective,
useTranslation,
} from 'sanity'

import {versionDocumentExists} from '../../../../../../core/releases'
import {usePaneRouter} from '../../../../../components'
import {useDocumentPane} from '../../../useDocumentPane'
import {VersionChip} from './VersionChip'
import {VersionPopoverMenu} from './VersionPopoverMenu'

export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() {
const paneRouter = usePaneRouter()
Expand All @@ -17,8 +23,18 @@ export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() {
dateStyle: 'medium',
timeStyle: 'short',
})
const {data: bundles, loading} = useBundles()

const {documentVersions, editState, displayed} = useDocumentPane()
const {documentVersions, editState, displayed, documentType} = useDocumentPane()

// remove the versions that the document already has
// remove the archived releases
const filteredReleases =
(documentVersions &&
bundles?.filter(
(bundle) => !versionDocumentExists(documentVersions, bundle._id) && !bundle.archivedAt,
)) ||
[]

const handleBundleChange = useCallback(
(bundleId: string) => () => {
Expand Down Expand Up @@ -89,6 +105,15 @@ export const DocumentPerspectiveMenu = memo(function DocumentPerspectiveMenu() {
text={release.title}
tone={'primary'}
icon={DotIcon}
menuContent={
<VersionPopoverMenu
documentId={displayed?._id || ''}
menuReleaseId={release._id}
releases={filteredReleases}
releasesLoading={loading}
documentType={documentType}
/>
}
/>
))}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {memo, type ReactNode} from 'react'
import {useClickOutsideEvent, useGlobalKeyDown} from '@sanity/ui'
import {memo, type MouseEvent, type ReactNode, useCallback, useMemo, useRef, useState} from 'react'
import {styled} from 'styled-components'

import {type BundleDocument, type releaseType} from '../../../../../../core'
import {Button, Tooltip} from '../../../../../../ui-components'
import {Button, Popover, Tooltip} from '../../../../../../ui-components'

const Chip = styled(Button)`
border-radius: 9999px !important;
Expand All @@ -26,9 +27,67 @@ export const VersionChip = memo(function VersionChip(props: {
text: string
tone: 'default' | 'primary' | 'positive' | 'caution' | 'critical'
icon: React.ComponentType
menuContent?: ReactNode
}) {
const {disabled, releaseOptions, selected, tooltipContent, version, onClick, text, tone, icon} =
props
const {
disabled,
releaseOptions,
selected,
tooltipContent,
version,
onClick,
text,
tone,
icon,
menuContent,
} = props

const [contextMenuPoint, setContextMenuPoint] = useState<{x: number; y: number} | undefined>(
undefined,
)
const popoverRef = useRef<HTMLDivElement | null>(null)

const close = useCallback(() => setContextMenuPoint(undefined), [])

const handleContextMenu = useCallback((event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()

setContextMenuPoint({x: event.clientX, y: event.clientY})
}, [])

useClickOutsideEvent(close, () => [popoverRef.current])

useGlobalKeyDown(
useCallback(
(event) => {
if (event.key === 'Escape') {
close()
}
},
[close],
),
)

const referenceElement = useMemo(() => {
if (!contextMenuPoint) {
return null
}

return {
getBoundingClientRect() {
return {
x: contextMenuPoint.x,
y: contextMenuPoint.y,
left: contextMenuPoint.x,
top: contextMenuPoint.y,
right: contextMenuPoint.x,
bottom: contextMenuPoint.y,
width: 0,
height: 0,
}
},
} as HTMLElement
}, [contextMenuPoint])

return (
<>
Expand All @@ -45,8 +104,20 @@ export const VersionChip = memo(function VersionChip(props: {
text={text}
tone={tone}
icon={icon}
onContextMenu={handleContextMenu}
/>
</Tooltip>

<Popover
content={menuContent}
fallbackPlacements={[]}
open={Boolean(referenceElement)}
portal
placement="bottom-start"
ref={popoverRef}
referenceElement={referenceElement}
zOffset={1000}
/>
</>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {CalendarIcon, CopyIcon, TrashIcon} from '@sanity/icons'
import {useTelemetry} from '@sanity/telemetry/react'
import {Menu, MenuDivider, Spinner, useToast} from '@sanity/ui'
import {memo, useCallback, useState} from 'react'
import {filter, firstValueFrom} from 'rxjs'
import {
type BundleDocument,
DEFAULT_STUDIO_CLIENT_OPTIONS,
getPublishedId,
getVersionFromId,
getVersionId,
isVersionId,
Translate,
useClient,
useDocumentOperation,
useDocumentStore,
usePerspective,
useTranslation,
} from 'sanity'
import {IntentLink} from 'sanity/router'

import {AddedVersion} from '../../../../../../core/releases/__telemetry__/releases.telemetry'
import {MenuGroup, MenuItem} from '../../../../../../ui-components'

export const VersionPopoverMenu = memo(function VersionPopoverMenu(props: {
documentId: string
releases: BundleDocument[]
releasesLoading: boolean
documentType: string
menuReleaseId: string
}) {
const {documentId, releases, releasesLoading, documentType, menuReleaseId} = props
const [isDiscarding, setIsDiscarding] = useState(false)
const {t} = useTranslation()
const {setPerspective} = usePerspective()
const isVersion = isVersionId(documentId)

const optionsReleaseList = releases.map((release) => ({
value: release,
}))

const toast = useToast()

const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
const publishedId = getPublishedId(documentId)

const releaseId = isVersion ? getVersionFromId(documentId) : documentId

const {createVersion} = useDocumentOperation(publishedId, documentType, menuReleaseId)

const telemetry = useTelemetry()
const documentStore = useDocumentStore()

const handleAddVersion = useCallback(
async (targetRelease: string) => {
// set up the listener before executing
const createVersionSuccess = firstValueFrom(
documentStore.pair
.operationEvents(getPublishedId(documentId), documentType)
.pipe(filter((e) => e.op === 'createVersion' && e.type === 'success')),
)

const docId = getVersionId(publishedId, targetRelease)

createVersion.execute(docId)

// only change if the version was created successfully
await createVersionSuccess
setPerspective(targetRelease)

telemetry.log(AddedVersion, {
schemaType: documentType,
documentOrigin: isVersion ? 'version' : 'draft',
})
},
[
createVersion,
documentId,
documentStore.pair,
documentType,
isVersion,
publishedId,
setPerspective,
telemetry,
],
)

const handleDiscardVersion = useCallback(async () => {
setIsDiscarding(true)
try {
// if it's a version it'll always use the versionId based on the menu that it's pressing
// otherwise it'll use the documentId
const docId = isVersion ? getVersionId(documentId, menuReleaseId) : documentId

await client.delete(docId)

toast.push({
closable: true,
status: 'success',
description: (
<Translate
t={t}
i18nKey={'release.action.discard-version.success'}
values={{title: document.title as string}}
/>
),
})
} catch (e) {
toast.push({
closable: true,
status: 'error',
title: t('release.action.discard-version.failure'),
})
}

setIsDiscarding(false)
}, [client, documentId, isVersion, menuReleaseId, t, toast])

/* @todo update literal */
return (
<Menu>
{isVersion && (
<MenuItem
as={IntentLink}
icon={CalendarIcon}
// eslint-disable-next-line @sanity/i18n/no-attribute-string-literals
text={`View release`}
params={{id: releaseId}}
intent={'release'}
target="_blank"
rel="noopener noreferrer"
/>
)}
{releasesLoading && <Spinner />}
<MenuGroup icon={CopyIcon} popover={{placement: 'right-start'}} text="Copy version to">
{optionsReleaseList.map((option) => (
<MenuItem
key={option.value._id}
onClick={() => handleAddVersion(option.value._id)}
text={option.value.title}
/>
))}
</MenuGroup>
<MenuDivider />
<MenuItem
icon={TrashIcon}
onClick={handleDiscardVersion}
disabled={isDiscarding}
text={t('release.action.discard-version')}
/>
</Menu>
)
})

0 comments on commit 4dd3b7c

Please sign in to comment.