|
9 | 9 | import * as React from 'react'; |
10 | 10 | import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components'; |
11 | 11 | import type { SortItem } from '@object-ui/components'; |
12 | | -import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox, Download, AlignJustify, icons, type LucideIcon } from 'lucide-react'; |
| 12 | +import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler, Inbox, Download, AlignJustify, Share2, icons, type LucideIcon } from 'lucide-react'; |
13 | 13 | import type { FilterGroup } from '@object-ui/components'; |
14 | 14 | import { ViewSwitcher, ViewType } from './ViewSwitcher'; |
15 | 15 | import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react'; |
@@ -64,6 +64,52 @@ function convertFilterGroupToAST(group: FilterGroup): any[] { |
64 | 64 | return [group.logic, ...conditions]; |
65 | 65 | } |
66 | 66 |
|
| 67 | +/** |
| 68 | + * Evaluate conditional formatting rules against a record. |
| 69 | + * Returns a CSSProperties object for the first matching rule, or empty object. |
| 70 | + * |
| 71 | + * Exported for use by child view renderers (e.g., ObjectGrid) and consumers |
| 72 | + * who need to evaluate formatting rules outside the ListView component. |
| 73 | + */ |
| 74 | +export function evaluateConditionalFormatting( |
| 75 | + record: Record<string, unknown>, |
| 76 | + rules?: ListViewSchema['conditionalFormatting'] |
| 77 | +): React.CSSProperties { |
| 78 | + if (!rules || rules.length === 0) return {}; |
| 79 | + for (const rule of rules) { |
| 80 | + const fieldValue = record[rule.field]; |
| 81 | + let match = false; |
| 82 | + switch (rule.operator) { |
| 83 | + case 'equals': |
| 84 | + match = fieldValue === rule.value; |
| 85 | + break; |
| 86 | + case 'not_equals': |
| 87 | + match = fieldValue !== rule.value; |
| 88 | + break; |
| 89 | + case 'contains': |
| 90 | + match = typeof fieldValue === 'string' && typeof rule.value === 'string' && fieldValue.includes(rule.value); |
| 91 | + break; |
| 92 | + case 'greater_than': |
| 93 | + match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue > rule.value; |
| 94 | + break; |
| 95 | + case 'less_than': |
| 96 | + match = typeof fieldValue === 'number' && typeof rule.value === 'number' && fieldValue < rule.value; |
| 97 | + break; |
| 98 | + case 'in': |
| 99 | + match = Array.isArray(rule.value) && rule.value.includes(fieldValue); |
| 100 | + break; |
| 101 | + } |
| 102 | + if (match) { |
| 103 | + const style: React.CSSProperties = {}; |
| 104 | + if (rule.backgroundColor) style.backgroundColor = rule.backgroundColor; |
| 105 | + if (rule.textColor) style.color = rule.textColor; |
| 106 | + if (rule.borderColor) style.borderColor = rule.borderColor; |
| 107 | + return style; |
| 108 | + } |
| 109 | + } |
| 110 | + return {}; |
| 111 | +} |
| 112 | + |
67 | 113 | export const ListView: React.FC<ListViewProps> = ({ |
68 | 114 | schema: propSchema, |
69 | 115 | className, |
@@ -132,8 +178,20 @@ export const ListView: React.FC<ListViewProps> = ({ |
132 | 178 | // Export State |
133 | 179 | const [showExport, setShowExport] = React.useState(false); |
134 | 180 |
|
135 | | - // Density Mode |
136 | | - const density = useDensityMode(schema.densityMode || 'comfortable'); |
| 181 | + // Density Mode — rowHeight maps to density if densityMode not explicitly set |
| 182 | + const resolvedDensity = React.useMemo(() => { |
| 183 | + if (schema.densityMode) return schema.densityMode; |
| 184 | + if (schema.rowHeight) { |
| 185 | + const map: Record<string, 'compact' | 'comfortable' | 'spacious'> = { |
| 186 | + compact: 'compact', |
| 187 | + medium: 'comfortable', |
| 188 | + tall: 'spacious', |
| 189 | + }; |
| 190 | + return map[schema.rowHeight] || 'comfortable'; |
| 191 | + } |
| 192 | + return 'comfortable'; |
| 193 | + }, [schema.densityMode, schema.rowHeight]); |
| 194 | + const density = useDensityMode(resolvedDensity); |
137 | 195 |
|
138 | 196 | const handlePullRefresh = React.useCallback(async () => { |
139 | 197 | setRefreshKey(k => k + 1); |
@@ -380,6 +438,8 @@ export const ListView: React.FC<ListViewProps> = ({ |
380 | 438 | type: 'object-grid', |
381 | 439 | ...baseProps, |
382 | 440 | columns: effectiveFields, |
| 441 | + ...(schema.conditionalFormatting ? { conditionalFormatting: schema.conditionalFormatting } : {}), |
| 442 | + ...(schema.inlineEdit != null ? { editable: schema.inlineEdit } : {}), |
383 | 443 | ...(schema.options?.grid || {}), |
384 | 444 | }; |
385 | 445 | case 'kanban': |
@@ -528,7 +588,14 @@ export const ListView: React.FC<ListViewProps> = ({ |
528 | 588 | }, [schema.fields]); |
529 | 589 |
|
530 | 590 | return ( |
531 | | - <div ref={pullRef} className={cn('flex flex-col h-full bg-background relative', className)}> |
| 591 | + <div |
| 592 | + ref={pullRef} |
| 593 | + className={cn('flex flex-col h-full bg-background relative', className)} |
| 594 | + {...(schema.aria?.label ? { 'aria-label': schema.aria.label } : {})} |
| 595 | + {...(schema.aria?.describedBy ? { 'aria-describedby': schema.aria.describedBy } : {})} |
| 596 | + {...(schema.aria?.live ? { 'aria-live': schema.aria.live } : {})} |
| 597 | + role="region" |
| 598 | + > |
532 | 599 | {pullDistance > 0 && ( |
533 | 600 | <div |
534 | 601 | className="flex items-center justify-center text-xs text-muted-foreground" |
@@ -747,6 +814,20 @@ export const ListView: React.FC<ListViewProps> = ({ |
747 | 814 | </PopoverContent> |
748 | 815 | </Popover> |
749 | 816 | )} |
| 817 | + |
| 818 | + {/* Share */} |
| 819 | + {schema.sharing?.enabled && ( |
| 820 | + <Button |
| 821 | + variant="ghost" |
| 822 | + size="sm" |
| 823 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" |
| 824 | + title={`Sharing: ${schema.sharing.visibility || 'private'}`} |
| 825 | + data-testid="share-button" |
| 826 | + > |
| 827 | + <Share2 className="h-3.5 w-3.5 mr-1.5" /> |
| 828 | + <span className="hidden sm:inline">Share</span> |
| 829 | + </Button> |
| 830 | + )} |
750 | 831 | </div> |
751 | 832 |
|
752 | 833 | {/* Right: Search */} |
|
0 commit comments