feat: add attachment search view

This commit is contained in:
rustmailer
2026-04-19 01:01:46 +08:00
parent 19b5168960
commit 7dd722f9b8
96 changed files with 36331 additions and 28970 deletions
+26
View File
@@ -0,0 +1,26 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { get_attachment_senders } from '@/api/attachment/api';
export const userAttachmentSenders = (searchTerm: string = "") => {
const { data: senders = [], isLoading, isError } = useQuery({
queryKey: ['attachment-senders', 'all'],
queryFn: get_attachment_senders,
staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 30,
});
const filtered = useMemo(() => {
if (!searchTerm) return senders;
const lower = searchTerm.toLowerCase();
return senders.filter(email =>
email.toLowerCase().includes(lower)
);
}, [senders, searchTerm]);
return {
senders: filtered,
isLoading,
isError
};
};
@@ -0,0 +1,66 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { get_all_attachment_tags } from '@/api/attachment/api';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
export interface TagCount {
tag: string;
count: number;
}
interface UseAvailableTagsResult {
tags: string[];
tagsCount: TagCount[];
isLoading: boolean;
isError: boolean;
error: unknown;
refetch: () => void;
}
export function useAvailableAttachmentTags(): UseAvailableTagsResult {
const {
data: tagsCount = [],
isLoading,
isError,
error,
refetch,
} = useQuery<TagCount[]>({
queryKey: ['attachment-tags'],
queryFn: get_all_attachment_tags,
staleTime: 60 * 1000,
retry: false,
refetchOnWindowFocus: false,
});
const tags = React.useMemo(() => {
return tagsCount.map(f => f.tag).sort();
}, [tagsCount]);
return {
tags,
tagsCount,
isLoading,
isError,
error,
refetch,
};
}
+152
View File
@@ -0,0 +1,152 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { PaginatedResponse } from '@/api';
import { AttachmentModel, search_attachment } from '@/api/attachment/api';
import { useQuery } from '@tanstack/react-query';
import { getRouteApi } from '@tanstack/react-router';
import React from 'react';
const routeApi = getRouteApi('/_authenticated/attachment/')
export function useSearchAttachments() {
// const queryClient = useQueryClient();
const search = routeApi.useSearch()
const navigate = routeApi.useNavigate()
const page = search.page;
const pageSize = search.pageSize;
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 updateParams = React.useCallback((newParams: Partial<typeof search>) => {
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_attachment_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>) => {
if ('has_attachment' in cleaned && cleaned.has_attachment === false) {
delete cleaned.has_attachment;
}
if (Object.keys(cleaned).length > 0) {
const payload = {
...cleaned,
...(cleaned.since && { since: cleaned.since.getTime() }),
...(cleaned.before && { before: cleaned.before.getTime() }),
};
setFilter(payload);
} else {
setFilter({});
}
};
const reset = () => {
setFilter({});
}
const {
data,
isLoading,
isError,
error,
isFetching,
} = useQuery<PaginatedResponse<AttachmentModel>>({
queryKey: ['search-attachments', filter, page, pageSize, sortBy, sortOrder],
queryFn: () =>
search_attachment({
filter: filter,
page,
page_size: pageSize,
sort_by: sortBy,
desc: sortOrder === "desc"
}),
staleTime: 1000,
retry: false,
});
return {
attachments: data?.items ?? [],
total: data?.total_items ?? 0,
totalPages: data?.total_pages ?? 1,
pageSize: data?.page_size ?? pageSize,
setSearchPageSize,
sortBy,
setSortBy,
sortOrder,
setSortOrder,
isLoading,
isError,
error: error as Error | null,
isFetching,
page,
setPage,
onSubmit,
reset,
filter,
setFilter
};
}