Merge pull request #4262 from yancat160/feat/datagrid-view-toggle
feat: add a manual table / card view toggle to DataGrid
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { Column, Row, SearchField } from '@umami/react-zen';
|
||||
import {
|
||||
Button,
|
||||
Column,
|
||||
Icon,
|
||||
Row,
|
||||
SearchField,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
} from '@umami/react-zen';
|
||||
import { LayoutGrid, Table2 } from 'lucide-react';
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
@@ -12,9 +22,13 @@ 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 { getItem, setItem } from '@/lib/storage';
|
||||
import type { PageResult } from '@/lib/types';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
const DISPLAY_MODE_STORAGE_KEY = 'umami.datagrid.displayMode';
|
||||
|
||||
type DisplayMode = 'table' | 'cards';
|
||||
|
||||
export interface DataGridProps {
|
||||
query: UseQueryResult<PageResult<any>, any>;
|
||||
@@ -43,7 +57,23 @@ export function DataGrid({
|
||||
const [search, setSearch] = useState(queryParams?.search || data?.search || '');
|
||||
const showPager = allowPaging && data && data.count > 0;
|
||||
const { isMobile } = useMobile();
|
||||
const displayMode = isMobile ? 'cards' : undefined;
|
||||
const [userDisplayMode, setUserDisplayMode] = useState<DisplayMode | null>(() => {
|
||||
// localStorage can hold anything (extensions, manual edits, schema drift),
|
||||
// so accept only the two values we know how to render and otherwise fall
|
||||
// back to the useMobile-driven default.
|
||||
const stored = getItem(DISPLAY_MODE_STORAGE_KEY);
|
||||
return stored === 'table' || stored === 'cards' ? stored : null;
|
||||
});
|
||||
|
||||
// Effective mode: explicit user choice wins, otherwise fall back to the
|
||||
// mobile-driven default (cards on small viewports, table elsewhere).
|
||||
const displayMode: DisplayMode | undefined = userDisplayMode ?? (isMobile ? 'cards' : undefined);
|
||||
|
||||
const handleToggleDisplayMode = () => {
|
||||
const next: DisplayMode = displayMode === 'cards' ? 'table' : 'cards';
|
||||
setItem(DISPLAY_MODE_STORAGE_KEY, next);
|
||||
setUserDisplayMode(next);
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
if (value !== search) {
|
||||
@@ -61,29 +91,42 @@ export function DataGrid({
|
||||
|
||||
const child = data ? (typeof children === 'function' ? children(data) : children) : null;
|
||||
|
||||
const viewToggleButton = (
|
||||
<TooltipTrigger delay={0}>
|
||||
<Button variant="zero" onPress={handleToggleDisplayMode}>
|
||||
<Icon>{displayMode === 'cards' ? <Table2 /> : <LayoutGrid />}</Icon>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<Text>{displayMode === 'cards' ? 'Switch to table view' : 'Switch to card view'}</Text>
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column gap="4" minHeight="300px" justifyContent="space-between">
|
||||
<Column gap="4">
|
||||
<Column gap="4" minHeight="300px">
|
||||
<Row alignItems="center" wrap="wrap" gap>
|
||||
{allowSearch && (
|
||||
<Row alignItems="center" justifyContent="space-between" wrap="wrap" gap>
|
||||
<SearchField
|
||||
value={search}
|
||||
onSearch={handleSearch}
|
||||
delay={searchDelay || DEFAULT_SEARCH_DELAY}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={t(labels.search)}
|
||||
/>
|
||||
{renderActions?.()}
|
||||
</Row>
|
||||
<SearchField
|
||||
value={search}
|
||||
onSearch={handleSearch}
|
||||
delay={searchDelay || DEFAULT_SEARCH_DELAY}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={t(labels.search)}
|
||||
/>
|
||||
)}
|
||||
<LoadingPanel
|
||||
data={data?.data}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
error={error}
|
||||
renderEmpty={renderEmpty}
|
||||
>
|
||||
{data && (
|
||||
<Row alignItems="center" gap style={{ marginLeft: 'auto' }}>
|
||||
{renderActions?.()}
|
||||
{viewToggleButton}
|
||||
</Row>
|
||||
</Row>
|
||||
<LoadingPanel
|
||||
data={data?.data}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
error={error}
|
||||
renderEmpty={renderEmpty}
|
||||
>
|
||||
{data && (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
@@ -91,9 +134,11 @@ export function DataGrid({
|
||||
overflowX: 'auto',
|
||||
}}
|
||||
>
|
||||
<Column>
|
||||
{isValidElement(child)
|
||||
? cloneElement(child as ReactElement<any>, { displayMode })
|
||||
: child}
|
||||
</Column>
|
||||
</div>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
|
||||
Reference in New Issue
Block a user