Skip to content

Commit

Permalink
feat(core): add ability to change release type
Browse files Browse the repository at this point in the history
  • Loading branch information
RitaDias committed Oct 31, 2024
1 parent f57e164 commit e52f487
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 44 deletions.
2 changes: 2 additions & 0 deletions packages/sanity/src/core/releases/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ const releasesLocaleStrings = {
'toast.unschedule.error': "Failed to unscheduled '<strong>{{title}}</strong>': {{error}}",
/** Text for toast when release has been unschedule */
'toast.unschedule.success': "The '<strong>{{title}}</strong>' release was unscheduled.",

'type-picker.tooltip.scheduled': 'The release is scheduled, unschedule it to change type',
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import {LockIcon, PinFilledIcon, PinIcon} from '@sanity/icons'
import {PinFilledIcon, PinIcon} from '@sanity/icons'
import {
Box,
// Custom button with full radius used here
// eslint-disable-next-line no-restricted-imports
Button,
Card,
Container,
Flex,
Stack,
Text,
} from '@sanity/ui'
import {format, isBefore} from 'date-fns'
import {useCallback, useMemo} from 'react'
import {useCallback} from 'react'

import {useTranslation} from '../../../i18n'
import {type ReleaseDocument} from '../../../store'
import {ReleaseAvatar} from '../../components/ReleaseAvatar'
import {usePerspective} from '../../hooks'
import {releasesLocaleNamespace} from '../../i18n'
import {getReleaseTone} from '../../util/getReleaseTone'
import {getReleasePublishDate} from '../../util/util'
import {ReleaseDetailsEditor} from './ReleaseDetailsEditor'
import {ReleaseTypePicker} from './ReleaseTypePicker'

export function ReleaseDashboardDetails({release}: {release: ReleaseDocument}) {
const {state, _id} = release

const {t: tCore} = useTranslation(releasesLocaleNamespace)
const {t} = useTranslation()
const {t: tRelease} = useTranslation(releasesLocaleNamespace)

const {currentGlobalBundleId, setPerspective, setPerspectiveFromRelease} = usePerspective()

Expand All @@ -38,22 +33,6 @@ export function ReleaseDashboardDetails({release}: {release: ReleaseDocument}) {
}
}, [_id, currentGlobalBundleId, setPerspective, setPerspectiveFromRelease])

const publishDate = getReleasePublishDate(release)
const isPublishDateInPast = !!publishDate && isBefore(new Date(publishDate), new Date())
const isReleaseScheduled = release.state === 'scheduling' || release.state === 'scheduled'

const publishDateLabel = useMemo(() => {
if (release.metadata.releaseType === 'asap') return t('release.type.asap')
if (release.metadata.releaseType === 'undecided') return t('release.type.undecided')
if (!publishDate) return null

return isPublishDateInPast
? tCore('dashboard.details.published-on', {
date: format(new Date(publishDate), `MMM d, yyyy`),
})
: format(new Date(publishDate), `PPpp`)
}, [isPublishDateInPast, publishDate, release.metadata.releaseType, t, tCore])

