import { IItem } from '@/components/ra-forms/LongForm/types'; import { getChildren, getItemsIds, getLevel } from '@/components/ra-forms/LongForm/utils'; import _ from 'lodash'; import { createContext, useCallback, useContext, useEffect, useMemo, useReducer } from 'react'; import { matchPath, useLocation } from 'react-router'; enum ActionType { SET_FORM_ROOT_PATH = 'setFormRootPath', SET_SYNC_WITH_LOCATION = 'setSyncWithLocation', SET_ACTIVE_ITEM = 'setActiveItem', ADD_ITEM = 'addItem', REMOVE_ITEM = 'removeItem', SET_ERROR_COUNT = 'setErrorCount' } type IProviderProps = React.PropsWithChildren<{ syncWithLocation?: boolean; rootMatchString?: string; errorCount?: boolean; }>; type IState = { formRootPath?: string; syncWithLocation: boolean; errorCount: boolean; items: Array; activeItem?: string; }; type IAction = | { type: ActionType.SET_FORM_ROOT_PATH; payload: string; } | { type: ActionType.SET_SYNC_WITH_LOCATION; payload: boolean; } | { type: ActionType.SET_ERROR_COUNT; payload: boolean; } | { type: ActionType.SET_ACTIVE_ITEM; payload?: string; } | { type: ActionType.ADD_ITEM; payload: IItem; } | { type: ActionType.REMOVE_ITEM; payload: string | IItem; }; type IDispatch = React.Dispatch; type IContext = { state: IState; dispatch: IDispatch; }; function reducer(state: IState, action: IAction): IState { const newState = _.clone(state); const { type, payload } = action; switch (type) { case ActionType.SET_FORM_ROOT_PATH: return _.extend(newState, { formRootPath: payload }); case ActionType.SET_SYNC_WITH_LOCATION: return _.extend(newState, { syncWithLocation: payload }); case ActionType.SET_ERROR_COUNT: return _.extend(newState, { errorCount: payload }); case ActionType.SET_ACTIVE_ITEM: return _.includes(getItemsIds(state.items), payload) ? _.extend(newState, { activeItem: payload }) : newState; case ActionType.ADD_ITEM: { const { id } = payload; return _.extend(newState, { items: _.chain(newState.items) .clone() .reject((item: IItem) => item.id === id) .concat([payload]) .orderBy((item) => item.index) .value() }); } case ActionType.REMOVE_ITEM: { const id = _.isString(payload) ? payload : payload.id; const items = _.chain(newState.items) .clone() .reject((item: IItem) => item.id === id) .value(); _.extend(newState, { items: items }); if (newState.activeItem === id) { _.extend(newState, { activeItem: _.first(items)?.id ?? undefined }); } return newState; } default: return newState; } } const DefaultState: IState = { syncWithLocation: true, errorCount: true, items: [], activeItem: undefined }; const Context = createContext(undefined); function Provider(props: IProviderProps) { const syncWithLocation = Boolean(props.syncWithLocation ?? true); const errorCount = Boolean(props.errorCount ?? true); const { rootMatchString } = props; const { pathname } = useLocation(); const [state, dispatch] = useReducer(reducer, _.cloneDeep(DefaultState)); const { formRootPath, items } = state; const value = useMemo(() => ({ state: state, dispatch: dispatch }), [state, dispatch]); useEffect(() => { // I possibili match sono ordinati per priorità (è fondamentale). const matchStrings = [ 'entities/:resource/create/*', ':resource/create/*', 'entities/:resource/:id/show/*', ':resource/:id/show/*', 'entities/:resource/:id/*', ':resource/:id/*' ]; if (rootMatchString !== undefined) { matchStrings.push(rootMatchString); } const match = _.chain(matchStrings) .map((s) => matchPath(s, pathname)) .reject((m) => m === null) .first() .value(); const pathnameBase = match?.pathnameBase; if (pathnameBase !== undefined && pathnameBase !== formRootPath) { dispatch({ type: ActionType.SET_FORM_ROOT_PATH, payload: pathnameBase }); } }, [pathname, formRootPath, rootMatchString]); useEffect(() => { dispatch({ type: ActionType.SET_SYNC_WITH_LOCATION, payload: Boolean(syncWithLocation) }); }, [syncWithLocation, dispatch]); useEffect(() => { dispatch({ type: ActionType.SET_ERROR_COUNT, payload: Boolean(errorCount) }); }, [errorCount, dispatch]); useEffect(() => { if (syncWithLocation && formRootPath !== undefined) { let locationItem = pathname.replace(formRootPath, '').replace(new RegExp(/^\/?/), ''); if (_.isEmpty(locationItem)) { locationItem = _.chain(items) .filter((item) => getChildren(item.id, items).length === 0) .orderBy([(item) => getLevel(item.id), (item) => item.index]) .first() .value()?.id ?? undefined; } dispatch({ type: ActionType.SET_ACTIVE_ITEM, payload: locationItem }); } }, [dispatch, formRootPath, pathname, items, syncWithLocation]); return {props.children}; } function useFormContext(): IContext { const context = useContext(Context); if (context === undefined) { throw new Error( '[LongForm] useFormContext: context undefined. Please provide a valid Context using LongForm.Provider' ); } return context; } function useFormState(): IState { const context = useFormContext(); return context.state; } function useFormDispatch(): IDispatch { const context = useFormContext(); return context.dispatch; } function useFormRootPath(): string | undefined { const state = useFormState(); return state.formRootPath; } function useActiveItem(): string | undefined { const state = useFormState(); return state.activeItem; } function useItems(): Array { const state = useFormState(); return state.items; } function useSyncWithLocation(): boolean { const state = useFormState(); return state.syncWithLocation; } function useErrorCount(): boolean { const state = useFormState(); return state.errorCount; } function useSetActiveItem(): (activeItem: string) => void { const dispatch = useFormDispatch(); const setActiveItem = useCallback( (activeItem: string) => { dispatch({ type: ActionType.SET_ACTIVE_ITEM, payload: activeItem }); }, [dispatch] ); return setActiveItem; } function useAddItem(): (item: IItem) => void { const dispatch = useFormDispatch(); const addItem = useCallback( (item: IItem) => { dispatch({ type: ActionType.ADD_ITEM, payload: item }); }, [dispatch] ); return addItem; } function useRemoveItem(): (item: string | IItem) => void { const dispatch = useFormDispatch(); const removeItem = useCallback( (item: string | IItem) => { dispatch({ type: ActionType.REMOVE_ITEM, payload: item }); }, [dispatch] ); return removeItem; } export type { IProviderProps }; export { Provider, useActiveItem, useAddItem, useErrorCount, useFormRootPath, useItems, useRemoveItem, useSetActiveItem, useSyncWithLocation };