diff --git a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx index 041a2c3c97a..e02e3e38eb1 100644 --- a/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx +++ b/packages/sanity/src/core/releases/components/BundleMenuButton/BundleMenuButton.tsx @@ -1,5 +1,4 @@ -/* eslint-disable @sanity/i18n/no-attribute-string-literals */ -import {EllipsisHorizontalIcon, TrashIcon} from '@sanity/icons' +import {ArchiveIcon, EllipsisHorizontalIcon, TrashIcon} from '@sanity/icons' import {Button, Menu, MenuButton, MenuItem, Spinner} from '@sanity/ui' import {useState} from 'react' import {useRouter} from 'sanity/router' @@ -12,8 +11,9 @@ type Props = { } export const BundleMenuButton = ({bundle}: Props) => { - const {deleteBundle} = useBundleOperations() + const {deleteBundle, updateBundle} = useBundleOperations() const router = useRouter() + const isBundleArchived = !!bundle?.archivedAt const [isPerformingOperation, setIsPerformingOperation] = useState(false) const bundleMenuDisabled = !bundle @@ -30,11 +30,22 @@ export const BundleMenuButton = ({bundle}: Props) => { } } + const handleOnToggleArchive = async () => { + if (bundleMenuDisabled) return + + setIsPerformingOperation(true) + await updateBundle({ + ...bundle, + archivedAt: isBundleArchived ? undefined : new Date().toISOString(), + }) + setIsPerformingOperation(false) + } + return ( { id="bundle-menu" menu={ - + + } popover={{ diff --git a/packages/sanity/src/core/releases/components/BundleMenuButton/__tests__/BundleMenuButton.test.tsx b/packages/sanity/src/core/releases/components/BundleMenuButton/__tests__/BundleMenuButton.test.tsx new file mode 100644 index 00000000000..113c3e6384f --- /dev/null +++ b/packages/sanity/src/core/releases/components/BundleMenuButton/__tests__/BundleMenuButton.test.tsx @@ -0,0 +1,108 @@ +import {describe, expect, jest, test} from '@jest/globals' +import {fireEvent, render, screen} from '@testing-library/react' +import {act} from 'react' +import {useRouter} from 'sanity/router' + +import {createTestProvider} from '../../../../../../test/testUtils/TestProvider' +import {type BundleDocument} from '../../../../store/bundles/types' +import {useBundleOperations} from '../../../../store/bundles/useBundleOperations' +import {releasesUsEnglishLocaleBundle} from '../../../i18n' +import {BundleMenuButton} from '../BundleMenuButton' + +jest.mock('../../../../store/bundles/useBundleOperations', () => ({ + useBundleOperations: jest.fn().mockReturnValue({ + deleteBundle: jest.fn(), + updateBundle: jest.fn(), + }), +})) + +jest.mock('sanity/router', () => ({ + ...(jest.requireActual('sanity/router') || {}), + useRouter: jest.fn().mockReturnValue({state: {}, navigate: jest.fn()}), +})) + +const renderTest = async (bundle: BundleDocument) => { + const wrapper = await createTestProvider({ + resources: [releasesUsEnglishLocaleBundle], + }) + return render(, {wrapper}) +} + +describe('BundleMenuButton', () => { + test('will archive an unarchived bundle', async () => { + const activeBundle: BundleDocument = { + _id: 'activeBundle', + _type: 'bundle', + archivedAt: undefined, + title: 'activeBundle', + name: 'activeBundle', + authorId: 'author', + _createdAt: new Date().toISOString(), + _updatedAt: new Date().toISOString(), + _rev: '', + } + + await renderTest(activeBundle) + + fireEvent.click(screen.getByLabelText('Release menu')) + + await act(() => { + fireEvent.click(screen.getByText('Archive')) + }) + + expect(useBundleOperations().updateBundle).toHaveBeenCalledWith({ + ...activeBundle, + archivedAt: expect.any(String), + }) + }) + + test('will unarchive an archived bundle', async () => { + const archivedBundle: BundleDocument = { + _id: 'activeBundle', + _type: 'bundle', + archivedAt: new Date().toISOString(), + title: 'activeBundle', + name: 'activeBundle', + authorId: 'author', + _createdAt: new Date().toISOString(), + _updatedAt: new Date().toISOString(), + _rev: '', + } + await renderTest(archivedBundle) + + fireEvent.click(screen.getByLabelText('Release menu')) + + await act(() => { + fireEvent.click(screen.getByText('Unarchive')) + }) + + expect(useBundleOperations().updateBundle).toHaveBeenCalledWith({ + ...archivedBundle, + archivedAt: undefined, + }) + }) + + test('will delete a bundle', async () => { + const activeBundle: BundleDocument = { + _id: 'activeBundle', + _type: 'bundle', + archivedAt: new Date().toISOString(), + title: 'activeBundle', + name: 'activeBundle', + authorId: 'author', + _createdAt: new Date().toISOString(), + _updatedAt: new Date().toISOString(), + _rev: '', + } + await renderTest(activeBundle) + + fireEvent.click(screen.getByLabelText('Release menu')) + + await act(() => { + fireEvent.click(screen.getByText('Delete')) + }) + + expect(useBundleOperations().deleteBundle).toHaveBeenCalledWith(activeBundle._id) + expect(useRouter().navigate).not.toHaveBeenCalled() + }) +}) diff --git a/packages/sanity/src/core/releases/components/BundlesTable/BundleRow.tsx b/packages/sanity/src/core/releases/components/BundlesTable/BundleRow.tsx index cc662af812f..188531780fa 100644 --- a/packages/sanity/src/core/releases/components/BundlesTable/BundleRow.tsx +++ b/packages/sanity/src/core/releases/components/BundlesTable/BundleRow.tsx @@ -2,7 +2,7 @@ import {Box, Card, Flex, Stack, Text} from '@sanity/ui' import {useRouter} from 'sanity/router' import {BundleBadge} from '../../../bundles/components/BundleBadge' -import {RelativeTime} from '../../../components/RelativeTime' +import {RelativeTime} from '../../../components' import {type BundleDocument} from '../../../store/bundles/types' import {BundleMenuButton} from '../BundleMenuButton/BundleMenuButton' diff --git a/packages/sanity/src/core/releases/components/BundlesTable/BundlesTable.tsx b/packages/sanity/src/core/releases/components/BundlesTable/BundlesTable.tsx index 5b347c821cc..f9ae26b47fb 100644 --- a/packages/sanity/src/core/releases/components/BundlesTable/BundlesTable.tsx +++ b/packages/sanity/src/core/releases/components/BundlesTable/BundlesTable.tsx @@ -1,4 +1,3 @@ -/* eslint-disable i18next/no-literal-string */ import {Card, Stack, Text} from '@sanity/ui' import {useMemo} from 'react' import {styled} from 'styled-components' @@ -51,7 +50,7 @@ export function BundlesTable({bundles, searchTerm, setSearchTerm}: BundlesTableP return ( diff --git a/packages/sanity/src/core/releases/components/BundlesTable/__tests__/BundlesTable.test.tsx b/packages/sanity/src/core/releases/components/BundlesTable/__tests__/BundlesTable.test.tsx index f6eec626ff7..fe70025b0a7 100644 --- a/packages/sanity/src/core/releases/components/BundlesTable/__tests__/BundlesTable.test.tsx +++ b/packages/sanity/src/core/releases/components/BundlesTable/__tests__/BundlesTable.test.tsx @@ -83,7 +83,7 @@ describe('BundlesTable', () => { const bundleRow = screen.getAllByTestId('bundle-row')[0] fireEvent.click(within(bundleRow).getByLabelText('Release menu')) - fireEvent.click(screen.getByText('Delete release')) + fireEvent.click(screen.getByText('Delete')) await waitFor(() => { expect(useBundleOperations().deleteBundle).toHaveBeenCalledWith('123') diff --git a/packages/sanity/src/core/releases/tool/BundlesOverview.tsx b/packages/sanity/src/core/releases/tool/BundlesOverview.tsx index b047127074f..42c89657441 100644 --- a/packages/sanity/src/core/releases/tool/BundlesOverview.tsx +++ b/packages/sanity/src/core/releases/tool/BundlesOverview.tsx @@ -1,7 +1,7 @@ import {AddIcon} from '@sanity/icons' import {Box, Button, type ButtonMode, Card, Container, Flex, Heading, Stack, Text} from '@sanity/ui' import {isBefore} from 'date-fns' -import {type MouseEventHandler, useCallback, useMemo, useState} from 'react' +import {type MouseEventHandler, useCallback, useEffect, useMemo, useState} from 'react' import {Button as StudioButton} from '../../../ui-components' import {CreateBundleDialog} from '../../bundles/components/dialog/CreateBundleDialog' @@ -28,19 +28,28 @@ export default function BundlesOverview() { const groupedBundles = useMemo( () => data?.reduce<{open: BundleDocument[]; archived: BundleDocument[]}>((groups, bundle) => { - const group = - bundle.publishedAt && isBefore(new Date(bundle.publishedAt), new Date()) - ? 'archived' - : 'open' + const isBundleArchived = + bundle.archivedAt || + (bundle.publishedAt && isBefore(new Date(bundle.publishedAt), new Date())) + const group = isBundleArchived ? 'archived' : 'open' return {...groups, [group]: [...groups[group], bundle]} }, EMPTY_BUNDLE_GROUPS) || EMPTY_BUNDLE_GROUPS, [data], ) + // switch to open mode if on archived mode and there are no archived bundles + useEffect(() => { + if (bundleGroupMode === 'archived' && !groupedBundles.archived.length) { + setBundleGroupMode('open') + } + }, [bundleGroupMode, groupedBundles.archived.length]) + + // clear search when mode changes + useEffect(() => setSearchTerm(''), [bundleGroupMode]) + const handleBundleGroupModeChange = useCallback>( ({currentTarget: {value: groupMode}}) => { - setSearchTerm('') // clear the table search applied setBundleGroupMode(groupMode as Mode) }, [], @@ -111,7 +120,8 @@ export default function BundlesOverview() { } const applySearchTermToBundles = useCallback( - (bundle: BundleDocument) => !searchTerm || bundle.title.includes(searchTerm), + (bundle: BundleDocument) => + !searchTerm || bundle.title.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()), [searchTerm], ) diff --git a/packages/sanity/src/core/releases/tool/__tests__/BundlesOverview.test.tsx b/packages/sanity/src/core/releases/tool/__tests__/BundlesOverview.test.tsx index d8a266a154a..1d87ac029fb 100644 --- a/packages/sanity/src/core/releases/tool/__tests__/BundlesOverview.test.tsx +++ b/packages/sanity/src/core/releases/tool/__tests__/BundlesOverview.test.tsx @@ -1,5 +1,5 @@ import {beforeEach, describe, expect, it, jest} from '@jest/globals' -import {fireEvent, render, screen} from '@testing-library/react' +import {fireEvent, render, screen, waitFor} from '@testing-library/react' import {type ReactNode} from 'react' import {queryByDataUi} from '../../../../../test/setup/customQueries' @@ -108,7 +108,8 @@ describe('BundlesOverview', () => { const bundles = [ {title: 'Bundle 1'}, {title: 'Bundle 2'}, - {title: 'Bundle 3', publishedAt: new Date()}, + {title: 'Bundle 3', publishedAt: new Date().toISOString()}, + {title: 'Bundle 4', archivedAt: new Date().toISOString()}, ] as unknown as BundleDocument[] beforeEach(async () => { @@ -132,11 +133,14 @@ describe('BundlesOverview', () => { expect(screen.getByText('Archived').closest('button')).not.toBeDisabled() }) - it('shows published bundles', () => { + it('shows published bundles', async () => { fireEvent.click(screen.getByText('Archived')) - screen.getByText('Bundle 3') - expect(screen.queryByText('Bundle 1')).toBeNull() + await waitFor(() => { + screen.getByText('Bundle 3') + screen.getByText('Bundle 4') + expect(screen.queryByText('Bundle 1')).toBeNull() + }) }) it('allows for searching bundles', () => { diff --git a/packages/sanity/src/core/store/bundles/types.ts b/packages/sanity/src/core/store/bundles/types.ts index ffd7edc1c4e..fd9121b0954 100644 --- a/packages/sanity/src/core/store/bundles/types.ts +++ b/packages/sanity/src/core/store/bundles/types.ts @@ -14,6 +14,7 @@ export interface BundleDocument extends SanityDocument { icon?: IconSymbol authorId: string publishedAt?: string + archivedAt?: string } /** diff --git a/packages/sanity/src/core/store/bundles/useBundleOperations.ts b/packages/sanity/src/core/store/bundles/useBundleOperations.ts index 7d37cc1d78b..22fef24c2b4 100644 --- a/packages/sanity/src/core/store/bundles/useBundleOperations.ts +++ b/packages/sanity/src/core/store/bundles/useBundleOperations.ts @@ -31,13 +31,22 @@ export function useBundleOperations() { const handleUpdateBundle = useCallback( async (bundle: BundleDocument) => { + if (!client) return null + const document = { ...bundle, _type: 'bundle', } as BundleDocument + const unsetKeys = Object.entries(bundle) + .filter(([_, value]) => value === undefined) + .map(([key]) => key) - const res = await client?.patch(bundle._id).set(document).commit() - return res + let clientOperation = client.patch(bundle._id).set(document) + if (unsetKeys.length) { + clientOperation = clientOperation.unset(unsetKeys) + } + + return clientOperation.commit() }, [client], )