Skip to content

Commit a4f4af3

Browse files
committedMar 12, 2025·
feat(sanity): refine release creation dialogue (#8875)
1 parent 0ec5daf commit a4f4af3

File tree

3 files changed

+144
-82
lines changed

3 files changed

+144
-82
lines changed
 

‎packages/sanity/src/core/releases/components/ScheduleDatePicker.tsx

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {EarthGlobeIcon} from '@sanity/icons'
2-
import {Flex} from '@sanity/ui'
2+
import {Box, Flex} from '@sanity/ui'
33
import {format, isValid, parse} from 'date-fns'
44
import {useCallback, useMemo} from 'react'
55

@@ -48,20 +48,21 @@ export const ScheduleDatePicker = ({
4848
const calendarLabels: CalendarLabels = useMemo(() => getCalendarLabels(t), [t])
4949

5050
return (
51-
<Flex flex={1} justify="space-between">
52-
<DateTimeInput
53-
selectTime
54-
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
55-
onChange={handlePublishAtCalendarChange}
56-
onInputChange={handlePublishAtInputChange}
57-
calendarLabels={calendarLabels}
58-
value={timezoneAdjustedValue}
59-
inputValue={format(timezoneAdjustedValue, inputDateFormat)}
60-
constrainSize={false}
61-
padding={0}
62-
isPastDisabled
63-
/>
64-
51+
<Flex gap={3}>
52+
<Box flex={1}>
53+
<DateTimeInput
54+
selectTime
55+
monthPickerVariant={MONTH_PICKER_VARIANT.carousel}
56+
onChange={handlePublishAtCalendarChange}
57+
onInputChange={handlePublishAtInputChange}
58+
calendarLabels={calendarLabels}
59+
value={timezoneAdjustedValue}
60+
inputValue={format(timezoneAdjustedValue, inputDateFormat)}
61+
constrainSize={false}
62+
padding={0}
63+
isPastDisabled
64+
/>
65+
</Box>
6566
<Button
6667
icon={EarthGlobeIcon}
6768
mode="bleed"

‎packages/sanity/src/core/releases/components/dialog/CreateReleaseDialog.tsx

+20-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ArrowRightIcon} from '@sanity/icons'
22
import {useTelemetry} from '@sanity/telemetry/react'
3-
import {Box, Flex, useToast} from '@sanity/ui'
3+
import {Box, Card, Flex, useToast} from '@sanity/ui'
44
import {type FormEvent, useCallback, useState} from 'react'
55

66
import {Button, Dialog} from '../../../../ui-components'
@@ -89,23 +89,26 @@ export function CreateReleaseDialog(props: CreateReleaseDialogProps): React.JSX.
8989
id="create-release-dialog"
9090
onClose={onCancel}
9191
width={1}
92+
padding={false}
9293
>
93-
<form onSubmit={handleOnSubmit}>
94-
<Box paddingX={4} paddingBottom={4}>
95-
<ReleaseForm onChange={handleOnChange} value={release} />
96-
</Box>
97-
<Flex justify="flex-end" paddingTop={5}>
98-
<Button
99-
size="large"
100-
disabled={isSubmitting}
101-
iconRight={ArrowRightIcon}
102-
type="submit"
103-
text={dialogTitle}
104-
loading={isSubmitting}
105-
data-testid="submit-release-button"
106-
/>
107-
</Flex>
108-
</form>
94+
<Card padding={4} borderTop>
95+
<form onSubmit={handleOnSubmit}>
96+
<Box paddingBottom={4}>
97+
<ReleaseForm onChange={handleOnChange} value={release} />
98+
</Box>
99+
<Flex justify="flex-end" paddingTop={5}>
100+
<Button
101+
size="large"
102+
disabled={isSubmitting}
103+
iconRight={ArrowRightIcon}
104+
type="submit"
105+
text={dialogTitle}
106+
loading={isSubmitting}
107+
data-testid="submit-release-button"
108+
/>
109+
</Flex>
110+
</form>
111+
</Card>
109112
</Dialog>
110113
)
111114
}

‎packages/sanity/src/core/releases/components/dialog/ReleaseForm.tsx

+108-50
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
1-
import {InfoOutlineIcon} from '@sanity/icons'
2-
import {Card, Flex, Stack, TabList, TabPanel, Text} from '@sanity/ui'
1+
import {ChevronDownIcon, InfoOutlineIcon} from '@sanity/icons'
2+
import {
3+
type BadgeTone,
4+
// eslint-disable-next-line no-restricted-imports -- fine-grained control needed
5+
Button,
6+
Flex,
7+
Menu,
8+
// eslint-disable-next-line no-restricted-imports -- fine-grained control needed
9+
MenuItem,
10+
Stack,
11+
TabPanel,
12+
Text,
13+
} from '@sanity/ui'
314
import {addHours, isValid, startOfHour} from 'date-fns'
4-
import {useCallback, useEffect, useState} from 'react'
15+
import {
16+
type ComponentType,
17+
type MouseEventHandler,
18+
useCallback,
19+
useEffect,
20+
useId,
21+
useState,
22+
} from 'react'
523

6-
import {Tab, Tooltip} from '../../../../ui-components'
24+
import {MenuButton, Tooltip} from '../../../../ui-components'
725
import {useTranslation} from '../../../i18n'
826
import useTimeZone from '../../../scheduledPublishing/hooks/useTimeZone'
9-
import {type EditableReleaseDocument, type ReleaseType} from '../../store/types'
27+
import {type EditableReleaseDocument, isReleaseType, type ReleaseType} from '../../store/types'
28+
import {ReleaseAvatar} from '../ReleaseAvatar'
1029
import {ScheduleDatePicker} from '../ScheduleDatePicker'
1130
import {TitleDescriptionForm} from './TitleDescriptionForm'
1231

13-
const RELEASE_TYPES: ReleaseType[] = ['asap', 'scheduled', 'undecided']
32+
const RELEASE_TYPES: Record<ReleaseType, {tone: BadgeTone}> = {
33+
asap: {
34+
tone: 'critical',
35+
},
36+
scheduled: {
37+
tone: 'primary',
38+
},
39+
undecided: {
40+
tone: 'suggest',
41+
},
42+
}
1443

1544
/** @internal */
1645
export function ReleaseForm(props: {
@@ -36,8 +65,14 @@ export function ReleaseForm(props: {
3665
[onChange, value],
3766
)
3867

39-
const handleButtonReleaseTypeChange = useCallback(
40-
(pickedReleaseType: ReleaseType) => {
68+
const handleButtonReleaseTypeChange = useCallback<MouseEventHandler<HTMLDivElement>>(
69+
(event) => {
70+
const pickedReleaseType = event.currentTarget.dataset.value
71+
72+
if (!isReleaseType(pickedReleaseType)) {
73+
return
74+
}
75+
4176
setButtonReleaseType(pickedReleaseType)
4277

4378
// select the start of the next hour
@@ -87,12 +122,17 @@ export function ReleaseForm(props: {
87122
}
88123
}, [currentTimezone, intendedPublishAt, timeZone, utcToCurrentZoneDate])
89124

125+
const menuButtonId = useId()
126+
const [menuButton, setMenuButton] = useState<HTMLElement | null>(null)
127+
90128
return (
91129
<Stack space={5}>
92-
<Stack space={2} style={{margin: -1}}>
93-
<Text muted size={1}>
94-
{t('release.dialog.tooltip.title')}
95-
<span style={{marginLeft: 10, opacity: 0.5}}>
130+
<Stack space={4}>
131+
<Flex gap={2} align="center">
132+
<Text as="label" htmlFor={menuButtonId}>
133+
{t('release.dialog.tooltip.title')}
134+
</Text>
135+
<Text muted size={1}>
96136
<Tooltip
97137
content={
98138
<Stack space={3} style={{maxWidth: 320 - 16}}>
@@ -108,48 +148,66 @@ export function ReleaseForm(props: {
108148
>
109149
<InfoOutlineIcon />
110150
</Tooltip>
111-
</span>
112-
</Text>
113-
<Flex gap={1}>
114-
<Card
115-
border
116-
overflow="hidden"
117-
padding={1}
118-
style={{borderRadius: 3.5, alignSelf: 'baseline'}}
119-
tone="inherit"
120-
>
121-
<Flex gap={1}>
122-
<TabList space={0.5}>
123-
{RELEASE_TYPES.map((type) => (
124-
<Tab
125-
aria-controls={`release-timing-${type}`}
126-
id={`release-timing-${type}-tab`}
127-
key={type}
128-
onClick={() => handleButtonReleaseTypeChange(type)}
129-
selected={buttonReleaseType === type}
130-
label={t(`release.type.${type}`)}
151+
</Text>
152+
</Flex>
153+
<Stack space={3}>
154+
<MenuButton
155+
id={menuButtonId}
156+
ref={setMenuButton}
157+
button={
158+
<Button mode="ghost">
159+
<Flex justify="space-between" align="center">
160+
<ReleaseTypeOption
161+
text={t(`release.type.${buttonReleaseType}`)}
162+
tone={RELEASE_TYPES[buttonReleaseType].tone}
131163
/>
164+
<Text size={1}>
165+
<ChevronDownIcon />
166+
</Text>
167+
</Flex>
168+
</Button>
169+
}
170+
popover={{
171+
placement: 'bottom',
172+
matchReferenceWidth: true,
173+
boundaryElement: menuButton,
174+
}}
175+
menu={
176+
<Menu>
177+
{Object.entries(RELEASE_TYPES).map(([type, {tone}]) => (
178+
<MenuItem key={type} data-value={type} onClick={handleButtonReleaseTypeChange}>
179+
<ReleaseTypeOption text={t(`release.type.${type}`)} tone={tone} />
180+
</MenuItem>
132181
))}
133-
</TabList>
134-
</Flex>
135-
</Card>
136-
{buttonReleaseType === 'scheduled' && (
137-
<TabPanel
138-
aria-labelledby="release-timing-at-time-tab"
139-
flex={1}
140-
id="release-timing-at-time"
141-
style={{outline: 'none'}}
142-
tabIndex={-1}
143-
>
144-
<ScheduleDatePicker
145-
initialValue={intendedPublishAt || new Date()}
146-
onChange={handleBundlePublishAtCalendarChange}
147-
/>
148-
</TabPanel>
149-
)}
150-
</Flex>
182+
</Menu>
183+
}
184+
/>
185+
<Flex gap={1}>
186+
{buttonReleaseType === 'scheduled' && (
187+
<TabPanel
188+
aria-labelledby="release-timing-at-time-tab"
189+
flex={1}
190+
id="release-timing-at-time"
191+
style={{outline: 'none'}}
192+
tabIndex={-1}
193+
>
194+
<ScheduleDatePicker
195+
initialValue={intendedPublishAt || new Date()}
196+
onChange={handleBundlePublishAtCalendarChange}
197+
/>
198+
</TabPanel>
199+
)}
200+
</Flex>
201+
</Stack>
151202
</Stack>
152203
<TitleDescriptionForm release={value} onChange={handleTitleDescriptionChange} />
153204
</Stack>
154205
)
155206
}
207+
208+
const ReleaseTypeOption: ComponentType<{text: string; tone: BadgeTone}> = ({tone, text}) => (
209+
<Flex gap={3} align="center">
210+
<ReleaseAvatar padding={1} tone={tone} />
211+
<Text>{text}</Text>
212+
</Flex>
213+
)

0 commit comments

Comments
 (0)
Please sign in to comment.