1 import { useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { Button } from '@proton/atoms';
6 import { BasicModal, Form, useBusySlotsAvailable } from '@proton/components';
7 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
8 import type { VIEWS } from '@proton/shared/lib/calendar/constants';
9 import { ICAL_ATTENDEE_STATUS, ICAL_EVENT_STATUS } from '@proton/shared/lib/calendar/constants';
10 import { getDisplayTitle } from '@proton/shared/lib/calendar/helper';
11 import type { WeekStartsOn } from '@proton/shared/lib/date-fns-utc/interface';
12 import { getIsAddressActive } from '@proton/shared/lib/helpers/address';
13 import type { Address } from '@proton/shared/lib/interfaces';
14 import type { EventModel } from '@proton/shared/lib/interfaces/calendar';
15 import noop from '@proton/utils/noop';
17 import { getCanDeleteEvent, getCanEditSharedEventData, getCannotSaveEvent } from '../../helpers/event';
18 import type { InviteActions } from '../../interfaces/Invite';
19 import { INVITE_ACTION_TYPES } from '../../interfaces/Invite';
20 import { busySlotsActions } from '../../store/busySlots/busySlotsSlice';
21 import { useCalendarDispatch } from '../../store/hooks';
22 import EventForm from './EventForm';
23 import validateEventModel from './eventForm/validateEventModel';
24 import { ACTION, useForm } from './hooks/useForm';
27 isSmallViewport: boolean;
29 displayWeekNumbers: boolean;
30 weekStartsOn: WeekStartsOn;
31 isCreateEvent: boolean;
32 isInvitation: boolean;
36 onSave: (inviteActions: InviteActions) => Promise<void>;
37 onDelete: (inviteActions: InviteActions) => Promise<void>;
39 setModel: (value: EventModel) => void;
41 onDisplayBusySlots: () => void;
46 const CreateEventModal = ({
65 const [mailSettings] = useMailSettings();
66 const isBusySlotsAvailable = useBusySlotsAvailable();
67 const dispatch = useCalendarDispatch();
68 const [participantError, setParticipantError] = useState(false);
69 const errors = { ...validateEventModel(model), participantError };
70 const { isSubmitted, loadingAction, handleDelete, handleSubmit, lastAction } = useForm({
71 containerEl: document.body, // Annoying to get a ref, mostly fine to use this
76 const { isOrganizer, isAttendee, selfAddress, selfAttendeeIndex, attendees, status } = model;
77 const { isOwned: isOwnedCalendar, isWritable: isCalendarWritable } = model.calendar;
79 const isCancelled = status === ICAL_EVENT_STATUS.CANCELLED;
80 const selfAttendee = selfAttendeeIndex !== undefined ? attendees[selfAttendeeIndex] : undefined;
81 const isSelfAddressActive = selfAddress ? getIsAddressActive(selfAddress) : true;
82 const userPartstat = selfAttendee?.partstat || ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
83 const sendCancellationNotice =
84 !isCancelled && [ICAL_ATTENDEE_STATUS.ACCEPTED, ICAL_ATTENDEE_STATUS.TENTATIVE].includes(userPartstat);
85 const canEditSharedEventData =
87 getCanEditSharedEventData({
95 const cannotSave = getCannotSaveEvent({
98 numberOfAttendees: attendees.length,
99 canEditSharedEventData,
100 maxAttendees: mailSettings?.RecipientLimit,
103 // new events have no uid yet
104 const inviteActions = {
105 // the type will be more properly assessed in getSaveEventActions by getCorrectedSaveEventActions
106 type: model.isAttendee ? INVITE_ACTION_TYPES.NONE : INVITE_ACTION_TYPES.SEND_INVITATION,
110 const handleSubmitWithInviteActions = () => handleSubmit(inviteActions);
111 const submitButton = (
114 data-testid="create-event-modal:save"
115 loading={loadingAction && lastAction === ACTION.SUBMIT}
116 disabled={loadingAction || cannotSave}
118 className={isCreateEvent ? 'w-full sm:w-auto' : ''}
120 {c('Action').t`Save`}
124 const deleteInviteActions = model.isAttendee
126 type: isSelfAddressActive ? INVITE_ACTION_TYPES.DECLINE_INVITATION : INVITE_ACTION_TYPES.DECLINE_DISABLED,
127 isProtonProtonInvite: model.isProtonProtonInvite,
128 partstat: ICAL_ATTENDEE_STATUS.DECLINED,
129 sendCancellationNotice,
130 selfAddress: model.selfAddress,
131 selfAttendeeIndex: model.selfAttendeeIndex,
134 type: isSelfAddressActive ? INVITE_ACTION_TYPES.CANCEL_INVITATION : INVITE_ACTION_TYPES.CANCEL_DISABLED,
135 isProtonProtonInvite: model.isProtonProtonInvite,
136 selfAddress: model.selfAddress,
137 selfAttendeeIndex: model.selfAttendeeIndex,
139 const handleDeleteWithNotice = () => handleDelete(deleteInviteActions);
140 const deleteButton = (
142 onClick={loadingAction ? noop : handleDeleteWithNotice}
143 loading={loadingAction && lastAction === ACTION.DELETE}
144 disabled={loadingAction}
145 >{c('Action').t`Delete`}</Button>
148 const endAlignedButtons = isCreateEvent ? (
151 <div className="flex w-full sm:w-auto flex-column-reverse sm:flex-row gap-2">
162 if (isBusySlotsAvailable && isOpen) {
163 dispatch(busySlotsActions.setDisplay(false));
177 if (!loadingAction) {
178 handleSubmitWithInviteActions();
181 // if the user can't edit shared event data, the modal will have a reduced form
182 title={canEditSharedEventData ? undefined : getDisplayTitle(model.title)}
183 // breaking only for non-editable form
184 titleClassName={canEditSharedEventData ? undefined : 'text-break'}
188 className="hidden sm:inline-block"
189 data-testid="event-creation-modal:cancel-event-creation"
190 disabled={loadingAction}
193 {c('Action').t`Cancel`}
200 displayWeekNumbers={displayWeekNumbers}
201 weekStartsOn={weekStartsOn}
202 addresses={addresses}
203 isSubmitted={isSubmitted}
208 canEditSharedEventData={canEditSharedEventData}
209 isCreateEvent={isCreateEvent}
210 isInvitation={isInvitation}
211 setParticipantError={setParticipantError}
212 isOwnedCalendar={isOwnedCalendar}
213 isCalendarWritable={isCalendarWritable}
214 isDrawerApp={isDrawerApp}
215 isSmallViewport={isSmallViewport}
216 onDisplayBusySlots={onDisplayBusySlots}
223 export default CreateEventModal;