Remove option component
[ProtonMail-WebClient.git] / packages / components / containers / calendar / calendarModal / personalCalendarModal / PersonalCalendarModal.tsx
blob8dddb78489561a245db8239b3914b1e169228586
1 import type { ChangeEvent } from 'react';
2 import { useMemo, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import Form from '@proton/components/components/form/Form';
8 import ColorPicker from '@proton/components/components/input/ColorPicker';
9 import TextArea from '@proton/components/components/input/TextArea';
10 import Loader from '@proton/components/components/loader/Loader';
11 import Option from '@proton/components/components/option/Option';
12 import { getCalendarModalSize } from '@proton/components/containers/calendar/calendarModal/helpers';
13 import { CALENDAR_MODAL_TYPE } from '@proton/components/containers/calendar/calendarModal/interface';
14 import { useContactEmailsCache } from '@proton/components/containers/contacts/ContactEmailsProvider';
15 import { useLoading } from '@proton/hooks';
16 import { dedupeNotifications, sortNotificationsByAscendingTrigger } from '@proton/shared/lib/calendar/alarms';
17 import { getIsCalendarWritable, getIsSubscribedCalendar, getShowDuration } from '@proton/shared/lib/calendar/calendar';
18 import { MAX_CHARS_API, MAX_DEFAULT_NOTIFICATIONS } from '@proton/shared/lib/calendar/constants';
19 import { getSharedCalendarSubHeaderText } from '@proton/shared/lib/calendar/sharing/shareProton/shareProton';
20 import type { Nullable } from '@proton/shared/lib/interfaces';
21 import type { NotificationModel, SubscribedCalendar, VisualCalendar } from '@proton/shared/lib/interfaces/calendar';
23 import {
24     InputFieldTwo,
25     ModalTwo,
26     ModalTwoContent,
27     ModalTwoFooter,
28     ModalTwoHeader,
29     SelectTwo,
30 } from '../../../../components';
31 import type { SelectChangeEvent } from '../../../../components/selectTwo/select';
32 import { TruncatedText } from '../../../../components/truncatedText';
33 import GenericError from '../../../error/GenericError';
34 import useGetCalendarActions from '../../hooks/useGetCalendarActions';
35 import useGetCalendarSetup from '../../hooks/useGetCalendarSetup';
36 import Notifications from '../../notifications/Notifications';
37 import BusySlotsCheckbox from '../BusySlotsCheckbox';
38 import { getCalendarPayload, getCalendarSettingsPayload, getDefaultModel, validate } from './calendarModalState';
40 import './PersonalCalendarModal.scss';
42 const URL_MAX_DISPLAY_LENGTH = 100;
44 const { COMPLETE, VISUAL, SHARED } = CALENDAR_MODAL_TYPE;
46 export interface CalendarModalProps {
47     calendar?: VisualCalendar | SubscribedCalendar;
48     calendars?: VisualCalendar[];
49     defaultCalendarID?: Nullable<string>;
50     onClose?: () => void;
51     onExit?: () => void;
52     onCreateCalendar?: (id: string) => void;
53     onEditCalendar?: () => void;
54     open?: boolean;
55     type?: CALENDAR_MODAL_TYPE;
58 export const PersonalCalendarModal = ({
59     calendar: initialCalendar,
60     calendars = [],
61     defaultCalendarID = '',
62     open,
63     onClose,
64     onExit,
65     onCreateCalendar,
66     onEditCalendar,
67     type = COMPLETE,
68 }: CalendarModalProps) => {
69     const [loadingAction, withLoadingAction] = useLoading();
70     const { contactEmailsMap } = useContactEmailsCache() || {};
72     const [isSubmitted, setIsSubmitted] = useState(false);
73     const [error, setError] = useState(false);
74     const [calendar, setCalendar] = useState(initialCalendar);
76     const [model, setModel] = useState(() => getDefaultModel());
78     const addressText = useMemo(() => {
79         const option = model.addressOptions.find(({ value: ID }) => ID === model.addressID);
80         return (option && option.text) || '';
81     }, [model.addressID, model.addressOptions]);
83     const showDuration = initialCalendar ? getShowDuration(initialCalendar) : true;
84     const subscribeURL =
85         initialCalendar && getIsSubscribedCalendar(initialCalendar)
86             ? initialCalendar.SubscriptionParameters.URL
87             : undefined;
89     const { error: setupError, loading: loadingSetup } = useGetCalendarSetup({ calendar: initialCalendar, setModel });
90     const { handleCreateCalendar, handleUpdateCalendar } = useGetCalendarActions({
91         type,
92         setCalendar,
93         setError,
94         defaultCalendarID,
95         onClose,
96         onCreateCalendar,
97         onEditCalendar,
98         calendars,
99     });
101     const formattedModel = {
102         ...model,
103         name: model.name.trim(),
104         description: model.description.trim(),
105     };
107     const errors = validate(formattedModel);
109     const handleProcessCalendar = async () => {
110         const formattedModelWithFormattedNotifications = {
111             ...formattedModel,
112             partDayNotifications: sortNotificationsByAscendingTrigger(dedupeNotifications(model.partDayNotifications)),
113             fullDayNotifications: sortNotificationsByAscendingTrigger(dedupeNotifications(model.fullDayNotifications)),
114         };
115         const calendarPayload = getCalendarPayload(formattedModelWithFormattedNotifications);
116         const calendarSettingsPayload = getCalendarSettingsPayload(formattedModelWithFormattedNotifications);
117         if (calendar) {
118             return handleUpdateCalendar(calendar, calendarPayload, calendarSettingsPayload);
119         }
120         return handleCreateCalendar(
121             formattedModelWithFormattedNotifications.addressID,
122             calendarPayload,
123             calendarSettingsPayload
124         );
125     };
127     const hasError = error || setupError;
129     const getTitle = (type: CALENDAR_MODAL_TYPE) => {
130         const editCalendarText = c('Title; edit calendar modal').t`Edit calendar`;
131         if (hasError) {
132             return c('Title').t`Error`;
133         }
135         if (type === VISUAL) {
136             return editCalendarText;
137         }
139         return initialCalendar ? editCalendarText : c('Title; create calendar modal').t`Create calendar`;
140     };
142     const getSubline = () => {
143         if (type !== CALENDAR_MODAL_TYPE.SHARED || !calendar || !contactEmailsMap) {
144             return;
145         }
147         const subHeaderText = getSharedCalendarSubHeaderText(calendar, contactEmailsMap);
149         return (
150             subHeaderText && (
151                 <>
152                     <div className="text-rg text-break mb-1 color-norm">{subHeaderText}</div>
153                     {!getIsCalendarWritable(calendar) && (
154                         <div className="text-rg text-ellipsis color-weak">
155                             {c('Info; access rights for shared calendar').t`View only`}
156                         </div>
157                     )}
158                 </>
159             )
160         );
161     };
163     const handleSubmit = () => {
164         if (hasError) {
165             window.location.reload();
166             return;
167         }
169         setIsSubmitted(true);
171         if (Object.keys(errors).length > 0) {
172             return;
173         }
175         void withLoadingAction(handleProcessCalendar());
176     };
178     const getFakeLabel = (labelText: string) => (
179         <span className="flex field-two-label-container justify-space-between flex-nowrap items-end">
180             <span className="field-two-label">{labelText}</span>
181         </span>
182     );
184     const getFakeInputTwo = ({ content, label }: { content: React.ReactNode; label: string }) => {
185         // classes taken from InputFieldTwo
186         return (
187             <div className="field-two-container w-full">
188                 {getFakeLabel(label)}
189                 <div className="field-two-field-container relative">{content}</div>
190             </div>
191         );
192     };
194     const calendarNameRow = (
195         <InputFieldTwo
196             id="calendar-name-input"
197             label={c('Label').t`Name`}
198             value={model.name}
199             error={isSubmitted && errors.name}
200             maxLength={MAX_CHARS_API.CALENDAR_NAME}
201             disableChange={loadingAction}
202             placeholder={c('Placeholder').t`Add a calendar name`}
203             onChange={({ target }: ChangeEvent<HTMLInputElement>) => setModel({ ...model, name: target.value })}
204             autoFocus
205         />
206     );
208     const colorRow = (
209         <InputFieldTwo
210             as={ColorPicker}
211             label={c('Label').t`Color`}
212             id="calendar-color"
213             color={model.color}
214             onChange={(color: string) => setModel({ ...model, color })}
215         />
216     );
218     const descriptionRow = (
219         <InputFieldTwo
220             as={TextArea}
221             label={c('Label').t`Description`}
222             autoGrow
223             id="calendar-description-textarea"
224             value={model.description}
225             placeholder={c('Placeholder').t`Add a calendar description`}
226             onChange={({ target }: ChangeEvent<HTMLTextAreaElement>) =>
227                 setModel({ ...model, description: target.value })
228             }
229             maxLength={MAX_CHARS_API.CALENDAR_DESCRIPTION}
230             isSubmitted={isSubmitted}
231             error={errors.description}
232         />
233     );
235     const addressRow = model.calendarID ? (
236         getFakeInputTwo({ content: addressText, label: c('Label').t`Email address` })
237     ) : (
238         <InputFieldTwo
239             as={SelectTwo}
240             id="calendar-address-select"
241             value={model.addressID}
242             // @ts-ignore
243             onChange={({ value }: SelectChangeEvent<string>) => setModel({ ...model, addressID: value })}
244             label={c('Label').t`Email address`}
245         >
246             {model.addressOptions.map(({ value, text }) => (
247                 <Option key={value} value={value} title={text} />
248             ))}
249         </InputFieldTwo>
250     );
252     const defaultEventDurationRow = showDuration ? (
253         <InputFieldTwo
254             as={SelectTwo}
255             label={c('Label').t`Event duration`}
256             id="duration-select"
257             data-testid="create-calendar/event-settings:event-duration"
258             value={model.duration}
259             // @ts-ignore
260             onChange={({ value }: SelectChangeEvent<string>) => setModel({ ...model, duration: +value })}
261         >
262             {[
263                 { text: c('Duration').t`30 minutes`, value: 30 },
264                 { text: c('Duration').t`60 minutes`, value: 60 },
265                 { text: c('Duration').t`90 minutes`, value: 90 },
266                 { text: c('Duration').t`120 minutes`, value: 120 },
267             ].map(({ value, text }) => (
268                 <Option key={value} value={value} title={text} />
269             ))}
270         </InputFieldTwo>
271     ) : null;
273     const defaultNotificationsRow = (
274         <>
275             <InputFieldTwo
276                 id="default-notification"
277                 as={Notifications}
278                 label={c('Label').t`Event notifications`}
279                 data-testid="create-calendar/event-settings:default-notification"
280                 hasType
281                 notifications={model.partDayNotifications}
282                 canAdd={model.partDayNotifications.length < MAX_DEFAULT_NOTIFICATIONS}
283                 defaultNotification={model.defaultPartDayNotification}
284                 onChange={(notifications: NotificationModel[]) => {
285                     setModel({
286                         ...model,
287                         partDayNotifications: notifications,
288                     });
289                 }}
290             />
292             <InputFieldTwo
293                 id="default-full-day-notification"
294                 as={Notifications}
295                 label={c('Label').t`All-day event notifications`}
296                 data-testid="create-calendar/event-settings:default-full-day-notification"
297                 hasType
298                 notifications={model.fullDayNotifications}
299                 canAdd={model.fullDayNotifications.length < MAX_DEFAULT_NOTIFICATIONS}
300                 defaultNotification={model.defaultFullDayNotification}
301                 onChange={(notifications: NotificationModel[]) => {
302                     setModel({
303                         ...model,
304                         fullDayNotifications: notifications,
305                     });
306                 }}
307             />
308         </>
309     );
311     const busySlotsCheckbox = (
312         <BusySlotsCheckbox
313             value={model.shareBusySlots}
314             onChange={(shareBusySlots) => setModel({ ...model, shareBusySlots })}
315         />
316     );
318     const subscribeURLRow =
319         subscribeURL &&
320         getFakeInputTwo({
321             content: (
322                 <span className="text-break-all">
323                     <TruncatedText maxChars={URL_MAX_DISPLAY_LENGTH}>{subscribeURL}</TruncatedText>
324                 </span>
325             ),
326             label: c('Label').t`URL`,
327         });
329     const getContentRows = (type: CALENDAR_MODAL_TYPE) => {
330         if (type === VISUAL) {
331             return (
332                 <>
333                     {calendarNameRow}
334                     {descriptionRow}
335                     {colorRow}
336                 </>
337             );
338         }
340         if (type === SHARED) {
341             return (
342                 <>
343                     {calendarNameRow}
344                     {colorRow}
345                     {addressRow}
346                     {descriptionRow}
347                     {defaultNotificationsRow}
348                     {busySlotsCheckbox}
349                 </>
350             );
351         }
353         return (
354             <>
355                 {calendarNameRow}
356                 {colorRow}
357                 {addressRow}
358                 {descriptionRow}
359                 {defaultEventDurationRow}
360                 {defaultNotificationsRow}
361                 {subscribeURLRow}
362                 {busySlotsCheckbox}
363             </>
364         );
365     };
367     return (
368         <ModalTwo
369             size={getCalendarModalSize(type)}
370             fullscreenOnMobile
371             className="w-full"
372             open={open}
373             onClose={onClose}
374             as={Form}
375             dense
376             onSubmit={() => {
377                 if (!loadingAction) {
378                     handleSubmit();
379                 }
380             }}
381             onExit={onExit}
382         >
383             {loadingSetup ? (
384                 <Loader />
385             ) : (
386                 <>
387                     <ModalTwoHeader title={getTitle(type)} subline={getSubline()} />
388                     <ModalTwoContent className="calendar-modal-content">
389                         {hasError ? <GenericError /> : getContentRows(type)}
390                     </ModalTwoContent>
391                     <ModalTwoFooter>
392                         {hasError ? (
393                             <Button onClick={() => window.location.reload()} className="ml-auto" color="norm">
394                                 {c('Action').t`Close`}
395                             </Button>
396                         ) : (
397                             <>
398                                 <Button onClick={onClose} disabled={loadingAction}>
399                                     {c('Action').t`Cancel`}
400                                 </Button>
401                                 <Button loading={loadingAction} type="submit" color="norm">
402                                     {c('Action').t`Save`}
403                                 </Button>
404                             </>
405                         )}
406                     </ModalTwoFooter>
407                 </>
408             )}
409         </ModalTwo>
410     );
413 export default PersonalCalendarModal;