1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2 import { useDispatch, useSelector } from 'react-redux';
3 import { useHistory } from 'react-router-dom';
5 import type { FormikTouched } from 'formik';
6 import { type FormikContextType } from 'formik';
8 import { itemEq } from '@proton/pass/lib/items/item.predicates';
9 import { draftDiscard, draftSave } from '@proton/pass/store/actions';
10 import type { Draft, DraftBase } from '@proton/pass/store/reducers';
11 import { selectItemDrafts } from '@proton/pass/store/selectors';
12 import type { MaybeNull } from '@proton/pass/types';
13 import { first } from '@proton/pass/utils/array/first';
14 import debounce from '@proton/utils/debounce';
16 const SAVE_DRAFT_TIMEOUT = 500;
17 const DRAFT_HASH = '#draft';
19 export const useMatchDraftHash = (): boolean => {
20 const { location } = useHistory();
21 return useMemo(() => location.hash === DRAFT_HASH, []);
24 type UseItemDraftOptions<V extends {}> = DraftBase & {
25 /** Apply sanitization over the draft values */
26 sanitizeHydration?: (formData: Draft<V>['formData']) => Draft<V>['formData'];
27 /** Callback called right before saving the draft to state if you
28 * need to run some extra sanity checks */
29 sanitizeSave?: (formData: Draft<V>['formData']) => Draft<V>['formData'];
30 /** Retrieve the sanitized draft values after form hydration.
31 * This may be useful when chaining multiple form `setValue`
32 * calls inside the same render cycle to avoid `form.values`
33 * containing stale values (ie: see `Alias.new`) */
34 onHydrated?: (hydration: MaybeNull<Draft<V>['formData']>) => void;
37 /** Everytime the passed values change, this hook triggers a debounced
38 * dispatch with the form data. The `itemDraft` action throttles caching
39 * to avoid swarming the service worker with encryption requests */
40 export const useItemDraft = <V extends {}>(form: FormikContextType<V>, options: UseItemDraftOptions<V>) => {
41 const history = useHistory();
42 const isDraft = useMatchDraftHash();
43 const drafts = useSelector(selectItemDrafts, () => true);
44 const draft = useMemo(() => (isDraft ? first(drafts) : undefined), []);
46 const shouldInvalidate = useRef(
48 const isEdit = draft === undefined && options.mode === 'edit';
49 return isEdit && drafts.some((entry) => entry.mode === 'edit' && itemEq(options)(entry));
53 const { onHydrated, sanitizeHydration, sanitizeSave, ...draftOptions } = options;
54 const { values, dirty } = form;
55 const [ready, setReady] = useState<boolean>(false);
57 const dispatch = useDispatch();
59 const saveDraft = useCallback(
61 (formData: V) => dispatch(draftSave({ ...draftOptions, formData: sanitizeSave?.(formData) ?? formData })),
69 const { location } = history;
70 const { hash } = location;
74 if (hash !== DRAFT_HASH) history.replace({ ...location, hash: 'draft' });
77 if (shouldInvalidate.current) {
78 dispatch(draftDiscard(draftOptions));
79 shouldInvalidate.current = false;
80 history.replace({ ...location, hash: '' });
85 return () => saveDraft.cancel();
86 }, [ready, values, dirty]);
91 const formValues = sanitizeHydration?.(draft.formData) ?? draft.formData;
93 await form.setTouched(
94 Object.keys(formValues).reduce<FormikTouched<any>>((touched, field) => {
95 touched[field] = true;
101 await form.setValues(formValues, true);
103 form.setErrors(await form.validateForm(draft.formData));
104 onHydrated?.(formValues);
105 } else onHydrated?.(null);
111 /* discard the draft if the component is unmounted :
112 * this either means the item was successfully saved or
113 * that the edit/creation discarded */
114 dispatch(draftDiscard(draftOptions));