Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: release dateTime picker bugs #8403

Draft
wants to merge 11 commits into
base: corel
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DatePicker = forwardRef(function DatePicker(
monthPickerVariant?: CalendarProps['monthPickerVariant']
padding?: number
showTimezone?: boolean
isPastDisabled?: boolean
},
ref: ForwardedRef<HTMLDivElement>,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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,
forwardRef,
type KeyboardEvent,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
Expand All @@ -14,6 +16,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 All @@ -35,6 +38,7 @@ export interface DateTimeInputProps {
monthPickerVariant?: CalendarProps['monthPickerVariant']
padding?: number
disableInput?: boolean
isPastDisabled?: boolean
}

export const DateTimeInput = forwardRef(function DateTimeInput(
Expand All @@ -53,17 +57,24 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
constrainSize = true,
monthPickerVariant,
padding,
disableInput,
isPastDisabled,
...rest
} = props
const {t} = useTranslation()
const popoverRef = useRef<HTMLDivElement | null>(null)
const ref = useRef<HTMLInputElement | null>(null)
const buttonRef = useRef(null)

const [referenceElement, setReferenceElement] = useState<HTMLInputElement | null>(null)

useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
forwardedRef,
() => ref.current,
)

useEffect(() => setReferenceElement(ref.current), [])

const [isPickerOpen, setPickerOpen] = useState(false)

useClickOutsideEvent(
Expand Down Expand Up @@ -104,7 +115,7 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
<LazyTextInput
ref={ref}
{...rest}
readOnly={readOnly}
readOnly={disableInput || readOnly}
value={inputValue}
onChange={onInputChange}
suffix={
Expand All @@ -116,17 +127,24 @@ export const DateTimeInput = forwardRef(function DateTimeInput(
<Popover
constrainSize={constrainSize}
data-testid="date-input-dialog"
referenceElement={referenceElement}
portal
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}
selectTime={selectTime}
timeStep={timeStep}
onKeyUp={handleKeyUp}
value={value}
isPastDisabled={isPastDisabled}
onChange={onChange}
padding={padding}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type CalendarProps = Omit<ComponentProps<'div'>, 'onSelect'> & {
monthPickerVariant?: (typeof MONTH_PICKER_VARIANT)[keyof typeof MONTH_PICKER_VARIANT]
padding?: number
showTimezone?: boolean
isPastDisabled?: boolean
}

// This is used to maintain focus on a child element of the calendar-grid between re-renders
Expand Down Expand Up @@ -76,6 +77,7 @@ export const Calendar = forwardRef(function Calendar(
timeStep = 1,
onSelect,
labels,
isPastDisabled,
monthPickerVariant = 'select',
padding = 2,
showTimezone = false,
Expand Down Expand Up @@ -232,12 +234,14 @@ export const Calendar = forwardRef(function Calendar(
icon={ChevronLeftIcon}
mode="bleed"
onClick={() => moveFocusedDate(-1)}
data-testid="calendar-prev-month"
tooltipProps={{content: 'Previous month'}}
/>
<Button
icon={ChevronRightIcon}
mode="bleed"
onClick={() => moveFocusedDate(1)}
data-testid="calendar-next-month"
tooltipProps={{content: 'Next month'}}
/>
</TooltipDelayGroupProvider>
Expand Down Expand Up @@ -312,6 +316,7 @@ export const Calendar = forwardRef(function Calendar(
focused={focusedDate}
onSelect={handleDateChange}
selected={selectedDate}
isPastDisabled={isPastDisabled}
/>
{PRESERVE_FOCUS_ELEMENT}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Card, Text} from '@sanity/ui'
import {isPast} from 'date-fns'
import {useCallback} from 'react'

interface CalendarDayProps {
Expand All @@ -8,10 +9,11 @@ interface CalendarDayProps {
isCurrentMonth?: boolean
isToday: boolean
selected?: boolean
isPastDisabled?: boolean
}

export function CalendarDay(props: CalendarDayProps) {
const {date, focused, isCurrentMonth, isToday, onSelect, selected} = props
const {date, focused, isCurrentMonth, isToday, onSelect, selected, isPastDisabled} = props

const handleClick = useCallback(() => {
onSelect(date)
Expand All @@ -28,6 +30,7 @@ export function CalendarDay(props: CalendarDayProps) {
data-focused={focused ? 'true' : ''}
role="button"
tabIndex={-1}
disabled={isPastDisabled && !isToday && isPast(date)}
onClick={handleClick}
padding={2}
radius={2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface CalendarMonthProps {
selected?: Date
onSelect: (date: Date) => void
hidden?: boolean
isPastDisabled?: boolean
weekDayNames: [
mon: string,
tue: string,
Expand Down Expand Up @@ -62,6 +63,7 @@ export function CalendarMonth(props: CalendarMonthProps) {
key={`${weekIdx}-${dayIdx}`}
onSelect={props.onSelect}
selected={selected}
isPastDisabled={props.isPastDisabled}
/>
)
}),
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
@@ -0,0 +1,72 @@
import {EarthGlobeIcon} from '@sanity/icons'
import {Flex} from '@sanity/ui'
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'
import {type CalendarLabels} from '../../components/inputs/DateInputs/calendar/types'
import {DateTimeInput} from '../../components/inputs/DateInputs/DateTimeInput'
import {getCalendarLabels} from '../../form/inputs/DateInputs'
import {useTranslation} from '../../i18n/hooks/useTranslation'
import useDialogTimeZone from '../../scheduledPublishing/hooks/useDialogTimeZone'
import useTimeZone from '../../scheduledPublishing/hooks/useTimeZone'

interface ScheduleDatePickerProps {
initialValue: Date
onChange: (date: Date) => void
}

const inputDateFormat = 'PP HH:mm'

export const ScheduleDatePicker = ({
initialValue: inputValue,
onChange,
}: ScheduleDatePickerProps) => {
const {t} = useTranslation()
const {timeZone} = useTimeZone()
const {dialogTimeZoneShow} = useDialogTimeZone()

const handlePublishAtCalendarChange = (date: Date | null) => {
if (!date) return

onChange(date)
}

const handlePublishAtInputChange = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
const date = event.currentTarget.value
const parsedDate = parse(date, inputDateFormat, new Date())

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

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

return (
<Flex flex={1} justify="space-between">
<DateTimeInput
selectTime
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
onChange={handlePublishAtCalendarChange}
onInputChange={handlePublishAtInputChange}
calendarLabels={calendarLabels}
value={inputValue}
inputValue={format(inputValue, inputDateFormat)}
constrainSize={false}
padding={0}
isPastDisabled
/>

<Button
icon={EarthGlobeIcon}
mode="bleed"
size="default"
text={`${timeZone.abbreviation}`}
onClick={dialogTimeZoneShow}
/>
</Flex>
)
}
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,9 +25,10 @@ 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 => {
const [release, setRelease] = useState((): EditableReleaseDocument => {
return {
_id: createReleaseId(),
metadata: {
Expand All @@ -38,15 +40,28 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
})
const [isSubmitting, setIsSubmitting] = useState(false)

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

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 = {
...value,
metadata: {...value.metadata, title: value.metadata?.title?.trim()},
...release,
metadata: {...release.metadata, title: release.metadata?.title?.trim()},
}
await createRelease(submitValue)
telemetry.log(CreatedRelease, {origin})
Expand All @@ -63,16 +78,21 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
// TODO: Remove the upper part

setIsSubmitting(false)
onSubmit(getReleaseIdFromReleaseDocumentId(value._id))
onSubmit(getReleaseIdFromReleaseDocumentId(release._id))
}
},
[value, createRelease, telemetry, origin, toast, onSubmit],
[isScheduledDateInPast, toast, tRelease, release, createRelease, telemetry, origin, onSubmit],
)

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

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

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

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

return (
Expand All @@ -85,12 +105,20 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
>
<form onSubmit={handleOnSubmit}>
<Box paddingX={4} paddingBottom={4}>
<ReleaseForm onChange={handleOnChange} value={value} />
<ReleaseForm onChange={handleOnChange} value={release} />
</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
Loading
Loading