feat: revamp option selection for files page

This commit is contained in:
diced
2026-02-26 16:53:31 -08:00
parent c0e1aa9ac6
commit 3c757374e1
10 changed files with 230 additions and 221 deletions
@@ -1,124 +0,0 @@
import { Response } from '@/lib/api/response';
import { IncompleteFile } from '@/lib/db/models/incompleteFile';
import { fetchApi } from '@/lib/fetchApi';
import { ActionIcon, Badge, Button, Card, Group, Modal, Paper, Stack, Text, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IncompleteFileStatus } from '@/prisma/client';
import { IconFileDots, IconTrashFilled } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import useSWR from 'swr';
const badgeMap: Record<IncompleteFileStatus, ReactNode> = {
PENDING: (
<Badge variant='light' color='gray'>
Pending
</Badge>
),
PROCESSING: (
<Badge variant='light' color='yellow'>
Processing
</Badge>
),
COMPLETE: (
<Badge variant='light' color='green'>
Complete
</Badge>
),
FAILED: (
<Badge variant='light' color='red'>
Failed
</Badge>
),
};
export default function PendingFilesButton() {
const [open, setOpen] = useState(false);
const { data: incompleteFiles, mutate } = useSWR<
Extract<IncompleteFile[], Response['/api/user/files/incomplete']>
>('/api/user/files/incomplete');
const handleDelete = async (incompleteFile: IncompleteFile) => {
const { error } = await fetchApi<Response['/api/user/files/incomplete']>(
'/api/user/files/incomplete',
'DELETE',
{
id: [incompleteFile.id],
},
);
if (error) {
showNotification({
title: 'Error',
message: `Failed to delete pending file: ${error.error}`,
color: 'red',
icon: <IconFileDots size='1rem' />,
});
} else {
showNotification({
message: 'Cleared Pending File!',
color: 'green',
icon: <IconTrashFilled size='1rem' />,
});
}
mutate();
};
return (
<>
<Modal opened={open} onClose={() => setOpen(false)} title='Pending Files'>
<Stack gap='xs'>
{incompleteFiles?.map((incompleteFile) => (
<Card key={incompleteFile.id} withBorder>
<Group justify='space-between'>
<Text fw='bolder'>{incompleteFile.metadata.file.filename}</Text>
{badgeMap[incompleteFile.status]}
</Group>
<Group justify='space-between'>
<Text size='xs' c='dimmed' fw='bold'>
{incompleteFile.metadata.file.type}
</Text>
<Text size='xs' c='dimmed'>
{incompleteFile.chunksComplete} / {incompleteFile.chunksTotal} processed
</Text>
</Group>
<Text size='xs' c='dimmed'>
{incompleteFile.id}
</Text>
<Group justify='space-between'>
<Button
fullWidth
size='compact-sm'
mt='xs'
color='red'
variant='light'
onClick={() => handleDelete(incompleteFile)}
leftSection={<IconTrashFilled size='1rem' />}
>
Clear
</Button>
</Group>
</Card>
))}
{incompleteFiles?.length === 0 && (
<Paper withBorder px='sm' py='xs'>
No pending files
</Paper>
)}
</Stack>
</Modal>
<Tooltip label='View pending files'>
<ActionIcon variant='outline' onClick={() => setOpen(true)}>
<IconFileDots size='1rem' />
</ActionIcon>
</Tooltip>
</>
);
}
@@ -0,0 +1,122 @@
import { Response } from '@/lib/api/response';
import { IncompleteFile } from '@/lib/db/models/incompleteFile';
import { fetchApi } from '@/lib/fetchApi';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { IncompleteFileStatus } from '@/prisma/client';
import { Badge, Button, Card, Group, Modal, Paper, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconFileDots, IconTrashFilled } from '@tabler/icons-react';
import { ReactNode } from 'react';
import useSWR from 'swr';
import { DashboardFilesModals } from '.';
const badgeMap: Record<IncompleteFileStatus, ReactNode> = {
PENDING: (
<Badge variant='light' color='gray'>
Pending
</Badge>
),
PROCESSING: (
<Badge variant='light' color='yellow'>
Processing
</Badge>
),
COMPLETE: (
<Badge variant='light' color='green'>
Complete
</Badge>
),
FAILED: (
<Badge variant='light' color='red'>
Failed
</Badge>
),
};
export default function PendingFilesModal({
modals,
setModals,
}: {
modals: DashboardFilesModals;
setModals: UpdateFn<DashboardFilesModals>;
}) {
const { data: incompleteFiles, mutate } = useSWR<
Extract<IncompleteFile[], Response['/api/user/files/incomplete']>
>('/api/user/files/incomplete');
const handleDelete = async (incompleteFile: IncompleteFile) => {
const { error } = await fetchApi<Response['/api/user/files/incomplete']>(
'/api/user/files/incomplete',
'DELETE',
{
id: [incompleteFile.id],
},
);
if (error) {
showNotification({
title: 'Error',
message: `Failed to delete pending file: ${error.error}`,
color: 'red',
icon: <IconFileDots size='1rem' />,
});
} else {
showNotification({
message: 'Cleared Pending File!',
color: 'green',
icon: <IconTrashFilled size='1rem' />,
});
}
mutate();
};
return (
<Modal opened={modals.pending} onClose={() => setModals('pending', false)}>
<Stack gap='xs'>
{incompleteFiles?.map((incompleteFile) => (
<Card key={incompleteFile.id} withBorder>
<Group justify='space-between'>
<Text fw='bolder'>{incompleteFile.metadata.file.filename}</Text>
{badgeMap[incompleteFile.status]}
</Group>
<Group justify='space-between'>
<Text size='xs' c='dimmed' fw='bold'>
{incompleteFile.metadata.file.type}
</Text>
<Text size='xs' c='dimmed'>
{incompleteFile.chunksComplete} / {incompleteFile.chunksTotal} processed
</Text>
</Group>
<Text size='xs' c='dimmed'>
{incompleteFile.id}
</Text>
<Group justify='space-between'>
<Button
fullWidth
size='compact-sm'
mt='xs'
color='red'
variant='light'
onClick={() => handleDelete(incompleteFile)}
leftSection={<IconTrashFilled size='1rem' />}
>
Clear
</Button>
</Group>
</Card>
))}
{incompleteFiles?.length === 0 && (
<Paper withBorder px='sm' py='xs'>
No pending files
</Paper>
)}
</Stack>
</Modal>
);
}
@@ -53,7 +53,7 @@ function SortableTableField({ item }: { item: FieldSettings }) {
);
}
export default function TableEditModal({ opened, onCLose }: { opened: boolean; onCLose: () => void }) {
export default function TableEditModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
const [fields, setIndex, reset] = useFileTableSettingsStore(
useShallow((state) => [state.fields, state.setIndex, state.reset]),
);
@@ -73,7 +73,7 @@ export default function TableEditModal({ opened, onCLose }: { opened: boolean; o
};
return (
<Modal opened={opened} onClose={onCLose} title='Table Options' centered>
<Modal opened={opened} onClose={onClose} title='Table Options' centered>
<Text mb='md' size='sm' c='dimmed'>
Select and drag fields below to make them appear/disappear/reorder in the file table view.
</Text>
+67 -41
View File
@@ -1,23 +1,44 @@
import GridTableSwitcher from '@/components/GridTableSwitcher';
import useObjectState from '@/lib/hooks/useObjectState';
import { useViewStore } from '@/lib/store/view';
import { ActionIcon, Group, Title, Tooltip } from '@mantine/core';
import FavoriteFiles from './views/FavoriteFiles';
import FileTable from './views/FileTable';
import Files from './views/Files';
import TagsButton from './tags/TagsButton';
import PendingFilesButton from './PendingFilesButton';
import { IconFileUpload, IconGridPatternFilled, IconTableOptions } from '@tabler/icons-react';
import { ActionIcon, Group, Menu, Title, Tooltip } from '@mantine/core';
import {
IconDots,
IconFileDots,
IconFileUpload,
IconGridPatternFilled,
IconTableOptions,
IconTags,
} from '@tabler/icons-react';
import { Link } from 'react-router-dom';
import { useState } from 'react';
import PendingFilesModal from './PendingFilesModal';
import TagsModal from './tags/TagsModal';
import FavoriteFiles from './views/FavoriteFiles';
import Files from './views/FilesGridView';
import FileTable from './views/FilesTableView';
export type DashboardFilesModals = {
table: boolean;
idSearch: boolean;
tags: boolean;
pending: boolean;
};
export default function DashboardFiles() {
const view = useViewStore((state) => state.files);
const [tableEditOpen, setTableEditOpen] = useState(false);
const [idSearchOpen, setIdSearchOpen] = useState(false);
const [modals, setModals] = useObjectState<DashboardFilesModals>({
table: false,
idSearch: false,
tags: false,
pending: false,
});
return (
<>
<TagsModal modals={modals} setModals={setModals} />
<PendingFilesModal modals={modals} setModals={setModals} />
<Group>
<Title>Files</Title>
@@ -29,29 +50,43 @@ export default function DashboardFiles() {
</Link>
</Tooltip>
<TagsButton />
<PendingFilesButton />
{view === 'table' && (
<>
<Tooltip label='Table Options'>
<ActionIcon variant='outline' onClick={() => setTableEditOpen((open) => !open)}>
<IconTableOptions size='1rem' />
<Menu>
<Menu.Target>
<Tooltip label='More actions'>
<ActionIcon variant='outline'>
<IconDots size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
</>
)}
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconTags size='1rem' />} onClick={() => setModals('tags', !modals.tags)}>
Manage Tags
</Menu.Item>
<Menu.Item
leftSection={<IconFileDots size='1rem' />}
onClick={() => setModals('pending', !modals.pending)}
>
View Pending Files
</Menu.Item>
{view === 'table' && (
<>
<Menu.Label>Table Options</Menu.Label>
<Menu.Item
leftSection={<IconGridPatternFilled size='1rem' />}
onClick={() => setModals('idSearch', !modals.idSearch)}
>
Search by ID
</Menu.Item>
<Menu.Item
leftSection={<IconTableOptions size='1rem' />}
onClick={() => setModals('table', !modals.table)}
>
Table Options
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
<GridTableSwitcher type='files' />
</Group>
@@ -63,16 +98,7 @@ export default function DashboardFiles() {
<Files />
</>
) : (
<FileTable
idSearch={{
open: idSearchOpen,
setOpen: setIdSearchOpen,
}}
tableEdit={{
open: tableEditOpen,
setOpen: setTableEditOpen,
}}
/>
<FileTable modals={modals} setModals={setModals} />
)}
</>
);
@@ -2,17 +2,24 @@ import { mutateFiles } from '@/components/file/actions';
import { Response } from '@/lib/api/response';
import { Tag } from '@/lib/db/models/tag';
import { fetchApi } from '@/lib/fetchApi';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { ActionIcon, Group, Modal, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconPencil, IconPlus, IconTagOff, IconTags, IconTrashFilled } from '@tabler/icons-react';
import { IconPencil, IconPlus, IconTagOff, IconTrashFilled } from '@tabler/icons-react';
import { useState } from 'react';
import useSWR from 'swr';
import { DashboardFilesModals } from '..';
import CreateTagModal from './CreateTagModal';
import EditTagModal from './EditTagModal';
import TagPill from './TagPill';
export default function TagsButton() {
const [open, setOpen] = useState(false);
export default function TagsModals({
modals,
setModals,
}: {
modals: DashboardFilesModals;
setModals: UpdateFn<DashboardFilesModals>;
}) {
const [createModalOpen, setCreateModalOpen] = useState(false);
const [selectedTag, setSelectedTag] = useState<Tag | null>(null);
@@ -47,8 +54,8 @@ export default function TagsButton() {
<EditTagModal open={!!selectedTag} onClose={() => setSelectedTag(null)} tag={selectedTag} />
<Modal
opened={open}
onClose={() => setOpen(false)}
opened={modals.tags}
onClose={() => setModals('tags', false)}
title={
<Group>
<Title>Tags</Title>
@@ -94,12 +101,6 @@ export default function TagsButton() {
)}
</Stack>
</Modal>
<Tooltip label='View tags'>
<ActionIcon variant='outline' onClick={() => setOpen(true)}>
<IconTags size='1rem' />
</ActionIcon>
</Tooltip>
</>
);
}
@@ -44,6 +44,8 @@ import { lazy, useEffect, useMemo, useReducer, useState } from 'react';
import { Link } from 'react-router-dom';
import useSWR from 'swr';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { DashboardFilesModals } from '..';
import TableEditModal, { NAMES } from '../TableEditModal';
import { bulkDelete, bulkFavorite } from '../bulk';
import TagPill from '../tags/TagPill';
@@ -179,19 +181,13 @@ function TagsFilter({
export default function FileTable({
id,
folderId,
tableEdit,
idSearch,
modals,
setModals,
}: {
id?: string;
folderId?: string;
tableEdit?: {
open: boolean;
setOpen: (open: boolean) => void;
};
idSearch?: {
open: boolean;
setOpen: (open: boolean) => void;
};
modals?: Partial<DashboardFilesModals>;
setModals?: UpdateFn<DashboardFilesModals>;
}) {
const clipboard = useClipboard();
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
@@ -388,7 +384,9 @@ export default function FileTable({
user={id}
/>
{tableEdit && <TableEditModal opened={tableEdit.open} onCLose={() => tableEdit.setOpen(false)} />}
{modals && setModals && modals.table && (
<TableEditModal opened={modals.table} onClose={() => setModals('table', false)} />
)}
<Box>
<Collapse in={selectedFiles.length > 0}>
@@ -481,8 +479,8 @@ export default function FileTable({
</Paper>
</Collapse>
{idSearch && (
<Collapse in={idSearch.open}>
{modals && setModals && modals.idSearch && (
<Collapse in={modals.idSearch}>
<Paper withBorder p='sm' mt='sm'>
<TextInput
placeholder='Search by ID'
+4 -4
View File
@@ -29,8 +29,8 @@ import { IconFolderPlus, IconHome, IconPlus, IconShare } from '@tabler/icons-rea
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import Files from '../files/views/Files';
import FileTable from '../files/views/FileTable';
import FilesGridView from '../files/views/FilesGridView';
import FilesTableView from '../files/views/FilesTableView';
import { mutateFolder } from './actions';
import FolderGridView from './views/FolderGridView';
import FolderTableView from './views/FolderTableView';
@@ -239,10 +239,10 @@ export default function DashboardFolders() {
<Collapse in={filesOpen}>
{view === 'grid' ? (
<Paper withBorder p='sm'>
<Files folderId={currentFolderId} />
<FilesGridView folderId={currentFolderId} />
</Paper>
) : (
<FileTable folderId={currentFolderId} />
<FilesTableView folderId={currentFolderId} />
)}
</Collapse>
</Box>
@@ -16,7 +16,6 @@ import {
IconShare,
IconShareOff,
IconTrashFilled,
IconZip,
} from '@tabler/icons-react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useMemo, useState } from 'react';
+12 -25
View File
@@ -1,20 +1,22 @@
import { type loader } from '@/client/pages/dashboard/admin/users/[id]/files';
import GridTableSwitcher from '@/components/GridTableSwitcher';
import useObjectState from '@/lib/hooks/useObjectState';
import { useViewStore } from '@/lib/store/view';
import { ActionIcon, Group, Title, Tooltip } from '@mantine/core';
import { IconArrowBackUp, IconGridPatternFilled, IconTableOptions } from '@tabler/icons-react';
import { Link, useLoaderData } from 'react-router-dom';
import FileTable from '../files/views/FileTable';
import Files from '../files/views/Files';
import { useState } from 'react';
import { DashboardFilesModals } from '../files';
import FilesTableView from '../files/views/FilesTableView';
import FilesGridView from '../files/views/FilesGridView';
export default function ViewUserFiles() {
const data = useLoaderData<typeof loader>();
const view = useViewStore((state) => state.files);
const [tableEditOpen, setTableEditOpen] = useState(false);
const [idSearchOpen, setIdSearchOpen] = useState(false);
const [modals, setModals] = useObjectState<Partial<DashboardFilesModals>>({
table: false,
idSearch: false,
});
if (!data) return;
@@ -32,18 +34,13 @@ export default function ViewUserFiles() {
</Tooltip>
<Tooltip label='Table Options'>
<ActionIcon variant='outline' onClick={() => setTableEditOpen((open) => !open)}>
<ActionIcon variant='outline' onClick={() => setModals('table', !modals.table)}>
<IconTableOptions size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Search by ID'>
<ActionIcon
variant='outline'
onClick={() => {
setIdSearchOpen((open) => !open);
}}
>
<ActionIcon variant='outline' onClick={() => setModals('idSearch', !modals.idSearch)}>
<IconGridPatternFilled size='1rem' />
</ActionIcon>
</Tooltip>
@@ -52,19 +49,9 @@ export default function ViewUserFiles() {
</Group>
{view === 'grid' ? (
<Files id={user.id} />
<FilesGridView id={user.id} />
) : (
<FileTable
id={user.id}
tableEdit={{
open: tableEditOpen,
setOpen: setTableEditOpen,
}}
idSearch={{
open: idSearchOpen,
setOpen: setIdSearchOpen,
}}
/>
<FilesTableView id={user.id} modals={modals} setModals={setModals} />
)}
</>
);