mirror of
https://github.com/diced/zipline.git
synced 2026-01-15 06:13:49 -08:00
360 lines
10 KiB
TypeScript
360 lines
10 KiB
TypeScript
import RelativeDate from '@/components/RelativeDate';
|
|
import FileModal from '@/components/file/DashboardFile/FileModal';
|
|
import { copyFile, deleteFile, viewFile } from '@/components/file/actions';
|
|
import { type File } from '@/lib/db/models/file';
|
|
import {
|
|
ActionIcon,
|
|
Box,
|
|
Button,
|
|
Group,
|
|
Paper,
|
|
Select,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
import { useClipboard } from '@mantine/hooks';
|
|
import { notifications } from '@mantine/notifications';
|
|
import type { Prisma } from '@prisma/client';
|
|
import { IconCopy, IconExternalLink, IconFile, IconStar, IconTrashFilled } from '@tabler/icons-react';
|
|
import { DataTable } from 'mantine-datatable';
|
|
import { useRouter } from 'next/router';
|
|
import { useEffect, useReducer, useState } from 'react';
|
|
import { bulkDelete, bulkFavorite } from '../bulk';
|
|
import { useApiPagination } from '../useApiPagination';
|
|
import { bytes } from '@/lib/bytes';
|
|
import { Response } from '@/lib/api/response';
|
|
import { useSettingsStore } from '@/lib/store/settings';
|
|
|
|
const PER_PAGE_OPTIONS = [10, 20, 50];
|
|
|
|
const NAMES = {
|
|
up: {
|
|
name: 'Name',
|
|
originalName: 'Original name',
|
|
type: 'Type',
|
|
},
|
|
low: {
|
|
name: 'name',
|
|
originalName: 'original name',
|
|
type: 'type',
|
|
},
|
|
};
|
|
|
|
function SearchFilter({
|
|
setSearchField,
|
|
searchQuery,
|
|
setSearchQuery,
|
|
field,
|
|
}: {
|
|
searchQuery: {
|
|
name: string;
|
|
originalName: string;
|
|
type: string;
|
|
};
|
|
setSearchField: (...args: any) => void;
|
|
setSearchQuery: (...args: any) => void;
|
|
field: 'name' | 'originalName' | 'type';
|
|
}) {
|
|
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setSearchField(field);
|
|
|
|
setSearchQuery({
|
|
field,
|
|
query: e.target.value,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<TextInput
|
|
label={NAMES.up[field]}
|
|
placeholder={`Search by ${NAMES.low[field]}`}
|
|
value={searchQuery[field]}
|
|
onChange={onChange}
|
|
variant='filled'
|
|
size='sm'
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default function FileTable({ id }: { id?: string }) {
|
|
const router = useRouter();
|
|
const clipboard = useClipboard();
|
|
const [searchThreshold, warnDeletion] = useSettingsStore((state) => [
|
|
state.settings.searchThreshold,
|
|
state.settings.warnDeletion,
|
|
]);
|
|
|
|
const [page, setPage] = useState<number>(router.query.page ? parseInt(router.query.page as string) : 1);
|
|
const [perpage, setPerpage] = useState<number>(20);
|
|
const [sort, setSort] = useState<keyof Prisma.FileOrderByWithAggregationInput>('createdAt');
|
|
const [order, setOrder] = useState<'asc' | 'desc'>('desc');
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
|
|
const [searchField, setSearchField] = useState<'name' | 'originalName' | 'type'>('name');
|
|
const [searchQuery, setSearchQuery] = useReducer(
|
|
(
|
|
state: { name: string; originalName: string; type: string },
|
|
action: { field: string; query: string }
|
|
) => {
|
|
return {
|
|
...state,
|
|
[action.field]: action.query,
|
|
};
|
|
},
|
|
{
|
|
name: '',
|
|
originalName: '',
|
|
type: '',
|
|
}
|
|
);
|
|
|
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
|
|
const searching =
|
|
searchQuery.name.trim() !== '' ||
|
|
searchQuery.originalName.trim() !== '' ||
|
|
searchQuery.type.trim() !== '';
|
|
|
|
const { data, isLoading } = useApiPagination({
|
|
page,
|
|
perpage,
|
|
filter: 'all',
|
|
sort,
|
|
order,
|
|
id,
|
|
...(searchQuery[searchField].trim() !== '' && {
|
|
search: {
|
|
threshold: searchThreshold,
|
|
field: searchField,
|
|
query: searchQuery[searchField],
|
|
},
|
|
}),
|
|
});
|
|
|
|
useEffect(() => {
|
|
router.replace(
|
|
{
|
|
query: {
|
|
...router.query,
|
|
page: page,
|
|
},
|
|
},
|
|
undefined,
|
|
{ shallow: true }
|
|
);
|
|
}, [page]);
|
|
|
|
useEffect(() => {
|
|
if (data && selectedFile) {
|
|
const file = data.page.find((x) => x.id === selectedFile.id);
|
|
|
|
if (file) {
|
|
setSelectedFile(file);
|
|
}
|
|
}
|
|
}, [data]);
|
|
|
|
useEffect(() => {
|
|
for (const field of ['name', 'originalName', 'type'] as const) {
|
|
if (field !== searchField) {
|
|
setSearchQuery({
|
|
field,
|
|
query: '',
|
|
});
|
|
}
|
|
}
|
|
}, [searchField]);
|
|
|
|
return (
|
|
<>
|
|
<FileModal
|
|
open={!!selectedFile}
|
|
setOpen={(open) => {
|
|
if (!open) setSelectedFile(null);
|
|
}}
|
|
file={selectedFile}
|
|
/>
|
|
|
|
<Box my='sm'>
|
|
{selectedFiles.length > 0 && (
|
|
<Paper withBorder p='sm' my='sm'>
|
|
<Title order={3}>Operations</Title>
|
|
|
|
<Text size='sm' color='dimmed' mb='xs'>
|
|
Selections are saved across page changes
|
|
</Text>
|
|
|
|
<Group>
|
|
<Button
|
|
variant='outline'
|
|
color='red'
|
|
leftIcon={<IconTrashFilled size='1rem' />}
|
|
onClick={() =>
|
|
bulkDelete(
|
|
selectedFiles.map((x) => x.id),
|
|
setSelectedFiles
|
|
)
|
|
}
|
|
>
|
|
Delete {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''}
|
|
</Button>
|
|
|
|
<Button
|
|
variant='outline'
|
|
color='yellow'
|
|
leftIcon={<IconStar size='1rem' />}
|
|
onClick={() => bulkFavorite(selectedFiles.map((x) => x.id))}
|
|
>
|
|
Favorite {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''}
|
|
</Button>
|
|
|
|
<Button
|
|
variant='outline'
|
|
onClick={() => {
|
|
setSelectedFiles([]);
|
|
}}
|
|
>
|
|
Clear selection
|
|
</Button>
|
|
</Group>
|
|
</Paper>
|
|
)}
|
|
|
|
<DataTable
|
|
borderRadius='sm'
|
|
withBorder
|
|
minHeight={200}
|
|
records={data?.page ?? []}
|
|
columns={[
|
|
{
|
|
accessor: 'name',
|
|
sortable: true,
|
|
filter: (
|
|
<SearchFilter
|
|
setSearchField={setSearchField}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
field='name'
|
|
/>
|
|
),
|
|
filtering: searchField === 'name' && searchQuery.name.trim() !== '',
|
|
},
|
|
{
|
|
accessor: 'originalName',
|
|
sortable: true,
|
|
filter: (
|
|
<SearchFilter
|
|
setSearchField={setSearchField}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
field='originalName'
|
|
/>
|
|
),
|
|
filtering: searchField === 'originalName' && searchQuery.originalName.trim() !== '',
|
|
},
|
|
{
|
|
accessor: 'type',
|
|
sortable: true,
|
|
filter: (
|
|
<SearchFilter
|
|
setSearchField={setSearchField}
|
|
searchQuery={searchQuery}
|
|
setSearchQuery={setSearchQuery}
|
|
field='type'
|
|
/>
|
|
),
|
|
filtering: searchField === 'type' && searchQuery.type.trim() !== '',
|
|
},
|
|
{ accessor: 'size', sortable: true, render: (file) => bytes(file.size) },
|
|
{
|
|
accessor: 'createdAt',
|
|
sortable: true,
|
|
render: (file) => <RelativeDate date={file.createdAt} />,
|
|
},
|
|
{
|
|
accessor: 'favorite',
|
|
sortable: true,
|
|
render: (file) => (file.favorite ? 'Yes' : 'No'),
|
|
},
|
|
{
|
|
accessor: 'similarity',
|
|
title: 'Relevance',
|
|
sortable: true,
|
|
render: (file) => (file.similarity ? file.similarity.toFixed(4) : 'N/A'),
|
|
hidden: !searching,
|
|
},
|
|
{
|
|
accessor: 'actions',
|
|
textAlignment: 'right',
|
|
width: 170,
|
|
render: (file) => (
|
|
<Group spacing='sm'>
|
|
<Tooltip label='More details'>
|
|
<ActionIcon>
|
|
<IconFile size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
|
|
<Tooltip label='View file in new tab'>
|
|
<ActionIcon
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
viewFile(file);
|
|
}}
|
|
>
|
|
<IconExternalLink size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
|
|
<Tooltip label='Copy file link to clipboard'>
|
|
<ActionIcon
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
copyFile(file, clipboard);
|
|
}}
|
|
>
|
|
<IconCopy size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
|
|
<Tooltip label='Delete file'>
|
|
<ActionIcon
|
|
color='red'
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
deleteFile(warnDeletion, file, () => {});
|
|
}}
|
|
>
|
|
<IconTrashFilled size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
),
|
|
},
|
|
]}
|
|
fetching={isLoading}
|
|
totalRecords={searching ? data?.page.length : data?.total ?? 0}
|
|
recordsPerPage={searching ? undefined : perpage}
|
|
onRecordsPerPageChange={searching ? undefined : setPerpage}
|
|
recordsPerPageOptions={searching ? undefined : PER_PAGE_OPTIONS}
|
|
page={searching ? undefined : page}
|
|
onPageChange={searching ? undefined : setPage}
|
|
sortStatus={{
|
|
columnAccessor: sort,
|
|
direction: order,
|
|
}}
|
|
onSortStatusChange={(data) => {
|
|
setSort(data.columnAccessor as keyof Prisma.FileOrderByWithAggregationInput);
|
|
setOrder(data.direction);
|
|
}}
|
|
onCellClick={({ record }) => setSelectedFile(record)}
|
|
selectedRecords={selectedFiles}
|
|
onSelectedRecordsChange={setSelectedFiles}
|
|
/>
|
|
</Box>
|
|
</>
|
|
);
|
|
}
|