Skip to content

Commit

Permalink
fix: creating new release prevents historical dates for publish; inpu…
Browse files Browse the repository at this point in the history
…ts allow for entering date
  • Loading branch information
jordanl17 committed Jan 24, 2025
1 parent 70353aa commit c93c322
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {CalendarIcon} from '@sanity/icons'
import {Box, Flex, LayerProvider, useClickOutsideEvent} from '@sanity/ui'
import {Box, Card, Flex, LayerProvider, Text, useClickOutsideEvent} from '@sanity/ui'
import {isPast} from 'date-fns'
import {
type FocusEvent,
type ForwardedRef,
Expand All @@ -14,6 +15,7 @@ import FocusLock from 'react-focus-lock'

import {Button} from '../../../../ui-components/button/Button'
import {Popover} from '../../../../ui-components/popover/Popover'
import {useTranslation} from '../../../i18n'
import {type CalendarProps} from './calendar/Calendar'
import {type CalendarLabels} from './calendar/types'
import {DatePicker} from './DatePicker'
Expand Down Expand Up @@ -58,6 +60,7 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
isPastDisabled,
...rest
} = props
const {t} = useTranslation()
const popoverRef = useRef<HTMLDivElement | null>(null)
const ref = useRef<HTMLInputElement | null>(null)
const buttonRef = useRef(null)
Expand Down Expand Up @@ -124,6 +127,11 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
content={
<Box overflow="auto">
<FocusLock onDeactivation={handleDeactivation}>
{inputValue && isPastDisabled && isPast(new Date(inputValue)) && (
<Card margin={1} padding={2} radius={2} shadow={1} tone="critical">
<Text size={1}>{t('inputs.dateTime.past-date-warning')}</Text>
</Card>
)}
<DatePicker
monthPickerVariant={monthPickerVariant}
calendarLabels={calendarLabels}
Expand Down
2 changes: 2 additions & 0 deletions packages/sanity/src/core/i18n/bundles/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'inputs.array.resolving-initial-value': 'Resolving initial value…',
/** Tooltip content when boolean input is disabled */
'inputs.boolean.disabled': 'Disabled',
/** Warning label when selected datetime is in the past */
'inputs.dateTime.past-date-warning': 'Select a date in the future.',
/** Placeholder value for datetime input */
'inputs.datetime.placeholder': 'e.g. {{example}}',
/** Acessibility label for button to open file options menu */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {EarthGlobeIcon} from '@sanity/icons'
import {Flex} from '@sanity/ui'
import {addMinutes, format, isBefore} from 'date-fns'
import {useMemo, useState} from 'react'
import {format, isValid, parse} from 'date-fns'
import {useCallback, useMemo} from 'react'

import {Button} from '../../../ui-components/button'
import {MONTH_PICKER_VARIANT} from '../../components/inputs/DateInputs/calendar/Calendar'
Expand All @@ -24,18 +24,25 @@ export const ScheduleDatePicker = ({
const {t} = useTranslation()
const {timeZone} = useTimeZone()
const {dialogTimeZoneShow} = useDialogTimeZone()
const [isBeforeNow, setIsBeforeNow] = useState(false)

const handleBundlePublishAtCalendarChange = (date: Date | null) => {
if (!date) return
if (isBefore(date, addMinutes(new Date(), 1))) {
setIsBeforeNow(true)
return
}

onChange(date)
}

const handleBundlePublishAtInputChange = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
const date = event.currentTarget.value
const parsedDate = parse(date, 'PP HH:mm', new Date())

if (isValid(parsedDate)) {
onChange(parsedDate)
}
},
[onChange],
)

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

return (
Expand All @@ -44,13 +51,13 @@ export const ScheduleDatePicker = ({
selectTime
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
onChange={handleBundlePublishAtCalendarChange}
onInputChange={handleBundlePublishAtInputChange}
calendarLabels={calendarLabels}
value={inputValue}
inputValue={format(inputValue, 'PPp')}
inputValue={format(inputValue, 'PP HH:mm')}
constrainSize={false}
padding={0}
isPastDisabled
disableInput
/>

<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {type FormEvent, useCallback, useState} from 'react'
import {Button, Dialog} from '../../../../ui-components'
import {useTranslation} from '../../../i18n'
import {CreatedRelease, type OriginInfo} from '../../__telemetry__/releases.telemetry'
import {releasesLocaleNamespace} from '../../i18n'
import {type EditableReleaseDocument} from '../../store/types'
import {useReleaseOperations} from '../../store/useReleaseOperations'
import {DEFAULT_RELEASE_TYPE} from '../../util/const'
import {createReleaseId} from '../../util/createReleaseId'
import {getReleaseIdFromReleaseDocumentId} from '../../util/getReleaseIdFromReleaseDocumentId'
import {ReleaseForm} from './ReleaseForm'
import {getIsScheduledDateInPast, ReleaseForm} from './ReleaseForm'

interface CreateReleaseDialogProps {
onCancel: () => void
Expand All @@ -24,6 +25,7 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
const toast = useToast()
const {createRelease} = useReleaseOperations()
const {t} = useTranslation()
const {t: tRelease} = useTranslation(releasesLocaleNamespace)
const telemetry = useTelemetry()

const [value, setValue] = useState((): EditableReleaseDocument => {
Expand All @@ -38,10 +40,23 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
})
const [isSubmitting, setIsSubmitting] = useState(false)

const [isScheduledDateInPast, setIsScheduledDateInPast] = useState(() =>
getIsScheduledDateInPast(value),
)

const handleOnSubmit = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (isScheduledDateInPast) {
toast.push({
closable: true,
status: 'warning',
title: tRelease('schedule-dialog.publish-date-in-past-warning'),
})
return // do not submit if date is in past
}

try {
event.preventDefault()
setIsSubmitting(true)

const submitValue = {
Expand All @@ -66,13 +81,18 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
onSubmit(getReleaseIdFromReleaseDocumentId(value._id))
}
},
[value, createRelease, telemetry, origin, toast, onSubmit],
[isScheduledDateInPast, toast, tRelease, value, createRelease, telemetry, origin, onSubmit],
)

const handleOnChange = useCallback((changedValue: EditableReleaseDocument) => {
setValue(changedValue)

// when the value changes, re-evaluate if the scheduled date is in the past
setIsScheduledDateInPast(getIsScheduledDateInPast(changedValue))
}, [])

const handleOnMouseEnter = () => setIsScheduledDateInPast(getIsScheduledDateInPast(value))

const dialogTitle = t('release.dialog.create.title')

return (
Expand All @@ -89,8 +109,16 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
</Box>
<Flex justify="flex-end" paddingTop={5}>
<Button
tooltipProps={{
disabled: !isScheduledDateInPast,
content: tRelease('schedule-dialog.publish-date-in-past-warning'),
}}
// to handle cases where the dialog is open for some time
// and so the validity of the date needs to be checked again
onMouseEnter={handleOnMouseEnter}
onFocus={handleOnMouseEnter}
size="large"
disabled={isSubmitting}
disabled={isSubmitting || isScheduledDateInPast}
iconRight={ArrowRightIcon}
type="submit"
text={dialogTitle}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {InfoOutlineIcon} from '@sanity/icons'
import {Card, Flex, Stack, TabList, TabPanel, Text} from '@sanity/ui'
import {isValid} from 'date-fns'
import {addMinutes, isPast, isValid} from 'date-fns'
import {useCallback, useEffect, useState} from 'react'

import {Tab, Tooltip} from '../../../../ui-components'
Expand All @@ -13,14 +13,21 @@ import {TitleDescriptionForm} from './TitleDescriptionForm'

const RELEASE_TYPES: ReleaseType[] = ['asap', 'scheduled', 'undecided']

/** @internal */
export const getIsScheduledDateInPast = (value: EditableReleaseDocument) =>
Boolean(
value.metadata.releaseType === 'scheduled' &&
value.metadata.intendedPublishAt &&
isPast(new Date(value.metadata.intendedPublishAt)),
)

/** @internal */
export function ReleaseForm(props: {
onChange: (params: EditableReleaseDocument) => void
value: EditableReleaseDocument
}): React.JSX.Element {
const {onChange, value} = props
const {releaseType} = value.metadata || {}
const publishAt = value.metadata.intendedPublishAt
const {t} = useTranslation()

const {DialogTimeZone, dialogProps} = useDialogTimeZone()
Expand All @@ -29,7 +36,7 @@ export function ReleaseForm(props: {

const [buttonReleaseType, setButtonReleaseType] = useState<ReleaseType>(releaseType ?? 'asap')

const [inputValue, setInputValue] = useState<Date>(publishAt ? new Date(publishAt) : new Date())
const [inputValue, setInputValue] = useState<Date | undefined>()

const handleBundlePublishAtCalendarChange = useCallback(
(date: Date) => {
Expand All @@ -42,9 +49,19 @@ export function ReleaseForm(props: {
const handleButtonReleaseTypeChange = useCallback(
(pickedReleaseType: ReleaseType) => {
setButtonReleaseType(pickedReleaseType)
const nextInputValue = addMinutes(new Date().setSeconds(0, 0), 1)
if (pickedReleaseType === 'scheduled') {
setInputValue(nextInputValue)
}

onChange({
...value,
metadata: {...value.metadata, releaseType: pickedReleaseType, intendedPublishAt: undefined},
metadata: {
...value.metadata,
releaseType: pickedReleaseType,
intendedPublishAt:
(pickedReleaseType === 'scheduled' && nextInputValue.toISOString()) || undefined,
},
})
},
[onChange, value],
Expand All @@ -70,7 +87,7 @@ export function ReleaseForm(props: {
*/
if (timeZone.name !== currentTimezone) {
setCurrentTimezone(timeZone.name)
if (isValid(inputValue)) {
if (inputValue && isValid(inputValue)) {
const currentZoneDate = utcToCurrentZoneDate(inputValue)
setInputValue(currentZoneDate)
}
Expand Down Expand Up @@ -101,7 +118,13 @@ export function ReleaseForm(props: {
</span>
</Text>
<Flex gap={1}>
<Card border overflow="hidden" padding={1} style={{borderRadius: 3.5}} tone="inherit">
<Card
border
overflow="hidden"
padding={1}
style={{borderRadius: 3.5, alignSelf: 'baseline'}}
tone="inherit"
>
<Flex gap={1}>
<TabList space={0.5}>
{RELEASE_TYPES.map((type) => (
Expand All @@ -126,7 +149,7 @@ export function ReleaseForm(props: {
tabIndex={-1}
>
<ScheduleDatePicker
initialValue={inputValue}
initialValue={inputValue || new Date()}
onChange={handleBundlePublishAtCalendarChange}
/>
</TabPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {useSchema} from '../../../../hooks/useSchema'
import {useTranslation} from '../../../../i18n/hooks/useTranslation'
import {Preview} from '../../../../preview/components/Preview'
import {CreatedRelease} from '../../../__telemetry__/releases.telemetry'
import {releasesLocaleNamespace} from '../../../i18n'
import {type EditableReleaseDocument} from '../../../store/types'
import {useReleaseOperations} from '../../../store/useReleaseOperations'
import {DEFAULT_RELEASE_TYPE} from '../../../util/const'
import {createReleaseId} from '../../../util/createReleaseId'
import {ReleaseForm} from '../../dialog/ReleaseForm'
import {getIsScheduledDateInPast, ReleaseForm} from '../../dialog/ReleaseForm'
import {ReleaseAvatar} from '../../ReleaseAvatar'

export function CopyToNewReleaseDialog(props: {
Expand All @@ -25,6 +26,7 @@ export function CopyToNewReleaseDialog(props: {
}): React.JSX.Element {
const {onClose, documentId, documentType, tone, title, onCreateVersion} = props
const {t} = useTranslation()
const {t: tRelease} = useTranslation(releasesLocaleNamespace)
const toast = useToast()

const schema = useSchema()
Expand All @@ -44,20 +46,36 @@ export function CopyToNewReleaseDialog(props: {
})
const [isSubmitting, setIsSubmitting] = useState(false)

const [isScheduledDateInPast, setIsScheduledDateInPast] = useState(() =>
getIsScheduledDateInPast(value),
)

const telemetry = useTelemetry()
const {createRelease} = useReleaseOperations()

const displayTitle = title || t('release.placeholder-untitled-release')

const handleOnChange = useCallback((changedValue: EditableReleaseDocument) => {
setValue(changedValue)

// when the value changes, re-evaluate if the scheduled date is in the past
setIsScheduledDateInPast(getIsScheduledDateInPast(changedValue))
}, [])

const handleAddVersion = useCallback(async () => {
onCreateVersion(newReleaseId)
}, [onCreateVersion, newReleaseId])

const handleCreateRelease = useCallback(async () => {
if (isScheduledDateInPast) {
toast.push({
closable: true,
status: 'warning',
title: tRelease('schedule-dialog.publish-date-in-past-warning'),
})
return // do not submit if date is in past
}

try {
setIsSubmitting(true)

Expand All @@ -76,7 +94,9 @@ export function CopyToNewReleaseDialog(props: {
} finally {
setIsSubmitting(false)
}
}, [createRelease, handleAddVersion, telemetry, toast, value])
}, [createRelease, handleAddVersion, isScheduledDateInPast, tRelease, telemetry, toast, value])

const handleOnMouseEnter = () => setIsScheduledDateInPast(getIsScheduledDateInPast(value))

return (
<Dialog
Expand All @@ -94,7 +114,9 @@ export function CopyToNewReleaseDialog(props: {
confirmButton: {
text: 'Add to release',
onClick: handleCreateRelease,
disabled: isSubmitting,
onMouseEnter: handleOnMouseEnter,
onFocus: handleOnMouseEnter,
disabled: isSubmitting || isScheduledDateInPast,
tone: 'primary',
},
}}
Expand Down
Loading

0 comments on commit c93c322

Please sign in to comment.