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

Closed
wants to merge 43 commits into from
Closed
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1b1f979
refactor: removing unneeded state for printed datetime
jordanl17 Jan 23, 2025
33c0d92
refactor: supporting disabling of just input for DateTimeInput; allow…
jordanl17 Jan 23, 2025
f124491
fix: setting seconds to zero for all schedule times
jordanl17 Jan 23, 2025
3c4ed80
fix: disabling picking dates in the past for scheduled releases
jordanl17 Jan 23, 2025
79af045
fix: schedule confirm does not allow historical dateTimes for schedul…
jordanl17 Jan 23, 2025
4101540
fix: updating type picker date prevents historical dates
jordanl17 Jan 24, 2025
6b40f3d
fix: default the release time picker to check if the current date is …
jordanl17 Jan 24, 2025
ea138a4
fix: creating new release prevents historical dates for publish; inpu…
jordanl17 Jan 24, 2025
dd69dfb
refactor: minor refactor of the create release date components
jordanl17 Jan 24, 2025
9c5fa42
test: fixing existing release date tests
jordanl17 Jan 24, 2025
3979bbc
refactor: further refactor of ReleaseTypePicker
jordanl17 Jan 24, 2025
1dace9d
test: increasing timeout on ReleaseDialog tests
jordanl17 Jan 27, 2025
5201cc8
refactor: removing unneeded state for printed datetime
jordanl17 Jan 23, 2025
e44a508
refactor: supporting disabling of just input for DateTimeInput; allow…
jordanl17 Jan 23, 2025
02d5856
fix: setting seconds to zero for all schedule times
jordanl17 Jan 23, 2025
43c3831
fix: disabling picking dates in the past for scheduled releases
jordanl17 Jan 23, 2025
3fc0bff
fix: schedule confirm does not allow historical dateTimes for schedul…
jordanl17 Jan 23, 2025
5554fe9
fix: updating type picker date prevents historical dates
jordanl17 Jan 24, 2025
9455514
fix: default the release time picker to check if the current date is …
jordanl17 Jan 24, 2025
86d8980
fix: creating new release prevents historical dates for publish; inpu…
jordanl17 Jan 24, 2025
ed9cf71
refactor: minor refactor of the create release date components
jordanl17 Jan 24, 2025
e25d61f
test: fixing existing release date tests
jordanl17 Jan 24, 2025
ba22da0
refactor: further refactor of ReleaseTypePicker
jordanl17 Jan 24, 2025
b733ba9
test: increasing timeout on ReleaseDialog tests
jordanl17 Jan 27, 2025
e63f574
Merge branch 'fix/corel-disabled-time-picker' of github.com:sanity-io…
jordanl17 Jan 29, 2025
3dcfebb
refactor: removing unneeded state for printed datetime
jordanl17 Jan 23, 2025
af04a40
refactor: supporting disabling of just input for DateTimeInput; allow…
jordanl17 Jan 23, 2025
10ebbb0
fix: setting seconds to zero for all schedule times
jordanl17 Jan 23, 2025
0c5aec1
fix: disabling picking dates in the past for scheduled releases
jordanl17 Jan 23, 2025
96f08e5
fix: schedule confirm does not allow historical dateTimes for schedul…
jordanl17 Jan 23, 2025
0868bff
fix: updating type picker date prevents historical dates
jordanl17 Jan 24, 2025
2357fb9
fix: default the release time picker to check if the current date is …
jordanl17 Jan 24, 2025
3856dbb
fix: creating new release prevents historical dates for publish; inpu…
jordanl17 Jan 24, 2025
b3a4253
refactor: minor refactor of the create release date components
jordanl17 Jan 24, 2025
c832fe4
test: fixing existing release date tests
jordanl17 Jan 24, 2025
d7ecbf9
refactor: further refactor of ReleaseTypePicker
jordanl17 Jan 24, 2025
a9a93e7
test: increasing timeout on ReleaseDialog tests
jordanl17 Jan 27, 2025
6ba2259
Merge branch 'fix/corel-disabled-time-picker' of github.com:sanity-io…
jordanl17 Jan 29, 2025
d3868c7
fix: changing to scheduled uses intended date first
jordanl17 Jan 29, 2025
808f868
refactor: better handling of temporal dates going into past; i18n of …
jordanl17 Jan 30, 2025
255250b
refactor: reverting back to sanity UI Buttons in ui component Dialog
jordanl17 Jan 30, 2025
47cb673
refactor: memoization optimization in DateTimeInput
jordanl17 Jan 30, 2025
ef495d0
refactor: improving code commentary around rerenderDialog state to ha…
jordanl17 Jan 30, 2025
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), [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this was something new I learned doing this:

Do not write or read ref.current during rendering, except for initialization. This makes your component’s behavior unpredictable.
From here

This is I think symptomatic that the sanity UI Popover should accept a react ref as referenceElement rather than a HTML element.

But by putting this into state and setting in the useEffect, it's ensured that the updates to the referenceElement happen after the initial render cycle, so that the ref.current value is accurate and aligned with the react state model

Copy link
Member Author

@jordanl17 jordanl17 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Add why the useEffect is there as a code comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, might need to add a task to improve this at some, it just feels bad that we need to add a useeffect where the only thing we are doing is this. Which might be fine but since useEffects tend to tank performance I'm aways just 👁️ when I see them

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking with Jordan, we should create a story for updating the popover to work as intended instead of having us rely on workarounds from the studio side. This fine for now we adding a code comment there.


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)) && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we move this outside to not have it in the render? use a useMemo / etc?

