From 54f7555dc5a5f20b28c4717db60b62622e8c4c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=92scar=20Casajuana?= Date: Fri, 13 Oct 2023 15:18:26 +0200 Subject: [PATCH] Move most actions logic to react-providers closes #109 --- packages/chakra-components/README.md | 6 +- .../components/Election/Actions/Actions.tsx | 2 +- .../Election/Actions/ActionsProvider.tsx | 38 ++++----- .../components/Election/Actions/Cancel.tsx | 5 +- .../components/Election/Actions/Continue.tsx | 5 +- .../src/components/Election/Actions/End.tsx | 5 +- .../src/components/Election/Actions/Pause.tsx | 5 +- .../Election/Actions/use-actions-toast.ts | 34 ++++++++ packages/react-providers/README.md | 78 ++++++++++++++++-- .../src/election/ActionsProvider.tsx} | 82 +++++++++++++------ .../react-providers/src/election/index.ts | 1 + 11 files changed, 194 insertions(+), 67 deletions(-) create mode 100644 packages/chakra-components/src/components/Election/Actions/use-actions-toast.ts rename packages/{chakra-components/src/components/Election/Actions/use-actions-provider.tsx => react-providers/src/election/ActionsProvider.tsx} (64%) diff --git a/packages/chakra-components/README.md b/packages/chakra-components/README.md index 8bebc4f2..0df5cdb7 100644 --- a/packages/chakra-components/README.md +++ b/packages/chakra-components/README.md @@ -36,7 +36,11 @@ const App = () => { > Beware that you'll also see a `ClientProvider` also from > `@vocdoni/react-providers`. You should always be using the one included with -> chakra components in order to have all the features they provide. +> `@vocdoni/chakra-components` in order to have all the features it provides. +> +> Note this also happens with other components and providers. If you are using +> `@vocdoni/chakra-components`, you should always prioritize its exports over +> the ones from `@vocdoni/react-providers`. Note `env` can be any of the [SDK available environments][sdk environments], either in string format, or using the SDK `EnvOptions` enum. diff --git a/packages/chakra-components/src/components/Election/Actions/Actions.tsx b/packages/chakra-components/src/components/Election/Actions/Actions.tsx index 70b6c05c..be703107 100644 --- a/packages/chakra-components/src/components/Election/Actions/Actions.tsx +++ b/packages/chakra-components/src/components/Election/Actions/Actions.tsx @@ -1,5 +1,5 @@ import { ButtonGroup, IconButton } from '@chakra-ui/button' -import { ChakraProps, chakra, useMultiStyleConfig } from '@chakra-ui/system' +import { chakra, ChakraProps, useMultiStyleConfig } from '@chakra-ui/system' import { useClient, useElection } from '@vocdoni/react-providers' import { areEqualHexStrings } from '@vocdoni/sdk' import { FaPause, FaPlay, FaStop } from 'react-icons/fa' diff --git a/packages/chakra-components/src/components/Election/Actions/ActionsProvider.tsx b/packages/chakra-components/src/components/Election/Actions/ActionsProvider.tsx index e9e385d6..602553c0 100644 --- a/packages/chakra-components/src/components/Election/Actions/ActionsProvider.tsx +++ b/packages/chakra-components/src/components/Election/Actions/ActionsProvider.tsx @@ -1,27 +1,19 @@ -import { PropsWithChildren, createContext, useContext } from 'react' -import { useActionsProvider } from './use-actions-provider' - -export type ActionsState = ReturnType - -export const ActionsContext = createContext(undefined) - -export const useActions = () => { - const ctxt = useContext(ActionsContext) - if (!ctxt) { - throw new Error( - 'useActions returned `undefined`, maybe you forgot to wrap the component within ?' - ) - } - - return ctxt +import { ActionsProvider as RActionsProvider } from '@vocdoni/react-providers' +import { PropsWithChildren, ReactElement } from 'react' +import { useActionsToast } from './use-actions-toast' + +export const ActionsProvider = (props: PropsWithChildren) => { + return ( + + + + ) } -export type ActionsProviderComponentProps = PropsWithChildren - -export const ActionsProvider = ({ children }: ActionsProviderComponentProps) => { - const value = useActionsProvider() +// We need to define an "internal" component in order to be able to use the +// hooks, otherwise they wouldn't have access to the ActionsProvider context +const ChakraInternalActionsProvider = ({ children }: PropsWithChildren) => { + useActionsToast() - return {children} + return children as ReactElement } - -ActionsProvider.displayName = 'ActionsProvider' diff --git a/packages/chakra-components/src/components/Election/Actions/Cancel.tsx b/packages/chakra-components/src/components/Election/Actions/Cancel.tsx index 1a89421a..df7fd56f 100644 --- a/packages/chakra-components/src/components/Election/Actions/Cancel.tsx +++ b/packages/chakra-components/src/components/Election/Actions/Cancel.tsx @@ -1,8 +1,7 @@ import { IconButtonProps } from '@chakra-ui/button' import { chakra, forwardRef } from '@chakra-ui/system' -import { useClient, useElection } from '@vocdoni/react-providers' -import { ElectionStatus, areEqualHexStrings } from '@vocdoni/sdk' -import { useActions } from './ActionsProvider' +import { useActions, useClient, useElection } from '@vocdoni/react-providers' +import { areEqualHexStrings, ElectionStatus } from '@vocdoni/sdk' export const ActionCancel = forwardRef((props, ref) => { const { account, localize } = useClient() diff --git a/packages/chakra-components/src/components/Election/Actions/Continue.tsx b/packages/chakra-components/src/components/Election/Actions/Continue.tsx index 42f8e6fa..de96a7cc 100644 --- a/packages/chakra-components/src/components/Election/Actions/Continue.tsx +++ b/packages/chakra-components/src/components/Election/Actions/Continue.tsx @@ -1,8 +1,7 @@ import { IconButtonProps } from '@chakra-ui/button' import { chakra, forwardRef } from '@chakra-ui/system' -import { useClient, useElection } from '@vocdoni/react-providers' -import { ElectionStatus, areEqualHexStrings } from '@vocdoni/sdk' -import { useActions } from './ActionsProvider' +import { useActions, useClient, useElection } from '@vocdoni/react-providers' +import { areEqualHexStrings, ElectionStatus } from '@vocdoni/sdk' export const ActionContinue = forwardRef((props, ref) => { const { account, localize } = useClient() diff --git a/packages/chakra-components/src/components/Election/Actions/End.tsx b/packages/chakra-components/src/components/Election/Actions/End.tsx index 141e145e..dff9ea20 100644 --- a/packages/chakra-components/src/components/Election/Actions/End.tsx +++ b/packages/chakra-components/src/components/Election/Actions/End.tsx @@ -1,8 +1,7 @@ import { IconButtonProps } from '@chakra-ui/button' import { chakra, forwardRef } from '@chakra-ui/system' -import { useClient, useElection } from '@vocdoni/react-providers' -import { ElectionStatus, areEqualHexStrings } from '@vocdoni/sdk' -import { useActions } from './ActionsProvider' +import { useActions, useClient, useElection } from '@vocdoni/react-providers' +import { areEqualHexStrings, ElectionStatus } from '@vocdoni/sdk' export const ActionEnd = forwardRef((props, ref) => { const { account, localize } = useClient() diff --git a/packages/chakra-components/src/components/Election/Actions/Pause.tsx b/packages/chakra-components/src/components/Election/Actions/Pause.tsx index aec86c28..541076d6 100644 --- a/packages/chakra-components/src/components/Election/Actions/Pause.tsx +++ b/packages/chakra-components/src/components/Election/Actions/Pause.tsx @@ -1,8 +1,7 @@ import { IconButtonProps } from '@chakra-ui/button' import { chakra, forwardRef } from '@chakra-ui/system' -import { useClient, useElection } from '@vocdoni/react-providers' -import { ElectionStatus, areEqualHexStrings } from '@vocdoni/sdk' -import { useActions } from './ActionsProvider' +import { useActions, useClient, useElection } from '@vocdoni/react-providers' +import { areEqualHexStrings, ElectionStatus } from '@vocdoni/sdk' export const ActionPause = forwardRef((props, ref) => { const { account, localize } = useClient() diff --git a/packages/chakra-components/src/components/Election/Actions/use-actions-toast.ts b/packages/chakra-components/src/components/Election/Actions/use-actions-toast.ts new file mode 100644 index 00000000..cab0cf63 --- /dev/null +++ b/packages/chakra-components/src/components/Election/Actions/use-actions-toast.ts @@ -0,0 +1,34 @@ +import { ToastId, useToast } from '@chakra-ui/toast' +import { useActions } from '@vocdoni/react-providers' +import { useEffect, useRef } from 'react' + +export const useActionsToast = () => { + const tRef = useRef() + const { info, error } = useActions() + const toast = useToast() + + // show toasts for info and error + useEffect(() => { + if (toast && info === null && tRef.current) { + toast.close(tRef.current) + } + if (info && toast) { + tRef.current = toast({ + title: info.title, + description: info.description, + status: 'info', + duration: null, + isClosable: false, + }) + } + if (error && toast) { + toast({ + title: error.title, + description: error.description, + status: 'error', + duration: 7000, + isClosable: false, + }) + } + }, [info, error, toast]) +} diff --git a/packages/react-providers/README.md b/packages/react-providers/README.md index dc972801..29fd0161 100644 --- a/packages/react-providers/README.md +++ b/packages/react-providers/README.md @@ -20,7 +20,7 @@ The very first step is to add the `` as a wrapper of your application or, at least, of your election: ~~~tsx -import { ClientProvider } from '@vocdoni/chakra-components' +import { ClientProvider } from '@vocdoni/react-providers' const App = () => { const signer = /* any ethers based signer */ @@ -35,13 +35,81 @@ const App = () => { `ClientProvider` is a dependency of the other providers, so you'll have to ensure you initialize that one as the parent. +### ElectionProvider + +The `ElectionProvider` is the one that will allow you to interact with the +election and easily execute actions like voting. + +You can use it as a wrapper of your elections: + +~~~tsx +import { ElectionProvider } from '@vocdoni/react-providers' + +const MyElection = () => { + return ( + + {/* your actual election code */} + + ) +} +~~~ + +You can either specify an id, and the provider will fetch the election for you, +or you can directly pass an `election` object. This is usefull in case you want +to render a list of elections and you already have the data: + +~~~tsx +import { ElectionProvider } from '@vocdoni/react-providers' + +const MyElectionList = () => { + const elections = /* your elections list */ + return ( + elections.map((election)=> ( + + + + )) + ) +} +~~~ + +Once you initialized the provider, you can use the `useElection` hook to +interact with the election: + + +~~~tsx +import { useElection } from '@vocdoni/react-providers' + +const MyElectionListItem = () => { + const { election } = useElection() + + return

