import { Check, Link } from '@mui/icons-material' import { ComponentType, useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { SuspenseLoaderProps, UnifiedCosmosMsg } from '@dao-dao/types' import { Action, ActionAndData, ActionKeyAndData, IActionDecoder, } from '@dao-dao/types/actions' import { useActionMatcher } from '../../contexts' import { useLoadingPromise, useUpdatingRef } from '../../hooks' import { ActionMatcherProvider } from '../../providers' import { ErrorPage } from '../error' import { IconButton } from '../icon_buttons' import { Loader } from '../logo' import { ActionCard, ActionCardLoader } from './ActionCard' export const ACTIONS_PER_PAGE = 20 export type ActionsRendererProps = { /** * Array of actions with data or action decoders from the matcher. */ actionData: (ActionAndData | IActionDecoder)[] hideCopyLink?: boolean onCopyLink?: () => void /** * Callback when all actions and data are loaded. */ onLoad?: (data: ActionKeyAndData[]) => void SuspenseLoader: ComponentType } // Groups actions together and renders uneditable cards. export const ActionsRenderer = ({ actionData, hideCopyLink, onCopyLink, onLoad, SuspenseLoader, }: ActionsRendererProps) => { const [copied, setCopied] = useState() // Unset copied after 2 seconds. useEffect(() => { const timeout = setTimeout(() => setCopied(undefined), 2000) // Cleanup on unmount. return () => clearTimeout(timeout) }, [copied]) const loadingAllActionsWithData = useLoadingPromise({ promise: () => Promise.all( actionData.map(async (data, index): Promise => { if ('decode' in data) { return { _id: index.toString(), actionKey: data.action.key, data: await data.decode().catch(() => ({})), } } else { return { _id: index.toString(), actionKey: data.action.key, data: data.data, } } }) ), deps: [actionData], }) // Call onLoad callback once all actions and data are loaded. const onLoadRef = useUpdatingRef(onLoad) const onLoadDefined = !!onLoad useEffect(() => { if ( !loadingAllActionsWithData.loading && !loadingAllActionsWithData.errored && !loadingAllActionsWithData.updating ) { onLoadRef.current?.(loadingAllActionsWithData.data) } }, [ loadingAllActionsWithData, onLoadRef, // Make sure to re-run the effect if the callback becomes defined. onLoadDefined, ]) return (
{actionData.map((data, index) => (
{!hideCopyLink && ( { const url = new URL(window.location.href) url.hash = '#' + `A${index + 1}` navigator.clipboard.writeText(url.href) setCopied(index) onCopyLink?.() }} size="sm" variant="none" /> )}
))}
) } export type ActionRendererProps< Data extends Record = Record, > = ({ action: Action } & ( | { data: Data decoder?: never } | { decoder: IActionDecoder data?: never } )) & Omit, 'data'> // Renders an action. export const ActionRenderer = < Data extends Record = Record, >({ action, ...props }: ActionRendererProps) => { const { t } = useTranslation() const data = useLoadingPromise({ promise: async () => { if (props.decoder) { try { return await props.decoder.decode() } catch (err) { console.error( `Decoder error for ${action.key} at index ${props.index}`, err ) throw err } } else { return props.data } }, deps: [props.data, props.decoder], }) return data.loading ? ( ) : data.errored ? ( ) : ( {...props} action={action} data={data.data} /> ) } type InnerActionRendererProps< Data extends Record = Record, > = { action: Action data: Data index: number allActionsWithData: ActionKeyAndData[] SuspenseLoader: ComponentType } const InnerActionRenderer = < Data extends Record = Record, >({ action, data, index, allActionsWithData, SuspenseLoader, }: InnerActionRendererProps) => { const form = useForm({ defaultValues: { data: data as any, }, }) return ( }> ) } export type ActionsMatchAndRenderProps = Omit< ActionsRendererProps, 'actionData' > & { /** * The messages to match. */ messages: UnifiedCosmosMsg[] /** * Callback when all actions and data are loaded. */ onLoad?: (data: ActionKeyAndData[]) => void } /** * An ActionsRenderer wrapper that renders the ActionsRenderer component with * matched actions for the provided messages, or loading/error appropriately. */ export const ActionsMatchAndRender = ({ messages, ...props }: ActionsMatchAndRenderProps) => ( ) const InnerActionsMatchAndRender = ( props: Omit ) => { const matcher = useActionMatcher() return ( <> {matcher.errored ? ( ) : matcher.ready ? ( ) : (
)} ) }