mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: add attachment search view
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user