* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
exportCalendar,
|
||||
getSecretLink,
|
||||
} from "@/features/Calendars/CalendarApi";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { exportCalendar, getSecretLink } from '@/features/Calendars/CalendarApi'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -10,90 +7,90 @@ import {
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
|
||||
import { SnackbarAlert } from "../Loading/SnackBarAlert";
|
||||
import { CalendarAccessRights, UserWithAccess } from "./CalendarAccessRights";
|
||||
Typography
|
||||
} from '@linagora/twake-mui'
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ErrorSnackbar } from '../Error/ErrorSnackbar'
|
||||
import { FieldWithLabel } from '../Event/components/FieldWithLabel'
|
||||
import { SnackbarAlert } from '../Loading/SnackBarAlert'
|
||||
import { CalendarAccessRights, UserWithAccess } from './CalendarAccessRights'
|
||||
|
||||
interface AccessTabProps {
|
||||
calendar: Calendar;
|
||||
usersWithAccess: UserWithAccess[];
|
||||
onUsersWithAccessChange: (users: UserWithAccess[]) => void;
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void;
|
||||
calendar: Calendar
|
||||
usersWithAccess: UserWithAccess[]
|
||||
onUsersWithAccessChange: (users: UserWithAccess[]) => void
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void
|
||||
}
|
||||
|
||||
export function AccessTab({
|
||||
calendar,
|
||||
usersWithAccess,
|
||||
onUsersWithAccessChange,
|
||||
onInvitesLoaded,
|
||||
onInvitesLoaded
|
||||
}: AccessTabProps) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace(".json", "")}`;
|
||||
const calDAVLink = `${window.DAV_BASE_URL}${calendar.link.replace('.json', '')}`
|
||||
|
||||
const isResource = useMemo(
|
||||
() => calendar?.owner?.resource,
|
||||
[calendar?.owner?.resource]
|
||||
);
|
||||
)
|
||||
|
||||
const [secretLink, setSecretLink] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [secretLink, setSecretLink] = useState('')
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSecret() {
|
||||
const existing = await getSecretLink(
|
||||
calendar.link.replace(".json", ""),
|
||||
calendar.link.replace('.json', ''),
|
||||
false
|
||||
);
|
||||
setSecretLink(existing.secretLink);
|
||||
)
|
||||
setSecretLink(existing.secretLink)
|
||||
}
|
||||
fetchSecret();
|
||||
}, [calendar.link]);
|
||||
fetchSecret()
|
||||
}, [calendar.link])
|
||||
|
||||
const handleCopy = (content: string) => {
|
||||
navigator.clipboard.writeText(content);
|
||||
setOpen(true);
|
||||
};
|
||||
navigator.clipboard.writeText(content)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleResetSecretLink = async () => {
|
||||
const newSecret = await getSecretLink(
|
||||
calendar.link.replace(".json", ""),
|
||||
calendar.link.replace('.json', ''),
|
||||
true
|
||||
);
|
||||
setSecretLink(newSecret.secretLink);
|
||||
};
|
||||
)
|
||||
setSecretLink(newSecret.secretLink)
|
||||
}
|
||||
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const [exportError, setExportError] = useState("");
|
||||
const [exportLoading, setExportLoading] = useState(false)
|
||||
const [exportError, setExportError] = useState('')
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setExportLoading(true);
|
||||
setExportLoading(true)
|
||||
const exportedData = await exportCalendar(
|
||||
calendar.link.replace(".json", "")
|
||||
);
|
||||
const blob = new Blob([exportedData], { type: "text/calendar" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${calendar.id.split("/")[1]}.ics`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
calendar.link.replace('.json', '')
|
||||
)
|
||||
const blob = new Blob([exportedData], { type: 'text/calendar' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${calendar.id.split('/')[1]}.ics`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
setExportError((e as Error).message);
|
||||
setExportError((e as Error).message)
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
setExportLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,13 +102,13 @@ export function AccessTab({
|
||||
/>
|
||||
|
||||
{!!window.DAV_BASE_URL && !isResource && (
|
||||
<FieldWithLabel label={t("calendar.caldav_access")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.caldav_access')} isExpanded={false}>
|
||||
<Box mt={2}>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("calendar.caldav_access") }}
|
||||
inputProps={{ 'aria-label': t('calendar.caldav_access') }}
|
||||
value={calDAVLink}
|
||||
size="small"
|
||||
InputProps={{
|
||||
@@ -124,20 +121,20 @@ export function AccessTab({
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
)}
|
||||
|
||||
<FieldWithLabel label={t("calendar.secretUrl")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.secretUrl')} isExpanded={false}>
|
||||
<Box mt={3} display="flex" alignItems="center" gap={1}>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("calendar.secretUrl") }}
|
||||
inputProps={{ 'aria-label': t('calendar.secretUrl') }}
|
||||
value={secretLink}
|
||||
size="small"
|
||||
InputProps={{
|
||||
@@ -147,32 +144,32 @@ export function AccessTab({
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleResetSecretLink}
|
||||
sx={{ borderRadius: "4px" }}
|
||||
sx={{ borderRadius: '4px' }}
|
||||
>
|
||||
{t("actions.reset")}
|
||||
{t('actions.reset')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", mt: 1, lineHeight: 1.5 }}
|
||||
sx={{ color: 'text.secondary', mt: 1, lineHeight: 1.5 }}
|
||||
>
|
||||
{t("calendar.secretUrlDesc")}
|
||||
{t('calendar.secretUrlDesc')}
|
||||
</Typography>
|
||||
</FieldWithLabel>
|
||||
|
||||
<FieldWithLabel label={t("calendar.exportCalendar")} isExpanded={false}>
|
||||
<FieldWithLabel label={t('calendar.exportCalendar')} isExpanded={false}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.secondary", my: 1, lineHeight: 1.5 }}
|
||||
sx={{ color: 'text.secondary', my: 1, lineHeight: 1.5 }}
|
||||
>
|
||||
{t("calendar.exportDesc")}
|
||||
{t('calendar.exportDesc')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -180,15 +177,15 @@ export function AccessTab({
|
||||
onClick={handleExport}
|
||||
startIcon={!exportLoading && <FileDownloadOutlinedIcon />}
|
||||
disabled={exportLoading}
|
||||
sx={{ borderRadius: "4px" }}
|
||||
sx={{ borderRadius: '4px' }}
|
||||
>
|
||||
{exportLoading ? (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<CircularProgress size={18} />
|
||||
{t("actions.exporting")}
|
||||
{t('actions.exporting')}
|
||||
</Box>
|
||||
) : (
|
||||
t("actions.export")
|
||||
t('actions.export')
|
||||
)}
|
||||
</Button>
|
||||
</FieldWithLabel>
|
||||
@@ -196,9 +193,9 @@ export function AccessTab({
|
||||
<SnackbarAlert
|
||||
setOpen={setOpen}
|
||||
open={open}
|
||||
message={t("common.link_copied")}
|
||||
message={t('common.link_copied')}
|
||||
/>
|
||||
<ErrorSnackbar error={exportError} type="calendar" />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,195 +1,199 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import EventPopover from "@/features/Events/EventModal";
|
||||
import EventPreviewModal from "@/features/Events/EventPreview";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import ImportAlert from "@/features/Events/ImportAlert";
|
||||
import SearchResultsPage from "@/features/Search/SearchResultsPage";
|
||||
import { setTimeZone } from "@/features/Settings/SettingsSlice";
|
||||
import { setDisplayedDateAndRange } from "@/utils/CalendarRangeManager";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars";
|
||||
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
|
||||
import { browserDefaultTimeZone } from "@/utils/timezone";
|
||||
import type { EventApi, LocaleInput } from "@fullcalendar/core";
|
||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import ruLocale from "@fullcalendar/core/locales/ru";
|
||||
import viLocale from "@fullcalendar/core/locales/vi";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import { Box, Button, radius } from "@linagora/twake-mui";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import moment from "moment-timezone";
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { useCalendarDataLoader } from "../../features/Calendars/useCalendarLoader";
|
||||
import { User } from "../Attendees/PeopleSearch";
|
||||
import { EventErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { EventErrorHandler } from "../Error/EventErrorHandler";
|
||||
import { EditModeDialog } from "../Event/EditModeDialog";
|
||||
import { Menubar, MenubarProps } from "../Menubar/Menubar";
|
||||
import "./Calendar.styl";
|
||||
import CalendarSelection from "./CalendarSelection";
|
||||
import "./CustomCalendar.styl";
|
||||
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
|
||||
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
|
||||
import { MiniCalendar } from "./MiniCalendar";
|
||||
import { TempCalendarsInput } from "./TempCalendarsInput";
|
||||
import { TimezoneSelector } from "./TimezoneSelector";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import EventPopover from '@/features/Events/EventModal'
|
||||
import EventPreviewModal from '@/features/Events/EventPreview'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import ImportAlert from '@/features/Events/ImportAlert'
|
||||
import SearchResultsPage from '@/features/Search/SearchResultsPage'
|
||||
import { setTimeZone } from '@/features/Settings/SettingsSlice'
|
||||
import { setDisplayedDateAndRange } from '@/utils/CalendarRangeManager'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import { setSelectedCalendars as setSelectedCalendarsToStorage } from '@/utils/storage/setSelectedCalendars'
|
||||
import { useSelectedCalendars } from '@/utils/storage/useSelectedCalendars'
|
||||
import { browserDefaultTimeZone } from '@/utils/timezone'
|
||||
import type { EventApi, LocaleInput } from '@fullcalendar/core'
|
||||
import { CalendarApi, DateSelectArg } from '@fullcalendar/core'
|
||||
import frLocale from '@fullcalendar/core/locales/fr'
|
||||
import ruLocale from '@fullcalendar/core/locales/ru'
|
||||
import viLocale from '@fullcalendar/core/locales/vi'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import momentTimezonePlugin from '@fullcalendar/moment-timezone'
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import { Box, Button, radius } from '@linagora/twake-mui'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import moment from 'moment-timezone'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { useCalendarDataLoader } from '../../features/Calendars/useCalendarLoader'
|
||||
import { User } from '../Attendees/PeopleSearch'
|
||||
import { EventErrorSnackbar } from '../Error/ErrorSnackbar'
|
||||
import { EventErrorHandler } from '../Error/EventErrorHandler'
|
||||
import { EditModeDialog } from '../Event/EditModeDialog'
|
||||
import { Menubar, MenubarProps } from '../Menubar/Menubar'
|
||||
import './Calendar.styl'
|
||||
import CalendarSelection from './CalendarSelection'
|
||||
import './CustomCalendar.styl'
|
||||
import { useCalendarEventHandlers } from './hooks/useCalendarEventHandlers'
|
||||
import { useCalendarViewHandlers } from './hooks/useCalendarViewHandlers'
|
||||
import { MiniCalendar } from './MiniCalendar'
|
||||
import { TempCalendarsInput } from './TempCalendarsInput'
|
||||
import { TimezoneSelector } from './TimezoneSelector'
|
||||
import {
|
||||
eventToFullCalendarFormat,
|
||||
extractEvents,
|
||||
updateSlotLabelVisibility,
|
||||
} from "./utils/calendarUtils";
|
||||
updateSlotLabelVisibility
|
||||
} from './utils/calendarUtils'
|
||||
|
||||
const localeMap: Record<string, LocaleInput | undefined> = {
|
||||
fr: frLocale,
|
||||
ru: ruLocale,
|
||||
vi: viLocale,
|
||||
en: undefined,
|
||||
};
|
||||
en: undefined
|
||||
}
|
||||
|
||||
interface CalendarAppProps {
|
||||
calendarRef: MutableRefObject<CalendarApi | null>;
|
||||
onDateChange?: (date: Date) => void;
|
||||
onViewChange?: (view: string) => void;
|
||||
menubarProps?: MenubarProps;
|
||||
calendarRef: MutableRefObject<CalendarApi | null>
|
||||
onDateChange?: (date: Date) => void
|
||||
onViewChange?: (view: string) => void
|
||||
menubarProps?: MenubarProps
|
||||
}
|
||||
|
||||
export default function CalendarApp({
|
||||
calendarRef,
|
||||
onDateChange,
|
||||
onViewChange,
|
||||
menubarProps,
|
||||
menubarProps
|
||||
}: CalendarAppProps) {
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [debouncedDate, setDebouncedDate] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState(new Date())
|
||||
const [debouncedDate, setDebouncedDate] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebouncedDate(selectedDate), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [selectedDate]);
|
||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date());
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const dispatch = useAppDispatch();
|
||||
const view = useAppSelector((state) => state.settings.view);
|
||||
const userData = useAppSelector((state) => state.user.userData);
|
||||
const t = setTimeout(() => setDebouncedDate(selectedDate), 300)
|
||||
return () => clearTimeout(t)
|
||||
}, [selectedDate])
|
||||
const [selectedMiniDate, setSelectedMiniDate] = useState(new Date())
|
||||
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||
const dispatch = useAppDispatch()
|
||||
const view = useAppSelector(state => state.settings.view)
|
||||
const userData = useAppSelector(state => state.user.userData)
|
||||
const workingDays = useAppSelector(
|
||||
(state) => state.settings.businessHours?.daysOfWeek
|
||||
);
|
||||
const hideWorkingDays = useAppSelector((state) => state.settings.workingDays);
|
||||
state => state.settings.businessHours?.daysOfWeek
|
||||
)
|
||||
const hideWorkingDays = useAppSelector(state => state.settings.workingDays)
|
||||
|
||||
const hideDeclinedEvents = useAppSelector(
|
||||
(state) => state.settings.hideDeclinedEvents
|
||||
);
|
||||
state => state.settings.hideDeclinedEvents
|
||||
)
|
||||
const hiddenDays = useMemo(() => {
|
||||
if (!hideWorkingDays || !workingDays || workingDays.length === 0) return [];
|
||||
const validWorkingDays = workingDays.filter((d) => d >= 0 && d <= 6);
|
||||
if (validWorkingDays.length === 0) return [];
|
||||
return [0, 1, 2, 3, 4, 5, 6].filter((d) => !validWorkingDays.includes(d));
|
||||
}, [hideWorkingDays, workingDays]);
|
||||
if (!hideWorkingDays || !workingDays || workingDays.length === 0) return []
|
||||
const validWorkingDays = workingDays.filter(d => d >= 0 && d <= 6)
|
||||
if (validWorkingDays.length === 0) return []
|
||||
return [0, 1, 2, 3, 4, 5, 6].filter(d => !validWorkingDays.includes(d))
|
||||
}, [hideWorkingDays, workingDays])
|
||||
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const isPending = useAppSelector((state) => state.calendars.pending);
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
const isPending = useAppSelector(state => state.calendars.pending)
|
||||
const displayWeekNumbers = useAppSelector(
|
||||
(state) => state.settings.displayWeekNumbers
|
||||
);
|
||||
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||
const storedCalendars = useSelectedCalendars();
|
||||
state => state.settings.displayWeekNumbers
|
||||
)
|
||||
const tempcalendars = useAppSelector(state => state.calendars.templist)
|
||||
const storedCalendars = useSelectedCalendars()
|
||||
const [selectedCalendars, setSelectedCalendars] =
|
||||
useState<string[]>(storedCalendars);
|
||||
useState<string[]>(storedCalendars)
|
||||
|
||||
const calendarIdsString = useMemo(
|
||||
() =>
|
||||
Object.keys(calendars || {})
|
||||
.sort()
|
||||
.join(","),
|
||||
.join(','),
|
||||
[calendars]
|
||||
);
|
||||
)
|
||||
const calendarIds = useMemo(
|
||||
() => (calendarIdsString ? calendarIdsString.split(",") : []),
|
||||
() => (calendarIdsString ? calendarIdsString.split(',') : []),
|
||||
[calendarIdsString]
|
||||
);
|
||||
)
|
||||
|
||||
const [currentView, setCurrentView] = useState("timeGridWeek");
|
||||
const [currentView, setCurrentView] = useState('timeGridWeek')
|
||||
const timezone =
|
||||
useAppSelector((state) => state.settings.timeZone) ??
|
||||
browserDefaultTimeZone;
|
||||
useAppSelector(state => state.settings.timeZone) ?? browserDefaultTimeZone
|
||||
|
||||
// Auto-select personal calendars when first loaded
|
||||
const initialLoadRef = useRef(true);
|
||||
const [eventErrors, setEventErrors] = useState<string[]>([]);
|
||||
const errorHandler = useRef(new EventErrorHandler());
|
||||
const initialLoadRef = useRef(true)
|
||||
const [eventErrors, setEventErrors] = useState<string[]>([])
|
||||
const errorHandler = useRef(new EventErrorHandler())
|
||||
|
||||
useEffect(() => {
|
||||
const handler = errorHandler.current;
|
||||
handler.setErrorCallback(setEventErrors);
|
||||
return () => handler.setErrorCallback(() => {});
|
||||
}, []);
|
||||
const handler = errorHandler.current
|
||||
handler.setErrorCallback(setEventErrors)
|
||||
return () => handler.setErrorCallback(() => {})
|
||||
}, [])
|
||||
|
||||
const handleErrorClose = () => {
|
||||
setEventErrors([]);
|
||||
errorHandler.current.clearAll();
|
||||
};
|
||||
setEventErrors([])
|
||||
errorHandler.current.clearAll()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoadRef.current && calendarIds.length > 0 && userId) {
|
||||
const cached = localStorage.getItem("selectedCalendars");
|
||||
if (cached && cached.length > 0) {
|
||||
const parsed = JSON.parse(cached) as string[];
|
||||
const valid = parsed.filter((id) => calendars[id]);
|
||||
setSelectedCalendars(valid);
|
||||
} else {
|
||||
const personalCalendarIds = calendarIds.filter(
|
||||
(id) => extractEventBaseUuid(id) === userId
|
||||
);
|
||||
setSelectedCalendars(personalCalendarIds);
|
||||
const updateSelectedCalendars = () => {
|
||||
if (initialLoadRef.current && calendarIds.length > 0 && userId) {
|
||||
const cached = localStorage.getItem('selectedCalendars')
|
||||
if (cached && cached.length > 0) {
|
||||
const parsed = JSON.parse(cached) as string[]
|
||||
const valid = parsed.filter(id => calendars[id])
|
||||
setSelectedCalendars(valid)
|
||||
} else {
|
||||
const personalCalendarIds = calendarIds.filter(
|
||||
id => extractEventBaseUuid(id) === userId
|
||||
)
|
||||
setSelectedCalendars(personalCalendarIds)
|
||||
}
|
||||
initialLoadRef.current = false
|
||||
}
|
||||
initialLoadRef.current = false;
|
||||
}
|
||||
}, [calendarIds, calendars, userId]);
|
||||
updateSelectedCalendars()
|
||||
}, [calendarIds, calendars, userId])
|
||||
|
||||
// Save selected cals to cache
|
||||
useEffect(() => {
|
||||
if (calendarIds.length > 0) {
|
||||
setSelectedCalendarsToStorage(selectedCalendars);
|
||||
setSelectedCalendarsToStorage(selectedCalendars)
|
||||
}
|
||||
}, [selectedCalendars, calendarIds.length]);
|
||||
}, [selectedCalendars, calendarIds.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (calendarIds.length === 0) return;
|
||||
const validCalendarIds = new Set(calendarIds);
|
||||
setSelectedCalendars((prev) => {
|
||||
const filtered = prev.filter((calId) => validCalendarIds.has(calId));
|
||||
if (filtered.length === prev.length) {
|
||||
const unchanged = filtered.every((id, index) => id === prev[index]);
|
||||
if (unchanged) {
|
||||
return prev;
|
||||
const updateSelectedCalendarsOnCalendarChange = () => {
|
||||
if (calendarIds.length === 0) return
|
||||
const validCalendarIds = new Set(calendarIds)
|
||||
setSelectedCalendars(prev => {
|
||||
const filtered = prev.filter(calId => validCalendarIds.has(calId))
|
||||
if (filtered.length === prev.length) {
|
||||
const unchanged = filtered.every((id, index) => id === prev[index])
|
||||
if (unchanged) {
|
||||
return prev
|
||||
}
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
}, [calendarIds]);
|
||||
return filtered
|
||||
})
|
||||
}
|
||||
updateSelectedCalendarsOnCalendarChange()
|
||||
}, [calendarIds])
|
||||
|
||||
const sortedSelectedCalendars = useMemo(
|
||||
() => [...selectedCalendars].sort(),
|
||||
[selectedCalendars]
|
||||
);
|
||||
)
|
||||
|
||||
const tempCalendarIdsString = useMemo(
|
||||
() =>
|
||||
Object.keys(tempcalendars || {})
|
||||
.sort()
|
||||
.join(","),
|
||||
.join(','),
|
||||
[tempcalendars]
|
||||
);
|
||||
)
|
||||
const tempCalendarIds = useMemo(
|
||||
() => (tempCalendarIdsString ? tempCalendarIdsString.split(",") : []),
|
||||
() => (tempCalendarIdsString ? tempCalendarIdsString.split(',') : []),
|
||||
[tempCalendarIdsString]
|
||||
);
|
||||
)
|
||||
|
||||
useCalendarDataLoader({
|
||||
selectedDate: debouncedDate,
|
||||
@@ -198,47 +202,47 @@ export default function CalendarApp({
|
||||
sortedSelectedCalendars,
|
||||
calendarIds,
|
||||
calendarIdsString,
|
||||
tempCalendarIds,
|
||||
});
|
||||
tempCalendarIds
|
||||
})
|
||||
|
||||
const filteredEvents: CalendarEvent[] = extractEvents(
|
||||
selectedCalendars,
|
||||
calendars || {},
|
||||
userData?.email,
|
||||
hideDeclinedEvents
|
||||
);
|
||||
)
|
||||
|
||||
const filteredTempEvents: CalendarEvent[] = extractEvents(
|
||||
tempCalendarIds,
|
||||
tempcalendars || {},
|
||||
userData?.email,
|
||||
hideDeclinedEvents
|
||||
);
|
||||
)
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const [openEventDisplay, setOpenEventDisplay] = useState(false)
|
||||
const [eventDisplayedId, setEventDisplayedId] = useState('')
|
||||
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false)
|
||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState('')
|
||||
|
||||
// Listen for eventModalError event to reopen modal on API failure
|
||||
useEffect(() => {
|
||||
const handleEventModalError = (event: CustomEvent) => {
|
||||
if (event.detail?.type === "create") {
|
||||
if (event.detail?.type === 'create') {
|
||||
// Reopen create event modal
|
||||
setAnchorEl(document.body);
|
||||
} else if (event.detail?.type === "update") {
|
||||
setAnchorEl(document.body)
|
||||
} else if (event.detail?.type === 'update') {
|
||||
// Store update modal info to sessionStorage for EventDisplayPreview to pick up
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"eventUpdateModalReopen",
|
||||
'eventUpdateModalReopen',
|
||||
JSON.stringify({
|
||||
eventId: event.detail.eventId,
|
||||
calId: event.detail.calId,
|
||||
typeOfAction: event.detail.typeOfAction,
|
||||
timestamp: Date.now(),
|
||||
timestamp: Date.now()
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
// Open EventDisplayPreview if it's not already open with matching event, so it can pick up the sessionStorage
|
||||
if (
|
||||
@@ -246,56 +250,52 @@ export default function CalendarApp({
|
||||
eventDisplayedId !== event.detail.eventId ||
|
||||
eventDisplayedCalId !== event.detail.calId
|
||||
) {
|
||||
setEventDisplayedId(event.detail.eventId);
|
||||
setEventDisplayedCalId(event.detail.calId);
|
||||
setEventDisplayedTemp(false);
|
||||
setOpenEventDisplay(true);
|
||||
setEventDisplayedId(event.detail.eventId)
|
||||
setEventDisplayedCalId(event.detail.calId)
|
||||
setEventDisplayedTemp(false)
|
||||
setOpenEventDisplay(true)
|
||||
} else {
|
||||
// If EventDisplayPreview is already open, trigger reopen by dispatching a custom event
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("eventUpdateModalReopen", {
|
||||
new CustomEvent('eventUpdateModalReopen', {
|
||||
detail: {
|
||||
eventId: event.detail.eventId,
|
||||
calId: event.detail.calId,
|
||||
typeOfAction: event.detail.typeOfAction,
|
||||
},
|
||||
typeOfAction: event.detail.typeOfAction
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"eventModalError",
|
||||
'eventModalError',
|
||||
handleEventModalError as EventListener
|
||||
);
|
||||
)
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"eventModalError",
|
||||
'eventModalError',
|
||||
handleEventModalError as EventListener
|
||||
);
|
||||
};
|
||||
}, [openEventDisplay, eventDisplayedId, eventDisplayedCalId]);
|
||||
)
|
||||
}
|
||||
}, [openEventDisplay, eventDisplayedId, eventDisplayedCalId])
|
||||
|
||||
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined);
|
||||
)
|
||||
const [, setTypeOfAction] = useState<'solo' | 'all' | undefined>(undefined)
|
||||
const [afterChoiceFunc, setAfterChoiceFunc] = useState<
|
||||
((type: "solo" | "all" | undefined) => void) | undefined
|
||||
>();
|
||||
const [, setSelectedEvent] = useState<CalendarEvent>({} as CalendarEvent);
|
||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
||||
null
|
||||
);
|
||||
((type: 'solo' | 'all' | undefined) => void) | undefined
|
||||
>()
|
||||
const [, setSelectedEvent] = useState<CalendarEvent>({} as CalendarEvent)
|
||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(null)
|
||||
|
||||
const [tempUsers, setTempUsers] = useState<User[]>([]);
|
||||
const [tempEvent, setTempEvent] = useState<CalendarEvent>(
|
||||
{} as CalendarEvent
|
||||
);
|
||||
const [tempUsers, setTempUsers] = useState<User[]>([])
|
||||
const [tempEvent, setTempEvent] = useState<CalendarEvent>({} as CalendarEvent)
|
||||
|
||||
// Event handlers
|
||||
const eventHandlers = useCalendarEventHandlers({
|
||||
@@ -313,8 +313,8 @@ export default function CalendarApp({
|
||||
setOpenEditModePopup,
|
||||
tempUsers,
|
||||
setTempEvent,
|
||||
timezone,
|
||||
});
|
||||
timezone
|
||||
})
|
||||
|
||||
// View handlers
|
||||
const viewHandlers = useCalendarViewHandlers({
|
||||
@@ -324,18 +324,22 @@ export default function CalendarApp({
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
errorHandler: errorHandler.current,
|
||||
});
|
||||
// Note: To preserve current logic, this will temporarily disable eslint for react-hooks/refs
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
errorHandler: errorHandler.current
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
window.__calendarRef = calendarRef;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
window.__calendarRef = calendarRef
|
||||
}
|
||||
}, [calendarRef])
|
||||
|
||||
const { t, lang } = useI18n();
|
||||
const { t, lang } = useI18n()
|
||||
|
||||
return (
|
||||
<main
|
||||
className={`main-layout calendar-layout ${menubarProps?.isIframe ? " isInIframe" : ""}`}
|
||||
className={`main-layout calendar-layout ${menubarProps?.isIframe ? ' isInIframe' : ''}`}
|
||||
>
|
||||
<Box
|
||||
className="sidebar"
|
||||
@@ -344,16 +348,16 @@ export default function CalendarApp({
|
||||
paddingBottom: 3,
|
||||
paddingLeft: 3,
|
||||
paddingRight: 2,
|
||||
width: "270px",
|
||||
width: '270px'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "sticky",
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: "#fff",
|
||||
paddingTop: menubarProps?.isIframe ? "10px" : 3,
|
||||
backgroundColor: '#fff',
|
||||
paddingTop: menubarProps?.isIframe ? '10px' : 3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
@@ -365,13 +369,13 @@ export default function CalendarApp({
|
||||
}
|
||||
sx={{
|
||||
borderRadius: radius.lg,
|
||||
fontSize: "16px",
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
lineHeight: "normal",
|
||||
lineHeight: 'normal'
|
||||
}}
|
||||
>
|
||||
<AddIcon sx={{ marginRight: 0.5, fontSize: "20px" }} />{" "}
|
||||
{t("event.createEvent")}
|
||||
<AddIcon sx={{ marginRight: 0.5, fontSize: '20px' }} />{' '}
|
||||
{t('event.createEvent')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -385,7 +389,7 @@ export default function CalendarApp({
|
||||
tempUsers={tempUsers}
|
||||
setTempUsers={setTempUsers}
|
||||
handleToggleEventPreview={() => {
|
||||
eventHandlers.handleDateSelect(null as unknown as DateSelectArg);
|
||||
eventHandlers.handleDateSelect(null as unknown as DateSelectArg)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -399,19 +403,19 @@ export default function CalendarApp({
|
||||
<div className="calendar">
|
||||
<ImportAlert />
|
||||
{menubarProps?.isIframe && <Menubar {...menubarProps} />}
|
||||
{view === "calendar" && (
|
||||
{view === 'calendar' && (
|
||||
<FullCalendar
|
||||
key={hiddenDays.join(",")}
|
||||
ref={(ref) => {
|
||||
key={hiddenDays.join(',')}
|
||||
ref={ref => {
|
||||
if (ref) {
|
||||
calendarRef.current = ref.getApi();
|
||||
calendarRef.current = ref.getApi()
|
||||
}
|
||||
}}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
momentTimezonePlugin,
|
||||
momentTimezonePlugin
|
||||
]}
|
||||
initialView="timeGridWeek"
|
||||
firstDay={1}
|
||||
@@ -420,16 +424,16 @@ export default function CalendarApp({
|
||||
hiddenDays={hiddenDays}
|
||||
selectable={true}
|
||||
timeZone={timezone}
|
||||
height={"100%"}
|
||||
height="100%"
|
||||
select={eventHandlers.handleDateSelect}
|
||||
nowIndicator
|
||||
slotLabelClassNames={(arg) => [
|
||||
updateSlotLabelVisibility(new Date(), arg, timezone),
|
||||
slotLabelClassNames={arg => [
|
||||
updateSlotLabelVisibility(new Date(), arg, timezone)
|
||||
]}
|
||||
nowIndicatorContent={viewHandlers.handleNowIndicatorContent}
|
||||
headerToolbar={false}
|
||||
views={{
|
||||
timeGridWeek: { titleFormat: { month: "long", year: "numeric" } },
|
||||
timeGridWeek: { titleFormat: { month: 'long', year: 'numeric' } }
|
||||
}}
|
||||
dayMaxEvents={true}
|
||||
events={eventToFullCalendarFormat(
|
||||
@@ -444,15 +448,15 @@ export default function CalendarApp({
|
||||
a.extendedProps.priority - b.extendedProps.priority
|
||||
}
|
||||
weekNumbers={
|
||||
currentView === "timeGridWeek" || currentView === "timeGridDay"
|
||||
currentView === 'timeGridWeek' || currentView === 'timeGridDay'
|
||||
}
|
||||
weekNumberFormat={{ week: "long" }}
|
||||
weekNumberContent={(arg) => {
|
||||
weekNumberFormat={{ week: 'long' }}
|
||||
weekNumberContent={arg => {
|
||||
return (
|
||||
<div className="weekSelector">
|
||||
{displayWeekNumbers && (
|
||||
<div>
|
||||
{t("menubar.views.week")} {arg.num}
|
||||
{t('menubar.views.week')} {arg.num}
|
||||
</div>
|
||||
)}
|
||||
<TimezoneSelector
|
||||
@@ -463,91 +467,91 @@ export default function CalendarApp({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}}
|
||||
dayCellContent={(arg) => {
|
||||
const month = arg.date.toLocaleDateString(t("locale"), {
|
||||
month: "short",
|
||||
timeZone: timezone,
|
||||
});
|
||||
if (arg.view.type === "dayGridMonth") {
|
||||
dayCellContent={arg => {
|
||||
const month = arg.date.toLocaleDateString(t('locale'), {
|
||||
month: 'short',
|
||||
timeZone: timezone
|
||||
})
|
||||
if (arg.view.type === 'dayGridMonth') {
|
||||
return (
|
||||
<span
|
||||
className={`fc-daygrid-day-number ${
|
||||
arg.isToday ? "current-date" : ""
|
||||
arg.isToday ? 'current-date' : ''
|
||||
}`}
|
||||
>
|
||||
{arg.dayNumberText === "1" ? month : ""} {arg.dayNumberText}
|
||||
{arg.dayNumberText === '1' ? month : ''} {arg.dayNumberText}
|
||||
</span>
|
||||
);
|
||||
)
|
||||
}
|
||||
}}
|
||||
slotDuration={"00:30:00"}
|
||||
slotLabelInterval={"01:00:00"}
|
||||
slotDuration="00:30:00"
|
||||
slotLabelInterval="01:00:00"
|
||||
scrollTime="12:00:00"
|
||||
unselectAuto={false}
|
||||
allDayText=""
|
||||
slotLabelFormat={{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}}
|
||||
datesSet={(arg) => {
|
||||
setCurrentView(arg.view.type);
|
||||
datesSet={arg => {
|
||||
setCurrentView(arg.view.type)
|
||||
const calendarCurrentDate =
|
||||
calendarRef.current?.getDate() || new Date(arg.start);
|
||||
setDisplayedDateAndRange(calendarCurrentDate);
|
||||
calendarRef.current?.getDate() || new Date(arg.start)
|
||||
setDisplayedDateAndRange(calendarCurrentDate)
|
||||
|
||||
if (arg.view.type === "dayGridMonth") {
|
||||
const start = new Date(arg.start).getTime();
|
||||
const end = new Date(arg.end).getTime();
|
||||
const middle = start + (end - start) / 2;
|
||||
setSelectedDate(new Date(middle));
|
||||
setSelectedMiniDate(calendarCurrentDate);
|
||||
if (arg.view.type === 'dayGridMonth') {
|
||||
const start = new Date(arg.start).getTime()
|
||||
const end = new Date(arg.end).getTime()
|
||||
const middle = start + (end - start) / 2
|
||||
setSelectedDate(new Date(middle))
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
} else {
|
||||
setSelectedDate(calendarCurrentDate);
|
||||
setSelectedMiniDate(calendarCurrentDate);
|
||||
setSelectedDate(calendarCurrentDate)
|
||||
setSelectedMiniDate(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Always use the calendar's current date for consistency
|
||||
if (onDateChange) {
|
||||
onDateChange(calendarCurrentDate);
|
||||
onDateChange(calendarCurrentDate)
|
||||
}
|
||||
|
||||
// Notify parent about view change
|
||||
if (onViewChange) {
|
||||
onViewChange(arg.view.type);
|
||||
onViewChange(arg.view.type)
|
||||
}
|
||||
|
||||
// Update slot label visibility when view changes
|
||||
setTimeout(() => {
|
||||
updateSlotLabelVisibility(new Date());
|
||||
}, 100);
|
||||
updateSlotLabelVisibility(new Date())
|
||||
}, 100)
|
||||
}}
|
||||
dayHeaderContent={(arg) => {
|
||||
const m = moment.tz(arg.date, timezone);
|
||||
dayHeaderContent={arg => {
|
||||
const m = moment.tz(arg.date, timezone)
|
||||
|
||||
const date = m.date();
|
||||
const date = m.date()
|
||||
const weekDay = m
|
||||
.toDate()
|
||||
.toLocaleDateString(t("locale"), {
|
||||
weekday: "short",
|
||||
timeZone: timezone,
|
||||
.toLocaleDateString(t('locale'), {
|
||||
weekday: 'short',
|
||||
timeZone: timezone
|
||||
})
|
||||
.toUpperCase();
|
||||
.toUpperCase()
|
||||
|
||||
return (
|
||||
<div className="fc-daygrid-day-top">
|
||||
<small>{weekDay}</small>
|
||||
{arg.view.type !== "dayGridMonth" && (
|
||||
{arg.view.type !== 'dayGridMonth' && (
|
||||
<span
|
||||
className={`fc-daygrid-day-number ${arg.isToday ? "current-date" : ""}`}
|
||||
className={`fc-daygrid-day-number ${arg.isToday ? 'current-date' : ''}`}
|
||||
>
|
||||
{date}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}}
|
||||
dayHeaderDidMount={viewHandlers.handleDayHeaderDidMount}
|
||||
dayHeaderWillUnmount={viewHandlers.handleDayHeaderWillUnmount}
|
||||
@@ -561,7 +565,7 @@ export default function CalendarApp({
|
||||
eventDidMount={viewHandlers.handleEventDidMount}
|
||||
/>
|
||||
)}
|
||||
{view === "search" && <SearchResultsPage />}
|
||||
{view === 'search' && <SearchResultsPage />}
|
||||
<EventPopover
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
@@ -574,10 +578,10 @@ export default function CalendarApp({
|
||||
<EditModeDialog
|
||||
type={openEditModePopup}
|
||||
setOpen={setOpenEditModePopup}
|
||||
eventAction={(type: "solo" | "all" | undefined) => {
|
||||
setTypeOfAction(type);
|
||||
eventAction={(type: 'solo' | 'all' | undefined) => {
|
||||
setTypeOfAction(type)
|
||||
if (afterChoiceFunc) {
|
||||
afterChoiceFunc(type);
|
||||
afterChoiceFunc(type)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -593,5 +597,5 @@ export default function CalendarApp({
|
||||
<EventErrorSnackbar messages={eventErrors} onClose={handleErrorClose} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAppSelector } from "@/app/hooks";
|
||||
import { AccessRight, Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { getUserDetails } from "@/features/User/userAPI";
|
||||
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||
import { normalizeEmail } from "@/utils/normalizeEmail";
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import { AccessRight, Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { getUserDetails } from '@/features/User/userAPI'
|
||||
import { makeDisplayName } from '@/utils/makeDisplayName'
|
||||
import { normalizeEmail } from '@/utils/normalizeEmail'
|
||||
import {
|
||||
AutocompleteRenderInputParams,
|
||||
Avatar,
|
||||
@@ -13,236 +13,236 @@ import {
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
|
||||
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
||||
import { FieldWithLabel } from "../Event/components/FieldWithLabel";
|
||||
import { stringAvatar } from "../Event/utils/eventUtils";
|
||||
import { ResourceAdmin } from "./ResourceAdmins";
|
||||
Typography
|
||||
} from '@linagora/twake-mui'
|
||||
import HighlightOffIcon from '@mui/icons-material/HighlightOff'
|
||||
import PeopleOutlineOutlinedIcon from '@mui/icons-material/PeopleOutlineOutlined'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { PeopleSearch, User } from '../Attendees/PeopleSearch'
|
||||
import { FieldWithLabel } from '../Event/components/FieldWithLabel'
|
||||
import { stringAvatar } from '../Event/utils/eventUtils'
|
||||
import { ResourceAdmin } from './ResourceAdmins'
|
||||
|
||||
export interface UserWithAccess extends User {
|
||||
accessRight: AccessRight;
|
||||
accessRight: AccessRight
|
||||
}
|
||||
|
||||
interface CalendarAccessRightsProps {
|
||||
calendar: Calendar;
|
||||
value: UserWithAccess[];
|
||||
onChange: (users: UserWithAccess[]) => void;
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void;
|
||||
calendar: Calendar
|
||||
value: UserWithAccess[]
|
||||
onChange: (users: UserWithAccess[]) => void
|
||||
onInvitesLoaded: (users: UserWithAccess[]) => void
|
||||
}
|
||||
|
||||
interface UserInCalendar {
|
||||
id: string;
|
||||
access: AccessRight;
|
||||
id: string
|
||||
access: AccessRight
|
||||
}
|
||||
|
||||
export function CalendarAccessRights({
|
||||
calendar,
|
||||
value: usersWithAccess,
|
||||
onChange,
|
||||
onInvitesLoaded,
|
||||
onInvitesLoaded
|
||||
}: CalendarAccessRightsProps) {
|
||||
const { t } = useI18n();
|
||||
const userData = useAppSelector((state) => state.user.userData);
|
||||
const isPersonalCalendar = userData?.openpaasId === calendar.id.split("/")[0];
|
||||
const currentUserEmail = normalizeEmail(userData?.email);
|
||||
const isDelegatedWithAdministration = !!calendar.invite?.some((invite) => {
|
||||
const invitedEmail = normalizeEmail(invite.href.replace(/^mailto:/i, ""));
|
||||
return invitedEmail === currentUserEmail && invite.access === 5;
|
||||
});
|
||||
const { t } = useI18n()
|
||||
const userData = useAppSelector(state => state.user.userData)
|
||||
const isPersonalCalendar = userData?.openpaasId === calendar.id.split('/')[0]
|
||||
const currentUserEmail = normalizeEmail(userData?.email)
|
||||
const isDelegatedWithAdministration = !!calendar.invite?.some(invite => {
|
||||
const invitedEmail = normalizeEmail(invite.href.replace(/^mailto:/i, ''))
|
||||
return invitedEmail === currentUserEmail && invite.access === 5
|
||||
})
|
||||
|
||||
const ownerEmail =
|
||||
calendar.owner?.preferredEmail ?? calendar.owner?.emails?.[0] ?? "";
|
||||
const ownerName = makeDisplayName(calendar) ?? ownerEmail;
|
||||
calendar.owner?.preferredEmail ?? calendar.owner?.emails?.[0] ?? ''
|
||||
const ownerName = makeDisplayName(calendar) ?? ownerEmail
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined);
|
||||
const [accessRight, setAccessRight] = useState<AccessRight>(2);
|
||||
const [inviteLoading, setInvitesLoading] = useState(false);
|
||||
const [resourceAdmins, setResourceAdmins] = useState<UserWithAccess[]>([]);
|
||||
const [adminLoading, setAdminLoading] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [searchWidth, setSearchWidth] = useState<number | undefined>(undefined)
|
||||
const [accessRight, setAccessRight] = useState<AccessRight>(2)
|
||||
const [inviteLoading, setInvitesLoading] = useState(false)
|
||||
const [resourceAdmins, setResourceAdmins] = useState<UserWithAccess[]>([])
|
||||
const [adminLoading, setAdminLoading] = useState(false)
|
||||
|
||||
const currentUsersRef = useRef<UserWithAccess[]>(usersWithAccess);
|
||||
const currentUsersRef = useRef<UserWithAccess[]>(usersWithAccess)
|
||||
useEffect(() => {
|
||||
currentUsersRef.current = usersWithAccess;
|
||||
}, [usersWithAccess]);
|
||||
currentUsersRef.current = usersWithAccess
|
||||
}, [usersWithAccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
if (!containerRef.current) return
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
setSearchWidth(entry.contentRect.width);
|
||||
setSearchWidth(entry.contentRect.width)
|
||||
}
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
})
|
||||
observer.observe(containerRef.current)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
const handleLoadUsers = useCallback(
|
||||
async (usersInCal: UserInCalendar[], cancelled: boolean) => {
|
||||
const results = await Promise.allSettled(
|
||||
usersInCal.map(async (user) => {
|
||||
const details = await getUserDetails(user.id);
|
||||
const email = details?.preferredEmail ?? details?.emails?.[0] ?? "";
|
||||
usersInCal.map(async user => {
|
||||
const details = await getUserDetails(user.id)
|
||||
const email = details?.preferredEmail ?? details?.emails?.[0] ?? ''
|
||||
return {
|
||||
openpaasId: user.id,
|
||||
displayName:
|
||||
[details?.firstname, details?.lastname]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.join(' ')
|
||||
.trim() || email,
|
||||
email,
|
||||
accessRight: user.access as AccessRight,
|
||||
} satisfies UserWithAccess;
|
||||
accessRight: user.access
|
||||
} satisfies UserWithAccess
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
if (cancelled) {
|
||||
return [] as UserWithAccess[];
|
||||
return [] as UserWithAccess[]
|
||||
}
|
||||
|
||||
const loaded: UserWithAccess[] = results
|
||||
.filter((r) => r.status === "fulfilled")
|
||||
.map((r) => (r as PromiseFulfilledResult<UserWithAccess>).value)
|
||||
.filter(Boolean) as UserWithAccess[];
|
||||
.filter(r => r.status === 'fulfilled')
|
||||
.map(r => (r as PromiseFulfilledResult<UserWithAccess>).value)
|
||||
.filter(Boolean)
|
||||
|
||||
return loaded;
|
||||
return loaded
|
||||
},
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!calendar.invite?.length) return;
|
||||
if (!calendar.invite?.length) return
|
||||
|
||||
let cancelled = false;
|
||||
let cancelled = false
|
||||
|
||||
async function loadInvitedUsers() {
|
||||
setInvitesLoading(true);
|
||||
setInvitesLoading(true)
|
||||
try {
|
||||
const usersInCal = (calendar.invite
|
||||
?.map((invite) => {
|
||||
const principalId = invite.principal.split("/").pop();
|
||||
if (!principalId) return null;
|
||||
?.map(invite => {
|
||||
const principalId = invite.principal.split('/').pop()
|
||||
if (!principalId) return null
|
||||
|
||||
return {
|
||||
id: principalId,
|
||||
access: invite.access,
|
||||
};
|
||||
access: invite.access
|
||||
}
|
||||
})
|
||||
?.filter(
|
||||
(invite) =>
|
||||
invite =>
|
||||
!!invite &&
|
||||
!calendar.owner.administrators?.some(
|
||||
(admin) => admin.id === invite.id
|
||||
admin => admin.id === invite.id
|
||||
)
|
||||
) || []) as UserInCalendar[];
|
||||
) || []) as UserInCalendar[]
|
||||
|
||||
const loaded = await handleLoadUsers(usersInCal, cancelled);
|
||||
const loaded = await handleLoadUsers(usersInCal, cancelled)
|
||||
|
||||
const loadedIds = new Set(loaded.map((u) => normalizeEmail(u.email)));
|
||||
const loadedIds = new Set(loaded.map(u => normalizeEmail(u.email)))
|
||||
const manuallyAdded = currentUsersRef.current.filter(
|
||||
(u) => !loadedIds.has(normalizeEmail(u.email))
|
||||
);
|
||||
const merged = [...loaded, ...manuallyAdded];
|
||||
u => !loadedIds.has(normalizeEmail(u.email))
|
||||
)
|
||||
const merged = [...loaded, ...manuallyAdded]
|
||||
|
||||
onInvitesLoaded(loaded);
|
||||
onChange(merged);
|
||||
onInvitesLoaded(loaded)
|
||||
onChange(merged)
|
||||
} finally {
|
||||
if (!cancelled) setInvitesLoading(false);
|
||||
if (!cancelled) setInvitesLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadInvitedUsers();
|
||||
loadInvitedUsers()
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
cancelled = true
|
||||
}
|
||||
}, [
|
||||
calendar.invite,
|
||||
calendar.owner.administrators,
|
||||
handleLoadUsers,
|
||||
onChange,
|
||||
onInvitesLoaded,
|
||||
]);
|
||||
onInvitesLoaded
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const isResource = calendar.owner.resource;
|
||||
const resourceAdmins = calendar.owner.administrators || [];
|
||||
if (!isResource || !resourceAdmins?.length) return;
|
||||
const isResource = calendar.owner.resource
|
||||
const resourceAdmins = calendar.owner.administrators || []
|
||||
if (!isResource || !resourceAdmins?.length) return
|
||||
|
||||
let cancelled = false;
|
||||
let cancelled = false
|
||||
|
||||
async function loadAdmins() {
|
||||
try {
|
||||
setAdminLoading(true);
|
||||
setAdminLoading(true)
|
||||
const resourceAdminsWithoutOwner = resourceAdmins
|
||||
.filter((admin) => admin.id !== calendar.owner._id)
|
||||
.map((admin) => ({
|
||||
.filter(admin => admin.id !== calendar.owner._id)
|
||||
.map(admin => ({
|
||||
id: admin.id,
|
||||
access: 5, // ADMIN
|
||||
})) satisfies UserInCalendar[];
|
||||
access: 5 // ADMIN
|
||||
})) satisfies UserInCalendar[]
|
||||
const admins = await handleLoadUsers(
|
||||
resourceAdminsWithoutOwner,
|
||||
cancelled
|
||||
);
|
||||
)
|
||||
|
||||
setResourceAdmins(admins);
|
||||
setResourceAdmins(admins)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
} finally {
|
||||
if (!cancelled) setAdminLoading(false);
|
||||
if (!cancelled) setAdminLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
loadAdmins()
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [calendar.owner, handleLoadUsers, setResourceAdmins]);
|
||||
cancelled = true
|
||||
}
|
||||
}, [calendar.owner, handleLoadUsers, setResourceAdmins])
|
||||
|
||||
const handleUserSelect = (_event: unknown, users: User[]) => {
|
||||
const updated: UserWithAccess[] = users.map((user) => {
|
||||
const updated: UserWithAccess[] = users.map(user => {
|
||||
const existing = usersWithAccess.find(
|
||||
(u) => normalizeEmail(u.email) === normalizeEmail(user.email)
|
||||
);
|
||||
return existing ?? { ...user, accessRight };
|
||||
});
|
||||
onChange(updated);
|
||||
};
|
||||
u => normalizeEmail(u.email) === normalizeEmail(user.email)
|
||||
)
|
||||
return existing ?? { ...user, accessRight }
|
||||
})
|
||||
onChange(updated)
|
||||
}
|
||||
|
||||
const handleRemoveUser = (email: string) => {
|
||||
onChange(
|
||||
usersWithAccess.filter(
|
||||
(u) => normalizeEmail(u.email) !== normalizeEmail(email)
|
||||
u => normalizeEmail(u.email) !== normalizeEmail(email)
|
||||
)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const handleChangeUserRight = (email: string, right: AccessRight) => {
|
||||
onChange(
|
||||
usersWithAccess.map((u) =>
|
||||
usersWithAccess.map(u =>
|
||||
normalizeEmail(u.email) === normalizeEmail(email)
|
||||
? { ...u, accessRight: right }
|
||||
: u
|
||||
)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const accessRightOptions: { value: AccessRight; label: string }[] = [
|
||||
{ value: 2, label: t("calendarPopover.access.viewAllEvents") },
|
||||
{ value: 3, label: t("calendarPopover.access.editor") },
|
||||
{ value: 5, label: t("calendarPopover.access.administrator") },
|
||||
];
|
||||
{ value: 2, label: t('calendarPopover.access.viewAllEvents') },
|
||||
{ value: 3, label: t('calendarPopover.access.editor') },
|
||||
{ value: 5, label: t('calendarPopover.access.administrator') }
|
||||
]
|
||||
|
||||
return (
|
||||
<FieldWithLabel
|
||||
label={
|
||||
isPersonalCalendar || isDelegatedWithAdministration
|
||||
? t("calendarPopover.access.grantAccessRights")
|
||||
: t("calendarPopover.access.accessRights")
|
||||
? t('calendarPopover.access.grantAccessRights')
|
||||
: t('calendarPopover.access.accessRights')
|
||||
}
|
||||
isExpanded={false}
|
||||
>
|
||||
@@ -251,27 +251,27 @@ export function CalendarAccessRights({
|
||||
<PeopleSearch
|
||||
selectedUsers={usersWithAccess}
|
||||
onChange={handleUserSelect}
|
||||
objectTypes={["user"]}
|
||||
objectTypes={['user']}
|
||||
onToggleEventPreview={() => {}}
|
||||
customSlotProps={{
|
||||
popper: {
|
||||
anchorEl: containerRef.current,
|
||||
placement: "bottom-start",
|
||||
placement: 'bottom-start',
|
||||
sx: {
|
||||
minWidth: searchWidth,
|
||||
"& .MuiPaper-root": {
|
||||
width: "100%",
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
width: '100%'
|
||||
}
|
||||
},
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
offset: [0, 8]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
customRenderInput={(
|
||||
params: AutocompleteRenderInputParams,
|
||||
@@ -282,33 +282,33 @@ export function CalendarAccessRights({
|
||||
{...params}
|
||||
fullWidth
|
||||
autoFocus
|
||||
placeholder={t("peopleSearch.label")}
|
||||
placeholder={t('peopleSearch.label')}
|
||||
value={query}
|
||||
inputRef={(el) => {
|
||||
const ref = params.InputProps.ref;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref && "current" in ref) {
|
||||
(
|
||||
inputRef={el => {
|
||||
const ref = params.InputProps.ref
|
||||
if (typeof ref === 'function') {
|
||||
ref(el)
|
||||
} else if (ref && 'current' in ref) {
|
||||
;(
|
||||
ref as React.MutableRefObject<HTMLInputElement | null>
|
||||
).current = el;
|
||||
).current = el
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
variant="outlined"
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
sx: {
|
||||
fontSize: "14px",
|
||||
"&::placeholder": { fontSize: "14px" },
|
||||
},
|
||||
fontSize: '14px',
|
||||
'&::placeholder': { fontSize: '14px' }
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<PeopleOutlineOutlinedIcon
|
||||
sx={{ color: "text.secondary" }}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
@@ -316,34 +316,34 @@ export function CalendarAccessRights({
|
||||
<InputAdornment position="end">
|
||||
<Select
|
||||
value={accessRight}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
setAccessRight(e.target.value as AccessRight)
|
||||
}
|
||||
variant="standard"
|
||||
disableUnderline
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
color: "text.secondary",
|
||||
"& .MuiSelect-select": {
|
||||
paddingRight: "24px !important",
|
||||
paddingY: 0,
|
||||
fontSize: '0.875rem',
|
||||
color: 'text.secondary',
|
||||
'& .MuiSelect-select': {
|
||||
paddingRight: '24px !important',
|
||||
paddingY: 0
|
||||
},
|
||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||
"&:before, &:after": { display: "none" },
|
||||
'& .MuiSelect-icon': { fontSize: '1rem' },
|
||||
'&:before, &:after': { display: 'none' }
|
||||
}}
|
||||
>
|
||||
{accessRightOptions.map((opt) => (
|
||||
{accessRightOptions.map(opt => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
sx={{ color: "text.secondary" }}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</InputAdornment>
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -360,14 +360,14 @@ export function CalendarAccessRights({
|
||||
px={1}
|
||||
py={0.5}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
borderRadius: '8px',
|
||||
'&:hover': { backgroundColor: 'action.hover' }
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
|
||||
<Avatar
|
||||
{...stringAvatar(ownerName)}
|
||||
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||
sx={{ width: 28, height: 28, fontSize: '0.875rem' }}
|
||||
/>
|
||||
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
|
||||
<Typography noWrap>{ownerName}</Typography>
|
||||
@@ -379,7 +379,7 @@ export function CalendarAccessRights({
|
||||
|
||||
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
|
||||
<Typography variant="caption">
|
||||
{t("calendarPopover.access.owner")}
|
||||
{t('calendarPopover.access.owner')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -388,7 +388,7 @@ export function CalendarAccessRights({
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : (
|
||||
resourceAdmins.map((admin) => (
|
||||
resourceAdmins.map(admin => (
|
||||
<ResourceAdmin key={admin.email} admin={admin} />
|
||||
))
|
||||
)}
|
||||
@@ -398,7 +398,7 @@ export function CalendarAccessRights({
|
||||
</Box>
|
||||
) : (
|
||||
usersWithAccess.length > 0 &&
|
||||
usersWithAccess.map((user) => (
|
||||
usersWithAccess.map(user => (
|
||||
<Box
|
||||
key={user.email}
|
||||
display="flex"
|
||||
@@ -407,14 +407,14 @@ export function CalendarAccessRights({
|
||||
px={1}
|
||||
py={0.5}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
borderRadius: '8px',
|
||||
'&:hover': { backgroundColor: 'action.hover' }
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
|
||||
<Avatar
|
||||
{...stringAvatar(user.displayName)}
|
||||
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||
sx={{ width: 28, height: 28, fontSize: '0.875rem' }}
|
||||
/>
|
||||
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
|
||||
<Typography noWrap>{user.displayName}</Typography>
|
||||
@@ -427,7 +427,7 @@ export function CalendarAccessRights({
|
||||
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
|
||||
<Select
|
||||
value={user.accessRight}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleChangeUserRight(
|
||||
user.email,
|
||||
e.target.value as AccessRight
|
||||
@@ -439,20 +439,20 @@ export function CalendarAccessRights({
|
||||
!(isPersonalCalendar || isDelegatedWithAdministration)
|
||||
}
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
color: "text.secondary",
|
||||
"& .MuiSelect-select": {
|
||||
paddingRight: "24px !important",
|
||||
paddingY: 0,
|
||||
fontSize: '0.875rem',
|
||||
color: 'text.secondary',
|
||||
'& .MuiSelect-select': {
|
||||
paddingRight: '24px !important',
|
||||
paddingY: 0
|
||||
},
|
||||
"& .MuiSelect-icon": { fontSize: "1rem" },
|
||||
'& .MuiSelect-icon': { fontSize: '1rem' }
|
||||
}}
|
||||
>
|
||||
{accessRightOptions.map((opt) => (
|
||||
{accessRightOptions.map(opt => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
sx={{ color: "text.secondary" }}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
@@ -461,9 +461,9 @@ export function CalendarAccessRights({
|
||||
{(isPersonalCalendar || isDelegatedWithAdministration) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={t("actions.remove")}
|
||||
aria-label={t('actions.remove')}
|
||||
onClick={() => handleRemoveUser(user.email)}
|
||||
sx={{ color: "text.secondary" }}
|
||||
sx={{ color: 'text.secondary' }}
|
||||
>
|
||||
<HighlightOffIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -474,5 +474,5 @@ export function CalendarAccessRights({
|
||||
)}
|
||||
</Box>
|
||||
</FieldWithLabel>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import CheckIcon from '@mui/icons-material/Check'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Popover,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import { useState, useEffect } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import { useState } from 'react'
|
||||
import { HexColorPicker } from 'react-colorful'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { getAccessiblePair } from '@/utils/getAccessiblePair'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
|
||||
export function ColorPicker({
|
||||
selectedColor,
|
||||
colors = defaultColors.slice(0, 4),
|
||||
onChange,
|
||||
onChange
|
||||
}: {
|
||||
selectedColor: Record<string, string>;
|
||||
colors?: Record<string, string>[];
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
selectedColor: Record<string, string>
|
||||
colors?: Record<string, string>[]
|
||||
onChange: (color: Record<string, string>) => void
|
||||
}) {
|
||||
const [customColor, setCustomColor] = useState(
|
||||
!colors.find((c) => c.light === selectedColor?.light)
|
||||
? selectedColor
|
||||
: undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!colors.find((c) => c.light === selectedColor?.light)) {
|
||||
setCustomColor(selectedColor);
|
||||
} else {
|
||||
setCustomColor(undefined);
|
||||
}
|
||||
}, [selectedColor, colors]);
|
||||
const customColor = !colors.find(c => c.light === selectedColor?.light)
|
||||
? selectedColor
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{colors.map((c) => (
|
||||
{colors.map(c => (
|
||||
<ColorBox
|
||||
key={c.light}
|
||||
color={c}
|
||||
@@ -56,24 +46,23 @@ export function ColorPicker({
|
||||
)}
|
||||
|
||||
<ColorPickerBox
|
||||
onChange={(c) => {
|
||||
onChange(c);
|
||||
setCustomColor(c);
|
||||
onChange={c => {
|
||||
onChange(c)
|
||||
}}
|
||||
selectedColor={selectedColor}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ColorBox({
|
||||
color,
|
||||
onChange,
|
||||
selectedColor,
|
||||
selectedColor
|
||||
}: {
|
||||
color: Record<string, string>;
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
selectedColor: Record<string, string>;
|
||||
color: Record<string, string>
|
||||
onChange: (color: Record<string, string>) => void
|
||||
selectedColor: Record<string, string>
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
@@ -81,109 +70,109 @@ function ColorBox({
|
||||
aria-label={`select color ${color.light}`}
|
||||
onClick={() => onChange(color)}
|
||||
style={{
|
||||
width: "46px",
|
||||
height: "32px",
|
||||
width: '46px',
|
||||
height: '32px',
|
||||
padding: 0,
|
||||
borderRadius: "4px",
|
||||
borderRadius: '4px',
|
||||
backgroundColor: color.light,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "7px",
|
||||
width: "100%",
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
backgroundColor: color.dark,
|
||||
height: '7px',
|
||||
width: '100%',
|
||||
borderRadius: '4px 4px 0px 0px',
|
||||
backgroundColor: color.dark
|
||||
}}
|
||||
></Box>
|
||||
<CheckIcon
|
||||
style={{
|
||||
visibility:
|
||||
selectedColor?.light === color.light ? "visible" : "hidden",
|
||||
color: color.dark,
|
||||
selectedColor?.light === color.light ? 'visible' : 'hidden',
|
||||
color: color.dark
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function ColorPickerBox({
|
||||
onChange,
|
||||
selectedColor,
|
||||
selectedColor
|
||||
}: {
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
selectedColor: Record<string, string>;
|
||||
onChange: (color: Record<string, string>) => void
|
||||
selectedColor: Record<string, string>
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
const [oldColor] = useState(
|
||||
selectedColor ?? { light: "#ffffff", dark: "#808080" }
|
||||
);
|
||||
const [color, setColor] = useState(oldColor);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const theme = useTheme();
|
||||
selectedColor ?? { light: '#ffffff', dark: '#808080' }
|
||||
)
|
||||
const [color, setColor] = useState(oldColor)
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const open = Boolean(anchorEl)
|
||||
const theme = useTheme()
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onChange(oldColor);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
onChange(oldColor)
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onChange(color);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
onChange(color)
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleColorChange = (c: string) => {
|
||||
const newLight = c;
|
||||
const newLight = c
|
||||
const newColor = {
|
||||
light: newLight,
|
||||
dark: getAccessiblePair(newLight, theme),
|
||||
};
|
||||
setColor(newColor);
|
||||
onChange(newColor);
|
||||
};
|
||||
dark: getAccessiblePair(newLight, theme)
|
||||
}
|
||||
setColor(newColor)
|
||||
onChange(newColor)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
key={"colorPicker"}
|
||||
key="colorPicker"
|
||||
role="button"
|
||||
aria-label={t("colorPicker.selectCustom")}
|
||||
aria-label={t('colorPicker.selectCustom')}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: "46px",
|
||||
height: "32px",
|
||||
width: '46px',
|
||||
height: '32px',
|
||||
padding: 0,
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #CBD2E0",
|
||||
backgroundColor: "#FFF",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #CBD2E0',
|
||||
backgroundColor: '#FFF',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "7px",
|
||||
width: "100%",
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
backgroundColor: "#CBD2E0",
|
||||
height: '7px',
|
||||
width: '100%',
|
||||
borderRadius: '4px 4px 0px 0px',
|
||||
backgroundColor: '#CBD2E0'
|
||||
}}
|
||||
></Box>
|
||||
<AddIcon
|
||||
style={{
|
||||
color: "#CBD2E0",
|
||||
color: '#CBD2E0'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -192,63 +181,63 @@ function ColorPickerBox({
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "center",
|
||||
horizontal: "center",
|
||||
vertical: 'center',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
style: {
|
||||
padding: "24px",
|
||||
width: "294px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0px 1px 3px #3C404326",
|
||||
},
|
||||
},
|
||||
padding: '24px',
|
||||
width: '294px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0px 1px 3px #3C404326'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="600">
|
||||
{t("colorPicker.title")}
|
||||
{t('colorPicker.title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
|
||||
{t("colorPicker.subtitle")}
|
||||
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
||||
{t('colorPicker.subtitle')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<HexColorPicker
|
||||
color={color.light}
|
||||
onChange={handleColorChange}
|
||||
style={{ width: "100%" }}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
{t("colorPicker.hex")}
|
||||
{t('colorPicker.hex')}
|
||||
</Typography>
|
||||
<TextField
|
||||
value={color.light?.toUpperCase()}
|
||||
onChange={(e) => handleColorChange(e.target.value)}
|
||||
onChange={e => handleColorChange(e.target.value)}
|
||||
variant="standard"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<Button onClick={handleClose}>{t("common.cancel")}</Button>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
|
||||
<Button onClick={handleClose}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
sx={{ textTransform: "none" }}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{t("actions.save")}
|
||||
{t('actions.save')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { MenuItem } from "@linagora/twake-mui";
|
||||
import React from "react";
|
||||
import { CalendarName } from "./CalendarName";
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { MenuItem } from '@linagora/twake-mui'
|
||||
import React from 'react'
|
||||
import { CalendarName } from './CalendarName'
|
||||
|
||||
export function CalendarItemList(
|
||||
userPersonalCalendars: Calendar[]
|
||||
): React.ReactNode {
|
||||
return Object.values(userPersonalCalendars).map((calendar) => (
|
||||
return Object.values(userPersonalCalendars).map(calendar => (
|
||||
<MenuItem key={calendar.id} value={calendar.id}>
|
||||
<CalendarName calendar={calendar} />
|
||||
</MenuItem>
|
||||
));
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import SettingsPage from "@/features/Settings/SettingsPage";
|
||||
import { getViewRange } from "@/utils/dateUtils";
|
||||
import type { CalendarApi } from "@fullcalendar/core";
|
||||
import CozyBridge from "cozy-external-bridge";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { refreshCalendars } from "../Event/utils/eventUtils";
|
||||
import { Menubar, MenubarProps } from "../Menubar/Menubar";
|
||||
import CalendarApp from "./Calendar";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import SettingsPage from '@/features/Settings/SettingsPage'
|
||||
import { getViewRange } from '@/utils/dateUtils'
|
||||
import type { CalendarApi } from '@fullcalendar/core'
|
||||
import CozyBridge from 'cozy-external-bridge'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ErrorSnackbar } from '../Error/ErrorSnackbar'
|
||||
import { refreshCalendars } from '../Event/utils/eventUtils'
|
||||
import { Menubar, MenubarProps } from '../Menubar/Menubar'
|
||||
import CalendarApp from './Calendar'
|
||||
|
||||
export default function CalendarLayout() {
|
||||
const calendarRef = useRef<CalendarApi | null>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const error = useAppSelector((state) => state.calendars.error);
|
||||
const selectedCalendars = useAppSelector((state) => state.calendars.list);
|
||||
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||
const view = useAppSelector((state) => state.settings.view);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
const [currentView, setCurrentView] = useState<string>("timeGridWeek");
|
||||
const isInIframe = useMemo(() => new CozyBridge().isInIframe(), []);
|
||||
const calendarRef = useRef<CalendarApi | null>(null)
|
||||
const dispatch = useAppDispatch()
|
||||
const error = useAppSelector(state => state.calendars.error)
|
||||
const selectedCalendars = useAppSelector(state => state.calendars.list)
|
||||
const tempcalendars = useAppSelector(state => state.calendars.templist)
|
||||
const view = useAppSelector(state => state.settings.view)
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date())
|
||||
const [currentView, setCurrentView] = useState<string>('timeGridWeek')
|
||||
const isInIframe = useMemo(() => new CozyBridge().isInIframe(), [])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
// Get current calendar range
|
||||
if (calendarRef.current) {
|
||||
const view = calendarRef.current.view;
|
||||
const calendarRange = getViewRange(view.activeStart, view.type);
|
||||
const view = calendarRef.current.view
|
||||
const calendarRange = getViewRange(view.activeStart, view.type)
|
||||
|
||||
// Refresh events for selected calendars
|
||||
await refreshCalendars(
|
||||
dispatch,
|
||||
Object.values(selectedCalendars),
|
||||
calendarRange
|
||||
);
|
||||
)
|
||||
if (tempcalendars) {
|
||||
await refreshCalendars(
|
||||
dispatch,
|
||||
Object.values(tempcalendars),
|
||||
calendarRange,
|
||||
"temp"
|
||||
);
|
||||
'temp'
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDateChange = (date: Date) => {
|
||||
setCurrentDate(date);
|
||||
};
|
||||
setCurrentDate(date)
|
||||
}
|
||||
|
||||
const handleViewChange = (view: string) => {
|
||||
setCurrentView(view);
|
||||
};
|
||||
setCurrentView(view)
|
||||
}
|
||||
|
||||
// Hide topbar navigation elements when in settings view (same as fullscreen dialog mode)
|
||||
useEffect(() => {
|
||||
if (view === "settings") {
|
||||
document.body.classList.add("fullscreen-view");
|
||||
if (view === 'settings') {
|
||||
document.body.classList.add('fullscreen-view')
|
||||
} else {
|
||||
document.body.classList.remove("fullscreen-view");
|
||||
document.body.classList.remove('fullscreen-view')
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
document.body.classList.remove("fullscreen-view");
|
||||
};
|
||||
}, [view]);
|
||||
document.body.classList.remove('fullscreen-view')
|
||||
}
|
||||
}, [view])
|
||||
|
||||
const menubarProps: MenubarProps = {
|
||||
calendarRef,
|
||||
@@ -72,13 +72,13 @@ export default function CalendarLayout() {
|
||||
onDateChange: handleDateChange,
|
||||
currentView,
|
||||
onViewChange: handleViewChange,
|
||||
isIframe: isInIframe,
|
||||
};
|
||||
isIframe: isInIframe
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
{!isInIframe && <Menubar {...menubarProps} />}
|
||||
{(view === "calendar" || view === "search") && (
|
||||
{(view === 'calendar' || view === 'search') && (
|
||||
<CalendarApp
|
||||
calendarRef={calendarRef}
|
||||
onDateChange={handleDateChange}
|
||||
@@ -86,8 +86,8 @@ export default function CalendarLayout() {
|
||||
menubarProps={menubarProps}
|
||||
/>
|
||||
)}
|
||||
{view === "settings" && <SettingsPage isInIframe={isInIframe} />}
|
||||
{view === 'settings' && <SettingsPage isInIframe={isInIframe} />}
|
||||
<ErrorSnackbar error={error} type="calendar" />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,190 +1,193 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
createCalendarAsync,
|
||||
importEventFromFileAsync,
|
||||
patchACLCalendarAsync,
|
||||
patchCalendarAsync,
|
||||
} from "@/features/Calendars/services";
|
||||
import { updateDelegationCalendarAsync } from "@/features/Calendars/services/updateDelegationCalendarAsync";
|
||||
import { accessRightToDavProp } from "@/utils/accessRightToDavProp";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { Button, Tab, Tabs } from "@linagora/twake-mui";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ResponsiveDialog } from "../Dialog";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
import { AccessTab } from "./AccessTab";
|
||||
import { UserWithAccess } from "./CalendarAccessRights";
|
||||
import { ImportTab } from "./ImportTab";
|
||||
import { SettingsTab } from "./SettingsTab";
|
||||
patchCalendarAsync
|
||||
} from '@/features/Calendars/services'
|
||||
import { updateDelegationCalendarAsync } from '@/features/Calendars/services/updateDelegationCalendarAsync'
|
||||
import { accessRightToDavProp } from '@/utils/accessRightToDavProp'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import { Button, Tab, Tabs } from '@linagora/twake-mui'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ResponsiveDialog } from '../Dialog'
|
||||
import { ErrorSnackbar } from '../Error/ErrorSnackbar'
|
||||
import { AccessTab } from './AccessTab'
|
||||
import { UserWithAccess } from './CalendarAccessRights'
|
||||
import { ImportTab } from './ImportTab'
|
||||
import { SettingsTab } from './SettingsTab'
|
||||
|
||||
function CalendarPopover({
|
||||
open,
|
||||
onClose,
|
||||
calendar,
|
||||
calendar
|
||||
}: {
|
||||
open: boolean;
|
||||
open: boolean
|
||||
onClose: (
|
||||
event: object | null,
|
||||
reason: "backdropClick" | "escapeKeyDown"
|
||||
) => void;
|
||||
calendar?: Calendar;
|
||||
reason: 'backdropClick' | 'escapeKeyDown'
|
||||
) => void
|
||||
calendar?: Calendar
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const userData = useAppSelector((state) => state.user.userData) ?? {};
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const { t } = useI18n()
|
||||
const dispatch = useAppDispatch()
|
||||
const userData = useAppSelector(state => state.user.userData) ?? {}
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
const isOwn = calendar?.id
|
||||
? extractEventBaseUuid(calendar.id) === userData.openpaasId
|
||||
: true;
|
||||
: true
|
||||
const canManageInvites =
|
||||
isOwn ||
|
||||
!!calendar?.invite?.some((invite) => {
|
||||
!!calendar?.invite?.some(invite => {
|
||||
const inviteEmail = invite.href
|
||||
.replace(/^mailto:/i, "")
|
||||
.replace(/^mailto:/i, '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const currentEmail = userData.email?.trim().toLowerCase();
|
||||
return inviteEmail === currentEmail && invite.access === 5;
|
||||
});
|
||||
.toLowerCase()
|
||||
const currentEmail = userData.email?.trim().toLowerCase()
|
||||
return inviteEmail === currentEmail && invite.access === 5
|
||||
})
|
||||
|
||||
// existing calendar params
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState<Record<string, string>>(defaultColors[0]);
|
||||
const [visibility, setVisibility] = useState<"private" | "public">("public");
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [color, setColor] = useState<Record<string, string>>(defaultColors[0])
|
||||
const [visibility, setVisibility] = useState<'private' | 'public'>('public')
|
||||
|
||||
// access tab state
|
||||
const [usersWithAccess, setUsersWithAccess] = useState<UserWithAccess[]>([]);
|
||||
const [usersWithAccess, setUsersWithAccess] = useState<UserWithAccess[]>([])
|
||||
|
||||
// Snapshot of the invitee list as loaded from calendar.invite on open.
|
||||
// Used to diff on save: what changed vs what was removed.
|
||||
const initialUsersRef = useRef<UserWithAccess[]>([]);
|
||||
const initialUsersRef = useRef<UserWithAccess[]>([])
|
||||
|
||||
// import tab state
|
||||
const [tab, setTab] = useState<"settings" | "access" | "import">("settings");
|
||||
const [importedContent, setImportedContent] = useState<File | null>(null);
|
||||
const [importTarget, setImportTarget] = useState("new");
|
||||
const [tab, setTab] = useState<'settings' | 'access' | 'import'>('settings')
|
||||
const [importedContent, setImportedContent] = useState<File | null>(null)
|
||||
const [importTarget, setImportTarget] = useState('new')
|
||||
|
||||
// new calendar params (for import new)
|
||||
const [newCalName, setNewCalName] = useState("");
|
||||
const [newCalDescription, setNewCalDescription] = useState("");
|
||||
const [newCalColor, setNewCalColor] = useState(defaultColors[0]);
|
||||
const [newCalName, setNewCalName] = useState('')
|
||||
const [newCalDescription, setNewCalDescription] = useState('')
|
||||
const [newCalColor, setNewCalColor] = useState(defaultColors[0])
|
||||
const [newCalVisibility, setNewCalVisibility] = useState<
|
||||
"public" | "private"
|
||||
>("public");
|
||||
'public' | 'private'
|
||||
>('public')
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (calendar) {
|
||||
setName(calendar.name);
|
||||
setDescription(calendar.description ?? "");
|
||||
setColor(calendar.color ?? defaultColors[0]);
|
||||
setVisibility(calendar.visibility ?? "public");
|
||||
setImportTarget(calendar.id ?? "new");
|
||||
} else {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor(defaultColors[0]);
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
if (!open) return
|
||||
const fillCalendarData = () => {
|
||||
if (calendar) {
|
||||
setName(calendar.name)
|
||||
setDescription(calendar.description ?? '')
|
||||
setColor(calendar.color ?? defaultColors[0])
|
||||
setVisibility(calendar.visibility ?? 'public')
|
||||
setImportTarget(calendar.id ?? 'new')
|
||||
} else {
|
||||
setName('')
|
||||
setDescription('')
|
||||
setColor(defaultColors[0])
|
||||
setVisibility('public')
|
||||
setImportTarget('new')
|
||||
}
|
||||
setUsersWithAccess([])
|
||||
initialUsersRef.current = []
|
||||
}
|
||||
setUsersWithAccess([]);
|
||||
initialUsersRef.current = [];
|
||||
}, [calendar, open]);
|
||||
fillCalendarData()
|
||||
}, [calendar, open])
|
||||
|
||||
const handleUsersWithAccessChange = useCallback((users: UserWithAccess[]) => {
|
||||
setUsersWithAccess(users);
|
||||
}, []);
|
||||
setUsersWithAccess(users)
|
||||
}, [])
|
||||
|
||||
const handleInvitesLoaded = useCallback((users: UserWithAccess[]) => {
|
||||
if (initialUsersRef.current.length === 0) {
|
||||
initialUsersRef.current = users;
|
||||
initialUsersRef.current = users
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const [saveError, setSaveError] = useState("");
|
||||
const [saveError, setSaveError] = useState('')
|
||||
|
||||
const updateCalendar = async (calId: string, calLink: string) => {
|
||||
const nameChanged = name.trim() !== calendar?.name;
|
||||
const descChanged = description.trim() !== (calendar?.description ?? "");
|
||||
const nameChanged = name.trim() !== calendar?.name
|
||||
const descChanged = description.trim() !== (calendar?.description ?? '')
|
||||
const colorChanged =
|
||||
JSON.stringify(color) !==
|
||||
JSON.stringify(calendar?.color ?? defaultColors[0]);
|
||||
JSON.stringify(calendar?.color ?? defaultColors[0])
|
||||
|
||||
if (nameChanged || descChanged || colorChanged) {
|
||||
await dispatch(
|
||||
patchCalendarAsync({
|
||||
calId,
|
||||
calLink,
|
||||
patch: { name: name.trim(), desc: description.trim(), color },
|
||||
patch: { name: name.trim(), desc: description.trim(), color }
|
||||
})
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
}
|
||||
if (canManageInvites && visibility !== calendar?.visibility) {
|
||||
await dispatch(
|
||||
patchACLCalendarAsync({
|
||||
calId,
|
||||
calLink,
|
||||
request: visibility === "public" ? "{DAV:}read" : "",
|
||||
request: visibility === 'public' ? '{DAV:}read' : ''
|
||||
})
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function updateCalendarInvites(
|
||||
calLink: string,
|
||||
initialUsers: UserWithAccess[]
|
||||
) {
|
||||
const normaliseEmail = (u: UserWithAccess) =>
|
||||
u.email?.trim().toLowerCase() ?? "";
|
||||
u.email?.trim().toLowerCase() ?? ''
|
||||
|
||||
const initialMap = new Map(
|
||||
initialUsers
|
||||
.filter((u) => !!normaliseEmail(u))
|
||||
.map((u) => [normaliseEmail(u), u])
|
||||
);
|
||||
.filter(u => !!normaliseEmail(u))
|
||||
.map(u => [normaliseEmail(u), u])
|
||||
)
|
||||
|
||||
const currentMap = new Map(
|
||||
usersWithAccess
|
||||
.filter((u) => !!normaliseEmail(u))
|
||||
.map((u) => [normaliseEmail(u), u])
|
||||
);
|
||||
.filter(u => !!normaliseEmail(u))
|
||||
.map(u => [normaliseEmail(u), u])
|
||||
)
|
||||
|
||||
const hasChanges =
|
||||
usersWithAccess.some((u) => {
|
||||
const email = normaliseEmail(u);
|
||||
if (!email) return false;
|
||||
const initial = initialMap.get(email);
|
||||
return !initial || initial.accessRight !== u.accessRight;
|
||||
usersWithAccess.some(u => {
|
||||
const email = normaliseEmail(u)
|
||||
if (!email) return false
|
||||
const initial = initialMap.get(email)
|
||||
return !initial || initial.accessRight !== u.accessRight
|
||||
}) ||
|
||||
initialUsers.some(
|
||||
(u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u))
|
||||
);
|
||||
u => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u))
|
||||
)
|
||||
|
||||
if (!hasChanges || !canManageInvites) return;
|
||||
if (!hasChanges || !canManageInvites) return
|
||||
|
||||
// Send all remaining users in `set`: the server treats it as the full list
|
||||
const set = usersWithAccess
|
||||
.filter((u) => !!normaliseEmail(u))
|
||||
.map((u) => ({
|
||||
"dav:href": `mailto:${normaliseEmail(u)}`,
|
||||
[accessRightToDavProp(u.accessRight)]: true,
|
||||
}));
|
||||
.filter(u => !!normaliseEmail(u))
|
||||
.map(u => ({
|
||||
'dav:href': `mailto:${normaliseEmail(u)}`,
|
||||
[accessRightToDavProp(u.accessRight)]: true
|
||||
}))
|
||||
|
||||
const remove = initialUsers
|
||||
.filter((u) => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u)))
|
||||
.map((u) => ({ "dav:href": `mailto:${normaliseEmail(u)}` }));
|
||||
.filter(u => !!normaliseEmail(u) && !currentMap.has(normaliseEmail(u)))
|
||||
.map(u => ({ 'dav:href': `mailto:${normaliseEmail(u)}` }))
|
||||
|
||||
await dispatch(
|
||||
updateDelegationCalendarAsync({
|
||||
calId: calendar?.id,
|
||||
calId: calendar?.id ?? '',
|
||||
calLink,
|
||||
share: { set, remove },
|
||||
share: { set, remove }
|
||||
})
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
}
|
||||
|
||||
const createCalendar = async (
|
||||
@@ -200,39 +203,39 @@ function CalendarPopover({
|
||||
desc: desc.trim(),
|
||||
color: color,
|
||||
userData,
|
||||
calId,
|
||||
calId
|
||||
})
|
||||
);
|
||||
)
|
||||
dispatch(
|
||||
patchACLCalendarAsync({
|
||||
calId: `${userData.openpaasId}/${calId}`,
|
||||
calLink: `/calendars/${userData.openpaasId}/${calId}.json`,
|
||||
request: visibility === "public" ? "{DAV:}read" : "",
|
||||
request: visibility === 'public' ? '{DAV:}read' : ''
|
||||
})
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) return;
|
||||
if (!name.trim()) return
|
||||
if (calendar) {
|
||||
// Snapshot before any await: handleClose resets the ref immediately after
|
||||
// calling handleSave(), so we must read it synchronously here.
|
||||
const initialUsersSnapshot = [...initialUsersRef.current];
|
||||
const initialUsersSnapshot = [...initialUsersRef.current]
|
||||
try {
|
||||
await updateCalendar(calendar.id, calendar.link);
|
||||
await updateCalendarInvites(calendar.link, initialUsersSnapshot);
|
||||
await updateCalendar(calendar.id, calendar.link)
|
||||
await updateCalendarInvites(calendar.link, initialUsersSnapshot)
|
||||
} catch {
|
||||
setSaveError(t("error.title"));
|
||||
return;
|
||||
setSaveError(t('error.title'))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
createCalendar(crypto.randomUUID(), name, description, color, visibility);
|
||||
createCalendar(crypto.randomUUID(), name, description, color, visibility)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (importTarget === "new") {
|
||||
const calId = crypto.randomUUID();
|
||||
if (importTarget === 'new') {
|
||||
const calId = crypto.randomUUID()
|
||||
if (newCalName.trim()) {
|
||||
await createCalendar(
|
||||
calId,
|
||||
@@ -240,14 +243,14 @@ function CalendarPopover({
|
||||
newCalDescription,
|
||||
newCalColor,
|
||||
newCalVisibility
|
||||
);
|
||||
)
|
||||
if (importedContent) {
|
||||
dispatch(
|
||||
importEventFromFileAsync({
|
||||
calLink: `/calendars/${userData.openpaasId}/${calId}.json`,
|
||||
file: importedContent,
|
||||
file: importedContent
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -255,91 +258,91 @@ function CalendarPopover({
|
||||
dispatch(
|
||||
importEventFromFileAsync({
|
||||
calLink: calendars[importTarget].link,
|
||||
file: importedContent,
|
||||
file: importedContent
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
handleClose({}, "backdropClick");
|
||||
};
|
||||
handleClose({}, 'backdropClick')
|
||||
}
|
||||
|
||||
const handleClose = (
|
||||
e: object | null,
|
||||
reason: "backdropClick" | "escapeKeyDown" | "cancel"
|
||||
reason: 'backdropClick' | 'escapeKeyDown' | 'cancel'
|
||||
): void => {
|
||||
if (reason !== "cancel") {
|
||||
handleSave();
|
||||
onClose(e, reason);
|
||||
if (reason !== 'cancel') {
|
||||
handleSave()
|
||||
onClose(e, reason)
|
||||
} else {
|
||||
onClose(e, "backdropClick");
|
||||
onClose(e, 'backdropClick')
|
||||
}
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor(defaultColors[0]);
|
||||
setTab("settings");
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
setImportedContent(null);
|
||||
setUsersWithAccess([]);
|
||||
initialUsersRef.current = [];
|
||||
setSaveError("");
|
||||
setNewCalName("");
|
||||
setNewCalDescription("");
|
||||
setNewCalColor(defaultColors[0]);
|
||||
setNewCalVisibility("public");
|
||||
};
|
||||
setName('')
|
||||
setDescription('')
|
||||
setColor(defaultColors[0])
|
||||
setTab('settings')
|
||||
setVisibility('public')
|
||||
setImportTarget('new')
|
||||
setImportedContent(null)
|
||||
setUsersWithAccess([])
|
||||
initialUsersRef.current = []
|
||||
setSaveError('')
|
||||
setNewCalName('')
|
||||
setNewCalDescription('')
|
||||
setNewCalColor(defaultColors[0])
|
||||
setNewCalVisibility('public')
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => handleClose({}, "backdropClick")}
|
||||
onClose={() => handleClose({}, 'backdropClick')}
|
||||
title={
|
||||
<Tabs value={tab} onChange={(_e, v) => setTab(v)}>
|
||||
<Tab
|
||||
value="settings"
|
||||
label={
|
||||
calendar
|
||||
? t("calendarPopover.tabs.settings")
|
||||
: t("calendarPopover.tabs.addNew")
|
||||
? t('calendarPopover.tabs.settings')
|
||||
: t('calendarPopover.tabs.addNew')
|
||||
}
|
||||
/>
|
||||
{calendar && (
|
||||
<Tab value="access" label={t("calendarPopover.tabs.access")} />
|
||||
<Tab value="access" label={t('calendarPopover.tabs.access')} />
|
||||
)}
|
||||
{isOwn && (
|
||||
<Tab value="import" label={t("calendarPopover.tabs.import")} />
|
||||
<Tab value="import" label={t('calendarPopover.tabs.import')} />
|
||||
)}
|
||||
</Tabs>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => handleClose({}, "cancel")}>
|
||||
{t("common.cancel")}
|
||||
<Button variant="outlined" onClick={() => handleClose({}, 'cancel')}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={tab === "import" ? !importedContent : !name.trim()}
|
||||
disabled={tab === 'import' ? !importedContent : !name.trim()}
|
||||
variant="contained"
|
||||
onClick={
|
||||
tab === "import"
|
||||
tab === 'import'
|
||||
? handleImport
|
||||
: () => handleClose({}, "backdropClick")
|
||||
: () => handleClose({}, 'backdropClick')
|
||||
}
|
||||
>
|
||||
{tab === "import"
|
||||
? t("actions.import")
|
||||
{tab === 'import'
|
||||
? t('actions.import')
|
||||
: calendar
|
||||
? t("actions.save")
|
||||
: t("actions.create")}
|
||||
? t('actions.save')
|
||||
: t('actions.create')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{tab === "import" && (
|
||||
{tab === 'import' && (
|
||||
<ImportTab
|
||||
importTarget={importTarget}
|
||||
setImportTarget={setImportTarget}
|
||||
setImportedContent={setImportedContent}
|
||||
userId={userData.openpaasId ?? ""}
|
||||
userId={userData.openpaasId ?? ''}
|
||||
newCalParams={{
|
||||
name: newCalName,
|
||||
setName: setNewCalName,
|
||||
@@ -348,11 +351,11 @@ function CalendarPopover({
|
||||
color: newCalColor,
|
||||
setColor: setNewCalColor,
|
||||
visibility: newCalVisibility,
|
||||
setVisibility: setNewCalVisibility,
|
||||
setVisibility: setNewCalVisibility
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tab === "settings" && (
|
||||
{tab === 'settings' && (
|
||||
<SettingsTab
|
||||
name={name}
|
||||
setName={setName}
|
||||
@@ -365,7 +368,7 @@ function CalendarPopover({
|
||||
calendar={calendar}
|
||||
/>
|
||||
)}
|
||||
{tab === "access" && calendar && (
|
||||
{tab === 'access' && calendar && (
|
||||
<AccessTab
|
||||
calendar={calendar}
|
||||
usersWithAccess={usersWithAccess}
|
||||
@@ -375,7 +378,7 @@ function CalendarPopover({
|
||||
)}
|
||||
<ErrorSnackbar error={saveError} type="calendar" />
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default CalendarPopover;
|
||||
export default CalendarPopover
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { useAppSelector } from "@/app/hooks";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||
import { renameDefault } from "@/utils/renameDefault";
|
||||
import { Box, Typography } from "@linagora/twake-mui";
|
||||
import SquareRoundedIcon from "@mui/icons-material/SquareRounded";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { OwnerCaption } from "./OwnerCaption";
|
||||
import { ResourceIcon } from "../Attendees/ResourceIcon";
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
import { makeDisplayName } from '@/utils/makeDisplayName'
|
||||
import { renameDefault } from '@/utils/renameDefault'
|
||||
import { Box, Typography } from '@linagora/twake-mui'
|
||||
import SquareRoundedIcon from '@mui/icons-material/SquareRounded'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { OwnerCaption } from './OwnerCaption'
|
||||
import { ResourceIcon } from '../Attendees/ResourceIcon'
|
||||
|
||||
export function CalendarName({ calendar }: { calendar: Calendar }) {
|
||||
const userData = useAppSelector((state) => state.user.userData);
|
||||
const { t } = useI18n();
|
||||
const userData = useAppSelector(state => state.user.userData)
|
||||
const { t } = useI18n()
|
||||
|
||||
const ownerId = calendar.id.split("/")[0];
|
||||
const ownerDisplayName = makeDisplayName(calendar) ?? "";
|
||||
const isOwnCalendar = userData.openpaasId === ownerId;
|
||||
const isResource = calendar.owner?.resource;
|
||||
const ownerId = calendar.id.split('/')[0]
|
||||
const ownerDisplayName = makeDisplayName(calendar) ?? ''
|
||||
const isOwnCalendar = userData.openpaasId === ownerId
|
||||
const isResource = calendar.owner?.resource
|
||||
const showCaption =
|
||||
calendar.name !== "#default" && !isOwnCalendar && !isResource;
|
||||
calendar.name !== '#default' && !isOwnCalendar && !isResource
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "16px",
|
||||
alignItems: "center",
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '16px',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{isResource ? (
|
||||
@@ -40,12 +40,12 @@ export function CalendarName({ calendar }: { calendar: Calendar }) {
|
||||
style={{
|
||||
color: calendar.color?.light ?? defaultColors[0].light,
|
||||
width: 24,
|
||||
height: 24,
|
||||
height: 24
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography variant="body2" sx={{ wordBreak: "break-word" }}>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
|
||||
{renameDefault(calendar.name, ownerDisplayName, t, isOwnCalendar)}
|
||||
</Typography>
|
||||
<OwnerCaption
|
||||
@@ -54,5 +54,5 @@ export function CalendarName({ calendar }: { calendar: Calendar }) {
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { CalendarData } from "@/features/Calendars/types/CalendarData";
|
||||
import { renameDefault } from "@/utils/renameDefault";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { getCalendars } from '@/features/Calendars/CalendarApi'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { CalendarData } from '@/features/Calendars/types/CalendarData'
|
||||
import { renameDefault } from '@/utils/renameDefault'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ResponsiveDialog } from "../Dialog";
|
||||
import { ColorPicker } from "./CalendarColorPicker";
|
||||
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
import { addCalendarResourceAsync } from "@/features/Calendars/api/addCalendarResourceAsync";
|
||||
import { Resource, ResourceSearch } from "../Attendees/ResourceSearch";
|
||||
import { ResourceIcon } from "../Attendees/ResourceIcon";
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { ResponsiveDialog } from '../Dialog'
|
||||
import { ColorPicker } from './CalendarColorPicker'
|
||||
import { getAccessiblePair } from '@/utils/getAccessiblePair'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
import { addCalendarResourceAsync } from '@/features/Calendars/api/addCalendarResourceAsync'
|
||||
import { Resource, ResourceSearch } from '../Attendees/ResourceSearch'
|
||||
import { ResourceIcon } from '../Attendees/ResourceIcon'
|
||||
|
||||
interface CalendarWithOwner {
|
||||
cal: CalendarData;
|
||||
owner: Resource;
|
||||
cal: CalendarData
|
||||
owner: Resource
|
||||
}
|
||||
|
||||
function CalendarItem({
|
||||
cal,
|
||||
onRemove,
|
||||
onColorChange,
|
||||
onColorChange
|
||||
}: {
|
||||
cal: CalendarWithOwner;
|
||||
onRemove: () => void;
|
||||
onColorChange: (color: Record<string, string>) => void;
|
||||
cal: CalendarWithOwner
|
||||
onRemove: () => void
|
||||
onColorChange: (color: Record<string, string>) => void
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { t } = useI18n();
|
||||
const theme = useTheme()
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<Box
|
||||
key={cal.cal["dav:name"]}
|
||||
key={cal.cal['dav:name']}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
gap={2}
|
||||
@@ -48,17 +48,17 @@ function CalendarItem({
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ResourceIcon avatarUrl={cal.owner.avatarUrl} />
|
||||
<Typography variant="body1">
|
||||
{renameDefault(cal.cal["dav:name"], cal.owner.displayName, t, false)}
|
||||
{renameDefault(cal.cal['dav:name'], cal.owner.displayName, t, false)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ColorPicker
|
||||
selectedColor={{
|
||||
light: cal.cal["apple:color"] ?? defaultColors[0].light,
|
||||
dark: cal.cal["apple:color"]
|
||||
? getAccessiblePair(cal.cal["apple:color"], theme)
|
||||
: defaultColors[0].dark,
|
||||
light: cal.cal['apple:color'] ?? defaultColors[0].light,
|
||||
dark: cal.cal['apple:color']
|
||||
? getAccessiblePair(cal.cal['apple:color'], theme)
|
||||
: defaultColors[0].dark
|
||||
}}
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
@@ -67,33 +67,30 @@ function CalendarItem({
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SelectedCalendarsList({
|
||||
calendars,
|
||||
selectedCal,
|
||||
onRemove,
|
||||
onColorChange,
|
||||
onColorChange
|
||||
}: {
|
||||
calendars: Record<string, Calendar>;
|
||||
selectedCal: CalendarWithOwner[];
|
||||
onRemove: (cal: CalendarWithOwner) => void;
|
||||
onColorChange: (
|
||||
cal: CalendarWithOwner,
|
||||
color: Record<string, string>
|
||||
) => void;
|
||||
calendars: Record<string, Calendar>
|
||||
selectedCal: CalendarWithOwner[]
|
||||
onRemove: (cal: CalendarWithOwner) => void
|
||||
onColorChange: (cal: CalendarWithOwner, color: Record<string, string>) => void
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
if (selectedCal.length === 0) return null;
|
||||
const { t } = useI18n()
|
||||
if (selectedCal.length === 0) return null
|
||||
|
||||
const groupedByOwner = selectedCal.reduce<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
owner: Resource;
|
||||
visibleCals: CalendarWithOwner[];
|
||||
alreadyExisting: boolean;
|
||||
owner: Resource
|
||||
visibleCals: CalendarWithOwner[]
|
||||
alreadyExisting: boolean
|
||||
}
|
||||
>
|
||||
>((acc, cal) => {
|
||||
@@ -101,106 +98,106 @@ function SelectedCalendarsList({
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
);
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
)
|
||||
|
||||
if (!acc[cal.owner.displayName]) {
|
||||
acc[cal.owner.displayName] = {
|
||||
owner: cal.owner,
|
||||
visibleCals: [],
|
||||
alreadyExisting: false,
|
||||
};
|
||||
alreadyExisting: false
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
acc[cal.owner.displayName].alreadyExisting = true;
|
||||
acc[cal.owner.displayName].alreadyExisting = true
|
||||
} else {
|
||||
acc[cal.owner.displayName].visibleCals.push(cal);
|
||||
acc[cal.owner.displayName].visibleCals.push(cal)
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Typography variant="h6" sx={{ margin: 0 }}>
|
||||
{t("common.resource")}
|
||||
{t('common.resource')}
|
||||
</Typography>
|
||||
|
||||
{Object.values(groupedByOwner).map(
|
||||
({ owner, visibleCals, alreadyExisting }) => (
|
||||
<Box key={owner.displayName} mb={2}>
|
||||
{visibleCals.length > 0 ? (
|
||||
visibleCals.map((cal) =>
|
||||
visibleCals.map(cal =>
|
||||
cal.cal ? (
|
||||
<CalendarItem
|
||||
key={cal.owner.displayName + cal.cal["dav:name"]}
|
||||
key={cal.owner.displayName + cal.cal['dav:name']}
|
||||
cal={cal}
|
||||
onRemove={() => onRemove(cal)}
|
||||
onColorChange={(color) => onColorChange(cal, color)}
|
||||
onColorChange={color => onColorChange(cal, color)}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
key={t("calendar.noPublicCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
key={t('calendar.noPublicCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
color="textSecondary"
|
||||
>
|
||||
{t("calendar.noPublicCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
{t('calendar.noPublicCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
</Typography>
|
||||
)
|
||||
)
|
||||
) : alreadyExisting ? (
|
||||
<Typography
|
||||
key={t("calendar.noMoreCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
key={t('calendar.noMoreCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
color="textSecondary"
|
||||
>
|
||||
{t("calendar.noMoreCalendarsFor", { name: owner.displayName })}
|
||||
{t('calendar.noMoreCalendarsFor', { name: owner.displayName })}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function CalendarResources({
|
||||
open,
|
||||
onClose,
|
||||
onClose
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: (ids?: string[]) => void;
|
||||
open: boolean
|
||||
onClose: (ids?: string[]) => void
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch()
|
||||
const theme = useTheme()
|
||||
|
||||
const openpaasId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
|
||||
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([]);
|
||||
const [selectedResources, setSelectedResources] = useState<Resource[]>([]);
|
||||
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([])
|
||||
const [selectedResources, setSelectedResources] = useState<Resource[]>([])
|
||||
|
||||
const fetchSeqRef = useRef(0);
|
||||
const fetchSeqRef = useRef(0)
|
||||
|
||||
const handleSave = async () => {
|
||||
if (selectedCal.length > 0) {
|
||||
const results = await Promise.allSettled(
|
||||
selectedCal.map(async (cal) => {
|
||||
const calId = crypto.randomUUID();
|
||||
selectedCal.map(async cal => {
|
||||
const calId = crypto.randomUUID()
|
||||
const exists = Object.values(calendars).some(
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
);
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
)
|
||||
if (!exists && cal.cal) {
|
||||
await dispatch(
|
||||
addCalendarResourceAsync({
|
||||
@@ -208,134 +205,134 @@ export default function CalendarResources({
|
||||
calId,
|
||||
cal: {
|
||||
...cal,
|
||||
color: cal.cal["apple:color"]
|
||||
color: cal.cal['apple:color']
|
||||
? {
|
||||
light: cal.cal["apple:color"],
|
||||
dark: getAccessiblePair(cal.cal["apple:color"], theme),
|
||||
light: cal.cal['apple:color'],
|
||||
dark: getAccessiblePair(cal.cal['apple:color'], theme)
|
||||
}
|
||||
: defaultColors[0],
|
||||
},
|
||||
: defaultColors[0]
|
||||
}
|
||||
})
|
||||
).unwrap();
|
||||
).unwrap()
|
||||
return cal.cal._links.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "");
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
const idList = results
|
||||
.filter((r) => r.status === "fulfilled")
|
||||
.map((r) => (r as PromiseFulfilledResult<string | null>).value)
|
||||
.filter(Boolean) as string[];
|
||||
.filter(r => r.status === 'fulfilled')
|
||||
.map(r => (r as PromiseFulfilledResult<string | null>).value)
|
||||
.filter(Boolean) as string[]
|
||||
|
||||
onClose(idList);
|
||||
onClose(idList)
|
||||
} else {
|
||||
onClose();
|
||||
onClose()
|
||||
}
|
||||
setSelectedCalendars([]);
|
||||
setSelectedResources([]);
|
||||
};
|
||||
const { t } = useI18n();
|
||||
setSelectedCalendars([])
|
||||
setSelectedResources([])
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
const handleClose = () => {
|
||||
fetchSeqRef.current += 1; // invalidate in-flight fetch results
|
||||
onClose();
|
||||
setSelectedCalendars([]);
|
||||
setSelectedResources([]);
|
||||
};
|
||||
fetchSeqRef.current += 1 // invalidate in-flight fetch results
|
||||
onClose()
|
||||
setSelectedCalendars([])
|
||||
setSelectedResources([])
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
contentSx={{ paddingTop: "8px !important" }}
|
||||
contentSx={{ paddingTop: '8px !important' }}
|
||||
onClose={handleClose}
|
||||
title={t("calendar.browseResources")}
|
||||
title={t('calendar.browseResources')}
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
{t("actions.add")}
|
||||
{t('actions.add')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ResourceSearch
|
||||
objectTypes={["resource"]}
|
||||
objectTypes={['resource']}
|
||||
selectedResources={selectedResources}
|
||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||
inputSlot={params => <TextField {...params} size="small" />}
|
||||
onChange={async (_event: React.SyntheticEvent, value: Resource[]) => {
|
||||
const requestSeq = ++fetchSeqRef.current;
|
||||
setSelectedResources(value);
|
||||
const requestSeq = ++fetchSeqRef.current
|
||||
setSelectedResources(value)
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
value.map(async (user: Resource) => {
|
||||
if (user?.openpaasId) {
|
||||
const cals = await getCalendars(
|
||||
user.openpaasId,
|
||||
"sharedPublic=true&"
|
||||
);
|
||||
return cals._embedded?.["dav:calendar"]
|
||||
? cals._embedded["dav:calendar"].map((cal) => ({
|
||||
'sharedPublic=true&'
|
||||
)
|
||||
return cals._embedded?.['dav:calendar']
|
||||
? cals._embedded['dav:calendar'].map(cal => ({
|
||||
cal,
|
||||
owner: user,
|
||||
owner: user
|
||||
}))
|
||||
: [{ cal: undefined, owner: user }];
|
||||
: [{ cal: undefined, owner: user }]
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
const successfulCals = results
|
||||
.filter((result) => result.status === "fulfilled")
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(
|
||||
(result) =>
|
||||
result =>
|
||||
(result as PromiseFulfilledResult<CalendarWithOwner[]>).value
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean);
|
||||
.filter(Boolean)
|
||||
|
||||
if (requestSeq !== fetchSeqRef.current) return;
|
||||
setSelectedCalendars(successfulCals as CalendarWithOwner[]);
|
||||
if (requestSeq !== fetchSeqRef.current) return
|
||||
setSelectedCalendars(successfulCals)
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectedCalendarsList
|
||||
calendars={calendars}
|
||||
selectedCal={selectedCal}
|
||||
onRemove={(cal) => {
|
||||
if (!cal.cal?._links?.self?.href) return;
|
||||
setSelectedCalendars((prev) =>
|
||||
onRemove={cal => {
|
||||
if (!cal.cal?._links?.self?.href) return
|
||||
setSelectedCalendars(prev =>
|
||||
prev.filter(
|
||||
(c) => c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
c => c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
)
|
||||
);
|
||||
)
|
||||
if (
|
||||
!selectedCal.find(
|
||||
(c) =>
|
||||
c =>
|
||||
cal.owner.displayName === c.owner.displayName &&
|
||||
c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
)
|
||||
) {
|
||||
setSelectedResources((prev) =>
|
||||
prev.filter((u) => u.displayName !== cal.owner.displayName)
|
||||
);
|
||||
setSelectedResources(prev =>
|
||||
prev.filter(u => u.displayName !== cal.owner.displayName)
|
||||
)
|
||||
}
|
||||
}}
|
||||
onColorChange={(cal, color) =>
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.map((prevcal) =>
|
||||
setSelectedCalendars(prev =>
|
||||
prev.map(prevcal =>
|
||||
prevcal.owner.displayName === cal.owner.displayName &&
|
||||
prevcal.cal._links.self?.href === cal.cal._links.self?.href
|
||||
? {
|
||||
...prevcal,
|
||||
cal: {
|
||||
...prevcal.cal,
|
||||
"apple:color": color.light,
|
||||
},
|
||||
'apple:color': color.light
|
||||
}
|
||||
}
|
||||
: prevcal
|
||||
)
|
||||
@@ -343,5 +340,5 @@ export default function CalendarResources({
|
||||
}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { getCalendars } from "@/features/Calendars/CalendarApi";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { addSharedCalendarAsync } from "@/features/Calendars/services";
|
||||
import { CalendarData } from "@/features/Calendars/types/CalendarData";
|
||||
import { renameDefault } from "@/utils/renameDefault";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { getCalendars } from '@/features/Calendars/CalendarApi'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { addSharedCalendarAsync } from '@/features/Calendars/services'
|
||||
import { CalendarData } from '@/features/Calendars/types/CalendarData'
|
||||
import { renameDefault } from '@/utils/renameDefault'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -11,61 +11,61 @@ import {
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
||||
import { ResponsiveDialog } from "../Dialog";
|
||||
import { stringAvatar } from "../Event/utils/eventUtils";
|
||||
import { ColorPicker } from "./CalendarColorPicker";
|
||||
import { getAccessiblePair } from "@/utils/getAccessiblePair";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { PeopleSearch, User } from '../Attendees/PeopleSearch'
|
||||
import { ResponsiveDialog } from '../Dialog'
|
||||
import { stringAvatar } from '../Event/utils/eventUtils'
|
||||
import { ColorPicker } from './CalendarColorPicker'
|
||||
import { getAccessiblePair } from '@/utils/getAccessiblePair'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
|
||||
interface CalendarWithOwner {
|
||||
cal: CalendarData;
|
||||
owner: User;
|
||||
cal: CalendarData
|
||||
owner: User
|
||||
}
|
||||
|
||||
function CalendarItem({
|
||||
cal,
|
||||
onRemove,
|
||||
onColorChange,
|
||||
onColorChange
|
||||
}: {
|
||||
cal: CalendarWithOwner;
|
||||
onRemove: () => void;
|
||||
onColorChange: (color: Record<string, string>) => void;
|
||||
cal: CalendarWithOwner
|
||||
onRemove: () => void
|
||||
onColorChange: (color: Record<string, string>) => void
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { t } = useI18n();
|
||||
const theme = useTheme()
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<Box
|
||||
key={cal.owner.email + cal.cal["dav:name"]}
|
||||
key={cal.owner.email + cal.cal['dav:name']}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
border: "1px solid #e5e7eb",
|
||||
border: '1px solid #e5e7eb',
|
||||
padding: 8,
|
||||
marginBottom: 8,
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={1}>
|
||||
<Avatar
|
||||
{...stringAvatar(cal.owner.displayName || cal.owner.email)}
|
||||
style={{
|
||||
border: `2px solid ${cal.cal["apple:color"] || defaultColors[0].light}`,
|
||||
boxShadow: cal.cal["apple:color"]
|
||||
? `0 0 0 2px ${cal.cal["apple:color"]}`
|
||||
: `0 0 0 2px ${defaultColors[0].light}`,
|
||||
border: `2px solid ${cal.cal['apple:color'] || defaultColors[0].light}`,
|
||||
boxShadow: cal.cal['apple:color']
|
||||
? `0 0 0 2px ${cal.cal['apple:color']}`
|
||||
: `0 0 0 2px ${defaultColors[0].light}`
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="body1">
|
||||
{renameDefault(
|
||||
cal.cal["dav:name"],
|
||||
cal.cal['dav:name'],
|
||||
cal.owner.displayName,
|
||||
t,
|
||||
false
|
||||
@@ -80,10 +80,10 @@ function CalendarItem({
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ColorPicker
|
||||
selectedColor={{
|
||||
light: cal.cal["apple:color"] ?? defaultColors[0].light,
|
||||
dark: cal.cal["apple:color"]
|
||||
? getAccessiblePair(cal.cal["apple:color"], theme)
|
||||
: defaultColors[0].dark,
|
||||
light: cal.cal['apple:color'] ?? defaultColors[0].light,
|
||||
dark: cal.cal['apple:color']
|
||||
? getAccessiblePair(cal.cal['apple:color'], theme)
|
||||
: defaultColors[0].dark
|
||||
}}
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
@@ -92,33 +92,30 @@ function CalendarItem({
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function SelectedCalendarsList({
|
||||
calendars,
|
||||
selectedCal,
|
||||
onRemove,
|
||||
onColorChange,
|
||||
onColorChange
|
||||
}: {
|
||||
calendars: Record<string, Calendar>;
|
||||
selectedCal: CalendarWithOwner[];
|
||||
onRemove: (cal: CalendarWithOwner) => void;
|
||||
onColorChange: (
|
||||
cal: CalendarWithOwner,
|
||||
color: Record<string, string>
|
||||
) => void;
|
||||
calendars: Record<string, Calendar>
|
||||
selectedCal: CalendarWithOwner[]
|
||||
onRemove: (cal: CalendarWithOwner) => void
|
||||
onColorChange: (cal: CalendarWithOwner, color: Record<string, string>) => void
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
if (selectedCal.length === 0) return null;
|
||||
const { t } = useI18n()
|
||||
if (selectedCal.length === 0) return null
|
||||
|
||||
const groupedByOwner = selectedCal.reduce<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
owner: User;
|
||||
visibleCals: CalendarWithOwner[];
|
||||
alreadyExisting: boolean;
|
||||
owner: User
|
||||
visibleCals: CalendarWithOwner[]
|
||||
alreadyExisting: boolean
|
||||
}
|
||||
>
|
||||
>((acc, cal) => {
|
||||
@@ -126,107 +123,107 @@ function SelectedCalendarsList({
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
);
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
)
|
||||
|
||||
if (!acc[cal.owner.email]) {
|
||||
acc[cal.owner.email] = {
|
||||
owner: cal.owner,
|
||||
visibleCals: [],
|
||||
alreadyExisting: false,
|
||||
};
|
||||
alreadyExisting: false
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
acc[cal.owner.email].alreadyExisting = true;
|
||||
acc[cal.owner.email].alreadyExisting = true
|
||||
} else {
|
||||
acc[cal.owner.email].visibleCals.push(cal);
|
||||
acc[cal.owner.email].visibleCals.push(cal)
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t("common.name")}
|
||||
{t('common.name')}
|
||||
</Typography>
|
||||
|
||||
{Object.values(groupedByOwner).map(
|
||||
({ owner, visibleCals, alreadyExisting }) => (
|
||||
<Box key={owner.email} mb={2}>
|
||||
{visibleCals.length > 0 ? (
|
||||
visibleCals.map((cal) =>
|
||||
visibleCals.map(cal =>
|
||||
cal.cal ? (
|
||||
<CalendarItem
|
||||
key={cal.owner.email + cal.cal["dav:name"]}
|
||||
key={cal.owner.email + cal.cal['dav:name']}
|
||||
cal={cal}
|
||||
onRemove={() => onRemove(cal)}
|
||||
onColorChange={(color) => onColorChange(cal, color)}
|
||||
onColorChange={color => onColorChange(cal, color)}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
key={t("calendar.noPublicCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
key={t('calendar.noPublicCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
color="textSecondary"
|
||||
>
|
||||
{t("calendar.noPublicCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
{t('calendar.noPublicCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
</Typography>
|
||||
)
|
||||
)
|
||||
) : alreadyExisting ? (
|
||||
<Typography
|
||||
key={t("calendar.noMoreCalendarsFor", {
|
||||
name: owner.displayName,
|
||||
key={t('calendar.noMoreCalendarsFor', {
|
||||
name: owner.displayName
|
||||
})}
|
||||
color="textSecondary"
|
||||
>
|
||||
{t("calendar.noMoreCalendarsFor", { name: owner.displayName })}
|
||||
{t('calendar.noMoreCalendarsFor', { name: owner.displayName })}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function CalendarSearch({
|
||||
open,
|
||||
onClose,
|
||||
onClose
|
||||
}: {
|
||||
open: boolean;
|
||||
open: boolean
|
||||
onClose: (
|
||||
result?: string[] | Record<string, never>,
|
||||
reason?: "backdropClick" | "escapeKeyDown"
|
||||
) => void;
|
||||
reason?: 'backdropClick' | 'escapeKeyDown'
|
||||
) => void
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch()
|
||||
const theme = useTheme()
|
||||
|
||||
const openpaasId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
|
||||
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([]);
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||
const [selectedCal, setSelectedCalendars] = useState<CalendarWithOwner[]>([])
|
||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (selectedCal.length > 0) {
|
||||
const idList = await Promise.all(
|
||||
selectedCal.map(async (cal) => {
|
||||
const calId = crypto.randomUUID();
|
||||
selectedCal.map(async cal => {
|
||||
const calId = crypto.randomUUID()
|
||||
const exists = Object.values(calendars).some(
|
||||
(existing: Calendar) =>
|
||||
existing.id ===
|
||||
cal.cal?._links?.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "")
|
||||
);
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
)
|
||||
if (!exists && cal.cal) {
|
||||
await dispatch(
|
||||
addSharedCalendarAsync({
|
||||
@@ -234,117 +231,117 @@ export default function CalendarSearch({
|
||||
calId,
|
||||
cal: {
|
||||
...cal,
|
||||
color: cal.cal["apple:color"]
|
||||
color: cal.cal['apple:color']
|
||||
? {
|
||||
light: cal.cal["apple:color"],
|
||||
dark: getAccessiblePair(cal.cal["apple:color"], theme),
|
||||
light: cal.cal['apple:color'],
|
||||
dark: getAccessiblePair(cal.cal['apple:color'], theme)
|
||||
}
|
||||
: defaultColors[0],
|
||||
},
|
||||
: defaultColors[0]
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
return cal.cal._links.self?.href
|
||||
?.replace("/calendars/", "")
|
||||
.replace(".json", "");
|
||||
?.replace('/calendars/', '')
|
||||
.replace('.json', '')
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
onClose(idList.filter(Boolean) as string[]);
|
||||
setSelectedCalendars([]);
|
||||
setSelectedUsers([]);
|
||||
onClose(idList.filter(Boolean) as string[])
|
||||
setSelectedCalendars([])
|
||||
setSelectedUsers([])
|
||||
}
|
||||
};
|
||||
const { t } = useI18n();
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
contentSx={{ paddingTop: "8px !important" }}
|
||||
contentSx={{ paddingTop: '8px !important' }}
|
||||
onClose={() => {
|
||||
onClose({}, "backdropClick");
|
||||
setSelectedCalendars([]);
|
||||
setSelectedUsers([]);
|
||||
onClose({}, 'backdropClick')
|
||||
setSelectedCalendars([])
|
||||
setSelectedUsers([])
|
||||
}}
|
||||
title={t("calendar.browseOtherCalendars")}
|
||||
title={t('calendar.browseOtherCalendars')}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onClose({}, "backdropClick")}
|
||||
onClick={() => onClose({}, 'backdropClick')}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
{t("actions.add")}
|
||||
{t('actions.add')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PeopleSearch
|
||||
objectTypes={["user"]}
|
||||
objectTypes={['user']}
|
||||
selectedUsers={selectedUsers}
|
||||
inputSlot={(params) => <TextField {...params} size="small" />}
|
||||
inputSlot={params => <TextField {...params} size="small" />}
|
||||
onChange={async (_event: React.SyntheticEvent, value: User[]) => {
|
||||
setSelectedUsers(value);
|
||||
setSelectedUsers(value)
|
||||
|
||||
const cals = await Promise.all(
|
||||
value.map(async (user: User) => {
|
||||
if (user?.openpaasId) {
|
||||
const cals = await getCalendars(
|
||||
user.openpaasId,
|
||||
"sharedPublic=true&"
|
||||
);
|
||||
return cals._embedded?.["dav:calendar"]
|
||||
? cals._embedded["dav:calendar"].map((cal) => ({
|
||||
'sharedPublic=true&'
|
||||
)
|
||||
return cals._embedded?.['dav:calendar']
|
||||
? cals._embedded['dav:calendar'].map(cal => ({
|
||||
cal,
|
||||
owner: user,
|
||||
owner: user
|
||||
}))
|
||||
: { cal: undefined, owner: user };
|
||||
: { cal: undefined, owner: user }
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
})
|
||||
);
|
||||
)
|
||||
setSelectedCalendars(
|
||||
cals.flat().filter(Boolean) as CalendarWithOwner[]
|
||||
);
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectedCalendarsList
|
||||
calendars={calendars}
|
||||
selectedCal={selectedCal}
|
||||
onRemove={(cal) => {
|
||||
if (!cal.cal?._links?.self?.href) return;
|
||||
setSelectedCalendars((prev) =>
|
||||
onRemove={cal => {
|
||||
if (!cal.cal?._links?.self?.href) return
|
||||
setSelectedCalendars(prev =>
|
||||
prev.filter(
|
||||
(c) => c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
c => c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
)
|
||||
);
|
||||
)
|
||||
if (
|
||||
!selectedCal.find(
|
||||
(c) =>
|
||||
c =>
|
||||
cal.owner.email === c.owner.email &&
|
||||
c.cal?._links?.self?.href !== cal.cal._links.self?.href
|
||||
)
|
||||
) {
|
||||
setSelectedUsers((prev) =>
|
||||
prev.filter((u) => u.email !== cal.owner.email)
|
||||
);
|
||||
setSelectedUsers(prev =>
|
||||
prev.filter(u => u.email !== cal.owner.email)
|
||||
)
|
||||
}
|
||||
}}
|
||||
onColorChange={(cal, color) =>
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.map((prevcal) =>
|
||||
setSelectedCalendars(prev =>
|
||||
prev.map(prevcal =>
|
||||
prevcal.owner.email === cal.owner.email &&
|
||||
prevcal.cal._links.self?.href === cal.cal._links.self?.href
|
||||
? {
|
||||
...prevcal,
|
||||
cal: {
|
||||
...prevcal.cal,
|
||||
"apple:color": color.light,
|
||||
},
|
||||
'apple:color': color.light
|
||||
}
|
||||
}
|
||||
: prevcal
|
||||
)
|
||||
@@ -352,5 +349,5 @@ export default function CalendarSearch({
|
||||
}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { removeCalendarAsync } from "@/features/Calendars/services";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||
import { renameDefault } from "@/utils/renameDefault";
|
||||
import { trimLongTextWithoutSpace } from "@/utils/textUtils";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { removeCalendarAsync } from '@/features/Calendars/services'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import { makeDisplayName } from '@/utils/makeDisplayName'
|
||||
import { renameDefault } from '@/utils/renameDefault'
|
||||
import { trimLongTextWithoutSpace } from '@/utils/textUtils'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
@@ -14,18 +14,18 @@ import {
|
||||
IconButton,
|
||||
ListItem,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from "@linagora/twake-mui";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import CalendarPopover from "./CalendarModal";
|
||||
import CalendarSearch from "./CalendarSearch";
|
||||
import { DeleteCalendarDialog } from "./DeleteCalendarDialog";
|
||||
import { OwnerCaption } from "./OwnerCaption";
|
||||
import CalendarResources from "./CalendarResources";
|
||||
MenuItem
|
||||
} from '@linagora/twake-mui'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'
|
||||
import { SetStateAction, useEffect, useMemo, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import CalendarPopover from './CalendarModal'
|
||||
import CalendarSearch from './CalendarSearch'
|
||||
import { DeleteCalendarDialog } from './DeleteCalendarDialog'
|
||||
import { OwnerCaption } from './OwnerCaption'
|
||||
import CalendarResources from './CalendarResources'
|
||||
|
||||
function CalendarAccordion({
|
||||
title,
|
||||
@@ -36,40 +36,46 @@ function CalendarAccordion({
|
||||
onAddClick,
|
||||
defaultExpanded = false,
|
||||
setOpen,
|
||||
hideOwner,
|
||||
hideOwner
|
||||
}: {
|
||||
title: string;
|
||||
calendars: string[];
|
||||
selectedCalendars: string[];
|
||||
handleToggle: (id: string) => void;
|
||||
showAddButton?: boolean;
|
||||
onAddClick?: () => void;
|
||||
defaultExpanded?: boolean;
|
||||
setOpen: (id: string) => void;
|
||||
hideOwner?: boolean;
|
||||
title: string
|
||||
calendars: string[]
|
||||
selectedCalendars: string[]
|
||||
handleToggle: (id: string) => void
|
||||
showAddButton?: boolean
|
||||
onAddClick?: () => void
|
||||
defaultExpanded?: boolean
|
||||
setOpen: (id: string) => void
|
||||
hideOwner?: boolean
|
||||
}) {
|
||||
const allCalendars = useAppSelector((state) => state.calendars.list);
|
||||
const { t } = useI18n();
|
||||
const allCalendars = useAppSelector(state => state.calendars.list)
|
||||
const { t } = useI18n()
|
||||
|
||||
const [expended, setExpended] = useState(defaultExpanded);
|
||||
useEffect(() => setExpended(defaultExpanded), [defaultExpanded]);
|
||||
const [expended, setExpended] = useState(defaultExpanded)
|
||||
|
||||
if (calendars.length === 0 && !showAddButton) return null;
|
||||
useEffect(() => {
|
||||
const handleExpendedChange = () => {
|
||||
setExpended(defaultExpanded)
|
||||
}
|
||||
handleExpendedChange()
|
||||
}, [defaultExpanded])
|
||||
|
||||
if (calendars.length === 0 && !showAddButton) return null
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpanded={defaultExpanded}
|
||||
expanded={expended}
|
||||
style={{
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
marginBottom: "12px",
|
||||
boxShadow: "none",
|
||||
marginBottom: '12px',
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
sx={{
|
||||
"&::before": {
|
||||
display: "none",
|
||||
},
|
||||
'&::before': {
|
||||
display: 'none'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
@@ -79,23 +85,23 @@ function CalendarAccordion({
|
||||
className="calendarListHeader"
|
||||
onClick={() => setExpended(!expended)}
|
||||
sx={{
|
||||
"& .MuiAccordionSummary-content": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
'& .MuiAccordionSummary-content': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{title}</span>
|
||||
{showAddButton && (
|
||||
<IconButton
|
||||
component="span"
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
if (expended) {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (onAddClick) {
|
||||
onAddClick();
|
||||
onAddClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -103,13 +109,13 @@ function CalendarAccordion({
|
||||
</IconButton>
|
||||
)}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails style={{ textAlign: "left", padding: 0 }}>
|
||||
{calendars.map((id) => (
|
||||
<AccordionDetails style={{ textAlign: 'left', padding: 0 }}>
|
||||
{calendars.map(id => (
|
||||
<CalendarSelector
|
||||
key={id}
|
||||
calendars={allCalendars}
|
||||
id={id}
|
||||
isPersonal={title === t("calendar.personal")}
|
||||
isPersonal={title === t('calendar.personal')}
|
||||
selectedCalendars={selectedCalendars}
|
||||
handleCalendarToggle={handleToggle}
|
||||
setOpen={() => setOpen(id)}
|
||||
@@ -118,111 +124,110 @@ function CalendarAccordion({
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function CalendarSelection({
|
||||
selectedCalendars,
|
||||
setSelectedCalendars,
|
||||
setSelectedCalendars
|
||||
}: {
|
||||
selectedCalendars: string[];
|
||||
setSelectedCalendars: (value: SetStateAction<string[]>) => void;
|
||||
selectedCalendars: string[]
|
||||
setSelectedCalendars: (value: SetStateAction<string[]>) => void
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const { t } = useI18n()
|
||||
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
|
||||
const personalCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) => extractEventBaseUuid(id) === userId
|
||||
);
|
||||
id => extractEventBaseUuid(id) === userId
|
||||
)
|
||||
const delegatedCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) =>
|
||||
id =>
|
||||
extractEventBaseUuid(id) !== userId &&
|
||||
calendars[id]?.delegated &&
|
||||
!calendars?.[id]?.owner?.resource
|
||||
);
|
||||
)
|
||||
const sharedCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) =>
|
||||
id =>
|
||||
extractEventBaseUuid(id) !== userId &&
|
||||
!calendars?.[id]?.delegated &&
|
||||
!calendars?.[id]?.owner?.resource
|
||||
);
|
||||
)
|
||||
const resourceCalendars = Object.keys(calendars || {}).filter(
|
||||
(id) =>
|
||||
id =>
|
||||
extractEventBaseUuid(id) !== userId && calendars?.[id]?.owner?.resource
|
||||
);
|
||||
)
|
||||
|
||||
const handleCalendarToggle = (name: string) => {
|
||||
setSelectedCalendars((prev: string[]) =>
|
||||
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
|
||||
);
|
||||
};
|
||||
const [selectedCalId, setSelectedCalId] = useState("");
|
||||
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
|
||||
)
|
||||
}
|
||||
const [selectedCalId, setSelectedCalId] = useState('')
|
||||
|
||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null)
|
||||
const [anchorElCalOthers, setAnchorElCalOthers] =
|
||||
useState<HTMLElement | null>(null);
|
||||
useState<HTMLElement | null>(null)
|
||||
const [anchorElCalResources, setAnchorElCalResources] =
|
||||
useState<HTMLElement | null>(null);
|
||||
useState<HTMLElement | null>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CalendarAccordion
|
||||
title={t("calendar.personal")}
|
||||
title={t('calendar.personal')}
|
||||
calendars={personalCalendars}
|
||||
selectedCalendars={selectedCalendars}
|
||||
handleToggle={handleCalendarToggle}
|
||||
showAddButton
|
||||
onAddClick={() => setAnchorElCal(document.body)}
|
||||
setOpen={(id: string) => {
|
||||
setAnchorElCal(document.body);
|
||||
setSelectedCalId(id);
|
||||
setAnchorElCal(document.body)
|
||||
setSelectedCalId(id)
|
||||
}}
|
||||
defaultExpanded
|
||||
/>
|
||||
|
||||
<CalendarAccordion
|
||||
title={t("calendar.delegated")}
|
||||
title={t('calendar.delegated')}
|
||||
calendars={delegatedCalendars}
|
||||
selectedCalendars={selectedCalendars}
|
||||
handleToggle={handleCalendarToggle}
|
||||
setOpen={(id: string) => {
|
||||
setAnchorElCal(document.body);
|
||||
setSelectedCalId(id);
|
||||
setAnchorElCal(document.body)
|
||||
setSelectedCalId(id)
|
||||
}}
|
||||
defaultExpanded
|
||||
/>
|
||||
|
||||
<CalendarAccordion
|
||||
title={t("calendar.other")}
|
||||
title={t('calendar.other')}
|
||||
calendars={sharedCalendars}
|
||||
selectedCalendars={selectedCalendars}
|
||||
showAddButton
|
||||
onAddClick={() => {
|
||||
setAnchorElCalOthers(document.body);
|
||||
setAnchorElCalOthers(document.body)
|
||||
}}
|
||||
handleToggle={handleCalendarToggle}
|
||||
setOpen={(id: string) => {
|
||||
setAnchorElCal(document.body);
|
||||
setSelectedCalId(id);
|
||||
setAnchorElCal(document.body)
|
||||
setSelectedCalId(id)
|
||||
}}
|
||||
defaultExpanded
|
||||
/>
|
||||
|
||||
<CalendarAccordion
|
||||
title={t("calendar.resources")}
|
||||
title={t('calendar.resources')}
|
||||
calendars={resourceCalendars}
|
||||
selectedCalendars={selectedCalendars}
|
||||
onAddClick={() => {
|
||||
setAnchorElCalResources(document.body);
|
||||
setAnchorElCalResources(document.body)
|
||||
}}
|
||||
showAddButton
|
||||
handleToggle={handleCalendarToggle}
|
||||
setOpen={(id: string) => {
|
||||
setAnchorElCal(document.body);
|
||||
setSelectedCalId(id);
|
||||
setAnchorElCal(document.body)
|
||||
setSelectedCalId(id)
|
||||
}}
|
||||
defaultExpanded
|
||||
hideOwner={true}
|
||||
@@ -232,32 +237,32 @@ export default function CalendarSelection({
|
||||
open={Boolean(anchorElCal)}
|
||||
calendar={calendars?.[selectedCalId] ?? undefined}
|
||||
onClose={() => {
|
||||
setSelectedCalId("");
|
||||
setAnchorElCal(null);
|
||||
setSelectedCalId('')
|
||||
setAnchorElCal(null)
|
||||
}}
|
||||
/>
|
||||
<CalendarSearch
|
||||
open={Boolean(anchorElCalOthers)}
|
||||
onClose={(newCalIds?: string[]) => {
|
||||
setAnchorElCalOthers(null);
|
||||
setAnchorElCalOthers(null)
|
||||
if (newCalIds?.length) {
|
||||
newCalIds.forEach((id) => handleCalendarToggle(id));
|
||||
newCalIds.forEach(id => handleCalendarToggle(id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CalendarResources
|
||||
open={Boolean(anchorElCalResources)}
|
||||
onClose={(newResourceIds?: string[]) => {
|
||||
setAnchorElCalResources(null);
|
||||
setAnchorElCalResources(null)
|
||||
if (newResourceIds?.length) {
|
||||
newResourceIds.forEach((id) => {
|
||||
handleCalendarToggle(id);
|
||||
});
|
||||
newResourceIds.forEach(id => {
|
||||
handleCalendarToggle(id)
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarSelector({
|
||||
@@ -267,113 +272,112 @@ function CalendarSelector({
|
||||
selectedCalendars,
|
||||
handleCalendarToggle,
|
||||
setOpen,
|
||||
hideOwner,
|
||||
hideOwner
|
||||
}: {
|
||||
calendars: Record<string, Calendar>;
|
||||
id: string;
|
||||
isPersonal: boolean;
|
||||
selectedCalendars: string[];
|
||||
handleCalendarToggle: (name: string) => void;
|
||||
setOpen: () => void;
|
||||
hideOwner?: boolean;
|
||||
calendars: Record<string, Calendar>
|
||||
id: string
|
||||
isPersonal: boolean
|
||||
selectedCalendars: string[]
|
||||
handleCalendarToggle: (name: string) => void
|
||||
setOpen: () => void
|
||||
hideOwner?: boolean
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const calLink =
|
||||
useAppSelector((state) => state.calendars.list[id].link) ?? "";
|
||||
const { t } = useI18n()
|
||||
const dispatch = useAppDispatch()
|
||||
const calLink = useAppSelector(state => state.calendars.list[id].link) ?? ''
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const open = Boolean(anchorEl)
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const [userId, calId] = id.split("/");
|
||||
const isDefault = isPersonal && userId === calId;
|
||||
setAnchorEl(null)
|
||||
}
|
||||
const [userId, calId] = id.split('/')
|
||||
const isDefault = isPersonal && userId === calId
|
||||
|
||||
const [deletePopupOpen, setDeletePopupOpen] = useState(false);
|
||||
const [deletePopupOpen, setDeletePopupOpen] = useState(false)
|
||||
const handleDeleteConfirm = () => {
|
||||
dispatch(removeCalendarAsync({ calId: id, calLink }));
|
||||
setDeletePopupOpen(false);
|
||||
handleClose();
|
||||
};
|
||||
dispatch(removeCalendarAsync({ calId: id, calLink }))
|
||||
setDeletePopupOpen(false)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const trimmedName = useMemo(
|
||||
() => trimLongTextWithoutSpace(calendars[id].name),
|
||||
[calendars, id]
|
||||
);
|
||||
)
|
||||
|
||||
const ownerDisplayName = useMemo(
|
||||
() => makeDisplayName(calendars[id]),
|
||||
[calendars, id]
|
||||
);
|
||||
)
|
||||
|
||||
const displayName = useMemo(
|
||||
() => renameDefault(trimmedName, ownerDisplayName ?? "", t, isPersonal),
|
||||
() => renameDefault(trimmedName, ownerDisplayName ?? '', t, isPersonal),
|
||||
[trimmedName, ownerDisplayName, t, isPersonal]
|
||||
);
|
||||
)
|
||||
|
||||
const showCaption =
|
||||
!isPersonal &&
|
||||
trimmedName !== "#default" &&
|
||||
trimmedName !== '#default' &&
|
||||
ownerDisplayName != null &&
|
||||
!hideOwner;
|
||||
!hideOwner
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
transition: "background-color 0.2s ease",
|
||||
"& .MoreBtn": { opacity: 0 },
|
||||
"&:hover": {
|
||||
backgroundColor: "#F3F3F6",
|
||||
"& .MoreBtn": { opacity: 1 },
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
transition: 'background-color 0.2s ease',
|
||||
'& .MoreBtn': { opacity: 0 },
|
||||
'&:hover': {
|
||||
backgroundColor: '#F3F3F6',
|
||||
'& .MoreBtn': { opacity: 1 }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
maxWidth: "calc(100% - 40px)",
|
||||
overflow: "hidden",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: 'calc(100% - 40px)',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
sx={{
|
||||
color: calendars[id].color?.light,
|
||||
"&.Mui-checked": { color: calendars[id].color?.light },
|
||||
'&.Mui-checked': { color: calendars[id].color?.light }
|
||||
}}
|
||||
size="small"
|
||||
checked={selectedCalendars.includes(id)}
|
||||
onChange={() => handleCalendarToggle(id)}
|
||||
inputProps={{ "aria-label": displayName }}
|
||||
inputProps={{ 'aria-label': displayName }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
padding: showCaption ? "6px" : undefined,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
padding: showCaption ? '6px' : undefined
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-word",
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
<OwnerCaption
|
||||
showCaption={showCaption}
|
||||
ownerDisplayName={ownerDisplayName ?? ""}
|
||||
ownerDisplayName={ownerDisplayName ?? ''}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
@@ -384,16 +388,16 @@ function CalendarSelector({
|
||||
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpen();
|
||||
handleClose();
|
||||
setOpen()
|
||||
handleClose()
|
||||
}}
|
||||
>
|
||||
{t("actions.modify")}
|
||||
{t('actions.modify')}
|
||||
</MenuItem>
|
||||
{!isDefault && <Divider />}
|
||||
{!isDefault && (
|
||||
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
|
||||
{isPersonal ? t("actions.delete") : t("actions.remove")}
|
||||
{isPersonal ? t('actions.delete') : t('actions.remove')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
@@ -407,5 +411,5 @@ function CalendarSelector({
|
||||
handleDeleteConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from "@linagora/twake-mui";
|
||||
import { useI18n } from "twake-i18n";
|
||||
DialogTitle
|
||||
} from '@linagora/twake-mui'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export function DeleteCalendarDialog({
|
||||
deletePopupOpen,
|
||||
@@ -15,39 +15,39 @@ export function DeleteCalendarDialog({
|
||||
calendars,
|
||||
id,
|
||||
isPersonal,
|
||||
handleDeleteConfirm,
|
||||
handleDeleteConfirm
|
||||
}: {
|
||||
deletePopupOpen: boolean;
|
||||
setDeletePopupOpen: (e: boolean) => void;
|
||||
calendars: Record<string, Calendar>;
|
||||
id: string;
|
||||
isPersonal: boolean;
|
||||
handleDeleteConfirm: () => void;
|
||||
deletePopupOpen: boolean
|
||||
setDeletePopupOpen: (e: boolean) => void
|
||||
calendars: Record<string, Calendar>
|
||||
id: string
|
||||
isPersonal: boolean
|
||||
handleDeleteConfirm: () => void
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<Dialog open={deletePopupOpen} onClose={() => setDeletePopupOpen(false)}>
|
||||
<DialogTitle>
|
||||
{t("calendar.delete.title", { name: calendars[id].name })}
|
||||
{t('calendar.delete.title', { name: calendars[id].name })}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{isPersonal
|
||||
? t("calendar.delete.personalWarning")
|
||||
: t("calendar.delete.sharedWarning")}
|
||||
? t('calendar.delete.personalWarning')
|
||||
: t('calendar.delete.sharedWarning')}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeletePopupOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDeleteConfirm} variant="contained">
|
||||
{isPersonal ? t("actions.delete") : t("actions.remove")}
|
||||
{isPersonal ? t('actions.delete') : t('actions.remove')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector } from "@/app/hooks";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -8,72 +8,72 @@ import {
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@linagora/twake-mui";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { CalendarItemList } from "./CalendarItemList";
|
||||
import { SettingsTab } from "./SettingsTab";
|
||||
Typography
|
||||
} from '@linagora/twake-mui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { CalendarItemList } from './CalendarItemList'
|
||||
import { SettingsTab } from './SettingsTab'
|
||||
|
||||
export function ImportTab({
|
||||
userId,
|
||||
importTarget,
|
||||
setImportTarget,
|
||||
setImportedContent,
|
||||
newCalParams,
|
||||
newCalParams
|
||||
}: {
|
||||
userId: string;
|
||||
importTarget: string;
|
||||
setImportTarget: (target: string) => void;
|
||||
setImportedContent: (content: File | null) => void;
|
||||
userId: string
|
||||
importTarget: string
|
||||
setImportTarget: (target: string) => void
|
||||
setImportedContent: (content: File | null) => void
|
||||
newCalParams: {
|
||||
name: string;
|
||||
setName: (name: string) => void;
|
||||
description: string;
|
||||
setDescription: (d: string) => void;
|
||||
color: Record<string, string>;
|
||||
setColor: (color: Record<string, string>) => void;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: (visibility: "public" | "private") => void;
|
||||
};
|
||||
name: string
|
||||
setName: (name: string) => void
|
||||
description: string
|
||||
setDescription: (d: string) => void
|
||||
color: Record<string, string>
|
||||
setColor: (color: Record<string, string>) => void
|
||||
visibility: 'public' | 'private'
|
||||
setVisibility: (visibility: 'public' | 'private') => void
|
||||
}
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [importMode] = useState<"file" | "url">("file");
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importUrl, setImportUrl] = useState("");
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const { t } = useI18n()
|
||||
const [importMode] = useState<'file' | 'url'>('file')
|
||||
const [importFile, setImportFile] = useState<File | null>(null)
|
||||
const [importUrl, setImportUrl] = useState('')
|
||||
const calendars = useAppSelector(state => state.calendars.list)
|
||||
const personalCalendars = Object.values(calendars).filter(
|
||||
(cal) => extractEventBaseUuid(cal.id) === userId
|
||||
);
|
||||
cal => extractEventBaseUuid(cal.id) === userId
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setImportedContent(importMode === "file" ? importFile : null);
|
||||
}, [importFile, importUrl, importMode, setImportedContent]);
|
||||
setImportedContent(importMode === 'file' ? importFile : null)
|
||||
}, [importFile, importUrl, importMode, setImportedContent])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Form group 1: Select file button - first group, margin top 0 */}
|
||||
{importMode === "file" && (
|
||||
{importMode === 'file' && (
|
||||
<Box mt={0}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
size="medium"
|
||||
sx={{ borderRadius: "12px" }}
|
||||
sx={{ borderRadius: '12px' }}
|
||||
>
|
||||
{t("common.select_file")}
|
||||
{t('common.select_file')}
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept=".ics"
|
||||
onChange={(e) => setImportFile(e.target.files?.[0] ?? null)}
|
||||
onChange={e => setImportFile(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</Button>
|
||||
{importFile && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ marginTop: "6px" }}
|
||||
sx={{ marginTop: '6px' }}
|
||||
>
|
||||
{importFile.name}
|
||||
</Typography>
|
||||
@@ -82,28 +82,28 @@ export function ImportTab({
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
display="block"
|
||||
sx={{ marginTop: "6px" }}
|
||||
sx={{ marginTop: '6px' }}
|
||||
>
|
||||
{t("calendar.import_file_description")}
|
||||
{t('calendar.import_file_description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Form group 2: URL field */}
|
||||
{importMode === "url" && (
|
||||
{importMode === 'url' && (
|
||||
<Box mt={0}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t("calendar.ics_feed_url")}
|
||||
label={t('calendar.ics_feed_url')}
|
||||
value={importUrl}
|
||||
onChange={(e) => setImportUrl(e.target.value)}
|
||||
onChange={e => setImportUrl(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
sx={{
|
||||
"&.MuiFormControl-root.MuiFormControl-marginDense": {
|
||||
marginTop: "6px",
|
||||
marginBottom: 0,
|
||||
},
|
||||
'&.MuiFormControl-root.MuiFormControl-marginDense': {
|
||||
marginTop: '6px',
|
||||
marginBottom: 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -116,33 +116,33 @@ export function ImportTab({
|
||||
size="small"
|
||||
margin="dense"
|
||||
sx={{
|
||||
"&.MuiFormControl-root.MuiFormControl-marginDense": {
|
||||
marginTop: "6px",
|
||||
marginBottom: 0,
|
||||
},
|
||||
'&.MuiFormControl-root.MuiFormControl-marginDense': {
|
||||
marginTop: '6px',
|
||||
marginBottom: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputLabel id="import-to-label">
|
||||
{t("calendar.import_to")}
|
||||
{t('calendar.import_to')}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="import-to-label"
|
||||
label={t("calendar.import_to")}
|
||||
label={t('calendar.import_to')}
|
||||
value={importTarget}
|
||||
onChange={(e) => setImportTarget(e.target.value)}
|
||||
onChange={e => setImportTarget(e.target.value)}
|
||||
>
|
||||
<MenuItem value="new">{t("calendar.new_calendar")}</MenuItem>
|
||||
<MenuItem value="new">{t('calendar.new_calendar')}</MenuItem>
|
||||
{CalendarItemList(personalCalendars)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{/* Form group 4: SettingsTab (when importing to new calendar) */}
|
||||
{importTarget === "new" && (
|
||||
{importTarget === 'new' && (
|
||||
<Box mt={2}>
|
||||
<SettingsTab {...newCalParams} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,126 @@
|
||||
import { useAppDispatch } from "@/app/hooks";
|
||||
import { setView } from "@/features/Settings/SettingsSlice";
|
||||
import { computeStartOfTheWeek } from "@/utils/dateUtils";
|
||||
import type { CalendarApi } from "@fullcalendar/core";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { DateCalendar } from "@mui/x-date-pickers";
|
||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { useAppDispatch } from '@/app/hooks'
|
||||
import { setView } from '@/features/Settings/SettingsSlice'
|
||||
import { computeStartOfTheWeek } from '@/utils/dateUtils'
|
||||
import type { CalendarApi } from '@fullcalendar/core'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { DateCalendar } from '@mui/x-date-pickers'
|
||||
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export function MiniCalendar({
|
||||
calendarRef,
|
||||
selectedDate,
|
||||
setSelectedMiniDate,
|
||||
setSelectedMiniDate
|
||||
}: {
|
||||
calendarRef: React.MutableRefObject<CalendarApi | null>;
|
||||
selectedDate: Date;
|
||||
setSelectedMiniDate: (d: Date) => void;
|
||||
calendarRef: React.MutableRefObject<CalendarApi | null>
|
||||
selectedDate: Date
|
||||
setSelectedMiniDate: (d: Date) => void
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [visibleDate, setVisibleDate] = useState(selectedDate);
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch()
|
||||
const [visibleDate, setVisibleDate] = useState(selectedDate)
|
||||
const { t } = useI18n()
|
||||
|
||||
useEffect(() => setVisibleDate(selectedDate), [selectedDate]);
|
||||
useEffect(() => {
|
||||
const handleVisibleDateChange = () => {
|
||||
setVisibleDate(selectedDate)
|
||||
}
|
||||
handleVisibleDateChange()
|
||||
}, [selectedDate])
|
||||
return (
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterMoment}
|
||||
adapterLocale={t("locale") ?? "en-gb"}
|
||||
adapterLocale={t('locale') ?? 'en-gb'}
|
||||
>
|
||||
<DateCalendar
|
||||
value={moment(visibleDate)}
|
||||
onChange={async (dateMoment, selectionState) => {
|
||||
if (!dateMoment) return;
|
||||
const date = dateMoment.toDate();
|
||||
if (selectionState === "finish") {
|
||||
await dispatch(setView("calendar"));
|
||||
setSelectedMiniDate(date);
|
||||
calendarRef.current?.gotoDate(date);
|
||||
if (!dateMoment) return
|
||||
const date = dateMoment.toDate()
|
||||
if (selectionState === 'finish') {
|
||||
await dispatch(setView('calendar'))
|
||||
setSelectedMiniDate(date)
|
||||
calendarRef.current?.gotoDate(date)
|
||||
}
|
||||
}}
|
||||
showDaysOutsideCurrentMonth
|
||||
onMonthChange={(month) => {
|
||||
setVisibleDate(month.toDate());
|
||||
onMonthChange={month => {
|
||||
setVisibleDate(month.toDate())
|
||||
}}
|
||||
views={["month", "day"]}
|
||||
views={['month', 'day']}
|
||||
slots={{
|
||||
switchViewIcon: KeyboardArrowDownIcon,
|
||||
switchViewIcon: KeyboardArrowDownIcon
|
||||
}}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "300px",
|
||||
"& .MuiPickersCalendarHeader-root": {
|
||||
marginTop: 3,
|
||||
},
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
'& .MuiPickersCalendarHeader-root': {
|
||||
marginTop: 3
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
day: (ownerState) => {
|
||||
const date = ownerState.day.toDate();
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const selected = new Date(selectedDate);
|
||||
selected.setHours(0, 0, 0, 0);
|
||||
day: ownerState => {
|
||||
const date = ownerState.day.toDate()
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const selected = new Date(selectedDate)
|
||||
selected.setHours(0, 0, 0, 0)
|
||||
|
||||
const isToday = date.getTime() === today.getTime();
|
||||
const isToday = date.getTime() === today.getTime()
|
||||
const isSelectedDay =
|
||||
calendarRef.current?.view.type === "timeGridDay" &&
|
||||
date.getTime() === selected.getTime();
|
||||
calendarRef.current?.view.type === 'timeGridDay' &&
|
||||
date.getTime() === selected.getTime()
|
||||
|
||||
const isInSelectedWeek =
|
||||
calendarRef.current?.view.type === "timeGridWeek" ||
|
||||
calendarRef.current?.view.type === 'timeGridWeek' ||
|
||||
calendarRef.current?.view.type === undefined
|
||||
? (() => {
|
||||
const startOfWeek = computeStartOfTheWeek(selected);
|
||||
const endOfWeek = new Date(startOfWeek);
|
||||
endOfWeek.setDate(startOfWeek.getDate() + 6);
|
||||
endOfWeek.setHours(23, 59, 59, 999);
|
||||
return date >= startOfWeek && date <= endOfWeek;
|
||||
const startOfWeek = computeStartOfTheWeek(selected)
|
||||
const endOfWeek = new Date(startOfWeek)
|
||||
endOfWeek.setDate(startOfWeek.getDate() + 6)
|
||||
endOfWeek.setHours(23, 59, 59, 999)
|
||||
return date >= startOfWeek && date <= endOfWeek
|
||||
})()
|
||||
: false;
|
||||
: false
|
||||
|
||||
const classNames = [
|
||||
isToday ? "today" : "",
|
||||
isSelectedDay ? "selectedDay" : "",
|
||||
isInSelectedWeek ? "selectedWeek" : "",
|
||||
].join(" ");
|
||||
isToday ? 'today' : '',
|
||||
isSelectedDay ? 'selectedDay' : '',
|
||||
isInSelectedWeek ? 'selectedWeek' : ''
|
||||
].join(' ')
|
||||
|
||||
return {
|
||||
className: classNames,
|
||||
selected: classNames.includes("selectedWeek"),
|
||||
selected: classNames.includes('selectedWeek'),
|
||||
outsideCurrentMonth: ownerState.isDayOutsideMonth,
|
||||
disableMargin: false,
|
||||
style: {
|
||||
backgroundColor: "transparent",
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
border: 0,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
flexDirection: 'column',
|
||||
border: 0
|
||||
},
|
||||
sx: {
|
||||
"&.Mui-selected": {
|
||||
color: "inherit !important",
|
||||
fontWeight: "inherit !important",
|
||||
'&.Mui-selected': {
|
||||
color: 'inherit !important',
|
||||
fontWeight: 'inherit !important'
|
||||
},
|
||||
"&.selectedDay": {
|
||||
backgroundColor: "lightgray !important",
|
||||
},
|
||||
"&.today": {
|
||||
background: "orange !important",
|
||||
color: "white !important",
|
||||
'&.selectedDay': {
|
||||
backgroundColor: 'lightgray !important'
|
||||
},
|
||||
'&.today': {
|
||||
background: 'orange !important',
|
||||
color: 'white !important'
|
||||
}
|
||||
},
|
||||
"data-testid": `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`,
|
||||
children: <>{ownerState.day.date()}</>,
|
||||
};
|
||||
},
|
||||
'data-testid': `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`,
|
||||
children: <>{ownerState.day.date()}</>
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { Typography } from "@linagora/twake-mui";
|
||||
import { Typography } from '@linagora/twake-mui'
|
||||
|
||||
export function OwnerCaption({
|
||||
showCaption,
|
||||
ownerDisplayName,
|
||||
ownerDisplayName
|
||||
}: {
|
||||
showCaption: boolean;
|
||||
ownerDisplayName: string;
|
||||
showCaption: boolean
|
||||
ownerDisplayName: string
|
||||
}) {
|
||||
return (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-word",
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{showCaption && ownerDisplayName}
|
||||
</Typography>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Avatar, Box, Typography } from "@linagora/twake-mui";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { stringAvatar } from "../Event/utils/eventUtils";
|
||||
import { UserWithAccess } from "./CalendarAccessRights";
|
||||
import { Avatar, Box, Typography } from '@linagora/twake-mui'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { stringAvatar } from '../Event/utils/eventUtils'
|
||||
import { UserWithAccess } from './CalendarAccessRights'
|
||||
|
||||
interface ResourceAdminProps {
|
||||
admin: UserWithAccess;
|
||||
admin: UserWithAccess
|
||||
}
|
||||
|
||||
export function ResourceAdmin({ admin }: ResourceAdminProps) {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -18,14 +18,14 @@ export function ResourceAdmin({ admin }: ResourceAdminProps) {
|
||||
px={1}
|
||||
py={0.5}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
"&:hover": { backgroundColor: "action.hover" },
|
||||
borderRadius: '8px',
|
||||
'&:hover': { backgroundColor: 'action.hover' }
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1.5} minWidth={0}>
|
||||
<Avatar
|
||||
{...stringAvatar(admin.displayName)}
|
||||
sx={{ width: 28, height: 28, fontSize: "0.875rem" }}
|
||||
sx={{ width: 28, height: 28, fontSize: '0.875rem' }}
|
||||
/>
|
||||
<Box minWidth={0} display="flex" flexDirection="column" gap={0}>
|
||||
<Typography noWrap>{admin.displayName}</Typography>
|
||||
@@ -37,9 +37,9 @@ export function ResourceAdmin({ admin }: ResourceAdminProps) {
|
||||
|
||||
<Box display="flex" alignItems="center" gap={0.5} flexShrink={0}>
|
||||
<Typography variant="caption">
|
||||
{t("calendarPopover.access.administrator")}
|
||||
{t('calendarPopover.access.administrator')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useAppSelector } from "@/app/hooks";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { useAppSelector } from '@/app/hooks'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@linagora/twake-mui";
|
||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import PublicIcon from "@mui/icons-material/Public";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { AddDescButton } from "../Event/AddDescButton";
|
||||
import { ColorPicker } from "./CalendarColorPicker";
|
||||
import { InfoRow } from "../Event/InfoRow";
|
||||
useTheme
|
||||
} from '@linagora/twake-mui'
|
||||
import LockOutlineIcon from '@mui/icons-material/LockOutline'
|
||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined'
|
||||
import { alpha } from '@mui/material/styles'
|
||||
import PublicIcon from '@mui/icons-material/Public'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { AddDescButton } from '../Event/AddDescButton'
|
||||
import { ColorPicker } from './CalendarColorPicker'
|
||||
import { InfoRow } from '../Event/InfoRow'
|
||||
|
||||
export function SettingsTab({
|
||||
name,
|
||||
@@ -28,35 +28,37 @@ export function SettingsTab({
|
||||
setColor,
|
||||
visibility,
|
||||
setVisibility,
|
||||
calendar,
|
||||
calendar
|
||||
}: {
|
||||
name: string;
|
||||
setName: (name: string) => void;
|
||||
description: string;
|
||||
setDescription: (d: string) => void;
|
||||
color: Record<string, string>;
|
||||
setColor: (color: Record<string, string>) => void;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: (visibility: "public" | "private") => void;
|
||||
calendar?: Calendar;
|
||||
name: string
|
||||
setName: (name: string) => void
|
||||
description: string
|
||||
setDescription: (d: string) => void
|
||||
color: Record<string, string>
|
||||
setColor: (color: Record<string, string>) => void
|
||||
visibility: 'public' | 'private'
|
||||
setVisibility: (visibility: 'public' | 'private') => void
|
||||
calendar?: Calendar
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [toggleDesc, setToggleDesc] = useState(Boolean(description));
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const isOwn = calendar ? extractEventBaseUuid(calendar.id) === userId : true;
|
||||
const theme = useTheme();
|
||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9);
|
||||
const infoIconSx = { minWidth: "25px", marginRight: 2, color: infoIconColor };
|
||||
const { t } = useI18n()
|
||||
const [toggleDesc, setToggleDesc] = useState(Boolean(description))
|
||||
const userId = useAppSelector(state => state.user.userData?.openpaasId) ?? ''
|
||||
const isOwn = calendar ? extractEventBaseUuid(calendar.id) === userId : true
|
||||
const theme = useTheme()
|
||||
const infoIconColor = alpha(theme.palette.grey[900], 0.9)
|
||||
const infoIconSx = { minWidth: '25px', marginRight: 2, color: infoIconColor }
|
||||
|
||||
const isResource = useMemo(
|
||||
() => calendar?.owner?.resource,
|
||||
[calendar?.owner?.resource]
|
||||
);
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (description) setToggleDesc(true);
|
||||
}, [description]);
|
||||
const handleToggleDesc = () => {
|
||||
if (description) setToggleDesc(true)
|
||||
}
|
||||
handleToggleDesc()
|
||||
}, [description])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,15 +66,15 @@ export function SettingsTab({
|
||||
<Box mt={0}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ margin: 0, marginBottom: isResource ? "16px" : 0 }}
|
||||
sx={{ margin: 0, marginBottom: isResource ? '16px' : 0 }}
|
||||
>
|
||||
{t(
|
||||
isResource
|
||||
? "calendarPopover.settings.resourceName"
|
||||
: "calendarPopover.settings.calendarName"
|
||||
? 'calendarPopover.settings.resourceName'
|
||||
: 'calendarPopover.settings.calendarName'
|
||||
)}
|
||||
</Typography>
|
||||
<Box sx={{ marginTop: "6px" }}>
|
||||
<Box sx={{ marginTop: '6px' }}>
|
||||
{isResource ? (
|
||||
<InfoRow
|
||||
alignItems="flex-start"
|
||||
@@ -83,24 +85,24 @@ export function SettingsTab({
|
||||
}
|
||||
text={name}
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: '16px',
|
||||
fontFamily: "'Inter', sans-serif"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
label=""
|
||||
inputProps={{ "aria-label": t("common.name") }}
|
||||
placeholder={t("common.name")}
|
||||
inputProps={{ 'aria-label': t('common.name') }}
|
||||
placeholder={t('common.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onChange={e => setName(e.target.value)}
|
||||
size="small"
|
||||
sx={{
|
||||
"&.MuiFormControl-root": {
|
||||
'&.MuiFormControl-root': {
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
},
|
||||
marginBottom: 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -123,11 +125,11 @@ export function SettingsTab({
|
||||
{/* Form group 3: Color */}
|
||||
<Box mt={2}>
|
||||
<Typography variant="h6" sx={{ margin: 0 }}>
|
||||
{t("calendar.color")}
|
||||
{t('calendar.color')}
|
||||
</Typography>
|
||||
<Box sx={{ marginTop: "6px" }}>
|
||||
<Box sx={{ marginTop: '6px' }}>
|
||||
<ColorPicker
|
||||
onChange={(color) => setColor(color)}
|
||||
onChange={color => setColor(color)}
|
||||
selectedColor={color}
|
||||
/>
|
||||
</Box>
|
||||
@@ -137,29 +139,29 @@ export function SettingsTab({
|
||||
{isOwn && (
|
||||
<Box mt={2}>
|
||||
<Typography variant="h6" sx={{ margin: 0 }}>
|
||||
{t("calendar.newEventsVisibility")}
|
||||
{t('calendar.newEventsVisibility')}
|
||||
</Typography>
|
||||
<Box sx={{ marginTop: "6px" }}>
|
||||
<Box sx={{ marginTop: '6px' }}>
|
||||
<ToggleButtonGroup
|
||||
value={visibility}
|
||||
exclusive
|
||||
onChange={(e, val) => val && setVisibility(val)}
|
||||
size="medium"
|
||||
sx={{ borderRadius: "12px" }}
|
||||
sx={{ borderRadius: '12px' }}
|
||||
>
|
||||
<ToggleButton value="public" sx={{ width: "140px" }}>
|
||||
<ToggleButton value="public" sx={{ width: '140px' }}>
|
||||
<PublicIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t("common.all")}
|
||||
{t('common.all')}
|
||||
</ToggleButton>
|
||||
|
||||
<ToggleButton value="private" sx={{ width: "140px" }}>
|
||||
<ToggleButton value="private" sx={{ width: '140px' }}>
|
||||
<LockOutlineIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t("common.you")}
|
||||
{t('common.you')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,156 +1,153 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/hooks";
|
||||
import { removeTempCal } from "@/features/Calendars/CalendarSlice";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { getTempCalendarsListAsync } from "@/features/Calendars/services";
|
||||
import { setView } from "@/features/Settings/SettingsSlice";
|
||||
import { defaultColors } from "@/utils/defaultColors";
|
||||
import { TextField } from "@linagora/twake-mui";
|
||||
import { useRef } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { PeopleSearch, User } from "../Attendees/PeopleSearch";
|
||||
import { useAppDispatch, useAppSelector } from '@/app/hooks'
|
||||
import { removeTempCal } from '@/features/Calendars/CalendarSlice'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { getTempCalendarsListAsync } from '@/features/Calendars/services'
|
||||
import { setView } from '@/features/Settings/SettingsSlice'
|
||||
import { defaultColors } from '@/utils/defaultColors'
|
||||
import { TextField } from '@linagora/twake-mui'
|
||||
import { useRef } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { PeopleSearch, User } from '../Attendees/PeopleSearch'
|
||||
|
||||
const requestControllers = new Map<string, AbortController>();
|
||||
const requestControllers = new Map<string, AbortController>()
|
||||
|
||||
export function TempCalendarsInput({
|
||||
tempUsers,
|
||||
setTempUsers,
|
||||
handleToggleEventPreview,
|
||||
handleToggleEventPreview
|
||||
}: {
|
||||
tempUsers: User[];
|
||||
setTempUsers: (users: User[]) => void;
|
||||
handleToggleEventPreview: () => void;
|
||||
tempUsers: User[]
|
||||
setTempUsers: (users: User[]) => void
|
||||
handleToggleEventPreview: () => void
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const tempcalendars =
|
||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch()
|
||||
const tempcalendars = useAppSelector(state => state.calendars.templist) ?? {}
|
||||
const { t } = useI18n()
|
||||
|
||||
const prevUsersRef = useRef<User[]>([]);
|
||||
const prevUsersRef = useRef<User[]>([])
|
||||
const userColorsRef = useRef(
|
||||
new Map<string, { light: string; dark: string }>()
|
||||
);
|
||||
)
|
||||
|
||||
const handleUserChange = async (_: React.SyntheticEvent, users: User[]) => {
|
||||
setTempUsers(users);
|
||||
const handleUserChange = (_: React.SyntheticEvent, users: User[]) => {
|
||||
setTempUsers(users)
|
||||
|
||||
const prevUsers = prevUsersRef.current;
|
||||
const prevUsers = prevUsersRef.current
|
||||
|
||||
const addedUsers = users.filter(
|
||||
(u) => !prevUsers.some((p) => p.email === u.email)
|
||||
);
|
||||
u => !prevUsers.some(p => p.email === u.email)
|
||||
)
|
||||
const removedUsers = prevUsers.filter(
|
||||
(p) => !users.some((u) => u.email === p.email)
|
||||
);
|
||||
p => !users.some(u => u.email === p.email)
|
||||
)
|
||||
|
||||
prevUsersRef.current = users;
|
||||
prevUsersRef.current = users
|
||||
|
||||
if (addedUsers.length > 0) {
|
||||
dispatch(setView("calendar"));
|
||||
dispatch(setView('calendar'))
|
||||
for (const user of addedUsers) {
|
||||
const controller = new AbortController();
|
||||
requestControllers.set(user.email, controller);
|
||||
const controller = new AbortController()
|
||||
requestControllers.set(user.email, controller)
|
||||
|
||||
if (!userColorsRef.current.has(user.email)) {
|
||||
const usedLights = Array.from(userColorsRef.current.values()).map(
|
||||
(c) => c.light
|
||||
);
|
||||
const colorPair = generateDistinctColor(usedLights);
|
||||
userColorsRef.current.set(user.email, colorPair);
|
||||
c => c.light
|
||||
)
|
||||
const colorPair = generateDistinctColor(usedLights)
|
||||
userColorsRef.current.set(user.email, colorPair)
|
||||
}
|
||||
|
||||
user.color = userColorsRef.current.get(user.email)!;
|
||||
dispatch(
|
||||
getTempCalendarsListAsync(user, { signal: controller.signal })
|
||||
);
|
||||
user.color = userColorsRef.current.get(user.email) ?? defaultColors[0]
|
||||
dispatch(getTempCalendarsListAsync(user, { signal: controller.signal }))
|
||||
}
|
||||
}
|
||||
|
||||
for (const user of removedUsers) {
|
||||
const controller = requestControllers.get(user.email);
|
||||
const controller = requestControllers.get(user.email)
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
requestControllers.delete(user.email);
|
||||
controller.abort()
|
||||
requestControllers.delete(user.email)
|
||||
}
|
||||
|
||||
const calIds = buildEmailToCalendarMap(tempcalendars).get(user.email);
|
||||
calIds?.forEach((id) => dispatch(removeTempCal(id)));
|
||||
userColorsRef.current.delete(user.email);
|
||||
const calIds = buildEmailToCalendarMap(tempcalendars).get(user.email)
|
||||
calIds?.forEach(id => dispatch(removeTempCal(id)))
|
||||
userColorsRef.current.delete(user.email)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<PeopleSearch
|
||||
objectTypes={["user", "resource"]}
|
||||
objectTypes={['user', 'resource']}
|
||||
selectedUsers={tempUsers}
|
||||
onChange={handleUserChange}
|
||||
onToggleEventPreview={handleToggleEventPreview}
|
||||
placeholder={t("peopleSearch.availabilityPlaceholder")}
|
||||
inputSlot={(params) => (
|
||||
placeholder={t('peopleSearch.availabilityPlaceholder')}
|
||||
inputSlot={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
size="small"
|
||||
sx={
|
||||
tempUsers.length > 0
|
||||
? {
|
||||
"& .MuiOutlinedInput-root": {
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
"& .MuiInputBase-input": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'start',
|
||||
'& .MuiInputBase-input': {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function buildEmailToCalendarMap(calRecord: Record<string, Calendar>) {
|
||||
const map = new Map<string, string[]>();
|
||||
const map = new Map<string, string[]>()
|
||||
for (const [id, cal] of Object.entries(calRecord)) {
|
||||
cal.owner?.emails?.forEach((email) => {
|
||||
const existing = map.get(email);
|
||||
cal.owner?.emails?.forEach(email => {
|
||||
const existing = map.get(email)
|
||||
if (existing) {
|
||||
existing.push(id);
|
||||
existing.push(id)
|
||||
} else {
|
||||
map.set(email, [id]);
|
||||
map.set(email, [id])
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
return map;
|
||||
return map
|
||||
}
|
||||
|
||||
function shiftLightness(hex: string, amount: number): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
|
||||
const clamp = (v: number) => Math.max(0, Math.min(255, v));
|
||||
const clamp = (v: number) => Math.max(0, Math.min(255, v))
|
||||
const toHex = (v: number) =>
|
||||
clamp(v + amount)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
.padStart(2, '0')
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function generateDistinctColor(usedLights: string[]): {
|
||||
light: string;
|
||||
dark: string;
|
||||
light: string
|
||||
dark: string
|
||||
} {
|
||||
for (const color of defaultColors) {
|
||||
if (!usedLights.includes(color.light)) return color;
|
||||
if (!usedLights.includes(color.light)) return color
|
||||
}
|
||||
|
||||
const cycle = usedLights.length % defaultColors.length;
|
||||
const round = Math.floor(usedLights.length / defaultColors.length);
|
||||
const base = defaultColors[cycle];
|
||||
const cycle = usedLights.length % defaultColors.length
|
||||
const round = Math.floor(usedLights.length / defaultColors.length)
|
||||
const base = defaultColors[cycle]
|
||||
|
||||
return {
|
||||
light: shiftLightness(base.light, round * 12),
|
||||
dark: shiftLightness(base.dark, -(round * 10)),
|
||||
};
|
||||
dark: shiftLightness(base.dark, -(round * 10))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import {
|
||||
browserDefaultTimeZone,
|
||||
getTimezoneOffset,
|
||||
resolveTimezone,
|
||||
} from "@/utils/timezone";
|
||||
import { TIMEZONES } from "@/utils/timezone-data";
|
||||
import { Button, Popover } from "@linagora/twake-mui";
|
||||
import { MouseEvent, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
|
||||
resolveTimezone
|
||||
} from '@/utils/timezone'
|
||||
import { TIMEZONES } from '@/utils/timezone-data'
|
||||
import { Button, Popover } from '@linagora/twake-mui'
|
||||
import { MouseEvent, useMemo, useRef, useState } from 'react'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
import { TimezoneAutocomplete } from '../Timezone/TimezoneAutocomplete'
|
||||
|
||||
interface TimezoneSelectProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
referenceDate: Date;
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
referenceDate: Date
|
||||
}
|
||||
|
||||
export function TimezoneSelector({
|
||||
value,
|
||||
onChange,
|
||||
referenceDate,
|
||||
referenceDate
|
||||
}: TimezoneSelectProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const timezoneList = useTimeZoneList();
|
||||
const timezoneList = useTimeZoneList()
|
||||
|
||||
const effectiveTimezone = value
|
||||
? resolveTimezone(value)
|
||||
: timezoneList.browserTz;
|
||||
const selectedOffset = getTimezoneOffset(effectiveTimezone, referenceDate);
|
||||
: timezoneList.browserTz
|
||||
const selectedOffset = getTimezoneOffset(effectiveTimezone, referenceDate)
|
||||
|
||||
const handleOpen = (event: MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const { t } = useI18n();
|
||||
const open = Boolean(anchorEl)
|
||||
const { t } = useI18n()
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@@ -47,14 +47,14 @@ export function TimezoneSelector({
|
||||
size="small"
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
minWidth: "auto",
|
||||
padding: "2px 4px",
|
||||
textTransform: 'none',
|
||||
minWidth: 'auto',
|
||||
padding: '2px 4px',
|
||||
margin: 0,
|
||||
lineHeight: 1.2,
|
||||
lineHeight: 1.2
|
||||
}}
|
||||
>
|
||||
{selectedOffset || t("common.select_timezone")}
|
||||
{selectedOffset || t('common.select_timezone')}
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
@@ -62,22 +62,22 @@ export function TimezoneSelector({
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: { width: 280, maxHeight: 400, overflow: "hidden", p: 0 },
|
||||
sx: { width: 280, maxHeight: 400, overflow: 'hidden', p: 0 }
|
||||
},
|
||||
transition: {
|
||||
onEntered: () => {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
},
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TimezoneAutocomplete
|
||||
@@ -98,14 +98,14 @@ export function TimezoneSelector({
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function useTimeZoneList() {
|
||||
return useMemo(() => {
|
||||
const zones = Object.keys(TIMEZONES.zones).sort();
|
||||
const browserTz = resolveTimezone(browserDefaultTimeZone);
|
||||
const zones = Object.keys(TIMEZONES.zones).sort()
|
||||
const browserTz = resolveTimezone(browserDefaultTimeZone)
|
||||
|
||||
return { zones, browserTz, getTimezoneOffset };
|
||||
}, []);
|
||||
return { zones, browserTz, getTimezoneOffset }
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { User } from "@/components/Attendees/PeopleSearch";
|
||||
import { formatLocalDateTime } from "@/components/Event/utils/dateTimeFormatters";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import { User } from '@/components/Attendees/PeopleSearch'
|
||||
import { formatLocalDateTime } from '@/components/Event/utils/dateTimeFormatters'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import {
|
||||
getEventAsync,
|
||||
putEventAsync,
|
||||
updateEventInstanceAsync,
|
||||
updateSeriesAsync,
|
||||
} from "@/features/Calendars/services";
|
||||
import { getEvent } from "@/features/Events/EventApi";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange";
|
||||
updateSeriesAsync
|
||||
} from '@/features/Calendars/services'
|
||||
import { getEvent } from '@/features/Events/EventApi'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import { updateAttendeesAfterTimeChange } from '@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange'
|
||||
import {
|
||||
AttendeeOptions,
|
||||
createAttendee,
|
||||
} from "@/features/User/models/attendee.mapper";
|
||||
import { getDeltaInMilliseconds } from "@/utils/dateUtils";
|
||||
createAttendee
|
||||
} from '@/features/User/models/attendee.mapper'
|
||||
import { getDeltaInMilliseconds } from '@/utils/dateUtils'
|
||||
import {
|
||||
CalendarApi,
|
||||
DateSelectArg,
|
||||
EventClickArg,
|
||||
EventDropArg,
|
||||
} from "@fullcalendar/core";
|
||||
import { EventResizeDoneArg } from "@fullcalendar/interaction";
|
||||
EventDropArg
|
||||
} from '@fullcalendar/core'
|
||||
import { EventResizeDoneArg } from '@fullcalendar/interaction'
|
||||
|
||||
export interface EventHandlersProps {
|
||||
setSelectedRange: (range: DateSelectArg | null) => void;
|
||||
setAnchorEl: (el: HTMLElement | null) => void;
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
dispatch: AppDispatch;
|
||||
setOpenEventDisplay: (open: boolean) => void;
|
||||
setEventDisplayedId: (id: string) => void;
|
||||
setEventDisplayedCalId: (id: string) => void;
|
||||
setEventDisplayedTemp: (temp: boolean) => void;
|
||||
calendars: Record<string, Calendar>;
|
||||
setSelectedEvent: (event: CalendarEvent) => void;
|
||||
setSelectedRange: (range: DateSelectArg | null) => void
|
||||
setAnchorEl: (el: HTMLElement | null) => void
|
||||
calendarRef: React.RefObject<CalendarApi | null>
|
||||
dispatch: AppDispatch
|
||||
setOpenEventDisplay: (open: boolean) => void
|
||||
setEventDisplayedId: (id: string) => void
|
||||
setEventDisplayedCalId: (id: string) => void
|
||||
setEventDisplayedTemp: (temp: boolean) => void
|
||||
calendars: Record<string, Calendar>
|
||||
setSelectedEvent: (event: CalendarEvent) => void
|
||||
setAfterChoiceFunc: (
|
||||
func: ((type: "solo" | "all" | undefined) => void) | undefined
|
||||
) => void;
|
||||
setOpenEditModePopup: (open: string) => void;
|
||||
tempUsers: User[];
|
||||
setTempEvent: (event: CalendarEvent) => void;
|
||||
timezone: string;
|
||||
func: ((type: 'solo' | 'all' | undefined) => void) | undefined
|
||||
) => void
|
||||
setOpenEditModePopup: (open: string) => void
|
||||
tempUsers: User[]
|
||||
setTempEvent: (event: CalendarEvent) => void
|
||||
timezone: string
|
||||
}
|
||||
|
||||
export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
@@ -60,55 +60,55 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
setOpenEditModePopup,
|
||||
tempUsers,
|
||||
setTempEvent,
|
||||
timezone,
|
||||
} = props;
|
||||
timezone
|
||||
} = props
|
||||
|
||||
const handleDateSelect = (selectInfo: DateSelectArg) => {
|
||||
setSelectedRange(selectInfo);
|
||||
setSelectedRange(selectInfo)
|
||||
if (tempUsers) {
|
||||
const newEvent: CalendarEvent = {
|
||||
start: selectInfo?.start
|
||||
? formatLocalDateTime(selectInfo?.start, timezone)
|
||||
: "",
|
||||
: '',
|
||||
end: selectInfo?.end
|
||||
? formatLocalDateTime(selectInfo?.end, timezone)
|
||||
: "",
|
||||
attendee: tempUsers.map((user) => {
|
||||
: '',
|
||||
attendee: tempUsers.map(user => {
|
||||
const attendeeOption: AttendeeOptions = {
|
||||
cal_address: user.email,
|
||||
cn: user.displayName,
|
||||
rsvp: "TRUE",
|
||||
};
|
||||
|
||||
if (user.objectType === "resource") {
|
||||
attendeeOption.cutype = "RESOURCE";
|
||||
rsvp: 'TRUE'
|
||||
}
|
||||
return createAttendee(attendeeOption);
|
||||
}),
|
||||
} as CalendarEvent;
|
||||
|
||||
setTempEvent(newEvent);
|
||||
if (user.objectType === 'resource') {
|
||||
attendeeOption.cutype = 'RESOURCE'
|
||||
}
|
||||
return createAttendee(attendeeOption)
|
||||
})
|
||||
} as CalendarEvent
|
||||
|
||||
setTempEvent(newEvent)
|
||||
}
|
||||
setAnchorEl(document.body);
|
||||
};
|
||||
setAnchorEl(document.body)
|
||||
}
|
||||
|
||||
const handleClosePopover = () => {
|
||||
calendarRef.current?.unselect();
|
||||
setAnchorEl(null);
|
||||
setSelectedRange(null);
|
||||
};
|
||||
calendarRef.current?.unselect()
|
||||
setAnchorEl(null)
|
||||
setSelectedRange(null)
|
||||
}
|
||||
|
||||
const handleCloseEventDisplay = () => {
|
||||
setOpenEventDisplay(false);
|
||||
};
|
||||
setOpenEventDisplay(false)
|
||||
}
|
||||
|
||||
const handleEventClick = (info: EventClickArg) => {
|
||||
info.jsEvent.preventDefault();
|
||||
info.jsEvent.preventDefault()
|
||||
|
||||
if (info.event.url) {
|
||||
window.open(info.event.url);
|
||||
window.open(info.event.url)
|
||||
} else {
|
||||
setOpenEventDisplay(true);
|
||||
setOpenEventDisplay(true)
|
||||
if (
|
||||
calendars[info.event.extendedProps.calId] &&
|
||||
calendars[info.event.extendedProps.calId].events[
|
||||
@@ -121,57 +121,57 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
info.event.extendedProps.uid
|
||||
]
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
setEventDisplayedId(info.event.extendedProps.uid);
|
||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||
setEventDisplayedTemp(info.event._def.extendedProps.temp);
|
||||
setEventDisplayedId(info.event.extendedProps.uid)
|
||||
setEventDisplayedCalId(info.event.extendedProps.calId)
|
||||
setEventDisplayedTemp(info.event._def.extendedProps.temp)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEventAllow = () => {
|
||||
return true;
|
||||
};
|
||||
return true
|
||||
}
|
||||
|
||||
const handleEventDrop = async (arg: EventDropArg) => {
|
||||
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
const calendar = calendars[arg.event._def.extendedProps.calId];
|
||||
]
|
||||
const calendar = calendars[arg.event._def.extendedProps.calId]
|
||||
|
||||
const isRecurring = event.uid.includes("/");
|
||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
|
||||
const isRecurring = event.uid.includes('/')
|
||||
const totalDeltaMs = getDeltaInMilliseconds(arg.delta)
|
||||
|
||||
const originalStart = new Date(event.start);
|
||||
const computedNewStart = new Date(originalStart.getTime() + totalDeltaMs);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
const computedNewEnd = new Date(originalEnd.getTime() + totalDeltaMs);
|
||||
const originalStart = new Date(event.start)
|
||||
const computedNewStart = new Date(originalStart.getTime() + totalDeltaMs)
|
||||
const originalEnd = new Date(event.end ?? '')
|
||||
const computedNewEnd = new Date(originalEnd.getTime() + totalDeltaMs)
|
||||
const newEvent = updateAttendeesAfterTimeChange(
|
||||
{
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
sequence: (event.sequence ?? 1) + 1,
|
||||
sequence: (event.sequence ?? 1) + 1
|
||||
} as CalendarEvent,
|
||||
true
|
||||
);
|
||||
)
|
||||
if (isRecurring) {
|
||||
setSelectedEvent(event);
|
||||
setOpenEditModePopup("edit");
|
||||
setSelectedEvent(event)
|
||||
setOpenEditModePopup('edit')
|
||||
setAfterChoiceFunc(
|
||||
() => async (typeOfAction: "solo" | "all" | undefined) => {
|
||||
if (typeOfAction === "solo") {
|
||||
() => async (typeOfAction: 'solo' | 'all' | undefined) => {
|
||||
if (typeOfAction === 'solo') {
|
||||
await dispatch(
|
||||
updateEventInstanceAsync({ cal: calendar, event: newEvent })
|
||||
);
|
||||
} else if (typeOfAction === "all") {
|
||||
const master = await getEvent(newEvent, true);
|
||||
)
|
||||
} else if (typeOfAction === 'all') {
|
||||
const master = await getEvent(newEvent, true)
|
||||
await dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: calendar,
|
||||
@@ -179,61 +179,61 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
...master,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
sequence: (master.sequence ?? 1) + 1,
|
||||
},
|
||||
sequence: (master.sequence ?? 1) + 1
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
} else {
|
||||
await dispatch(
|
||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEventResize = async (arg: EventResizeDoneArg) => {
|
||||
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const event =
|
||||
calendars[arg.event._def.extendedProps.calId].events[
|
||||
arg.event._def.extendedProps.uid
|
||||
];
|
||||
const calendar = calendars[arg.event._def.extendedProps.calId];
|
||||
]
|
||||
const calendar = calendars[arg.event._def.extendedProps.calId]
|
||||
|
||||
const isRecurring = event.uid.includes("/");
|
||||
const isRecurring = event.uid.includes('/')
|
||||
|
||||
const originalStart = new Date(event.start);
|
||||
const originalStart = new Date(event.start)
|
||||
const computedNewStart = new Date(
|
||||
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
|
||||
);
|
||||
const originalEnd = new Date(event.end ?? "");
|
||||
)
|
||||
const originalEnd = new Date(event.end ?? '')
|
||||
const computedNewEnd = new Date(
|
||||
originalEnd.getTime() + getDeltaInMilliseconds(arg.endDelta)
|
||||
);
|
||||
)
|
||||
const newEvent = updateAttendeesAfterTimeChange(
|
||||
{
|
||||
...event,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
sequence: (event.sequence ?? 1) + 1,
|
||||
sequence: (event.sequence ?? 1) + 1
|
||||
} as CalendarEvent,
|
||||
true
|
||||
);
|
||||
)
|
||||
if (isRecurring) {
|
||||
setSelectedEvent(event);
|
||||
setOpenEditModePopup("edit");
|
||||
setSelectedEvent(event)
|
||||
setOpenEditModePopup('edit')
|
||||
setAfterChoiceFunc(
|
||||
() => async (typeOfAction: "solo" | "all" | undefined) => {
|
||||
if (typeOfAction === "solo") {
|
||||
() => async (typeOfAction: 'solo' | 'all' | undefined) => {
|
||||
if (typeOfAction === 'solo') {
|
||||
await dispatch(
|
||||
updateEventInstanceAsync({ cal: calendar, event: newEvent })
|
||||
);
|
||||
} else if (typeOfAction === "all") {
|
||||
const master = await getEvent(newEvent, true);
|
||||
)
|
||||
} else if (typeOfAction === 'all') {
|
||||
const master = await getEvent(newEvent, true)
|
||||
|
||||
await dispatch(
|
||||
updateSeriesAsync({
|
||||
@@ -242,19 +242,19 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
...master,
|
||||
start: computedNewStart.toISOString(),
|
||||
end: computedNewEnd.toISOString(),
|
||||
sequence: (master.sequence ?? 1) + 1,
|
||||
},
|
||||
sequence: (master.sequence ?? 1) + 1
|
||||
}
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
} else {
|
||||
await dispatch(
|
||||
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
handleDateSelect,
|
||||
@@ -263,6 +263,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
|
||||
handleEventClick,
|
||||
handleEventAllow,
|
||||
handleEventDrop,
|
||||
handleEventResize,
|
||||
};
|
||||
};
|
||||
handleEventResize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,108 @@
|
||||
interface CalendarHTMLElement extends HTMLElement {
|
||||
__calendarMouseMoveHandler?: (e: MouseEvent) => void;
|
||||
__calendarMouseLeaveHandler?: () => void;
|
||||
__calendarMouseMoveHandler?: (e: MouseEvent) => void
|
||||
__calendarMouseLeaveHandler?: () => void
|
||||
}
|
||||
export interface MouseHandlersProps {
|
||||
calendarEl: CalendarHTMLElement;
|
||||
calendarEl: CalendarHTMLElement
|
||||
}
|
||||
|
||||
export const createMouseHandlers = (props: MouseHandlersProps) => {
|
||||
const { calendarEl } = props;
|
||||
const { calendarEl } = props
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||
if (!timegridEl) return;
|
||||
const timegridEl = calendarEl.querySelector('.fc-timegrid-body')
|
||||
if (!timegridEl) return
|
||||
|
||||
const allDayTable = calendarEl.querySelector(".fc-scrollgrid-sync-table");
|
||||
const allDayTable = calendarEl.querySelector('.fc-scrollgrid-sync-table')
|
||||
if (allDayTable) {
|
||||
const allDayRect = allDayTable.getBoundingClientRect();
|
||||
const allDayRect = allDayTable.getBoundingClientRect()
|
||||
if (e.clientY >= allDayRect.top && e.clientY <= allDayRect.bottom) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
.querySelectorAll('.hour-highlight')
|
||||
.forEach((el: Element) => el.remove())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
if (target && target.closest(".fc-timegrid-slot-label")) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target && target.closest('.fc-timegrid-slot-label')) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
return;
|
||||
.querySelectorAll('.hour-highlight')
|
||||
.forEach((el: Element) => el.remove())
|
||||
return
|
||||
}
|
||||
|
||||
const dayColumns = timegridEl.querySelectorAll(".fc-timegrid-col");
|
||||
if (dayColumns.length === 0) return;
|
||||
const dayColumns = timegridEl.querySelectorAll('.fc-timegrid-col')
|
||||
if (dayColumns.length === 0) return
|
||||
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
.querySelectorAll('.hour-highlight')
|
||||
.forEach((el: Element) => el.remove())
|
||||
|
||||
let targetColumn: Element | null = null;
|
||||
let targetColumn: Element | null = null
|
||||
for (const column of dayColumns) {
|
||||
const rect = column.getBoundingClientRect();
|
||||
const rect = column.getBoundingClientRect()
|
||||
if (e.clientX >= rect.left && e.clientX <= rect.right) {
|
||||
targetColumn = column;
|
||||
break;
|
||||
targetColumn = column
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (targetColumn) {
|
||||
const rect = targetColumn.getBoundingClientRect();
|
||||
const relativeY = e.clientY - rect.top;
|
||||
const slotHeight = rect.height / 48;
|
||||
const slotIndex = Math.floor(relativeY / slotHeight);
|
||||
const rect = targetColumn.getBoundingClientRect()
|
||||
const relativeY = e.clientY - rect.top
|
||||
const slotHeight = rect.height / 48
|
||||
const slotIndex = Math.floor(relativeY / slotHeight)
|
||||
|
||||
if (relativeY >= 0 && relativeY <= rect.height) {
|
||||
const highlight = document.createElement("div");
|
||||
highlight.className = "hour-highlight";
|
||||
highlight.style.top = `${slotIndex * slotHeight}px`;
|
||||
highlight.style.height = `${slotHeight}px`;
|
||||
|
||||
(targetColumn as HTMLElement).style.position = "relative";
|
||||
targetColumn.appendChild(highlight);
|
||||
const highlight = document.createElement('div')
|
||||
highlight.className = 'hour-highlight'
|
||||
highlight.style.top = `${slotIndex * slotHeight}px`
|
||||
highlight.style.height = `${slotHeight}px`
|
||||
;(targetColumn as HTMLElement).style.position = 'relative'
|
||||
targetColumn.appendChild(highlight)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
const timegridEl = calendarEl.querySelector(".fc-timegrid-body");
|
||||
const timegridEl = calendarEl.querySelector('.fc-timegrid-body')
|
||||
if (timegridEl) {
|
||||
timegridEl
|
||||
.querySelectorAll(".hour-highlight")
|
||||
.forEach((el: Element) => el.remove());
|
||||
.querySelectorAll('.hour-highlight')
|
||||
.forEach((el: Element) => el.remove())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const addMouseEventListeners = () => {
|
||||
calendarEl.addEventListener("mousemove", handleMouseMove);
|
||||
calendarEl.addEventListener("mouseleave", handleMouseLeave);
|
||||
calendarEl.addEventListener('mousemove', handleMouseMove)
|
||||
calendarEl.addEventListener('mouseleave', handleMouseLeave)
|
||||
|
||||
calendarEl.__calendarMouseMoveHandler = handleMouseMove;
|
||||
calendarEl.__calendarMouseLeaveHandler = handleMouseLeave;
|
||||
};
|
||||
calendarEl.__calendarMouseMoveHandler = handleMouseMove
|
||||
calendarEl.__calendarMouseLeaveHandler = handleMouseLeave
|
||||
}
|
||||
|
||||
const removeMouseEventListeners = () => {
|
||||
if (calendarEl.__calendarMouseMoveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mousemove",
|
||||
'mousemove',
|
||||
calendarEl.__calendarMouseMoveHandler
|
||||
);
|
||||
delete calendarEl.__calendarMouseMoveHandler;
|
||||
)
|
||||
delete calendarEl.__calendarMouseMoveHandler
|
||||
}
|
||||
if (calendarEl.__calendarMouseLeaveHandler) {
|
||||
calendarEl.removeEventListener(
|
||||
"mouseleave",
|
||||
'mouseleave',
|
||||
calendarEl.__calendarMouseLeaveHandler
|
||||
);
|
||||
delete calendarEl.__calendarMouseLeaveHandler;
|
||||
)
|
||||
delete calendarEl.__calendarMouseLeaveHandler
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
handleMouseMove,
|
||||
handleMouseLeave,
|
||||
addMouseEventListeners,
|
||||
removeMouseEventListeners,
|
||||
};
|
||||
};
|
||||
removeMouseEventListeners
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { EventErrorHandler } from "@/components/Error/EventErrorHandler";
|
||||
import { EventChip } from "@/components/Event/EventChip/EventChip";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { EventErrorHandler } from '@/components/Error/EventErrorHandler'
|
||||
import { EventChip } from '@/components/Event/EventChip/EventChip'
|
||||
import { Calendar } from '@/features/Calendars/CalendarTypes'
|
||||
import { userAttendee } from '@/features/User/models/attendee'
|
||||
import {
|
||||
CalendarApi,
|
||||
DayHeaderMountArg,
|
||||
EventContentArg,
|
||||
EventMountArg,
|
||||
NowIndicatorContentArg,
|
||||
ViewMountArg,
|
||||
} from "@fullcalendar/core";
|
||||
import React from "react";
|
||||
import { createMouseHandlers } from "./mouseHandlers";
|
||||
ViewMountArg
|
||||
} from '@fullcalendar/core'
|
||||
import React from 'react'
|
||||
import { createMouseHandlers } from './mouseHandlers'
|
||||
|
||||
export interface ViewHandlersProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
setSelectedDate: (date: Date) => void;
|
||||
setSelectedMiniDate: (date: Date) => void;
|
||||
onViewChange?: (view: string) => void;
|
||||
calendars: Record<string, Calendar>;
|
||||
tempcalendars: Record<string, Calendar>;
|
||||
errorHandler: EventErrorHandler;
|
||||
calendarRef: React.RefObject<CalendarApi | null>
|
||||
setSelectedDate: (date: Date) => void
|
||||
setSelectedMiniDate: (date: Date) => void
|
||||
onViewChange?: (view: string) => void
|
||||
calendars: Record<string, Calendar>
|
||||
tempcalendars: Record<string, Calendar>
|
||||
errorHandler: EventErrorHandler
|
||||
}
|
||||
|
||||
export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
@@ -31,126 +31,126 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
onViewChange,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
errorHandler,
|
||||
} = props;
|
||||
errorHandler
|
||||
} = props
|
||||
|
||||
const handleNowIndicatorContent = (arg: NowIndicatorContentArg) => {
|
||||
if (arg.isAxis) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ style: { display: "flex", alignItems: "center" } },
|
||||
'div',
|
||||
{ style: { display: 'flex', alignItems: 'center' } },
|
||||
React.createElement(
|
||||
"div",
|
||||
{ className: "now-time-label" },
|
||||
'div',
|
||||
{ className: 'now-time-label' },
|
||||
new Date().toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: arg.view.dateEnv.timeZone,
|
||||
timeZone: arg.view.dateEnv.timeZone
|
||||
})
|
||||
)
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDayHeaderDidMount = (arg: DayHeaderMountArg) => {
|
||||
if (arg.view.type === "timeGridWeek") {
|
||||
const headerEl = arg.el;
|
||||
if (arg.view.type === 'timeGridWeek') {
|
||||
const headerEl = arg.el
|
||||
|
||||
const handleDayHeaderClick = () => {
|
||||
calendarRef.current?.changeView("timeGridDay", arg.date);
|
||||
setSelectedDate(new Date(arg.date));
|
||||
setSelectedMiniDate(new Date(arg.date));
|
||||
calendarRef.current?.changeView('timeGridDay', arg.date)
|
||||
setSelectedDate(new Date(arg.date))
|
||||
setSelectedMiniDate(new Date(arg.date))
|
||||
|
||||
if (onViewChange) {
|
||||
onViewChange("timeGridDay");
|
||||
onViewChange('timeGridDay')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
headerEl.addEventListener("click", handleDayHeaderClick);
|
||||
headerEl.__dayHeaderClickHandler = handleDayHeaderClick;
|
||||
headerEl.addEventListener('click', handleDayHeaderClick)
|
||||
headerEl.__dayHeaderClickHandler = handleDayHeaderClick
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDayHeaderWillUnmount = (arg: DayHeaderMountArg) => {
|
||||
const headerEl = arg.el;
|
||||
const headerEl = arg.el
|
||||
if (headerEl.__dayHeaderClickHandler) {
|
||||
headerEl.removeEventListener("click", headerEl.__dayHeaderClickHandler);
|
||||
delete headerEl.__dayHeaderClickHandler;
|
||||
headerEl.removeEventListener('click', headerEl.__dayHeaderClickHandler)
|
||||
delete headerEl.__dayHeaderClickHandler
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleViewDidMount = (arg: ViewMountArg) => {
|
||||
if (arg.view.type === "timeGridWeek" || arg.view.type === "timeGridDay") {
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
if (arg.view.type === 'timeGridWeek' || arg.view.type === 'timeGridDay') {
|
||||
const calendarEl = document.querySelector('.fc') as HTMLElement
|
||||
if (calendarEl) {
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||
mouseHandlers.addMouseEventListeners();
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl })
|
||||
mouseHandlers.addMouseEventListeners()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleViewWillUnmount = (arg: ViewMountArg) => {
|
||||
if (arg.el.__timeInterval) {
|
||||
clearInterval(arg.el.__timeInterval);
|
||||
delete arg.el.__timeInterval;
|
||||
clearInterval(arg.el.__timeInterval)
|
||||
delete arg.el.__timeInterval
|
||||
}
|
||||
|
||||
if (arg.el.__timeObserver) {
|
||||
arg.el.__timeObserver.disconnect();
|
||||
delete arg.el.__timeObserver;
|
||||
arg.el.__timeObserver.disconnect()
|
||||
delete arg.el.__timeObserver
|
||||
}
|
||||
|
||||
const calendarEl = document.querySelector(".fc") as HTMLElement;
|
||||
const calendarEl = document.querySelector('.fc') as HTMLElement
|
||||
if (calendarEl) {
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl });
|
||||
mouseHandlers.removeMouseEventListeners();
|
||||
const mouseHandlers = createMouseHandlers({ calendarEl })
|
||||
mouseHandlers.removeMouseEventListeners()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleEventContent = (arg: EventContentArg) => {
|
||||
return React.createElement(EventChip, {
|
||||
arg,
|
||||
calendars,
|
||||
tempcalendars,
|
||||
errorHandler,
|
||||
});
|
||||
};
|
||||
errorHandler
|
||||
})
|
||||
}
|
||||
|
||||
const handleEventDidMount = (arg: EventMountArg) => {
|
||||
const attendees = arg.event._def.extendedProps.attendee || [];
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return;
|
||||
const attendees = arg.event._def.extendedProps.attendee || []
|
||||
if (!calendars[arg.event._def.extendedProps.calId]) return
|
||||
const ownerEmails = new Set(
|
||||
calendars[arg.event._def.extendedProps.calId].owner?.emails?.map(
|
||||
(email) => email.toLowerCase()
|
||||
calendars[arg.event._def.extendedProps.calId].owner?.emails?.map(email =>
|
||||
email.toLowerCase()
|
||||
)
|
||||
);
|
||||
)
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
);
|
||||
)
|
||||
|
||||
if (!showSpecialDisplay[0]) return;
|
||||
if (!showSpecialDisplay[0]) return
|
||||
|
||||
arg.el.classList.remove(
|
||||
"declined-event",
|
||||
"tentative-event",
|
||||
"needs-action-event"
|
||||
);
|
||||
'declined-event',
|
||||
'tentative-event',
|
||||
'needs-action-event'
|
||||
)
|
||||
|
||||
switch (showSpecialDisplay[0].partstat) {
|
||||
case "DECLINED":
|
||||
arg.el.classList.add("declined-event");
|
||||
break;
|
||||
case "TENTATIVE":
|
||||
arg.el.classList.add("tentative-event");
|
||||
break;
|
||||
case "NEEDS-ACTION":
|
||||
arg.el.classList.add("needs-action-event");
|
||||
break;
|
||||
case 'DECLINED':
|
||||
arg.el.classList.add('declined-event')
|
||||
break
|
||||
case 'TENTATIVE':
|
||||
arg.el.classList.add('tentative-event')
|
||||
break
|
||||
case 'NEEDS-ACTION':
|
||||
arg.el.classList.add('needs-action-event')
|
||||
break
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
handleNowIndicatorContent,
|
||||
@@ -159,6 +159,6 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
handleViewDidMount,
|
||||
handleViewWillUnmount,
|
||||
handleEventContent,
|
||||
handleEventDidMount,
|
||||
};
|
||||
};
|
||||
handleEventDidMount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback } from "react";
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
createEventHandlers,
|
||||
EventHandlersProps,
|
||||
} from "../handlers/eventHandlers";
|
||||
EventHandlersProps
|
||||
} from '../handlers/eventHandlers'
|
||||
|
||||
export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
||||
const eventHandlers = createEventHandlers(props);
|
||||
const eventHandlers = createEventHandlers(props)
|
||||
|
||||
return {
|
||||
handleDateSelect: useCallback(eventHandlers.handleDateSelect, [
|
||||
@@ -14,13 +14,13 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
||||
props.setAnchorEl,
|
||||
props.setTempEvent,
|
||||
props.tempUsers,
|
||||
props.timezone,
|
||||
props.timezone
|
||||
]),
|
||||
handleClosePopover: useCallback(eventHandlers.handleClosePopover, [
|
||||
props.calendarRef,
|
||||
props.setAnchorEl,
|
||||
props.setSelectedRange,
|
||||
props.dispatch,
|
||||
props.dispatch
|
||||
]),
|
||||
handleCloseEventDisplay: useCallback(
|
||||
eventHandlers.handleCloseEventDisplay,
|
||||
@@ -32,7 +32,7 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
||||
props.setEventDisplayedCalId,
|
||||
props.setEventDisplayedTemp,
|
||||
props.calendars,
|
||||
props.dispatch,
|
||||
props.dispatch
|
||||
]),
|
||||
handleEventAllow: useCallback(eventHandlers.handleEventAllow, []),
|
||||
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
|
||||
@@ -40,11 +40,11 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
||||
props.dispatch,
|
||||
props.setSelectedEvent,
|
||||
props.setOpenEditModePopup,
|
||||
props.setAfterChoiceFunc,
|
||||
props.setAfterChoiceFunc
|
||||
]),
|
||||
handleEventResize: useCallback(eventHandlers.handleEventResize, [
|
||||
props.calendars,
|
||||
props.dispatch,
|
||||
]),
|
||||
};
|
||||
};
|
||||
props.dispatch
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
createViewHandlers,
|
||||
ViewHandlersProps,
|
||||
} from "../handlers/viewHandlers";
|
||||
import { useCallback } from 'react'
|
||||
import { createViewHandlers, ViewHandlersProps } from '../handlers/viewHandlers'
|
||||
|
||||
export const useCalendarViewHandlers = (props: ViewHandlersProps) => {
|
||||
const viewHandlers = createViewHandlers(props);
|
||||
const viewHandlers = createViewHandlers(props)
|
||||
|
||||
return {
|
||||
handleDayHeaderDidMount: useCallback(viewHandlers.handleDayHeaderDidMount, [
|
||||
props.calendarRef,
|
||||
props.setSelectedDate,
|
||||
props.setSelectedMiniDate,
|
||||
props.onViewChange,
|
||||
props.onViewChange
|
||||
]),
|
||||
handleDayHeaderWillUnmount: useCallback(
|
||||
viewHandlers.handleDayHeaderWillUnmount,
|
||||
@@ -23,14 +20,14 @@ export const useCalendarViewHandlers = (props: ViewHandlersProps) => {
|
||||
handleViewWillUnmount: useCallback(viewHandlers.handleViewWillUnmount, []),
|
||||
handleEventContent: useCallback(viewHandlers.handleEventContent, [
|
||||
props.calendars,
|
||||
props.tempcalendars,
|
||||
props.tempcalendars
|
||||
]),
|
||||
handleEventDidMount: useCallback(viewHandlers.handleEventDidMount, [
|
||||
props.calendars,
|
||||
props.calendars
|
||||
]),
|
||||
handleNowIndicatorContent: useCallback(
|
||||
viewHandlers.handleNowIndicatorContent,
|
||||
[]
|
||||
),
|
||||
};
|
||||
};
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar, DelegationAccess } from "@/features/Calendars/CalendarTypes";
|
||||
import { getCalendarDetailAsync } from "@/features/Calendars/services";
|
||||
import { AclEntry } from "@/features/Calendars/types/CalendarData";
|
||||
import { CalendarEvent } from "@/features/Events/EventsTypes";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "@/utils/dateUtils";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { getEffectiveEmail } from "@/utils/getEffectiveEmail";
|
||||
import { isEventOrganiser } from "@/utils/isEventOrganiser";
|
||||
import { convertEventDateTimeToISO } from "@/utils/timezone";
|
||||
import { EventInput, SlotLabelContentArg } from "@fullcalendar/core";
|
||||
import moment from "moment-timezone";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { AppDispatch } from '@/app/store'
|
||||
import { Calendar, DelegationAccess } from '@/features/Calendars/CalendarTypes'
|
||||
import { getCalendarDetailAsync } from '@/features/Calendars/services'
|
||||
import { AclEntry } from '@/features/Calendars/types/CalendarData'
|
||||
import { CalendarEvent } from '@/features/Events/EventsTypes'
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from '@/utils/dateUtils'
|
||||
import { extractEventBaseUuid } from '@/utils/extractEventBaseUuid'
|
||||
import { getEffectiveEmail } from '@/utils/getEffectiveEmail'
|
||||
import { isEventOrganiser } from '@/utils/isEventOrganiser'
|
||||
import { convertEventDateTimeToISO } from '@/utils/timezone'
|
||||
import { EventInput, SlotLabelContentArg } from '@fullcalendar/core'
|
||||
import moment from 'moment-timezone'
|
||||
import { useI18n } from 'twake-i18n'
|
||||
|
||||
export const updateSlotLabelVisibility = (
|
||||
currentTime: Date,
|
||||
slotLabel: SlotLabelContentArg,
|
||||
timezone: string
|
||||
) => {
|
||||
const isCurrentWeekOrDay = checkIfCurrentWeekOrDay();
|
||||
const isCurrentWeekOrDay = checkIfCurrentWeekOrDay()
|
||||
|
||||
if (!isCurrentWeekOrDay) {
|
||||
return "fc-timegrid-slot-label";
|
||||
return 'fc-timegrid-slot-label'
|
||||
}
|
||||
|
||||
const current = moment.tz(currentTime, timezone);
|
||||
const currentMinutes = current.hours() * 60 + current.minutes();
|
||||
const timeText = slotLabel?.text?.trim();
|
||||
const current = moment.tz(currentTime, timezone)
|
||||
const currentMinutes = current.hours() * 60 + current.minutes()
|
||||
const timeText = slotLabel?.text?.trim()
|
||||
|
||||
if (timeText && timeText.match(/^\d{1,2}:\d{2}$/)) {
|
||||
const [hours, minutes] = timeText.split(":").map(Number);
|
||||
const labelMinutes = hours * 60 + minutes;
|
||||
const [hours, minutes] = timeText.split(':').map(Number)
|
||||
const labelMinutes = hours * 60 + minutes
|
||||
|
||||
let timeDiff = Math.abs(currentMinutes - labelMinutes);
|
||||
let timeDiff = Math.abs(currentMinutes - labelMinutes)
|
||||
|
||||
if (timeDiff > 12 * 60) {
|
||||
timeDiff = 24 * 60 - timeDiff;
|
||||
timeDiff = 24 * 60 - timeDiff
|
||||
}
|
||||
|
||||
if (timeDiff <= 15) {
|
||||
return "timegrid-slot-label-hidden";
|
||||
return 'timegrid-slot-label-hidden'
|
||||
}
|
||||
}
|
||||
|
||||
return "fc-timegrid-slot-label";
|
||||
};
|
||||
return 'fc-timegrid-slot-label'
|
||||
}
|
||||
|
||||
export const checkIfCurrentWeekOrDay = (): boolean => {
|
||||
const todayColumn = document.querySelector(".fc-day-today");
|
||||
const todayColumn = document.querySelector('.fc-day-today')
|
||||
|
||||
if (!todayColumn) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const nowIndicator = document.querySelector(
|
||||
".fc-timegrid-now-indicator-arrow"
|
||||
);
|
||||
return !!nowIndicator;
|
||||
};
|
||||
'.fc-timegrid-now-indicator-arrow'
|
||||
)
|
||||
return !!nowIndicator
|
||||
}
|
||||
|
||||
export function formatEventChipTitle(
|
||||
e: CalendarEvent,
|
||||
t: (key: string) => string
|
||||
) {
|
||||
if (!e.title) {
|
||||
return t("event.untitled");
|
||||
return t('event.untitled')
|
||||
}
|
||||
return e.title === "Busy" && e.class === "PRIVATE"
|
||||
? t("event.form.busy")
|
||||
: e.title;
|
||||
return e.title === 'Busy' && e.class === 'PRIVATE'
|
||||
? t('event.form.busy')
|
||||
: e.title
|
||||
}
|
||||
|
||||
type ConvertedEvent = CalendarEvent & {
|
||||
colors: Record<string, string> | undefined;
|
||||
editable: boolean;
|
||||
priority: number;
|
||||
};
|
||||
colors: Record<string, string> | undefined
|
||||
editable: boolean
|
||||
priority: number
|
||||
}
|
||||
|
||||
function applyTimezoneToEvent(
|
||||
event: CalendarEvent,
|
||||
convertedEvent: ConvertedEvent
|
||||
): void {
|
||||
const eventTimezone = event.timezone || "Etc/UTC";
|
||||
const isAllDay = event.allday ?? false;
|
||||
const eventTimezone = event.timezone || 'Etc/UTC'
|
||||
const isAllDay = event.allday ?? false
|
||||
|
||||
if (!isAllDay && event.start) {
|
||||
const startISO = convertEventDateTimeToISO(event.start, eventTimezone, {
|
||||
isAllDay,
|
||||
});
|
||||
if (startISO) convertedEvent.start = startISO;
|
||||
isAllDay
|
||||
})
|
||||
if (startISO) convertedEvent.start = startISO
|
||||
}
|
||||
|
||||
if (!isAllDay && event.end && eventTimezone) {
|
||||
const endISO = convertEventDateTimeToISO(event.end, eventTimezone, {
|
||||
isAllDay,
|
||||
});
|
||||
if (endISO) convertedEvent.end = endISO;
|
||||
isAllDay
|
||||
})
|
||||
if (endISO) convertedEvent.end = endISO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,28 +109,28 @@ function buildConvertedEvent(
|
||||
const isWriteDelegated =
|
||||
(calendar?.delegated &&
|
||||
calendar.access?.write &&
|
||||
(!event.class || event.class === "PUBLIC")) ??
|
||||
false;
|
||||
(!event.class || event.class === 'PUBLIC')) ??
|
||||
false
|
||||
|
||||
const effectiveEmail = getEffectiveEmail(
|
||||
calendar,
|
||||
isWriteDelegated,
|
||||
userAddress
|
||||
);
|
||||
const isOrganiser = isEventOrganiser(event, effectiveEmail);
|
||||
const isPersonalEvent = extractEventBaseUuid(event.calId) === userId;
|
||||
)
|
||||
const isOrganiser = isEventOrganiser(event, effectiveEmail)
|
||||
const isPersonalEvent = extractEventBaseUuid(event.calId) === userId
|
||||
|
||||
const convertedEvent: ConvertedEvent = {
|
||||
...event,
|
||||
title: formatEventChipTitle(event, t),
|
||||
colors: event.color,
|
||||
editable: (isPersonalEvent || isWriteDelegated) && isOrganiser && !pending,
|
||||
priority: isPersonalEvent ? 1 : 0,
|
||||
};
|
||||
priority: isPersonalEvent ? 1 : 0
|
||||
}
|
||||
|
||||
applyTimezoneToEvent(event, convertedEvent);
|
||||
applyTimezoneToEvent(event, convertedEvent)
|
||||
|
||||
return convertedEvent;
|
||||
return convertedEvent
|
||||
}
|
||||
|
||||
export const eventToFullCalendarFormat = (
|
||||
@@ -142,10 +142,10 @@ export const eventToFullCalendarFormat = (
|
||||
calendars: Record<string, Calendar>
|
||||
): EventInput[] => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
return filteredEvents
|
||||
.concat(filteredTempEvents.map((event) => ({ ...event, temp: true })))
|
||||
.map((event) =>
|
||||
.concat(filteredTempEvents.map(event => ({ ...event, temp: true })))
|
||||
.map(event =>
|
||||
buildConvertedEvent(
|
||||
event,
|
||||
calendars[event.calId],
|
||||
@@ -154,8 +154,8 @@ export const eventToFullCalendarFormat = (
|
||||
pending,
|
||||
t
|
||||
)
|
||||
) as EventInput[];
|
||||
};
|
||||
) as EventInput[]
|
||||
}
|
||||
|
||||
export const extractEvents = (
|
||||
selectedCalendars: string[],
|
||||
@@ -163,29 +163,29 @@ export const extractEvents = (
|
||||
userAddress?: string,
|
||||
hideDeclinedEvents?: boolean | null
|
||||
) => {
|
||||
const allEvents: CalendarEvent[] = [];
|
||||
const allEvents: CalendarEvent[] = []
|
||||
|
||||
selectedCalendars.forEach((id) => {
|
||||
const calendar = calendars[id];
|
||||
selectedCalendars.forEach(id => {
|
||||
const calendar = calendars[id]
|
||||
if (calendar?.events) {
|
||||
allEvents.push(...Object.values(calendar.events));
|
||||
allEvents.push(...Object.values(calendar.events))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return allEvents
|
||||
.filter((event) => event.status !== "CANCELLED")
|
||||
.filter(event => event.status !== 'CANCELLED')
|
||||
.filter(
|
||||
(event) =>
|
||||
event =>
|
||||
!(
|
||||
hideDeclinedEvents &&
|
||||
event.attendee?.some(
|
||||
(a) =>
|
||||
a =>
|
||||
calendars[event.calId].owner.emails.includes(a.cal_address) &&
|
||||
a.partstat === "DECLINED"
|
||||
a.partstat === 'DECLINED'
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const updateCalsDetails = (
|
||||
selectedCalendars: string[],
|
||||
@@ -195,141 +195,141 @@ export const updateCalsDetails = (
|
||||
previousRangeKey: string,
|
||||
dispatch: AppDispatch,
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp",
|
||||
calType?: 'temp',
|
||||
controllers?: Map<string, AbortController>
|
||||
) => {
|
||||
if (pending || !rangeKey) return;
|
||||
if (pending || !rangeKey) return
|
||||
|
||||
const newCalendars = selectedCalendars.filter(
|
||||
(id) => !previousSelectedCalendars.includes(id)
|
||||
);
|
||||
id => !previousSelectedCalendars.includes(id)
|
||||
)
|
||||
|
||||
newCalendars.forEach((id) => {
|
||||
newCalendars.forEach(id => {
|
||||
if (controllers) {
|
||||
const controller = new AbortController();
|
||||
controllers.set(id, controller);
|
||||
const controller = new AbortController()
|
||||
controllers.set(id, controller)
|
||||
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end)
|
||||
},
|
||||
calType,
|
||||
signal: controller.signal,
|
||||
signal: controller.signal
|
||||
})
|
||||
);
|
||||
)
|
||||
} else {
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end)
|
||||
},
|
||||
calType,
|
||||
calType
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (rangeKey !== previousRangeKey) {
|
||||
selectedCalendars?.forEach((id) => {
|
||||
selectedCalendars?.forEach(id => {
|
||||
if (id) {
|
||||
if (controllers) {
|
||||
const controller = new AbortController();
|
||||
controllers.set(id, controller);
|
||||
const controller = new AbortController()
|
||||
controllers.set(id, controller)
|
||||
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end)
|
||||
},
|
||||
calType,
|
||||
signal: controller.signal,
|
||||
signal: controller.signal
|
||||
})
|
||||
);
|
||||
)
|
||||
} else {
|
||||
dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end)
|
||||
},
|
||||
calType,
|
||||
calType
|
||||
})
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
|
||||
let hasRead = false;
|
||||
export function getCalendarVisibility(acl: AclEntry[]): 'private' | 'public' {
|
||||
let hasRead = false
|
||||
if (acl) {
|
||||
for (const entry of acl) {
|
||||
if (entry.principal !== "{DAV:}authenticated") continue;
|
||||
if (entry.principal !== '{DAV:}authenticated') continue
|
||||
|
||||
if (entry.privilege === "{DAV:}read") {
|
||||
hasRead = true;
|
||||
break; // highest visibility, can stop
|
||||
if (entry.privilege === '{DAV:}read') {
|
||||
hasRead = true
|
||||
break // highest visibility, can stop
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasRead) return "public";
|
||||
return "private";
|
||||
if (hasRead) return 'public'
|
||||
return 'private'
|
||||
}
|
||||
|
||||
export function getCalendarDelegationAccess(
|
||||
acl: AclEntry[],
|
||||
userId: string
|
||||
): DelegationAccess {
|
||||
const userPrincipal = `principals/users/${userId}`;
|
||||
const userPrincipal = `principals/users/${userId}`
|
||||
const access: DelegationAccess = {
|
||||
freebusy: false,
|
||||
read: false,
|
||||
write: false,
|
||||
"write-properties": false,
|
||||
all: false,
|
||||
};
|
||||
|
||||
for (const entry of acl ?? []) {
|
||||
if (entry.principal !== userPrincipal) continue;
|
||||
privilegeToAccess(entry.privilege, access);
|
||||
'write-properties': false,
|
||||
all: false
|
||||
}
|
||||
|
||||
return access;
|
||||
for (const entry of acl ?? []) {
|
||||
if (entry.principal !== userPrincipal) continue
|
||||
privilegeToAccess(entry.privilege, access)
|
||||
}
|
||||
|
||||
return access
|
||||
}
|
||||
|
||||
function privilegeToAccess(privilege: string, currentAccess: DelegationAccess) {
|
||||
switch (privilege) {
|
||||
case "{urn:ietf:params:xml:ns:caldav}read-free-busy":
|
||||
currentAccess["freebusy"] = true;
|
||||
break;
|
||||
case "{DAV:}read":
|
||||
currentAccess["read"] = true;
|
||||
currentAccess["freebusy"] = true; // read implies read-free-busy
|
||||
break;
|
||||
case "{DAV:}write-properties":
|
||||
currentAccess["write-properties"] = true;
|
||||
break;
|
||||
case "{DAV:}write":
|
||||
currentAccess["write-properties"] = true; // write implies write-properties
|
||||
currentAccess["write"] = true;
|
||||
break;
|
||||
case "{DAV:}all":
|
||||
currentAccess["freebusy"] = true;
|
||||
currentAccess["read"] = true;
|
||||
currentAccess["write-properties"] = true;
|
||||
currentAccess["write"] = true;
|
||||
currentAccess["all"] = true;
|
||||
break;
|
||||
case '{urn:ietf:params:xml:ns:caldav}read-free-busy':
|
||||
currentAccess['freebusy'] = true
|
||||
break
|
||||
case '{DAV:}read':
|
||||
currentAccess['read'] = true
|
||||
currentAccess['freebusy'] = true // read implies read-free-busy
|
||||
break
|
||||
case '{DAV:}write-properties':
|
||||
currentAccess['write-properties'] = true
|
||||
break
|
||||
case '{DAV:}write':
|
||||
currentAccess['write-properties'] = true // write implies write-properties
|
||||
currentAccess['write'] = true
|
||||
break
|
||||
case '{DAV:}all':
|
||||
currentAccess['freebusy'] = true
|
||||
currentAccess['read'] = true
|
||||
currentAccess['write-properties'] = true
|
||||
currentAccess['write'] = true
|
||||
currentAccess['all'] = true
|
||||
break
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user