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:
Mike Cao
2026-05-15 00:29:09 -04:00
committed by GitHub
+67 -22
View File
@@ -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>