diff options
Diffstat (limited to 'src/components/common/DataGrid.tsx')
| -rw-r--r-- | src/components/common/DataGrid.tsx | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/src/components/common/DataGrid.tsx b/src/components/common/DataGrid.tsx new file mode 100644 index 0000000..7e07b8d --- /dev/null +++ b/src/components/common/DataGrid.tsx @@ -0,0 +1,107 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { Column, Row, SearchField } from '@umami/react-zen'; +import { + cloneElement, + isValidElement, + type ReactElement, + type ReactNode, + useCallback, + useState, +} from 'react'; +import { Empty } from '@/components/common/Empty'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Pager } from '@/components/common/Pager'; +import { useMessages, useMobile, useNavigation } from '@/components/hooks'; +import type { PageResult } from '@/lib/types'; + +const DEFAULT_SEARCH_DELAY = 600; + +export interface DataGridProps { + query: UseQueryResult<PageResult<any>, any>; + searchDelay?: number; + allowSearch?: boolean; + allowPaging?: boolean; + autoFocus?: boolean; + renderActions?: () => ReactNode; + renderEmpty?: () => ReactNode; + children: ReactNode | ((data: any) => ReactNode); +} + +export function DataGrid({ + query, + searchDelay = 600, + allowSearch, + allowPaging = true, + autoFocus, + renderActions, + renderEmpty = () => <Empty />, + children, +}: DataGridProps) { + const { formatMessage, labels } = useMessages(); + const { data, error, isLoading, isFetching } = query; + const { router, updateParams, query: queryParams } = useNavigation(); + const [search, setSearch] = useState(queryParams?.search || data?.search || ''); + const showPager = allowPaging && data && data.count > data.pageSize; + const { isMobile } = useMobile(); + const displayMode = isMobile ? 'cards' : undefined; + + const handleSearch = (value: string) => { + if (value !== search) { + setSearch(value); + router.push(updateParams({ search: value, page: 1 })); + } + }; + + const handlePageChange = useCallback( + (page: number) => { + router.push(updateParams({ search, page })); + }, + [search], + ); + + const child = data ? (typeof children === 'function' ? children(data) : children) : null; + + return ( + <Column gap="4" minHeight="300px"> + {allowSearch && ( + <Row alignItems="center" justifyContent="space-between" wrap="wrap" gap> + <SearchField + value={search} + onSearch={handleSearch} + delay={searchDelay || DEFAULT_SEARCH_DELAY} + autoFocus={autoFocus} + placeholder={formatMessage(labels.search)} + /> + {renderActions?.()} + </Row> + )} + <LoadingPanel + data={data} + isLoading={isLoading} + isFetching={isFetching} + error={error} + renderEmpty={renderEmpty} + > + {data && ( + <> + <Column> + {isValidElement(child) + ? cloneElement(child as ReactElement<any>, { displayMode }) + : child} + </Column> + {showPager && ( + <Row marginTop="6"> + <Pager + page={data.page} + pageSize={data.pageSize} + count={data.count} + onPageChange={handlePageChange} + /> + </Row> + )} + </> + )} + </LoadingPanel> + </Column> + ); +} |