diff --git a/client-electron/src/hooks/filemanager-android.ts b/client-electron/src/hooks/filemanager-android.ts new file mode 100644 index 0000000..687bb39 --- /dev/null +++ b/client-electron/src/hooks/filemanager-android.ts @@ -0,0 +1,271 @@ +import { create } from 'zustand' +import useAdb from './useAdb' +import { toast } from '@/components/ui/use-toast' +import { type LinuxFileType } from '@yume-chan/adb' +import { fromUnixTime } from 'date-fns' +import { Consumable, ReadableStream, WritableStream } from '@yume-chan/stream-extra' +import JSZip from 'jszip' + +export interface AndroidFile { + filename: string + type: LinuxFileType + size: bigint + dateModified: Date +} + +interface FileManagerAndroidHook { + rootPath: string + currentPath: string + pushPath: (path: string) => void + popPath: () => void + setCurrentPath: (path: string) => void + scanPath: (path?: string) => Promise + pushFile: (file: File, targetPath: string) => Promise + pushFiles: (files: File[], targetPath: string) => Promise + createDirectory: (dirName: string) => Promise + delete: (filename: string) => Promise + rename: (filename: string, newName: string) => Promise + download: (files: AndroidFile[]) => Promise +} + +const useFileManager = create((set, get) => ({ + rootPath: '/mnt/sdcard/coffeevending', + currentPath: '', + pushPath(path) { + set({ currentPath: get().currentPath + '/' + path }) + console.log('currentPath', get().currentPath) + }, + popPath() { + set({ currentPath: get().currentPath.slice(0, get().currentPath.lastIndexOf('/')) }) + console.log('currentPath', get().currentPath) + }, + setCurrentPath(path) { + set({ currentPath: path }) + }, + async scanPath(path) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const sync = await adb.sync() + try { + console.log('scanning path', get().rootPath + path) + const entries = await sync.readdir(get().rootPath + path) + return entries.reduce((acc, entry) => { + if (entry.name === '.' || entry.name === '..') { + return acc + } + + return [ + ...acc, + { + filename: entry.name, + type: entry.type, + size: entry.size, + dateModified: fromUnixTime(Number(entry.mtime)) + } + ] + }, []) + } finally { + await sync.dispose() + } + }, + async pushFile(file, targetPath) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const buffer = await file.arrayBuffer() + const sync = await adb.sync() + try { + await sync.write({ + filename: targetPath + '/' + file.name, + file: new ReadableStream({ + start(controller) { + controller.enqueue(new Consumable(new Uint8Array(buffer))) + controller.close() + } + }) + }) + } finally { + await sync.dispose() + } + }, + async pushFiles(files, targetPath) { + for (const file of files) { + await get().pushFile(file, targetPath) + } + }, + async createDirectory(name) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const process = await adb.subprocess.spawn('mkdir ' + get().rootPath + '/' + name) + process.stderr.pipeTo( + new WritableStream({ + write(chunk) { + console.error(chunk) + } + }) + ) + + process.stdout.pipeTo( + new WritableStream({ + write(chunk) { + console.log(chunk) + } + }) + ) + + if ((await process.exit) != 0) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to create directory', + description: 'Please try again' + }) + } + }, + async delete(filename) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const process = await adb.subprocess.spawn('rm ' + get().rootPath + '/' + filename) + process.stderr.pipeTo( + new WritableStream({ + write(chunk) { + console.error(chunk) + } + }) + ) + + process.stdout.pipeTo( + new WritableStream({ + write(chunk) { + console.log(chunk) + } + }) + ) + + if ((await process.exit) != 0) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to delete file', + description: 'Please try again' + }) + } + }, + async rename(filename, newName) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const process = await adb.subprocess.spawn( + 'mv ' + get().rootPath + '/' + filename + ' ' + get().rootPath + '/' + newName + ) + process.stderr.pipeTo( + new WritableStream({ + write(chunk) { + console.error(chunk) + } + }) + ) + + process.stdout.pipeTo( + new WritableStream({ + write(chunk) { + console.log(chunk) + } + }) + ) + + if ((await process.exit) != 0) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to rename file', + description: 'Please try again' + }) + } + }, + async download(files) { + const adb = useAdb.getState().adb + if (!adb) { + toast({ + duration: 3000, + variant: 'destructive', + title: 'Failed to connect to device', + description: 'Please connect Adb first' + }) + return + } + + const sync = await adb.sync() + try { + const zip = new JSZip() + for (const file of files) { + const buffer: Uint8Array[] = [] + const fileStream = sync.read(get().rootPath + '/' + file.filename) + await fileStream.pipeTo( + new WritableStream({ + write(chunk) { + buffer.push(chunk) + } + }) + ) + + zip.file(file.filename, new Blob(buffer)) + } + + const blob = await zip.generateAsync({ type: 'blob' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'download.zip' + a.click() + URL.revokeObjectURL(url) + } finally { + await sync.dispose() + } + } +})) + +export default useFileManager diff --git a/client-electron/src/main.tsx b/client-electron/src/main.tsx index d2bb9b5..c2e141c 100644 --- a/client-electron/src/main.tsx +++ b/client-electron/src/main.tsx @@ -4,9 +4,9 @@ import App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - - - + // + + // ) if (window.electronRuntime) { diff --git a/client-electron/src/pages/android/android.tsx b/client-electron/src/pages/android/android.tsx index 00a61fe..f97b9e0 100644 --- a/client-electron/src/pages/android/android.tsx +++ b/client-electron/src/pages/android/android.tsx @@ -36,7 +36,7 @@ const AndroidPage: React.FC = () => { - + diff --git a/client-electron/src/pages/android/components/file-manager-tab.tsx b/client-electron/src/pages/android/components/file-manager-tab.tsx index 669dd2d..665a26d 100644 --- a/client-electron/src/pages/android/components/file-manager-tab.tsx +++ b/client-electron/src/pages/android/components/file-manager-tab.tsx @@ -1,158 +1,97 @@ -import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { LinuxFileType, type Adb } from '@yume-chan/adb' -import { Consumable, WritableStream, ReadableStream } from '@yume-chan/stream-extra' -import JSZip from 'jszip' -import { useCallback, useState } from 'react' +import useFileManager, { type AndroidFile } from '@/hooks/filemanager-android' +import { useEffect, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' +import DataTable from './filemanager-table/data-table' +import { ChevronRightIcon, FileIcon } from '@radix-ui/react-icons' +import { type ColumnDef } from '@tanstack/react-table' +import { LinuxFileType } from '@yume-chan/adb' +import { formatDate } from 'date-fns' +import DataTableColumnHeader from './filemanager-table/data-table-column-header' +import DataTableRowActions from './filemanager-table/data-table-row-actions' +import { Checkbox } from '@/components/ui/checkbox' -interface FileManagerTabProps { - adb: Adb | undefined -} +export const FileManagerTab: React.FC = () => { + const { currentPath, pushPath, popPath, setCurrentPath, scanPath } = useFileManager( + useShallow(state => ({ + currentPath: state.currentPath, + pushPath: state.pushPath, + popPath: state.popPath, + setCurrentPath: state.setCurrentPath, + scanPath: state.scanPath + })) + ) -type filesType = { - file: string - indent: number - blob?: Blob - files?: filesType[] -} - -export const FileManagerTab: React.FC = ({ adb }) => { - const [path, setPath] = useState('') - const [pushPath, setPushPath] = useState('') - const [pushFile, setPushFile] = useState() - - const [files, setFiles] = useState() - - const pushFiles = async (filename: string, blob: Blob, targetPath: string) => { - if (!adb) return - - const buffer = await blob.arrayBuffer() - console.log(blob, buffer, targetPath, filename) - const sync = await adb.sync() - try { - await sync.write({ - filename: targetPath + '/' + filename, - file: new ReadableStream({ - start(controller) { - controller.enqueue(new Consumable(new Uint8Array(buffer))) - controller.close() - } - }) - }) - } finally { - await sync.dispose() - } - } - - const zipFiles = (files: filesType) => { - const zip = new JSZip() - - const addFiles = (folder: filesType, parent: JSZip | null) => { - folder.files?.forEach(file => { - if (file.files) { - let newFolder - if (parent) { - newFolder = parent.folder(file.file) - } else { - newFolder = zip.folder(file.file) - } - addFiles(file, newFolder) - } else { - if (!file.blob) { - return - } - if (parent) { - parent.file(file.file, file.blob) - } else { - zip.file(file.file, file.blob) - } - } - }) - } - - addFiles(files, zip.folder(files.file)) - zip.generateAsync({ type: 'blob' }).then(content => { - const url = URL.createObjectURL(content) - const a = document.createElement('a') - a.href = url - a.download = files.file + '.zip' - a.click() + useEffect(() => { + console.log('scanning path', currentPath) + scanPath(currentPath).then(files => { + console.log(files) + setFiles(files) + setIsLoading(false) }) - } + }, [currentPath]) - const download = useCallback(() => { - const readFiles = async (adb: Adb) => { - const sync = await adb.sync() - try { - const folder: filesType = { - file: path.split('/').pop() || 'scannedFiles', - indent: 0, - files: [] - } + const [files, setFiles] = useState(undefined) + const [isLoading, setIsLoading] = useState(true) - const TypeBlob: { - [key: string]: string - } = { - mp4: 'video/mp4', - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - gif: 'image/gif' - } - - // scan all files in the folder - const scanFiles = async (folder: filesType, path: string, indent: number) => { - const entries = await sync.readdir(path) - for (const entry of entries) { - if (entry.name.startsWith('.')) { - continue - } - if (entry.type === LinuxFileType.Directory) { - const newFolder: filesType = { - file: entry.name, - indent: indent, - files: [] - } - folder.files?.push(newFolder) - await scanFiles(newFolder, `${path}/${entry.name}`, indent + 1) - } else { - const buffer: Uint8Array[] = [] - const fileStream = sync.read(`${path}/${entry.name}`) - await fileStream.pipeTo( - new WritableStream({ - write(chunk) { - buffer.push(chunk) - } - }) - ) - - const blob = new Blob(buffer, { type: TypeBlob[entry.name.split('.').pop()!] }) - folder.files?.push({ - file: entry.name, - blob: blob, - indent: indent - }) - } - } - } - console.log('Scanning files...') - await scanFiles(folder, path, 1) - console.log('Scanned files') - setFiles(folder) - console.log(folder) - - // Zip files - zipFiles(folder) - } finally { - await sync.dispose() - } + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false + }, + { + id: 'filename', + header: ({ column }) => , + cell: ({ row }) => { + return ( +
{ + if (row.original.type !== LinuxFileType.File) pushPath(row.original.filename) + }} + > + {row.original.type === LinuxFileType.File && } + + {row.original.filename} + +
+ ) + }, + enableSorting: true, + enableHiding: false + }, + { + id: 'size', + header: ({ column }) => , + cell: ({ row }) =>
{row.original.size.toString()}
+ }, + { + id: 'dateModified', + header: ({ column }) => , + cell: ({ row }) =>
{formatDate(row.original.dateModified, 'dd MMM yyyy HH:mm:ss')}
+ }, + { + id: 'actions', + cell: ({ row }) => } - - if (adb) { - readFiles(adb) - } - }, [adb, path]) + ] return ( @@ -161,46 +100,34 @@ export const FileManagerTab: React.FC = ({ adb }) => { Manage files in Android -
-
-
- setPath(e.target.value)} /> - -
-
- setPushPath(e.target.value)} /> - setPushFile(e.target.files?.item(0))} /> - -
-
-
{files && }
+
+ setCurrentPath('')}> + ROOT + + {currentPath && + currentPath.split('/').map((path, index) => { + return ( +
+ + setCurrentPath( + currentPath + .split('/') + .slice(0, index + 1) + .join('/') + ) + } + > + {path} + + {currentPath.split('/').length - 1 !== index && } +
+ ) + })}
+ ) } - -const FileTree = ({ files }: { files: filesType }) => { - return ( -
- {files.files?.map((file, index) => { - return ( -
- {file.file} -
{file.files && }
-
- ) - })} -
- ) -} diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-column-header.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-column-header.tsx new file mode 100644 index 0000000..aa315b6 --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-column-header.tsx @@ -0,0 +1,62 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons' +import { type Column } from '@tanstack/react-table' + +interface DataTableColumnHeaderProps extends React.HTMLAttributes { + column: Column + title: string +} + +const DataTableColumnHeader = ({ + column, + title, + className +}: DataTableColumnHeaderProps) => { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + column.toggleVisibility(false)}> + + Hide + + + +
+ ) +} + +export default DataTableColumnHeader diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-faceted-filter.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-faceted-filter.tsx new file mode 100644 index 0000000..f7efb65 --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-faceted-filter.tsx @@ -0,0 +1,125 @@ +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { Separator } from '@/components/ui/separator' +import { PlusCircledIcon, CheckIcon } from '@radix-ui/react-icons' +import { type Column } from '@tanstack/react-table' +import { Badge } from '@/components/ui/badge' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from '@/components/ui/command' + +interface DataTableFacetedFilterProps { + column?: Column + title?: string + options: TOption extends { value: string; label: string; icon?: React.ComponentType<{ className?: string }> } + ? TOption[] + : never +} + +const DataTableFacetedFilter = ({ + column, + title, + options +}: DataTableFacetedFilterProps) => { + const facets = column?.getFacetedUniqueValues() + const selectedValues = new Set(column?.getFilterValue() as string[]) + + return ( + + + + + + + + + No results found. + + {options.map(option => { + const isSelected = selectedValues.has(option.value) + return ( + { + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + const filterValues = Array.from(selectedValues) + column?.setFilterValue(filterValues.length ? filterValues : undefined) + }} + > +
+ +
+ {option.icon && } + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} +
+ ) + })} +
+ {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + + + + )} +
+
+
+
+ ) +} + +export default DataTableFacetedFilter diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-pagination.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-pagination.tsx new file mode 100644 index 0000000..493e4b9 --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-pagination.tsx @@ -0,0 +1,83 @@ +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { DoubleArrowLeftIcon, ChevronLeftIcon, ChevronRightIcon, DoubleArrowRightIcon } from '@radix-ui/react-icons' +import { type Table } from '@tanstack/react-table' + +interface DataTablePaginationProps { + table: Table +} + +const DataTablePagination = ({ table }: DataTablePaginationProps) => { + return ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} +
+
+ + + + +
+
+
+ ) +} + +export default DataTablePagination diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-row-actions.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-row-actions.tsx new file mode 100644 index 0000000..76a5224 --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-row-actions.tsx @@ -0,0 +1,62 @@ +import { type Row } from '@tanstack/react-table' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' +import { DotsHorizontalIcon } from '@radix-ui/react-icons' +import { labels } from '@/models/data' + +interface DataTableRowActionsProps { + row: Row +} + +const DataTableRowActions = ({ row }: DataTableRowActionsProps) => { + const task = { + label: 'none' + } + + return ( + + + + + + Edit + Make a copy + + + Labels + + + {labels.map(label => ( + + {label.label} + + ))} + + + + + + Delete + ⌘⌫ + + + + ) +} + +export default DataTableRowActions diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-toolbar.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-toolbar.tsx new file mode 100644 index 0000000..769323f --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-toolbar.tsx @@ -0,0 +1,32 @@ +import { Button } from '@/components/ui/button' +import { type Table } from '@tanstack/react-table' +import DataTableViewOptions from './data-table-view-options' +import { Cross2Icon } from '@radix-ui/react-icons' + +interface DataTableToolbarProps { + table: Table +} + +const DataTableToolbar = ({ table }: DataTableToolbarProps) => { + const isFiltered = table.getState().columnFilters.length > 0 + + return ( +
+
+
+
+ {isFiltered && ( + + )} +
+ +
+
+
+ ) +} + +export default DataTableToolbar diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table-view-options.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table-view-options.tsx new file mode 100644 index 0000000..6a4c150 --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table-view-options.tsx @@ -0,0 +1,49 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { MixerHorizontalIcon } from '@radix-ui/react-icons' +import { type Table } from '@tanstack/react-table' + +interface DataTableViewOptionsProps { + table: Table +} + +const DataTableViewOptions = ({ table }: DataTableViewOptionsProps) => { + return ( + + + + + + Toggle columns + + {table + .getAllColumns() + .filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide()) + .map(column => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} + + + ) +} + +export default DataTableViewOptions diff --git a/client-electron/src/pages/android/components/filemanager-table/data-table.tsx b/client-electron/src/pages/android/components/filemanager-table/data-table.tsx new file mode 100644 index 0000000..e95771e --- /dev/null +++ b/client-electron/src/pages/android/components/filemanager-table/data-table.tsx @@ -0,0 +1,120 @@ +import type { VisibilityState, SortingState, ColumnDef, FilterFn, ColumnFiltersState } from '@tanstack/react-table' +import { + useReactTable, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + flexRender +} from '@tanstack/react-table' +import { rankItem } from '@tanstack/match-sorter-utils' +import { useState } from 'react' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import DataTablePagination from './data-table-pagination' +import { ReloadIcon } from '@radix-ui/react-icons' +import DataTableToolbar from './data-table-toolbar' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta({ + itemRank + }) + + // Return if the item should be filtered in/out + return itemRank.passed +} + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] + isLoading: boolean +} + +const DataTable = ({ columns, data, isLoading }: DataTableProps) => { + const [rowSelection, setRowSelection] = useState({}) + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnFilters, setColumnFilters] = useState([]) + const [sorting, setSorting] = useState([]) + + const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter + }, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters + }, + globalFilterFn: fuzzyFilter, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues() + }) + return ( +
+ + +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : isLoading ? ( + + + Loading... + + + + ) : ( + + + No results. + + + )} + +
+
+ +
+ ) +} + +export default DataTable diff --git a/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx index 3369fe9..aa315b6 100644 --- a/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx +++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx @@ -1,5 +1,4 @@ import { Button } from '@/components/ui/button' -import { Calendar } from '@/components/ui/calendar' import { DropdownMenu, DropdownMenuContent, @@ -8,52 +7,23 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' -import { ArrowDownIcon, ArrowUpIcon, CalendarIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons' +import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons' import { type Column } from '@tanstack/react-table' -import { DateRange } from 'react-day-picker' interface DataTableColumnHeaderProps extends React.HTMLAttributes { column: Column title: string - isDate?: boolean } const DataTableColumnHeader = ({ column, title, - isDate, className }: DataTableColumnHeaderProps) => { - if (!column.getCanSort() && !isDate) { + if (!column.getCanSort()) { return
{title}
} - if (isDate) { - return ( -
- - - - - - column.setFilterValue(date)} - numberOfMonths={2} - disabled={date => date > new Date() || date < new Date('1900-01-01')} - initialFocus - /> - - -
- ) - } - return (
diff --git a/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx index 6897f20..b20c547 100644 --- a/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx +++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx @@ -39,7 +39,7 @@ const DataTableRowActions = ({ row }: DataTableRowActionsProps) = Labels - + {labels.map(label => ( {label.label} diff --git a/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx index 1e74aae..63f5f6f 100644 --- a/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx +++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx @@ -16,6 +16,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import DataTablePagination from './data-table-pagination' import { ReloadIcon } from '@radix-ui/react-icons' +// eslint-disable-next-line @typescript-eslint/no-explicit-any const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { // Rank the item const itemRank = rankItem(row.getValue(columnId), value)