feat: add contacts settings with import/export functionality and update UI components
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
import { useRouter } from "@/i18n/navigation";
|
import { useRouter } from "@/i18n/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ArrowLeft, Upload, Download, Users, BookUser } from "lucide-react";
|
import { ArrowLeft, Users, BookUser } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||||
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
||||||
@@ -13,7 +13,6 @@ import { ContactForm } from "@/components/contacts/contact-form";
|
|||||||
import { ContactGroupList } from "@/components/contacts/contact-group-list";
|
import { ContactGroupList } from "@/components/contacts/contact-group-list";
|
||||||
import { ContactGroupForm } from "@/components/contacts/contact-group-form";
|
import { ContactGroupForm } from "@/components/contacts/contact-group-form";
|
||||||
import { ContactGroupDetail } from "@/components/contacts/contact-group-detail";
|
import { ContactGroupDetail } from "@/components/contacts/contact-group-detail";
|
||||||
import { ContactImportDialog } from "@/components/contacts/contact-import-dialog";
|
|
||||||
import { exportContacts } from "@/components/contacts/contact-export";
|
import { exportContacts } from "@/components/contacts/contact-export";
|
||||||
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
import { useContactStore, getContactDisplayName } from "@/stores/contact-store";
|
||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
@@ -32,7 +31,6 @@ type View =
|
|||||||
| "group-detail"
|
| "group-detail"
|
||||||
| "group-create"
|
| "group-create"
|
||||||
| "group-edit"
|
| "group-edit"
|
||||||
| "import"
|
|
||||||
| "bulk-add-to-group";
|
| "bulk-add-to-group";
|
||||||
|
|
||||||
export default function ContactsPage() {
|
export default function ContactsPage() {
|
||||||
@@ -69,7 +67,6 @@ export default function ContactsPage() {
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
bulkDeleteContacts,
|
bulkDeleteContacts,
|
||||||
bulkAddToGroup,
|
bulkAddToGroup,
|
||||||
importContacts,
|
|
||||||
} = useContactStore();
|
} = useContactStore();
|
||||||
|
|
||||||
const [view, setView] = useState<View>("list");
|
const [view, setView] = useState<View>("list");
|
||||||
@@ -177,8 +174,6 @@ export default function ContactsPage() {
|
|||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (view === "group-create" || view === "group-edit") {
|
if (view === "group-create" || view === "group-edit") {
|
||||||
setView(selectedGroup ? "group-detail" : "list");
|
setView(selectedGroup ? "group-detail" : "list");
|
||||||
} else if (view === "import") {
|
|
||||||
setView("list");
|
|
||||||
} else if (view === "bulk-add-to-group") {
|
} else if (view === "bulk-add-to-group") {
|
||||||
setView("list");
|
setView("list");
|
||||||
} else {
|
} else {
|
||||||
@@ -313,13 +308,6 @@ export default function ContactsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = useCallback(async (importedContacts: ContactCard[]) => {
|
|
||||||
return importContacts(
|
|
||||||
supportsSync && client ? client : null,
|
|
||||||
importedContacts
|
|
||||||
);
|
|
||||||
}, [supportsSync, client, importContacts]);
|
|
||||||
|
|
||||||
if (!isAuthenticated) return null;
|
if (!isAuthenticated) return null;
|
||||||
|
|
||||||
const renderRightPanel = () => {
|
const renderRightPanel = () => {
|
||||||
@@ -376,15 +364,6 @@ export default function ContactsPage() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "import":
|
|
||||||
return (
|
|
||||||
<ContactImportDialog
|
|
||||||
existingContacts={contacts}
|
|
||||||
onImport={handleImport}
|
|
||||||
onClose={handleCancel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "bulk-add-to-group":
|
case "bulk-add-to-group":
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -467,45 +446,6 @@ export default function ContactsPage() {
|
|||||||
"border-r border-border flex flex-col flex-shrink-0",
|
"border-r border-border flex flex-col flex-shrink-0",
|
||||||
isMobile ? "w-full" : "w-80"
|
isMobile ? "w-full" : "w-80"
|
||||||
)}>
|
)}>
|
||||||
<div className={cn("p-4 border-b border-border", isMobile && "px-3 py-3")}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => router.push("/")}
|
|
||||||
className="justify-start"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
{t("back_to_mail")}
|
|
||||||
</Button>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => setView("import")}
|
|
||||||
title={t("import.title")}
|
|
||||||
>
|
|
||||||
<Upload className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => {
|
|
||||||
if (contacts.length > 0) {
|
|
||||||
exportContacts(contacts.filter(c => c.kind !== "group"));
|
|
||||||
toast.success(t("export.success", { count: contacts.filter(c => c.kind !== "group").length }));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={t("export.title")}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex border-b border-border">
|
<div className="flex border-b border-border">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("all")}
|
onClick={() => setActiveTab("all")}
|
||||||
@@ -546,7 +486,6 @@ export default function ContactsPage() {
|
|||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
onSelectContact={handleSelectContact}
|
onSelectContact={handleSelectContact}
|
||||||
onCreateNew={handleCreateNew}
|
onCreateNew={handleCreateNew}
|
||||||
onImport={() => setView("import")}
|
|
||||||
supportsSync={supportsSync}
|
supportsSync={supportsSync}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
selectedContactIds={selectedContactIds}
|
selectedContactIds={selectedContactIds}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Tags,
|
Tags,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Wrench,
|
Wrench,
|
||||||
|
BookUser,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -38,6 +39,7 @@ import { FolderSettings } from '@/components/settings/folder-settings';
|
|||||||
import { KeywordSettings } from '@/components/settings/keyword-settings';
|
import { KeywordSettings } from '@/components/settings/keyword-settings';
|
||||||
import { AccountSecuritySettings } from '@/components/settings/account-security-settings';
|
import { AccountSecuritySettings } from '@/components/settings/account-security-settings';
|
||||||
import { FilesSettingsComponent } from '@/components/settings/files-settings';
|
import { FilesSettingsComponent } from '@/components/settings/files-settings';
|
||||||
|
import { ContactsSettings } from '@/components/settings/contacts-settings';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { useEmailStore } from '@/stores/email-store';
|
import { useEmailStore } from '@/stores/email-store';
|
||||||
import { useIsDesktop } from '@/hooks/use-media-query';
|
import { useIsDesktop } from '@/hooks/use-media-query';
|
||||||
@@ -45,7 +47,7 @@ import { NavigationRail } from '@/components/layout/navigation-rail';
|
|||||||
import { useConfig } from '@/hooks/use-config';
|
import { useConfig } from '@/hooks/use-config';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'vacation' | 'calendar' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'advanced';
|
type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'advanced';
|
||||||
type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system';
|
type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system';
|
||||||
|
|
||||||
interface TabDef {
|
interface TabDef {
|
||||||
@@ -63,6 +65,7 @@ const tabIcons: Record<Tab, LucideIcon> = {
|
|||||||
identities: UserPen,
|
identities: UserPen,
|
||||||
vacation: PalmtreeIcon,
|
vacation: PalmtreeIcon,
|
||||||
calendar: Calendar,
|
calendar: Calendar,
|
||||||
|
contacts: BookUser,
|
||||||
filters: Filter,
|
filters: Filter,
|
||||||
templates: FileText,
|
templates: FileText,
|
||||||
folders: FolderOpen,
|
folders: FolderOpen,
|
||||||
@@ -126,6 +129,7 @@ export default function SettingsPage() {
|
|||||||
{ id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'organization' },
|
{ id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'organization' },
|
||||||
{ id: 'keywords', label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'organization' },
|
{ id: 'keywords', label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'organization' },
|
||||||
...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
|
...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
|
||||||
|
{ id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' },
|
||||||
...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
|
...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
|
||||||
{ id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' },
|
{ id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' },
|
||||||
];
|
];
|
||||||
@@ -158,6 +162,7 @@ export default function SettingsPage() {
|
|||||||
{activeTab === 'identities' && <IdentitySettings />}
|
{activeTab === 'identities' && <IdentitySettings />}
|
||||||
{activeTab === 'vacation' && <VacationSettings />}
|
{activeTab === 'vacation' && <VacationSettings />}
|
||||||
{activeTab === 'calendar' && <><CalendarSettings /><div className="mt-8"><CalendarManagementSettings /></div></>}
|
{activeTab === 'calendar' && <><CalendarSettings /><div className="mt-8"><CalendarManagementSettings /></div></>}
|
||||||
|
{activeTab === 'contacts' && <ContactsSettings />}
|
||||||
{activeTab === 'filters' && <FilterSettings />}
|
{activeTab === 'filters' && <FilterSettings />}
|
||||||
{activeTab === 'templates' && <TemplateSettings />}
|
{activeTab === 'templates' && <TemplateSettings />}
|
||||||
{activeTab === 'folders' && <FolderSettings />}
|
{activeTab === 'folders' && <FolderSettings />}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Search, Plus, BookUser, Info, Check, Trash2, Users, Download, X, UserPlus, Upload } from "lucide-react";
|
import { Search, Plus, BookUser, Info, Check, Trash2, Users, Download, X, UserPlus } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ContactListItem } from "./contact-list-item";
|
import { ContactListItem } from "./contact-list-item";
|
||||||
@@ -17,7 +17,6 @@ interface ContactListProps {
|
|||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
onSelectContact: (id: string) => void;
|
onSelectContact: (id: string) => void;
|
||||||
onCreateNew: () => void;
|
onCreateNew: () => void;
|
||||||
onImport?: () => void;
|
|
||||||
supportsSync: boolean;
|
supportsSync: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
selectedContactIds: Set<string>;
|
selectedContactIds: Set<string>;
|
||||||
@@ -36,7 +35,6 @@ export function ContactList({
|
|||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSelectContact,
|
onSelectContact,
|
||||||
onCreateNew,
|
onCreateNew,
|
||||||
onImport,
|
|
||||||
supportsSync,
|
supportsSync,
|
||||||
className,
|
className,
|
||||||
selectedContactIds,
|
selectedContactIds,
|
||||||
@@ -185,12 +183,6 @@ export function ContactList({
|
|||||||
<UserPlus className="w-4 h-4 mr-1.5" />
|
<UserPlus className="w-4 h-4 mr-1.5" />
|
||||||
{t("create_new")}
|
{t("create_new")}
|
||||||
</Button>
|
</Button>
|
||||||
{onImport && (
|
|
||||||
<Button variant="outline" size="sm" onClick={onImport}>
|
|
||||||
<Upload className="w-4 h-4 mr-1.5" />
|
|
||||||
{t("import_vcard")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Upload, Download } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { SettingsSection, SettingItem } from "./settings-section";
|
||||||
|
import { ContactImportDialog } from "@/components/contacts/contact-import-dialog";
|
||||||
|
import { exportContacts } from "@/components/contacts/contact-export";
|
||||||
|
import { useContactStore } from "@/stores/contact-store";
|
||||||
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
|
import { toast } from "@/stores/toast-store";
|
||||||
|
|
||||||
|
export function ContactsSettings() {
|
||||||
|
const t = useTranslations("contacts");
|
||||||
|
const tSettings = useTranslations("settings.contacts");
|
||||||
|
const { client } = useAuthStore();
|
||||||
|
const {
|
||||||
|
contacts,
|
||||||
|
supportsSync,
|
||||||
|
importContacts,
|
||||||
|
} = useContactStore();
|
||||||
|
const [showImport, setShowImport] = useState(false);
|
||||||
|
|
||||||
|
const individuals = contacts.filter(c => c.kind !== "group");
|
||||||
|
|
||||||
|
const handleImport = useCallback(async (importedContacts: import("@/lib/jmap/types").ContactCard[]) => {
|
||||||
|
return importContacts(
|
||||||
|
supportsSync && client ? client : null,
|
||||||
|
importedContacts
|
||||||
|
);
|
||||||
|
}, [supportsSync, client, importContacts]);
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
if (individuals.length > 0) {
|
||||||
|
exportContacts(individuals);
|
||||||
|
toast.success(t("export.success", { count: individuals.length }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showImport) {
|
||||||
|
return (
|
||||||
|
<div className="border border-border rounded-lg overflow-hidden" style={{ minHeight: 400 }}>
|
||||||
|
<ContactImportDialog
|
||||||
|
existingContacts={contacts}
|
||||||
|
onImport={handleImport}
|
||||||
|
onClose={() => setShowImport(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
title={tSettings("title")}
|
||||||
|
description={tSettings("description")}
|
||||||
|
>
|
||||||
|
<SettingItem
|
||||||
|
label={tSettings("import_label")}
|
||||||
|
description={tSettings("import_description")}
|
||||||
|
>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setShowImport(true)}>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
{t("import.title")}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
label={tSettings("export_label")}
|
||||||
|
description={tSettings("export_description")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={individuals.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
{t("export.title")}
|
||||||
|
</Button>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
-1
@@ -500,7 +500,8 @@
|
|||||||
"folders": "Folders",
|
"folders": "Folders",
|
||||||
"keywords": "Keywords",
|
"keywords": "Keywords",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"files": "Files"
|
"files": "Files",
|
||||||
|
"contacts": "Contacts"
|
||||||
},
|
},
|
||||||
"tab_groups": {
|
"tab_groups": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -917,6 +918,14 @@
|
|||||||
"button": "Import"
|
"button": "Import"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"contacts": {
|
||||||
|
"title": "Contacts",
|
||||||
|
"description": "Import and export your contacts",
|
||||||
|
"import_label": "Import Contacts",
|
||||||
|
"import_description": "Import contacts from a vCard (.vcf) file",
|
||||||
|
"export_label": "Export Contacts",
|
||||||
|
"export_description": "Export all contacts as a vCard (.vcf) file"
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Email Filters",
|
"title": "Email Filters",
|
||||||
"description": "Create rules to automatically sort, label, and manage incoming emails",
|
"description": "Create rules to automatically sort, label, and manage incoming emails",
|
||||||
|
|||||||
Reference in New Issue
Block a user