mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
Merge pull request #113 from ktdd/search-improvements-v1
Search improvements
This commit is contained in:
@@ -57,7 +57,7 @@ use tantivy::{
|
||||
AggregationCollector, Key,
|
||||
},
|
||||
collector::{Count, FacetCollector, TopDocs},
|
||||
query::{AllQuery, BooleanQuery, EmptyQuery, Occur, Query, QueryParser, RangeQuery, TermQuery},
|
||||
query::{AllQuery, BooleanQuery, EmptyQuery, Occur, Query, QueryParser, RangeQuery, RegexQuery, TermQuery},
|
||||
schema::{Facet, IndexRecordOption, Value},
|
||||
store::{Compressor, ZstdCompressor},
|
||||
DocAddress, Index, IndexBuilder, IndexReader, IndexSettings, IndexWriter, Order,
|
||||
@@ -306,11 +306,12 @@ impl EnvelopeIndexManager {
|
||||
(f.f_bcc, &filter.bcc),
|
||||
] {
|
||||
if let Some(ref v) = opt_value {
|
||||
let term = Term::from_field_text(field, v);
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
if let Ok(query) = RegexQuery::from_pattern(v.as_str(), field) {
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(query),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,11 +328,12 @@ impl EnvelopeIndexManager {
|
||||
}
|
||||
|
||||
if let Some(ref name) = filter.attachment_name {
|
||||
let term = Term::from_field_text(f.f_attachments, name);
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
if let Ok(query) = RegexQuery::from_pattern(name.as_str(), f.f_attachments) {
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(query),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let start_bound = if let Some(from) = filter.since {
|
||||
@@ -351,20 +353,28 @@ impl EnvelopeIndexManager {
|
||||
subqueries.push((Occur::Must, Box::new(q)));
|
||||
}
|
||||
|
||||
if let Some(account_id) = filter.account_id {
|
||||
let term = Term::from_field_u64(f.f_account_id, account_id);
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
if let Some(account_ids) = filter.account_ids {
|
||||
let mut should_queries: Vec<(Occur, Box<dyn Query>)> = Vec::new();
|
||||
for id in account_ids {
|
||||
let term = Term::from_field_u64(f.f_account_id, id);
|
||||
should_queries.push((
|
||||
Occur::Should,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
}
|
||||
subqueries.push((Occur::Must, Box::new(BooleanQuery::new(should_queries))));
|
||||
}
|
||||
|
||||
if let Some(mailbox_id) = filter.mailbox_id {
|
||||
let term = Term::from_field_u64(f.f_mailbox_id, mailbox_id);
|
||||
subqueries.push((
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
if let Some(mailbox_ids) = filter.mailbox_ids {
|
||||
let mut should_queries: Vec<(Occur, Box<dyn Query>)> = Vec::new();
|
||||
for id in mailbox_ids {
|
||||
let term = Term::from_field_u64(f.f_mailbox_id, id);
|
||||
should_queries.push((
|
||||
Occur::Should,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic)),
|
||||
));
|
||||
}
|
||||
subqueries.push((Occur::Must, Box::new(BooleanQuery::new(should_queries))));
|
||||
}
|
||||
|
||||
let start_bound = if let Some(from) = filter.min_size {
|
||||
|
||||
@@ -39,8 +39,8 @@ pub struct SearchFilter {
|
||||
pub bcc: Option<String>,
|
||||
pub since: Option<i64>,
|
||||
pub before: Option<i64>,
|
||||
pub account_id: Option<u64>,
|
||||
pub mailbox_id: Option<u64>,
|
||||
pub account_ids: Option<Vec<u64>>,
|
||||
pub mailbox_ids: Option<Vec<u64>>,
|
||||
pub min_size: Option<u64>,
|
||||
pub max_size: Option<u64>,
|
||||
pub message_id: Option<String>,
|
||||
|
||||
@@ -21,6 +21,7 @@ import axiosInstance from "@/api/axiosInstance";
|
||||
|
||||
|
||||
export interface MailboxData {
|
||||
account_id: number;
|
||||
attributes: { attr: string; extension: string | null }[];
|
||||
delimiter: string | null;
|
||||
exists: number;
|
||||
|
||||
@@ -273,7 +273,7 @@ export function VirtualizedSelect({
|
||||
.filter(Boolean);
|
||||
|
||||
if (selectedLabels.length === 0) return placeholder;
|
||||
return `${selectedLabels[0]} +${selectedLabels.length - 1} more`;
|
||||
return selectedLabels.join(", ");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -287,7 +287,7 @@ export function VirtualizedSelect({
|
||||
className={cn('justify-between', className)}
|
||||
disabled={isLoading || disabled}
|
||||
>
|
||||
{getDisplayText()}
|
||||
<span className='truncate'>{getDisplayText()}</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -32,46 +32,42 @@ import { VirtualizedSelect } from "@/components/virtualized-select";
|
||||
import useMinimalAccountList from "@/hooks/use-minimal-account-list";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { list_mailboxes, MailboxData } from "@/api/mailbox/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { useSearchContext } from "./context";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const getSearchFilterSchema = (t: (key: string) => string) => z.object({
|
||||
const searchFilterSchema = z.object({
|
||||
text: z.string().optional().or(z.literal("")),
|
||||
from: z
|
||||
.string()
|
||||
.email({ message: t('validation.invalidEmail') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
to: z
|
||||
.string()
|
||||
.email({ message: t('validation.invalidEmail') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
cc: z
|
||||
.string()
|
||||
.email({ message: t('validation.invalidEmail') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
bcc: z
|
||||
.string()
|
||||
.email({ message: t('validation.invalidEmail') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
has_attachment: z.boolean().optional(),
|
||||
attachment_name: z.string().optional().or(z.literal("")),
|
||||
since: z.date().optional(),
|
||||
before: z.date().optional(),
|
||||
account_id: z.number().optional().or(z.literal("")),
|
||||
mailbox_id: z.number().optional().or(z.literal("")),
|
||||
account_ids: z.array(z.number()),
|
||||
mailbox_ids: z.array(z.number()),
|
||||
size_preset: z.enum(['any', 'tiny', 'small', 'medium', 'large', 'huge']).optional(),
|
||||
message_id: z.string().optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
type SearchFilterForm = z.infer<ReturnType<typeof getSearchFilterSchema>>;
|
||||
type SearchFilterForm = z.infer<typeof searchFilterSchema>;
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -89,6 +85,7 @@ const isEmptyValue = (value: any): boolean => {
|
||||
if (typeof value === 'number' && isNaN(value)) return true;
|
||||
if (value === false) return true;
|
||||
if (value === 0) return true;
|
||||
if (Array.isArray(value) && value.length === 0) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -120,11 +117,10 @@ function withSizePreset(values: Record<string, any>) {
|
||||
export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChange }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<number | undefined>(undefined);
|
||||
const [selectedAccountIds, setSelectedAccountIds] = useState<number[]>([]);
|
||||
const { accountsOptions, isLoading: accountsIsLoading } = useMinimalAccountList();
|
||||
const { selectedTags } = useSearchContext();
|
||||
|
||||
const searchFilterSchema = getSearchFilterSchema(t)
|
||||
const form = useForm<SearchFilterForm>({
|
||||
resolver: zodResolver(searchFilterSchema),
|
||||
defaultValues: {
|
||||
@@ -139,23 +135,29 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
has_attachment: false,
|
||||
since: undefined,
|
||||
before: undefined,
|
||||
account_id: undefined,
|
||||
mailbox_id: undefined,
|
||||
account_ids: [],
|
||||
mailbox_ids: [],
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { data: mailboxes, isLoading: isMailboxesLoading } = useQuery({
|
||||
queryKey: ['search-account-mailboxes', `${selectedAccountId}`],
|
||||
queryFn: () => list_mailboxes(selectedAccountId!, false),
|
||||
enabled: !!selectedAccountId,
|
||||
|
||||
const { mailboxes, isMailboxesLoading } = useQueries({
|
||||
queries: selectedAccountIds.map((id) => ({
|
||||
queryKey: ['search-account-mailboxes', id],
|
||||
queryFn: () => list_mailboxes(id!, false),
|
||||
})),
|
||||
combine: (results) => ({
|
||||
mailboxes: results.flatMap((result) => result.data?.sort((a, b) => a.name.localeCompare(b.name))),
|
||||
isMailboxesLoading: results.some((result) => result.isLoading),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
const mailboxesOptions = mailboxes?.map((mailbox: MailboxData) => ({
|
||||
const mailboxesOptions = mailboxes?.filter((item) => !!item).map((mailbox: MailboxData) => ({
|
||||
value: mailbox.id.toString(),
|
||||
label: mailbox.name,
|
||||
description: accountsOptions.find((item) => Number(item.value) === mailbox.account_id)!.label
|
||||
})) || [];
|
||||
|
||||
|
||||
@@ -163,7 +165,6 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
let cleaned = cleanEmpty(values);
|
||||
const payload = withSizePreset(cleaned);
|
||||
|
||||
|
||||
const finalPayload =
|
||||
selectedTags.length > 0
|
||||
? { ...payload, tags: selectedTags }
|
||||
@@ -189,12 +190,12 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
attachment_name: "",
|
||||
since: undefined,
|
||||
before: undefined,
|
||||
account_id: undefined,
|
||||
mailbox_id: undefined,
|
||||
account_ids: [],
|
||||
mailbox_ids: [],
|
||||
size_preset: 'any',
|
||||
message_id: "",
|
||||
});
|
||||
setSelectedAccountId(undefined);
|
||||
setSelectedAccountIds([]);
|
||||
}
|
||||
|
||||
return (<Sheet
|
||||
@@ -218,21 +219,21 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="account_id"
|
||||
name="account_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem className="min-w-[180px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="text-xs whitespace-nowrap">{t('search.account')}:</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormLabel className="text-xs whitespace-nowrap">{t('search.account')}</FormLabel>
|
||||
<FormControl className="flex-1">
|
||||
<VirtualizedSelect
|
||||
options={accountsOptions}
|
||||
isLoading={accountsIsLoading}
|
||||
onSelectOption={(values) => {
|
||||
const account_id = parseInt(values[0], 10);
|
||||
setSelectedAccountId(account_id);
|
||||
field.onChange(account_id);
|
||||
const ids = values.map((id) => parseInt(id, 10)).sort()
|
||||
setSelectedAccountIds(ids)
|
||||
field.onChange(ids)
|
||||
}}
|
||||
value={field.value?.toString() ?? ""}
|
||||
value={field.value.map(String)}
|
||||
placeholder={t('search.selectAccount')}
|
||||
className="h-10 w-full"
|
||||
noItemsComponent={
|
||||
@@ -247,6 +248,7 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
multiple
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -256,17 +258,19 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mailbox_id"
|
||||
name="mailbox_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem className="min-w-[180px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="text-xs whitespace-nowrap">{t('search.mailbox')}:</FormLabel>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormLabel className="text-xs whitespace-nowrap">{t('search.mailbox')}</FormLabel>
|
||||
<FormControl className="flex-1">
|
||||
<VirtualizedSelect
|
||||
options={mailboxesOptions}
|
||||
isLoading={isMailboxesLoading}
|
||||
onSelectOption={(values) => field.onChange(parseInt(values[0], 10))}
|
||||
value={field.value?.toString() ?? ""}
|
||||
onSelectOption={(values) => {
|
||||
field.onChange(values.map((id) => parseInt(id, 10)))
|
||||
}}
|
||||
value={field.value.map(String)}
|
||||
placeholder={t('search.selectMailbox')}
|
||||
className="h-10 w-full"
|
||||
noItemsComponent={
|
||||
@@ -276,6 +280,7 @@ export function SearchFormDialog({ onSubmit, isLoading, reset, open, onOpenChang
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
multiple
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user