parent
4d65472694
commit
aa060a3dd1
2 changed files with 240 additions and 0 deletions
@ -0,0 +1,200 @@ |
|||||||
|
import { FC, useMemo, useRef, useState } from 'react'; |
||||||
|
|
||||||
|
import api from '../lib/api'; |
||||||
|
import { DialogWithHeader } from './Dialog'; |
||||||
|
import handleAPIError from '../lib/handleAPIError'; |
||||||
|
import List from './List'; |
||||||
|
import useActiveFetch from '../hooks/useActiveFetch'; |
||||||
|
import useChecklist from '../hooks/useChecklist'; |
||||||
|
import useConfirmDialog from '../hooks/useConfirmDialog'; |
||||||
|
import useFetch from '../hooks/useFetch'; |
||||||
|
|
||||||
|
const reduceHeader = <A extends unknown[], R extends React.ReactNode>( |
||||||
|
header: R | ((...args: A) => R), |
||||||
|
...args: A |
||||||
|
): R => (typeof header === 'function' ? header(...args) : header); |
||||||
|
|
||||||
|
const CrudList = < |
||||||
|
Overview, |
||||||
|
Detail, |
||||||
|
OverviewList extends Record<string, Overview> = Record<string, Overview>, |
||||||
|
>( |
||||||
|
...[props]: Parameters<FC<CrudListProps<Overview, Detail, OverviewList>>> |
||||||
|
): ReturnType<FC<CrudListProps<Overview, Detail, OverviewList>>> => { |
||||||
|
const { |
||||||
|
addHeader: rAddHeader, |
||||||
|
editHeader: rEditHeader, |
||||||
|
entriesUrl, |
||||||
|
getAddLoading, |
||||||
|
getDeleteErrorMessage, |
||||||
|
getDeleteHeader, |
||||||
|
getDeleteSuccessMessage, |
||||||
|
getEditLoading = (previous?: boolean) => previous, |
||||||
|
listEmpty, |
||||||
|
listProps, |
||||||
|
refreshInterval = 5000, |
||||||
|
renderAddForm, |
||||||
|
renderDeleteItem, |
||||||
|
renderEditForm, |
||||||
|
renderListItem, |
||||||
|
} = props; |
||||||
|
|
||||||
|
const addDialogRef = useRef<DialogForwardedRefContent>(null); |
||||||
|
const editDialogRef = useRef<DialogForwardedRefContent>(null); |
||||||
|
|
||||||
|
const { |
||||||
|
confirmDialog, |
||||||
|
finishConfirm, |
||||||
|
setConfirmDialogLoading, |
||||||
|
setConfirmDialogOpen, |
||||||
|
setConfirmDialogProps, |
||||||
|
} = useConfirmDialog(); |
||||||
|
|
||||||
|
const [edit, setEdit] = useState<boolean>(false); |
||||||
|
const [entry, setEntry] = useState<Detail | undefined>(); |
||||||
|
const [entries, setEntries] = useState<OverviewList | undefined>(); |
||||||
|
|
||||||
|
const { loading: loadingEntriesPeriodic } = useFetch<OverviewList>( |
||||||
|
entriesUrl, |
||||||
|
{ |
||||||
|
onSuccess: (data) => setEntries(data), |
||||||
|
refreshInterval, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const { fetch: getEntries, loading: loadingEntriesActive } = |
||||||
|
useActiveFetch<OverviewList>({ |
||||||
|
onData: (data) => setEntries(data), |
||||||
|
url: entriesUrl, |
||||||
|
}); |
||||||
|
|
||||||
|
const { fetch: getEntry, loading: loadingEntry } = useActiveFetch<Detail>({ |
||||||
|
onData: (data) => setEntry(data), |
||||||
|
url: entriesUrl, |
||||||
|
}); |
||||||
|
|
||||||
|
const addHeader = useMemo<React.ReactNode>( |
||||||
|
() => reduceHeader(rAddHeader), |
||||||
|
[rAddHeader], |
||||||
|
); |
||||||
|
|
||||||
|
const editHeader = useMemo<React.ReactNode>( |
||||||
|
() => reduceHeader(rEditHeader, entry), |
||||||
|
[entry, rEditHeader], |
||||||
|
); |
||||||
|
|
||||||
|
const formTools = useMemo<CrudListFormTools>( |
||||||
|
() => ({ |
||||||
|
confirm: { |
||||||
|
finish: finishConfirm, |
||||||
|
loading: setConfirmDialogLoading, |
||||||
|
open: setConfirmDialogOpen, |
||||||
|
prepare: setConfirmDialogProps, |
||||||
|
}, |
||||||
|
}), |
||||||
|
[ |
||||||
|
finishConfirm, |
||||||
|
setConfirmDialogLoading, |
||||||
|
setConfirmDialogOpen, |
||||||
|
setConfirmDialogProps, |
||||||
|
], |
||||||
|
); |
||||||
|
|
||||||
|
const loadingEntries = useMemo<boolean>( |
||||||
|
() => loadingEntriesPeriodic || loadingEntriesActive, |
||||||
|
[loadingEntriesActive, loadingEntriesPeriodic], |
||||||
|
); |
||||||
|
|
||||||
|
const { |
||||||
|
buildDeleteDialogProps, |
||||||
|
checks, |
||||||
|
getCheck, |
||||||
|
hasAllChecks, |
||||||
|
hasChecks, |
||||||
|
multipleItems, |
||||||
|
resetChecks, |
||||||
|
setAllChecks, |
||||||
|
setCheck, |
||||||
|
} = useChecklist({ list: entries }); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<List<Overview> |
||||||
|
allowCheckAll={multipleItems} |
||||||
|
allowEdit |
||||||
|
allowItemButton={edit} |
||||||
|
disableDelete={!hasChecks} |
||||||
|
edit={edit} |
||||||
|
getListCheckboxProps={() => ({ |
||||||
|
checked: hasAllChecks, |
||||||
|
onChange: (event, checked) => setAllChecks(checked), |
||||||
|
})} |
||||||
|
getListItemCheckboxProps={(key) => ({ |
||||||
|
checked: getCheck(key), |
||||||
|
onChange: (event, checked) => setCheck(key, checked), |
||||||
|
})} |
||||||
|
header |
||||||
|
listEmpty={listEmpty} |
||||||
|
listItems={entries} |
||||||
|
loading={loadingEntries} |
||||||
|
onAdd={() => addDialogRef?.current?.setOpen(true)} |
||||||
|
onDelete={() => { |
||||||
|
setConfirmDialogProps( |
||||||
|
buildDeleteDialogProps({ |
||||||
|
onProceedAppend: () => { |
||||||
|
setConfirmDialogLoading(true); |
||||||
|
|
||||||
|
Promise.all( |
||||||
|
checks.map((key) => api.delete(`${entriesUrl}/${key}`)), |
||||||
|
) |
||||||
|
.then(() => { |
||||||
|
finishConfirm('Success', getDeleteSuccessMessage()); |
||||||
|
|
||||||
|
getEntries(); |
||||||
|
}) |
||||||
|
.catch((error) => { |
||||||
|
const emsg = handleAPIError(error); |
||||||
|
|
||||||
|
finishConfirm('Error', getDeleteErrorMessage(emsg)); |
||||||
|
}); |
||||||
|
|
||||||
|
resetChecks(); |
||||||
|
}, |
||||||
|
getConfirmDialogTitle: getDeleteHeader, |
||||||
|
renderEntry: (...args) => renderDeleteItem(entries, ...args), |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
setConfirmDialogOpen(true); |
||||||
|
}} |
||||||
|
onEdit={() => setEdit((previous) => !previous)} |
||||||
|
onItemClick={(value, key) => { |
||||||
|
editDialogRef?.current?.setOpen(true); |
||||||
|
|
||||||
|
getEntry(`/${key}`); |
||||||
|
}} |
||||||
|
renderListItem={renderListItem} |
||||||
|
{...listProps} |
||||||
|
/> |
||||||
|
<DialogWithHeader |
||||||
|
header={addHeader} |
||||||
|
loading={getAddLoading?.call(null)} |
||||||
|
ref={addDialogRef} |
||||||
|
showClose |
||||||
|
> |
||||||
|
{renderAddForm(formTools)} |
||||||
|
</DialogWithHeader> |
||||||
|
<DialogWithHeader |
||||||
|
header={editHeader} |
||||||
|
loading={getEditLoading(loadingEntry)} |
||||||
|
ref={editDialogRef} |
||||||
|
showClose |
||||||
|
> |
||||||
|
{renderEditForm(formTools, entry)} |
||||||
|
</DialogWithHeader> |
||||||
|
{confirmDialog} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default CrudList; |
@ -0,0 +1,40 @@ |
|||||||
|
type CrudListFormTools = { |
||||||
|
confirm: { |
||||||
|
finish: (header: React.ReactNode, message: Message) => void; |
||||||
|
loading: (value: boolean) => void; |
||||||
|
open: (value: boolean) => void; |
||||||
|
prepare: (value: React.SetStateAction<ConfirmDialogProps>) => void; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
type CrudListOptionalProps<Overview> = { |
||||||
|
getAddLoading?: (previous?: boolean) => boolean; |
||||||
|
getEditLoading?: (previous?: boolean) => boolean; |
||||||
|
listProps?: Partial<ListProps<Overview>>; |
||||||
|
refreshInterval?: number; |
||||||
|
}; |
||||||
|
|
||||||
|
type CrudListProps< |
||||||
|
Overview, |
||||||
|
Detail, |
||||||
|
OverviewList extends Record<string, Overview> = Record<string, Overview>, |
||||||
|
> = Pick<ListProps<Overview>, 'listEmpty' | 'renderListItem'> & |
||||||
|
CrudListOptionalProps<Overview> & { |
||||||
|
addHeader: React.ReactNode | (() => React.ReactNode); |
||||||
|
editHeader: |
||||||
|
| React.ReactNode |
||||||
|
| ((detail: Detail | undefined) => React.ReactNode); |
||||||
|
entriesUrl: string; |
||||||
|
getDeleteErrorMessage: (previous: Message) => Message; |
||||||
|
getDeleteHeader: BuildDeleteDialogPropsArgs['getConfirmDialogTitle']; |
||||||
|
getDeleteSuccessMessage: () => Message; |
||||||
|
renderAddForm: (tools: CrudListFormTools) => React.ReactNode; |
||||||
|
renderDeleteItem: ( |
||||||
|
entries: OverviewList | undefined, |
||||||
|
...args: Parameters<RenderFormEntryFunction> |
||||||
|
) => ReturnType<RenderFormEntryFunction>; |
||||||
|
renderEditForm: ( |
||||||
|
tools: CrudListFormTools, |
||||||
|
detail: Detail | undefined, |
||||||
|
) => React.ReactNode; |
||||||
|
}; |
Loading…
Reference in new issue