{election.title.default}

+} +~~~ + +### OrganizationProvider + +Works more or less the same as the `ElectionProvider`, but for organizations: + +~~~tsx +import { OrganizationProvider } from '@vocdoni/react-providers' + +const MyElection = () => { + return ( + + {/* your actual organization code */} + + ) +} +~~~ + ### hooks - `useClient` allows you to interact with the `ClientProvider` layer. All the methods it exports allow you to use the client while interacting with the context/state: - + `fetchAccount` - + `createAccount` + + `fetchAccount`: fetches connected account information (and balance) + + `createAccount`: creates a new account using the connected signer + `setClient`: allows you to change the client during runtime + `localize`: internal method used for localization + `setSigner`: allows you to change the signer during runtime @@ -53,8 +121,8 @@ ensure you initialize that one as the parent. (used by flows like the spreadsheet/csv login one) + `vote`: A helper method to vote, using the current context info. - `useOrganization`: - + `fetch` - + `update` + + `fetch`: fetches the organization data + + `update`: update the organization (only for organization owners) License ------- diff --git a/packages/chakra-components/src/components/Election/Actions/use-actions-provider.tsx b/packages/react-providers/src/election/ActionsProvider.tsx similarity index 64% rename from packages/chakra-components/src/components/Election/Actions/use-actions-provider.tsx rename to packages/react-providers/src/election/ActionsProvider.tsx index 2a3475f4..1d7c1fb7 100644 --- a/packages/chakra-components/src/components/Election/Actions/use-actions-provider.tsx +++ b/packages/react-providers/src/election/ActionsProvider.tsx @@ -1,6 +1,5 @@ -import { ToastId, useToast } from '@chakra-ui/toast' import { useClient, useElection } from '@vocdoni/react-providers' -import { useRef, useState } from 'react' +import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' type LoadingState = { continue: boolean @@ -16,37 +15,36 @@ const BaseLoadingState: LoadingState = { cancel: false, } +export type ActionsStatusMessage = { + title: string + description?: string +} + export const useActionsProvider = () => { - const toast = useToast() - const tRef = useRef() const { localize } = useClient() const { client, election, fetchElection } = useElection() const [loading, setLoading] = useState(BaseLoadingState) + const [error, setError] = useState(null) + const [info, setInfo] = useState(null) const load = (key: keyof LoadingState) => setLoading((loading) => ({ ...loading, [key]: true })) const close = () => { setLoading(BaseLoadingState) - if (tRef.current) { - toast.close(tRef.current) - } + setInfo(null) } - const info = (description: string) => { - tRef.current = toast({ + const infostate = (description: string) => { + setInfo({ title: localize('actions.waiting_title'), description, - isClosable: false, - duration: null, }) } - const error = (description: string) => { - toast({ - description, + const errorstate = (description: string) => { + setError({ title: localize('actions.error_title'), - duration: 7000, - status: 'error', + description, }) } @@ -56,7 +54,7 @@ export const useActionsProvider = () => { } load('cancel') - info( + infostate( localize('actions.cancel_description', { election, }) @@ -67,7 +65,7 @@ export const useActionsProvider = () => { await fetchElection(election.id) } catch (e: any) { if (typeof e === 'string') { - return error(e) + return errorstate(e) } console.warn('catched error in "cancel" action', e) } finally { @@ -81,7 +79,7 @@ export const useActionsProvider = () => { } load('end') - info( + infostate( localize('actions.end_description', { election, }) @@ -92,7 +90,7 @@ export const useActionsProvider = () => { await fetchElection(election.id) } catch (e: any) { if (typeof e === 'string') { - return error(e) + return errorstate(e) } console.warn('catched error in "end" action', e) } finally { @@ -106,7 +104,7 @@ export const useActionsProvider = () => { } load('pause') - info( + infostate( localize('actions.pause_description', { election, }) @@ -117,7 +115,7 @@ export const useActionsProvider = () => { await fetchElection(election.id) } catch (e: any) { if (typeof e === 'string') { - return error(e) + return errorstate(e) } console.warn('catched error in "pause" action', e) } finally { @@ -131,7 +129,7 @@ export const useActionsProvider = () => { } load('continue') - info( + infostate( localize('actions.continue_description', { election, }) @@ -142,7 +140,7 @@ export const useActionsProvider = () => { await fetchElection(election.id) } catch (e: any) { if (typeof e === 'string') { - return error(e) + return errorstate(e) } console.warn('catched error in "continue" action', e) } finally { @@ -150,12 +148,46 @@ export const useActionsProvider = () => { } } + const disabled = Object.values(loading).some((value) => value === true) + + // clear errors when election or loading status change + useEffect(() => { + if (election && !disabled) { + setError(null) + } + }, [disabled, election]) + return { - disabled: Object.values(loading).some((value) => value === true), + disabled, cancel, end, + error, + info, loading, pause, resume, } } + +export type ActionsState = ReturnType + +export const ActionsContext = createContext(undefined) + +export const useActions = () => { + const ctxt = useContext(ActionsContext) + if (!ctxt) { + throw new Error( + 'useActions returned `undefined`, maybe you forgot to wrap the component within ?' + ) + } + + return ctxt +} + +export const ActionsProvider = (props: PropsWithChildren) => { + const value = useActionsProvider() + + return +} + +ActionsProvider.displayName = 'ActionsProvider' diff --git a/packages/react-providers/src/election/index.ts b/packages/react-providers/src/election/index.ts index bc3ccafc..593cf942 100644 --- a/packages/react-providers/src/election/index.ts +++ b/packages/react-providers/src/election/index.ts @@ -1,2 +1,3 @@ +export * from './ActionsProvider' export * from './ElectionProvider' export type { ElectionProviderProps } from './use-election-provider'