feat(striker-ui): add CRUD list

main
Tsu-ba-me 11 months ago
parent 4d65472694
commit aa060a3dd1
  1. 200
      striker-ui/components/CrudList.tsx
  2. 40
      striker-ui/types/CrudList.d.ts

@ -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…
Cancel
Save