@@ -25,15 +25,13 @@ import React, { useEffect, useState, useCallback } from 'react';
2525import type { ObjectGridSchema , DataSource , ListColumn , ViewData } from '@object-ui/types' ;
2626import { SchemaRenderer , useDataScope , useNavigationOverlay , useAction } from '@object-ui/react' ;
2727import { getCellRenderer } from '@object-ui/fields' ;
28- import { Button , NavigationOverlay } from '@object-ui/components' ;
29- import { usePullToRefresh } from '@object-ui/mobile' ;
3028import {
31- DropdownMenu ,
32- DropdownMenuContent ,
33- DropdownMenuItem ,
34- DropdownMenuTrigger ,
29+ Button , NavigationOverlay ,
30+ Popover , PopoverContent , PopoverTrigger ,
31+ DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuTrigger ,
3532} from '@object-ui/components' ;
36- import { Edit , Trash2 , MoreVertical , ChevronRight , ChevronDown } from 'lucide-react' ;
33+ import { usePullToRefresh } from '@object-ui/mobile' ;
34+ import { Edit , Trash2 , MoreVertical , ChevronRight , ChevronDown , Download } from 'lucide-react' ;
3735import { useRowColor } from './useRowColor' ;
3836import { useGroupedData } from './useGroupedData' ;
3937
@@ -125,6 +123,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
125123 const [ objectSchema , setObjectSchema ] = useState < any > ( null ) ;
126124 const [ useCardView , setUseCardView ] = useState ( false ) ;
127125 const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
126+ const [ showExport , setShowExport ] = useState ( false ) ;
128127
129128 // Column state persistence (order and widths)
130129 const columnStorageKey = React . useMemo ( ( ) => {
@@ -514,6 +513,47 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
514513 return generatedColumns ;
515514 } , [ objectSchema , schemaFields , schemaColumns , dataConfig , hasInlineData , navigation . handleClick , executeAction ] ) ;
516515
516+ const handleExport = useCallback ( ( format : 'csv' | 'xlsx' | 'json' | 'pdf' ) => {
517+ const exportConfig = schema . exportOptions ;
518+ const maxRecords = exportConfig ?. maxRecords || 0 ;
519+ const includeHeaders = exportConfig ?. includeHeaders !== false ;
520+ const prefix = exportConfig ?. fileNamePrefix || schema . objectName || 'export' ;
521+ const exportData = maxRecords > 0 ? data . slice ( 0 , maxRecords ) : data ;
522+
523+ if ( format === 'csv' ) {
524+ const cols = generateColumns ( ) . filter ( ( c : any ) => c . accessorKey !== '_actions' ) ;
525+ const fields = cols . map ( ( c : any ) => c . accessorKey ) ;
526+ const headers = cols . map ( ( c : any ) => c . header ) ;
527+ const rows : string [ ] = [ ] ;
528+ if ( includeHeaders ) {
529+ rows . push ( headers . join ( ',' ) ) ;
530+ }
531+ exportData . forEach ( record => {
532+ rows . push ( fields . map ( ( f : string ) => {
533+ const val = record [ f ] ;
534+ const str = val == null ? '' : String ( val ) ;
535+ return str . includes ( ',' ) || str . includes ( '"' ) || str . includes ( '\n' ) || str . includes ( '\r' ) ? `"${ str . replace ( / " / g, '""' ) } "` : str ;
536+ } ) . join ( ',' ) ) ;
537+ } ) ;
538+ const blob = new Blob ( [ rows . join ( '\n' ) ] , { type : 'text/csv;charset=utf-8;' } ) ;
539+ const url = URL . createObjectURL ( blob ) ;
540+ const a = document . createElement ( 'a' ) ;
541+ a . href = url ;
542+ a . download = `${ prefix } .csv` ;
543+ a . click ( ) ;
544+ URL . revokeObjectURL ( url ) ;
545+ } else if ( format === 'json' ) {
546+ const blob = new Blob ( [ JSON . stringify ( exportData , null , 2 ) ] , { type : 'application/json' } ) ;
547+ const url = URL . createObjectURL ( blob ) ;
548+ const a = document . createElement ( 'a' ) ;
549+ a . href = url ;
550+ a . download = `${ prefix } .json` ;
551+ a . click ( ) ;
552+ URL . revokeObjectURL ( url ) ;
553+ }
554+ setShowExport ( false ) ;
555+ } , [ data , schema . exportOptions , schema . objectName , generateColumns ] ) ;
556+
517557 if ( error ) {
518558 return (
519559 < div className = "p-3 sm:p-4 border border-red-300 bg-red-50 rounded-md" >
@@ -713,6 +753,40 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
713753 ) ;
714754 }
715755
756+ // Export toolbar (shown when exportOptions is configured)
757+ const exportToolbar = schema . exportOptions ? (
758+ < div className = "flex items-center justify-end px-2 py-1" >
759+ < Popover open = { showExport } onOpenChange = { setShowExport } >
760+ < PopoverTrigger asChild >
761+ < Button
762+ variant = "ghost"
763+ size = "sm"
764+ className = "h-7 px-2 text-muted-foreground hover:text-primary text-xs"
765+ >
766+ < Download className = "h-3.5 w-3.5 mr-1.5" />
767+ < span className = "hidden sm:inline" > Export</ span >
768+ </ Button >
769+ </ PopoverTrigger >
770+ < PopoverContent align = "end" className = "w-48 p-2" >
771+ < div className = "space-y-1" >
772+ { ( schema . exportOptions . formats || [ 'csv' , 'json' ] ) . map ( format => (
773+ < Button
774+ key = { format }
775+ variant = "ghost"
776+ size = "sm"
777+ className = "w-full justify-start h-8 text-xs"
778+ onClick = { ( ) => handleExport ( format ) }
779+ >
780+ < Download className = "h-3.5 w-3.5 mr-2" />
781+ Export as { format . toUpperCase ( ) }
782+ </ Button >
783+ ) ) }
784+ </ div >
785+ </ PopoverContent >
786+ </ Popover >
787+ </ div >
788+ ) : null ;
789+
716790 // Render grid content: grouped (multiple tables with headers) or flat (single table)
717791 const gridContent = isGrouped ? (
718792 < div className = "space-y-2" >
@@ -745,7 +819,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
745819 < NavigationOverlay
746820 { ...navigation }
747821 title = { detailTitle }
748- mainContent = { gridContent }
822+ mainContent = { < > { exportToolbar } { gridContent } </ > }
749823 >
750824 { ( record ) => (
751825 < div className = "space-y-3" >
@@ -773,6 +847,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
773847 { isRefreshing ? 'Refreshing…' : 'Pull to refresh' }
774848 </ div >
775849 ) }
850+ { exportToolbar }
776851 { gridContent }
777852 { navigation . isOverlay && (
778853 < NavigationOverlay
0 commit comments