Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -10,7 +10,9 @@ import {
|
||||
IconButton,
|
||||
Stack,
|
||||
SxProps,
|
||||
Theme
|
||||
Theme,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
@@ -110,35 +112,42 @@ function ResponsiveDialog({
|
||||
actionsJustifyContent = 'flex-end',
|
||||
sx,
|
||||
...otherDialogProps
|
||||
}: ResponsiveDialogProps) {
|
||||
}: ResponsiveDialogProps): JSX.Element {
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
const isInIframe = useMemo(() => new CozyBridge().isInIframe(), [])
|
||||
const baseSx: SxProps<Theme> = {
|
||||
'& .MuiBackdrop-root': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
opacity: isExpanded ? '0 !important' : undefined,
|
||||
transition: isExpanded ? 'none !important' : undefined,
|
||||
pointerEvents: isExpanded ? 'none' : undefined
|
||||
},
|
||||
'& .MuiDialog-paper': {
|
||||
maxWidth: isExpanded ? '100%' : normalMaxWidth,
|
||||
width: '100%',
|
||||
height: isExpanded
|
||||
? `calc(100vh - ${isInIframe ? '0px' : headerHeight})`
|
||||
: undefined,
|
||||
maxHeight: isExpanded && isInIframe ? '100%' : undefined,
|
||||
margin: isExpanded ? `${isInIframe ? 0 : headerHeight} 0 0 0` : '32px',
|
||||
boxShadow: isExpanded ? 'none !important' : undefined,
|
||||
transition: isExpanded ? 'none !important' : undefined,
|
||||
zIndex: isExpanded ? 1200 : 1300
|
||||
},
|
||||
'& .MuiDialogActions-root .MuiBox-root': {
|
||||
maxWidth: isExpanded ? expandedContentMaxWidth : undefined,
|
||||
margin: isExpanded ? '0 auto' : undefined,
|
||||
padding: '0',
|
||||
width: isExpanded ? '100%' : undefined,
|
||||
justifyContent: isExpanded ? 'flex-end' : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const baseSx: SxProps<Theme> = isMobile
|
||||
? undefined
|
||||
: {
|
||||
'& .MuiBackdrop-root': {
|
||||
opacity: isExpanded ? '0 !important' : undefined,
|
||||
transition: isExpanded ? 'none !important' : undefined,
|
||||
pointerEvents: isExpanded ? 'none' : undefined
|
||||
},
|
||||
'& .MuiDialog-paper': {
|
||||
maxWidth: isExpanded ? '100%' : normalMaxWidth,
|
||||
width: '100%',
|
||||
height: isExpanded
|
||||
? `calc(100vh - ${isInIframe ? '0px' : headerHeight})`
|
||||
: undefined,
|
||||
maxHeight: isExpanded && isInIframe ? '100%' : undefined,
|
||||
margin: isExpanded
|
||||
? `${isInIframe ? 0 : headerHeight} 0 0 0`
|
||||
: '32px',
|
||||
boxShadow: isExpanded ? 'none !important' : undefined,
|
||||
transition: isExpanded ? 'none !important' : undefined,
|
||||
zIndex: isExpanded ? 1200 : 1300
|
||||
},
|
||||
'& .MuiDialogActions-root .MuiBox-root': {
|
||||
maxWidth: isExpanded ? expandedContentMaxWidth : undefined,
|
||||
margin: isExpanded ? '0 auto' : undefined,
|
||||
padding: '0',
|
||||
width: isExpanded ? '100%' : undefined,
|
||||
justifyContent: isExpanded ? 'flex-end' : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const baseContentSx: SxProps<Theme> = {
|
||||
width: '100%'
|
||||
@@ -161,7 +170,7 @@ function ResponsiveDialog({
|
||||
const handleClose = (
|
||||
event: unknown,
|
||||
reason: 'backdropClick' | 'escapeKeyDown'
|
||||
) => {
|
||||
): void => {
|
||||
if (isExpanded && reason === 'backdropClick') {
|
||||
return
|
||||
}
|
||||
@@ -175,6 +184,7 @@ function ResponsiveDialog({
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth={false}
|
||||
fullScreen={isMobile && open}
|
||||
fullWidth
|
||||
transitionDuration={isExpanded ? 0 : 300}
|
||||
sx={[baseSx, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
@@ -238,7 +248,7 @@ function ResponsiveDialog({
|
||||
<DialogActions
|
||||
sx={{
|
||||
borderTop: actionsBorderTop
|
||||
? theme => `1px solid ${theme.palette.divider}`
|
||||
? (theme: Theme): string => `1px solid ${theme.palette.divider}`
|
||||
: undefined,
|
||||
justifyContent: actionsJustifyContent
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Box, Link, Typography } from '@linagora/twake-mui'
|
||||
import {
|
||||
Box,
|
||||
Link,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery
|
||||
} from '@linagora/twake-mui'
|
||||
import React from 'react'
|
||||
|
||||
type InfoRowProps = {
|
||||
@@ -12,14 +18,14 @@ type InfoRowProps = {
|
||||
flexWrap?: React.CSSProperties['flexWrap']
|
||||
}
|
||||
|
||||
function detectUrls(text: string) {
|
||||
function detectUrls(text: string): JSX.Element[] {
|
||||
// Simple regex that captures whole URLs without splitting them apart
|
||||
const urlRegex = /(https?:\/\/[^\s]+|www\.[^\s]+)/gi
|
||||
|
||||
const parts = []
|
||||
let lastIndex = 0
|
||||
|
||||
text.replace(urlRegex, (match, _, offset) => {
|
||||
text.replace(urlRegex, (match, _, offset: number) => {
|
||||
// Push the text before the match
|
||||
if (lastIndex < offset) {
|
||||
parts.push(
|
||||
@@ -66,10 +72,12 @@ export function InfoRow({
|
||||
style,
|
||||
alignItems = 'center',
|
||||
flexWrap = 'nowrap'
|
||||
}: InfoRowProps) {
|
||||
}: InfoRowProps): JSX.Element {
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems,
|
||||
gap: 1,
|
||||
@@ -87,8 +95,8 @@ export function InfoRow({
|
||||
sx={{
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-line',
|
||||
maxHeight: '33vh',
|
||||
overflowY: 'auto',
|
||||
maxHeight: isMobile ? 'none' : '33vh',
|
||||
overflowY: isMobile ? undefined : 'auto',
|
||||
width: '100%',
|
||||
...style
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Box, TextFieldProps, Typography } from '@linagora/twake-mui'
|
||||
import {
|
||||
Box,
|
||||
TextFieldProps,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
||||
import {
|
||||
DatePicker,
|
||||
DatePickerFieldProps
|
||||
DatePickerFieldProps,
|
||||
DatePickerSlotProps
|
||||
} from '@mui/x-date-pickers/DatePicker'
|
||||
import { PickerValue } from '@mui/x-date-pickers/internals'
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
|
||||
@@ -88,8 +95,10 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
onStartTimeChange,
|
||||
onEndDateChange,
|
||||
onEndTimeChange
|
||||
}) => {
|
||||
}): JSX.Element => {
|
||||
const { t, lang } = useI18n()
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
const initialDurationRef = React.useRef<number | null>(null)
|
||||
const isUserActionRef = React.useRef(false)
|
||||
@@ -139,7 +148,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
? t('dateTimeFields.date')
|
||||
: t('dateTimeFields.startDate')
|
||||
|
||||
const handleStartDateChange = (value: PickerValue) => {
|
||||
const handleStartDateChange = (value: PickerValue): void => {
|
||||
if (!value || !value.isValid()) return
|
||||
|
||||
isUserActionRef.current = true
|
||||
@@ -169,7 +178,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartTimeChange = (value: PickerValue) => {
|
||||
const handleStartTimeChange = (value: PickerValue): void => {
|
||||
if (!value || !value.isValid()) return
|
||||
|
||||
isUserActionRef.current = true
|
||||
@@ -196,7 +205,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleEndDateChange = (value: PickerValue) => {
|
||||
const handleEndDateChange = (value: PickerValue): void => {
|
||||
if (!value || !value.isValid()) return
|
||||
|
||||
isUserActionRef.current = true
|
||||
@@ -233,7 +242,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
onEndDateChange(newDateStr)
|
||||
}
|
||||
|
||||
const handleEndTimeChange = (value: PickerValue) => {
|
||||
const handleEndTimeChange = (value: PickerValue): void => {
|
||||
if (!value || !value.isValid()) return
|
||||
|
||||
isUserActionRef.current = true
|
||||
@@ -270,7 +279,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
testId: string,
|
||||
hasError = false,
|
||||
testLabel?: string
|
||||
) => ({
|
||||
): Partial<DatePickerSlotProps<true>> => ({
|
||||
textField: {
|
||||
size: 'small' as const,
|
||||
margin: 'dense' as const,
|
||||
@@ -360,7 +369,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
>
|
||||
{isExpanded || shouldShowFullFieldsInNormal ? (
|
||||
<>
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box sx={{ maxWidth: '300px', width: '48%' }}>
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
@@ -407,7 +421,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
alignItems={isMobile ? 'stretch' : 'center'}
|
||||
>
|
||||
<Box sx={{ maxWidth: '300px', width: '48%' }}>
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
@@ -456,7 +475,12 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
</Box>
|
||||
</>
|
||||
) : shouldShowEndDateNormal ? (
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
alignItems={isMobile ? 'stretch' : 'center'}
|
||||
>
|
||||
<Box sx={{ maxWidth: '300px', width: '48%' }}>
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
@@ -501,8 +525,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box sx={{ maxWidth: '300px', width: '48%' }}>
|
||||
<Box
|
||||
display="flex"
|
||||
gap={1}
|
||||
flexDirection={isMobile ? 'column' : 'row'}
|
||||
alignItems={isMobile ? 'stretch' : 'center'}
|
||||
>
|
||||
<Box
|
||||
sx={isMobile ? undefined : { maxWidth: '300px', width: '48%' }}
|
||||
>
|
||||
<DatePicker
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDateValue}
|
||||
@@ -519,62 +550,64 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: '110px' }}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
disabled={allday}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(
|
||||
'start-time-input',
|
||||
false,
|
||||
t('dateTimeFields.startTime')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!allday && (
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: 'center',
|
||||
mx: 0.5,
|
||||
mt: 0.5
|
||||
}}
|
||||
>
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ maxWidth: '110px' }}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
disabled={allday}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(
|
||||
'end-time-input',
|
||||
!!validation.errors.dateTime,
|
||||
t('dateTimeFields.endTime')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" gap={1} flexDirection="row">
|
||||
<Box sx={{ width: isMobile ? '100%' : '110px' }}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
disabled={allday}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(
|
||||
'start-time-input',
|
||||
false,
|
||||
t('dateTimeFields.startTime')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!allday && (
|
||||
<Typography
|
||||
sx={{
|
||||
alignSelf: 'center',
|
||||
mx: 0.5,
|
||||
mt: 0.5
|
||||
}}
|
||||
>
|
||||
-
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ width: isMobile ? '100%' : '110px' }}>
|
||||
<TimePicker
|
||||
ampm={false}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
disabled={allday}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{
|
||||
field: EditableTimeField,
|
||||
actionBar: () => null
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: { sx: { display: 'none' } },
|
||||
popper: { sx: timePickerPopperSx },
|
||||
field: getTimeFieldSlotProps(
|
||||
'end-time-input',
|
||||
!!validation.errors.dateTime,
|
||||
t('dateTimeFields.endTime')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PartStat } from '@/features/User/models/attendee'
|
||||
import { userData } from '@/features/User/userDataTypes'
|
||||
import { Box, Typography } from '@linagora/twake-mui'
|
||||
import { Box, Typography, useTheme, useMediaQuery } from '@linagora/twake-mui'
|
||||
import { Dispatch, SetStateAction, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ContextualizedEvent } from '../EventsTypes'
|
||||
@@ -21,8 +21,10 @@ export function AttendanceValidation({
|
||||
user,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup
|
||||
}: AttendanceValidationProps) {
|
||||
}: AttendanceValidationProps): JSX.Element | null {
|
||||
const { currentUserAttendee, isOwn, calendar } = contextualizedEvent
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
const { t } = useI18n()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null)
|
||||
@@ -50,7 +52,7 @@ export function AttendanceValidation({
|
||||
return null
|
||||
}
|
||||
|
||||
const handleLoadingChange = (loading: boolean, value?: PartStat) => {
|
||||
const handleLoadingChange = (loading: boolean, value?: PartStat): void => {
|
||||
setIsLoading(loading)
|
||||
setLoadingValue(loading && value ? value : null)
|
||||
}
|
||||
@@ -66,16 +68,34 @@ export function AttendanceValidation({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ marginRight: 1 }}>
|
||||
{calendar.owner?.resource
|
||||
? t('eventPreview.authorizeQuestion')
|
||||
: t('eventPreview.attendingQuestion')}
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} mx={1} alignItems="center">
|
||||
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? '16px' : undefined,
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Typography variant="body2" sx={{ marginRight: 1 }}>
|
||||
{calendar.owner?.resource
|
||||
? t('eventPreview.authorizeQuestion')
|
||||
: t('eventPreview.attendingQuestion')}
|
||||
</Typography>
|
||||
)}
|
||||
<Box display="flex" gap={1} mx={1} alignItems="center">
|
||||
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
|
||||
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
|
||||
</Box>
|
||||
</Box>
|
||||
{!contextualizedEvent.isOrganizer && (
|
||||
<Typography
|
||||
@@ -91,6 +111,6 @@ export function AttendanceValidation({
|
||||
setOpen={setOpenCounterModal}
|
||||
contextualizedEvent={contextualizedEvent}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function EventCounterModal({
|
||||
open: boolean
|
||||
setOpen: (b: boolean) => void
|
||||
contextualizedEvent: ContextualizedEvent
|
||||
}) {
|
||||
}): JSX.Element {
|
||||
const { t } = useI18n()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showSuccessToast, setShowSuccessToast] = useState(false)
|
||||
@@ -56,7 +56,7 @@ export function EventCounterModal({
|
||||
errors: { dateTime: '' }
|
||||
})
|
||||
|
||||
const handleStartDateChange = (value: string) => {
|
||||
const handleStartDateChange = (value: string): void => {
|
||||
setStartDate(value)
|
||||
if (value > endDate) {
|
||||
setEndDate(value)
|
||||
@@ -65,18 +65,18 @@ export function EventCounterModal({
|
||||
setValidation({ errors: { dateTime: '' } })
|
||||
}
|
||||
|
||||
const handleStartTimeChange = (value: string) => {
|
||||
const handleStartTimeChange = (value: string): void => {
|
||||
setStartTime(value)
|
||||
setValidation({ errors: { dateTime: '' } })
|
||||
}
|
||||
|
||||
const handleEndDateChange = (value: string) => {
|
||||
const handleEndDateChange = (value: string): void => {
|
||||
setEndDate(value)
|
||||
setHasEndDateChanged(true)
|
||||
setValidation({ errors: { dateTime: '' } })
|
||||
}
|
||||
|
||||
const handleEndTimeChange = (value: string) => {
|
||||
const handleEndTimeChange = (value: string): void => {
|
||||
setEndTime(value)
|
||||
setValidation({ errors: { dateTime: '' } })
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export function EventCounterModal({
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!validate()) return
|
||||
if (
|
||||
!contextualizedEvent.currentUserAttendee?.cal_address ||
|
||||
@@ -163,6 +163,21 @@ export function EventCounterModal({
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={t('eventPreview.proposeNewTime')}
|
||||
actions={
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Button variant="text" onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('eventPreview.sendProposal')}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{/* Event title */}
|
||||
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
||||
@@ -235,21 +250,6 @@ export function EventCounterModal({
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
<Box display="flex" justifyContent="flex-end" gap={2} mt={3}>
|
||||
<Button variant="text" onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('eventPreview.sendProposal')}
|
||||
</Button>
|
||||
</Box>
|
||||
</ResponsiveDialog>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import { useAttendeesFreeBusy } from '@/components/Attendees/useFreeBusy'
|
||||
import { renderAttendeeBadge } from '@/components/Event/utils/eventUtils'
|
||||
import { useScreenSizeDetection } from '@/useScreenSizeDetection'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import { AvatarGroup, Box, Typography } from '@linagora/twake-mui'
|
||||
import { AvatarGroup, Box, Button, Typography } from '@linagora/twake-mui'
|
||||
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined'
|
||||
import { alpha, useTheme } from '@mui/material/styles'
|
||||
import { useState } from 'react'
|
||||
@@ -30,17 +31,22 @@ export function EventPreviewAttendees({
|
||||
end,
|
||||
timezone,
|
||||
eventUid
|
||||
}: EventPreviewAttendeesProps) {
|
||||
}: EventPreviewAttendeesProps): JSX.Element {
|
||||
const { t } = useI18n()
|
||||
const theme = useTheme()
|
||||
const { isTooSmall: isMobile } = useScreenSizeDetection()
|
||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9)
|
||||
const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor }
|
||||
// Icon takes 25px width + 16px (mr: 2) = 41px, use negative margin to align avatars
|
||||
const mobileAvatarOffset = '-42px'
|
||||
const userEmail = useAppSelector(state => state.user.userData.email)
|
||||
const [showAllAttendees, setShowAllAttendees] = useState(false)
|
||||
|
||||
const attendeePreview = makeAttendeePreview(allAttendees, t)
|
||||
|
||||
const toFreeBusyAttendee = (a: userAttendee) => ({
|
||||
const toFreeBusyAttendee = (
|
||||
a: userAttendee
|
||||
): { email: string; userId: null } => ({
|
||||
email: a.cal_address,
|
||||
userId: null
|
||||
})
|
||||
@@ -60,7 +66,7 @@ export function EventPreviewAttendees({
|
||||
enabled: !!(start && end && showAllAttendees)
|
||||
})
|
||||
|
||||
const busyCaption = (a: userAttendee) =>
|
||||
const busyCaption = (a: userAttendee): string | undefined =>
|
||||
freeBusyMap[a.cal_address] === 'busy'
|
||||
? a.cal_address === userEmail
|
||||
? t('event.freeBusy.busyCalOwner')
|
||||
@@ -73,44 +79,100 @@ export function EventPreviewAttendees({
|
||||
<Box sx={{ ...infoIconSx, mt: 1 }}>
|
||||
<PeopleAltOutlinedIcon />
|
||||
</Box>
|
||||
<Box style={{ marginBottom: 1, display: 'flex', flexDirection: 'row' }}>
|
||||
<Box sx={{ marginRight: 2 }}>
|
||||
<Typography>
|
||||
{t('eventPreview.guests', { count: allAttendees.length })}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '13px', color: 'text.secondary' }}>
|
||||
{attendeePreview}
|
||||
</Typography>
|
||||
</Box>
|
||||
{!showAllAttendees && (
|
||||
<AvatarGroup max={ATTENDEE_DISPLAY_LIMIT}>
|
||||
{organizer &&
|
||||
renderAttendeeBadge(
|
||||
organizer,
|
||||
'org',
|
||||
t,
|
||||
showAllAttendees,
|
||||
true
|
||||
)}
|
||||
{attendees.map((a, idx) =>
|
||||
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
|
||||
)}
|
||||
</AvatarGroup>
|
||||
)}
|
||||
<Typography
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: 1,
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? 2 : undefined
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: 2,
|
||||
fontSize: '14px',
|
||||
color: 'text.secondary',
|
||||
alignSelf: 'center'
|
||||
marginRight: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
onClick={() => setShowAllAttendees(prev => !prev)}
|
||||
>
|
||||
{showAllAttendees
|
||||
? t('eventPreview.showLess')
|
||||
: t('eventPreview.showMore')}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography>
|
||||
{t('eventPreview.guests', { count: allAttendees.length })}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '13px', color: 'text.secondary' }}>
|
||||
{attendeePreview}
|
||||
</Typography>
|
||||
</Box>
|
||||
{isMobile && showAllAttendees && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
sx={{
|
||||
marginLeft: 2,
|
||||
fontSize: '14px',
|
||||
color: 'text.secondary',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
onClick={() => setShowAllAttendees(false)}
|
||||
>
|
||||
{t('eventPreview.showLess')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!showAllAttendees && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
ml: isMobile ? mobileAvatarOffset : undefined
|
||||
}}
|
||||
>
|
||||
<AvatarGroup max={ATTENDEE_DISPLAY_LIMIT}>
|
||||
{organizer &&
|
||||
renderAttendeeBadge(
|
||||
organizer,
|
||||
'org',
|
||||
t,
|
||||
showAllAttendees,
|
||||
true
|
||||
)}
|
||||
{attendees.map((a, idx) =>
|
||||
renderAttendeeBadge(a, idx.toString(), t, showAllAttendees)
|
||||
)}
|
||||
</AvatarGroup>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
sx={{
|
||||
marginLeft: 2,
|
||||
fontSize: '14px',
|
||||
color: 'text.secondary',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
onClick={() => setShowAllAttendees(true)}
|
||||
>
|
||||
{t('eventPreview.showMore')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isMobile && showAllAttendees && (
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
sx={{
|
||||
marginLeft: 2,
|
||||
fontSize: '14px',
|
||||
color: 'text.secondary',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
onClick={() => setShowAllAttendees(false)}
|
||||
>
|
||||
{t('eventPreview.showLess')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -33,7 +33,14 @@ export function EventPreviewDetails({
|
||||
const { t } = useI18n()
|
||||
const theme = useTheme()
|
||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9)
|
||||
const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor }
|
||||
const infoIconSx = {
|
||||
minWidth: '25px',
|
||||
marginRight: 2,
|
||||
color: infoIconColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center'
|
||||
}
|
||||
|
||||
const resources = useMemo(
|
||||
() =>
|
||||
@@ -87,13 +94,18 @@ export function EventPreviewDetails({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{/* Video */}
|
||||
{event.x_openpass_videoconference && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={{ ...infoIconSx, mt: 1 }}>
|
||||
<Box sx={infoIconSx}>
|
||||
<VideocamOutlinedIcon />
|
||||
</Box>
|
||||
}
|
||||
@@ -136,7 +148,6 @@ export function EventPreviewDetails({
|
||||
{/* Location */}
|
||||
{event.location && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
<LocationOnOutlinedIcon />
|
||||
@@ -149,7 +160,6 @@ export function EventPreviewDetails({
|
||||
{/* Resource */}
|
||||
{resources?.length > 0 && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
flexWrap="wrap"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
@@ -203,7 +213,13 @@ export function EventPreviewDetails({
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
<Box
|
||||
sx={{
|
||||
...infoIconSx,
|
||||
alignItems: 'flex-start',
|
||||
alignSelf: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<SubjectIcon />
|
||||
</Box>
|
||||
}
|
||||
@@ -214,7 +230,6 @@ export function EventPreviewDetails({
|
||||
{/* Alarm */}
|
||||
{event.alarm && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
<NotificationsNoneIcon />
|
||||
@@ -237,7 +252,6 @@ export function EventPreviewDetails({
|
||||
{/* Repetition */}
|
||||
{event.repetition && (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
icon={
|
||||
<Box sx={infoIconSx}>
|
||||
<RepeatIcon />
|
||||
@@ -267,6 +281,6 @@ export function EventPreviewDetails({
|
||||
error
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function EventPreviewHeader({
|
||||
display="flex"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap={0.5}
|
||||
gap={2}
|
||||
width="100%"
|
||||
>
|
||||
{window.DEBUG && (
|
||||
|
||||
@@ -92,6 +92,7 @@ export default function EventPreviewModal({
|
||||
}
|
||||
actionsJustifyContent="center"
|
||||
style={{ overflow: 'auto' }}
|
||||
titleSx={{ backgroundColor: '#FCFCFC' }}
|
||||
title={
|
||||
<EventPreviewHeader
|
||||
event={event}
|
||||
|
||||
Reference in New Issue
Block a user