From 6c1a71e66755569c1fc8b85ce7f7ecb4b85ad2ff Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 25 Dec 2024 01:50:42 +0000 Subject: [PATCH] feat(releases): published label; unarchive server action; and fixture fix (#8140) --- .../releases/__fixtures__/release.fixture.ts | 2 - .../src/core/releases/i18n/resources.ts | 16 ++- .../ReleaseMenuButton/ReleaseMenuButton.tsx | 40 +++--- .../ReleasePublishAllButton.tsx | 6 +- .../tool/detail/ReleaseTypePicker.tsx | 121 +++++++++++------- .../__tests__/ReleaseTypePicker.test.tsx | 35 ++++- 6 files changed, 141 insertions(+), 79 deletions(-) diff --git a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts index 90aafcc034b..74f64fd6559 100644 --- a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts +++ b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts @@ -67,11 +67,9 @@ export const publishedASAPRelease: ReleaseDocument = { _createdAt: '2023-10-10T08:00:00Z', _updatedAt: '2023-10-10T09:00:00Z', state: 'published', - publishAt: '2023-10-10T09:00:00Z', metadata: { title: 'published Release', releaseType: 'asap', - intendedPublishAt: '2023-10-10T09:00:00Z', description: 'archived Release description', }, } diff --git a/packages/sanity/src/core/releases/i18n/resources.ts b/packages/sanity/src/core/releases/i18n/resources.ts index 40fa620f7aa..eafb48ec6af 100644 --- a/packages/sanity/src/core/releases/i18n/resources.ts +++ b/packages/sanity/src/core/releases/i18n/resources.ts @@ -42,10 +42,6 @@ const releasesLocaleStrings = { 'action.immediate-revert-release': 'Revert now', /** Label for unarchiving a release */ 'action.unarchive': 'Unarchive release', - /** Header for the dialog confirming the archive of a release */ - 'archive-dialog.confirm-archive-header': - "Are you sure you want to archive the '{{title}}' release?", - /* The text for the activity event when a document is added to a release */ 'activity.event.add-document': 'added a document version', /* The text for the activity event when the release is archived */ @@ -75,6 +71,9 @@ const releasesLocaleStrings = { /** The title for the activity panel shown in the releases detail screen */ 'activity.panel.title': 'Activity', + /** Header for the dialog confirming the archive of a release */ + 'archive-dialog.confirm-archive-header': + "Are you sure you want to archive the '{{title}}' release?", /** Title for the dialog confirming the archive of a release */ 'archive-dialog.confirm-archive-title': "Are you sure you want to archive the '{{title}}' release?", @@ -91,7 +90,9 @@ const releasesLocaleStrings = { /** Text for when a release / document was created */ 'created': 'Created ', - /** Text for the releases detail screen when a release was published */ + /** Text for the releases detail screen when a release was published ASAP */ + 'dashboard.details.published-asap': 'Published', + /** Text for the releases detail screen when a release was published from scheduling */ 'dashboard.details.published-on': 'Published on {{date}}', /** Text for the releases detail screen in the pin release button. */ @@ -304,6 +305,11 @@ const releasesLocaleStrings = { 'toast.unschedule.error': "Failed to unscheduled '{{title}}': {{error}}", /** Text for toast when release has been unschedule */ 'toast.unschedule.success': "The '{{title}}' release was unscheduled.", + /** Text for toast when release has been unarchived */ + 'toast.unarchive.success': "The '{{title}}' release was unarchived.", + /** Text for toast when release failed to unarchive */ + 'toast.unarchive.error': "Failed to unarchive '{{title}}': {{error}}", + /** Description for toast when release deletion failed */ /** Text for tooltip when a release has been scheduled */ 'type-picker.tooltip.scheduled': 'The release is scheduled, unschedule it to change type', /** Text for toast when release failed to revert */ diff --git a/packages/sanity/src/core/releases/tool/components/ReleaseMenuButton/ReleaseMenuButton.tsx b/packages/sanity/src/core/releases/tool/components/ReleaseMenuButton/ReleaseMenuButton.tsx index 13898f9162a..28c08ec93d6 100644 --- a/packages/sanity/src/core/releases/tool/components/ReleaseMenuButton/ReleaseMenuButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/ReleaseMenuButton/ReleaseMenuButton.tsx @@ -7,7 +7,7 @@ import { } from '@sanity/icons' import {type DefinedTelemetryLog, useTelemetry} from '@sanity/telemetry/react' import {Menu, Spinner, Text, useToast} from '@sanity/ui' -import {useCallback, useMemo, useState} from 'react' +import {type MouseEventHandler, useCallback, useMemo, useState} from 'react' import {useRouter} from 'sanity/router' import {Button, Dialog, MenuButton, MenuItem} from '../../../../../ui-components' @@ -15,6 +15,7 @@ import {Translate, useTranslation} from '../../../../i18n' import { ArchivedRelease, DeletedRelease, + UnarchivedRelease, UnscheduledRelease, } from '../../../__telemetry__/releases.telemetry' import {releasesLocaleNamespace} from '../../../i18n' @@ -32,7 +33,7 @@ export type ReleaseMenuButtonProps = { release: ReleaseDocument } -type ReleaseAction = 'archive' | 'delete' | 'unschedule' +type ReleaseAction = 'archive' | 'unarchive' | 'delete' | 'unschedule' interface BaseReleaseActionsMap { toastSuccessI18nKey: string @@ -78,6 +79,12 @@ const RELEASE_ACTION_MAP: Record< toastFailureI18nKey: 'toast.archive.error', telemetry: ArchivedRelease, }, + unarchive: { + confirmDialog: false, + toastSuccessI18nKey: 'toast.unarchive.success', + toastFailureI18nKey: 'toast.unarchive.error', + telemetry: UnarchivedRelease, + }, unschedule: { confirmDialog: false, toastSuccessI18nKey: 'toast.unschedule.success', @@ -89,7 +96,7 @@ const RELEASE_ACTION_MAP: Record< export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) => { const toast = useToast() const router = useRouter() - const {archive, deleteRelease, unschedule} = useReleaseOperations() + const {archive, unarchive, deleteRelease, unschedule} = useReleaseOperations() const {loading: isLoadingReleaseDocuments, results: releaseDocuments} = useBundleDocuments( getReleaseIdFromReleaseDocumentId(release._id), ) @@ -116,6 +123,7 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) const actionLookup = { delete: handleDelete, archive, + unarchive, unschedule, } const actionValues = RELEASE_ACTION_MAP[action] @@ -160,6 +168,7 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) releaseMenuDisabled, handleDelete, archive, + unarchive, unschedule, release._id, telemetry, @@ -169,11 +178,6 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) ], ) - const handleUnarchive = async () => { - // noop - // TODO: similar to handleArchive - complete once server action exists - } - const confirmActionDialog = useMemo(() => { if (!selectedAction) return null @@ -224,8 +228,10 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) t, ]) - const handleOnInitiateAction = useCallback( - (action: ReleaseAction) => { + const handleOnInitiateAction = useCallback>( + (event) => { + const action = event.currentTarget.getAttribute('data-value') as ReleaseAction + if (releaseDocuments.length > 0 && RELEASE_ACTION_MAP[action].confirmDialog) { setSelectedAction(action) } else { @@ -241,9 +247,8 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) if (release.state === 'archived') return ( handleOnInitiateAction('archive')} + data-value="archive" + onClick={handleOnInitiateAction} icon={ArchiveIcon} text={t('action.archive')} data-testid="archive-release-menu-item" @@ -270,7 +276,8 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) return ( handleOnInitiateAction('delete')} + data-value="delete" + onClick={handleOnInitiateAction} disabled={releaseMenuDisabled || isPerformingOperation} icon={TrashIcon} text={t('action.delete-release')} @@ -284,7 +291,8 @@ export const ReleaseMenuButton = ({ignoreCTA, release}: ReleaseMenuButtonProps) return ( handleOnInitiateAction('unschedule')} + data-value="unschedule" + onClick={handleOnInitiateAction} disabled={releaseMenuDisabled || isPerformingOperation} icon={CloseCircleIcon} text={t('action.unschedule')} diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleasePublishAllButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleasePublishAllButton.tsx index 527073e75f5..3fd5bc2fd7b 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleasePublishAllButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleasePublishAllButton.tsx @@ -2,7 +2,6 @@ import {ErrorOutlineIcon, PublishIcon} from '@sanity/icons' import {useTelemetry} from '@sanity/telemetry/react' import {Flex, Text, useToast} from '@sanity/ui' import {useCallback, useMemo, useState} from 'react' -import {useRouter} from 'sanity/router' import {Button, Dialog} from '../../../../../ui-components' import {ToneIcon} from '../../../../../ui-components/toneIcon/ToneIcon' @@ -27,7 +26,6 @@ export const ReleasePublishAllButton = ({ disabled, }: ReleasePublishAllButtonProps) => { const toast = useToast() - const router = useRouter() const {publishRelease} = useReleaseOperations() const {t} = useTranslation(releasesLocaleNamespace) const perspective = usePerspective() @@ -69,8 +67,6 @@ export const ReleasePublishAllButton = ({ ), }) - // TODO: handle a published release on the document list - router.navigate({}) if ( isReleaseDocument(perspective.selectedPerspective) && perspective.selectedPerspective?._id === release._id @@ -94,7 +90,7 @@ export const ReleasePublishAllButton = ({ } finally { setPublishBundleStatus('idle') } - }, [release, publishBundleStatus, publishRelease, telemetry, toast, t, router, perspective]) + }, [release, publishBundleStatus, publishRelease, telemetry, toast, t, perspective]) const confirmPublishDialog = useMemo(() => { if (publishBundleStatus === 'idle') return null diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx index 3bba587c407..77ad23f985a 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx @@ -1,5 +1,5 @@ import {LockIcon} from '@sanity/icons' -import {Flex, Spinner, Stack, TabList, Text, useClickOutsideEvent} from '@sanity/ui' +import {Card, Flex, Spinner, Stack, TabList, Text, useClickOutsideEvent} from '@sanity/ui' import {format, isBefore, isValid} from 'date-fns' import {isEqual} from 'lodash' import {useCallback, useEffect, useMemo, useRef, useState} from 'react' @@ -94,18 +94,31 @@ export function ReleaseTypePicker(props: {release: ReleaseDocument}): JSX.Elemen const isReleaseScheduled = isReleaseScheduledOrScheduling(release) const publishDateLabel = useMemo(() => { + if (release.state === 'published') { + if (isPublishDateInPast && release.publishAt) + return tRelease('dashboard.details.published-on', { + date: format(new Date(publishDate), 'MMM d, yyyy, pp'), + }) + + return tRelease('dashboard.details.published-asap') + } + if (releaseType === 'asap') return t('release.type.asap') if (releaseType === 'undecided') return t('release.type.undecided') const labelDate = publishDate || inputValue if (!labelDate) return null - if (isPublishDateInPast && release.publishAt) - return tRelease('dashboard.details.published-on', { - date: format(new Date(publishDate), 'MMM d, yyyy'), - }) - return format(new Date(labelDate), `PPpp`) - }, [inputValue, isPublishDateInPast, publishDate, release.publishAt, releaseType, t, tRelease]) + }, [ + inputValue, + isPublishDateInPast, + publishDate, + release.publishAt, + release.state, + releaseType, + t, + tRelease, + ]) const handleButtonReleaseTypeChange = useCallback((pickedReleaseType: ReleaseType) => { setDateInputOpen(pickedReleaseType === 'scheduled') @@ -189,6 +202,34 @@ export function ReleaseTypePicker(props: {release: ReleaseDocument}): JSX.Elemen ) } + const tone = + release.state === 'published' + ? 'positive' + : getReleaseTone({...release, metadata: {...release.metadata, releaseType}}) + + const labelContent = useMemo( + () => ( + + {isUpdating ? ( + + ) : ( + + )} + + + {publishDateLabel} + + + {isReleaseScheduled && ( + + + + )} + + ), + [isReleaseScheduled, isUpdating, publishDateLabel, tone], + ) + return ( } @@ -197,44 +238,34 @@ export function ReleaseTypePicker(props: {release: ReleaseDocument}): JSX.Elemen placement="bottom-start" ref={popoverRef} > - + {release.state === 'published' ? ( + + {labelContent} + + ) : ( + + )} ) } diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx index 739c86487fa..4d6c1ac769c 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx @@ -9,6 +9,7 @@ import { activeScheduledRelease, activeUndecidedRelease, publishedASAPRelease, + scheduledRelease, } from '../../../__fixtures__/release.fixture' import {releasesUsEnglishLocaleBundle} from '../../../i18n' import { @@ -34,7 +35,7 @@ const renderComponent = async (release = activeASAPRelease) => { render(, {wrapper}) await waitFor(() => { - expect(screen.getByTestId('release-type-picker')).toBeInTheDocument() + expect(screen.getByTestId('release-type-label')).toBeInTheDocument() }) } @@ -67,6 +68,26 @@ describe('ReleaseTypePicker', () => { expect(screen.getByText('Oct 10, 2023', {exact: false})).toBeInTheDocument() }) + + it('renders the label with a published text when release was asap published', async () => { + await renderComponent(publishedASAPRelease) + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + + expect(screen.getByTestId('published-release-type-label')).toBeInTheDocument() + + expect(screen.getByText('Published')).toBeInTheDocument() + }) + + it('renders the label with a published text when release was schedule published', async () => { + await renderComponent({...scheduledRelease, state: 'published'}) + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + + expect(screen.getByTestId('published-release-type-label')).toBeInTheDocument() + + expect(screen.getByText('Published on Oct 10, 2023, 3:00:00 AM')).toBeInTheDocument() + }) }) describe('interacting with the popup content', () => { @@ -127,8 +148,10 @@ describe('ReleaseTypePicker', () => { const Calendar = getByDataUi(document.body, 'CalendarMonth') - // Select the 10th day in the calendar + // Select the 10th day in the calendar month fireEvent.click(within(Calendar).getByText('10')) + fireEvent.change(screen.getByLabelText('Select hour'), {target: {value: 10}}) + fireEvent.change(screen.getByLabelText('Select minute'), {target: {value: 55}}) expect(mockUpdateRelease).not.toHaveBeenCalled() // Close the popup and check if the release is updated @@ -139,7 +162,8 @@ describe('ReleaseTypePicker', () => { metadata: expect.objectContaining({ ...activeASAPRelease.metadata, releaseType: 'scheduled', - intendedPublishAt: expect.stringMatching(/^\d{4}-\d{2}-10T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), + /** @todo improve the assertion on the dateTime */ + intendedPublishAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:55:\d{2}\.\d{3}Z$/), }), }) }) @@ -172,11 +196,10 @@ describe('ReleaseTypePicker', () => { expect(pickerButton).toBeDisabled() }) - it('disables the picker for published releases', async () => { + it('does not show button for picker when release is published state', async () => { await renderComponent(publishedASAPRelease) - const pickerButton = screen.getByRole('button') - expect(pickerButton).toBeDisabled() + expect(screen.queryByRole('button')).not.toBeInTheDocument() }) it('shows a spinner when updating the release', async () => {