Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / components / modals / ShareLinkModal / ShareLinkSettingsModal.tsx
blob3d0f1ab3a359cd439503ac9c39c8c6f6f436d268
1 import type { ChangeEvent, FormEvent } from 'react';
2 import { useMemo, useState } from 'react';
4 import { getUnixTime } from 'date-fns';
5 import { c, msgid } from 'ttag';
7 import { Button } from '@proton/atoms';
8 import type { ModalProps } from '@proton/components';
9 import {
10     Alert,
11     InputFieldTwo,
12     Label,
13     ModalTwo,
14     ModalTwoContent,
15     ModalTwoFooter,
16     ModalTwoHeader,
17     PasswordInputTwo,
18     Toggle,
19     useConfirmActionModal,
20     useModalTwoStatic,
21     useToggle,
22 } from '@proton/components';
23 import { useLoading } from '@proton/hooks';
24 import { MAX_SHARED_URL_PASSWORD_LENGTH } from '@proton/shared/lib/drive/constants';
25 import clsx from '@proton/utils/clsx';
27 import { ExpirationTimeDatePicker } from './PublicSharing';
29 interface Props {
30     initialExpiration: number | null;
31     customPassword: string;
32     isDeleting?: boolean;
33     stopSharing: () => Promise<void>;
34     onSaveLinkClick: (
35         password?: string,
36         duration?: number | null
37     ) => Promise<void | (unknown & { expirationTime: number | null })>;
38     modificationDisabled: boolean;
39     confirmationMessage: string;
40     havePublicSharedLink: boolean;
41     isShareUrlEnabled: boolean;
44 const SharingSettingsModal = ({
45     customPassword,
46     initialExpiration,
47     onSaveLinkClick,
48     isDeleting,
49     stopSharing,
50     modificationDisabled,
51     confirmationMessage,
52     havePublicSharedLink,
53     isShareUrlEnabled,
54     ...modalProps
55 }: Props & ModalProps) => {
56     const [password, setPassword] = useState(customPassword);
57     const [expiration, setExpiration] = useState(initialExpiration);
58     const [confirmActionModal, showConfirmActionModal] = useConfirmActionModal();
59     const [isSubmitting, withSubmitting] = useLoading();
60     const { state: passwordEnabled, toggle: togglePasswordEnabled } = useToggle(!!customPassword);
61     const { state: expirationEnabled, toggle: toggleExpiration } = useToggle(!!initialExpiration);
63     const isFormDirty = useMemo(() => {
64         // If initialExpiration or customPassword is empty, that means it was disabled
65         const expirationChanged = expiration !== initialExpiration || expirationEnabled !== !!initialExpiration;
66         const passwordChanged = password !== customPassword || passwordEnabled !== !!customPassword;
67         return Boolean(expirationChanged || passwordChanged);
68     }, [password, customPassword, passwordEnabled, expiration, initialExpiration, expirationEnabled]);
70     const handleClose = () => {
71         if (!isFormDirty) {
72             modalProps.onClose?.();
73             return;
74         }
76         void showConfirmActionModal({
77             title: c('Title').t`Discard changes?`,
78             submitText: c('Title').t`Discard`,
79             message: c('Info').t`You will lose all unsaved changes.`,
80             onSubmit: async () => modalProps.onClose?.(),
81             canUndo: true,
82         });
83     };
85     const handleStopSharing = async () => {
86         void showConfirmActionModal({
87             title: c('Title').t`Stop sharing?`,
88             submitText: c('Action').t`Stop sharing`,
89             message: confirmationMessage,
90             canUndo: true, // Just to hide the message
91             onSubmit: stopSharing,
92         });
93     };
95     const isPasswordInvalid = password.length > MAX_SHARED_URL_PASSWORD_LENGTH;
97     const isSaveDisabled = !isFormDirty || isDeleting || isPasswordInvalid;
99     const handleSubmit = async (e: FormEvent) => {
100         e.preventDefault();
102         const newCustomPassword = !passwordEnabled || !password ? '' : password;
103         const newExpiration = !expirationEnabled || !expiration ? null : expiration;
104         const newDuration = newExpiration ? newExpiration - getUnixTime(Date.now()) : null;
106         // Instead of blocking user action, we just save the form without sending a request
107         // For exemple if the user toggled the password field but don't put any password, we just don't do anything.
108         // This make the UX smoother
109         const needUpdate = newCustomPassword !== customPassword || newExpiration !== initialExpiration;
110         if (needUpdate) {
111             await withSubmitting(onSaveLinkClick(newCustomPassword, newDuration));
112         }
113         modalProps.onClose?.();
114     };
116     return (
117         <>
118             <ModalTwo
119                 as="form"
120                 size="large"
121                 onSubmit={handleSubmit}
122                 {...modalProps}
123                 onClose={handleClose}
124                 fullscreenOnMobile
125             >
126                 <ModalTwoHeader title={c('Title').t`Settings`} />
127                 <ModalTwoContent>
128                     {isShareUrlEnabled ? (
129                         <>
130                             {havePublicSharedLink && modificationDisabled && (
131                                 <Alert type="warning">
132                                     {c('Info')
133                                         .t`This link was created with old Drive version and can not be modified. Delete this link and create a new one to change the settings.`}
134                                 </Alert>
135                             )}
136                             <div
137                                 className="flex flex-column justify-space-between gap-2  md:items-center md:gap-0 md:flex-row md:h-custom md:items-center "
138                                 style={{ '--h-custom': '2.25rem' }}
139                                 data-testid="sharing-modal-settings-expirationSection"
140                             >
141                                 <Label
142                                     htmlFor="expirationDateInputId"
143                                     className={clsx(
144                                         'flex flex-column p-0 text-semibold',
145                                         !havePublicSharedLink && 'opacity-30'
146                                     )}
147                                 >
148                                     {c('Label').t`Set expiration date`}
149                                     <span className="color-weak text-normal">{c('Label')
150                                         .t`Public link expiration date`}</span>
151                                 </Label>
152                                 <div className="flex items-center justify-space-between gap-2 ">
153                                     <ExpirationTimeDatePicker
154                                         className="w-custom max-w-custom"
155                                         containerProps={{
156                                             style: { '--w-custom': '12.5rem', '--max-w-custom': '12.5rem' },
157                                         }}
158                                         id="expirationDateInputId"
159                                         disabled={!expirationEnabled}
160                                         allowTime={false}
161                                         expiration={expiration}
162                                         handleExpirationChange={setExpiration}
163                                         placeholder={c('Placeholder').t`Set date`}
164                                         data-testid="expiration-data-input"
165                                     />
166                                     <Toggle
167                                         disabled={!havePublicSharedLink}
168                                         id="toggleExpiration"
169                                         checked={expirationEnabled}
170                                         onChange={toggleExpiration}
171                                     />
172                                 </div>
173                             </div>
174                             <div
175                                 className="mt-5 flex  flex-column justify-space-between gap-2 md:flex-row md:gap-0 md:items-center md:h-custom w-auto md:flex-nowrap md:items-center"
176                                 style={{ '--h-custom': '2.25rem' }}
177                                 data-testid="sharing-modal-settings-passwordSection"
178                             >
179                                 <Label
180                                     className={clsx(
181                                         'flex flex-column p-0 text-semibold',
182                                         !havePublicSharedLink && 'opacity-30'
183                                     )}
184                                     htmlFor="sharing-modal-password"
185                                 >
186                                     {c('Label').t`Set link password`}
187                                     <span className="color-weak text-normal">{c('Label').t`Public link password`}</span>
188                                 </Label>
189                                 <div className="flex items-center justify-space-between gap-2 md:flex-nowrap">
190                                     <InputFieldTwo
191                                         disabled={!passwordEnabled}
192                                         dense
193                                         className="items-center"
194                                         rootClassName="flex items-center justify-end pr-0 w-custom"
195                                         rootStyle={{ '--w-custom': '12.5rem' }}
196                                         id="sharing-modal-password"
197                                         as={PasswordInputTwo}
198                                         data-testid="password-input"
199                                         label={false}
200                                         autoComplete="new-password"
201                                         value={password}
202                                         onInput={(e: ChangeEvent<HTMLInputElement>) => {
203                                             setPassword(e.target.value);
204                                         }}
205                                         error={
206                                             isPasswordInvalid &&
207                                             c('Info').ngettext(
208                                                 msgid`Max ${MAX_SHARED_URL_PASSWORD_LENGTH} character`,
209                                                 `Max ${MAX_SHARED_URL_PASSWORD_LENGTH} characters`,
210                                                 MAX_SHARED_URL_PASSWORD_LENGTH
211                                             )
212                                         }
213                                         placeholder={c('Placeholder').t`Choose password`}
214                                     />
215                                     <Toggle
216                                         id="togglePassword"
217                                         disabled={!havePublicSharedLink}
218                                         checked={passwordEnabled}
219                                         onChange={togglePasswordEnabled}
220                                     />
221                                 </div>
222                             </div>
223                             <hr className="my-5" />
224                         </>
225                     ) : null}
226                     <div
227                         className="flex flex-nowrap justify-space-between items-center"
228                         data-testid="share-modal-settings-deleteShareSection"
229                     >
230                         <div className="flex flex-column flex-1 p-0" data-testid="delete-share-text">
231                             <span className="text-semibold">{c('Label').t`Stop sharing`}</span>
232                             <span className="color-weak">{c('Label')
233                                 .t`Erase this link and remove everyone with access`}</span>
234                         </div>
235                         <Button
236                             className="flex items-center"
237                             shape="ghost"
238                             color="danger"
239                             onClick={handleStopSharing}
240                             data-testid="delete-share-button"
241                         >{c('Action').t`Stop sharing`}</Button>
242                     </div>
243                 </ModalTwoContent>
244                 <ModalTwoFooter>
245                     <Button onClick={handleClose}>{c('Action').t`Back`}</Button>
246                     <Button disabled={isSaveDisabled} loading={isSubmitting} color="norm" type="submit">{c('Action')
247                         .t`Save changes`}</Button>
248                 </ModalTwoFooter>
249             </ModalTwo>
250             {confirmActionModal}
251         </>
252     );
255 export default SharingSettingsModal;
257 export const useLinkSharingSettingsModal = () => {
258     return useModalTwoStatic(SharingSettingsModal);