⚡️ feat: Optimize file lists using react-virtual.
This commit is contained in:
parent
7a30f1a112
commit
7330c45c76
6 changed files with 201 additions and 105 deletions
45
web/package-lock.json
generated
45
web/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "telegram-files-web",
|
"name": "telegram-files-web",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.2.0",
|
"@dnd-kit/core": "^6.2.0",
|
||||||
"@dnd-kit/modifiers": "^8.0.0",
|
"@dnd-kit/modifiers": "^8.0.0",
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.1",
|
"@radix-ui/react-toggle": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
|
"@tanstack/react-virtual": "^3.13.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
|
@ -2787,6 +2788,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.13.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
|
@ -9858,6 +9886,19 @@
|
||||||
"@t3-oss/env-core": "0.10.1"
|
"@t3-oss/env-core": "0.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tanstack/react-virtual": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==",
|
||||||
|
"requires": {
|
||||||
|
"@tanstack/virtual-core": "3.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tanstack/virtual-core": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g=="
|
||||||
|
},
|
||||||
"@types/d3-array": {
|
"@types/d3-array": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.1",
|
"@radix-ui/react-toggle": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@t3-oss/env-nextjs": "^0.10.1",
|
"@t3-oss/env-nextjs": "^0.10.1",
|
||||||
|
"@tanstack/react-virtual": "^3.13.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { FileCard } from "./file-card";
|
import { FileCard } from "./file-card";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
import { Download, LoaderCircle, LoaderPinwheel } from "lucide-react";
|
||||||
import { useFiles } from "@/hooks/use-files";
|
import { useFiles } from "@/hooks/use-files";
|
||||||
import { FileFilters } from "@/components/file-filters";
|
import { FileFilters } from "@/components/file-filters";
|
||||||
import { useRowHeightLocalStorage } from "@/components/table-row-height-switch";
|
import {
|
||||||
|
getRowHeightPX,
|
||||||
|
useRowHeightLocalStorage,
|
||||||
|
} from "@/components/table-row-height-switch";
|
||||||
import { type Column } from "@/components/table-column-filter";
|
import { type Column } from "@/components/table-column-filter";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import FileNotFount from "@/components/file-not-found";
|
import FileNotFount from "@/components/file-not-found";
|
||||||
|
|
@ -22,6 +17,7 @@ import useIsMobile from "@/hooks/use-is-mobile";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { POST } from "@/lib/api";
|
import { POST } from "@/lib/api";
|
||||||
import FileRow from "@/components/file-row";
|
import FileRow from "@/components/file-row";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
|
||||||
interface FileListProps {
|
interface FileListProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
@ -29,21 +25,26 @@ interface FileListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS: Column[] = [
|
||||||
{ id: "content", label: "Content", isVisible: true },
|
{
|
||||||
|
id: "content",
|
||||||
|
label: "Content",
|
||||||
|
isVisible: true,
|
||||||
|
className: "text-center",
|
||||||
|
},
|
||||||
{ id: "type", label: "Type", isVisible: true, className: "w-16 text-center" },
|
{ id: "type", label: "Type", isVisible: true, className: "w-16 text-center" },
|
||||||
{
|
{
|
||||||
id: "size",
|
id: "size",
|
||||||
label: "Size",
|
label: "Size",
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
className: "w-20 max-w-20 text-center",
|
className: "w-20 text-center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
className: "w-16 text-center",
|
className: "w-32 text-center",
|
||||||
},
|
},
|
||||||
{ id: "extra", label: "Extra", isVisible: true },
|
{ id: "extra", label: "Extra", isVisible: true, className: "flex-1" },
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
label: "Actions",
|
label: "Actions",
|
||||||
|
|
@ -56,6 +57,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
|
||||||
const observerTarget = useRef(null);
|
const observerTarget = useRef(null);
|
||||||
|
const tableParentRef = useRef(null);
|
||||||
const [columns, setColumns] = useState<Column[]>(COLUMNS);
|
const [columns, setColumns] = useState<Column[]>(COLUMNS);
|
||||||
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
|
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
|
||||||
"telegramFileList",
|
"telegramFileList",
|
||||||
|
|
@ -85,6 +87,42 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const rowVirtual = useVirtualizer({
|
||||||
|
count: files.length,
|
||||||
|
getScrollElement: () => tableParentRef.current,
|
||||||
|
estimateSize: (index) => {
|
||||||
|
const file = files[index]!;
|
||||||
|
const height = getRowHeightPX(rowHeight);
|
||||||
|
|
||||||
|
if (
|
||||||
|
file.downloadStatus === "idle" ||
|
||||||
|
file.downloadStatus === "completed" ||
|
||||||
|
file.size === 0
|
||||||
|
) {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
return height + 8;
|
||||||
|
},
|
||||||
|
paddingStart: 1,
|
||||||
|
paddingEnd: 1,
|
||||||
|
overscan: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
rowVirtual.measure();
|
||||||
|
}, [rowHeight, rowVirtual]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [lastItem] = [...rowVirtual.getVirtualItems()].reverse();
|
||||||
|
if (!lastItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastItem.index >= files.length - 1) {
|
||||||
|
void handleLoadMore();
|
||||||
|
}
|
||||||
|
//eslint-disable-next-line
|
||||||
|
}, [files.length, handleLoadMore, rowVirtual.getVirtualItems()]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
|
|
@ -108,7 +146,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
case "s":
|
case "s":
|
||||||
return {
|
return {
|
||||||
content: "h-6 w-6",
|
content: "h-6 w-6",
|
||||||
contentCell: "w-4",
|
contentCell: "w-16",
|
||||||
};
|
};
|
||||||
case "m":
|
case "m":
|
||||||
return {
|
return {
|
||||||
|
|
@ -194,7 +232,7 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
rowHeight={rowHeight}
|
rowHeight={rowHeight}
|
||||||
setRowHeight={setRowHeight}
|
setRowHeight={setRowHeight}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-4">
|
<div className="h-[calc(100vh-13rem)] space-y-4 overflow-hidden">
|
||||||
{selectedFiles.size > 0 && (
|
{selectedFiles.size > 0 && (
|
||||||
<div className="flex items-center justify-between rounded-lg bg-muted/50 p-4">
|
<div className="flex items-center justify-between rounded-lg bg-muted/50 p-4">
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
|
|
@ -234,61 +272,75 @@ export function FileList({ accountId, chatId }: FileListProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative min-h-[calc(100vh-14rem)] rounded-md border">
|
<div
|
||||||
<Table>
|
className="relative h-full overflow-auto rounded-md border"
|
||||||
<TableHeader>
|
ref={tableParentRef}
|
||||||
<TableRow>
|
>
|
||||||
<TableHead className="w-[30px] text-center">
|
<div className="sticky top-0 z-20 flex h-10 items-center border-b bg-background/90 text-sm text-muted-foreground backdrop-blur-sm">
|
||||||
<Checkbox
|
<div className="w-[30px] text-center">
|
||||||
checked={selectedFiles.size === files.length}
|
<Checkbox
|
||||||
onCheckedChange={handleSelectAll}
|
checked={selectedFiles.size === files.length}
|
||||||
/>
|
onCheckedChange={handleSelectAll}
|
||||||
</TableHead>
|
/>
|
||||||
{columns.map((col) =>
|
</div>
|
||||||
col.isVisible ? (
|
{columns.map((col) =>
|
||||||
<TableHead
|
col.isVisible ? (
|
||||||
key={col.id}
|
<div
|
||||||
suppressHydrationWarning
|
key={col.id}
|
||||||
className={cn(
|
suppressHydrationWarning
|
||||||
col.className ?? "",
|
className={cn(
|
||||||
col.id === "content" ? dynamicClass.contentCell : "",
|
col.className ?? "",
|
||||||
)}
|
col.id === "content" ? dynamicClass.contentCell : "",
|
||||||
>
|
)}
|
||||||
{col.label}
|
>
|
||||||
</TableHead>
|
{col.label}
|
||||||
) : null,
|
</div>
|
||||||
)}
|
) : null,
|
||||||
</TableRow>
|
)}
|
||||||
</TableHeader>
|
</div>
|
||||||
<TableBody className="[&_tr:last-child]:border-b">
|
{files.length === 0 && isLoading && (
|
||||||
{files.map((file, index) => (
|
<div className="sticky left-1/2 top-0 z-10 flex h-full w-full items-center justify-center bg-accent">
|
||||||
<FileRow
|
|
||||||
file={file}
|
|
||||||
checked={selectedFiles.has(file.id)}
|
|
||||||
onCheckedChange={() => handleSelectFile(file.id)}
|
|
||||||
properties={{ rowHeight, dynamicClass, columns }}
|
|
||||||
key={`${file.messageId}-${file.uniqueId}-${index}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{!isLoading && files.length === 0 && (
|
|
||||||
<TableRow className="border-b-0">
|
|
||||||
<TableCell colSpan={columns.length + 1}>
|
|
||||||
<FileNotFount />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
{isLoading && (
|
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-accent bg-opacity-90">
|
|
||||||
<LoaderPinwheel
|
<LoaderPinwheel
|
||||||
className="h-8 w-8 animate-spin"
|
className="h-8 w-8 animate-spin"
|
||||||
style={{ strokeWidth: "0.8px" }}
|
style={{ strokeWidth: "0.8px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="h-full">
|
||||||
|
<div
|
||||||
|
className={cn("relative w-full")}
|
||||||
|
style={{ height: `${rowVirtual.getTotalSize()}px` }}
|
||||||
|
>
|
||||||
|
{files.length !== 0 &&
|
||||||
|
rowVirtual.getVirtualItems().map((virtualRow) => {
|
||||||
|
const file = files[virtualRow.index]!;
|
||||||
|
return (
|
||||||
|
<FileRow
|
||||||
|
index={virtualRow.index}
|
||||||
|
className={cn(
|
||||||
|
"absolute left-0 top-0 flex w-full items-center",
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
height: `${virtualRow.size}px`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}}
|
||||||
|
ref={rowVirtual.measureElement}
|
||||||
|
file={file}
|
||||||
|
checked={selectedFiles.has(file.id)}
|
||||||
|
onCheckedChange={() => handleSelectFile(file.id)}
|
||||||
|
properties={{
|
||||||
|
rowHeight: rowHeight,
|
||||||
|
dynamicClass,
|
||||||
|
columns,
|
||||||
|
}}
|
||||||
|
key={`${file.messageId}-${file.uniqueId}-${virtualRow.index}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{!isLoading && files.length === 0 && <FileNotFount />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={observerTarget} className="h-4"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { TableCell, TableRow } from "@/components/ui/table";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { type RowHeight } from "@/components/table-row-height-switch";
|
||||||
getRowHeightTailwindClass,
|
|
||||||
type RowHeight,
|
|
||||||
} from "@/components/table-row-height-switch";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import FileProgress from "@/components/file-progress";
|
import FileProgress from "@/components/file-progress";
|
||||||
import React, { memo, type ReactNode, useState } from "react";
|
import React, { memo, type ReactNode, useState } from "react";
|
||||||
|
|
@ -26,6 +22,10 @@ import { type Column } from "./table-column-filter";
|
||||||
import { useFileSpeed } from "@/hooks/use-file-speed";
|
import { useFileSpeed } from "@/hooks/use-file-speed";
|
||||||
|
|
||||||
interface FileRowProps {
|
interface FileRowProps {
|
||||||
|
index: number;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
ref?: React.Ref<HTMLTableRowElement>;
|
||||||
file: TelegramFile;
|
file: TelegramFile;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -40,6 +40,10 @@ interface FileRowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FileRow({
|
export default function FileRow({
|
||||||
|
index,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
ref,
|
||||||
file,
|
file,
|
||||||
checked,
|
checked,
|
||||||
properties,
|
properties,
|
||||||
|
|
@ -112,13 +116,13 @@ export default function FileRow({
|
||||||
),
|
),
|
||||||
type: (
|
type: (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="capitalize">{file.type}</span>
|
<span className="text-sm capitalize">{file.type}</span>
|
||||||
{process.env.NODE_ENV === "development" && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<span className="text-xs">{file.id}</span>
|
<span className="text-xs">{file.id}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
size: <span>{prettyBytes(file.size)}</span>,
|
size: <span className="text-sm">{prettyBytes(file.size)}</span>,
|
||||||
status: <FileStatus file={file} />,
|
status: <FileStatus file={file} />,
|
||||||
extra: <FileExtra file={file} rowHeight={rowHeight} />,
|
extra: <FileExtra file={file} rowHeight={rowHeight} />,
|
||||||
actions: (
|
actions: (
|
||||||
|
|
@ -131,22 +135,25 @@ export default function FileRow({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<div
|
||||||
<TableRow
|
data-index={index}
|
||||||
className={cn(getRowHeightTailwindClass(rowHeight), "border-b-0")}
|
className={cn("flex w-full flex-col border-b", className)}
|
||||||
onMouseEnter={() => setHovered(true)}
|
style={style}
|
||||||
onMouseLeave={() => setHovered(false)}
|
ref={ref}
|
||||||
>
|
onMouseEnter={() => setHovered(true)}
|
||||||
<TableCell className="text-center">
|
onMouseLeave={() => setHovered(false)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-1 items-center hover:bg-accent">
|
||||||
|
<div className="w-[30px] text-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onCheckedChange={onCheckedChange}
|
onCheckedChange={onCheckedChange}
|
||||||
disabled={file.downloadStatus !== "idle"}
|
disabled={file.downloadStatus !== "idle"}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</div>
|
||||||
{columns.map((col) =>
|
{columns.map((col) =>
|
||||||
col.isVisible ? (
|
col.isVisible ? (
|
||||||
<TableCell
|
<div
|
||||||
key={col.id}
|
key={col.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
col.className ?? "",
|
col.className ?? "",
|
||||||
|
|
@ -154,16 +161,14 @@ export default function FileRow({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{columnRenders[col.id]}
|
{columnRenders[col.id]}
|
||||||
</TableCell>
|
</div>
|
||||||
) : null,
|
) : null,
|
||||||
)}
|
)}
|
||||||
</TableRow>
|
</div>
|
||||||
<TableRow>
|
<div className="w-full">
|
||||||
<TableCell colSpan={columns.length + 1} className="h-px p-0">
|
<FileProgress file={file} downloadProgress={downloadProgress} />
|
||||||
<FileProgress file={file} downloadProgress={downloadProgress} />
|
</div>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core";
|
||||||
|
|
@ -14,7 +12,7 @@ import {
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { ChevronDown, Columns, Menu } from "lucide-react";
|
import { ChevronDown, Columns, Menu } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -84,9 +82,7 @@ export default function TableColumnFilter({
|
||||||
onColumnConfigChange,
|
onColumnConfigChange,
|
||||||
}: TableColumnFilterProps) {
|
}: TableColumnFilterProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [columnConfig, setColumnConfig] = useState<Column[]>(
|
const [columnConfig, setColumnConfig] = useState<Column[]>(columns);
|
||||||
columns.map((col) => ({ id: col.id, label: col.label, isVisible: true })),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleVisibility = (id: string) => {
|
const handleToggleVisibility = (id: string) => {
|
||||||
setColumnConfig((prev) =>
|
setColumnConfig((prev) =>
|
||||||
|
|
@ -107,10 +103,9 @@ export default function TableColumnFilter({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyChanges = () => {
|
useEffect(() => {
|
||||||
const visibleColumns = columnConfig.filter((col) => col.isVisible);
|
onColumnConfigChange(columnConfig);
|
||||||
onColumnConfigChange(visibleColumns);
|
}, [columnConfig, onColumnConfigChange]);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu open={isOpen}>
|
<DropdownMenu open={isOpen}>
|
||||||
|
|
@ -151,10 +146,6 @@ export default function TableColumnFilter({
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={applyChanges}>
|
|
||||||
Apply Changes
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,22 @@ const heightOptions = [
|
||||||
{
|
{
|
||||||
id: "s",
|
id: "s",
|
||||||
label: "Small",
|
label: "Small",
|
||||||
value: "h-6",
|
value: "h-12",
|
||||||
|
px: 48,
|
||||||
icon: <Rows4 className="h-4 w-4" />,
|
icon: <Rows4 className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "m",
|
id: "m",
|
||||||
label: "Medium",
|
label: "Medium",
|
||||||
value: "h-24",
|
value: "h-24",
|
||||||
|
px: 96,
|
||||||
icon: <Rows3 className="h-4 w-4" />,
|
icon: <Rows3 className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "l",
|
id: "l",
|
||||||
label: "Large",
|
label: "Large",
|
||||||
value: "h-64",
|
value: "h-64",
|
||||||
|
px: 256,
|
||||||
icon: <Rows2 className="h-4 w-4" />,
|
icon: <Rows2 className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
@ -27,7 +30,10 @@ const heightOptions = [
|
||||||
export type RowHeight = (typeof heightOptions)[number]["id"];
|
export type RowHeight = (typeof heightOptions)[number]["id"];
|
||||||
|
|
||||||
export const getRowHeightTailwindClass = (rowHeight: RowHeight | undefined) =>
|
export const getRowHeightTailwindClass = (rowHeight: RowHeight | undefined) =>
|
||||||
heightOptions.find((h) => h.id === rowHeight)?.value;
|
heightOptions.find((h) => h.id === rowHeight)?.value ?? heightOptions[1].value;
|
||||||
|
|
||||||
|
export const getRowHeightPX = (rowHeight: RowHeight | undefined) =>
|
||||||
|
heightOptions.find((h) => h.id === rowHeight)?.px ?? heightOptions[1].px;
|
||||||
|
|
||||||
export function useRowHeightLocalStorage(
|
export function useRowHeightLocalStorage(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue