From 5979768790c0119f047ef084c5ac45a10b5d9418 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 30 May 2024 14:02:49 +0200 Subject: [PATCH 1/2] add useMutationhandler documentation --- .../components/toast/useMutationsHandler.ts | 2 +- .../usemutationshandler.guideline.mdx | 156 ++++++++++ .../usemutationshandler.stories.tsx | 291 ++++++++++++++++++ 3 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 stories/useMutationsHandler/usemutationshandler.guideline.mdx create mode 100644 stories/useMutationsHandler/usemutationshandler.stories.tsx diff --git a/src/lib/components/toast/useMutationsHandler.ts b/src/lib/components/toast/useMutationsHandler.ts index aa677fea29..d80897a612 100644 --- a/src/lib/components/toast/useMutationsHandler.ts +++ b/src/lib/components/toast/useMutationsHandler.ts @@ -86,7 +86,7 @@ type Props = { | { onAllMutationsSuccess?: () => void; onMainMutationSuccess?: never } ); -type MinimalMutationResult = Pick< +export type MinimalMutationResult = Pick< UseMutationResult, 'isError' | 'isIdle' | 'isSuccess' | 'isLoading' | 'error' | 'data' >; diff --git a/stories/useMutationsHandler/usemutationshandler.guideline.mdx b/stories/useMutationsHandler/usemutationshandler.guideline.mdx new file mode 100644 index 0000000000..29c7ff7f1d --- /dev/null +++ b/stories/useMutationsHandler/usemutationshandler.guideline.mdx @@ -0,0 +1,156 @@ +import * as Stories from './usemutationshandler.stories'; +import { Meta, Controls, Story, Canvas, Source } from '@storybook/blocks'; + + + +# useMutationsHandler + +useMutationsHandler is a hook to simplify the handling of mutations and the display of feedback to the user. +It provide a way to define a callback to call on success and a toast to display after the fail or success of a mutation. +It also allows to define a single toast and callback for multiple related mutations instead of managing each mutation individually. + +## Properties + + + +## Required properties + +Only two properties are required to use this hook : mainMutation ans messageDescriptionBuilder. +Like all other hook, it should be called at the top of your component. + +### mainMutation + +The mainMutation object contains two properties : name and mutation. + +mutation is the call of useMutation hook from react-query, see here from more information about this hook. +The result should at least contain these properties from the hook : + +- data: data returned on success +- error: error message on error +- isError: boolean; +- isIdle: boolean; +- isLoading: boolean; +- isSuccess: boolean; + +### messageDescriptionBuilder + +The messageDescriptionBuilder takes as parameters an array of mutations and returns a React Node. +This array is composed of objects derived from mainMutation result and the eventual result of the mutations from dependentMutation array. + +This allows the rendering of a single message on success or failure, even when there are multiple mutations. +As the return type is a React node it is possible to display simple message as well as more complex and detailed message. + +### Minimal Example + +```jsx +const mainMutation = useMutation({}); +useMutationsHandler({ + mainMutation: { + mutation: mainMutation, + name: 'Main Mutation', + }, + messageDescriptionBuilder: (mutations) => { + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + return `One or more mutations failed`; + } + }, +}); +``` + +It is then possible to call the mutation with : + +```jsx +const { mutate } = mainMutation; +const handleClick = () => { + mutate(); +}; +``` + +Click on the button below to see the hook in action: + + + +## Optional properties + +### dependentMutations + +The dependent mutations is an optional property containing an array of mutations that should be called +only after the main mutation succeed. +The type of the mutation is the same as mainMutation. + +
+ Show example : + + + +```jsx +useMutationsHandler({ + mainMutation: { mutation: mainMutation, name: 'Main Mutation' }, + dependantMutations, + messageDescriptionBuilder: (mutations) => { + console.log('mutations', mutations); + + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + const failedMutations = mutations.filter( + (mutation) => mutation.status === 'error', + ); + return ( +
+ You can adapt this text to provide more info to the user
+ For example with a list of the failed mutations: +
    + {failedMutations.map((mutation) => ( +
  • + {mutation.name} failed: {mutation.error} +
  • + ))} +
+
+ ); + } + }, + toastProps: { + style: { width: '40rem' }, + }, +}); +``` + +
+ +### Toast props + +The toastProps parameter is an object containing a selection of Toast props (for more info on Toast component, see [here](?path=/docs/components-feedback-toast--stories)). +This allows the customization of the toast properties : + + + + +### Callbacks + +There are 2 possible callbacks for the useMutationshandlers hook : onMainMutationSuccess and onAllMutationSuccess. +It is not possible to call both, only one callback should be chosen depending on the wanted behaviour. + +```jsx +onMainMutationSuccess: () => { + // your code here +}; +``` + +```jsx +onAllMutationsSuccess: () => { + // your code here +}; +``` diff --git a/stories/useMutationsHandler/usemutationshandler.stories.tsx b/stories/useMutationsHandler/usemutationshandler.stories.tsx new file mode 100644 index 0000000000..bfee22ae3e --- /dev/null +++ b/stories/useMutationsHandler/usemutationshandler.stories.tsx @@ -0,0 +1,291 @@ +import { Meta } from '@storybook/react'; +import { spacing, useMutationsHandler } from '../../src/lib'; + +import { Button } from '../../src/lib/components/buttonv2/Buttonv2.component'; + +import { useMutation } from 'react-query'; + +const meta: Meta = { + title: 'Hooks/useMutationsHandler', +}; +const successMutation = () => { + return useMutation({ + mutationFn: async () => { + return Promise.resolve('success'); + }, + }); +}; +const errorMutation = () => { + return useMutation({ + mutationFn: () => { + const res = Promise.reject("Don't worry, this is an expected error"); + return res; + }, + }); +}; + +export const Default = { + render: () => { + const mainMutation = errorMutation(); + + const dependantMut1 = successMutation(); + const dependantMutations = [ + { mutation: dependantMut1, name: 'Dependant Mutation 1' }, + ]; + const { mutate: test1, reset } = mainMutation; + const { mutate: test2 } = dependantMut1; + + useMutationsHandler({ + mainMutation: { mutation: mainMutation, name: 'Main Mutation' }, + dependantMutations, + messageDescriptionBuilder: (mutations) => { + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + return `One or more mutations failed`; + } + }, + + onMainMutationSuccess: () => { + reset(); + }, + }); + const handleClick = () => { + // test1(null, { + // onSuccess: () => test2(), + // }); + test2(); + }; + + return ( +
+
+ ); + }, + argTypes: { + mainMutation: { + description: + 'The object containing the main mutation to be executed and its name', + }, + dependantMutations: { + description: + 'An array of objects containing the dependant mutations to be executed and their names', + }, + messageDescriptionBuilder: { + description: + 'A function that takes an array of mutations and returns a ReactNode use to build the toast message', + }, + toastProps: { + description: 'Props to customize the toast', + }, + onMainMutationSuccess: { + description: + 'Callback to be executed when the main mutation is successful', + }, + onAllMutationsSuccess: { + description: 'Callback to be executed when all mutations are successful', + }, + }, +}; + +export default meta; + +// use onSuccess to call the dependent mutations +// after the main mutation is successful +// if the main mutation is an error, the dependent mutations will not be called +// resulting in an error meassage (one or more mutations failed) +export const MainMutationSuccess = { + render: ({ args }) => { + const mainMutation = successMutation(); + const { mutate } = mainMutation; + useMutationsHandler({ + mainMutation: { mutation: mainMutation, name: 'Main Mutation' }, + messageDescriptionBuilder: (mutations) => { + const mutationSuccess = mutations.every( + (mutation) => mutation.status === 'success', + ); + if (mutationSuccess) { + return `Mutation succeeded`; + } else { + return `Mutation failed`; + } + }, + }); + + const handleClick = () => { + mutate(); + }; + + return ( +
+
+ ); + }, +}; + +export const CustomToastStyle = { + render: ({ position, autoDismiss, duration, withProgressBar }) => { + const mainMutation = successMutation(); + + const { mutate, reset } = mainMutation; + + useMutationsHandler({ + mainMutation: { mutation: mainMutation, name: 'Main Mutation' }, + messageDescriptionBuilder: (mutations) => { + console.log('mutations', mutations); + + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + return `One or more mutations failed`; + } + }, + toastProps: { + position, + autoDismiss, + duration, + withProgressBar, + }, + onMainMutationSuccess: () => { + reset(); + }, + }); + const handleClick = () => { + mutate(); + }; + + return ( +
+
+ ); + }, + args: { + position: 'top-right', + autoDismiss: true, + duration: 5000, + withProgressBar: false, + }, + argTypes: { + autoDismiss: { + control: 'boolean', + description: 'Whether the toast should dismiss automatically', + }, + duration: { + control: 'number', + description: 'The duration before the toast dismisses', + if: { arg: 'autoDismiss' }, + }, + withProgressBar: { + control: 'boolean', + description: 'Whether the toast should display a progress bar', + }, + position: { + control: { + type: 'select', + }, + options: [ + 'top-left', + 'top-right', + 'top-center', + 'bottom-left', + 'bottom-right', + 'bottom-center', + ], + description: 'The position of the toast', + }, + }, + parameters: { + controls: { expanded: true }, + }, +}; + +export const CustomMessageDescriptionBuilder = { + render: () => { + const mainMutation = successMutation(); + const dependantMut1 = successMutation(); + const dependantMut2 = errorMutation(); + const dependantMutations = [ + { mutation: dependantMut1, name: 'Dependant Mutation 1' }, + { mutation: dependantMut2, name: 'Dependant Mutation 2' }, + ]; + + const { mutate, reset } = mainMutation; + const { mutate: dependantMutate1 } = dependantMut1; + const { mutate: dependantMutate2 } = dependantMut2; + + useMutationsHandler({ + mainMutation: { mutation: mainMutation, name: 'Main Mutation' }, + dependantMutations, + messageDescriptionBuilder: (mutations) => { + console.log('mutations', mutations); + + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + const failedMutations = mutations.filter( + (mutation) => mutation.status === 'error', + ); + return ( +
+ You can adapt this text to provide more info to the user
+ For example with a list of the failed mutations: +
    + {failedMutations.map((mutation) => ( +
  • + {mutation.name} failed: {mutation.error} +
  • + ))} +
+
+ ); + } + }, + + onMainMutationSuccess: () => { + reset(); + }, + toastProps: { + style: { width: '40rem' }, + }, + }); + const handleClick = () => { + mutate(void 0, { + onSuccess() { + dependantMutate1(); + dependantMutate2(); + }, + }); + }; + + return ( +
+
+ ); + }, +}; From 7f07b52a17d74f3a65cd6796c9f724cef126a587 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 30 May 2024 15:07:35 +0200 Subject: [PATCH 2/2] change docs, add complete example --- stories/Hooks/useMutationsHandler.mdx | 121 ++++++++ .../usemutationshandler.guideline.mdx | 156 ---------- .../usemutationshandler.stories.tsx | 291 ------------------ 3 files changed, 121 insertions(+), 447 deletions(-) create mode 100644 stories/Hooks/useMutationsHandler.mdx delete mode 100644 stories/useMutationsHandler/usemutationshandler.guideline.mdx delete mode 100644 stories/useMutationsHandler/usemutationshandler.stories.tsx diff --git a/stories/Hooks/useMutationsHandler.mdx b/stories/Hooks/useMutationsHandler.mdx new file mode 100644 index 0000000000..28cc9d7761 --- /dev/null +++ b/stories/Hooks/useMutationsHandler.mdx @@ -0,0 +1,121 @@ +# useMutationsHandler + +The `useMutationsHandler` hook is a powerful utility designed to manage and handle the results of multiple mutations using `react-query`. +It provides a unified way to handle success and error states, display toast notifications, +and manage side effects after mutation completions. + +## Usage + +```jsx +import { useMutationsHandler } from '@scality/core-ui'; +import { Button } from '@scality/core-ui/dist/next'; +import { useMutation } from 'react-query'; + +function useMainMutation() { + return useMutation({ + mutationFn: () => { + return Promise.resolve('mainMutation'); + }, + }); +} + +function useDependantMutation() { + return useMutation({ + mutationFn: () => { + return Promise.resolve('dependantMutation'); + }, + }); +} + +export const ExampleComponent = () => { + const useMainM = useMainMutation(); + const useDependantM = useDependantMutation(); + const { mutate: mainMutate } = useMainM; + const { mutate: dependantMutate } = useDependantM; + + const mainMutation = { + mutation: useMainM, + name: 'mainMutation', + }; + + const dependantMutations = [ + { + mutation: useDependantM, + name: 'depandantMutation', + }, + ]; + + useMutationsHandler({ + mainMutation, + dependantMutations, + messageDescriptionBuilder: (mutations) => { + const mutationsStatus = mutations.map(({ status }) => status); + const mutationsAllSuccess = mutationsStatus.every( + (status) => status === 'success', + ); + + if (mutationsAllSuccess) { + return `All mutations were successful`; + } else { + return `Some mutations failed`; + } + }, + onAllMutationsSuccess: () => { + // do something after success of all mutations + }, + }); + + const handleClick = () => { + mainMutate(null, { + onSuccess: () => dependantMutate(), + }); + }; + + return ( +