feat(ui): sync search filters with URL and add dashboard navigation

This commit is contained in:
rustmailer
2026-03-18 20:16:01 +08:00
parent d690f57290
commit 2228e98410
7 changed files with 307 additions and 128 deletions
+1
View File
@@ -30,6 +30,7 @@ export interface EmailEnvelope {
id: string; id: string;
message_id: string; message_id: string;
account_id: number; account_id: number;
mailbox_id: number;
account_email?: string; account_email?: string;
mailbox_name?: string; mailbox_name?: string;
uid: number; uid: number;
+68 -19
View File
@@ -22,7 +22,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts'; import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts';
import { Mail, HardDrive, Database, Users, Inbox, Info } from 'lucide-react'; import { Mail, HardDrive, Database, Users, Inbox, Info, Search } from 'lucide-react';
import { formatBytes, formatNumber } from '@/lib/utils'; import { formatBytes, formatNumber } from '@/lib/utils';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api'; import { get_dashboard_stats, INITIAL_DASHBOARD_STATS, TimeBucket } from '@/api/system/api';
@@ -30,6 +30,8 @@ import { Main } from '@/components/layout/main';
import { FixedHeader } from '@/components/layout/fixed-header'; import { FixedHeader } from '@/components/layout/fixed-header';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getToken } from '@/stores/authStore'; import { getToken } from '@/stores/authStore';
import { useNavigate } from '@tanstack/react-router';
import useMinimalAccountList from '@/hooks/use-minimal-account-list';
interface DailyActivity { interface DailyActivity {
date: string; date: string;
@@ -111,6 +113,8 @@ export default function MailArchiveDashboard() {
placeholderData: INITIAL_DASHBOARD_STATS, placeholderData: INITIAL_DASHBOARD_STATS,
}); });
const navigate = useNavigate();
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const currentLocale = i18n.resolvedLanguage || i18n.language || navigator.language; const currentLocale = i18n.resolvedLanguage || i18n.language || navigator.language;
@@ -123,6 +127,28 @@ export default function MailArchiveDashboard() {
const hasTopEmails = stats?.top_largest_emails && stats.top_largest_emails.length > 0; const hasTopEmails = stats?.top_largest_emails && stats.top_largest_emails.length > 0;
const hasTopAccounts = stats?.top_accounts && stats.top_accounts.length > 0; const hasTopAccounts = stats?.top_accounts && stats.top_accounts.length > 0;
const { minimalList } = useMinimalAccountList();
const getAccountIdByEmail = (email: string): number | null => {
if (!minimalList) return null;
const account = minimalList.find(a => a.email === email);
return account ? account.id : null;
};
const handleQuickSearch = (filter: Record<string, any>) => {
navigate({
to: '/search',
search: (prev: any) => ({
page: 1,
pageSize: prev.pageSize ?? 50,
sortBy: prev.sortBy ?? "DATE",
sortOrder: prev.sortOrder ?? "desc",
q: JSON.stringify(filter),
}),
});
};
const attachmentData = totalAttachments > 0 const attachmentData = totalAttachments > 0
? [ ? [
{ name: 'With Attachments', value: attachmentRatio, fill: COLORS[1] }, { name: 'With Attachments', value: attachmentRatio, fill: COLORS[1] },
@@ -380,9 +406,14 @@ export default function MailArchiveDashboard() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{stats!.top_senders.map((s) => ( {stats!.top_senders.map((s) => (
<TableRow key={s.key}> <TableRow
<TableCell className="font-medium max-w-[180px] truncate" title={s.key}> key={s.key}
{s.key} className="cursor-pointer hover:bg-accent/50 group"
onClick={() => handleQuickSearch({ from: s.key })}
>
<TableCell className="font-medium max-w-[380px] truncate flex items-center gap-2">
<Search size={12} className="opacity-0 group-hover:opacity-100 text-primary transition-opacity" />
<span title={s.key}>{s.key}</span>
</TableCell> </TableCell>
<TableCell className="text-right">{formatNumber(s.count)}</TableCell> <TableCell className="text-right">{formatNumber(s.count)}</TableCell>
</TableRow> </TableRow>
@@ -410,14 +441,18 @@ export default function MailArchiveDashboard() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{stats!.top_largest_emails.map((m, index) => ( {stats!.top_largest_emails.map((m, index) => (
<TableRow key={index}> <TableRow
<TableCell key={index}
className="max-w-[180px] truncate font-medium" className="cursor-pointer hover:bg-accent/50 group"
title={m.subject} onClick={() => handleQuickSearch({ text: m.subject })}
> >
{m.subject || t('dashboard.noSubject')} <TableCell className="max-w-[350px] truncate font-medium flex items-center gap-2" title={m.subject}>
<Search size={12} className="opacity-0 group-hover:opacity-100 text-primary transition-opacity" />
<span>{m.subject || t('dashboard.noSubject')}</span>
</TableCell>
<TableCell className="text-right font-mono text-orange-600">
{formatBytes(m.size_bytes)}
</TableCell> </TableCell>
<TableCell className="text-right">{formatBytes(m.size_bytes)}</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -442,14 +477,28 @@ export default function MailArchiveDashboard() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{stats!.top_accounts.map((acc) => ( {stats!.top_accounts.map((acc) => {
<TableRow key={acc.key}> const accountId = getAccountIdByEmail(acc.key);
<TableCell className="font-medium max-w-[160px] truncate" title={acc.key}> return (
{acc.key} <TableRow
</TableCell> key={acc.key}
<TableCell className="text-right">{formatNumber(acc.count)}</TableCell> className="group cursor-pointer hover:bg-accent/50 transition-colors"
</TableRow> onClick={() => {
))} if (accountId) {
handleQuickSearch({ account_ids: [accountId] });
} else {
handleQuickSearch({ to: acc.key });
}
}}
>
<TableCell className="font-medium max-w-[300px] truncate flex items-center gap-2">
<Search size={12} className="opacity-0 group-hover:opacity-100 text-primary transition-opacity" />
<span title={acc.key}>{acc.key}</span>
</TableCell>
<TableCell className="text-right">{formatNumber(acc.count)}</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
) : ( ) : (
+131 -56
View File
@@ -19,7 +19,7 @@
import { dateFnsLocaleMap, formatBytes } from "@/lib/utils" import { dateFnsLocaleMap, formatBytes } from "@/lib/utils"
import { format, formatDistanceToNow } from "date-fns" import { format, formatDistanceToNow } from "date-fns"
import { MessageSquareText, Paperclip } from "lucide-react" import { MessageSquareText, Paperclip, Search } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { EmailEnvelope } from "@/api" import { EmailEnvelope } from "@/api"
@@ -32,9 +32,10 @@ import LongText from "@/components/long-text"
import { DataTableColumnHeader } from "./table/data-table-column-header" import { DataTableColumnHeader } from "./table/data-table-column-header"
import { SearchTable } from "./table/table" import { SearchTable } from "./table/table"
import { DataTableRowActions } from "./table/data-table-row-actions" import { DataTableRowActions } from "./table/data-table-row-actions"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { DataTableToolbar } from "./table/toolbar" import { DataTableToolbar } from "./table/toolbar"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { useSearchMessages } from "@/hooks/use-search-messages"
interface MailListProps { interface MailListProps {
items: EmailEnvelope[] items: EmailEnvelope[]
@@ -87,87 +88,161 @@ export function MailListTable({
{ {
accessorKey: "account_email", accessorKey: "account_email",
header: t('search.account'), header: t('search.account'),
cell: ({ row }) => <LongText className='text-xs'>{row.original.account_email}</LongText>, cell: ({ row }) => {
meta: { className: 'text-left text-xs' }, const { setFilter } = useSearchMessages();
minSize: 150, const { account_email, account_id } = row.original;
maxSize: 156,
return (
<div className="group relative flex items-center w-full min-w-0 h-full">
<button
onClick={(e) => {
e.stopPropagation();
setFilter((prev: any) => ({
...prev,
account_ids: [account_id],
mailbox_ids: undefined
}));
}}
className="absolute left-0 z-10 opacity-0 group-hover:opacity-100 p-0.5 bg-background border rounded shadow-sm hover:bg-accent"
>
<Search size={12} className="text-primary" />
</button>
<span className="text-[11px] truncate group-hover:pl-5 transition-all duration-200">
{account_email}
</span>
</div>
);
},
meta: { className: 'w-[150px]' },
minSize: 150, maxSize: 150,
}, },
{ {
accessorKey: "mailbox_name", accessorKey: "mailbox_name",
header: t('search.mailbox'), header: t('search.mailbox'),
cell: ({ row }) => { cell: ({ row }) => {
const mailbox = row.original.mailbox_name const { setFilter } = useSearchMessages();
const tags = row.original.tags ?? [] const { mailbox_name, mailbox_id, account_id, tags } = row.original;
if (!mailbox) return null if (!mailbox_name) return null;
const safeTags = tags ?? [];
const visible = tags.slice(0, 3) const visibleTags = safeTags.slice(0, 2);
const rest = tags.length - visible.length
const fullTags = tags.join(' · ')
return ( return (
<TooltipProvider delayDuration={200}> <div className="group relative flex items-center w-full min-w-0 h-full">
<Tooltip> <button
<TooltipTrigger asChild> onClick={(e) => {
<div className="flex flex-col leading-tight max-w-[130px] cursor-default"> e.stopPropagation();
<span className="text-xs truncate"> setFilter((prev: any) => ({
{mailbox} ...prev,
</span> account_ids: [account_id],
mailbox_ids: [mailbox_id]
{visible.length > 0 && ( }));
<span className="text-[10px] text-primary/80 truncate"> }}
{visible.join(' · ')} className="absolute left-0 z-10 opacity-0 group-hover:opacity-100 p-0.5 bg-background border rounded shadow-sm hover:bg-accent transition-all"
{rest > 0 && ` · +${rest}`} >
</span> <Search size={11} className="text-primary" />
)} </button>
</div> <div className="flex flex-col min-w-0 transition-all duration-200 group-hover:pl-5">
</TooltipTrigger> <span className="text-[11px] truncate font-medium leading-none">
{mailbox_name}
<TooltipContent </span>
side="right" {safeTags.length > 0 && (
align="start" <span className="text-[9px] text-primary/70 truncate leading-none mt-0.5">
className="max-w-xs" {visibleTags.join(' · ')}
> {safeTags.length > 2 && ` · +${safeTags.length - 2}`}
<div className="text-xs font-medium mb-1"> </span>
{mailbox} )}
</div> </div>
</div>
<div className="text-[11px] text-muted-foreground break-words"> );
{fullTags}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}, },
meta: { className: 'text-left text-xs' }, maxSize: 120,
minSize: 116,
maxSize: 116,
}, },
{ {
accessorKey: "from", accessorKey: "from",
header: t('search.from'), header: t('search.from'),
cell: ({ row }) => <LongText className='text-xs'>{row.original.from}</LongText>, cell: ({ row }) => {
const fromEmail = row.original.from;
const { setFilter } = useSearchMessages();
return (
<div className="group relative flex items-center w-full min-w-0">
<button
onClick={(e) => {
e.stopPropagation();
setFilter((prev: Record<string, any>) => ({ ...prev, from: fromEmail }));
}}
className="absolute left-0 z-10 opacity-0 group-hover:opacity-100 p-1 bg-background/90 hover:bg-accent rounded shadow-sm transition-all cursor-pointer"
title={`Filter by ${fromEmail}`}
>
<Search size={12} className="text-primary" />
</button>
<LongText
className='text-xs pl-0 group-hover:pl-6 transition-all duration-200 ease-in-out'
>
{fromEmail}
</LongText>
</div>
);
},
meta: { className: 'text-left text-xs' }, meta: { className: 'text-left text-xs' },
minSize: 150, minSize: 150,
maxSize: 156, maxSize: 300,
}, },
{ {
accessorKey: "to", accessorKey: "to",
header: t('search.to'), header: t('search.to'),
cell: ({ row }) => <LongText className='text-xs'>{row.original.to.join(", ")}</LongText>, cell: ({ row }) => {
const recipients = row.original.to || [];
const { setFilter } = useSearchMessages();
return (
<div className="group relative flex items-center w-full min-w-0">
{recipients.length > 0 && (
<button
onClick={(e) => {
e.stopPropagation();
setFilter((prev: Record<string, any>) => ({ ...prev, to: recipients[0] }));
}}
className="absolute left-0 z-10 opacity-0 group-hover:opacity-100 p-1 bg-background/90 hover:bg-accent rounded shadow-sm transition-all cursor-pointer"
title={`${t('search.filter_by')} ${recipients[0]}`}
>
<Search size={12} className="text-primary" />
</button>
)}
<div className="text-xs transition-all duration-200 ease-in-out group-hover:pl-6 flex flex-wrap gap-x-1 min-w-0 overflow-hidden">
{recipients.map((email, index) => (
<span key={index} className="flex items-center">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setFilter((prev: Record<string, any>) => ({ ...prev, to: email }));
}}
className="hover:text-primary hover:underline transition-colors truncate max-w-[150px]"
title={`${t('search.filter_by')} ${email}`}
>
{email}
</button>
{index < recipients.length - 1 && (
<span className="text-muted-foreground ml-0.5">,</span>
)}
</span>
))}
</div>
</div>
);
},
meta: { className: 'text-left text-xs' }, meta: { className: 'text-left text-xs' },
minSize: 150, minSize: 150,
maxSize: 156, maxSize: 200,
}, },
{ {
accessorKey: "subject", accessorKey: "subject",
header: t('search.subject'), header: t('search.subject'),
cell: ({ row }) => <LongText className='text-xs'>{row.original.subject}</LongText>, cell: ({ row }) => <LongText className='text-xs'>{row.original.subject}</LongText>,
meta: { className: 'text-left text-xs' }, meta: { className: 'text-left text-xs' },
minSize: 450, maxSize: 600,
maxSize: 456,
}, },
{ {
id: "text_preview", id: "text_preview",
+1 -2
View File
@@ -163,7 +163,7 @@ export function MailboxPopover() {
const { data: activeMailboxes = [], isLoading: activeIsLoading } = useQuery({ const { data: activeMailboxes = [], isLoading: activeIsLoading } = useQuery({
queryKey: ['search-mailboxes', activeAccountId], queryKey: ['search-mailboxes', activeAccountId],
queryFn: () => list_mailboxes(activeAccountId!, false), queryFn: () => list_mailboxes(activeAccountId!, false),
enabled: !!activeAccountId, enabled: !!activeAccountId,
}); });
const treeData = React.useMemo(() => { const treeData = React.useMemo(() => {
@@ -184,7 +184,6 @@ export function MailboxPopover() {
}; };
const handleDeleteClick = (id: string) => { const handleDeleteClick = (id: string) => {
console.log("delete=", id);
setDeleteMailboxId(id); setDeleteMailboxId(id);
setSelectedAccountId(activeAccountId); setSelectedAccountId(activeAccountId);
setOpen('delete-mailbox'); setOpen('delete-mailbox');
+59 -16
View File
@@ -20,29 +20,73 @@
import { EmailEnvelope, PaginatedResponse } from '@/api'; import { EmailEnvelope, PaginatedResponse } from '@/api';
import { search_messages } from '@/api/search/api'; import { search_messages } from '@/api/search/api';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getRouteApi } from '@tanstack/react-router';
import React from 'react'; import React from 'react';
import { useState } from 'react';
const routeApi = getRouteApi('/_authenticated/search/')
export function useSearchMessages() { export function useSearchMessages() {
// const queryClient = useQueryClient(); // const queryClient = useQueryClient();
const [filter, _setFilter] = useState<Record<string, any>>({}); const search = routeApi.useSearch()
const [page, setPage] = useState(1); const navigate = routeApi.useNavigate()
const [pageSize, setPageSize] = useState(Number(localStorage.getItem('bichon_search_page_size')) || 30);
const [sortBy, setSortBy] = useState<"DATE" | "SIZE">("DATE");
const [sortOrder, setSortOrder] = useState<"desc" | "asc">("desc");
const setFilter = React.useCallback((val: any) => { const page = search.page;
_setFilter(val); const pageSize = search.pageSize;
setPage(1); const sortBy = search.sortBy;
}, []); const sortOrder = search.sortOrder;
const filter = React.useMemo(() => {
if (!search.q) return {};
try {
return JSON.parse(search.q);
} catch (e) {
console.error("URL 'q' parameter parse error:", e);
return {};
}
}, [search.q]);
const setSearchPageSize = (value: number) => {
localStorage.setItem('bichon_search_page_size', value.toString()); const updateParams = React.useCallback((newParams: Partial<typeof search>) => {
setPageSize(value); navigate({
} search: (prev) => ({
...prev,
...newParams,
}),
replace: false,
});
}, [navigate]);
const setFilter = React.useCallback((val: any | ((prev: any) => any)) => {
navigate({
search: (prev) => {
let currentFilter = {};
try {
currentFilter = prev.q ? JSON.parse(prev.q) : {};
} catch (e) {
currentFilter = {};
}
const nextFilter = typeof val === 'function' ? val(currentFilter) : val;
return {
...prev,
page: 1,
q: Object.keys(nextFilter).length > 0 ? JSON.stringify(nextFilter) : undefined
};
}
});
}, [navigate]);
const setPage = (p: number) => updateParams({ page: p });
const setSearchPageSize = (size: number) => {
localStorage.setItem('bichon_search_page_size', size.toString());
updateParams({ pageSize: size, page: 1 });
};
const setSortBy = (val: "DATE" | "SIZE") => updateParams({ sortBy: val });
const setSortOrder = (val: "desc" | "asc") => updateParams({ sortOrder: val });
const onSubmit = (cleaned: Record<string, any>) => { const onSubmit = (cleaned: Record<string, any>) => {
if ('has_attachment' in cleaned && cleaned.has_attachment === false) { if ('has_attachment' in cleaned && cleaned.has_attachment === false) {
@@ -55,7 +99,6 @@ export function useSearchMessages() {
...(cleaned.before && { before: cleaned.before.getTime() }), ...(cleaned.before && { before: cleaned.before.getTime() }),
}; };
setFilter(payload); setFilter(payload);
setPage(1);
} else { } else {
setFilter({}); setFilter({});
} }
+28 -33
View File
@@ -17,6 +17,7 @@ import { Route as AuthenticatedRouteImport } from './routes/_authenticated/route
import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index' import { Route as AuthenticatedIndexImport } from './routes/_authenticated/index'
import { Route as authSignInImport } from './routes/(auth)/sign-in' import { Route as authSignInImport } from './routes/(auth)/sign-in'
import { Route as auth500Import } from './routes/(auth)/500' import { Route as auth500Import } from './routes/(auth)/500'
import { Route as AuthenticatedSearchIndexImport } from './routes/_authenticated/search/index'
// Create Virtual Routes // Create Virtual Routes
@@ -37,9 +38,6 @@ const AuthenticatedUsersIndexLazyImport = createFileRoute(
const AuthenticatedSettingsIndexLazyImport = createFileRoute( const AuthenticatedSettingsIndexLazyImport = createFileRoute(
'/_authenticated/settings/', '/_authenticated/settings/',
)() )()
const AuthenticatedSearchIndexLazyImport = createFileRoute(
'/_authenticated/search/',
)()
const AuthenticatedOauth2IndexLazyImport = createFileRoute( const AuthenticatedOauth2IndexLazyImport = createFileRoute(
'/_authenticated/oauth2/', '/_authenticated/oauth2/',
)() )()
@@ -175,15 +173,6 @@ const AuthenticatedSettingsIndexLazyRoute =
import('./routes/_authenticated/settings/index.lazy').then((d) => d.Route), import('./routes/_authenticated/settings/index.lazy').then((d) => d.Route),
) )
const AuthenticatedSearchIndexLazyRoute =
AuthenticatedSearchIndexLazyImport.update({
id: '/search/',
path: '/search/',
getParentRoute: () => AuthenticatedRouteRoute,
} as any).lazy(() =>
import('./routes/_authenticated/search/index.lazy').then((d) => d.Route),
)
const AuthenticatedOauth2IndexLazyRoute = const AuthenticatedOauth2IndexLazyRoute =
AuthenticatedOauth2IndexLazyImport.update({ AuthenticatedOauth2IndexLazyImport.update({
id: '/oauth2/', id: '/oauth2/',
@@ -222,6 +211,12 @@ const AuthenticatedAccountsIndexLazyRoute =
import('./routes/_authenticated/accounts/index.lazy').then((d) => d.Route), import('./routes/_authenticated/accounts/index.lazy').then((d) => d.Route),
) )
const AuthenticatedSearchIndexRoute = AuthenticatedSearchIndexImport.update({
id: '/search/',
path: '/search/',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedUsersRolesLazyRoute = const AuthenticatedUsersRolesLazyRoute =
AuthenticatedUsersRolesLazyImport.update({ AuthenticatedUsersRolesLazyImport.update({
id: '/roles', id: '/roles',
@@ -425,6 +420,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedUsersRolesLazyImport preLoaderRoute: typeof AuthenticatedUsersRolesLazyImport
parentRoute: typeof AuthenticatedUsersRouteLazyImport parentRoute: typeof AuthenticatedUsersRouteLazyImport
} }
'/_authenticated/search/': {
id: '/_authenticated/search/'
path: '/search'
fullPath: '/search'
preLoaderRoute: typeof AuthenticatedSearchIndexImport
parentRoute: typeof AuthenticatedRouteImport
}
'/_authenticated/accounts/': { '/_authenticated/accounts/': {
id: '/_authenticated/accounts/' id: '/_authenticated/accounts/'
path: '/accounts' path: '/accounts'
@@ -453,13 +455,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedOauth2IndexLazyImport preLoaderRoute: typeof AuthenticatedOauth2IndexLazyImport
parentRoute: typeof AuthenticatedRouteImport parentRoute: typeof AuthenticatedRouteImport
} }
'/_authenticated/search/': {
id: '/_authenticated/search/'
path: '/search'
fullPath: '/search'
preLoaderRoute: typeof AuthenticatedSearchIndexLazyImport
parentRoute: typeof AuthenticatedRouteImport
}
'/_authenticated/settings/': { '/_authenticated/settings/': {
id: '/_authenticated/settings/' id: '/_authenticated/settings/'
path: '/' path: '/'
@@ -529,11 +524,11 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedSettingsRouteLazyRoute: typeof AuthenticatedSettingsRouteLazyRouteWithChildren AuthenticatedSettingsRouteLazyRoute: typeof AuthenticatedSettingsRouteLazyRouteWithChildren
AuthenticatedUsersRouteLazyRoute: typeof AuthenticatedUsersRouteLazyRouteWithChildren AuthenticatedUsersRouteLazyRoute: typeof AuthenticatedUsersRouteLazyRouteWithChildren
AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
AuthenticatedSearchIndexRoute: typeof AuthenticatedSearchIndexRoute
AuthenticatedAccountsIndexLazyRoute: typeof AuthenticatedAccountsIndexLazyRoute AuthenticatedAccountsIndexLazyRoute: typeof AuthenticatedAccountsIndexLazyRoute
AuthenticatedApiDocsIndexLazyRoute: typeof AuthenticatedApiDocsIndexLazyRoute AuthenticatedApiDocsIndexLazyRoute: typeof AuthenticatedApiDocsIndexLazyRoute
AuthenticatedOauth2ResultIndexLazyRoute: typeof AuthenticatedOauth2ResultIndexLazyRoute AuthenticatedOauth2ResultIndexLazyRoute: typeof AuthenticatedOauth2ResultIndexLazyRoute
AuthenticatedOauth2IndexLazyRoute: typeof AuthenticatedOauth2IndexLazyRoute AuthenticatedOauth2IndexLazyRoute: typeof AuthenticatedOauth2IndexLazyRoute
AuthenticatedSearchIndexLazyRoute: typeof AuthenticatedSearchIndexLazyRoute
} }
const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = { const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
@@ -542,12 +537,12 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedUsersRouteLazyRoute: AuthenticatedUsersRouteLazyRoute:
AuthenticatedUsersRouteLazyRouteWithChildren, AuthenticatedUsersRouteLazyRouteWithChildren,
AuthenticatedIndexRoute: AuthenticatedIndexRoute, AuthenticatedIndexRoute: AuthenticatedIndexRoute,
AuthenticatedSearchIndexRoute: AuthenticatedSearchIndexRoute,
AuthenticatedAccountsIndexLazyRoute: AuthenticatedAccountsIndexLazyRoute, AuthenticatedAccountsIndexLazyRoute: AuthenticatedAccountsIndexLazyRoute,
AuthenticatedApiDocsIndexLazyRoute: AuthenticatedApiDocsIndexLazyRoute, AuthenticatedApiDocsIndexLazyRoute: AuthenticatedApiDocsIndexLazyRoute,
AuthenticatedOauth2ResultIndexLazyRoute: AuthenticatedOauth2ResultIndexLazyRoute:
AuthenticatedOauth2ResultIndexLazyRoute, AuthenticatedOauth2ResultIndexLazyRoute,
AuthenticatedOauth2IndexLazyRoute: AuthenticatedOauth2IndexLazyRoute, AuthenticatedOauth2IndexLazyRoute: AuthenticatedOauth2IndexLazyRoute,
AuthenticatedSearchIndexLazyRoute: AuthenticatedSearchIndexLazyRoute,
} }
const AuthenticatedRouteRouteWithChildren = const AuthenticatedRouteRouteWithChildren =
@@ -571,11 +566,11 @@ export interface FileRoutesByFullPath {
'/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute '/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
'/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute '/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute
'/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/users/roles': typeof AuthenticatedUsersRolesLazyRoute
'/search': typeof AuthenticatedSearchIndexRoute
'/accounts': typeof AuthenticatedAccountsIndexLazyRoute '/accounts': typeof AuthenticatedAccountsIndexLazyRoute
'/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute '/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute
'/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute '/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute
'/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute
'/search': typeof AuthenticatedSearchIndexLazyRoute
'/settings/': typeof AuthenticatedSettingsIndexLazyRoute '/settings/': typeof AuthenticatedSettingsIndexLazyRoute
'/users/': typeof AuthenticatedUsersIndexLazyRoute '/users/': typeof AuthenticatedUsersIndexLazyRoute
} }
@@ -595,11 +590,11 @@ export interface FileRoutesByTo {
'/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute '/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
'/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute '/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute
'/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/users/roles': typeof AuthenticatedUsersRolesLazyRoute
'/search': typeof AuthenticatedSearchIndexRoute
'/accounts': typeof AuthenticatedAccountsIndexLazyRoute '/accounts': typeof AuthenticatedAccountsIndexLazyRoute
'/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute '/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute
'/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute '/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute
'/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute
'/search': typeof AuthenticatedSearchIndexLazyRoute
'/settings': typeof AuthenticatedSettingsIndexLazyRoute '/settings': typeof AuthenticatedSettingsIndexLazyRoute
'/users': typeof AuthenticatedUsersIndexLazyRoute '/users': typeof AuthenticatedUsersIndexLazyRoute
} }
@@ -624,11 +619,11 @@ export interface FileRoutesById {
'/_authenticated/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute '/_authenticated/settings/proxy': typeof AuthenticatedSettingsProxyLazyRoute
'/_authenticated/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute '/_authenticated/users/api-tokens': typeof AuthenticatedUsersApiTokensLazyRoute
'/_authenticated/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/_authenticated/users/roles': typeof AuthenticatedUsersRolesLazyRoute
'/_authenticated/search/': typeof AuthenticatedSearchIndexRoute
'/_authenticated/accounts/': typeof AuthenticatedAccountsIndexLazyRoute '/_authenticated/accounts/': typeof AuthenticatedAccountsIndexLazyRoute
'/_authenticated/api-docs/': typeof AuthenticatedApiDocsIndexLazyRoute '/_authenticated/api-docs/': typeof AuthenticatedApiDocsIndexLazyRoute
'/_authenticated/oauth2-result/': typeof AuthenticatedOauth2ResultIndexLazyRoute '/_authenticated/oauth2-result/': typeof AuthenticatedOauth2ResultIndexLazyRoute
'/_authenticated/oauth2/': typeof AuthenticatedOauth2IndexLazyRoute '/_authenticated/oauth2/': typeof AuthenticatedOauth2IndexLazyRoute
'/_authenticated/search/': typeof AuthenticatedSearchIndexLazyRoute
'/_authenticated/settings/': typeof AuthenticatedSettingsIndexLazyRoute '/_authenticated/settings/': typeof AuthenticatedSettingsIndexLazyRoute
'/_authenticated/users/': typeof AuthenticatedUsersIndexLazyRoute '/_authenticated/users/': typeof AuthenticatedUsersIndexLazyRoute
} }
@@ -653,11 +648,11 @@ export interface FileRouteTypes {
| '/settings/proxy' | '/settings/proxy'
| '/users/api-tokens' | '/users/api-tokens'
| '/users/roles' | '/users/roles'
| '/search'
| '/accounts' | '/accounts'
| '/api-docs' | '/api-docs'
| '/oauth2-result' | '/oauth2-result'
| '/oauth2' | '/oauth2'
| '/search'
| '/settings/' | '/settings/'
| '/users/' | '/users/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
@@ -676,11 +671,11 @@ export interface FileRouteTypes {
| '/settings/proxy' | '/settings/proxy'
| '/users/api-tokens' | '/users/api-tokens'
| '/users/roles' | '/users/roles'
| '/search'
| '/accounts' | '/accounts'
| '/api-docs' | '/api-docs'
| '/oauth2-result' | '/oauth2-result'
| '/oauth2' | '/oauth2'
| '/search'
| '/settings' | '/settings'
| '/users' | '/users'
id: id:
@@ -703,11 +698,11 @@ export interface FileRouteTypes {
| '/_authenticated/settings/proxy' | '/_authenticated/settings/proxy'
| '/_authenticated/users/api-tokens' | '/_authenticated/users/api-tokens'
| '/_authenticated/users/roles' | '/_authenticated/users/roles'
| '/_authenticated/search/'
| '/_authenticated/accounts/' | '/_authenticated/accounts/'
| '/_authenticated/api-docs/' | '/_authenticated/api-docs/'
| '/_authenticated/oauth2-result/' | '/_authenticated/oauth2-result/'
| '/_authenticated/oauth2/' | '/_authenticated/oauth2/'
| '/_authenticated/search/'
| '/_authenticated/settings/' | '/_authenticated/settings/'
| '/_authenticated/users/' | '/_authenticated/users/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
@@ -761,11 +756,11 @@ export const routeTree = rootRoute
"/_authenticated/settings", "/_authenticated/settings",
"/_authenticated/users", "/_authenticated/users",
"/_authenticated/", "/_authenticated/",
"/_authenticated/search/",
"/_authenticated/accounts/", "/_authenticated/accounts/",
"/_authenticated/api-docs/", "/_authenticated/api-docs/",
"/_authenticated/oauth2-result/", "/_authenticated/oauth2-result/",
"/_authenticated/oauth2/", "/_authenticated/oauth2/"
"/_authenticated/search/"
] ]
}, },
"/(auth)/500": { "/(auth)/500": {
@@ -842,6 +837,10 @@ export const routeTree = rootRoute
"filePath": "_authenticated/users/roles.lazy.tsx", "filePath": "_authenticated/users/roles.lazy.tsx",
"parent": "/_authenticated/users" "parent": "/_authenticated/users"
}, },
"/_authenticated/search/": {
"filePath": "_authenticated/search/index.tsx",
"parent": "/_authenticated"
},
"/_authenticated/accounts/": { "/_authenticated/accounts/": {
"filePath": "_authenticated/accounts/index.lazy.tsx", "filePath": "_authenticated/accounts/index.lazy.tsx",
"parent": "/_authenticated" "parent": "/_authenticated"
@@ -858,10 +857,6 @@ export const routeTree = rootRoute
"filePath": "_authenticated/oauth2/index.lazy.tsx", "filePath": "_authenticated/oauth2/index.lazy.tsx",
"parent": "/_authenticated" "parent": "/_authenticated"
}, },
"/_authenticated/search/": {
"filePath": "_authenticated/search/index.lazy.tsx",
"parent": "/_authenticated"
},
"/_authenticated/settings/": { "/_authenticated/settings/": {
"filePath": "_authenticated/settings/index.lazy.tsx", "filePath": "_authenticated/settings/index.lazy.tsx",
"parent": "/_authenticated/settings" "parent": "/_authenticated/settings"
@@ -17,9 +17,26 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createLazyFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import Search from '@/features/search' import Search from '@/features/search'
import { z } from 'zod'
export const Route = createLazyFileRoute('/_authenticated/search/')({ const searchSchema = z.object({
page: z.number().catch(1),
pageSize: z.number().optional(),
sortBy: z.enum(['DATE', 'SIZE']).catch('DATE'),
sortOrder: z.enum(['asc', 'desc']).catch('desc'),
q: z.string().optional(),
})
export const Route = createFileRoute('/_authenticated/search/')({
component: Search, component: Search,
validateSearch: (search) => {
const result = searchSchema.parse(search);
return {
...result,
page: result.page ?? 1,
pageSize: result.pageSize ?? (Number(localStorage.getItem('bichon_search_page_size')) || 30),
}
}
}) })