339 lines
9.7 KiB
JavaScript
339 lines
9.7 KiB
JavaScript
import React from 'react';
|
|
import { Button, Checkbox, Input, Paragraph, ScrollView, Text, XStack, YStack } from 'tamagui';
|
|
import { getIcon } from '../IconMapper.jsx';
|
|
import { useGridView } from './context.js';
|
|
import { formatValueByColumn } from './utils.js';
|
|
import { getTypographyRoleProps } from '../../styles/index.js';
|
|
|
|
function renderToolbarItem(item) {
|
|
if (!item) {
|
|
return null;
|
|
}
|
|
|
|
if (React.isValidElement(item)) {
|
|
return item;
|
|
}
|
|
|
|
if (item.kind === 'button') {
|
|
const IconComponent = item.icon ? getIcon(item.icon) : null;
|
|
return (
|
|
<Button
|
|
key={item.key || item.label}
|
|
size="$3"
|
|
theme={item.theme}
|
|
chromeless={item.chromeless}
|
|
disabled={item.disabled}
|
|
icon={IconComponent ? <IconComponent size="sm" /> : undefined}
|
|
onPress={item.onClick || item.onPress}
|
|
>
|
|
{item.label}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
if (item.kind === 'text') {
|
|
return (
|
|
<Text key={item.key || item.text} color="$textSecondary">
|
|
{item.text}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
if (item.kind === 'search') {
|
|
const SearchIcon = getIcon('search');
|
|
return (
|
|
<XStack key={item.key || item.placeholder || 'search'} alignItems="center" gap="$2">
|
|
{SearchIcon ? <SearchIcon size="sm" color="$textMuted" /> : null}
|
|
<Input
|
|
width={item.width || 240}
|
|
value={item.value}
|
|
placeholder={item.placeholder || 'Search'}
|
|
onChangeText={(value) => item.onChange?.(value)}
|
|
backgroundColor="$bgPanel"
|
|
borderColor="$lineSubtle"
|
|
focusStyle={{ borderColor: '$accent' }}
|
|
/>
|
|
</XStack>
|
|
);
|
|
}
|
|
|
|
if (item.kind === 'node') {
|
|
return <React.Fragment key={item.key || 'node'}>{item.node}</React.Fragment>;
|
|
}
|
|
|
|
return <React.Fragment key={item.key || 'item'}>{item}</React.Fragment>;
|
|
}
|
|
|
|
function DefaultPanelRecordRenderer({ row }) {
|
|
const grid = useGridView();
|
|
const titleColumn =
|
|
grid.resolvedColumns.find((column) =>
|
|
['customer', 'name', 'title', 'label'].includes(column.field)
|
|
) ||
|
|
grid.resolvedColumns.find((column) => column.type === 'text') ||
|
|
grid.resolvedColumns[0];
|
|
const subtitleColumn = grid.resolvedColumns.find((column) =>
|
|
['description', 'region', 'owner', 'status'].includes(column.field)
|
|
);
|
|
const summaryColumns = grid.resolvedColumns.filter(
|
|
(column) => ![titleColumn?.field, subtitleColumn?.field, 'description'].includes(column.field)
|
|
);
|
|
|
|
return (
|
|
<YStack gap="$3">
|
|
<YStack gap="$1">
|
|
<Text fontSize="$2" letterSpacing={1} textTransform="uppercase" color="$textSecondary">
|
|
Record Summary
|
|
</Text>
|
|
<Text {...getTypographyRoleProps('sectionTitle')}>
|
|
{titleColumn ? row?.[titleColumn.field] : row?.id}
|
|
</Text>
|
|
{subtitleColumn ? (
|
|
<Paragraph color="$textSecondary">
|
|
{row?.[subtitleColumn.field] || ''}
|
|
</Paragraph>
|
|
) : null}
|
|
</YStack>
|
|
|
|
<XStack gap="$2" flexWrap="wrap">
|
|
{summaryColumns.slice(0, 3).map((column) => (
|
|
<YStack
|
|
key={`${row.id}-${column.field}-chip`}
|
|
paddingHorizontal="$3"
|
|
paddingVertical="$2"
|
|
borderRadius="$radiusMd"
|
|
backgroundColor="$bgPanel"
|
|
borderWidth={1}
|
|
borderColor="$lineSubtle"
|
|
>
|
|
<Text fontSize="$2" color="$textMuted">
|
|
{column.label}
|
|
</Text>
|
|
<Text {...getTypographyRoleProps('tableHeader', { color: '$textPrimary' })}>
|
|
{formatValueByColumn(row?.[column.field], column)}
|
|
</Text>
|
|
</YStack>
|
|
))}
|
|
</XStack>
|
|
|
|
<XStack gap="$3" flexWrap="wrap">
|
|
{summaryColumns.map((column) => (
|
|
<YStack
|
|
key={`${row.id}-${column.field}`}
|
|
minWidth={160}
|
|
flex={1}
|
|
padding="$3"
|
|
borderRadius="$radiusMd"
|
|
borderWidth={1}
|
|
borderColor="$lineSubtle"
|
|
backgroundColor="$bgPanel"
|
|
gap="$1"
|
|
>
|
|
<Text fontSize="$3" color="$textMuted">
|
|
{column.label}
|
|
</Text>
|
|
<Text color="$textPrimary">{formatValueByColumn(row?.[column.field], column)}</Text>
|
|
</YStack>
|
|
))}
|
|
</XStack>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
export function PanelToolBar({ items = [], visible = true }) {
|
|
if (visible === false) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<XStack gap="$2" alignItems="center" justifyContent="flex-end" flexWrap="wrap">
|
|
{items.map((item, index) => (
|
|
<React.Fragment key={item?.key || item?.label || item?.text || index}>
|
|
{renderToolbarItem(item)}
|
|
</React.Fragment>
|
|
))}
|
|
</XStack>
|
|
);
|
|
}
|
|
|
|
export function PanelFooterStatusBar({ text, visible = true }) {
|
|
const grid = useGridView();
|
|
if (visible === false) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Text color="$textMuted">
|
|
{text || grid.statusText}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
export function PanelHeader({ title, toolbarItems = [], visible = true, showDivider = true }) {
|
|
const grid = useGridView();
|
|
const RefreshIcon = getIcon('refresh');
|
|
const CloseIcon = getIcon('close');
|
|
|
|
if (visible === false) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<XStack
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap="$3"
|
|
padding="$3"
|
|
minHeight={64}
|
|
borderBottomWidth={showDivider ? 1 : 0}
|
|
borderBottomColor="$lineSubtle"
|
|
backgroundColor="$bgPanel"
|
|
>
|
|
<Text {...getTypographyRoleProps('panelTitle', { fontSize: '$6' })}>
|
|
{title}
|
|
</Text>
|
|
|
|
<XStack gap="$2" alignItems="center" flexWrap="wrap" justifyContent="flex-end">
|
|
<PanelToolBar items={toolbarItems} />
|
|
<Button
|
|
size="$3"
|
|
chromeless
|
|
circular
|
|
icon={RefreshIcon ? <RefreshIcon size="sm" color="$textSecondary" /> : undefined}
|
|
onPress={grid.reload}
|
|
/>
|
|
{grid.closeable !== false ? (
|
|
<Button
|
|
size="$3"
|
|
chromeless
|
|
circular
|
|
disabled={!grid.close}
|
|
icon={CloseIcon ? <CloseIcon size="sm" color="$textSecondary" /> : undefined}
|
|
onPress={grid.close}
|
|
/>
|
|
) : null}
|
|
</XStack>
|
|
</XStack>
|
|
);
|
|
}
|
|
|
|
export function PanelFooter({ toolbarItems = [], visible = true }) {
|
|
if (visible === false) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<XStack
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap="$3"
|
|
padding="$3"
|
|
minHeight={56}
|
|
borderTopWidth={1}
|
|
borderTopColor="$lineSubtle"
|
|
backgroundColor="$bgPanel"
|
|
flexWrap="wrap"
|
|
>
|
|
<PanelFooterStatusBar />
|
|
<PanelToolBar items={toolbarItems} />
|
|
</XStack>
|
|
);
|
|
}
|
|
|
|
export function PanelBodyView({
|
|
visible = true,
|
|
recordRenderer: RecordRenderer = DefaultPanelRecordRenderer,
|
|
columns = 2
|
|
}) {
|
|
const grid = useGridView();
|
|
|
|
if (visible === false) {
|
|
return null;
|
|
}
|
|
|
|
if (grid.error) {
|
|
const ErrorIcon = getIcon('error');
|
|
return (
|
|
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5" gap="$2">
|
|
{ErrorIcon ? <ErrorIcon size="lg" color="$danger" /> : null}
|
|
<Text color="$danger" fontWeight="600">{grid.error}</Text>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
if (grid.isLoading && !grid.rows.length) {
|
|
return (
|
|
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5">
|
|
<Text color="$textMuted">Loading cards...</Text>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
if (!grid.rows.length) {
|
|
const EmptyIcon = getIcon('folder');
|
|
return (
|
|
<YStack flex={1} alignItems="center" justifyContent="center" padding="$5" gap="$2">
|
|
{EmptyIcon ? <EmptyIcon size="xl" color="$textMuted" /> : null}
|
|
<Text color="$textSecondary" fontWeight="600">No records available</Text>
|
|
<Text color="$textMuted" fontSize="$3">There's nothing to show here yet.</Text>
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
const responsiveColumns = typeof columns === 'number' ? columns : 2;
|
|
|
|
return (
|
|
<ScrollView flex={1}>
|
|
<YStack padding="$4" gap="$3">
|
|
<XStack gap="$3" flexWrap="wrap">
|
|
{grid.rows.map((row) => {
|
|
const isSelected = grid.selectedIds.has(row.id);
|
|
return (
|
|
<YStack
|
|
key={row.id}
|
|
minWidth={responsiveColumns > 1 ? 320 : 240}
|
|
flex={1}
|
|
flexBasis={responsiveColumns > 1 ? '48%' : '100%'}
|
|
padding="$4"
|
|
borderWidth={1}
|
|
borderColor={isSelected ? '$accent' : '$lineSubtle'}
|
|
backgroundColor={isSelected ? '$accentBg' : '$bgPanel'}
|
|
borderRadius="$radiusLg"
|
|
gap="$3"
|
|
hoverStyle={isSelected ? undefined : { borderColor: '$lineStrong' }}
|
|
>
|
|
{grid.selectable ? (
|
|
<XStack justifyContent="flex-start">
|
|
<Checkbox
|
|
checked={isSelected}
|
|
onCheckedChange={() => grid.toggleSelectRow(row.id)}
|
|
borderColor="$lineStrong"
|
|
backgroundColor="$bgPanel"
|
|
focusStyle={{ borderColor: '$accent' }}
|
|
>
|
|
<Checkbox.Indicator>
|
|
{(() => {
|
|
const Check = getIcon('check');
|
|
return Check ? <Check size="sm" color="$accent" /> : null;
|
|
})()}
|
|
</Checkbox.Indicator>
|
|
</Checkbox>
|
|
</XStack>
|
|
) : null}
|
|
|
|
<RecordRenderer row={row} />
|
|
</YStack>
|
|
);
|
|
})}
|
|
</XStack>
|
|
|
|
{grid.isLoading ? (
|
|
<Text color="$textMuted">
|
|
Refreshing records...
|
|
</Text>
|
|
) : null}
|
|
</YStack>
|
|
</ScrollView>
|
|
);
|
|
}
|