Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / hooks / useActionRequest.ts
bloba4dc54e5338f5d44bd76898696323a28dc6f6aa0
1 import { useEffect, useMemo, useRef, useState } from 'react';
2 import { useDispatch, useSelector } from 'react-redux';
4 import type { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
5 import type { Action } from 'redux';
7 import { withRevalidate } from '@proton/pass/store/request/enhancers';
8 import type { RequestFlow } from '@proton/pass/store/request/flow';
9 import type { ActionRequestEntry, RequestMeta, WithRequest } from '@proton/pass/store/request/types';
10 import { selectRequest } from '@proton/pass/store/selectors';
11 import type { Maybe, MaybePromise } from '@proton/pass/types/utils';
13 export type UseActionRequestOptions<
14     IntentAction extends WithRequest<Action, 'start', any> = any,
15     SuccessAction extends WithRequest<Action, 'success', any> = any,
16     FailureAction extends WithRequest<Action, 'failure', any> = any,
17 > = {
18     initialLoading?: boolean;
19     initialRequestId?: string;
20     onStart?: (request: ActionRequestEntry<IntentAction>) => MaybePromise<void>;
21     onSuccess?: (request: ActionRequestEntry<SuccessAction>) => MaybePromise<void>;
22     onFailure?: (request: ActionRequestEntry<FailureAction>) => MaybePromise<void>;
25 /* `options` is wrapped in a ref to avoid setting it as
26  * a dependency to the status change effect. We only want
27  * to trigger the callbacks once. */
28 export const useActionRequest = <
29     IntentAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'start', any>>,
30     SuccessAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'success', any>>,
31     FailureAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'failure', any>>,
33     intent: IntentAction,
34     options?: UseActionRequestOptions<ReturnType<IntentAction>, ReturnType<SuccessAction>, ReturnType<FailureAction>>
35 ) => {
36     const dispatch = useDispatch();
37     const [loading, setLoading] = useState(options?.initialLoading ?? false);
38     const [error, setError] = useState(false);
39     const [requestId, setRequestId] = useState<string>(options?.initialRequestId ?? '');
40     const request = useSelector(selectRequest(requestId));
42     const optionsRef = useRef(options);
43     optionsRef.current = options;
45     const progress = (() => {
46         if (!request) return 0;
47         return request?.status === 'start' ? (request.progress ?? 0) : 100;
48     })();
50     useEffect(() => {
51         if (!request) return setLoading(false);
53         switch (request.status) {
54             case 'start':
55                 setLoading(true);
56                 setError(false);
57                 void optionsRef.current?.onStart?.(request as any);
58                 break;
59             case 'success':
60                 setLoading(false);
61                 setError(false);
62                 void optionsRef.current?.onSuccess?.(request as any);
63                 break;
64             case 'failure':
65                 setLoading(false);
66                 setError(true);
67                 void optionsRef.current?.onFailure?.(request as any);
68                 break;
69         }
70     }, [request]);
72     return useMemo(() => {
73         const actionCreator = (...args: Parameters<IntentAction>) => {
74             const action = intent(...args);
75             setRequestId(action.meta.request.id);
76             return action;
77         };
79         return {
80             dispatch: (...args: Parameters<IntentAction>) => {
81                 dispatch(actionCreator(...args));
82             },
83             revalidate: (...args: Parameters<IntentAction>) => {
84                 dispatch(withRevalidate(actionCreator(...args)));
85             },
86             progress,
87             loading,
88             error,
89         };
90     }, [request, progress, loading]);
93 type UseRequestOptions<T extends RequestFlow<any, any, any>> = {
94     initialLoading?: boolean;
95     initialRequestId?: string;
96     onStart?: (request: ActionRequestEntry<ReturnType<T['intent']>>) => MaybePromise<void>;
97     onFailure?: (request: ActionRequestEntry<ReturnType<T['failure']>>) => MaybePromise<void>;
98     onSuccess?: (request: ActionRequestEntry<ReturnType<T['success']>>) => MaybePromise<void>;
101 export const useRequest = <T extends RequestFlow<any, any, any>>(actions: T, options: UseRequestOptions<T>) => {
102     const [data, setData] = useState<Maybe<ActionRequestEntry<ReturnType<T['success']>>['data']>>();
104     const req = useActionRequest<T['intent'], T['success'], T['failure']>(actions.intent, {
105         ...options,
106         onSuccess: (req) => {
107             if (req.data) setData(req.data);
108             return options?.onSuccess?.(req);
109         },
110     });
112     return useMemo(() => ({ ...req, data }), [req, data]);