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