return (
<Container width={3}>
<Stack padding={3} paddingY={[4, 4, 5, 6]} space={[3, 3, 4, 5]}>
Expand All @@ -67,25 +46,10 @@ export function ReleaseDashboardDetails({release}: {release: ReleaseDocument}) {
radius="full"
selected={_id === currentGlobalBundleId}
space={2}
text={tCore('dashboard.details.pin-release')}
text={tRelease('dashboard.details.pin-release')}
tone={getReleaseTone(release)}
/>
{/* TODO: replace with the release time field here //{' '} */}
{/* <ReleaseTimeField onChange={handleTimeChange} release={release} value={timeValue} /> */}
<Card
padding={2}
style={isReleaseScheduled ? {opacity: 0.75} : undefined}
radius={2}
tone={isReleaseScheduled ? 'positive' : 'transparent'}
>
<Flex flex={1} gap={2} align="center">
<ReleaseAvatar padding={0} tone={getReleaseTone(release)} />
<Text muted size={1} weight="medium">
{publishDateLabel}
</Text>
{isReleaseScheduled && <LockIcon />}
</Flex>
</Card>
<ReleaseTypePicker release={release} />
</Flex>

<Box padding={2}>
Expand Down
198 changes: 198 additions & 0 deletions packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import {LockIcon} from '@sanity/icons'
import {Flex, Spinner, Stack, TabList, Text, useClickOutsideEvent} from '@sanity/ui'
import {format, isBefore} from 'date-fns'
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {type ReleaseDocument, type ReleaseType, useDateTimeFormat, useTranslation} from 'sanity'

import {Button, Popover, Tab} from '../../../../ui-components'
import {MONTH_PICKER_VARIANT} from '../../../../ui-components/inputs/DateInputs/calendar/Calendar'
import {type CalendarLabels} from '../../../../ui-components/inputs/DateInputs/calendar/types'
import {DatePicker} from '../../../../ui-components/inputs/DateInputs/DatePicker'
import {LazyTextInput} from '../../../../ui-components/inputs/DateInputs/LazyTextInput'
import {getCalendarLabels} from '../../../form/inputs/DateInputs/utils'
import {useReleaseOperations} from '../../../store/release/useReleaseOperations'
import {ReleaseAvatar} from '../../components/ReleaseAvatar'
import {releasesLocaleNamespace} from '../../i18n'
import {getReleaseTone} from '../../util/getReleaseTone'

export function ReleaseTypePicker(props: {release: ReleaseDocument}): JSX.Element {
const {release} = props

const popoverRef = useRef<HTMLDivElement | null>(null)
const buttonRef = useRef<HTMLButtonElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null)

const {t: tRelease} = useTranslation(releasesLocaleNamespace)
const {t} = useTranslation()
const {updateRelease} = useReleaseOperations()

const [open, setOpen] = useState(false)
const [dateInputOpen, setDateInputOpen] = useState(release.metadata.releaseType === 'scheduled')
const [releaseType, setReleaseType] = useState<ReleaseType>(release.metadata.releaseType)
const [updatedDate, setUpdatedDate] = useState<string | undefined>(
release.metadata.intendedPublishAt || release.publishAt,
)
const [isUpdating, setIsUpdating] = useState(false)

const dateFormatter = useDateTimeFormat()
const [inputValue, setInputValue] = useState<string | undefined>(
release.metadata.intendedPublishAt
? dateFormatter.format(new Date(release.metadata.intendedPublishAt))
: undefined,
)

const calendarLabels: CalendarLabels = useMemo(() => getCalendarLabels(t), [t])

const close = useCallback(() => {
if (open) {
setIsUpdating(true)
updateRelease({
...release,
metadata: {...release.metadata, intendedPublishAt: updatedDate, releaseType},
}).then(() => {
setIsUpdating(false)
})
setOpen(false)
setDateInputOpen(releaseType === 'scheduled')
}
}, [updateRelease, release, updatedDate, releaseType, open])

useClickOutsideEvent(close, () => [popoverRef.current, buttonRef.current, inputRef.current])

useEffect(() => {
if (open) inputRef.current?.focus()
}, [open])

const publishDate = updatedDate
const isPublishDateInPast = !!publishDate && isBefore(new Date(publishDate), new Date())
const isReleaseScheduled = release.state === 'scheduling'

const publishDateLabel = useMemo(() => {
if (releaseType === 'asap') return t('release.type.asap')
if (releaseType === 'undecided') return t('release.type.undecided')
if (!publishDate) return null

return isPublishDateInPast
? tRelease('dashboard.details.published-on', {
date: format(new Date(publishDate), `MMM d, yyyy`),
})
: format(new Date(publishDate), `PPpp`)
}, [isPublishDateInPast, publishDate, releaseType, t, tRelease])

const handleButtonReleaseTypeChange = useCallback((pickedReleaseType: ReleaseType) => {
if (pickedReleaseType === 'scheduled') {
setDateInputOpen(true)
}

setReleaseType(pickedReleaseType)
}, [])

const handleBundlePublishAtChange = useCallback(
(date: Date | null) => {
setInputValue(dateFormatter.format(date || undefined))

setUpdatedDate(date ? date.toISOString() : undefined)
},
[dateFormatter],
)

const handleInputChange = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
const date = new Date(event.currentTarget.value)
setInputValue(dateFormatter.format(new Date(event.currentTarget.value) || undefined))

setUpdatedDate(date ? date.toISOString() : undefined)
},
[dateFormatter],
)

