Skip to content

Commit

Permalink
feat(releases): adding correct confirmation dialog to archive release…
Browse files Browse the repository at this point in the history
… flow (#7827)
  • Loading branch information
jordanl17 authored Nov 18, 2024
1 parent eea0283 commit 474bfba
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 135 deletions.
17 changes: 17 additions & 0 deletions packages/sanity/src/core/releases/__fixtures__/release.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {type ReleaseDocument} from '../store/types'

export const activeScheduledRelease: ReleaseDocument = {
_id: '_.releases.activeRelease',
_type: 'system.release',
createdBy: '',
_createdAt: '2023-10-01T08:00:00Z',
_updatedAt: '2023-10-01T09:00:00Z',
state: 'active',
name: 'activeRelease',
metadata: {
title: 'active Release',
releaseType: 'scheduled',
intendedPublishAt: '2023-10-01T10:00:00Z',
description: 'active Release description',
},
}
14 changes: 14 additions & 0 deletions packages/sanity/src/core/releases/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ const releasesLocaleStrings = {
'actions.summary': 'Summary',
/** Label for unarchiving a release */
'action.unarchive': 'Unarchive release',
/** Title for the dialog confirming the archive of a release */
'archive-dialog.confirm-archive-title':
"Are you sure you want to archive the <strong>'{{title}}'</strong> release?",
/** Description for the dialog confirming the archive of a release with one document */
'archive-dialog.confirm-archive-description_one': 'This will archive 1 document version.',
/** Description for the dialog confirming the publish of a release with more than one document */
'archive-dialog.confirm-archive-description_other':
'This will archive {{count}} document versions.',
/** Label for the button to proceed with archiving a release */
'archive-dialog.confirm-archive-button': 'Yes, archive now',

/** Title for changes to published documents */
'changes-published-docs.title': 'Changes to published documents',
Expand Down Expand Up @@ -203,6 +213,10 @@ const releasesLocaleStrings = {
'table-header.edited': 'Edited',
/** Header for the document table in the release tool - time */
'table-header.time': 'Time',
/** Text for toast when release has been archived */
'toast.archive.success': "The '<strong>{{title}}</strong>' release was archived.",
/** Text for toast when release failed to archive */
'toast.archive.error': "Failed to archive '<strong>{{title}}</strong>': {{error}}",
/** Text for toast when release failed to publish */
'toast.publish.error': "Failed to publish '<strong>{{title}}</strong>': {{error}}",
/** Text for toast when release has been published */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {type Mocked, vi} from 'vitest'

import {type ReleaseOperationsStore} from '../../createReleaseOperationStore'

export const useReleaseOperationsMock: Mocked<ReleaseOperationsStore> = {
archive: vi.fn(),
unarchive: vi.fn(),
createRelease: vi.fn(),
createVersion: vi.fn(),
discardVersion: vi.fn(),
publishRelease: vi.fn(),
schedule: vi.fn(),
unschedule: vi.fn(),
updateRelease: vi.fn(),
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ export function createReleaseOperationsStore(options: {
client: SanityClient
}): ReleaseOperationsStore {
const {client} = options
const handleCreateRelease = async (release: EditableReleaseDocument) => {
await requestAction(client, {
const handleCreateRelease = (release: EditableReleaseDocument) =>
requestAction(client, {
actionType: 'sanity.action.release.create',
releaseId: getBundleIdFromReleaseDocumentId(release._id),
[METADATA_PROPERTY_NAME]: release.metadata,
})
}

const handleUpdateRelease = async (release: EditableReleaseDocument) => {
const bundleId = getBundleIdFromReleaseDocumentId(release._id)
Expand All @@ -56,49 +55,46 @@ export function createReleaseOperationsStore(options: {
})
}

const handlePublishRelease = async (releaseId: string) =>
const handlePublishRelease = (releaseId: string) =>
requestAction(client, [
{
actionType: 'sanity.action.release.publish',
releaseId: getBundleIdFromReleaseDocumentId(releaseId),
},
])

const handleScheduleRelease = async (releaseId: string, publishAt: Date) => {
await requestAction(client, [
const handleScheduleRelease = (releaseId: string, publishAt: Date) =>
requestAction(client, [
{
actionType: 'sanity.action.release.schedule',
releaseId: getBundleIdFromReleaseDocumentId(releaseId),
publishAt: publishAt.toISOString(),
},
])
}
const handleUnscheduleRelease = async (releaseId: string) => {
await requestAction(client, [

const handleUnscheduleRelease = (releaseId: string) =>
requestAction(client, [
{
actionType: 'sanity.action.release.unschedule',
releaseId: getBundleIdFromReleaseDocumentId(releaseId),
},
])
}

const handleArchiveRelease = async (releaseId: string) => {
await requestAction(client, [
const handleArchiveRelease = (releaseId: string) =>
requestAction(client, [
{
actionType: 'sanity.action.release.archive',
releaseId: getBundleIdFromReleaseDocumentId(releaseId),
},
])
}

const handleUnarchiveRelease = async (releaseId: string) => {
await requestAction(client, [
const handleUnarchiveRelease = (releaseId: string) =>
requestAction(client, [
{
actionType: 'sanity.action.release.unarchive',
releaseId: getBundleIdFromReleaseDocumentId(releaseId),
},
])
}

const handleCreateVersion = async (releaseId: string, documentId: string) => {
// the documentId will show you where the document is coming from and which
Expand Down Expand Up @@ -127,18 +123,13 @@ export function createReleaseOperationsStore(options: {
: client.create(versionDocument))
}

const handleDiscardVersion = async (releaseId: string, documentId: string) => {
if (!document) {
throw new Error(`Document with id ${documentId} not found`)
}

await requestAction(client, [
const handleDiscardVersion = (releaseId: string, documentId: string) =>
requestAction(client, [
{
actionType: 'sanity.action.document.discard',
draftId: getVersionId(documentId, releaseId),
},
])
}

return {
archive: handleArchiveRelease,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {ArchiveIcon, ArrowRightIcon, EllipsisHorizontalIcon, UnarchiveIcon} from '@sanity/icons'
import {ArchiveIcon, EllipsisHorizontalIcon, UnarchiveIcon} from '@sanity/icons'
import {useTelemetry} from '@sanity/telemetry/react'
import {Box, Flex, Menu, Spinner, Text} from '@sanity/ui'
import {type FormEventHandler, useState} from 'react'
import {Menu, Spinner, Text, useToast} from '@sanity/ui'
import {useCallback, useMemo, useState} from 'react'

import {Button, Dialog, MenuButton, MenuItem} from '../../../../../ui-components'
import {LoadingBlock} from '../../../../components/loadingBlock'
import {useTranslation} from '../../../../i18n'
import {ArchivedRelease, UnarchivedRelease} from '../../../__telemetry__/releases.telemetry'
import {Translate, useTranslation} from '../../../../i18n'
import {ArchivedRelease} from '../../../__telemetry__/releases.telemetry'
import {releasesLocaleNamespace} from '../../../i18n'
import {type ReleaseDocument} from '../../../store/types'
import {useReleaseOperations} from '../../../store/useReleaseOperations'
import {getBundleIdFromReleaseDocumentId} from '../../../util/getBundleIdFromReleaseDocumentId'
import {useBundleDocuments} from '../../detail/useBundleDocuments'

export type ReleaseMenuButtonProps = {
disabled?: boolean
Expand All @@ -19,35 +20,125 @@ export type ReleaseMenuButtonProps = {
const ARCHIVABLE_STATES = ['active', 'published']

export const ReleaseMenuButton = ({disabled, release}: ReleaseMenuButtonProps) => {
const {archive, unarchive} = useReleaseOperations()
const toast = useToast()
const {archive} = useReleaseOperations()
const {loading: isLoadingReleaseDocuments, results: releaseDocuments} = useBundleDocuments(
getBundleIdFromReleaseDocumentId(release._id),
)
const [isPerformingOperation, setIsPerformingOperation] = useState(false)
const [selectedAction, setSelectedAction] = useState<'edit' | 'confirm-archive'>()

const releaseMenuDisabled = !release || disabled
const releaseMenuDisabled = !release || isLoadingReleaseDocuments || disabled
const {t} = useTranslation(releasesLocaleNamespace)
const telemetry = useTelemetry()

const handleArchive = async (e: Parameters<FormEventHandler<HTMLFormElement>>[0]) => {
const handleArchive = useCallback(async () => {
if (releaseMenuDisabled) return
e.preventDefault()

setIsPerformingOperation(true)
await archive(release._id)
try {
setIsPerformingOperation(true)
await archive(release._id)

// it's in the process of becoming true, so the event we want to track is archive
telemetry.log(ArchivedRelease)
setIsPerformingOperation(false)
}
// it's in the process of becoming true, so the event we want to track is archive
telemetry.log(ArchivedRelease)
toast.push({
closable: true,
status: 'success',
title: (
<Text muted size={1}>
<Translate
t={t}
i18nKey="toast.archive.success"
values={{title: release.metadata.title}}
/>
</Text>
),
})
} catch (archivingError) {
toast.push({
status: 'error',
title: (
<Text muted size={1}>
<Translate
t={t}
i18nKey="toast.archive.error"
values={{title: release.metadata.title, error: archivingError.toString()}}
/>
</Text>
),
})
console.error(archivingError)
} finally {
setIsPerformingOperation(false)
setSelectedAction(undefined)
}
}, [archive, release._id, release.metadata.title, releaseMenuDisabled, t, telemetry, toast])

const handleUnarchive = async () => {
setIsPerformingOperation(true)
await unarchive(release._id)

// it's in the process of becoming false, so the event we want to track is unarchive
telemetry.log(UnarchivedRelease)
setIsPerformingOperation(false)
// noop
// TODO: similar to handleArchive - complete once server action exists
}

const confirmArchiveDialog = useMemo(() => {
if (selectedAction !== 'confirm-archive') return null

const dialogDescription =
releaseDocuments.length === 1
? 'archive-dialog.confirm-archive-description_one'
: 'archive-dialog.confirm-archive-description_other'

return (
<Dialog
id="confirm-archive-dialog"
data-testid="confirm-archive-dialog"
header={
<Translate
t={t}
i18nKey={'archive-dialog.confirm-archive-title'}
values={{
title: release.metadata.title,
}}
/>
}
onClose={() => setSelectedAction(undefined)}
footer={{
confirmButton: {
text: t('archive-dialog.confirm-archive-button'),
tone: 'positive',
onClick: handleArchive,
loading: isPerformingOperation,
disabled: isPerformingOperation,
},
}}
>
<Text muted size={1}>
<Translate
t={t}
i18nKey={dialogDescription}
values={{
count: releaseDocuments.length,
}}
/>
</Text>
</Dialog>
)
}, [
handleArchive,
isPerformingOperation,
release.metadata.title,
releaseDocuments.length,
selectedAction,
t,
])

const handleOnInitiateArchive = useCallback(() => {
if (releaseDocuments.length > 0) {
setSelectedAction('confirm-archive')
} else {
handleArchive()
}
}, [handleArchive, releaseDocuments.length])

return (
<>
<MenuButton
Expand All @@ -67,17 +158,19 @@ export const ReleaseMenuButton = ({disabled, release}: ReleaseMenuButtonProps) =
{!release?.state || release.state === 'archived' ? (
<MenuItem
onClick={handleUnarchive}
// TODO: disabled as CL action not yet impl
disabled
icon={UnarchiveIcon}
text={t('action.unarchive')}
data-testid="unarchive-release"
/>
) : (
<MenuItem
tooltipProps={{
disabled: ARCHIVABLE_STATES.includes(release.state),
disabled: ARCHIVABLE_STATES.includes(release.state) || isPerformingOperation,
content: t('action.archive.tooltip'),
}}
onClick={() => setSelectedAction('confirm-archive')}
onClick={handleOnInitiateArchive}
icon={ArchiveIcon}
text={t('action.archive')}
data-testid="archive-release"
Expand All @@ -94,36 +187,7 @@ export const ReleaseMenuButton = ({disabled, release}: ReleaseMenuButtonProps) =
tone: 'default',
}}
/>
{selectedAction === 'confirm-archive' && (
<Dialog
header="Confirm archiving release"
id="create-release-dialog"
onClose={() => setSelectedAction(undefined)}
width={1}
>
<form onSubmit={handleArchive}>
<Box padding={4}>
{/* TODO localize string */}
{/* eslint-disable-next-line i18next/no-literal-string */}
<Text>Are you sure you want to archive the release? There's no going back (yet)</Text>
{isPerformingOperation && <LoadingBlock showText title={'archiving, wait'} />}
</Box>
<Flex justify="flex-end" paddingTop={5}>
<Button
size="large"
iconRight={ArrowRightIcon}
type="submit"
text={
// TODO localize string
// eslint-disable-next-line @sanity/i18n/no-attribute-string-literals
'Archive release'
}
data-testid="archive-release-button"
/>
</Flex>
</form>
</Dialog>
)}
{confirmArchiveDialog}
</>
)
}
Loading

0 comments on commit 474bfba

Please sign in to comment.