Skip to content

Commit

Permalink
add useMutationsHandler in core-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
hervedombya committed Oct 5, 2023
1 parent b4d6888 commit 3ac52bd
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 0 deletions.
66 changes: 66 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@storybook/theming": "^6.4.22",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.1.9",
"@types/jest": "^27.5.0",
"@types/react": "^18.0.8",
Expand Down
40 changes: 40 additions & 0 deletions src/lib/components/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ReactNode, createContext, useContext, useState } from 'react';
import { Toast, ToastProps } from './Toast.component';

type ToastContextState = Omit<ToastProps, 'onClose'>;

export interface ToastContextType {
showToast: (toastProps: ToastContextState) => void;
}

export const ToastContext = createContext<ToastContextType | undefined>(
undefined,
);

interface ToastProviderProps {
children: ReactNode;
}
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
const [toastProps, setToastProps] = useState<ToastContextState | null>(null);

const showToast = (toastProps: ToastContextState) => {
setToastProps(toastProps);
};

return (
<ToastContext.Provider value={{ showToast }}>
{children}
{toastProps && (
<Toast {...toastProps} onClose={() => setToastProps(null)} />
)}
</ToastContext.Provider>
);
};

export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
131 changes: 131 additions & 0 deletions src/lib/components/toast/useMutationsHandler.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { MutationConfig, useMutationsHandler } from './useMutationsHandler';
import { useToast } from './ToastProvider';

jest.mock('./ToastProvider', () => ({
useToast: jest.fn(),
}));

const mockUseToast = useToast as jest.MockedFunction<typeof useToast>;

describe('useMutationsHandler', () => {
const mainMutation = {
mutation: {
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
},
name: 'mutation1',
} as MutationConfig<unknown, unknown>;
const dependantMutations = [
{
mutation: {
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
},
name: 'mutation2',
},
{
mutation: {
isLoading: false,
isSuccess: false,
isError: false,
isIdle: true,
},
name: 'mutation3',
isPrimary: false,
},
] as MutationConfig<unknown, unknown>[];

const messageDescriptionBuilder = jest.fn(() => 'message');

it('should call onPrimarySuccess when a primary mutation succeeds', async () => {
const showToast = jest.fn();
const onMainMutationSuccess = jest.fn();

mockUseToast.mockImplementation(() => ({
showToast,
}));

const { waitFor } = renderHook(() =>
useMutationsHandler({
mainMutation,
dependantMutations,
messageDescriptionBuilder,
onMainMutationSuccess,
}),
);

await act(async () => {
await waitFor(() => {
expect(onMainMutationSuccess).toHaveBeenCalled();
});
});
});

it('should show a success toast when all mutations succeed', async () => {
const showToast = jest.fn();

mockUseToast.mockImplementation(() => ({
showToast,
}));

const { waitFor } = renderHook(() =>
useMutationsHandler({
mainMutation,
dependantMutations,
messageDescriptionBuilder,
}),
);

await act(async () => {
await waitFor(() => {
expect(showToast).toHaveBeenCalledWith({
open: true,
status: 'success',
message: 'message',
});
});
});
});

it('should show an error toast when at least one mutation fails', async () => {
const showToast = jest.fn();

mockUseToast.mockImplementation(() => ({
showToast,
}));

const mutationsWithError = [
{
mutation: {
isLoading: false,
isSuccess: false,
isError: true,
},
name: 'mutation4',
},
] as MutationConfig<unknown, unknown>[];

const { waitFor } = renderHook(() =>
useMutationsHandler({
mainMutation,
dependantMutations: mutationsWithError,
messageDescriptionBuilder,
}),
);

await act(async () => {
await waitFor(() => {
expect(showToast).toHaveBeenCalledWith({
open: true,
status: 'error',
message: 'message',
});
});
});
});
});
96 changes: 96 additions & 0 deletions src/lib/components/toast/useMutationsHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { UseMutationResult } from 'react-query';
import { useToast } from './ToastProvider';

export type MutationConfig<Data, Variables> = {
mutation: UseMutationResult<Data, unknown, Variables, unknown>;
name: string;
};

type DescriptionBuilder<Data> = {
data?: Data;
error?: unknown;
name: string;
};

type MutationsHandlerProps<Data, Variables> = {
mainMutation: MutationConfig<Data, Variables>;
dependantMutations?: MutationConfig<Data, Variables>[];
messageDescriptionBuilder: (
successMutations: DescriptionBuilder<Data>[],
errorMutations: DescriptionBuilder<Data>[],
) => ReactNode;
toastStyles?: React.CSSProperties;
onMainMutationSuccess?: () => void;
};

export const useMutationsHandler = <Data, Variables>({
mainMutation,
dependantMutations,
messageDescriptionBuilder,
toastStyles,
onMainMutationSuccess,
}: MutationsHandlerProps<Data, Variables>) => {
const { showToast } = useToast();
const mutations = [
mainMutation,
...(dependantMutations ? dependantMutations : []),
];

const handleMutationsCompletion = useCallback(async () => {
const results = await Promise.all(mutations.map((m) => m.mutation));

const loadingMutations = mutations.filter(
(_, index) => results[index].isLoading,
);
const successMutations = mutations.filter(
(_, index) => results[index].isSuccess,
);
const errorMutations = mutations.filter(
(_, index) => results[index].isError,
);

const successDescriptionBuilder = successMutations.map((m) => ({
data: m.mutation?.data,
error: m.mutation?.error,
name: m.name,
}));

const errorDescriptionBuilder = errorMutations.map((m) => ({
data: m.mutation?.data,
error: m.mutation?.error,
name: m.name,
}));

if (loadingMutations.length === 0) {
if (errorMutations.length > 0) {
onMainMutationSuccess?.();
showToast({
open: true,
status: 'error',
message: messageDescriptionBuilder(
successDescriptionBuilder,
errorDescriptionBuilder,
),
style: toastStyles,
});
return;
} else if (successMutations.length > 0) {
onMainMutationSuccess?.();
showToast({
open: true,
status: 'success',
message: messageDescriptionBuilder(
successDescriptionBuilder,
errorDescriptionBuilder,
),
style: toastStyles,
});
}
}
}, [JSON.stringify(mutations)]);

useEffect(() => {
handleMutationsCompletion();
}, [handleMutationsCompletion]);
};
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ export { FormattedDateTime } from './components/date/FormattedDateTime';
export { IconHelp } from './components/IconHelper';
export { Dropzone } from './components/dropzone/Dropzone';
export { Toast } from './components/toast/Toast.component';
export { ToastProvider, useToast } from './components/toast/ToastProvider';
export { useMutationsHandler } from './components/toast/useMutationsHandler';

0 comments on commit 3ac52bd

Please sign in to comment.