Skip to content

Commit

Permalink
feat(releases): timezone selection updates filtering and dateTime dis…
Browse files Browse the repository at this point in the history
…play on release overview (#7854)
  • Loading branch information
jordanl17 authored Nov 21, 2024
1 parent 07c92c7 commit 7a6ff5f
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {AddIcon, ChevronDownIcon, CloseIcon, EarthGlobeIcon} from '@sanity/icons'
import {Box, type ButtonMode, Card, Container, Flex, Stack, Text} from '@sanity/ui'
import {endOfDay, format, isSameDay, startOfDay} from 'date-fns'
import {zonedTimeToUtc} from 'date-fns-tz'
import {AnimatePresence, motion} from 'framer-motion'
import {type MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {type RouterContextValue, type SearchParam, useRouter} from 'sanity/router'
Expand All @@ -13,6 +12,7 @@ import {
type CalendarProps,
} from '../../../../ui-components/inputs/DateFilters/calendar/CalendarFilter'
import {useTranslation} from '../../../i18n'
import useDialogTimeZone from '../../../scheduledPublishing/hooks/useDialogTimeZone'
import useTimeZone from '../../../scheduledPublishing/hooks/useTimeZone'
import {CreateReleaseDialog} from '../../components/dialog/CreateReleaseDialog'
import {usePerspective} from '../../hooks/usePerspective'
Expand Down Expand Up @@ -74,18 +74,22 @@ export interface TableRelease extends ReleaseDocument {
isDeleted?: boolean
}

// TODO: use the selected timezone rather than client
const getTimezoneAdjustedDateTimeRange = (date: Date) => {
const {timeZone} = Intl.DateTimeFormat().resolvedOptions()
const useTimezoneAdjustedDateTimeRange = () => {
const {zoneDateToUtc} = useTimeZone()

return [startOfDay(date), endOfDay(date)].map((time) => zonedTimeToUtc(time, timeZone))
return useCallback(
(date: Date) => [startOfDay(date), endOfDay(date)].map(zoneDateToUtc),
[zoneDateToUtc],
)
}

const ReleaseCalendarDay: CalendarProps['renderCalendarDay'] = (props) => {
const {data: releases} = useReleases()
const getTimezoneAdjustedDateTimeRange = useTimezoneAdjustedDateTimeRange()

const {date} = props

const [startOfDayUTC, endOfDayUTC] = getTimezoneAdjustedDateTimeRange(date)
const [startOfDayForTimeZone, endOfDayForTimeZone] = getTimezoneAdjustedDateTimeRange(date)

const dayHasReleases = releases?.some((release) => {
const releasePublishAt = release.publishAt || release.metadata.intendedPublishAt
Expand All @@ -95,8 +99,8 @@ const ReleaseCalendarDay: CalendarProps['renderCalendarDay'] = (props) => {

return (
release.metadata.releaseType === 'scheduled' &&
publishDateUTC >= startOfDayUTC &&
publishDateUTC <= endOfDayUTC
publishDateUTC >= startOfDayForTimeZone &&
publishDateUTC <= endOfDayForTimeZone
)
})

Expand Down Expand Up @@ -135,8 +139,10 @@ export function ReleasesOverview() {
const loadingTableData = loading || (!releasesMetadata && Boolean(releaseIds.length))
const {t} = useTranslation(releasesLocaleNamespace)
const {t: tCore} = useTranslation()
const {timeZone} = useTimeZone()
const {timeZone, utcToCurrentZoneDate} = useTimeZone()
const {currentGlobalBundleId} = usePerspective()
const {DialogTimeZone, dialogProps, dialogTimeZoneShow} = useDialogTimeZone()
const getTimezoneAdjustedDateTimeRange = useTimezoneAdjustedDateTimeRange()

const getRowProps = useCallback(
(datum: TableRelease): Partial<TableRowProps> =>
Expand Down Expand Up @@ -179,11 +185,19 @@ export function ReleasesOverview() {
[],
)

const handleSelectFilterDate = useCallback((date?: Date) => {
setReleaseFilterDate((prevFilterDate) =>
prevFilterDate && date && isSameDay(prevFilterDate, date) ? undefined : date,
)
}, [])
const handleSelectFilterDate = useCallback(
(date?: Date) =>
setReleaseFilterDate((prevFilterDate) => {
if (!date) return undefined

const timeZoneAdjustedDate = utcToCurrentZoneDate(date)

return prevFilterDate && isSameDay(prevFilterDate, timeZoneAdjustedDate)
? undefined
: timeZoneAdjustedDate
}),
[utcToCurrentZoneDate],
)

const clearFilterDate = useCallback(() => {
setReleaseFilterDate(undefined)
Expand Down Expand Up @@ -301,15 +315,22 @@ export function ReleasesOverview() {
const filteredReleases = useMemo(() => {
if (!releaseFilterDate) return releaseGroupMode === 'open' ? tableReleases : archivedReleases

const [startOfDayUTC, endOfDayUTC] = getTimezoneAdjustedDateTimeRange(releaseFilterDate)
const [startOfDayForTimeZone, endOfDayForTimeZone] =
getTimezoneAdjustedDateTimeRange(releaseFilterDate)

return tableReleases.filter((release) => {
if (!release.publishAt || release.metadata.releaseType !== 'scheduled') return false

const publishDateUTC = new Date(release.publishAt)
return publishDateUTC >= startOfDayUTC && publishDateUTC <= endOfDayUTC
return publishDateUTC >= startOfDayForTimeZone && publishDateUTC <= endOfDayForTimeZone
})
}, [releaseFilterDate, releaseGroupMode, tableReleases, archivedReleases])
}, [
releaseFilterDate,
releaseGroupMode,
tableReleases,
archivedReleases,
getTimezoneAdjustedDateTimeRange,
])

return (
<Flex direction="row" flex={1} style={{height: '100%'}}>
Expand Down Expand Up @@ -347,7 +368,9 @@ export function ReleasesOverview() {
mode="bleed"
padding={2}
text={`${timeZone.abbreviation} (${timeZone.namePretty})`}
onClick={dialogTimeZoneShow}
/>
{DialogTimeZone && <DialogTimeZone {...dialogProps} />}
{loadingOrHasReleases && createReleaseButton}
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {LockIcon, PinFilledIcon, PinIcon} from '@sanity/icons'
import {Box, Card, Flex, Stack, Text} from '@sanity/ui'
import {format} from 'date-fns'
import {type TFunction} from 'i18next'
import {useCallback} from 'react'
import {useCallback, useMemo} from 'react'
import {useRouter} from 'sanity/router'

import {Button, Tooltip} from '../../../../ui-components'
import {RelativeTime} from '../../../components'
import {Translate, useTranslation} from '../../../i18n'
import useTimeZone, {getLocalTimeZone} from '../../../scheduledPublishing/hooks/useTimeZone'
import {ReleaseAvatar} from '../../components/ReleaseAvatar'
import {usePerspective} from '../../hooks/usePerspective'
import {releasesLocaleNamespace} from '../../i18n'
Expand All @@ -22,9 +23,18 @@ import {type TableRelease} from './ReleasesOverview'

const ReleaseTime = ({release}: {release: TableRelease}) => {
const {t} = useTranslation()
const {timeZone, utcToCurrentZoneDate} = useTimeZone()
const {abbreviation: localeTimeZoneAbbreviation} = getLocalTimeZone()

const {metadata} = release

const getTimeString = () => {
const getTimezoneAbbreviation = useCallback(
() =>
timeZone.abbreviation === localeTimeZoneAbbreviation ? '' : `(${timeZone.abbreviation})`,
[localeTimeZoneAbbreviation, timeZone.abbreviation],
)

const timeString = useMemo(() => {
if (metadata.releaseType === 'asap') {
return t('release.type.asap')
}
Expand All @@ -34,12 +44,14 @@ const ReleaseTime = ({release}: {release: TableRelease}) => {

const publishDate = getPublishDateFromRelease(release)

return publishDate ? format(new Date(publishDate), 'PPpp') : null
}
return publishDate
? `${format(utcToCurrentZoneDate(publishDate), 'PPpp')} ${getTimezoneAbbreviation()}`
: null
}, [metadata.releaseType, release, utcToCurrentZoneDate, getTimezoneAbbreviation, t])

return (
<Text muted size={1}>
{getTimeString()}
{timeString}
</Text>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react'
import {format, set} from 'date-fns'
import {useRouter} from 'sanity/router'
import {beforeEach, describe, expect, it, vi} from 'vitest'

import {getByDataUi, queryByDataUi} from '../../../../../../test/setup/customQueries'
import {createTestProvider} from '../../../../../../test/testUtils/TestProvider'
import {
getLocalTimeZoneMockReturn,
mockGetLocaleTimeZone,
mockUseTimeZone,
useTimeZoneMockReturn,
} from '../../../../scheduledPublishing/hooks/__tests__/__mocks__/useTimeZone.mock'
import {
activeASAPRelease,
activeScheduledRelease,
Expand All @@ -30,7 +37,12 @@ import {type ReleasesMetadata} from '../../../store/useReleasesMetadata'
import {useBundleDocumentsMockReturnWithResults} from '../../detail/__tests__/__mocks__/useBundleDocuments.mock'
import {ReleasesOverview} from '../ReleasesOverview'

const TODAY = new Date()
const TODAY = set(new Date(), {
hours: 22,
minutes: 0,
seconds: 0,
milliseconds: 0,
})

vi.mock('sanity', () => ({
SANITY_VERSION: '0.0.0',
Expand Down Expand Up @@ -59,6 +71,12 @@ vi.mock('../../../hooks/usePerspective', () => ({
usePerspective: vi.fn(() => usePerspectiveMockReturn),
}))

vi.mock('../../../../scheduledPublishing/hooks/useTimeZone', async (importOriginal) => ({
...(await importOriginal()),
getLocalTimeZone: vi.fn(() => getLocalTimeZoneMockReturn),
default: vi.fn(() => useTimeZoneMockReturn),
}))

describe('ReleasesOverview', () => {
beforeEach(() => {
mockUseReleases.mockRestore()
Expand Down Expand Up @@ -132,7 +150,10 @@ describe('ReleasesOverview', () => {
const releases: ReleaseDocument[] = [
{
...activeScheduledRelease,
metadata: {...activeScheduledRelease.metadata, intendedPublishAt: TODAY.toISOString()},
metadata: {
...activeScheduledRelease.metadata,
intendedPublishAt: TODAY.toISOString(),
},
},
activeASAPRelease,
activeUndecidedRelease,
Expand All @@ -142,6 +163,7 @@ describe('ReleasesOverview', () => {
let activeRender: ReturnType<typeof render>

beforeEach(async () => {
mockUseTimeZone.mockRestore()
mockUseReleases.mockReturnValue({
...useReleasesMockReturn,
archivedReleases: [archivedScheduledRelease, publishedASAPRelease],
Expand Down Expand Up @@ -191,6 +213,13 @@ describe('ReleasesOverview', () => {
within(asapReleaseRow).getByText('Undecided')
})

it('shows time for scheduled releases', () => {
const scheduledReleaseRow = screen.getAllByTestId('table-row')[2]

const date = format(TODAY, 'MMM d, yyyy')
within(scheduledReleaseRow).getByText(`${date}, 10:00:00 PM`)
})

it('has release menu actions for each release', () => {
const releaseRows = screen.getAllByTestId('table-row')
releaseRows.forEach((row) => {
Expand Down Expand Up @@ -283,6 +312,65 @@ describe('ReleasesOverview', () => {
})
})

describe('timezone selection', () => {
it('shows the selected timezone', () => {
screen.getByText('SCT (Sanity/Oslo)')
})

it('opens the timezone selector', () => {
fireEvent.click(screen.getByText('SCT (Sanity/Oslo)'))

within(getByDataUi(document.body, 'DialogCard')).getByText('Select time zone')
})

it('shows dates with timezone abbreviation when it is not the locale', () => {
mockGetLocaleTimeZone.mockReturnValue({
abbreviation: 'NST', // Not Sanity Time
namePretty: 'Not Sanity Time',
offset: '+00:00',
name: 'NST',
alternativeName: 'Not Sanity Time',
mainCities: 'Not Sanity City',
value: 'Not Sanity Time',
})

activeRender.rerender(<ReleasesOverview />)

const scheduledReleaseRow = screen.getAllByTestId('table-row')[2]

const date = format(TODAY, 'MMM d, yyyy')
within(scheduledReleaseRow).getByText(`${date}, 10:00:00 PM (SCT)`)
})

describe('when a different timezone is selected', () => {
beforeEach(() => {
mockUseTimeZone.mockReturnValue({
...useTimeZoneMockReturn,
// spoof a timezone that is 8 hours ahead of UTC
zoneDateToUtc: vi.fn((date) => set(date, {hours: new Date(date).getHours() - 8})),
})

activeRender.rerender(<ReleasesOverview />)
})

it('shows today as having no releases', () => {
const todayTile = within(getByDataUi(document.body, 'Calendar')).getByText(
TODAY.getDate(),
)
expect(todayTile.parentNode).not.toHaveStyle('font-weight: 700')
})

it('shows no releases when filtered by today', () => {
const todayTile = within(getByDataUi(document.body, 'Calendar')).getByText(
TODAY.getDate(),
)
fireEvent.click(todayTile)

expect(screen.queryAllByTestId('table-row')).toHaveLength(0)
})
})
})

describe('archived releases', () => {
beforeEach(() => {
fireEvent.click(screen.getByText('Archived'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {type Mock, type Mocked, vi} from 'vitest'

import {type NormalizedTimeZone} from '../../../types'
import useTimeZone, {getLocalTimeZone} from '../../useTimeZone'

const mockTimeZone: NormalizedTimeZone = {
abbreviation: 'SCT', // Sanity Central Time :)
namePretty: 'Sanity/Oslo',
offset: '+00:00',
name: 'SCT',
alternativeName: 'Sanity/Oslo',
mainCities: 'Oslo',
value: 'SCT',
}

// default export
export const useTimeZoneMockReturn: Mocked<ReturnType<typeof useTimeZone>> = {
zoneDateToUtc: vi.fn((date) => date),
utcToCurrentZoneDate: vi.fn((date) => date),
getCurrentZoneDate: vi.fn(() => new Date()),
timeZone: mockTimeZone,
setTimeZone: vi.fn(),
formatDateTz: vi.fn(),
}

export const getLocalTimeZoneMockReturn: Mocked<ReturnType<typeof getLocalTimeZone>> = mockTimeZone

// default export
export const mockUseTimeZone = useTimeZone as Mock<typeof useTimeZone>

export const mockGetLocaleTimeZone = getLocalTimeZone as Mock<typeof getLocalTimeZone>

0 comments on commit 7a6ff5f

Please sign in to comment.