Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||||
|
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||||
|
import { userAttendee } from '@/features/User/models/attendee'
|
||||||
|
import { Resource } from '../Attendees/ResourceSearch'
|
||||||
|
|
||||||
|
export interface EventFormFieldsProps {
|
||||||
|
// Form state
|
||||||
|
title: string
|
||||||
|
setTitle: (title: string) => void
|
||||||
|
description: string
|
||||||
|
setDescription: (description: string) => void
|
||||||
|
location: string
|
||||||
|
setLocation: (location: string) => void
|
||||||
|
start: string
|
||||||
|
setStart: (start: string) => void
|
||||||
|
end: string
|
||||||
|
setEnd: (end: string) => void
|
||||||
|
allday: boolean
|
||||||
|
setAllDay: (allday: boolean) => void
|
||||||
|
repetition: RepetitionObject
|
||||||
|
setRepetition: (repetition: RepetitionObject) => void
|
||||||
|
typeOfAction?: 'solo' | 'all'
|
||||||
|
attendees: userAttendee[]
|
||||||
|
setAttendees: (attendees: userAttendee[]) => void
|
||||||
|
alarm: string
|
||||||
|
setAlarm: (alarm: string) => void
|
||||||
|
busy: string
|
||||||
|
setBusy: (busy: string) => void
|
||||||
|
eventClass: 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL'
|
||||||
|
setEventClass: (eventClass: 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL') => void
|
||||||
|
timezone: string
|
||||||
|
setTimezone: (timezone: string) => void
|
||||||
|
calendarid: string
|
||||||
|
setCalendarid: (calendarid: string) => void
|
||||||
|
hasVideoConference: boolean
|
||||||
|
setHasVideoConference: (hasVideoConference: boolean) => void
|
||||||
|
meetingLink: string | null
|
||||||
|
setMeetingLink: (meetingLink: string | null) => void
|
||||||
|
selectedResources: Resource[]
|
||||||
|
setSelectedResources: (resources: Resource[]) => void
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
showMore: boolean
|
||||||
|
showDescription: boolean
|
||||||
|
setShowDescription: (showDescription: boolean) => void
|
||||||
|
showRepeat: boolean
|
||||||
|
setShowRepeat: (showRepeat: boolean) => void
|
||||||
|
isOpen?: boolean
|
||||||
|
|
||||||
|
// Data
|
||||||
|
userPersonalCalendars: Calendar[]
|
||||||
|
timezoneList: {
|
||||||
|
zones: string[]
|
||||||
|
browserTz: string
|
||||||
|
getTimezoneOffset: (tzName: string, date: Date) => string
|
||||||
|
}
|
||||||
|
eventId?: string | null
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
onStartChange?: (newStart: string) => void
|
||||||
|
onEndChange?: (newEnd: string) => void
|
||||||
|
onAllDayChange?: (
|
||||||
|
newAllDay: boolean,
|
||||||
|
newStart: string,
|
||||||
|
newEnd: string
|
||||||
|
) => void
|
||||||
|
onCalendarChange?: (newCalendarId: string) => void
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
onValidationChange?: (isValid: boolean) => void
|
||||||
|
showValidationErrors?: boolean
|
||||||
|
onHasEndDateChangedChange?: (has: boolean) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||||
|
import { getTimezoneOffset } from '@/utils/timezone'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
Typography
|
||||||
|
} from '@linagora/twake-mui'
|
||||||
|
import ArrowDropDown from '@mui/icons-material/ArrowDropDown'
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { DateTimeFields } from './DateTimeFields'
|
||||||
|
import { FieldWithLabel } from '../FieldWithLabel'
|
||||||
|
import { useAllDayToggle } from '../../hooks/useAllDayToggle'
|
||||||
|
import { combineDateTime } from '../../utils/dateTimeHelpers'
|
||||||
|
import { validateEventForm } from '../../utils/formValidation'
|
||||||
|
import type {
|
||||||
|
EventDateTimeFieldProps,
|
||||||
|
TimezoneListResult
|
||||||
|
} from '../../fields/DateTimeField.types'
|
||||||
|
import { TimezoneAutocomplete } from '@/components/Timezone/TimezoneAutocomplete'
|
||||||
|
import { SmallTimezoneSelector } from '@/components/Timezone/SmallTimeZoneSelector'
|
||||||
|
import { DateTimeRepeatPanel } from './DateTimeSubPanels'
|
||||||
|
|
||||||
|
interface DateTimeControlsRowProps {
|
||||||
|
allday: boolean
|
||||||
|
handleAllDayToggle: () => void
|
||||||
|
showRepeat: boolean
|
||||||
|
setShowRepeat: (value: boolean) => void
|
||||||
|
typeOfAction?: 'solo' | 'all'
|
||||||
|
repetition: RepetitionObject
|
||||||
|
setRepetition: (value: RepetitionObject) => void
|
||||||
|
start: string
|
||||||
|
timezone: string
|
||||||
|
setTimezone: (value: string) => void
|
||||||
|
timezoneList: TimezoneListResult
|
||||||
|
showMore: boolean
|
||||||
|
offset: string
|
||||||
|
tzLabel: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateTimeControlsRow: React.FC<DateTimeControlsRowProps> = ({
|
||||||
|
allday,
|
||||||
|
handleAllDayToggle,
|
||||||
|
showRepeat,
|
||||||
|
setShowRepeat,
|
||||||
|
typeOfAction,
|
||||||
|
repetition,
|
||||||
|
setRepetition,
|
||||||
|
start,
|
||||||
|
timezone,
|
||||||
|
setTimezone,
|
||||||
|
timezoneList,
|
||||||
|
showMore,
|
||||||
|
offset,
|
||||||
|
tzLabel
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
const [timezoneDrawerOpen, setTimezoneDrawerOpen] = useState(false)
|
||||||
|
|
||||||
|
const handleRepeatToggle = (): void => {
|
||||||
|
const newShowRepeat = !showRepeat
|
||||||
|
setShowRepeat(newShowRepeat)
|
||||||
|
if (newShowRepeat) {
|
||||||
|
const days = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']
|
||||||
|
const eventStartDate = new Date(start)
|
||||||
|
const jsDay = eventStartDate.getDay()
|
||||||
|
const icsDay = days[(jsDay + 6) % 7]
|
||||||
|
setRepetition({
|
||||||
|
freq: 'weekly',
|
||||||
|
interval: 1,
|
||||||
|
occurrences: 0,
|
||||||
|
endDate: '',
|
||||||
|
byday: [icsDay]
|
||||||
|
} as RepetitionObject)
|
||||||
|
} else {
|
||||||
|
setRepetition({
|
||||||
|
freq: '',
|
||||||
|
interval: 1,
|
||||||
|
occurrences: 0,
|
||||||
|
endDate: '',
|
||||||
|
byday: null
|
||||||
|
} as RepetitionObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel label=" " isExpanded={showMore && !isMobile}>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
gap={2}
|
||||||
|
alignItems={isMobile ? 'start' : 'center'}
|
||||||
|
flexDirection={isMobile ? 'column' : 'row'}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox checked={allday} onChange={handleAllDayToggle} />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="h6">{t('event.form.allDay')}</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
showRepeat || (typeOfAction === 'solo' && !!repetition?.freq)
|
||||||
|
}
|
||||||
|
disabled={typeOfAction === 'solo'}
|
||||||
|
onChange={handleRepeatToggle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="h6">{t('event.form.repeat')}</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Timezone selector */}
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setTimezoneDrawerOpen(true)}
|
||||||
|
sx={{ textTransform: 'none', px: 0, color: 'text.secondary' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">
|
||||||
|
({offset}) {tzLabel}
|
||||||
|
</Typography>
|
||||||
|
<ArrowDropDown />
|
||||||
|
</Button>
|
||||||
|
<SmallTimezoneSelector
|
||||||
|
open={timezoneDrawerOpen}
|
||||||
|
onClose={() => setTimezoneDrawerOpen(false)}
|
||||||
|
value={timezone}
|
||||||
|
onChange={tz => {
|
||||||
|
setTimezone(tz)
|
||||||
|
setTimezoneDrawerOpen(false)
|
||||||
|
}}
|
||||||
|
referenceDate={new Date(start)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TimezoneAutocomplete
|
||||||
|
value={timezone}
|
||||||
|
onChange={setTimezone}
|
||||||
|
zones={timezoneList.zones}
|
||||||
|
getTimezoneOffset={(tzName: string) =>
|
||||||
|
timezoneList.getTimezoneOffset(tzName, new Date(start))
|
||||||
|
}
|
||||||
|
showIcon={false}
|
||||||
|
width={220}
|
||||||
|
size="small"
|
||||||
|
placeholder={t('event.form.timezonePlaceholder')}
|
||||||
|
hideBorder
|
||||||
|
inputPadding="8px 65px 8px 0px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateTimeExpanded: React.FC<
|
||||||
|
Pick<
|
||||||
|
EventDateTimeFieldProps,
|
||||||
|
| 'start'
|
||||||
|
| 'setStart'
|
||||||
|
| 'end'
|
||||||
|
| 'setEnd'
|
||||||
|
| 'allday'
|
||||||
|
| 'setAllDay'
|
||||||
|
| 'timezone'
|
||||||
|
| 'setTimezone'
|
||||||
|
| 'repetition'
|
||||||
|
| 'setRepetition'
|
||||||
|
| 'showRepeat'
|
||||||
|
| 'setShowRepeat'
|
||||||
|
| 'showMore'
|
||||||
|
| 'showValidationErrors'
|
||||||
|
| 'timezoneList'
|
||||||
|
| 'typeOfAction'
|
||||||
|
| 'onStartChange'
|
||||||
|
| 'onEndChange'
|
||||||
|
| 'onAllDayChange'
|
||||||
|
> & {
|
||||||
|
startDate: string
|
||||||
|
startTime: string
|
||||||
|
endDate: string
|
||||||
|
endTime: string
|
||||||
|
hasEndDateChanged: boolean
|
||||||
|
setStartTime: (time: string) => void
|
||||||
|
setEndTime: (time: string) => void
|
||||||
|
setStartDate: (date: string) => void
|
||||||
|
setEndDate: (date: string) => void
|
||||||
|
setHasEndDateChanged: (isEndDateChanged: boolean) => void
|
||||||
|
}
|
||||||
|
> = ({
|
||||||
|
start,
|
||||||
|
setStart,
|
||||||
|
end,
|
||||||
|
setEnd,
|
||||||
|
allday,
|
||||||
|
setAllDay,
|
||||||
|
timezone,
|
||||||
|
setTimezone,
|
||||||
|
repetition,
|
||||||
|
setRepetition,
|
||||||
|
showRepeat,
|
||||||
|
setShowRepeat,
|
||||||
|
showMore,
|
||||||
|
hasEndDateChanged,
|
||||||
|
setHasEndDateChanged,
|
||||||
|
showValidationErrors,
|
||||||
|
timezoneList,
|
||||||
|
typeOfAction,
|
||||||
|
onStartChange,
|
||||||
|
onEndChange,
|
||||||
|
onAllDayChange,
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
setStartTime,
|
||||||
|
setEndTime,
|
||||||
|
setStartDate,
|
||||||
|
setEndDate
|
||||||
|
}) => {
|
||||||
|
const { handleAllDayToggle } = useAllDayToggle({
|
||||||
|
allday,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
setStartTime,
|
||||||
|
setEndTime,
|
||||||
|
setStart,
|
||||||
|
setEnd,
|
||||||
|
setAllDay,
|
||||||
|
onAllDayChange
|
||||||
|
})
|
||||||
|
|
||||||
|
const validation = validateEventForm({
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
showValidationErrors,
|
||||||
|
hasEndDateChanged,
|
||||||
|
showMore
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMultiDayRange = startDate !== endDate
|
||||||
|
const endDateExplicitlySet = hasEndDateChanged && isMultiDayRange
|
||||||
|
const endDateImplicitlyVisible = !allday && isMultiDayRange
|
||||||
|
const showEndDate =
|
||||||
|
showMore || allday || endDateExplicitlySet || endDateImplicitlyVisible
|
||||||
|
|
||||||
|
const offset = getTimezoneOffset(
|
||||||
|
timezone ? timezone : timezoneList.browserTz,
|
||||||
|
new Date(start)
|
||||||
|
)
|
||||||
|
const tzLabel = timezone
|
||||||
|
? timezone.split('/').pop()?.replace(/_/g, ' ')
|
||||||
|
: timezoneList.browserTz
|
||||||
|
|
||||||
|
const repeatIsActive =
|
||||||
|
showRepeat || (typeOfAction === 'solo' && Boolean(repetition?.freq))
|
||||||
|
|
||||||
|
const handleStartDateChange = useCallback(
|
||||||
|
(newDate: string, newTime?: string) => {
|
||||||
|
setStartDate(newDate)
|
||||||
|
const newStart = combineDateTime(newDate, newTime ?? startTime)
|
||||||
|
if (onStartChange) {
|
||||||
|
onStartChange(newStart)
|
||||||
|
} else {
|
||||||
|
setStart(newStart)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setStartDate, startTime, onStartChange, setStart]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleStartTimeChange = useCallback(
|
||||||
|
(newTime: string, newDate?: string) => {
|
||||||
|
setStartTime(newTime)
|
||||||
|
const newStart = combineDateTime(newDate ?? startDate, newTime)
|
||||||
|
if (onStartChange) {
|
||||||
|
onStartChange(newStart)
|
||||||
|
} else {
|
||||||
|
setStart(newStart)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setStartTime, startDate, onStartChange, setStart]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEndDateChange = useCallback(
|
||||||
|
(newDate: string, newTime?: string) => {
|
||||||
|
setEndDate(newDate)
|
||||||
|
if (showMore) setHasEndDateChanged(true)
|
||||||
|
const newEnd = combineDateTime(newDate, newTime ?? endTime)
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd)
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setEndDate, showMore, setHasEndDateChanged, endTime, onEndChange, setEnd]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleEndTimeChange = useCallback(
|
||||||
|
(newTime: string, newDate?: string) => {
|
||||||
|
setEndTime(newTime)
|
||||||
|
const newEnd = combineDateTime(newDate ?? endDate, newTime)
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd)
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[endDate, onEndChange, setEnd, setEndTime]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DateTimeFields
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
showMore={showMore}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
|
validation={validation}
|
||||||
|
onStartDateChange={handleStartDateChange}
|
||||||
|
onStartTimeChange={handleStartTimeChange}
|
||||||
|
onEndDateChange={handleEndDateChange}
|
||||||
|
onEndTimeChange={handleEndTimeChange}
|
||||||
|
showEndDate={showEndDate}
|
||||||
|
onToggleEndDate={() => {}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* All-day, Repeat checkboxes + Timezone selector row */}
|
||||||
|
<DateTimeControlsRow
|
||||||
|
allday={allday}
|
||||||
|
handleAllDayToggle={handleAllDayToggle}
|
||||||
|
showRepeat={showRepeat}
|
||||||
|
setShowRepeat={setShowRepeat}
|
||||||
|
typeOfAction={typeOfAction}
|
||||||
|
repetition={repetition}
|
||||||
|
setRepetition={setRepetition}
|
||||||
|
start={start}
|
||||||
|
timezone={timezone}
|
||||||
|
setTimezone={setTimezone}
|
||||||
|
timezoneList={timezoneList}
|
||||||
|
showMore={showMore}
|
||||||
|
offset={offset}
|
||||||
|
tzLabel={tzLabel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{repeatIsActive && (
|
||||||
|
<DateTimeRepeatPanel
|
||||||
|
repetition={repetition}
|
||||||
|
setRepetition={setRepetition}
|
||||||
|
start={start}
|
||||||
|
typeOfAction={typeOfAction}
|
||||||
|
showMore={showMore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||||
|
import React from 'react'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { DateTimeSummary } from '../DateTimeSummary'
|
||||||
|
import { FieldWithLabel } from '../FieldWithLabel'
|
||||||
|
import RepeatEvent from '../../EventRepeat'
|
||||||
|
|
||||||
|
interface DateTimeSummarySectionProps {
|
||||||
|
startDate: string
|
||||||
|
startTime: string
|
||||||
|
endDate: string
|
||||||
|
endTime: string
|
||||||
|
allday: boolean
|
||||||
|
timezone: string
|
||||||
|
repetition: RepetitionObject
|
||||||
|
hasEndDateChanged: boolean
|
||||||
|
onExpand: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateTimeSummarySection: React.FC<DateTimeSummarySectionProps> = ({
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
timezone,
|
||||||
|
repetition,
|
||||||
|
hasEndDateChanged,
|
||||||
|
onExpand
|
||||||
|
}) => {
|
||||||
|
const isMultiDay = startDate !== endDate
|
||||||
|
const showEndDate = allday || (hasEndDateChanged && isMultiDay) || isMultiDay
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateTimeSummary
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
timezone={timezone}
|
||||||
|
repetition={repetition}
|
||||||
|
showEndDate={showEndDate}
|
||||||
|
onClick={onExpand}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DateTimeRepeatPanelProps {
|
||||||
|
repetition: RepetitionObject
|
||||||
|
setRepetition: (value: RepetitionObject) => void
|
||||||
|
start: string
|
||||||
|
typeOfAction?: 'solo' | 'all'
|
||||||
|
showMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateTimeRepeatPanel: React.FC<DateTimeRepeatPanelProps> = ({
|
||||||
|
repetition,
|
||||||
|
setRepetition,
|
||||||
|
start,
|
||||||
|
typeOfAction,
|
||||||
|
showMore
|
||||||
|
}) => {
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel label=" " isExpanded={showMore && !isMobile}>
|
||||||
|
<RepeatEvent
|
||||||
|
repetition={repetition}
|
||||||
|
eventStart={new Date(start)}
|
||||||
|
setRepetition={setRepetition}
|
||||||
|
isOwn={typeOfAction !== 'solo'}
|
||||||
|
/>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { FormControl, TextField } from '@linagora/twake-mui'
|
||||||
|
import React from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { FieldWithLabel } from './FieldWithLabel'
|
||||||
|
import { Resource, ResourceSearch } from '../../Attendees/ResourceSearch'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { FreeBusyField } from '../fields/FreeBusyField'
|
||||||
|
import { NotificationField } from '../fields/NotificationField'
|
||||||
|
import { VisibilityField } from '../fields/VisibilityField'
|
||||||
|
import { EventFormFieldsProps } from '../EventFormFields.types'
|
||||||
|
|
||||||
|
export const EventFormFieldsExpanded: React.FC<
|
||||||
|
Pick<
|
||||||
|
EventFormFieldsProps,
|
||||||
|
| 'alarm'
|
||||||
|
| 'setAlarm'
|
||||||
|
| 'busy'
|
||||||
|
| 'setBusy'
|
||||||
|
| 'eventClass'
|
||||||
|
| 'setEventClass'
|
||||||
|
| 'showMore'
|
||||||
|
| 'selectedResources'
|
||||||
|
| 'setSelectedResources'
|
||||||
|
>
|
||||||
|
> = ({
|
||||||
|
alarm,
|
||||||
|
setAlarm,
|
||||||
|
busy,
|
||||||
|
setBusy,
|
||||||
|
eventClass,
|
||||||
|
setEventClass,
|
||||||
|
showMore,
|
||||||
|
selectedResources,
|
||||||
|
setSelectedResources
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showMore && (
|
||||||
|
<FieldWithLabel
|
||||||
|
label={t('event.form.resource')}
|
||||||
|
isExpanded={showMore && !isMobile}
|
||||||
|
>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<ResourceSearch
|
||||||
|
objectTypes={['resource']}
|
||||||
|
selectedResources={selectedResources}
|
||||||
|
inputSlot={params => <TextField {...params} size="small" />}
|
||||||
|
onChange={(_event: React.SyntheticEvent, value: Resource[]) =>
|
||||||
|
setSelectedResources(value)
|
||||||
|
}
|
||||||
|
hideLabel={true}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NotificationField
|
||||||
|
alarm={alarm}
|
||||||
|
setAlarm={setAlarm}
|
||||||
|
showMore={showMore}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FreeBusyField busy={busy} setBusy={setBusy} showMore={showMore} />
|
||||||
|
|
||||||
|
<VisibilityField
|
||||||
|
eventClass={eventClass}
|
||||||
|
setEventClass={setEventClass}
|
||||||
|
showMore={showMore}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||||
|
import { defaultColors } from '@/utils/defaultColors'
|
||||||
|
import { makeDisplayName } from '@/utils/makeDisplayName'
|
||||||
|
import { renameDefault } from '@/utils/renameDefault'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Typography
|
||||||
|
} from '@linagora/twake-mui'
|
||||||
|
import SquareRoundedIcon from '@mui/icons-material/SquareRounded'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { CalendarItemList } from '../../Calendar/CalendarItemList'
|
||||||
|
import { OwnerCaption } from '../../Calendar/OwnerCaption'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
import { SectionPreviewRow } from '../components/SectionPreviewRow'
|
||||||
|
|
||||||
|
export interface CalendarSelectFieldProps {
|
||||||
|
calendarid: string
|
||||||
|
setCalendarid: (value: string) => void
|
||||||
|
userPersonalCalendars: Calendar[]
|
||||||
|
showMore: boolean
|
||||||
|
/** Optional callback for additional logic on change (e.g. UpdateModal tracks newCalId) */
|
||||||
|
onCalendarChange?: (newCalendarId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarSelectFieldCollapsed: React.FC<{
|
||||||
|
calendarid: string
|
||||||
|
userPersonalCalendars: Calendar[]
|
||||||
|
selectedCalendar: Calendar | undefined
|
||||||
|
selectedOwnerDisplayName: string
|
||||||
|
isSelectedDelegated: boolean
|
||||||
|
setHasClickedCalendarSection: (value: boolean) => void
|
||||||
|
}> = ({
|
||||||
|
calendarid,
|
||||||
|
userPersonalCalendars,
|
||||||
|
selectedCalendar,
|
||||||
|
selectedOwnerDisplayName,
|
||||||
|
isSelectedDelegated,
|
||||||
|
setHasClickedCalendarSection
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionPreviewRow
|
||||||
|
icon={
|
||||||
|
<SquareRoundedIcon
|
||||||
|
sx={{
|
||||||
|
color:
|
||||||
|
userPersonalCalendars.find(cal => cal.id === calendarid)?.color
|
||||||
|
?.light ?? defaultColors[0].light,
|
||||||
|
width: 24,
|
||||||
|
height: 24
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => setHasClickedCalendarSection(true)}
|
||||||
|
>
|
||||||
|
{selectedCalendar?.name ? (
|
||||||
|
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
{renameDefault(
|
||||||
|
selectedCalendar.name,
|
||||||
|
selectedOwnerDisplayName,
|
||||||
|
t,
|
||||||
|
!isSelectedDelegated
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<OwnerCaption
|
||||||
|
showCaption={
|
||||||
|
isSelectedDelegated && selectedCalendar.name !== '#default'
|
||||||
|
}
|
||||||
|
ownerDisplayName={selectedOwnerDisplayName}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
t('event.form.calendar')
|
||||||
|
)}
|
||||||
|
</SectionPreviewRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarSelectFieldExpanded: React.FC<{
|
||||||
|
calendarid: string
|
||||||
|
setCalendarid: (value: string) => void
|
||||||
|
userPersonalCalendars: Calendar[]
|
||||||
|
calendarSelectOpen: boolean
|
||||||
|
setCalendarSelectOpen: (value: boolean) => void
|
||||||
|
onCalendarChange?: (newCalendarId: string) => void
|
||||||
|
}> = ({
|
||||||
|
calendarid,
|
||||||
|
setCalendarid,
|
||||||
|
userPersonalCalendars,
|
||||||
|
calendarSelectOpen,
|
||||||
|
setCalendarSelectOpen,
|
||||||
|
onCalendarChange
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const delegatedCalendars = userPersonalCalendars.filter(cal => cal.delegated)
|
||||||
|
const personalCalendars = userPersonalCalendars.filter(cal => !cal.delegated)
|
||||||
|
|
||||||
|
const handleCalendarChange = (newCalendarId: string): void => {
|
||||||
|
setCalendarid(newCalendarId)
|
||||||
|
onCalendarChange?.(newCalendarId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<Select
|
||||||
|
value={calendarid ?? ''}
|
||||||
|
label=""
|
||||||
|
SelectDisplayProps={{ 'aria-label': t('event.form.calendar') }}
|
||||||
|
displayEmpty
|
||||||
|
open={calendarSelectOpen}
|
||||||
|
onOpen={() => setCalendarSelectOpen(true)}
|
||||||
|
onClose={() => setCalendarSelectOpen(false)}
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
handleCalendarChange(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{CalendarItemList(personalCalendars)}
|
||||||
|
{delegatedCalendars.length > 0 && personalCalendars.length > 0 && (
|
||||||
|
<Divider component="li" />
|
||||||
|
)}
|
||||||
|
{CalendarItemList(delegatedCalendars)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CalendarSelectField: React.FC<CalendarSelectFieldProps> = ({
|
||||||
|
calendarid,
|
||||||
|
setCalendarid,
|
||||||
|
userPersonalCalendars,
|
||||||
|
showMore,
|
||||||
|
onCalendarChange
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// Local UI state
|
||||||
|
const [hasClickedCalendarSection, setHasClickedCalendarSection] =
|
||||||
|
useState(false)
|
||||||
|
const [calendarSelectOpen, setCalendarSelectOpen] = useState(false)
|
||||||
|
|
||||||
|
// Auto-open the Select once it mounts after clicking the preview row
|
||||||
|
useEffect(() => {
|
||||||
|
const openSelectAfterClick = (): void => {
|
||||||
|
if (hasClickedCalendarSection) {
|
||||||
|
setCalendarSelectOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openSelectAfterClick()
|
||||||
|
}, [hasClickedCalendarSection])
|
||||||
|
|
||||||
|
const selectedCalendar = userPersonalCalendars.find(
|
||||||
|
cal => cal.id === calendarid
|
||||||
|
)
|
||||||
|
const selectedOwnerDisplayName = selectedCalendar
|
||||||
|
? (makeDisplayName(selectedCalendar) ?? '')
|
||||||
|
: ''
|
||||||
|
const isSelectedDelegated = !!selectedCalendar?.delegated
|
||||||
|
|
||||||
|
const isCollapsed = !showMore && !hasClickedCalendarSection
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel
|
||||||
|
label={isCollapsed ? '' : t('event.form.calendar')}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<CalendarSelectFieldCollapsed
|
||||||
|
calendarid={calendarid}
|
||||||
|
userPersonalCalendars={userPersonalCalendars}
|
||||||
|
setHasClickedCalendarSection={setHasClickedCalendarSection}
|
||||||
|
selectedCalendar={selectedCalendar}
|
||||||
|
selectedOwnerDisplayName={selectedOwnerDisplayName}
|
||||||
|
isSelectedDelegated={isSelectedDelegated}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CalendarSelectFieldExpanded
|
||||||
|
calendarid={calendarid}
|
||||||
|
setCalendarid={setCalendarid}
|
||||||
|
userPersonalCalendars={userPersonalCalendars}
|
||||||
|
calendarSelectOpen={calendarSelectOpen}
|
||||||
|
setCalendarSelectOpen={setCalendarSelectOpen}
|
||||||
|
onCalendarChange={onCalendarChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { RepetitionObject } from '@/features/Events/EventsTypes'
|
||||||
|
|
||||||
|
export interface TimezoneListResult {
|
||||||
|
zones: string[]
|
||||||
|
browserTz: string
|
||||||
|
getTimezoneOffset: (tzName: string, date: Date) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventDateTimeFieldProps {
|
||||||
|
/** Controlled start value (ISO or YYYY-MM-DD) */
|
||||||
|
start: string
|
||||||
|
setStart: (value: string) => void
|
||||||
|
/** Controlled end value */
|
||||||
|
end: string
|
||||||
|
setEnd: (value: string) => void
|
||||||
|
/** All-day flag */
|
||||||
|
allday: boolean
|
||||||
|
setAllDay: (value: boolean) => void
|
||||||
|
/** Current timezone name */
|
||||||
|
timezone: string
|
||||||
|
setTimezone: (value: string) => void
|
||||||
|
/** Repetition rule */
|
||||||
|
repetition: RepetitionObject
|
||||||
|
setRepetition: (value: RepetitionObject) => void
|
||||||
|
showRepeat: boolean
|
||||||
|
setShowRepeat: (value: boolean) => void
|
||||||
|
/** Whether the full (expanded) dialog mode is active */
|
||||||
|
showMore: boolean
|
||||||
|
/** show/hide validation error messages */
|
||||||
|
showValidationErrors: boolean
|
||||||
|
timezoneList: TimezoneListResult
|
||||||
|
typeOfAction?: 'solo' | 'all'
|
||||||
|
/** Optional callbacks for calendar preview sync (EventModal only) */
|
||||||
|
onStartChange?: (newStart: string) => void
|
||||||
|
onEndChange?: (newEnd: string) => void
|
||||||
|
onAllDayChange?: (
|
||||||
|
newAllDay: boolean,
|
||||||
|
newStart: string,
|
||||||
|
newEnd: string
|
||||||
|
) => void
|
||||||
|
onHasEndDateChangedChange?: (has: boolean) => void
|
||||||
|
/** Notify parent when the validation state changes */
|
||||||
|
onValidationChange?: (isValid: boolean) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
import { useDateTimeSplit } from '../hooks/useDateTimeSplit'
|
||||||
|
import { DateTimeExpanded } from '../components/DateTimeFields/DateTimeExpanded'
|
||||||
|
import { DateTimeSummarySection } from '../components/DateTimeFields/DateTimeSubPanels'
|
||||||
|
import type { EventDateTimeFieldProps } from './DateTimeField.types'
|
||||||
|
|
||||||
|
export const EventDateTimeField: React.FC<EventDateTimeFieldProps> = ({
|
||||||
|
start,
|
||||||
|
setStart,
|
||||||
|
end,
|
||||||
|
setEnd,
|
||||||
|
allday,
|
||||||
|
setAllDay,
|
||||||
|
timezone,
|
||||||
|
setTimezone,
|
||||||
|
repetition,
|
||||||
|
setRepetition,
|
||||||
|
showRepeat,
|
||||||
|
setShowRepeat,
|
||||||
|
showMore,
|
||||||
|
showValidationErrors,
|
||||||
|
timezoneList,
|
||||||
|
typeOfAction,
|
||||||
|
onStartChange,
|
||||||
|
onEndChange,
|
||||||
|
onAllDayChange,
|
||||||
|
onHasEndDateChangedChange,
|
||||||
|
onValidationChange
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
const [hasClickedDateTimeSection, setHasClickedDateTimeSection] =
|
||||||
|
useState(false)
|
||||||
|
const [hasEndDateChanged, setHasEndDateChanged] = useState(false)
|
||||||
|
|
||||||
|
// Validation callback for DateTimeField → notify parent
|
||||||
|
const handleValidationChange = useCallback(
|
||||||
|
(isValid: boolean) => {
|
||||||
|
onValidationChange?.(isValid)
|
||||||
|
},
|
||||||
|
[onValidationChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
startDate,
|
||||||
|
setStartDate,
|
||||||
|
startTime,
|
||||||
|
setStartTime,
|
||||||
|
endDate,
|
||||||
|
setEndDate,
|
||||||
|
endTime,
|
||||||
|
setEndTime
|
||||||
|
} = useDateTimeSplit({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
allday,
|
||||||
|
showMore,
|
||||||
|
hasEndDateChanged,
|
||||||
|
setHasEndDateChanged,
|
||||||
|
showValidationErrors,
|
||||||
|
onEndChange,
|
||||||
|
setEnd,
|
||||||
|
onHasEndDateChangedChange,
|
||||||
|
onValidationChange: handleValidationChange
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCollapsed = !showMore && !hasClickedDateTimeSection
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FieldWithLabel
|
||||||
|
label={isCollapsed ? '' : t('event.form.dateTime')}
|
||||||
|
isExpanded={showMore && !isMobile}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<DateTimeSummarySection
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
timezone={timezone}
|
||||||
|
repetition={repetition}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
|
onExpand={() => setHasClickedDateTimeSection(true)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DateTimeExpanded
|
||||||
|
start={start}
|
||||||
|
setStart={setStart}
|
||||||
|
end={end}
|
||||||
|
setEnd={setEnd}
|
||||||
|
allday={allday}
|
||||||
|
setAllDay={setAllDay}
|
||||||
|
timezone={timezone}
|
||||||
|
setTimezone={setTimezone}
|
||||||
|
repetition={repetition}
|
||||||
|
setRepetition={setRepetition}
|
||||||
|
showRepeat={showRepeat}
|
||||||
|
setShowRepeat={setShowRepeat}
|
||||||
|
showMore={showMore}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
|
setHasEndDateChanged={setHasEndDateChanged}
|
||||||
|
showValidationErrors={showValidationErrors}
|
||||||
|
timezoneList={timezoneList}
|
||||||
|
typeOfAction={typeOfAction}
|
||||||
|
onStartChange={onStartChange}
|
||||||
|
onEndChange={onEndChange}
|
||||||
|
onAllDayChange={onAllDayChange}
|
||||||
|
startDate={startDate}
|
||||||
|
setStartDate={setStartDate}
|
||||||
|
startTime={startTime}
|
||||||
|
setStartTime={setStartTime}
|
||||||
|
endDate={endDate}
|
||||||
|
setEndDate={setEndDate}
|
||||||
|
endTime={endTime}
|
||||||
|
setEndTime={setEndTime}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent
|
||||||
|
} from '@linagora/twake-mui'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
|
||||||
|
export interface FreeBusyFieldProps {
|
||||||
|
busy: string
|
||||||
|
setBusy: (value: string) => void
|
||||||
|
/** Only rendered in expanded (showMore) mode */
|
||||||
|
showMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FreeBusyField: React.FC<FreeBusyFieldProps> = ({
|
||||||
|
busy,
|
||||||
|
setBusy,
|
||||||
|
showMore
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
if (!showMore) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel label={t('event.form.showMeAs')} isExpanded>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<Select
|
||||||
|
labelId="busy"
|
||||||
|
value={busy}
|
||||||
|
onChange={(e: SelectChangeEvent) => setBusy(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="TRANSPARENT">{t('event.form.free')}</MenuItem>
|
||||||
|
<MenuItem value="OPAQUE">{t('event.form.busy')}</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { TextField } from '@linagora/twake-mui'
|
||||||
|
import { LocationOn as LocationIcon } from '@mui/icons-material'
|
||||||
|
import { useRef, useState } from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||||
|
import { useEventLocation } from '../hooks/useEventLocation'
|
||||||
|
import { EventFormFieldsProps } from '../EventFormFields.types'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
import { SectionPreviewRow } from '../components/SectionPreviewRow'
|
||||||
|
|
||||||
|
const showInputLabel = (showMore: boolean, label: string): string => {
|
||||||
|
if (showMore) {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LocationField({
|
||||||
|
location,
|
||||||
|
setLocation,
|
||||||
|
showMore,
|
||||||
|
isOpen = false
|
||||||
|
}: Pick<
|
||||||
|
EventFormFieldsProps,
|
||||||
|
'location' | 'setLocation' | 'showMore' | 'isOpen'
|
||||||
|
>): JSX.Element {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||||
|
|
||||||
|
const [hasClickedLocationSection, setHasClickedLocationSection] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const locationInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEventLocation({
|
||||||
|
isOpen,
|
||||||
|
hasClickedLocationSection,
|
||||||
|
setHasClickedLocationSection,
|
||||||
|
locationInputRef
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLocationExpanded = showMore || hasClickedLocationSection
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel
|
||||||
|
label={showInputLabel(isLocationExpanded, t('event.form.location'))}
|
||||||
|
isExpanded={isLocationExpanded && !isMobile}
|
||||||
|
>
|
||||||
|
{!isLocationExpanded ? (
|
||||||
|
<SectionPreviewRow
|
||||||
|
icon={<LocationIcon />}
|
||||||
|
onClick={() => setHasClickedLocationSection(true)}
|
||||||
|
>
|
||||||
|
{location || t('event.form.locationPlaceholder')}
|
||||||
|
</SectionPreviewRow>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
autoComplete="off"
|
||||||
|
fullWidth
|
||||||
|
label=""
|
||||||
|
inputRef={locationInputRef}
|
||||||
|
inputProps={{ 'aria-label': t('event.form.location') }}
|
||||||
|
placeholder={t('event.form.locationPlaceholder')}
|
||||||
|
value={location}
|
||||||
|
onChange={e => setLocation(e.target.value)}
|
||||||
|
size={isMobile ? 'medium' : 'small'}
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent
|
||||||
|
} from '@linagora/twake-mui'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
|
||||||
|
export interface NotificationFieldProps {
|
||||||
|
alarm: string
|
||||||
|
setAlarm: (value: string) => void
|
||||||
|
/** Only rendered in expanded (showMore) mode */
|
||||||
|
showMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationField: React.FC<NotificationFieldProps> = ({
|
||||||
|
alarm,
|
||||||
|
setAlarm,
|
||||||
|
showMore
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
if (!showMore) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel label={t('event.form.notification')} isExpanded>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<Select
|
||||||
|
labelId="notification"
|
||||||
|
value={alarm}
|
||||||
|
displayEmpty
|
||||||
|
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="">{t('event.form.notifications.')}</MenuItem>
|
||||||
|
<MenuItem value="-PT1M">
|
||||||
|
{t('event.form.notifications.-PT1M')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT5M">
|
||||||
|
{t('event.form.notifications.-PT5M')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT10M">
|
||||||
|
{t('event.form.notifications.-PT10M')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT15M">
|
||||||
|
{t('event.form.notifications.-PT15M')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT30M">
|
||||||
|
{t('event.form.notifications.-PT30M')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT1H">
|
||||||
|
{t('event.form.notifications.-PT1H')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT2H">
|
||||||
|
{t('event.form.notifications.-PT2H')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT5H">
|
||||||
|
{t('event.form.notifications.-PT5H')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT12H">
|
||||||
|
{t('event.form.notifications.-PT12H')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT1D">
|
||||||
|
{t('event.form.notifications.-PT1D')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT2D">
|
||||||
|
{t('event.form.notifications.-PT2D')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="-PT1W">
|
||||||
|
{t('event.form.notifications.-PT1W')}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import iconCamera from '@/static/images/icon-camera.svg'
|
||||||
|
import {
|
||||||
|
addVideoConferenceToDescription,
|
||||||
|
generateMeetingLink,
|
||||||
|
removeVideoConferenceFromDescription
|
||||||
|
} from '@/utils/videoConferenceUtils'
|
||||||
|
import { Box, Button, IconButton } from '@linagora/twake-mui'
|
||||||
|
import {
|
||||||
|
Close as DeleteIcon,
|
||||||
|
ContentCopy as CopyIcon
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import React from 'react'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { SnackbarAlert } from '../../Loading/SnackBarAlert'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
import { SectionPreviewRow } from '../components/SectionPreviewRow'
|
||||||
|
|
||||||
|
export interface VideoConferenceFieldProps {
|
||||||
|
hasVideoConference: boolean
|
||||||
|
setHasVideoConference: (value: boolean) => void
|
||||||
|
meetingLink: string | null
|
||||||
|
setMeetingLink: (value: string | null) => void
|
||||||
|
description: string
|
||||||
|
setDescription: (value: string) => void
|
||||||
|
showMore: boolean
|
||||||
|
/** setShowDescription is called when a conference link is added in simple mode */
|
||||||
|
setShowDescription?: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoConferenceFieldInShortMode: React.FC<{
|
||||||
|
hasVideoConference: boolean
|
||||||
|
meetingLink: string | null
|
||||||
|
cameraIcon: React.ReactNode
|
||||||
|
handleCopyMeetingLink: () => Promise<void>
|
||||||
|
handleDeleteVideoConference: () => void
|
||||||
|
handleAddVideoConference: () => void
|
||||||
|
}> = ({
|
||||||
|
hasVideoConference,
|
||||||
|
meetingLink,
|
||||||
|
cameraIcon,
|
||||||
|
handleCopyMeetingLink,
|
||||||
|
handleDeleteVideoConference,
|
||||||
|
handleAddVideoConference
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
if (hasVideoConference && meetingLink) {
|
||||||
|
return (
|
||||||
|
<Box display="flex" gap={1} alignItems="center">
|
||||||
|
<Button
|
||||||
|
startIcon={cameraIcon}
|
||||||
|
onClick={() =>
|
||||||
|
window.open(meetingLink, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ borderRadius: '4px', mr: 1 }}
|
||||||
|
>
|
||||||
|
{t('event.form.joinVisioConference')}
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => void handleCopyMeetingLink()}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: 'primary.main' }}
|
||||||
|
aria-label={t('event.form.copyMeetingLink')}
|
||||||
|
title={t('event.form.copyMeetingLink')}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteVideoConference}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: 'error.main' }}
|
||||||
|
aria-label={t('event.form.removeVideoConference')}
|
||||||
|
title={t('event.form.removeVideoConference')}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionPreviewRow icon={cameraIcon} onClick={handleAddVideoConference}>
|
||||||
|
{t('event.form.addVisioConference')}
|
||||||
|
</SectionPreviewRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoConferenceFieldInExpanedMode: React.FC<{
|
||||||
|
hasVideoConference: boolean
|
||||||
|
meetingLink: string | null
|
||||||
|
cameraIcon: React.ReactNode
|
||||||
|
handleCopyMeetingLink: () => Promise<void>
|
||||||
|
handleDeleteVideoConference: () => void
|
||||||
|
handleAddVideoConference: () => void
|
||||||
|
}> = ({
|
||||||
|
hasVideoConference,
|
||||||
|
meetingLink,
|
||||||
|
cameraIcon,
|
||||||
|
handleCopyMeetingLink,
|
||||||
|
handleDeleteVideoConference,
|
||||||
|
handleAddVideoConference
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" gap={1} alignItems="center">
|
||||||
|
<Button
|
||||||
|
startIcon={cameraIcon}
|
||||||
|
onClick={handleAddVideoConference}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
sx={{
|
||||||
|
borderRadius: '4px',
|
||||||
|
display: hasVideoConference ? 'none' : 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('event.form.addVisioConference')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{hasVideoConference && meetingLink && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
startIcon={cameraIcon}
|
||||||
|
onClick={() =>
|
||||||
|
window.open(meetingLink, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
size="medium"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ borderRadius: '4px', mr: 1 }}
|
||||||
|
>
|
||||||
|
{t('event.form.joinVisioConference')}
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => void handleCopyMeetingLink()}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: 'primary.main' }}
|
||||||
|
aria-label={t('event.form.copyMeetingLink')}
|
||||||
|
title={t('event.form.copyMeetingLink')}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteVideoConference}
|
||||||
|
size="small"
|
||||||
|
sx={{ color: 'error.main' }}
|
||||||
|
aria-label={t('event.form.removeVideoConference')}
|
||||||
|
title={t('event.form.removeVideoConference')}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoConferenceField: React.FC<VideoConferenceFieldProps> = ({
|
||||||
|
hasVideoConference,
|
||||||
|
setHasVideoConference,
|
||||||
|
meetingLink,
|
||||||
|
setMeetingLink,
|
||||||
|
description,
|
||||||
|
setDescription,
|
||||||
|
showMore,
|
||||||
|
setShowDescription
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const [openToast, setOpenToast] = React.useState(false)
|
||||||
|
|
||||||
|
const handleAddVideoConference = (): void => {
|
||||||
|
const newMeetingLink = generateMeetingLink()
|
||||||
|
const updatedDescription = addVideoConferenceToDescription(
|
||||||
|
description,
|
||||||
|
newMeetingLink
|
||||||
|
)
|
||||||
|
setDescription(updatedDescription)
|
||||||
|
setHasVideoConference(true)
|
||||||
|
setMeetingLink(newMeetingLink)
|
||||||
|
if (showMore) {
|
||||||
|
setShowDescription?.(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopyMeetingLink = async (): Promise<void> => {
|
||||||
|
if (!meetingLink) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(meetingLink)
|
||||||
|
setOpenToast(true)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy link:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteVideoConference = (): void => {
|
||||||
|
setDescription(removeVideoConferenceFromDescription(description))
|
||||||
|
setHasVideoConference(false)
|
||||||
|
setMeetingLink(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraIcon = (
|
||||||
|
<img src={iconCamera} alt="camera" width={24} height={24} />
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FieldWithLabel
|
||||||
|
label={showMore ? t('event.form.videoMeeting') : ''}
|
||||||
|
isExpanded={showMore}
|
||||||
|
>
|
||||||
|
{!showMore ? (
|
||||||
|
<VideoConferenceFieldInShortMode
|
||||||
|
hasVideoConference={hasVideoConference}
|
||||||
|
meetingLink={meetingLink}
|
||||||
|
cameraIcon={cameraIcon}
|
||||||
|
handleCopyMeetingLink={handleCopyMeetingLink}
|
||||||
|
handleDeleteVideoConference={handleDeleteVideoConference}
|
||||||
|
handleAddVideoConference={handleAddVideoConference}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<VideoConferenceFieldInExpanedMode
|
||||||
|
hasVideoConference={hasVideoConference}
|
||||||
|
meetingLink={meetingLink}
|
||||||
|
cameraIcon={cameraIcon}
|
||||||
|
handleCopyMeetingLink={handleCopyMeetingLink}
|
||||||
|
handleDeleteVideoConference={handleDeleteVideoConference}
|
||||||
|
handleAddVideoConference={handleAddVideoConference}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldWithLabel>
|
||||||
|
|
||||||
|
<SnackbarAlert
|
||||||
|
setOpen={setOpenToast}
|
||||||
|
open={openToast}
|
||||||
|
message={t('event.form.meetCopied')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ToggleButton, ToggleButtonGroup } from '@linagora/twake-mui'
|
||||||
|
import { Public as PublicIcon } from '@mui/icons-material'
|
||||||
|
import LockOutlineIcon from '@mui/icons-material/LockOutline'
|
||||||
|
import { useI18n } from 'twake-i18n'
|
||||||
|
import { FieldWithLabel } from '../components/FieldWithLabel'
|
||||||
|
|
||||||
|
export interface VisibilityFieldProps {
|
||||||
|
eventClass: 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL'
|
||||||
|
setEventClass: (value: 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL') => void
|
||||||
|
/** Only rendered in expanded (showMore) mode */
|
||||||
|
showMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VisibilityField: React.FC<VisibilityFieldProps> = ({
|
||||||
|
eventClass,
|
||||||
|
setEventClass,
|
||||||
|
showMore
|
||||||
|
}) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
if (!showMore) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWithLabel label={t('event.form.visibleTo')} isExpanded>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={eventClass}
|
||||||
|
exclusive
|
||||||
|
onChange={(_e, newValue: string) => {
|
||||||
|
if (newValue !== null) {
|
||||||
|
setEventClass(newValue as 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
<ToggleButton value="PUBLIC" sx={{ minWidth: '160px' }}>
|
||||||
|
<PublicIcon sx={{ mr: 1 }} />
|
||||||
|
{t('event.form.visibleAll')}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="PRIVATE" sx={{ minWidth: '160px' }}>
|
||||||
|
<LockOutlineIcon sx={{ mr: 1 }} />
|
||||||
|
{t('event.form.visibleParticipants')}
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</FieldWithLabel>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { combineDateTime } from '../utils/dateTimeHelpers'
|
||||||
|
|
||||||
|
interface UseAutoEndTimeParams {
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
allday: boolean
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
setEndTime: (value: string) => void
|
||||||
|
onEndChange?: (newEnd: string) => void
|
||||||
|
setEnd: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-calculates end time as startTime + 1 hour when endTime is not yet set.
|
||||||
|
* Extracted from useDateTimeSplit to keep each hook under the CC threshold.
|
||||||
|
*/
|
||||||
|
export function useAutoEndTime({
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
setEndTime,
|
||||||
|
onEndChange,
|
||||||
|
setEnd
|
||||||
|
}: UseAutoEndTimeParams): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const needsAutoEnd = Boolean(startTime) && !endTime && !allday
|
||||||
|
if (!needsAutoEnd) return
|
||||||
|
const [hours, minutes] = startTime.split(':')
|
||||||
|
const endHour = (parseInt(hours) + 1) % 24
|
||||||
|
const calculatedEndTime = `${endHour.toString().padStart(2, '0')}:${minutes}`
|
||||||
|
setEndTime(calculatedEndTime)
|
||||||
|
const newEnd = combineDateTime(endDate || startDate, calculatedEndTime)
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd)
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
endDate,
|
||||||
|
startDate,
|
||||||
|
onEndChange,
|
||||||
|
setEnd,
|
||||||
|
setEndTime
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
// Effects that auto-focus the title input must not run in the test environment
|
||||||
|
// because jsdom has no layout engine and focus() calls produce false positives.
|
||||||
|
const isNotTestEnv = process.env.NODE_ENV !== 'test'
|
||||||
|
|
||||||
|
function useFocusTitleOnOpen(
|
||||||
|
isOpen: boolean,
|
||||||
|
eventId: string | null | undefined,
|
||||||
|
titleInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const isNewEventOpen = isOpen && !eventId
|
||||||
|
if (!isNewEventOpen || !isNotTestEnv) return
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
titleInputRef.current?.focus()
|
||||||
|
}, 100)
|
||||||
|
return (): void => clearTimeout(timer)
|
||||||
|
}, [isOpen, eventId, titleInputRef])
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFocusTitleOnToggle(
|
||||||
|
isOpen: boolean,
|
||||||
|
showMore: boolean,
|
||||||
|
titleInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
): void {
|
||||||
|
const prevShowMoreRef = useRef<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isFirstRender = prevShowMoreRef.current === undefined
|
||||||
|
const hasChanged = prevShowMoreRef.current !== showMore
|
||||||
|
prevShowMoreRef.current = showMore
|
||||||
|
|
||||||
|
const shouldFocusOnToggle =
|
||||||
|
!isFirstRender && isOpen && isNotTestEnv && hasChanged
|
||||||
|
if (!shouldFocusOnToggle) return
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
titleInputRef.current?.focus()
|
||||||
|
}, 150)
|
||||||
|
return (): void => clearTimeout(timer)
|
||||||
|
}, [showMore, isOpen, titleInputRef])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAutoFocusTitle = ({
|
||||||
|
isOpen,
|
||||||
|
eventId,
|
||||||
|
titleInputRef,
|
||||||
|
showMore
|
||||||
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
eventId?: string | null
|
||||||
|
titleInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
showMore: boolean
|
||||||
|
}): void => {
|
||||||
|
useFocusTitleOnOpen(isOpen, eventId, titleInputRef)
|
||||||
|
useFocusTitleOnToggle(isOpen, showMore, titleInputRef)
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { splitDateTime } from '../utils/dateTimeHelpers'
|
||||||
|
import { validateEventForm } from '../utils/formValidation'
|
||||||
|
import { useAutoEndTime } from './useAutoEndTime'
|
||||||
|
|
||||||
|
interface UseDateTimeSplitParams {
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
allday: boolean
|
||||||
|
showMore: boolean
|
||||||
|
hasEndDateChanged: boolean
|
||||||
|
setHasEndDateChanged: (value: boolean) => void
|
||||||
|
showValidationErrors: boolean
|
||||||
|
onEndChange?: (newEnd: string) => void
|
||||||
|
setEnd: (value: string) => void
|
||||||
|
onHasEndDateChangedChange?: (has: boolean) => void
|
||||||
|
onValidationChange?: (isValid: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseDateTimeSplitResult {
|
||||||
|
startDate: string
|
||||||
|
setStartDate: (value: string) => void
|
||||||
|
startTime: string
|
||||||
|
setStartTime: (value: string) => void
|
||||||
|
endDate: string
|
||||||
|
setEndDate: (value: string) => void
|
||||||
|
endTime: string
|
||||||
|
setEndTime: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeDateAndTime = (
|
||||||
|
datetime: string,
|
||||||
|
setDate: (date: string) => void,
|
||||||
|
setTime: (time: string) => void
|
||||||
|
): void => {
|
||||||
|
if (!datetime) return
|
||||||
|
|
||||||
|
const { date, time } = splitDateTime(datetime)
|
||||||
|
setDate(date)
|
||||||
|
setTime(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDateTimeSplit({
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
allday,
|
||||||
|
showMore,
|
||||||
|
hasEndDateChanged,
|
||||||
|
setHasEndDateChanged,
|
||||||
|
showValidationErrors,
|
||||||
|
onEndChange,
|
||||||
|
setEnd,
|
||||||
|
onHasEndDateChangedChange,
|
||||||
|
onValidationChange
|
||||||
|
}: UseDateTimeSplitParams): UseDateTimeSplitResult {
|
||||||
|
const [startDate, setStartDate] = useState('')
|
||||||
|
const [startTime, setStartTime] = useState('')
|
||||||
|
const [endDate, setEndDate] = useState('')
|
||||||
|
const [endTime, setEndTime] = useState('')
|
||||||
|
|
||||||
|
// Sync split fields from parent ISO strings
|
||||||
|
useEffect(() => {
|
||||||
|
changeDateAndTime(start, setStartDate, setStartTime)
|
||||||
|
}, [start])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
changeDateAndTime(end, setEndDate, setEndTime)
|
||||||
|
}, [end])
|
||||||
|
|
||||||
|
// Notify parent about validation
|
||||||
|
const isValid = validateEventForm({
|
||||||
|
startDate,
|
||||||
|
startTime,
|
||||||
|
endDate,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
showValidationErrors,
|
||||||
|
hasEndDateChanged,
|
||||||
|
showMore
|
||||||
|
}).isValid
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onValidationChange?.(isValid)
|
||||||
|
}, [isValid, onValidationChange])
|
||||||
|
|
||||||
|
// Notify parent when end-date-changed flag changes
|
||||||
|
useEffect(() => {
|
||||||
|
onHasEndDateChangedChange?.(hasEndDateChanged)
|
||||||
|
}, [hasEndDateChanged, onHasEndDateChangedChange])
|
||||||
|
|
||||||
|
// Reset end-date-changed when dates fall back to same day in normal mode
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldReset =
|
||||||
|
!showMore &&
|
||||||
|
hasEndDateChanged &&
|
||||||
|
Boolean(startDate) &&
|
||||||
|
startDate === endDate
|
||||||
|
if (shouldReset) setHasEndDateChanged(false)
|
||||||
|
}, [showMore, hasEndDateChanged, startDate, endDate, setHasEndDateChanged])
|
||||||
|
|
||||||
|
// Auto end-time calculation delegated to its own hook
|
||||||
|
useAutoEndTime({
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
allday,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
setEndTime,
|
||||||
|
onEndChange,
|
||||||
|
setEnd
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate,
|
||||||
|
setStartDate,
|
||||||
|
startTime,
|
||||||
|
setStartTime,
|
||||||
|
endDate,
|
||||||
|
setEndDate,
|
||||||
|
endTime,
|
||||||
|
setEndTime
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useEventLocation = ({
|
||||||
|
isOpen,
|
||||||
|
hasClickedLocationSection,
|
||||||
|
setHasClickedLocationSection,
|
||||||
|
locationInputRef
|
||||||
|
}: {
|
||||||
|
isOpen: boolean
|
||||||
|
hasClickedLocationSection: boolean
|
||||||
|
setHasClickedLocationSection: (hasClicked: boolean) => void
|
||||||
|
locationInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
}): void => {
|
||||||
|
// Reset location click state when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setHasClickedLocationSection(false)
|
||||||
|
}
|
||||||
|
}, [isOpen, setHasClickedLocationSection])
|
||||||
|
|
||||||
|
// Focus location field when user clicks the location preview row
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasClickedLocationSection && process.env.NODE_ENV !== 'test') {
|
||||||
|
locationInputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}, [hasClickedLocationSection, locationInputRef])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user