11import type { UseQueryResult } from '@tanstack/react-query' ;
2- import { Column , Row , SearchField } from '@umami/react-zen' ;
2+ import {
3+ Button ,
4+ Column ,
5+ Icon ,
6+ Row ,
7+ SearchField ,
8+ Text ,
9+ Tooltip ,
10+ TooltipTrigger ,
11+ } from '@umami/react-zen' ;
12+ import { LayoutGrid , Table2 } from 'lucide-react' ;
313import {
414 cloneElement ,
515 isValidElement ,
@@ -12,9 +22,13 @@ import { Empty } from '@/components/common/Empty';
1222import { LoadingPanel } from '@/components/common/LoadingPanel' ;
1323import { Pager } from '@/components/common/Pager' ;
1424import { useMessages , useMobile , useNavigation } from '@/components/hooks' ;
25+ import { getItem , setItem } from '@/lib/storage' ;
1526import type { PageResult } from '@/lib/types' ;
1627
1728const DEFAULT_SEARCH_DELAY = 600 ;
29+ const DISPLAY_MODE_STORAGE_KEY = 'umami.datagrid.displayMode' ;
30+
31+ type DisplayMode = 'table' | 'cards' ;
1832
1933export interface DataGridProps {
2034 query : UseQueryResult < PageResult < any > , any > ;
@@ -43,7 +57,23 @@ export function DataGrid({
4357 const [ search , setSearch ] = useState ( queryParams ?. search || data ?. search || '' ) ;
4458 const showPager = allowPaging && data && data . count > 0 ;
4559 const { isMobile } = useMobile ( ) ;
46- const displayMode = isMobile ? 'cards' : undefined ;
60+ const [ userDisplayMode , setUserDisplayMode ] = useState < DisplayMode | null > ( ( ) => {
61+ // localStorage can hold anything (extensions, manual edits, schema drift),
62+ // so accept only the two values we know how to render and otherwise fall
63+ // back to the useMobile-driven default.
64+ const stored = getItem ( DISPLAY_MODE_STORAGE_KEY ) ;
65+ return stored === 'table' || stored === 'cards' ? stored : null ;
66+ } ) ;
67+
68+ // Effective mode: explicit user choice wins, otherwise fall back to the
69+ // mobile-driven default (cards on small viewports, table elsewhere).
70+ const displayMode : DisplayMode | undefined = userDisplayMode ?? ( isMobile ? 'cards' : undefined ) ;
71+
72+ const handleToggleDisplayMode = ( ) => {
73+ const next : DisplayMode = displayMode === 'cards' ? 'table' : 'cards' ;
74+ setItem ( DISPLAY_MODE_STORAGE_KEY , next ) ;
75+ setUserDisplayMode ( next ) ;
76+ } ;
4777
4878 const handleSearch = ( value : string ) => {
4979 if ( value !== search ) {
@@ -61,39 +91,54 @@ export function DataGrid({
6191
6292 const child = data ? ( typeof children === 'function' ? children ( data ) : children ) : null ;
6393
94+ const viewToggleButton = (
95+ < TooltipTrigger delay = { 0 } >
96+ < Button variant = "zero" onPress = { handleToggleDisplayMode } >
97+ < Icon > { displayMode === 'cards' ? < Table2 /> : < LayoutGrid /> } </ Icon >
98+ </ Button >
99+ < Tooltip >
100+ < Text > { displayMode === 'cards' ? 'Switch to table view' : 'Switch to card view' } </ Text >
101+ </ Tooltip >
102+ </ TooltipTrigger >
103+ ) ;
104+
64105 return (
65- < Column gap = "4" minHeight = "300px" justifyContent = "space-between" >
66- < Column gap = "4" >
106+ < Column gap = "4" minHeight = "300px" >
107+ < Row alignItems = "center" wrap = "wrap" gap >
67108 { allowSearch && (
68- < Row alignItems = "center" justifyContent = "space-between" wrap = "wrap" gap >
69- < SearchField
70- value = { search }
71- onSearch = { handleSearch }
72- delay = { searchDelay || DEFAULT_SEARCH_DELAY }
73- autoFocus = { autoFocus }
74- placeholder = { t ( labels . search ) }
75- />
76- { renderActions ?.( ) }
77- </ Row >
109+ < SearchField
110+ value = { search }
111+ onSearch = { handleSearch }
112+ delay = { searchDelay || DEFAULT_SEARCH_DELAY }
113+ autoFocus = { autoFocus }
114+ placeholder = { t ( labels . search ) }
115+ />
78116 ) }
79- < LoadingPanel
80- data = { data ?. data }
81- isLoading = { isLoading }
82- isFetching = { isFetching }
83- error = { error }
84- renderEmpty = { renderEmpty }
85- >
86- { data && (
117+ < Row alignItems = "center" gap style = { { marginLeft : 'auto' } } >
118+ { renderActions ?.( ) }
119+ { viewToggleButton }
120+ </ Row >
121+ </ Row >
122+ < LoadingPanel
123+ data = { data ?. data }
124+ isLoading = { isLoading }
125+ isFetching = { isFetching }
126+ error = { error }
127+ renderEmpty = { renderEmpty }
128+ >
129+ { data && (
87130 < div
88131 style = { {
89132 display : 'grid' ,
90133 gridTemplateColumns : 'minmax(0, 1fr)' ,
91134 overflowX : 'auto' ,
92135 } }
93136 >
137+ < Column >
94138 { isValidElement ( child )
95139 ? cloneElement ( child as ReactElement < any > , { displayMode } )
96140 : child }
141+ </ Column >
97142 </ div >
98143 ) }
99144 </ LoadingPanel >
0 commit comments