Copy link
Member Author

@jordanl17 jordanl17 Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Memoize in DateTimeInput

<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
10 changes: 9 additions & 1 deletion 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 Expand Up @@ -1160,6 +1162,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
/* Relative time, just now */
'relative-time.just-now': 'just now',

/** Action message to add document to new release */
'release.action.add-to-new-release': 'Add to release',
/** Action message to add document to release */
'release.action.add-to-release': 'Add to {{title}}',
/** Action message for when document is already in release */
Expand Down Expand Up @@ -1210,6 +1214,8 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'release.chip.tooltip.unknown-date': 'Unknown date',
/** Label for tooltip on deleted release */
'release.deleted-tooltip': 'This release has been deleted',
/** Title for copying version to a new release dialog */
'release.dialog.copy-to-release.title': 'Copy version to new release',
/** Title for creating releases dialog */
'release.dialog.create.title': 'Create release',
/** Label for description in tooltip to explain release types */
Expand All @@ -1232,8 +1238,10 @@ export const studioLocaleStrings = defineLocalesResources('studio', {
'release.navbar.tooltip': 'Releases',
/** The placeholder text when the release doesn't have a title */
'release.placeholder-untitled-release': 'Untitled release',
/**The toast title that will be shown when the user has a release perspective which is now archived */
/** The toast title that will be shown when the user has a release perspective which is now archived */
'release.toast.archived-release.title': "The '{{title}}' release was archived",
/** The toast tiele that will be shown the creating a release fails */
'release.toast.create-release-error.title': 'Failed to create release',
/**The toast title that will be shown when the user has a release perspective which is now deleted */
'release.toast.not-found-release.title': "The '{{title}}' release could not be found",
/** Label for when a version of a document has already been added to the release */
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,10 +6,12 @@ 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 {getIsScheduledDateInPast} from '../../util/getIsScheduledDateInPast'
import {getReleaseIdFromReleaseDocumentId} from '../../util/getReleaseIdFromReleaseDocumentId'
import {ReleaseForm} from './ReleaseForm'

Expand All @@ -24,9 +26,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 @@ -37,16 +40,32 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
} as const
})
const [isSubmitting, setIsSubmitting] = useState(false)
const [_, setRerenderDialog] = useState(0)

const isScheduledDateInPast = getIsScheduledDateInPast(release)

const handleOnSubmit = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()

// re-evaluate if date is in past
// as dialog could have been left idle for a while
if (getIsScheduledDateInPast(release)) {
toast.push({
closable: true,
status: 'warning',
title: tRelease('schedule-dialog.publish-date-in-past-warning'),
})
setRerenderDialog((cur) => cur + 1)
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 @@ -55,22 +74,22 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
toast.push({
closable: true,
status: 'error',
title: `Failed to create release`,
title: t('release.toast.create-release-error.title'),
})
} finally {
// TODO: Remove this! temporary fix to give some time for the release to be created and the releases store state updated before closing the dialog.
await new Promise((resolve) => setTimeout(resolve, 1000))
// TODO: Remove the upper part

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

const handleOnChange = useCallback((changedValue: EditableReleaseDocument) => {
setValue(changedValue)
setRelease(changedValue)
}, [])

const dialogTitle = t('release.dialog.create.title')
Expand All @@ -85,12 +104,16 @@ 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'),
}}
size="large"
disabled={isSubmitting}
disabled={isSubmitting || isScheduledDateInPast}
iconRight={ArrowRightIcon}
type="submit"
text={dialogTitle}
Expand Down
Loading
Loading