diff --git a/web/src/components/pagination.tsx b/web/src/components/pagination.tsx
index 04b9298..644830e 100644
--- a/web/src/components/pagination.tsx
+++ b/web/src/components/pagination.tsx
@@ -24,6 +24,7 @@ import {
DoubleArrowRightIcon,
} from '@radix-ui/react-icons'
import { Button } from '@/components/ui/button'
+import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
@@ -33,14 +34,15 @@ import {
} from '@/components/ui/select'
import { useTranslation } from 'react-i18next'
import { showNumbers } from '@/lib/utils'
+import { useEffect, useState } from 'react'
interface PaginationProps {
totalItems: number
- pageIndex: number,
- pageSize: number,
- hasNextPage: () => boolean,
- setPageIndex: (pageIndex: number) => void,
- setPageSize: (pageSize: number) => void,
+ pageIndex: number
+ pageSize: number
+ hasNextPage: () => boolean
+ setPageIndex: (pageIndex: number) => void
+ setPageSize: (pageSize: number) => void
}
export function EnvelopeListPagination({
@@ -52,8 +54,13 @@ export function EnvelopeListPagination({
setPageSize,
}: PaginationProps) {
const { t } = useTranslation()
+ const [pageInput, setPageInput] = useState(pageIndex + 1)
const pageCount = Math.ceil(totalItems / pageSize)
+ useEffect(() => {
+ setPageInput(pageIndex + 1)
+ }, [pageIndex])
+
const handlePageSizeChange = (value: string) => {
const newPageSize = Number(value)
setPageSize(newPageSize)
@@ -69,7 +76,7 @@ export function EnvelopeListPagination({
setPageIndex(newPageIndex)
}
- const currentPage = pageIndex + 1;
+ const currentPage = pageIndex + 1
const pageNumbers = showNumbers(currentPage, pageCount)
return (
@@ -97,7 +104,16 @@ export function EnvelopeListPagination({
- {t("table.page")} {pageIndex + 1} {t("table.of")} {pageCount}
+ {t("table.page")}
+ {
+ if (Number.isNaN(pageInput)) return
+ if (pageInput > 0) setPageIndex(pageInput - 1)
+ else setPageIndex(0)
+ }}
+ onChange={(e) => setPageInput(Number(e.target.value))}
+ className='mx-2 w-20'
+ />
+ {t("table.of")} {pageCount}
)
-}
\ No newline at end of file
+}
diff --git a/web/src/components/ui/scroll-area.tsx b/web/src/components/ui/scroll-area.tsx
index e8ca960..ba9b499 100644
--- a/web/src/components/ui/scroll-area.tsx
+++ b/web/src/components/ui/scroll-area.tsx
@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
interface ScrollAreaProps
extends React.ComponentPropsWithoutRef {
- orientation?: 'horizontal' | 'vertical'
+ orientation?: 'horizontal' | 'vertical' | 'both'
}
const ScrollArea = React.forwardRef<
@@ -24,7 +24,12 @@ const ScrollArea = React.forwardRef<
>
{children}
-
+ {orientation === "both" ? (
+ <>
+
+
+ >
+ ) : }
))
diff --git a/web/src/features/search/columns-dialog.tsx b/web/src/features/search/columns-dialog.tsx
new file mode 100755
index 0000000..3545576
--- /dev/null
+++ b/web/src/features/search/columns-dialog.tsx
@@ -0,0 +1,143 @@
+//
+// Copyright (c) 2025 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 .
+
+import { useTranslation } from 'react-i18next'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { SquarePen } from 'lucide-react'
+import { Button } from '@/components/button'
+import { Checkbox } from '@/components/ui/checkbox'
+import { useState } from 'react'
+import { useSearchContext } from './context'
+import { Label } from '@/components/ui/label'
+
+interface Props {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+const defaultColumns = (t: (key: string) => string) => [
+ {
+ label: t('search.account'),
+ value: "account_email"
+ },
+ {
+ label: t('search.mailbox'),
+ value: "mailbox_name"
+ },
+ {
+ label: t('search.from'),
+ value: "from"
+ },
+ {
+ label: t('search.to'),
+ value: "to"
+ },
+ {
+ label: t('search.subject'),
+ value: "subject"
+ },
+ {
+ label: t('mail.attachments'),
+ value: "attachments"
+ },
+ {
+ label: t('search.size'),
+ value: "size"
+ },
+ {
+ label: t('search.date'),
+ value: "date"
+ },
+]
+
+export function ColumnsDialog({ open, onOpenChange }: Props) {
+ const { t } = useTranslation()
+ const { setColumnVisibility } = useSearchContext()
+ const columns = defaultColumns(t)
+
+ const [selected, setSelected] = useState(() => {
+ const _columns = localStorage.getItem("searchTableColumns")
+ ? JSON.parse(localStorage.getItem("searchTableColumns") as string) as Record
+ : undefined
+
+ if (_columns) return new Map(Object.entries(_columns).map(([key, value]) => [key, value]))
+ return new Map(columns.map((col) => [col.value, true]))
+ })
+
+ const handleSave = () => {
+ const _selected = Object.fromEntries(selected)
+ setColumnVisibility(_selected)
+ localStorage.setItem("searchTableColumns", JSON.stringify(_selected))
+ onOpenChange(false)
+ }
+
+ const toggleSelected = (column: string) => {
+ setSelected(prev => {
+ const value = new Map(prev)
+
+ if (value.get(column)) {
+ value.set(column, false)
+ } else {
+ value.set(column, true)
+ }
+
+ return value
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/web/src/features/search/context/index.tsx b/web/src/features/search/context/index.tsx
index f8e5150..3698646 100644
--- a/web/src/features/search/context/index.tsx
+++ b/web/src/features/search/context/index.tsx
@@ -19,8 +19,9 @@
import React from 'react'
import { EmailEnvelope } from '@/api'
+import { SortingState, VisibilityState } from '@tanstack/react-table'
-export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'search-form' | 'restore'
+export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'search-form' | 'restore' | 'columns'
interface SearchContextType {
open: SearchDialogType | null
@@ -32,6 +33,10 @@ interface SearchContextType {
selected: Map>
setSelected: React.Dispatch>>>
selectedTags: string[]
+ sorting: SortingState
+ setSorting: React.Dispatch>
+ columnVisibility: VisibilityState
+ setColumnVisibility: React.Dispatch>
}
const SearchContext = React.createContext(null)
diff --git a/web/src/features/search/index.tsx b/web/src/features/search/index.tsx
index 876678e..a5cf1e4 100644
--- a/web/src/features/search/index.tsx
+++ b/web/src/features/search/index.tsx
@@ -23,10 +23,9 @@ import { Main } from '@/components/layout/main';
import { useSearchMessages } from '@/hooks/use-search-messages';
import { SearchFormDialog } from './search-form';
import { EnvelopeListPagination } from '@/components/pagination';
-import { MailList } from './mail-list';
import React from 'react';
import { EmailEnvelope } from '@/api';
-import { ArrowDownWideNarrow, ArrowUpWideNarrow, Filter, SearchIcon } from 'lucide-react';
+import { Filter, SearchIcon, SquarePen } from 'lucide-react';
import { MailDisplayDrawer } from './mail-display-dialog';
import { EnvelopeDeleteDialog } from './delete-dialog';
import SearchProvider, { SearchDialogType } from './context';
@@ -39,8 +38,9 @@ import { EditTagsDialog } from './add-tag-dialog';
import { useTranslation } from 'react-i18next';
import Logo from '@/assets/logo.svg'
import { RestoreMessageDialog } from './restore-message-dialog';
-import { Separator } from '@/components/ui/separator';
-import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { ColumnsDialog } from './columns-dialog';
+import { MailListTable } from './mail-list-table';
+import { SortingState, VisibilityState } from '@tanstack/react-table';
export default function Search() {
const { t } = useTranslation()
@@ -49,6 +49,11 @@ export default function Search() {
const [toDelete, setToDelete] = React.useState