import { useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Eye, EyeOff, Link2, Plus, Pencil, Trash2, GripVertical, Loader2, ExternalLink as ExternalLinkIcon, RotateCcw } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../api/client'; import type { ExternalLink } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; import { AddExternalLinkModal } from './AddExternalLinkModal'; import { ConfirmModal } from './ConfirmModal'; import { getIconByName } from './IconPicker'; import { defaultNavItems } from './Layout'; import { useAuth } from '../contexts/AuthContext'; import { useToast } from '../contexts/ToastContext'; import { getHiddenSidebarSystemItemIds, getSidebarOrder, saveHiddenSidebarSystemItemIds, saveSidebarOrder, } from '../utils/sidebarLayout'; type SidebarLayoutItem = | { type: 'system'; id: string; navItem: typeof defaultNavItems[number] } | { type: 'external'; id: string; link: ExternalLink }; export function ExternalLinksSettings() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { authEnabled, hasPermission } = useAuth(); const { showToast } = useToast(); const [showAddModal, setShowAddModal] = useState(false); const [editingLink, setEditingLink] = useState(null); const [deletingLink, setDeletingLink] = useState(null); const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const [hiddenSystemItemIds, setHiddenSystemItemIds] = useState(getHiddenSidebarSystemItemIds); const [sidebarOrder, setSidebarOrder] = useState(() => getSidebarOrder(defaultNavItems.map(i => i.id))); const { data: links, isLoading } = useQuery({ queryKey: ['external-links'], queryFn: api.getExternalLinks, }); const deleteMutation = useMutation({ mutationFn: (id: number) => api.deleteExternalLink(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['external-links'] }); }, }); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, enabled: authEnabled && hasPermission('settings:update'), }); const updateDefaultSidebarMutation = useMutation({ mutationFn: (defaultSidebarOrder: string) => api.updateSettings({ default_sidebar_order: defaultSidebarOrder }), onSuccess: (_data, defaultSidebarOrder) => { queryClient.invalidateQueries({ queryKey: ['settings'] }); queryClient.invalidateQueries({ queryKey: ['default-sidebar-order'] }); showToast( defaultSidebarOrder ? t('settings.sidebarDefaultSet') : t('settings.sidebarDefaultCleared'), 'success' ); }, onError: () => { showToast(t('settings.sidebarDefaultFailed'), 'error'); }, }); const layoutItems = useMemo(() => { const navItemsMap = new Map(defaultNavItems.map(item => [item.id, item])); const externalLinksMap = new Map((links || []).map(link => [`ext-${link.id}`, link])); const result: SidebarLayoutItem[] = []; const seen = new Set(); const addItem = (id: string) => { if (seen.has(id)) return; const navItem = navItemsMap.get(id); if (navItem) { result.push({ type: 'system', id, navItem }); seen.add(id); return; } const link = externalLinksMap.get(id); if (link) { result.push({ type: 'external', id, link }); seen.add(id); } }; sidebarOrder.forEach(addItem); defaultNavItems.forEach(item => addItem(item.id)); (links || []).forEach(link => addItem(`ext-${link.id}`)); return result; }, [links, sidebarOrder]); const persistSidebarOrder = (order: string[]) => { setSidebarOrder(order); saveSidebarOrder(order); }; const handleDragStart = (e: React.DragEvent, id: string) => { setDraggedId(id); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', id); }; const handleDragOver = (e: React.DragEvent, id: string) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverId(id); }; const handleDrop = (e: React.DragEvent, targetId: string) => { e.preventDefault(); if (draggedId === null || draggedId === targetId) { setDraggedId(null); setDragOverId(null); return; } const currentOrder = layoutItems.map(item => item.id); const draggedIndex = currentOrder.indexOf(draggedId); const targetIndex = currentOrder.indexOf(targetId); if (draggedIndex === -1 || targetIndex === -1) { setDraggedId(null); setDragOverId(null); return; } currentOrder.splice(draggedIndex, 1); currentOrder.splice(targetIndex, 0, draggedId); persistSidebarOrder(currentOrder); setDraggedId(null); setDragOverId(null); }; const handleDragEnd = () => { setDraggedId(null); setDragOverId(null); }; const handleDelete = (link: ExternalLink) => { setDeletingLink(link); }; const confirmDelete = () => { if (deletingLink) { deleteMutation.mutate(deletingLink.id); setDeletingLink(null); } }; const resetSidebarLayout = () => { const resetOrder = [ ...defaultNavItems.map(item => item.id), ...(links || []).map(link => `ext-${link.id}`), ]; setHiddenSystemItemIds([]); setSidebarOrder(resetOrder); saveHiddenSidebarSystemItemIds([]); saveSidebarOrder(resetOrder); }; const handleToggleDefaultSidebarOrder = (enabled: boolean) => { const currentOrder = layoutItems.map(item => item.id); updateDefaultSidebarMutation.mutate(enabled ? JSON.stringify({ order: currentOrder, hiddenSystemItemIds, }) : ''); }; const toggleSystemItemVisibility = (id: string) => { if (id === 'settings') return; const isHidden = hiddenSystemItemIds.includes(id); const nextIds = isHidden ? hiddenSystemItemIds.filter(hiddenId => hiddenId !== id) : [...hiddenSystemItemIds, id]; setHiddenSystemItemIds(nextIds); saveHiddenSidebarSystemItemIds(nextIds); }; const canSetDefaultSidebarOrder = authEnabled && hasPermission('settings:update'); const isDefaultSidebarEnabled = !!settings?.default_sidebar_order; return ( <>

{t('externalLinks.sidebarLayout')}

{canSetDefaultSidebarOrder && ( )}

{t('externalLinks.sidebarLayoutDescription')}

{isLoading ? (
) : (
{layoutItems.map((item) => { const isHidden = item.type === 'system' && hiddenSystemItemIds.includes(item.id); const isSettings = item.id === 'settings'; if (item.type === 'system') { const Icon = item.navItem.icon; return (
handleDragStart(e, item.id)} onDragOver={(e) => handleDragOver(e, item.id)} onDragLeave={() => setDragOverId(null)} onDrop={(e) => handleDrop(e, item.id)} onDragEnd={handleDragEnd} className={`relative flex items-center gap-3 p-3 rounded-lg bg-bambu-dark border border-bambu-dark-tertiary transition-colors ${ draggedId === item.id ? 'opacity-50' : isHidden ? 'opacity-60' : '' } ${ dragOverId === item.id && draggedId !== item.id ? 'before:absolute before:left-3 before:right-3 before:top-0 before:h-0.5 before:bg-bambu-green' : '' }`} >
{t(item.navItem.labelKey)} {isSettings ? t('externalLinks.requiredInSidebar') : isHidden ? t('externalLinks.hiddenFromSidebar') : t('externalLinks.visibleInSidebar')}
); } const link = item.link; const Icon = getIconByName(link.icon); return (
handleDragStart(e, item.id)} onDragOver={(e) => handleDragOver(e, item.id)} onDragLeave={() => setDragOverId(null)} onDrop={(e) => handleDrop(e, item.id)} onDragEnd={handleDragEnd} className={`relative flex items-center gap-3 p-3 rounded-lg bg-bambu-dark border border-bambu-dark-tertiary transition-colors ${ draggedId === item.id ? 'opacity-50' : '' } ${ dragOverId === item.id && draggedId !== item.id ? 'before:absolute before:left-3 before:right-3 before:top-0 before:h-0.5 before:bg-bambu-green' : '' }`} >
{link.custom_icon ? ( ) : ( )}
{link.name}
{link.url}
); })}
)}
{(showAddModal || editingLink) && ( { setShowAddModal(false); setEditingLink(null); }} /> )} {deletingLink && ( setDeletingLink(null)} /> )} ); }