feat: enhance folder settings with subfolder creation and navigation improvements

This commit is contained in:
Linus Rath
2026-03-11 23:19:01 +01:00
parent cc8195209b
commit a88b4e4407
7 changed files with 287 additions and 140 deletions
+10 -3
View File
@@ -11,6 +11,7 @@ import {
} from "date-fns"; } from "date-fns";
import { useCalendarStore } from "@/stores/calendar-store"; import { useCalendarStore } from "@/stores/calendar-store";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
import { useIdentityStore } from "@/stores/identity-store"; import { useIdentityStore } from "@/stores/identity-store";
import { toast } from "@/stores/toast-store"; import { toast } from "@/stores/toast-store";
@@ -44,7 +45,8 @@ export default function CalendarPage() {
const router = useRouter(); const router = useRouter();
const t = useTranslations("calendar"); const t = useTranslations("calendar");
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { client, isAuthenticated } = useAuthStore(); const { client, isAuthenticated, logout } = useAuthStore();
const { quota, isPushConnected } = useEmailStore();
const { const {
calendars, events, selectedDate, viewMode, selectedCalendarIds, calendars, events, selectedDate, viewMode, selectedCalendarIds,
isLoading, isLoadingEvents, supportsCalendar, error, isLoading, isLoadingEvents, supportsCalendar, error,
@@ -637,8 +639,13 @@ export default function CalendarPage() {
<div className="flex h-screen bg-background"> <div className="flex h-screen bg-background">
{/* Left Navigation Rail */} {/* Left Navigation Rail */}
{!isMobile && ( {!isMobile && (
<div className="w-14 border-r border-border bg-secondary flex flex-col items-center py-3 flex-shrink-0"> <div className="w-14 border-r border-border bg-secondary flex flex-col flex-shrink-0">
<NavigationRail collapsed className="py-0" /> <NavigationRail
collapsed
quota={quota}
isPushConnected={isPushConnected}
onLogout={() => { logout(); router.push('/login'); }}
/>
</div> </div>
)} )}
+10 -3
View File
@@ -17,6 +17,7 @@ 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";
import { useEmailStore } from "@/stores/email-store";
import { toast } from "@/stores/toast-store"; import { toast } from "@/stores/toast-store";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { NavigationRail } from "@/components/layout/navigation-rail"; import { NavigationRail } from "@/components/layout/navigation-rail";
@@ -37,7 +38,8 @@ type View =
export default function ContactsPage() { export default function ContactsPage() {
const router = useRouter(); const router = useRouter();
const t = useTranslations("contacts"); const t = useTranslations("contacts");
const { client, isAuthenticated } = useAuthStore(); const { client, isAuthenticated, logout } = useAuthStore();
const { quota, isPushConnected } = useEmailStore();
const { const {
contacts, contacts,
selectedContactId, selectedContactId,
@@ -440,8 +442,13 @@ export default function ContactsPage() {
return ( return (
<div className="flex h-screen bg-background"> <div className="flex h-screen bg-background">
{!isMobile && ( {!isMobile && (
<div className="w-14 border-r border-border bg-secondary flex flex-col items-center flex-shrink-0"> <div className="w-14 border-r border-border bg-secondary flex flex-col flex-shrink-0">
<NavigationRail collapsed /> <NavigationRail
collapsed
quota={quota}
isPushConnected={isPushConnected}
onLogout={() => { logout(); router.push('/login'); }}
/>
</div> </div>
)} )}
+1 -1
View File
@@ -49,7 +49,7 @@ export default function Home() {
const [composerDraftText, setComposerDraftText] = useState(""); const [composerDraftText, setComposerDraftText] = useState("");
const [pendingDraft, setPendingDraft] = useState<ComposerDraftData | null>(null); const [pendingDraft, setPendingDraft] = useState<ComposerDraftData | null>(null);
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
const [initialCheckDone, setInitialCheckDone] = useState(false); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
const [showShortcutsModal, setShowShortcutsModal] = useState(false); const [showShortcutsModal, setShowShortcutsModal] = useState(false);
const [showAdvancedFields, setShowAdvancedFields] = useState(false); const [showAdvancedFields, setShowAdvancedFields] = useState(false);
// Mobile conversation view state // Mobile conversation view state
+14 -1
View File
@@ -17,7 +17,9 @@ import { AdvancedSettings } from '@/components/settings/advanced-settings';
import { FolderSettings } from '@/components/settings/folder-settings'; import { FolderSettings } from '@/components/settings/folder-settings';
import { KeywordSettings } from '@/components/settings/keyword-settings'; import { KeywordSettings } from '@/components/settings/keyword-settings';
import { useAuthStore } from '@/stores/auth-store'; import { useAuthStore } from '@/stores/auth-store';
import { useEmailStore } from '@/stores/email-store';
import { useIsDesktop } from '@/hooks/use-media-query'; import { useIsDesktop } from '@/hooks/use-media-query';
import { NavigationRail } from '@/components/layout/navigation-rail';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
type Tab = 'appearance' | 'email' | 'account' | 'identities' | 'vacation' | 'calendar' | 'filters' | 'templates' | 'folders' | 'keywords' | 'advanced'; type Tab = 'appearance' | 'email' | 'account' | 'identities' | 'vacation' | 'calendar' | 'filters' | 'templates' | 'folders' | 'keywords' | 'advanced';
@@ -25,7 +27,8 @@ type Tab = 'appearance' | 'email' | 'account' | 'identities' | 'vacation' | 'cal
export default function SettingsPage() { export default function SettingsPage() {
const router = useRouter(); const router = useRouter();
const t = useTranslations('settings'); const t = useTranslations('settings');
const { client, isAuthenticated } = useAuthStore(); const { client, isAuthenticated, logout } = useAuthStore();
const { quota, isPushConnected } = useEmailStore();
const [activeTab, setActiveTab] = useState<Tab>('appearance'); const [activeTab, setActiveTab] = useState<Tab>('appearance');
const [mobileShowContent, setMobileShowContent] = useState(false); const [mobileShowContent, setMobileShowContent] = useState(false);
const isDesktop = useIsDesktop(); const isDesktop = useIsDesktop();
@@ -153,6 +156,16 @@ export default function SettingsPage() {
// Desktop layout // Desktop layout
return ( return (
<div className="flex h-screen bg-background"> <div className="flex h-screen bg-background">
{/* Navigation Rail */}
<div className="w-14 border-r border-border bg-secondary flex flex-col flex-shrink-0">
<NavigationRail
collapsed
quota={quota}
isPushConnected={isPushConnected}
onLogout={() => { logout(); router.push('/login'); }}
/>
</div>
{/* Settings Sidebar */} {/* Settings Sidebar */}
<div className="w-64 border-r border-border bg-secondary flex flex-col"> <div className="w-64 border-r border-border bg-secondary flex flex-col">
{/* Header */} {/* Header */}
+62 -26
View File
@@ -29,7 +29,7 @@ import {
ShieldAlert, ShieldAlert,
ShieldCheck, ShieldCheck,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils";
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
interface Position { interface Position {
@@ -143,14 +143,31 @@ export function EmailContextMenu({
color: KEYWORD_PALETTE[kw.color]?.dot || "bg-gray-500", color: KEYWORD_PALETTE[kw.color]?.dot || "bg-gray-500",
})); }));
// Filter mailboxes for move-to submenu (exclude current, drafts, virtual nodes) // Build mailbox tree for move-to submenu with proper hierarchy
const moveTargets = mailboxes.filter( const moveTargetIds = new Set(
(m) => mailboxes
m.id !== selectedMailbox && .filter(
m.role !== "drafts" && (m) =>
!m.id.startsWith("shared-") && m.id !== selectedMailbox &&
m.myRights?.mayAddItems m.role !== "drafts" &&
!m.id.startsWith("shared-") &&
m.myRights?.mayAddItems
)
.map((m) => m.id)
); );
const mailboxTree = buildMailboxTree(mailboxes);
// Filter tree to only include branches that contain valid move targets
const filterTree = (nodes: MailboxNode[]): MailboxNode[] => {
return nodes.reduce<MailboxNode[]>((acc, node) => {
const filteredChildren = filterTree(node.children);
if (moveTargetIds.has(node.id) || filteredChildren.length > 0) {
acc.push({ ...node, children: filteredChildren });
}
return acc;
}, []);
};
const moveTree = filterTree(mailboxTree);
const handleAction = (action: () => void) => { const handleAction = (action: () => void) => {
action(); action();
@@ -222,25 +239,44 @@ export function EmailContextMenu({
<ContextMenuSeparator /> <ContextMenuSeparator />
{/* Move to submenu */} {/* Move to submenu */}
{moveTargets.length > 0 && ( {moveTree.length > 0 && (
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")}> <ContextMenuSubMenu icon={FolderInput} label={t("move_to")}>
{moveTargets.map((mailbox) => { {(() => {
const Icon = getMailboxIcon(mailbox.role); const renderNodes = (nodes: MailboxNode[]) => {
return ( return nodes.map((node) => {
<ContextMenuItem const Icon = getMailboxIcon(node.role);
key={mailbox.id} const isTarget = moveTargetIds.has(node.id);
icon={Icon} return (
label={mailbox.name} <div key={node.id}>
onClick={() => {isTarget ? (
handleAction(() => <ContextMenuItem
showBatchActions icon={Icon}
? onBatchMoveToMailbox?.(mailbox.id) label={node.name}
: onMoveToMailbox?.(mailbox.id) onClick={() =>
) handleAction(() =>
} showBatchActions
/> ? onBatchMoveToMailbox?.(node.id)
); : onMoveToMailbox?.(node.id)
})} )
}
/>
) : (
<div className="px-3 py-1.5 text-sm flex items-center gap-2 text-muted-foreground">
<Icon className="w-4 h-4 flex-shrink-0" />
<span>{node.name}</span>
</div>
)}
{node.children.length > 0 && (
<div className="pl-4">
{renderNodes(node.children)}
</div>
)}
</div>
);
});
};
return renderNodes(moveTree);
})()}
</ContextMenuSubMenu> </ContextMenuSubMenu>
)} )}
+187 -106
View File
@@ -14,7 +14,8 @@ import {
Bell, Zap, Globe, Lock, Eye, MessageSquare, Mail, Bell, Zap, Globe, Lock, Eye, MessageSquare, Mail,
type LucideIcon, type LucideIcon,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils';
import { ChevronRight, ChevronDown } from 'lucide-react';
const STANDARD_ROLES = ['inbox', 'drafts', 'sent', 'trash', 'junk', 'archive'] as const; const STANDARD_ROLES = ['inbox', 'drafts', 'sent', 'trash', 'junk', 'archive'] as const;
@@ -101,14 +102,17 @@ export function FolderSettings() {
const { folderIcons, setFolderIcon } = useSettingsStore(); const { folderIcons, setFolderIcon } = useSettingsStore();
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [creatingParentId, setCreatingParentId] = useState<string | null>(null);
const [newFolderName, setNewFolderName] = useState(''); const [newFolderName, setNewFolderName] = useState('');
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [editingName, setEditingName] = useState(''); const [editingName, setEditingName] = useState('');
const [deletingId, setDeletingId] = useState<string | null>(null); const [deletingId, setDeletingId] = useState<string | null>(null);
const [iconPickerId, setIconPickerId] = useState<string | null>(null); const [iconPickerId, setIconPickerId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const ownMailboxes = mailboxes.filter(mb => !mb.isShared); const ownMailboxes = mailboxes.filter(mb => !mb.isShared);
const folderTree = buildMailboxTree(ownMailboxes);
const getRoleMailboxId = (role: string): string => { const getRoleMailboxId = (role: string): string => {
const mb = ownMailboxes.find(m => m.role === role); const mb = ownMailboxes.find(m => m.role === role);
@@ -139,13 +143,30 @@ export function FolderSettings() {
return 'Folder'; return 'Folder';
}; };
const toggleExpanded = (id: string) => {
setExpandedFolders(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const startCreateSubfolder = (parentId: string) => {
setCreatingParentId(parentId);
setIsCreating(true);
setNewFolderName('');
setExpandedFolders(prev => new Set(prev).add(parentId));
};
const handleCreate = async () => { const handleCreate = async () => {
if (!client || !newFolderName.trim()) return; if (!client || !newFolderName.trim()) return;
setIsLoading(true); setIsLoading(true);
try { try {
await createMailbox(client, newFolderName.trim()); await createMailbox(client, newFolderName.trim(), creatingParentId ?? undefined);
setNewFolderName(''); setNewFolderName('');
setIsCreating(false); setIsCreating(false);
setCreatingParentId(null);
toast.success(t('folder_created')); toast.success(t('folder_created'));
} catch { } catch {
toast.error(t('error_create')); toast.error(t('error_create'));
@@ -213,12 +234,71 @@ export function FolderSettings() {
setEditingName(''); setEditingName('');
}; };
const renderFolderRow = (mb: typeof ownMailboxes[0]) => { const renderCreateInline = (parentId: string | null, depth: number) => {
if (!isCreating || creatingParentId !== parentId) return null;
const parentName = parentId ? ownMailboxes.find(m => m.id === parentId)?.name : null;
return (
<div
className="flex items-center gap-2 mt-1 p-2.5 bg-muted/30 rounded-md border border-border"
style={{ marginLeft: depth * 16 }}
>
<FolderPlus className="w-4 h-4 text-primary flex-shrink-0" />
<div className="flex-1 flex flex-col gap-1">
{parentName && (
<span className="text-xs text-muted-foreground">
{t('subfolder_of', { name: parentName })}
</span>
)}
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreate();
if (e.key === 'Escape') {
setIsCreating(false);
setNewFolderName('');
setCreatingParentId(null);
}
}}
placeholder={parentId ? t('subfolder_name') : t('new_folder_name')}
className="flex-1 px-2 py-1 text-sm rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
autoFocus
disabled={isLoading}
/>
</div>
<button
onClick={handleCreate}
disabled={isLoading || !newFolderName.trim()}
className="px-3 py-1 text-xs font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
>
{t('create')}
</button>
<button
onClick={() => {
setIsCreating(false);
setNewFolderName('');
setCreatingParentId(null);
}}
className="px-3 py-1 text-xs bg-muted text-foreground rounded-md hover:bg-accent"
>
{t('cancel')}
</button>
</div>
);
};
const renderFolderNode = (node: MailboxNode): React.ReactNode => {
const mb = node;
const hasChildren = node.children.length > 0;
const isExpanded = expandedFolders.has(node.id);
const depth = node.depth;
const Icon = getIconForMailbox(mb); const Icon = getIconForMailbox(mb);
if (editingId === mb.id) { if (editingId === mb.id) {
return ( return (
<div key={mb.id} className="flex items-center gap-2 py-2 px-3"> <div key={mb.id}>
<div className="flex items-center gap-2 py-2 px-3" style={{ paddingLeft: 12 + depth * 16 }}>
<Icon className="w-4 h-4 text-muted-foreground flex-shrink-0" /> <Icon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<input <input
type="text" type="text"
@@ -248,12 +328,14 @@ export function FolderSettings() {
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
</div>
); );
} }
if (deletingId === mb.id) { if (deletingId === mb.id) {
return ( return (
<div key={mb.id} className="flex items-center gap-3 py-2.5 px-3 bg-destructive/5 rounded-md border border-destructive/20"> <div key={mb.id}>
<div className="flex items-center gap-3 py-2.5 px-3 bg-destructive/5 rounded-md border border-destructive/20" style={{ marginLeft: depth * 16 }}>
<Trash2 className="w-4 h-4 text-destructive flex-shrink-0" /> <Trash2 className="w-4 h-4 text-destructive flex-shrink-0" />
<p className="text-sm text-foreground flex-1"> <p className="text-sm text-foreground flex-1">
{t('confirm_delete', { name: mb.name })} {t('confirm_delete', { name: mb.name })}
@@ -272,74 +354,108 @@ export function FolderSettings() {
{t('cancel')} {t('cancel')}
</button> </button>
</div> </div>
</div>
); );
} }
return ( return (
<div <div key={mb.id}>
key={mb.id} <div
className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-muted/50" className="flex items-center justify-between py-2 px-3 rounded-md hover:bg-muted/50"
> style={{ paddingLeft: 12 + depth * 16 }}
<div className="flex items-center gap-2.5 min-w-0"> >
<div className="relative flex-shrink-0"> <div className="flex items-center gap-2.5 min-w-0">
<button {/* Expand/collapse toggle for folders with children */}
onClick={() => setIconPickerId(iconPickerId === mb.id ? null : mb.id)} {hasChildren ? (
className={cn( <button
"p-1 rounded-md transition-colors", onClick={() => toggleExpanded(mb.id)}
iconPickerId === mb.id className="p-0.5 text-muted-foreground hover:text-foreground rounded transition-colors flex-shrink-0"
? "bg-accent" >
: "hover:bg-accent" {isExpanded
? <ChevronDown className="w-3.5 h-3.5" />
: <ChevronRight className="w-3.5 h-3.5" />
}
</button>
) : (
<span className="w-4.5 flex-shrink-0" />
)}
<div className="relative flex-shrink-0">
<button
onClick={() => setIconPickerId(iconPickerId === mb.id ? null : mb.id)}
className={cn(
"p-1 rounded-md transition-colors",
iconPickerId === mb.id
? "bg-accent"
: "hover:bg-accent"
)}
title={t('change_icon')}
>
<Icon className={cn(
"w-4 h-4",
mb.role ? "text-primary" : "text-muted-foreground"
)} />
</button>
{iconPickerId === mb.id && (
<IconPicker
currentIcon={getIconName(mb)}
onSelect={(iconName) => {
setFolderIcon(mb.id, iconName);
setIconPickerId(null);
}}
onClose={() => setIconPickerId(null)}
/>
)} )}
title={t('change_icon')} </div>
> <span className="text-sm text-foreground truncate">{mb.name}</span>
<Icon className={cn( {mb.role && (
"w-4 h-4", <span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium flex-shrink-0">
mb.role ? "text-primary" : "text-muted-foreground" {t(`role_${mb.role}`)}
)} /> </span>
</button> )}
{iconPickerId === mb.id && ( {mb.unreadEmails > 0 && (
<IconPicker <span className="text-xs tabular-nums px-1.5 py-0.5 rounded-full bg-primary text-primary-foreground font-medium flex-shrink-0">
currentIcon={getIconName(mb)} {mb.unreadEmails}
onSelect={(iconName) => { </span>
setFolderIcon(mb.id, iconName); )}
setIconPickerId(null); </div>
}} <div className="flex items-center gap-0.5">
onClose={() => setIconPickerId(null)} {mb.myRights?.mayCreateChild && (
/> <button
onClick={() => startCreateSubfolder(mb.id)}
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
title={t('create_subfolder')}
>
<FolderPlus className="w-3.5 h-3.5" />
</button>
)}
{mb.myRights?.mayRename && (
<button
onClick={() => startEdit(mb)}
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
title={t('rename')}
>
<Pencil className="w-3.5 h-3.5" />
</button>
)}
{mb.myRights?.mayDelete && !mb.role && (
<button
onClick={() => setDeletingId(mb.id)}
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
title={t('delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)} )}
</div> </div>
<span className="text-sm text-foreground truncate">{mb.name}</span>
{mb.role && (
<span className="text-xs px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium flex-shrink-0">
{t(`role_${mb.role}`)}
</span>
)}
{mb.unreadEmails > 0 && (
<span className="text-xs tabular-nums px-1.5 py-0.5 rounded-full bg-primary text-primary-foreground font-medium flex-shrink-0">
{mb.unreadEmails}
</span>
)}
</div>
<div className="flex items-center gap-0.5">
{mb.myRights?.mayRename && (
<button
onClick={() => startEdit(mb)}
className="p-1.5 text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
title={t('rename')}
>
<Pencil className="w-3.5 h-3.5" />
</button>
)}
{mb.myRights?.mayDelete && !mb.role && (
<button
onClick={() => setDeletingId(mb.id)}
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
title={t('delete')}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div> </div>
{/* Inline subfolder creation */}
{renderCreateInline(mb.id, depth + 1)}
{/* Render children if expanded */}
{hasChildren && isExpanded && (
<div>
{node.children.map(child => renderFolderNode(child))}
</div>
)}
</div> </div>
); );
}; };
@@ -349,56 +465,21 @@ export function FolderSettings() {
{/* Folder List — primary section */} {/* Folder List — primary section */}
<SettingsSection title={t('folder_list')} description={t('folder_list_description')}> <SettingsSection title={t('folder_list')} description={t('folder_list_description')}>
<div className="space-y-0.5"> <div className="space-y-0.5">
{ownMailboxes.length === 0 ? ( {folderTree.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center"> <div className="flex flex-col items-center justify-center py-8 text-center">
<Folder className="w-10 h-10 text-muted-foreground/40 mb-3" /> <Folder className="w-10 h-10 text-muted-foreground/40 mb-3" />
<p className="text-sm text-muted-foreground">{t('no_folders')}</p> <p className="text-sm text-muted-foreground">{t('no_folders')}</p>
</div> </div>
) : ( ) : (
ownMailboxes.map(renderFolderRow) folderTree.map(node => renderFolderNode(node))
)} )}
</div> </div>
{/* Create folder */} {/* Create top-level folder */}
{isCreating ? ( {renderCreateInline(null, 0)}
<div className="flex items-center gap-2 mt-3 p-2.5 bg-muted/30 rounded-md border border-border"> {!isCreating && (
<FolderPlus className="w-4 h-4 text-primary flex-shrink-0" />
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreate();
if (e.key === 'Escape') {
setIsCreating(false);
setNewFolderName('');
}
}}
placeholder={t('new_folder_name')}
className="flex-1 px-2 py-1 text-sm rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
autoFocus
disabled={isLoading}
/>
<button
onClick={handleCreate}
disabled={isLoading || !newFolderName.trim()}
className="px-3 py-1 text-xs font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
>
{t('create')}
</button>
<button
onClick={() => {
setIsCreating(false);
setNewFolderName('');
}}
className="px-3 py-1 text-xs bg-muted text-foreground rounded-md hover:bg-accent"
>
{t('cancel')}
</button>
</div>
) : (
<button <button
onClick={() => setIsCreating(true)} onClick={() => { setIsCreating(true); setCreatingParentId(null); setNewFolderName(''); }}
className="flex items-center gap-2 mt-3 px-3 py-2 text-sm text-primary hover:bg-primary/5 rounded-md transition-colors w-full border border-dashed border-primary/30 hover:border-primary/50" className="flex items-center gap-2 mt-3 px-3 py-2 text-sm text-primary hover:bg-primary/5 rounded-md transition-colors w-full border border-dashed border-primary/30 hover:border-primary/50"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
+3
View File
@@ -750,6 +750,9 @@
"role_archive": "Archive", "role_archive": "Archive",
"role_none": "None", "role_none": "None",
"create_folder": "Create Folder", "create_folder": "Create Folder",
"create_subfolder": "Create Subfolder",
"subfolder_of": "Inside {name}",
"subfolder_name": "Subfolder name",
"new_folder_name": "Folder name", "new_folder_name": "Folder name",
"rename": "Rename", "rename": "Rename",
"change_icon": "Change icon", "change_icon": "Change icon",