feat: add sidebar apps management feature
- Implemented sidebar apps functionality including adding, editing, and deleting apps. - Created a modal for managing sidebar apps with forms for inputting app details. - Added icon picker component for selecting app icons. - Introduced inline app view for displaying apps within the sidebar. - Updated translations for new sidebar apps feature in Dutch and Portuguese. - Enhanced settings store to manage sidebar apps state. - Added hooks for managing sidebar apps state and modal visibility.
This commit is contained in:
@@ -30,6 +30,9 @@ import { ICalImportModal } from "@/components/calendar/ical-import-modal";
|
||||
import { ICalSubscriptionModal } from "@/components/calendar/ical-subscription-modal";
|
||||
import { RecurrenceScopeDialog, type RecurrenceEditScope } from "@/components/calendar/recurrence-scope-dialog";
|
||||
import { NavigationRail } from "@/components/layout/navigation-rail";
|
||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||
import { ResizeHandle } from "@/components/layout/resize-handle";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types";
|
||||
@@ -48,6 +51,7 @@ export default function CalendarPage() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("calendar");
|
||||
const isMobile = useIsMobile();
|
||||
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
|
||||
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
|
||||
const { quota, isPushConnected } = useEmailStore();
|
||||
@@ -703,12 +707,20 @@ export default function CalendarPage() {
|
||||
quota={quota}
|
||||
isPushConnected={isPushConnected}
|
||||
onLogout={() => { logout(); router.push('/login'); }}
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inlineApp && (
|
||||
<InlineAppView apps={loadedApps} activeAppId={inlineApp!.id} onClose={closeInlineApp} className="flex-1" />
|
||||
)}
|
||||
|
||||
{/* Sidebar - full height */}
|
||||
{!isMobile && (
|
||||
{!isMobile && !inlineApp && (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -748,6 +760,7 @@ export default function CalendarPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{!inlineApp && (
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<CalendarToolbar
|
||||
selectedDate={selectedDate}
|
||||
@@ -802,12 +815,19 @@ export default function CalendarPage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
{isMobile && (
|
||||
<NavigationRail orientation="horizontal" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
{isMobile && (
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailEvent && detailAnchorRect && (
|
||||
<EventDetailPopover
|
||||
@@ -859,6 +879,7 @@ export default function CalendarPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
<RecurrenceScopeDialog
|
||||
isOpen={!!pendingScopeAction}
|
||||
actionType={pendingScopeAction?.type || "edit"}
|
||||
|
||||
@@ -20,6 +20,9 @@ import { useEmailStore } from "@/stores/email-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NavigationRail } from "@/components/layout/navigation-rail";
|
||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||
import { ResizeHandle } from "@/components/layout/resize-handle";
|
||||
import { useIsMobile } from "@/hooks/use-media-query";
|
||||
import type { ContactCard } from "@/lib/jmap/types";
|
||||
@@ -38,6 +41,7 @@ export default function ContactsPage() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("contacts");
|
||||
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
|
||||
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
|
||||
const { quota, isPushConnected } = useEmailStore();
|
||||
const {
|
||||
@@ -476,12 +480,19 @@ export default function ContactsPage() {
|
||||
quota={quota}
|
||||
isPushConnected={isPushConnected}
|
||||
onLogout={() => { logout(); router.push('/login'); }}
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{inlineApp && (
|
||||
<InlineAppView apps={loadedApps} activeAppId={inlineApp!.id} onClose={closeInlineApp} />
|
||||
)}
|
||||
<div className={cn("flex flex-1 min-h-0", inlineApp && "hidden")}>
|
||||
{showListPanel && (
|
||||
<>
|
||||
{/* Panel 1: Categories sidebar */}
|
||||
@@ -582,10 +593,17 @@ export default function ContactsPage() {
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<NavigationRail orientation="horizontal" />
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
<ConfirmDialog {...confirmDialogProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,9 @@ import { useFileStore } from "@/stores/file-store";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NavigationRail } from "@/components/layout/navigation-rail";
|
||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||
import { useIsMobile } from "@/hooks/use-media-query";
|
||||
import { FileBrowser } from "@/components/files/file-browser";
|
||||
import { ImagePreviewModal } from "@/components/files/image-preview-modal";
|
||||
@@ -24,6 +27,7 @@ export default function FilesPage() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("files");
|
||||
const { isAuthenticated, logout, checkAuth, isLoading: authLoading, client } = useAuthStore();
|
||||
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
|
||||
const { quota, isPushConnected } = useEmailStore();
|
||||
const {
|
||||
@@ -354,12 +358,19 @@ export default function FilesPage() {
|
||||
quota={quota}
|
||||
isPushConnected={isPushConnected}
|
||||
onLogout={() => { logout(); router.push('/login'); }}
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{inlineApp && (
|
||||
<InlineAppView apps={loadedApps} activeAppId={inlineApp!.id} onClose={closeInlineApp} />
|
||||
)}
|
||||
<div className={cn("flex flex-1 min-h-0", inlineApp && "hidden")}>
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
{folderLayout !== "sidebar" && (
|
||||
<div className={cn("p-4 border-b border-border", isMobile && "px-3 py-3")}>
|
||||
@@ -433,7 +444,13 @@ export default function FilesPage() {
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<NavigationRail orientation="horizontal" />
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -457,6 +474,7 @@ export default function FilesPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
<ConfirmDialog {...confirmDialogProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
+25
-5
@@ -35,6 +35,9 @@ import { DragDropProvider } from "@/contexts/drag-drop-context";
|
||||
import { isFilterEmpty, activeFilterCount } from "@/lib/jmap/search-utils";
|
||||
import { WelcomeBanner } from "@/components/ui/welcome-banner";
|
||||
import { NavigationRail } from "@/components/layout/navigation-rail";
|
||||
import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal";
|
||||
import { InlineAppView } from "@/components/layout/inline-app-view";
|
||||
import { useSidebarApps } from "@/hooks/use-sidebar-apps";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FilePreviewModal } from "@/components/files/file-preview-modal";
|
||||
import { isFilePreviewable } from "@/lib/file-preview";
|
||||
@@ -53,6 +56,7 @@ export default function Home() {
|
||||
const [composerDraftText, setComposerDraftText] = useState("");
|
||||
const [pendingDraft, setPendingDraft] = useState<ComposerDraftData | null>(null);
|
||||
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
|
||||
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
|
||||
const [showShortcutsModal, setShowShortcutsModal] = useState(false);
|
||||
const [showAdvancedFields, setShowAdvancedFields] = useState(false);
|
||||
@@ -1007,12 +1011,20 @@ export default function Home() {
|
||||
isPushConnected={isPushConnected}
|
||||
onLogout={handleLogout}
|
||||
onShowShortcuts={() => setShowShortcutsModal(true)}
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inlineApp && (
|
||||
<InlineAppView apps={loadedApps} activeAppId={inlineApp!.id} onClose={closeInlineApp} className="flex-1" />
|
||||
)}
|
||||
|
||||
{/* Mobile/Tablet Sidebar Overlay Backdrop */}
|
||||
{(isMobile || isTablet) && sidebarOpen && (
|
||||
{(isMobile || isTablet) && sidebarOpen && !inlineApp && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
@@ -1029,7 +1041,8 @@ export default function Home() {
|
||||
"max-lg:transform max-lg:transition-transform max-lg:duration-300 max-lg:ease-in-out",
|
||||
!sidebarOpen && "max-lg:-translate-x-full",
|
||||
// Desktop: normal flow
|
||||
"lg:relative lg:translate-x-0"
|
||||
"lg:relative lg:translate-x-0",
|
||||
inlineApp && "hidden"
|
||||
)}
|
||||
style={!isMobile && !isTablet ? { width: sidebarCollapsed ? 64 : sidebarWidth } : undefined}
|
||||
>
|
||||
@@ -1055,7 +1068,7 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* Sidebar resize handle (desktop only, hidden when collapsed) */}
|
||||
{!isMobile && !isTablet && !sidebarCollapsed && (
|
||||
{!isMobile && !isTablet && !sidebarCollapsed && !inlineApp && (
|
||||
<ResizeHandle
|
||||
onResizeStart={() => { dragStartWidth.current = sidebarWidth; setIsResizing(true); }}
|
||||
onResize={(delta) => setSidebarWidth(dragStartWidth.current + delta)}
|
||||
@@ -1065,7 +1078,7 @@ export default function Home() {
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col flex-1 min-w-0 h-full">
|
||||
<div className={cn("flex flex-col flex-1 min-w-0 h-full", inlineApp && "hidden")}>
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{/* Email List - full width on mobile, fixed width on tablet/desktop */}
|
||||
<div
|
||||
@@ -1552,7 +1565,13 @@ export default function Home() {
|
||||
|
||||
{/* Bottom Navigation - mobile and tablet */}
|
||||
{(isMobile || isTablet) && activeView !== "viewer" && (
|
||||
<NavigationRail orientation="horizontal" />
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1575,6 +1594,7 @@ export default function Home() {
|
||||
{/* Screen reader live region for dynamic status announcements */}
|
||||
<div className="sr-only" aria-live="polite" aria-atomic="true" id="sr-status" />
|
||||
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
<ConfirmDialog {...confirmDialogProps} />
|
||||
</div>
|
||||
</DragDropProvider>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Wrench,
|
||||
BookUser,
|
||||
KeyRound,
|
||||
PanelLeftClose,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -42,15 +43,19 @@ import { AccountSecuritySettings } from '@/components/settings/account-security-
|
||||
import { FilesSettingsComponent } from '@/components/settings/files-settings';
|
||||
import { ContactsSettings } from '@/components/settings/contacts-settings';
|
||||
import { SmimeSettings } from '@/components/settings/smime-settings';
|
||||
import { SidebarAppsSettings } from '@/components/settings/sidebar-apps-settings';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { useIsDesktop } from '@/hooks/use-media-query';
|
||||
import { NavigationRail } from '@/components/layout/navigation-rail';
|
||||
import { SidebarAppsModal } from '@/components/layout/sidebar-apps-modal';
|
||||
import { InlineAppView } from '@/components/layout/inline-app-view';
|
||||
import { useSidebarApps } from '@/hooks/use-sidebar-apps';
|
||||
import { ResizeHandle } from '@/components/layout/resize-handle';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'encryption' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'advanced';
|
||||
type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'encryption' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'sidebar_apps' | 'advanced';
|
||||
type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system';
|
||||
|
||||
interface TabDef {
|
||||
@@ -75,6 +80,7 @@ const tabIcons: Record<Tab, LucideIcon> = {
|
||||
folders: FolderOpen,
|
||||
keywords: Tags,
|
||||
files: HardDrive,
|
||||
sidebar_apps: PanelLeftClose,
|
||||
advanced: Wrench,
|
||||
};
|
||||
|
||||
@@ -85,6 +91,7 @@ export default function SettingsPage() {
|
||||
const t = useTranslations('settings');
|
||||
const tSidebar = useTranslations('sidebar');
|
||||
const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore();
|
||||
const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps();
|
||||
const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client);
|
||||
const { quota, isPushConnected } = useEmailStore();
|
||||
const { stalwartFeaturesEnabled } = useConfig();
|
||||
@@ -143,6 +150,7 @@ export default function SettingsPage() {
|
||||
...(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 }] : []),
|
||||
{ id: 'sidebar_apps', label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' },
|
||||
{ id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' },
|
||||
];
|
||||
|
||||
@@ -181,6 +189,7 @@ export default function SettingsPage() {
|
||||
{activeTab === 'folders' && <FolderSettings />}
|
||||
{activeTab === 'keywords' && <KeywordSettings />}
|
||||
{activeTab === 'files' && <FilesSettingsComponent />}
|
||||
{activeTab === 'sidebar_apps' && <SidebarAppsSettings />}
|
||||
{activeTab === 'advanced' && <AdvancedSettings />}
|
||||
</>
|
||||
);
|
||||
@@ -212,7 +221,14 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<NavigationRail orientation="horizontal" />
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -280,7 +296,14 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<NavigationRail orientation="horizontal" />
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -295,9 +318,18 @@ export default function SettingsPage() {
|
||||
quota={quota}
|
||||
isPushConnected={isPushConnected}
|
||||
onLogout={() => { logout(); router.push('/login'); }}
|
||||
onManageApps={handleManageApps}
|
||||
onInlineApp={handleInlineApp}
|
||||
onCloseInlineApp={closeInlineApp}
|
||||
activeAppId={inlineApp?.id ?? null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{inlineApp && (
|
||||
<InlineAppView apps={loadedApps} activeAppId={inlineApp!.id} onClose={closeInlineApp} className="flex-1" />
|
||||
)}
|
||||
{!inlineApp && (
|
||||
<>
|
||||
{/* Settings Sidebar */}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -385,6 +417,9 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<SidebarAppsModal isOpen={showAppsModal} onClose={closeAppsModal} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user