Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -89,7 +89,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
onEndDateChange,
|
onEndDateChange,
|
||||||
onEndTimeChange,
|
onEndTimeChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
|
|
||||||
const initialDurationRef = React.useRef<number | null>(null);
|
const initialDurationRef = React.useRef<number | null>(null);
|
||||||
const isUserActionRef = React.useRef(false);
|
const isUserActionRef = React.useRef(false);
|
||||||
@@ -341,8 +341,9 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider
|
<LocalizationProvider
|
||||||
|
key={lang}
|
||||||
dateAdapter={AdapterDayjs}
|
dateAdapter={AdapterDayjs}
|
||||||
adapterLocale={t("locale") ?? "en"}
|
adapterLocale={lang ?? "en"}
|
||||||
localeText={{
|
localeText={{
|
||||||
okButtonLabel: t("common.ok"),
|
okButtonLabel: t("common.ok"),
|
||||||
cancelButtonLabel: t("common.cancel"),
|
cancelButtonLabel: t("common.cancel"),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Box, Typography } from "@linagora/twake-mui";
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { ContextualizedEvent } from "../EventsTypes";
|
import { ContextualizedEvent } from "../EventsTypes";
|
||||||
|
import { EventCounterModal } from "./EventCounterModal";
|
||||||
import { RSVPButton } from "./RSVPButton";
|
import { RSVPButton } from "./RSVPButton";
|
||||||
|
|
||||||
interface AttendanceValidationProps {
|
interface AttendanceValidationProps {
|
||||||
@@ -25,8 +26,8 @@ export function AttendanceValidation({
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null);
|
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null);
|
||||||
|
const [openCounterModal, setOpenCounterModal] = useState(false);
|
||||||
|
|
||||||
// Check if we should show RSVP buttons
|
|
||||||
const hasNoAttendeesOrOrganizer =
|
const hasNoAttendeesOrOrganizer =
|
||||||
!(contextualizedEvent.event?.attendee?.length > 0) &&
|
!(contextualizedEvent.event?.attendee?.length > 0) &&
|
||||||
!contextualizedEvent.event?.organizer;
|
!contextualizedEvent.event?.organizer;
|
||||||
@@ -68,16 +69,30 @@ export function AttendanceValidation({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography sx={{ marginRight: 2 }}>
|
<Typography variant="body2" sx={{ marginRight: 1 }}>
|
||||||
{calendar.owner?.resource
|
{calendar.owner?.resource
|
||||||
? t("eventPreview.authorizeQuestion")
|
? t("eventPreview.authorizeQuestion")
|
||||||
: t("eventPreview.attendingQuestion")}
|
: t("eventPreview.attendingQuestion")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box display="flex" gap="15px" alignItems="center">
|
<Box display="flex" gap={1} mx={1} alignItems="center">
|
||||||
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
|
<RSVPButton rsvpValue="ACCEPTED" {...commonButtonProps} />
|
||||||
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
|
|
||||||
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
|
<RSVPButton rsvpValue="DECLINED" {...commonButtonProps} />
|
||||||
|
<RSVPButton rsvpValue="TENTATIVE" {...commonButtonProps} />
|
||||||
</Box>
|
</Box>
|
||||||
|
{!contextualizedEvent.isOrganizer && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => setOpenCounterModal(!openCounterModal)}
|
||||||
|
sx={{ marginLeft: 1, cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{t("eventPreview.proposeNewTime")}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<EventCounterModal
|
||||||
|
open={openCounterModal}
|
||||||
|
setOpen={setOpenCounterModal}
|
||||||
|
contextualizedEvent={contextualizedEvent}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
|
||||||
|
import { ResponsiveDialog } from "@/components/Dialog";
|
||||||
|
import { DateTimeFields } from "@/components/Event/components/DateTimeFields";
|
||||||
|
import { FieldWithLabel } from "@/components/Event/components/FieldWithLabel";
|
||||||
|
import { splitDateTime } from "@/components/Event/utils/dateTimeHelpers";
|
||||||
|
import { Box, Button, TextField, Typography } from "@linagora/twake-mui";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useI18n } from "twake-i18n";
|
||||||
|
import { postCounterProposal } from "../api/sendCounterProposal";
|
||||||
|
import { EventTimeSubtitle } from "../EventPreview/EventTimeSubtitle";
|
||||||
|
import { ContextualizedEvent } from "../EventsTypes";
|
||||||
|
|
||||||
|
export function EventCounterModal({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
contextualizedEvent,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (b: boolean) => void;
|
||||||
|
contextualizedEvent: ContextualizedEvent;
|
||||||
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const allday = contextualizedEvent.event.allday ?? false;
|
||||||
|
|
||||||
|
const timezone = contextualizedEvent.event.timezone;
|
||||||
|
|
||||||
|
const startSplit = splitDateTime(
|
||||||
|
moment
|
||||||
|
.tz(contextualizedEvent.event.start, timezone)
|
||||||
|
.format("YYYY-MM-DDTHH:mm")
|
||||||
|
);
|
||||||
|
const endSplit = splitDateTime(
|
||||||
|
moment
|
||||||
|
.tz(
|
||||||
|
contextualizedEvent.event.end ?? contextualizedEvent.event.start,
|
||||||
|
timezone
|
||||||
|
)
|
||||||
|
.format("YYYY-MM-DDTHH:mm")
|
||||||
|
);
|
||||||
|
|
||||||
|
const [startDate, setStartDate] = useState(startSplit.date);
|
||||||
|
const [startTime, setStartTime] = useState(startSplit.time);
|
||||||
|
const [endDate, setEndDate] = useState(endSplit.date);
|
||||||
|
const [endTime, setEndTime] = useState(endSplit.time);
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
const [hasEndDateChanged, setHasEndDateChanged] = useState(false);
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [validation, setValidation] = useState<{
|
||||||
|
errors: { dateTime: string };
|
||||||
|
}>({
|
||||||
|
errors: { dateTime: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStartDateChange = (value: string) => {
|
||||||
|
setStartDate(value);
|
||||||
|
if (value > endDate) {
|
||||||
|
setEndDate(value);
|
||||||
|
setHasEndDateChanged(true);
|
||||||
|
}
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartTimeChange = (value: string) => {
|
||||||
|
setStartTime(value);
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndDateChange = (value: string) => {
|
||||||
|
setEndDate(value);
|
||||||
|
setHasEndDateChanged(true);
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndTimeChange = (value: string) => {
|
||||||
|
setEndTime(value);
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (): boolean => {
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
setValidation({
|
||||||
|
errors: { dateTime: t("event.validation.startRequired") },
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!allday && (!startTime || !endTime)) {
|
||||||
|
setValidation({
|
||||||
|
errors: { dateTime: t("event.validation.startRequired") },
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
endDate < startDate ||
|
||||||
|
(!allday && endDate === startDate && endTime <= startTime)
|
||||||
|
) {
|
||||||
|
setValidation({
|
||||||
|
errors: { dateTime: t("event.validation.endAfterStart") },
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validate()) return;
|
||||||
|
if (
|
||||||
|
!contextualizedEvent.currentUserAttendee?.cal_address ||
|
||||||
|
!contextualizedEvent.event.organizer?.cal_address
|
||||||
|
) {
|
||||||
|
setValidation({ errors: { dateTime: t("error.unknown") } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await postCounterProposal({
|
||||||
|
event: contextualizedEvent.event,
|
||||||
|
senderEmail: contextualizedEvent.currentUserAttendee.cal_address,
|
||||||
|
recipientEmail: contextualizedEvent.event.organizer.cal_address,
|
||||||
|
proposedStart: contextualizedEvent.event.allday
|
||||||
|
? startDate
|
||||||
|
: `${startDate}T${startTime}`,
|
||||||
|
proposedEnd: contextualizedEvent.event.allday
|
||||||
|
? endDate
|
||||||
|
: `${endDate}T${endTime}`,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setValidation({ errors: { dateTime: t("error.unknown") } });
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setStartDate(startSplit.date);
|
||||||
|
setStartTime(startSplit.time);
|
||||||
|
setEndDate(endSplit.date);
|
||||||
|
setEndTime(endSplit.time);
|
||||||
|
setShowMore(false);
|
||||||
|
setHasEndDateChanged(false);
|
||||||
|
setMessage("");
|
||||||
|
setValidation({ errors: { dateTime: "" } });
|
||||||
|
}, [open, startSplit.date, startSplit.time, endSplit.date, endSplit.time]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
title={t("eventPreview.proposeNewTime")}
|
||||||
|
>
|
||||||
|
{/* Event title */}
|
||||||
|
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatEventChipTitle(contextualizedEvent.event, t)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Current event time */}
|
||||||
|
|
||||||
|
<EventTimeSubtitle
|
||||||
|
event={contextualizedEvent.event}
|
||||||
|
t={t}
|
||||||
|
timezone={contextualizedEvent.event.timezone}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Your proposal label */}
|
||||||
|
<FieldWithLabel label={t("eventPreview.yourProposal")} isExpanded={false}>
|
||||||
|
<DateTimeFields
|
||||||
|
startDate={startDate}
|
||||||
|
startTime={startTime}
|
||||||
|
endDate={endDate}
|
||||||
|
endTime={endTime}
|
||||||
|
allday={allday}
|
||||||
|
showMore={showMore}
|
||||||
|
hasEndDateChanged={hasEndDateChanged}
|
||||||
|
validation={validation}
|
||||||
|
onStartDateChange={handleStartDateChange}
|
||||||
|
onStartTimeChange={handleStartTimeChange}
|
||||||
|
onEndDateChange={handleEndDateChange}
|
||||||
|
onEndTimeChange={handleEndTimeChange}
|
||||||
|
showEndDate={
|
||||||
|
showMore ||
|
||||||
|
allday ||
|
||||||
|
(hasEndDateChanged && startDate !== endDate) ||
|
||||||
|
(!showMore && !allday && startDate !== endDate)
|
||||||
|
}
|
||||||
|
onToggleEndDate={() => setShowMore((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
</FieldWithLabel>
|
||||||
|
{/* Optional message */}
|
||||||
|
<Box mt={2}>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
multiline
|
||||||
|
size="small"
|
||||||
|
minRows={2}
|
||||||
|
maxRows={10}
|
||||||
|
fullWidth
|
||||||
|
placeholder={t("eventPreview.optionalMessage")}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
overflowY: "auto",
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
"& textarea": {
|
||||||
|
resize: "vertical",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Box display="flex" justifyContent="flex-end" gap={2} mt={3}>
|
||||||
|
<Button variant="text" onClick={() => setOpen(false)}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{t("eventPreview.sendProposal")}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { CalendarName } from "@/components/Calendar/CalendarName";
|
|||||||
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
|
import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
|
||||||
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
|
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
|
||||||
import { EditModeDialog } from "@/components/Event/EditModeDialog";
|
import { EditModeDialog } from "@/components/Event/EditModeDialog";
|
||||||
import { getTimezoneOffset } from "@/utils/timezone";
|
|
||||||
import { DateSelectArg } from "@fullcalendar/core";
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
import { Box, Chip, Tooltip, Typography } from "@linagora/twake-mui";
|
import { Box, Chip, Tooltip, Typography } from "@linagora/twake-mui";
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
@@ -16,8 +15,7 @@ import { EventPreviewDetails } from "../EventPreview/EventPreviewDetails";
|
|||||||
import { EventPreviewHeader } from "../EventPreview/EventPreviewHeader";
|
import { EventPreviewHeader } from "../EventPreview/EventPreviewHeader";
|
||||||
import { useEventPreviewState } from "../EventPreview/useEventPreviewState";
|
import { useEventPreviewState } from "../EventPreview/useEventPreviewState";
|
||||||
import EventUpdateModal from "../EventUpdateModal";
|
import EventUpdateModal from "../EventUpdateModal";
|
||||||
import { formatDate } from "./utils/formatDate";
|
import { EventTimeSubtitle } from "./EventTimeSubtitle";
|
||||||
import { formatEnd } from "./utils/formatEnd";
|
|
||||||
|
|
||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -160,12 +158,7 @@ export default function EventPreviewModal({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography color="text.secondaryContainer">
|
<EventTimeSubtitle event={event} t={t} timezone={timezone} />
|
||||||
{formatDate(event.start, t, timezone, event.allday)}
|
|
||||||
{event.end &&
|
|
||||||
formatEnd(event.start, event.end, t, timezone, event.allday) &&
|
|
||||||
` – ${formatEnd(event.start, event.end, t, timezone, event.allday)} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Event details (attendees, location, description, etc.) */}
|
{/* Event details (attendees, location, description, etc.) */}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { getTimezoneOffset } from "@/utils/timezone";
|
||||||
|
import { Typography } from "@linagora/twake-mui";
|
||||||
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
|
import { formatDate } from "./utils/formatDate";
|
||||||
|
import { formatEnd } from "./utils/formatEnd";
|
||||||
|
|
||||||
|
export function EventTimeSubtitle({
|
||||||
|
event,
|
||||||
|
t,
|
||||||
|
timezone,
|
||||||
|
}: {
|
||||||
|
event: CalendarEvent;
|
||||||
|
t: (k: string, p?: string | object | undefined) => string;
|
||||||
|
timezone: string;
|
||||||
|
}) {
|
||||||
|
const formattedEnd = event.end
|
||||||
|
? formatEnd(event.start, event.end, t, timezone, event.allday)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography color="text.secondaryContainer">
|
||||||
|
{formatDate(event.start, t, timezone, event.allday)}
|
||||||
|
{formattedEnd &&
|
||||||
|
` – ${formattedEnd} ${!event.allday ? getTimezoneOffset(timezone, new Date(event.start)) : ""}`}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { api } from "@/utils/apiUtils";
|
||||||
|
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||||
|
import { TIMEZONES } from "@/utils/timezone-data";
|
||||||
|
import ICAL from "ical.js";
|
||||||
|
import { CalendarEvent } from "../EventsTypes";
|
||||||
|
import { makeTimezone, makeVevent } from "../utils";
|
||||||
|
|
||||||
|
export async function postCounterProposal({
|
||||||
|
event,
|
||||||
|
senderEmail,
|
||||||
|
recipientEmail,
|
||||||
|
proposedStart,
|
||||||
|
proposedEnd,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
event: CalendarEvent;
|
||||||
|
senderEmail: string;
|
||||||
|
recipientEmail: string;
|
||||||
|
proposedStart: string;
|
||||||
|
proposedEnd: string;
|
||||||
|
message?: string;
|
||||||
|
}): Promise<Response> {
|
||||||
|
// Build the counter event with proposed dates
|
||||||
|
const counterEvent: CalendarEvent = {
|
||||||
|
...event,
|
||||||
|
start: proposedStart,
|
||||||
|
end: proposedEnd,
|
||||||
|
sequence: event.sequence ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build vevent jCal
|
||||||
|
const vevent = makeVevent(
|
||||||
|
counterEvent,
|
||||||
|
counterEvent.timezone,
|
||||||
|
senderEmail,
|
||||||
|
!event.recurrenceId
|
||||||
|
);
|
||||||
|
if (message) {
|
||||||
|
vevent?.[1]?.push(["comment", {}, "text", message]);
|
||||||
|
}
|
||||||
|
// Build vtimezone
|
||||||
|
const timezoneData = TIMEZONES.zones[counterEvent.timezone];
|
||||||
|
const vtimezone = makeTimezone(timezoneData, counterEvent);
|
||||||
|
|
||||||
|
// Assemble full vcalendar with METHOD:COUNTER
|
||||||
|
const jcal = [
|
||||||
|
"vcalendar",
|
||||||
|
[
|
||||||
|
["version", {}, "text", "2.0"],
|
||||||
|
["prodid", {}, "text", "-//OpenPaaS//OpenPaaS//EN"],
|
||||||
|
["method", {}, "text", "COUNTER"],
|
||||||
|
],
|
||||||
|
[vevent, vtimezone.component.jCal],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Serialize to raw ICS
|
||||||
|
const counterICS = new ICAL.Component(jcal).toString();
|
||||||
|
|
||||||
|
const postResponse = await api(`dav${event.URL}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
ical: counterICS,
|
||||||
|
sender: senderEmail,
|
||||||
|
recipient: recipientEmail,
|
||||||
|
uid: extractEventBaseUuid(event.uid),
|
||||||
|
sequence: counterEvent.sequence,
|
||||||
|
method: "COUNTER",
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
accept: "application/json,text/plain,*/*",
|
||||||
|
"content-type": "application/calendar+json",
|
||||||
|
Prefer: "return=representation",
|
||||||
|
"X-Http-Method-Override": "ITIP",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!postResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`postCounterProposal failed with status ${postResponse.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return postResponse;
|
||||||
|
}
|
||||||
@@ -9,9 +9,6 @@ export function createEventContext(
|
|||||||
): ContextualizedEvent {
|
): ContextualizedEvent {
|
||||||
const isOwn = calendar.owner?.emails?.includes(user.email) ?? false;
|
const isOwn = calendar.owner?.emails?.includes(user.email) ?? false;
|
||||||
const isRecurring = event?.uid?.includes("/") ?? false;
|
const isRecurring = event?.uid?.includes("/") ?? false;
|
||||||
const isOrganizer = event.organizer
|
|
||||||
? user?.email === event.organizer.cal_address
|
|
||||||
: isOwn;
|
|
||||||
const attendeeEmail = calendar.delegated
|
const attendeeEmail = calendar.delegated
|
||||||
? calendar.owner?.emails?.[0]
|
? calendar.owner?.emails?.[0]
|
||||||
: user.email;
|
: user.email;
|
||||||
@@ -20,6 +17,10 @@ export function createEventContext(
|
|||||||
(person) => person.cutype === "RESOURCE" && person.cn === calendar.name
|
(person) => person.cutype === "RESOURCE" && person.cn === calendar.name
|
||||||
)
|
)
|
||||||
: event.attendee?.find((person) => person.cal_address === attendeeEmail);
|
: event.attendee?.find((person) => person.cal_address === attendeeEmail);
|
||||||
|
const isOrganizer = event.organizer
|
||||||
|
? attendeeEmail === event.organizer.cal_address
|
||||||
|
: isOwn;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event,
|
event,
|
||||||
calendar,
|
calendar,
|
||||||
|
|||||||
+6
-2
@@ -282,9 +282,13 @@
|
|||||||
},
|
},
|
||||||
"attendingQuestion": "Attending?",
|
"attendingQuestion": "Attending?",
|
||||||
"authorizeQuestion": "Authorize?",
|
"authorizeQuestion": "Authorize?",
|
||||||
"ACCEPTED": "Accept",
|
"proposeNewTime": "New time",
|
||||||
|
"yourProposal": "Your proposal",
|
||||||
|
"optionalMessage": "Add optional message to the organizer",
|
||||||
|
"sendProposal": "Send proposal",
|
||||||
|
"ACCEPTED": "Yes",
|
||||||
"TENTATIVE": "Maybe",
|
"TENTATIVE": "Maybe",
|
||||||
"DECLINED": "Decline",
|
"DECLINED": "No",
|
||||||
"NEEDS-ACTION": "Pending",
|
"NEEDS-ACTION": "Pending",
|
||||||
"attendingStatus": {
|
"attendingStatus": {
|
||||||
"ACCEPTED": "Accepted",
|
"ACCEPTED": "Accepted",
|
||||||
|
|||||||
+6
-2
@@ -283,9 +283,13 @@
|
|||||||
},
|
},
|
||||||
"attendingQuestion": "Participer ?",
|
"attendingQuestion": "Participer ?",
|
||||||
"authorizeQuestion": "Autoriser ?",
|
"authorizeQuestion": "Autoriser ?",
|
||||||
"ACCEPTED": "Accepter",
|
"proposeNewTime": "Nouvel horaire",
|
||||||
|
"yourProposal": "Votre proposition",
|
||||||
|
"optionalMessage": "Ajouter un message optionnel à l'organisateur",
|
||||||
|
"sendProposal": "Envoyer la proposition",
|
||||||
|
"ACCEPTED": "Oui",
|
||||||
"TENTATIVE": "Peut-être",
|
"TENTATIVE": "Peut-être",
|
||||||
"DECLINED": "Décliner",
|
"DECLINED": "Non",
|
||||||
"NEEDS-ACTION": "En attente",
|
"NEEDS-ACTION": "En attente",
|
||||||
"attendingStatus": {
|
"attendingStatus": {
|
||||||
"ACCEPTED": "Accepté",
|
"ACCEPTED": "Accepté",
|
||||||
|
|||||||
@@ -283,6 +283,10 @@
|
|||||||
},
|
},
|
||||||
"attendingQuestion": "Присоединитесь?",
|
"attendingQuestion": "Присоединитесь?",
|
||||||
"authorizeQuestion": "Авторизовать?",
|
"authorizeQuestion": "Авторизовать?",
|
||||||
|
"proposeNewTime": "Предложить новое время",
|
||||||
|
"yourProposal": "Ваше предложение",
|
||||||
|
"optionalMessage": "Добавить необязательное сообщение организатору",
|
||||||
|
"sendProposal": "Отправить предложение",
|
||||||
"ACCEPTED": "Да",
|
"ACCEPTED": "Да",
|
||||||
"TENTATIVE": "Возможно",
|
"TENTATIVE": "Возможно",
|
||||||
"DECLINED": "Нет",
|
"DECLINED": "Нет",
|
||||||
|
|||||||
+7
-3
@@ -279,11 +279,15 @@
|
|||||||
"label": "Rảnh",
|
"label": "Rảnh",
|
||||||
"tooltip": "Người khác sẽ thấy bạn rảnh trong khoảng thời gian này."
|
"tooltip": "Người khác sẽ thấy bạn rảnh trong khoảng thời gian này."
|
||||||
},
|
},
|
||||||
"attendingQuestion": "Tham gia?",
|
"attendingQuestion": "Tham dự?",
|
||||||
"authorizeQuestion": "Cho phép?",
|
"authorizeQuestion": "Cho phép?",
|
||||||
"ACCEPTED": "Chấp nhận",
|
"proposeNewTime": "Hẹn lại",
|
||||||
|
"yourProposal": "Đề xuất của bạn",
|
||||||
|
"optionalMessage": "Thêm tin nhắn tùy chọn cho người tổ chức",
|
||||||
|
"sendProposal": "Gửi đề xuất",
|
||||||
|
"ACCEPTED": "Có",
|
||||||
"TENTATIVE": "Có thể",
|
"TENTATIVE": "Có thể",
|
||||||
"DECLINED": "Từ chối",
|
"DECLINED": "Không",
|
||||||
"NEEDS-ACTION": "Đang chờ",
|
"NEEDS-ACTION": "Đang chờ",
|
||||||
"attendingStatus": {
|
"attendingStatus": {
|
||||||
"ACCEPTED": "Đã chấp nhận",
|
"ACCEPTED": "Đã chấp nhận",
|
||||||
|
|||||||
Reference in New Issue
Block a user