Skip to content

Commit

Permalink
feat(releases): archive and unarchive releases (#7072)
Browse files Browse the repository at this point in the history
* refactor(sanity): update types to bundle

* feat(sanity): add ability to create releases from dialog + update types

* feat(sanity): update list on global and document to use store

* feat(sanity): update types, add icon and hue picker to bundles, update uis

* feat(sanity): add date picker to bundleform

* refactor(sanity): update publishAt fields in menus

* refactor(sanity): update type in dummyGetter

* chore(sanity): remove BUNDLES const

* chore(sanity): update LATEST type to Partial

* chore(sanity): update validation for bundle date creation

* chore(sanity): clean up code

* refactor(sanity): add archived filter

* refactor(sanity): make single bundle menu + clean up

* chore(sanity): add missing properties

* feat(sanity): add loading & remove unused code

* feat(sanity): add loading to document version, update filter

* chore(sanity): rename VersionBadge to BundleBadge

* chore(sanity): clean up methods and style

* chore(sanity): remove unused import

* refactor(sanity): use Bundle provider instead of store

* chore(sanity): update bundleRow to use the new name for badge

* chore(sanity): clean up code

* refactor(sanity): re-add oncancel and oncreate props in dialog

* refactor(sanity): add scroll to bundle menu

* refactor(sanity): move from archived to archivedAt

* refactor(sanity): move version provider to its own file + organise + add useVersion

* refactor(sanity): remove provider + context, move logic to hook

* chore(sanity): remove comments + change name from setCurrentVersion to setGlobalBundle

* chore(sanity): rename useVersion to useBundle

* chore(sanity): update comments

* chore(sanity): Rename currentVersion

* refactor(sanity): rename currentBudnle and setGlobalBundle

* chore(sanity): rename versions to bundles in core directory

* chore(sanity): rename to GlobalPerspectiveMenu and move to navbar directory

* chore(sanity): rename to DocumentPerspectiveMenu and move to navbar directory

* feat(releases): support for (un)archive

* chore(releases): updating testing for BundlesOverview

* chore(releases): new tests for BundleMenuButton and (un)archive

* fix(releases): disabling bundle menu btn when action is performed

---------

Co-authored-by: RitaDias <anarita151@gmail.com>
Co-authored-by: RitaDias <rita@sanity.io>
  • Loading branch information
3 people committed Jul 8, 2024
1 parent 0548159 commit b5b7776
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -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 (
<MenuButton
button={
<Button
disabled={bundleMenuDisabled}
disabled={bundleMenuDisabled || isPerformingOperation}
icon={isPerformingOperation ? Spinner : EllipsisHorizontalIcon}
mode="bleed"
padding={2}
Expand All @@ -44,7 +55,14 @@ export const BundleMenuButton = ({bundle}: Props) => {
id="bundle-menu"
menu={
<Menu>
<MenuItem onClick={handleOnDeleteBundle} icon={TrashIcon} text="Delete release" />
<MenuItem
onClick={handleOnToggleArchive}
// TODO: swap line once UnaryIcon is available
// icon={isBundleArchived ? UnarchiveIcon : ArchiveIcon}
icon={isBundleArchived ? ArchiveIcon : ArchiveIcon}
text={isBundleArchived ? 'Unarchive' : 'Archive'}
/>
<MenuItem onClick={handleOnDeleteBundle} icon={TrashIcon} text="Delete" />
</Menu>
}
popover={{
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<BundleMenuButton bundle={bundle} />, {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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -51,7 +50,7 @@ export function BundlesTable({bundles, searchTerm, setSearchTerm}: BundlesTableP
return (
<Stack as="table" space={1}>
<BundleHeader
searchDisabled={!bundles.length}
searchDisabled={!searchTerm && !bundles.length}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
24 changes: 17 additions & 7 deletions packages/sanity/src/core/releases/tool/BundlesOverview.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<MouseEventHandler<HTMLButtonElement>>(
({currentTarget: {value: groupMode}}) => {
setSearchTerm('') // clear the table search applied
setBundleGroupMode(groupMode as Mode)
},
[],
Expand Down Expand Up @@ -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],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/src/core/store/bundles/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface BundleDocument extends SanityDocument {
icon?: IconSymbol
authorId: string
publishedAt?: string
archivedAt?: string
}

/**
Expand Down
13 changes: 11 additions & 2 deletions packages/sanity/src/core/store/bundles/useBundleOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
)
Expand Down

0 comments on commit b5b7776

Please sign in to comment.