Unified loading states.

This commit is contained in:
Mike Cao 2025-06-13 21:13:11 -07:00
parent 7b5591a3ce
commit da8c7e99c5
52 changed files with 506 additions and 364 deletions

View file

@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { Loading, SearchField, Row, Column } from '@umami/react-zen';
import { SearchField, Row, Column } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { Pager } from '@/components/common/Pager';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PagedQueryResult } from '@/lib/types';
@ -24,16 +23,14 @@ export function DataGrid({
allowSearch = true,
allowPaging = true,
autoFocus,
renderEmpty,
children,
}: DataTableProps) {
const { formatMessage, labels, messages } = useMessages();
const { formatMessage, labels } = useMessages();
const { result, params, setParams, query } = queryResult || {};
const { error, isLoading, isFetched } = query || {};
const { error, isLoading, isFetching } = query || {};
const { page, pageSize, count, data } = result || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const noResults = Boolean(search && !hasData);
const { router, renderUrl } = useNavigation();
const handleSearch = (search: string) => {
@ -46,7 +43,7 @@ export function DataGrid({
};
return (
<Column gap="4">
<Column gap="4" minHeight="300px">
{allowSearch && (hasData || search) && (
<Row width="280px" alignItems="center">
<SearchField
@ -58,12 +55,9 @@ export function DataGrid({
/>
</Row>
)}
<LoadingPanel data={data} isLoading={isLoading} isFetched={isFetched} error={error}>
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
<Column>
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
{isLoading && <Loading position="page" />}
{!isLoading && !hasData && !search && (renderEmpty ? renderEmpty() : <Empty />)}
{!isLoading && noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
</Column>
{allowPaging && hasData && (
<Row marginTop="6">

View file

@ -1,32 +1,59 @@
import { ReactNode } from 'react';
import { Spinner, Dots, Column, type ColumnProps } from '@umami/react-zen';
import { Loading, Column, type ColumnProps } from '@umami/react-zen';
import { ErrorMessage } from '@/components/common/ErrorMessage';
import { Empty } from '@/components/common/Empty';
export interface LoadingPanelProps extends ColumnProps {
data?: any;
error?: Error;
isEmpty?: boolean;
isLoading?: boolean;
isFetching?: boolean;
loadingIcon?: 'dots' | 'spinner';
renderEmpty?: () => ReactNode;
children: ReactNode;
}
export function LoadingPanel({
data,
error,
isEmpty,
isFetched,
isLoading,
isFetching,
loadingIcon = 'dots',
renderEmpty = () => <Empty />,
children,
...props
}: {
error?: Error;
isEmpty?: boolean;
isFetched?: boolean;
isLoading?: boolean;
loadingIcon?: 'dots' | 'spinner';
renderEmpty?: () => ReactNode;
children: ReactNode;
} & ColumnProps) {
}: LoadingPanelProps) {
const empty = isEmpty ?? checkEmpty(data);
return (
<Column {...props}>
{isLoading && !isFetched && (loadingIcon === 'dots' ? <Dots /> : <Spinner />)}
<Column position="relative" flexGrow={1} {...props}>
{/* Show loading spinner only if no data exists */}
{(isLoading || isFetching) && !data && <Loading icon={loadingIcon} position="page" />}
{/* Show error */}
{error && <ErrorMessage />}
{!error && !isLoading && isEmpty && renderEmpty()}
{!error && !isLoading && !isEmpty && children}
{/* Show empty state (once loaded) */}
{!error && !isLoading && !isFetching && empty && renderEmpty()}
{/* Show main content when data exists */}
{!error && !empty && children}
</Column>
);
}
function checkEmpty(data: any) {
if (!data) return false;
if (Array.isArray(data)) {
return data.length <= 0;
}
if (typeof data === 'object') {
return Object.keys(data).length <= 0;
}
return !!data;
}