import React, { type MouseEventHandler, type ReactNode, useReducer, } from 'react'; import { Button } from '../../Button'; import { IconButton } from '../../IconButton'; import styles from './PanelEditableSection.module.css'; import { useTranslate } from '../../../hooks/useTranslate'; const readOnlyState = { status: 'readonly', isEditMode: false, } as const; type ReadOnlyState = typeof readOnlyState; const editionState = { status: 'edition', isEditMode: true, } as const; type EditionState = typeof editionState; const loadingState = { status: 'loading', isEditMode: true, } as const; type LoadingState = typeof loadingState; const getErrorState = (error: Error) => ({ status: 'error', isEditMode: true, error, }) as const; type ErrorState = ReturnType; const successState = { status: 'success', isEditMode: false, } as const; type SuccessState = typeof successState; type State = | ReadOnlyState | EditionState | LoadingState | ErrorState | SuccessState; const initialState = readOnlyState; type Action = | { type: 'tryUpdate' } | { type: 'updateFailed'; error: Error } | { type: 'updateSucceeded' } | { type: 'startEditMode' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'tryUpdate': return loadingState; case 'updateFailed': return getErrorState(action.error); case 'updateSucceeded': return successState; case 'startEditMode': return editionState; default: return state; } } export type PanelEditableSectionProps = { /** * The title of the PanelSection. */ title: string | ReactNode; /** * The content of the PanelSection. */ children: ReactNode; /** * The content of the PanelSection when in edit mode. */ editSection: ReactNode; /** * The translation for the cancel button. */ cancelTranslation: string; /** * The translation for the save button. */ saveTranslation: string; /** * Whether the save button should be disabled. * @default false */ disableSave?: boolean; /** * Translation for the aria-label of the edit IconButton. */ editButtonLabel?: string; /** * Function to render an error in the form. */ renderError?(error: Error): ReactNode; /** * Handler that is called when an error occurs. */ onError?(error: Error): void; /** * Handler that is called when the form is saved. */ onSave: MouseEventHandler; /** * Handler that is called when the cancel button is clicked. */ onCancel: MouseEventHandler; /** * Handler that is called when the edit mode is toggled. */ onEditClick?: MouseEventHandler; }; export function PanelEditableSection({ title, children, editSection, cancelTranslation, saveTranslation, disableSave = false, renderError, onError, onSave, onCancel, onEditClick, editButtonLabel, ...rest }: PanelEditableSectionProps) { const t = useTranslate(); const [state, dispatch] = useReducer(reducer, initialState); const handleAction = (action: MouseEventHandler) => async (event: React.MouseEvent) => { try { dispatch({ type: 'tryUpdate' }); await action(event); dispatch({ type: 'updateSucceeded' }); } catch (rawError) { const error = rawError instanceof Error ? rawError : new Error( typeof rawError === 'string' ? rawError : 'Failed editing panel section', ); dispatch({ type: 'updateFailed', error }); if (onError) { onError(error); } } }; return (
{title}
{!state.isEditMode && ( { dispatch({ type: 'startEditMode' }); if (onEditClick) { onEditClick(event); } }} hasNegativeMargins aria-label={editButtonLabel ?? t('edit')} /> )}
{state.isEditMode ? ( <> {editSection} {state.status === 'error' && renderError && renderError(state.error)}
) : ( children )}
); }