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';
19 useConfirmActionModal,
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';
30 initialExpiration: number | null;
31 customPassword: string;
33 stopSharing: () => Promise<void>;
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 = ({
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 = () => {
72 modalProps.onClose?.();
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?.(),
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,
95 const isPasswordInvalid = password.length > MAX_SHARED_URL_PASSWORD_LENGTH;
97 const isSaveDisabled = !isFormDirty || isDeleting || isPasswordInvalid;
99 const handleSubmit = async (e: FormEvent) => {
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;
111 await withSubmitting(onSaveLinkClick(newCustomPassword, newDuration));
113 modalProps.onClose?.();
121 onSubmit={handleSubmit}
123 onClose={handleClose}
126 <ModalTwoHeader title={c('Title').t`Settings`} />
128 {isShareUrlEnabled ? (
130 {havePublicSharedLink && modificationDisabled && (
131 <Alert type="warning">
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.`}
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"
142 htmlFor="expirationDateInputId"
144 'flex flex-column p-0 text-semibold',
145 !havePublicSharedLink && 'opacity-30'
148 {c('Label').t`Set expiration date`}
149 <span className="color-weak text-normal">{c('Label')
150 .t`Public link expiration date`}</span>
152 <div className="flex items-center justify-space-between gap-2 ">
153 <ExpirationTimeDatePicker
154 className="w-custom max-w-custom"
156 style: { '--w-custom': '12.5rem', '--max-w-custom': '12.5rem' },
158 id="expirationDateInputId"
159 disabled={!expirationEnabled}
161 expiration={expiration}
162 handleExpirationChange={setExpiration}
163 placeholder={c('Placeholder').t`Set date`}
164 data-testid="expiration-data-input"
167 disabled={!havePublicSharedLink}
168 id="toggleExpiration"
169 checked={expirationEnabled}
170 onChange={toggleExpiration}
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"
181 'flex flex-column p-0 text-semibold',
182 !havePublicSharedLink && 'opacity-30'
184 htmlFor="sharing-modal-password"
186 {c('Label').t`Set link password`}
187 <span className="color-weak text-normal">{c('Label').t`Public link password`}</span>
189 <div className="flex items-center justify-space-between gap-2 md:flex-nowrap">
191 disabled={!passwordEnabled}
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"
200 autoComplete="new-password"
202 onInput={(e: ChangeEvent<HTMLInputElement>) => {
203 setPassword(e.target.value);
208 msgid`Max ${MAX_SHARED_URL_PASSWORD_LENGTH} character`,
209 `Max ${MAX_SHARED_URL_PASSWORD_LENGTH} characters`,
210 MAX_SHARED_URL_PASSWORD_LENGTH
213 placeholder={c('Placeholder').t`Choose password`}
217 disabled={!havePublicSharedLink}
218 checked={passwordEnabled}
219 onChange={togglePasswordEnabled}
223 <hr className="my-5" />
227 className="flex flex-nowrap justify-space-between items-center"
228 data-testid="share-modal-settings-deleteShareSection"
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>
236 className="flex items-center"
239 onClick={handleStopSharing}
240 data-testid="delete-share-button"
241 >{c('Action').t`Stop sharing`}</Button>
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>
255 export default SharingSettingsModal;
257 export const useLinkSharingSettingsModal = () => {
258 return useModalTwoStatic(SharingSettingsModal);