Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / components / eventModal / CreateEventModal.tsx
blob8c1ee150a89ce736d2fd93aac0ec30425627def5
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';
26 interface Props {
27     isSmallViewport: boolean;
28     isOpen: boolean;
29     displayWeekNumbers: boolean;
30     weekStartsOn: WeekStartsOn;
31     isCreateEvent: boolean;
32     isInvitation: boolean;
33     isDrawerApp: boolean;
34     model: EventModel;
35     addresses: Address[];
36     onSave: (inviteActions: InviteActions) => Promise<void>;
37     onDelete: (inviteActions: InviteActions) => Promise<void>;
38     onClose: () => void;
39     setModel: (value: EventModel) => void;
40     tzid: string;
41     onDisplayBusySlots: () => void;
42     onExit: () => void;
43     view: VIEWS;
46 const CreateEventModal = ({
47     isSmallViewport,
48     isOpen,
49     displayWeekNumbers,
50     weekStartsOn,
51     isCreateEvent,
52     isInvitation,
53     isDrawerApp,
54     addresses,
55     model,
56     setModel,
57     onSave,
58     onDelete,
59     onClose,
60     tzid,
61     onDisplayBusySlots,
62     view,
63     ...rest
64 }: Props) => {
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
72         errors,
73         onSave,
74         onDelete,
75     });
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 =
86         isCreateEvent ||
87         getCanEditSharedEventData({
88             isOwnedCalendar,
89             isCalendarWritable,
90             isOrganizer,
91             isAttendee,
92             isInvitation,
93             selfAddress,
94         });
95     const cannotSave = getCannotSaveEvent({
96         isOwnedCalendar,
97         isOrganizer,
98         numberOfAttendees: attendees.length,
99         canEditSharedEventData,
100         maxAttendees: mailSettings?.RecipientLimit,
101     });
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,
107         selfAddress,
108     };
110     const handleSubmitWithInviteActions = () => handleSubmit(inviteActions);
111     const submitButton = (
112         <Button
113             color="norm"
114             data-testid="create-event-modal:save"
115             loading={loadingAction && lastAction === ACTION.SUBMIT}
116             disabled={loadingAction || cannotSave}
117             type="submit"
118             className={isCreateEvent ? 'w-full sm:w-auto' : ''}
119         >
120             {c('Action').t`Save`}
121         </Button>
122     );
124     const deleteInviteActions = model.isAttendee
125         ? {
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,
132           }
133         : {
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,
138           };
139     const handleDeleteWithNotice = () => handleDelete(deleteInviteActions);
140     const deleteButton = (
141         <Button
142             onClick={loadingAction ? noop : handleDeleteWithNotice}
143             loading={loadingAction && lastAction === ACTION.DELETE}
144             disabled={loadingAction}
145         >{c('Action').t`Delete`}</Button>
146     );
148     const endAlignedButtons = isCreateEvent ? (
149         submitButton
150     ) : (
151         <div className="flex w-full sm:w-auto flex-column-reverse sm:flex-row gap-2">
152             {getCanDeleteEvent({
153                 isOwnedCalendar,
154                 isCalendarWritable,
155                 isInvitation,
156             }) && deleteButton}
157             {submitButton}
158         </div>
159     );
161     useEffect(() => {
162         if (isBusySlotsAvailable && isOpen) {
163             dispatch(busySlotsActions.setDisplay(false));
164         }
165     }, [isOpen]);
167     return (
168         <BasicModal
169             size="large"
170             fullscreenOnMobile
171             onClose={onClose}
172             {...rest}
173             isOpen={isOpen}
174             className="w-full"
175             as={Form}
176             onSubmit={() => {
177                 if (!loadingAction) {
178                     handleSubmitWithInviteActions();
179                 }
180             }}
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'}
185             footer={
186                 <>
187                     <Button
188                         className="hidden sm:inline-block"
189                         data-testid="event-creation-modal:cancel-event-creation"
190                         disabled={loadingAction}
191                         onClick={onClose}
192                     >
193                         {c('Action').t`Cancel`}
194                     </Button>
195                     {endAlignedButtons}
196                 </>
197             }
198         >
199             <EventForm
200                 displayWeekNumbers={displayWeekNumbers}
201                 weekStartsOn={weekStartsOn}
202                 addresses={addresses}
203                 isSubmitted={isSubmitted}
204                 errors={errors}
205                 model={model}
206                 setModel={setModel}
207                 tzid={tzid}
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}
217                 view={view}
218             />
219         </BasicModal>
220     );
223 export default CreateEventModal;