const PopoverContent = () => {
return (
<Stack space={1}>
<TabList space={0.5}>
<Tab
aria-controls="release-timing-asap"
id="release-timing-asap-tab"
onClick={() => handleButtonReleaseTypeChange('asap')}
label={t('release.type.asap')}
selected={releaseType === 'asap'}
/>
<Tab
aria-controls="release-timing-at-time"
id="release-timing-at-time-tab"
onClick={() => handleButtonReleaseTypeChange('scheduled')}
selected={releaseType === 'scheduled'}
label={t('release.type.scheduled')}
/>
<Tab
aria-controls="release-timing-undecided"
id="release-timing-undecided-tab"
onClick={() => handleButtonReleaseTypeChange('undecided')}
selected={releaseType === 'undecided'}
label={t('release.type.undecided')}
/>
</TabList>
{dateInputOpen && (
<>
<LazyTextInput value={inputValue} onChange={handleInputChange} />

<DatePicker
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
calendarLabels={calendarLabels}
selectTime
padding={0}
value={updatedDate ? new Date(updatedDate) : undefined}
onChange={handleBundlePublishAtChange}
/>
</>
)}
</Stack>
)
}

return (
<Popover
content={<PopoverContent />}
open={open}
padding={1}
placement="bottom-start"
portal
ref={popoverRef}
>
<Button
disabled={isReleaseScheduled || isPublishDateInPast}
mode="bleed"
onClick={() => setOpen(!open)}
padding={2}
ref={buttonRef}
tooltipProps={{
placement: 'bottom',
content: isReleaseScheduled && tRelease('type-picker.tooltip.scheduled'),
}}
selected={open}
tone={getReleaseTone({...release, metadata: {...release.metadata, releaseType}})}
>
<Flex flex={1} gap={2}>
{isUpdating ? (
<Spinner size={1} />
) : (
<ReleaseAvatar
tone={getReleaseTone({...release, metadata: {...release.metadata, releaseType}})}
padding={0}
/>
)}

<Text muted size={1} weight="medium">
{publishDateLabel}
</Text>

{isReleaseScheduled && (
<Text size={1}>
<LockIcon />
</Text>
)}
</Flex>
</Button>
</Popover>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ export const DatePicker = forwardRef(function DatePicker(
timeStep?: number
calendarLabels: CalendarLabels
monthPickerVariant?: CalendarProps['monthPickerVariant']
padding?: number
},
ref: ForwardedRef<HTMLDivElement>,
) {
const {value = new Date(), onChange, calendarLabels, ...rest} = props
const {value = new Date(), onChange, calendarLabels, padding = 2, ...rest} = props
const [focusedDate, setFocusedDay] = useState<Date>()

const handleSelect = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type CalendarProps = Omit<ComponentProps<'div'>, 'onSelect'> & {
onFocusedDateChange: (index: Date) => void
labels: CalendarLabels
monthPickerVariant?: (typeof MONTH_PICKER_VARIANT)[keyof typeof MONTH_PICKER_VARIANT]
padding?: number
}

// This is used to maintain focus on a child element of the calendar-grid between re-renders
Expand Down Expand Up @@ -73,6 +74,7 @@ export const Calendar = forwardRef(function Calendar(
onSelect,
labels,
monthPickerVariant = 'select',
padding = 2,
...restProps
} = props

Expand Down Expand Up @@ -273,7 +275,7 @@ export const Calendar = forwardRef(function Calendar(
return (
<Box data-ui="Calendar" {...restProps} ref={ref}>
{/* Select date */}
<Box padding={2}>
<Box padding={padding}>
{/* Day presets */}
{features.dayPresets && (
<Grid columns={3} data-ui="CalendaryDayPresets" gap={1}>
Expand Down

0 comments on commit e52f487

Please sign in to comment.