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