aboutsummaryrefslogtreecommitdiff
path: root/src/components/common/DataGrid.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/common/DataGrid.tsx')
-rw-r--r--src/components/common/DataGrid.tsx107
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>
+ );
+}