"use client"; import { useEffect, useMemo, useState, useRef, type DragEvent } from 'react'; import { Check, X, ArrowLeft, Plus, Trash2, Edit2, LayoutGrid, List, Columns3, Save, Filter, ChevronDown } from 'lucide-react'; import type { Node } from '@/types/database'; import ConfirmDialog from '../common/ConfirmDialog'; import InputDialog from '../common/InputDialog'; import { getNodeIcon } from '@/utils/nodeIcons'; import LucideIconPicker, { DynamicIcon } from '../common/LucideIconPicker'; import { usePersistentState } from '@/hooks/usePersistentState'; type DimensionViewMode = 'grid' | 'list' | 'kanban'; type OverlayMode = 'folders' | 'filtered'; // Each column can have a primary dimension and optional secondary filter (AND logic) interface ColumnFilter { id: string; dimension: string; // Primary dimension filterBy?: string; // Optional secondary dimension (AND filter) } interface SavedView { id: string; name: string; filters: string[]; // Legacy: simple dimension names columnFilters?: ColumnFilter[]; // NEW: columns with optional secondary filters viewMode: DimensionViewMode; kanbanColumns?: { dimension: string; order: number }[]; createdAt: string; } interface DimensionSummary { dimension: string; count: number; isPriority: boolean; description?: string | null; } interface FolderViewOverlayProps { onClose: () => void; onNodeOpen: (nodeId: number) => void; refreshToken: number; onDataChanged?: () => void; } const PAGE_SIZE = 100; export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, onDataChanged }: FolderViewOverlayProps) { const [view, setView] = useState<'dimensions' | 'nodes'>('dimensions'); const [dimensions, setDimensions] = useState([]); const [dimensionsLoading, setDimensionsLoading] = useState(true); const [dimensionsError, setDimensionsError] = useState(null); const [selectedDimension, setSelectedDimension] = useState(null); const [nodes, setNodes] = useState([]); const [nodesLoading, setNodesLoading] = useState(false); const [nodesError, setNodesError] = useState(null); const [hasMoreNodes, setHasMoreNodes] = useState(false); const [nodeOffset, setNodeOffset] = useState(0); const [deletingDimension, setDeletingDimension] = useState(null); const [dimensionPendingDelete, setDimensionPendingDelete] = useState(null); const [dragHoverDimension, setDragHoverDimension] = useState(null); const [editingDescription, setEditingDescription] = useState(false); const [editDescriptionText, setEditDescriptionText] = useState(''); const [editingDimensionName, setEditingDimensionName] = useState(false); const [editDimensionNameText, setEditDimensionNameText] = useState(''); const [showAddDimensionDialog, setShowAddDimensionDialog] = useState(false); const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null); // View mode state (persisted) const [viewMode, setViewMode] = usePersistentState('ui.dimensionViewMode', 'grid'); // All nodes state (for list/kanban view at top level) const [allNodes, setAllNodes] = useState([]); const [allNodesLoading, setAllNodesLoading] = useState(false); // Kanban columns state (global, persisted) const [kanbanColumns, setKanbanColumns] = usePersistentState<{ dimension: string; order: number }[]>( 'ui.kanbanColumns.global', [] ); // Kanban-specific drag states const [draggedNodeId, setDraggedNodeId] = useState(null); const [draggedFromColumn, setDraggedFromColumn] = useState(null); const [dragOverColumn, setDragOverColumn] = useState(null); const [showKanbanColumnPicker, setShowKanbanColumnPicker] = useState(false); const [kanbanSearchQuery, setKanbanSearchQuery] = useState(''); // Filter/View system state const [overlayMode, setOverlayMode] = useState('folders'); // Primary column state - each column has unique ID, allows duplicate dimensions const [columns, setColumns] = usePersistentState('ui.viewColumns', []); const [filteredNodes, setFilteredNodes] = useState([]); const [filteredNodesLoading, setFilteredNodesLoading] = useState(false); const [showFilterPicker, setShowFilterPicker] = useState(false); const [filterSearchQuery, setFilterSearchQuery] = useState(''); const [savedViews, setSavedViews] = usePersistentState('ui.savedViews', []); const [activeViewId, setActiveViewId] = useState(null); const [showSaveViewDialog, setShowSaveViewDialog] = useState(false); const [showSavedViewsDropdown, setShowSavedViewsDropdown] = useState(false); // Which column is showing filter picker (by column ID) const [showColumnFilterPicker, setShowColumnFilterPicker] = useState(null); // Derive selectedFilters for backward compatibility (unique dimensions) // Memoized to prevent infinite useEffect loops const selectedFilters = useMemo(() => [...new Set(columns.map(c => c.dimension))], [columns] ); // Kanban drag-and-drop state const [draggedNode, setDraggedNode] = useState<{ id: number; fromDimension: string } | null>(null); const [dropTargetDimension, setDropTargetDimension] = useState(null); // Kanban column reordering state const [draggedColumn, setDraggedColumn] = useState(null); const [columnDropTarget, setColumnDropTarget] = useState(null); // Node priority ordering within dimensions (persisted) const [dimensionOrders, setDimensionOrders] = usePersistentState>('ui.dimensionOrders', {}); // Dimension icons (persisted) - maps dimension name to Lucide icon name const [dimensionIcons, setDimensionIcons] = usePersistentState>('ui.dimensionIcons', {}); // Dimension edit modal state const [editingDimensionModal, setEditingDimensionModal] = useState(null); const [editModalDescription, setEditModalDescription] = useState(''); const [editModalIcon, setEditModalIcon] = useState('Folder'); const [savingDimensionEdit, setSavingDimensionEdit] = useState(false); // Within-dimension reorder drag state const [reorderDrag, setReorderDrag] = useState<{ nodeId: number; dimension: string; index: number } | null>(null); const [reorderDropIndex, setReorderDropIndex] = useState(null); useEffect(() => { fetchDimensions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (view === 'dimensions') { fetchDimensions(); } else if (selectedDimension) { fetchNodes(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [refreshToken]); useEffect(() => { if (!selectedDimension) return; fetchNodes(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDimension?.dimension]); // Fetch all nodes when viewMode is list or kanban (at top level) useEffect(() => { if (view === 'dimensions' && (viewMode === 'list' || viewMode === 'kanban')) { fetchAllNodes(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewMode, view, refreshToken]); const fetchAllNodes = async () => { setAllNodesLoading(true); try { const response = await fetch('/api/nodes?limit=500'); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch nodes'); } setAllNodes(data.data || []); } catch (error) { console.error('Error fetching all nodes:', error); } finally { setAllNodesLoading(false); } }; // Fetch filtered nodes when filters change useEffect(() => { if (overlayMode === 'filtered' && selectedFilters.length > 0) { fetchFilteredNodes(); } else if (overlayMode === 'filtered' && selectedFilters.length === 0) { setFilteredNodes([]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFilters, overlayMode, refreshToken]); const fetchFilteredNodes = async () => { if (selectedFilters.length === 0) { setFilteredNodes([]); return; } setFilteredNodesLoading(true); try { // Fetch nodes that match ANY of the selected dimensions const response = await fetch(`/api/nodes?limit=500`); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch nodes'); } // Filter client-side to get nodes matching selected dimensions const allFetched: Node[] = data.data || []; const filtered = allFetched.filter(node => node.dimensions?.some(d => selectedFilters.includes(d)) ); setFilteredNodes(filtered); } catch (error) { console.error('Error fetching filtered nodes:', error); } finally { setFilteredNodesLoading(false); } }; // Filter helper functions // Add a new column (allows duplicate dimensions) const addColumn = (dimension: string) => { const newColumn: ColumnFilter = { id: `col-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, dimension }; setColumns([...columns, newColumn]); setShowFilterPicker(false); setFilterSearchQuery(''); }; // Remove a column by ID const removeColumn = (columnId: string) => { setColumns(columns.filter(c => c.id !== columnId)); }; // Legacy: remove first column with this dimension (for chip X buttons) const removeFilter = (dimension: string) => { const idx = columns.findIndex(c => c.dimension === dimension); if (idx !== -1) { setColumns(columns.filter((_, i) => i !== idx)); } }; const clearFilters = () => { setColumns([]); setActiveViewId(null); }; // Column filter helper functions (per-column AND filters) const getColumn = (columnId: string): ColumnFilter | undefined => { return columns.find(c => c.id === columnId); }; const setColumnSecondaryFilter = (columnId: string, filterBy: string | undefined) => { setColumns(columns.map(c => c.id === columnId ? { ...c, filterBy } : c )); setShowColumnFilterPicker(null); }; const getColumnLabel = (column: ColumnFilter): string => { return column.filterBy ? `${column.dimension} + ${column.filterBy}` : column.dimension; }; const getNodesForColumnById = (columnId: string): Node[] => { const column = getColumn(columnId); if (!column) return []; return filteredNodes.filter(node => { const hasPrimary = node.dimensions?.includes(column.dimension); if (!hasPrimary) return false; if (column.filterBy) { return node.dimensions?.includes(column.filterBy); } return true; }); }; // Kanban column reorder handler (now works with column IDs) const handleColumnReorder = (draggedColumnId: string, targetColumnId: string) => { if (draggedColumnId === targetColumnId) return; const currentColumns = [...columns]; const draggedIndex = currentColumns.findIndex(c => c.id === draggedColumnId); const targetIndex = currentColumns.findIndex(c => c.id === targetColumnId); if (draggedIndex === -1 || targetIndex === -1) return; // Remove dragged item and insert at target position const [draggedItem] = currentColumns.splice(draggedIndex, 1); currentColumns.splice(targetIndex, 0, draggedItem); setColumns(currentColumns); }; // Kanban drag-and-drop handler const handleKanbanDrop = async (nodeId: number, fromDimension: string, toDimension: string) => { if (fromDimension === toDimension) return; // Find the node to get its current dimensions const node = filteredNodes.find(n => n.id === nodeId); if (!node) return; // Calculate new dimensions: remove fromDimension, add toDimension const currentDimensions = node.dimensions || []; const newDimensions = currentDimensions .filter(d => d !== fromDimension) .concat(toDimension) .filter((d, i, arr) => arr.indexOf(d) === i); // Remove duplicates try { // Update the node via API const response = await fetch(`/api/nodes/${nodeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: newDimensions }) }); if (response.ok) { // Refresh the filtered nodes fetchFilteredNodes(); // Notify parent that data changed onDataChanged?.(); } } catch (error) { console.error('Failed to update node dimensions:', error); } }; // Saved view helper functions const saveCurrentView = (name: string) => { const newView: SavedView = { id: `view-${Date.now()}`, name: name.trim(), filters: selectedFilters, // Legacy: unique dimensions columnFilters: columns.length > 0 ? [...columns] : undefined, viewMode, kanbanColumns: viewMode === 'kanban' ? [...kanbanColumns] : undefined, createdAt: new Date().toISOString() }; setSavedViews([...savedViews, newView]); setActiveViewId(newView.id); setShowSaveViewDialog(false); }; const loadSavedView = (view: SavedView) => { setOverlayMode('filtered'); // Load columnFilters if available, otherwise convert legacy filters if (view.columnFilters && view.columnFilters.length > 0) { setColumns(view.columnFilters); } else { // Legacy: convert simple filters to columns setColumns(view.filters.map((dim, i) => ({ id: `col-legacy-${i}`, dimension: dim }))); } setViewMode(view.viewMode); if (view.kanbanColumns) { setKanbanColumns(view.kanbanColumns); } setActiveViewId(view.id); setShowSavedViewsDropdown(false); }; const deleteSavedView = (viewId: string) => { setSavedViews(savedViews.filter(v => v.id !== viewId)); if (activeViewId === viewId) { setActiveViewId(null); } }; const activeView = savedViews.find(v => v.id === activeViewId); // Sort nodes by their priority order within a dimension const sortNodesByDimensionOrder = (nodes: Node[], dimension: string): Node[] => { const order = dimensionOrders[dimension] || []; return [...nodes].sort((a, b) => { const aIndex = order.indexOf(a.id); const bIndex = order.indexOf(b.id); // Nodes in order array come first, sorted by their position // Nodes not in order array go to end, sorted by ID if (aIndex === -1 && bIndex === -1) return a.id - b.id; if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; }); }; // Handle reordering a node within a dimension const handleReorderDrop = (dimension: string, fromIndex: number, toIndex: number, nodes: Node[]) => { if (fromIndex === toIndex) return; // Get current order or create from current node order const currentOrder = dimensionOrders[dimension] || nodes.map(n => n.id); const nodeIds = [...currentOrder]; // Ensure all nodes are in the order array nodes.forEach(n => { if (!nodeIds.includes(n.id)) { nodeIds.push(n.id); } }); // Move the node from fromIndex to toIndex const [movedId] = nodeIds.splice(fromIndex, 1); nodeIds.splice(toIndex, 0, movedId); // Update the dimension orders setDimensionOrders({ ...dimensionOrders, [dimension]: nodeIds }); setReorderDrag(null); setReorderDropIndex(null); }; // Get nodes grouped by column for list/grid views (respects column filters) const getColumnsWithNodes = () => { return columns.map(column => { const nodesInColumn = getNodesForColumnById(column.id); const sortedNodes = sortNodesByDimensionOrder(nodesInColumn, column.dimension); return { column, label: getColumnLabel(column), nodes: sortedNodes, hasFilter: !!column.filterBy }; }); }; const sortedDimensions = useMemo(() => { return [...dimensions].sort((a, b) => { if (a.isPriority !== b.isPriority) { return a.isPriority ? -1 : 1; } return a.dimension.localeCompare(b.dimension); }); }, [dimensions]); const fetchDimensions = async () => { setDimensionsLoading(true); setDimensionsError(null); try { const response = await fetch('/api/dimensions/popular'); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch dimensions'); } setDimensions(data.data || []); } catch (error) { console.error('Error fetching dimensions:', error); setDimensionsError('Failed to load dimensions'); } finally { setDimensionsLoading(false); } }; const fetchNodes = async (reset = false) => { if (!selectedDimension) return; setNodesLoading(true); setNodesError(null); try { const offset = reset ? 0 : nodeOffset; const response = await fetch(`/api/nodes?dimensions=${encodeURIComponent(selectedDimension.dimension)}&limit=${PAGE_SIZE}&offset=${offset}`); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to fetch nodes'); } const fetchedNodes: Node[] = data.data || []; setNodes((prev) => reset ? fetchedNodes : [...prev, ...fetchedNodes]); setHasMoreNodes(fetchedNodes.length === PAGE_SIZE); setNodeOffset(offset + fetchedNodes.length); } catch (error) { console.error('Error fetching nodes:', error); setNodesError('Failed to load nodes'); } finally { setNodesLoading(false); } }; const handleSelectDimension = (dimension: DimensionSummary) => { setSelectedDimension(dimension); setNodes([]); setNodeOffset(0); setHasMoreNodes(false); setView('nodes'); }; const handleBackToDimensions = () => { setView('dimensions'); setSelectedDimension(null); setNodes([]); setNodeOffset(0); setHasMoreNodes(false); }; const handleAddDimension = async (name: string) => { if (!name || !name.trim()) return; try { const response = await fetch('/api/dimensions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim() }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to create dimension'); } await fetchDimensions(); onDataChanged?.(); setShowAddDimensionDialog(false); } catch (error) { console.error('Error adding dimension:', error); alert('Failed to create dimension. Please try again.'); } }; const handleToggleLock = async (dimension: string) => { try { const response = await fetch('/api/dimensions/popular', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimension }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to toggle dimension'); } await fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error toggling lock:', error); alert('Failed to update lock state.'); } }; const handleDeleteDimension = async (dimension: string) => { setDeletingDimension(dimension); try { const response = await fetch(`/api/dimensions?name=${encodeURIComponent(dimension)}`, { method: 'DELETE' }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to delete dimension'); } if (selectedDimension?.dimension === dimension) { handleBackToDimensions(); } await fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error deleting dimension:', error); alert('Failed to delete dimension. Please try again.'); } finally { setDeletingDimension((current) => (current === dimension ? null : current)); setDimensionPendingDelete((current) => (current === dimension ? null : current)); } }; const handleNodeTileDragStart = (event: DragEvent, node: Node) => { event.dataTransfer.effectAllowed = 'copy'; const nodeData = { id: node.id, dimensions: node.dimensions || [] }; // Store in ref for webview compatibility (dataTransfer.getData can fail in Electron/Tauri) draggedNodeRef.current = nodeData; // Also set in dataTransfer for browser compatibility event.dataTransfer.setData('application/node-info', JSON.stringify(nodeData)); event.dataTransfer.setData('text/plain', JSON.stringify(nodeData)); // Provide a compact drag preview so drop targets stay visible const preview = document.createElement('div'); preview.textContent = node.title || `Node #${node.id}`; preview.style.position = 'fixed'; preview.style.top = '-1000px'; preview.style.left = '-1000px'; preview.style.padding = '4px 8px'; preview.style.background = '#0f0f0f'; preview.style.color = '#f8fafc'; preview.style.fontSize = '11px'; preview.style.fontWeight = '600'; preview.style.borderRadius = '6px'; preview.style.border = '1px solid #1f1f1f'; document.body.appendChild(preview); event.dataTransfer.setDragImage(preview, 6, 6); setTimeout(() => { if (preview.parentNode) { preview.parentNode.removeChild(preview); } }, 0); }; const handleNodeTileDragEnd = () => { // Clear ref if drag ends without a drop draggedNodeRef.current = null; }; const handleDimensionDragOver = (event: DragEvent) => { if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; } }; const handleDimensionDragEnter = (event: DragEvent, dimension: string) => { if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) { setDragHoverDimension(dimension); } }; const handleDimensionDragLeave = (event: DragEvent, dimension: string) => { if (dragHoverDimension === dimension) { setDragHoverDimension(null); } }; const handleNodeDropOnDimension = async (event: DragEvent, dimension: string) => { event.preventDefault(); event.stopPropagation(); // Try to get data from ref first (works in Electron/Tauri webviews) let payload: { id: number; dimensions?: string[] } | null = draggedNodeRef.current; // Fallback to dataTransfer for browser compatibility if (!payload) { const raw = event.dataTransfer.getData('application/node-info') || event.dataTransfer.getData('text/plain'); if (raw) { try { payload = JSON.parse(raw); } catch (e) { console.error('Failed to parse drag data:', e); } } } // Clear the ref draggedNodeRef.current = null; if (!payload?.id) { console.warn('No valid node data in drop event'); return; } try { const currentDimensions = payload.dimensions || []; if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) { setDragHoverDimension(null); return; } const updatedDimensions = Array.from(new Set([...currentDimensions, dimension])); const response = await fetch(`/api/nodes/${payload.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: updatedDimensions }) }); if (!response.ok) { throw new Error('Failed to update node dimensions'); } if (selectedDimension?.dimension === dimension) { fetchNodes(true); } fetchDimensions(); onDataChanged?.(); } catch (error) { console.error('Error handling node drop:', error); alert('Failed to add dimension to node. Please try again.'); } finally { setDragHoverDimension(null); } }; const getContentPreview = (value?: string | null): string => { if (!value) return ''; const trimmed = value.trim(); if (trimmed.length <= 160) return trimmed; return `${trimmed.slice(0, 160)}…`; }; const handleEditDescription = () => { if (!selectedDimension) return; setEditingDescription(true); setEditDescriptionText(selectedDimension.description || ''); }; const handleSaveDescription = async () => { if (!selectedDimension) return; try { const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: selectedDimension.dimension, description: editDescriptionText.trim() }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update description'); } // Update the local selectedDimension with new description setSelectedDimension(prev => prev ? { ...prev, description: editDescriptionText.trim() } : null); await fetchDimensions(); onDataChanged?.(); setEditingDescription(false); setEditDescriptionText(''); } catch (error) { console.error('Error updating description:', error); alert('Failed to update description. Please try again.'); } }; const handleCancelDescription = () => { setEditingDescription(false); setEditDescriptionText(''); }; // Dimension edit modal handlers (from folder cards) const openDimensionEditModal = (dimension: DimensionSummary) => { setEditingDimensionModal(dimension); setEditModalDescription(dimension.description || ''); setEditModalIcon(dimensionIcons[dimension.dimension] || 'Folder'); }; const closeDimensionEditModal = () => { setEditingDimensionModal(null); setEditModalDescription(''); setEditModalIcon('Folder'); setSavingDimensionEdit(false); }; const saveDimensionEdit = async () => { if (!editingDimensionModal) return; setSavingDimensionEdit(true); try { // Save description to API const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editingDimensionModal.dimension, description: editModalDescription.trim() }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update dimension'); } // Save icon to localStorage setDimensionIcons(prev => ({ ...prev, [editingDimensionModal.dimension]: editModalIcon })); await fetchDimensions(); onDataChanged?.(); closeDimensionEditModal(); } catch (error) { console.error('Error saving dimension:', error); alert('Failed to save dimension. Please try again.'); setSavingDimensionEdit(false); } }; const handleEditDimensionName = () => { if (!selectedDimension) return; setEditingDimensionName(true); setEditDimensionNameText(selectedDimension.dimension); }; const handleSaveDimensionName = async () => { if (!selectedDimension || !editDimensionNameText.trim()) return; const newName = editDimensionNameText.trim(); if (newName === selectedDimension.dimension) { // No change, just cancel handleCancelDimensionName(); return; } try { const response = await fetch('/api/dimensions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ currentName: selectedDimension.dimension, newName: newName, description: selectedDimension.description }) }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.error || 'Failed to update dimension name'); } // Update the local selectedDimension with new name setSelectedDimension(prev => prev ? { ...prev, dimension: newName } : null); await fetchDimensions(); onDataChanged?.(); setEditingDimensionName(false); setEditDimensionNameText(''); } catch (error) { console.error('Error updating dimension name:', error); alert('Failed to update dimension name. Please try again.'); } }; const handleCancelDimensionName = () => { setEditingDimensionName(false); setEditDimensionNameText(''); }; const renderDimensionGrid = () => { if (dimensionsLoading) { return (
Loading dimensions...
); } if (dimensionsError) { return (
{dimensionsError}
); } if (sortedDimensions.length === 0) { return (
No dimensions yet. Create one to get started.
); } return (
{sortedDimensions.map((dimension) => (
handleSelectDimension(dimension)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleSelectDimension(dimension); } }} onDragOver={(event) => handleDimensionDragOver(event)} onDragEnter={(event) => handleDimensionDragEnter(event, dimension.dimension)} onDragLeave={(event) => handleDimensionDragLeave(event, dimension.dimension)} onDrop={(event) => handleNodeDropOnDimension(event, dimension.dimension)} style={{ background: dragHoverDimension === dimension.dimension ? '#0d1a12' : '#0a0a0a', border: dimension.isPriority ? '1px solid #1a3a25' : '1px solid #1a1a1a', borderRadius: '10px', padding: '14px 16px', textAlign: 'left', display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', transition: 'all 0.15s ease' }} onMouseEnter={(e) => { if (dragHoverDimension !== dimension.dimension) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = dimension.isPriority ? '#22c55e' : '#333'; } }} onMouseLeave={(e) => { if (dragHoverDimension !== dimension.dimension) { e.currentTarget.style.background = '#0a0a0a'; e.currentTarget.style.borderColor = dimension.isPriority ? '#1a3a25' : '#1a1a1a'; } }} > {/* Dimension icon */}
{/* Name and count */}
{dimension.dimension}
{dimension.description && (
{dimension.description}
)}
{/* Count badge */} {dimension.count} {/* Action buttons - show on hover via CSS would be ideal, but inline for now */}
))}
); }; // Kanban helper functions const getNodesForKanbanColumn = (dimension: string) => { return nodes.filter(node => node.dimensions?.includes(dimension)); }; const handleAddKanbanColumn = (dimension: string) => { const newColumn = { dimension, order: kanbanColumns.length }; setKanbanColumns([...kanbanColumns, newColumn]); setShowKanbanColumnPicker(false); setKanbanSearchQuery(''); }; const handleRemoveKanbanColumn = (dimension: string) => { setKanbanColumns(kanbanColumns.filter(c => c.dimension !== dimension)); }; const handleKanbanNodeDragStart = (e: DragEvent, nodeId: number, fromColumn: string) => { setDraggedNodeId(nodeId); setDraggedFromColumn(fromColumn); e.dataTransfer.effectAllowed = 'move'; }; const handleKanbanNodeDragEnd = () => { setDraggedNodeId(null); setDraggedFromColumn(null); setDragOverColumn(null); }; const handleKanbanColumnDragOver = (e: DragEvent, columnDimension: string) => { e.preventDefault(); if (draggedNodeId !== null) { setDragOverColumn(columnDimension); } }; const handleKanbanColumnDragLeave = () => { setDragOverColumn(null); }; const handleKanbanNodeDrop = async (e: DragEvent, targetDimension: string) => { e.preventDefault(); if (draggedNodeId === null || draggedFromColumn === targetDimension) { handleKanbanNodeDragEnd(); return; } try { const node = nodes.find(n => n.id === draggedNodeId); if (!node) return; const currentDimensions = node.dimensions || []; let updatedDimensions: string[]; if (draggedFromColumn === '__uncategorized__') { // Adding to a new dimension updatedDimensions = [...currentDimensions, targetDimension]; } else { // Replace old dimension with new one updatedDimensions = currentDimensions.map(d => d === draggedFromColumn ? targetDimension : d ); } const response = await fetch(`/api/nodes/${draggedNodeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: updatedDimensions }) }); if (!response.ok) { throw new Error('Failed to update node dimensions'); } fetchNodes(true); onDataChanged?.(); } catch (error) { console.error('Error updating node dimension:', error); alert('Failed to move node. Please try again.'); } finally { handleKanbanNodeDragEnd(); } }; const filteredKanbanDimensions = dimensions.filter(d => d.dimension.toLowerCase().includes(kanbanSearchQuery.toLowerCase()) && !kanbanColumns.some(c => c.dimension === d.dimension) && d.dimension !== selectedDimension?.dimension ); const sortedKanbanColumns = [...kanbanColumns].sort((a, b) => a.order - b.order); // Render functions for each view mode const renderGridContent = () => (
{nodes.map((node) => (
handleNodeTileDragStart(event, node)} onDragEnd={handleNodeTileDragEnd} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ background: '#0a0a0a', border: '1px solid #161616', borderRadius: '16px', padding: '20px', display: 'flex', flexDirection: 'column', gap: '12px', cursor: 'pointer', transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', position: 'relative', minHeight: '200px', maxHeight: '200px', overflow: 'hidden', boxShadow: '0 1px 4px rgba(0, 0, 0, 0.2)' }} onMouseEnter={(e) => { e.currentTarget.style.background = '#111111'; e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = '#0a0a0a'; e.currentTarget.style.transform = 'translateY(0px)'; e.currentTarget.style.boxShadow = '0 1px 4px rgba(0, 0, 0, 0.2)'; }} >
{getNodeIcon(node)}
{node.title || 'Untitled'}
#{node.id}
{node.content && (
{getContentPreview(node.content)}
)} {node.link && (
{node.link}
)} {node.dimensions && node.dimensions.length > 0 && (
{node.dimensions.slice(0, 4).map((dimension, index) => { const isCurrentDimension = dimension === selectedDimension?.dimension; return ( {dimension} ); })} {node.dimensions.length > 4 && ( +{node.dimensions.length - 4} )}
)}
))} {nodesLoading && (
Loading...
)} {!nodesLoading && nodes.length === 0 && (
No nodes in this dimension yet.
)}
); const renderListContent = () => (
{nodes.map((node) => ( ))} {nodesLoading && (
Loading...
)} {!nodesLoading && nodes.length === 0 && (
No nodes in this dimension yet.
)}
); const renderKanbanContent = () => (
{/* Kanban Column Setup Bar */}
Group by: {kanbanColumns.length === 0 && ( Add dimensions to create columns )}
{showKanbanColumnPicker && (
setKanbanSearchQuery(e.target.value)} style={{ width: '100%', padding: '8px 12px', background: '#0a0a0a', border: 'none', borderBottom: '1px solid #333', color: '#fff', fontSize: '12px', outline: 'none' }} autoFocus />
{filteredKanbanDimensions.length === 0 ? (
No dimensions available
) : ( filteredKanbanDimensions.map(dim => ( )) )}
)}
{showKanbanColumnPicker && (
setShowKanbanColumnPicker(false)} /> )}
{/* Kanban Board */}
{sortedKanbanColumns.map(column => { const columnNodes = getNodesForKanbanColumn(column.dimension); const isDropTarget = dragOverColumn === column.dimension && draggedFromColumn !== column.dimension; return (
handleKanbanColumnDragOver(e, column.dimension)} onDragLeave={handleKanbanColumnDragLeave} onDrop={(e) => handleKanbanNodeDrop(e, column.dimension)} > {/* Column Header */}
{column.dimension} {columnNodes.length}
{/* Column Content */}
{columnNodes.map(node => (
handleKanbanNodeDragStart(e, node.id, column.dimension)} onDragEnd={handleKanbanNodeDragEnd} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ padding: '10px', marginBottom: '6px', background: draggedNodeId === node.id ? '#1a1a1a' : '#111', border: '1px solid #222', borderRadius: '8px', cursor: 'pointer', opacity: draggedNodeId === node.id ? 0.5 : 1, transition: 'all 0.2s' }} onMouseEnter={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#1a1a1a'; e.currentTarget.style.borderColor = '#333'; } }} onMouseLeave={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = '#222'; } }} >
{node.title || 'Untitled'}
{node.dimensions && node.dimensions.length > 1 && (
{node.dimensions .filter(d => d !== column.dimension && d !== selectedDimension?.dimension) .slice(0, 2) .map(dim => ( {dim} ))}
)}
))} {columnNodes.length === 0 && (
Drop nodes here
)}
); })} {/* Empty State */} {kanbanColumns.length === 0 && (
Add dimension columns to organize your nodes
)}
); // Render all nodes view (list or kanban) - shown at top level when not in folder view const renderAllNodesView = () => { if (allNodesLoading) { return (
Loading nodes...
); } if (viewMode === 'list') { return (
Showing {allNodes.length} nodes
{allNodes.map((node) => ( ))} {allNodes.length === 0 && (
No nodes yet.
)}
); } if (viewMode === 'kanban') { return (
{/* Kanban Column Setup Bar */}
Group by: {kanbanColumns.length === 0 && ( Add dimensions to create columns )}
{showKanbanColumnPicker && (
setKanbanSearchQuery(e.target.value)} style={{ width: '100%', padding: '8px 12px', background: '#0a0a0a', border: 'none', borderBottom: '1px solid #333', color: '#fff', fontSize: '12px', outline: 'none' }} autoFocus />
{dimensions.filter(d => d.dimension.toLowerCase().includes(kanbanSearchQuery.toLowerCase()) && !kanbanColumns.some(c => c.dimension === d.dimension) ).length === 0 ? (
No dimensions available
) : ( dimensions.filter(d => d.dimension.toLowerCase().includes(kanbanSearchQuery.toLowerCase()) && !kanbanColumns.some(c => c.dimension === d.dimension) ).map(dim => ( )) )}
)}
{showKanbanColumnPicker && (
setShowKanbanColumnPicker(false)} /> )}
{/* Kanban Board */}
{sortedKanbanColumns.map(column => { const columnNodes = allNodes.filter(node => node.dimensions?.includes(column.dimension)); const isDropTarget = dragOverColumn === column.dimension && draggedFromColumn !== column.dimension; return (
handleKanbanColumnDragOver(e, column.dimension)} onDragLeave={handleKanbanColumnDragLeave} onDrop={(e) => handleAllNodesKanbanDrop(e, column.dimension)} > {/* Column Header */}
{column.dimension} {columnNodes.length}
{/* Column Content */}
{columnNodes.map(node => (
handleKanbanNodeDragStart(e, node.id, column.dimension)} onDragEnd={handleKanbanNodeDragEnd} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ padding: '10px', marginBottom: '6px', background: draggedNodeId === node.id ? '#1a1a1a' : '#111', border: '1px solid #222', borderRadius: '8px', cursor: 'pointer', opacity: draggedNodeId === node.id ? 0.5 : 1, transition: 'all 0.2s' }} onMouseEnter={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#1a1a1a'; e.currentTarget.style.borderColor = '#333'; } }} onMouseLeave={(e) => { if (draggedNodeId !== node.id) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = '#222'; } }} >
{node.title || 'Untitled'}
{node.dimensions && node.dimensions.length > 1 && (
{node.dimensions .filter(d => d !== column.dimension) .slice(0, 2) .map(dim => ( {dim} ))}
)}
))} {columnNodes.length === 0 && (
Drop nodes here
)}
); })} {/* Empty State */} {kanbanColumns.length === 0 && (
Add dimension columns to organize your nodes
)}
); } return null; }; // Handle kanban drop for all nodes view const handleAllNodesKanbanDrop = async (e: DragEvent, targetDimension: string) => { e.preventDefault(); if (draggedNodeId === null || draggedFromColumn === targetDimension) { handleKanbanNodeDragEnd(); return; } try { const node = allNodes.find(n => n.id === draggedNodeId); if (!node) return; const currentDimensions = node.dimensions || []; let updatedDimensions: string[]; if (draggedFromColumn === '__uncategorized__') { updatedDimensions = [...currentDimensions, targetDimension]; } else { updatedDimensions = currentDimensions.map(d => d === draggedFromColumn ? targetDimension : d ); } const response = await fetch(`/api/nodes/${draggedNodeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dimensions: updatedDimensions }) }); if (!response.ok) { throw new Error('Failed to update node dimensions'); } fetchAllNodes(); onDataChanged?.(); } catch (error) { console.error('Error updating node dimension:', error); alert('Failed to move node. Please try again.'); } finally { handleKanbanNodeDragEnd(); } }; const renderNodeGrid = () => { if (!selectedDimension) return null; return (
Showing {nodes.length} nodes tagged with {selectedDimension.dimension.toUpperCase()}
{renderGridContent()} {nodesError && (
{nodesError}
)} {hasMoreNodes && (
)}
); }; // Render filtered view content based on viewMode const renderFilteredView = () => { if (filteredNodesLoading) { return (
Loading...
); } if (selectedFilters.length === 0) { return (
Select dimension filters to view nodes
); } if (filteredNodes.length === 0) { return (
No nodes match the selected filters
); } // List view with dimension grouping if (viewMode === 'list') { const groups = getColumnsWithNodes(); return (
{groups.map(group => (
{/* Left side: dimension name + AND filter button */}
{group.label} ({group.nodes.length}) {showColumnFilterPicker === group.column.id && ( <>
setShowColumnFilterPicker(null)} />
Filter by (AND)
{group.hasFilter && ( )} {dimensions .filter(d => d.dimension !== group.column.dimension && d.dimension !== group.column.filterBy) .map(dim => ( ))}
)}
{/* Right side: remove button */}
{group.nodes.map((node, index) => (
{ setReorderDrag({ nodeId: node.id, dimension: group.column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { setReorderDrag(null); setReorderDropIndex(null); }} onDragOver={(e) => { e.preventDefault(); if (reorderDrag && reorderDrag.dimension === group.column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} onDrop={(e) => { e.preventDefault(); if (reorderDrag && reorderDrag.dimension === group.column.dimension) { handleReorderDrop(group.column.dimension, reorderDrag.index, index, group.nodes); } }} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 10px', marginBottom: '2px', background: reorderDrag?.nodeId === node.id ? '#1a2a1f' : 'transparent', border: reorderDropIndex === index && reorderDrag?.dimension === group.column.dimension && reorderDrag?.nodeId !== node.id ? '1px dashed #22c55e' : '1px solid transparent', borderRadius: '6px', cursor: 'grab', textAlign: 'left', transition: 'all 0.15s', opacity: reorderDrag?.nodeId === node.id ? 0.5 : 1 }} onMouseEnter={(e) => { if (!reorderDrag) e.currentTarget.style.background = '#111'; }} onMouseLeave={(e) => { if (!reorderDrag) e.currentTarget.style.background = 'transparent'; }} > ⋮⋮ {node.id}
{getNodeIcon(node)}
{node.title || 'Untitled'}
{node.dimensions && node.dimensions.length > 1 && (
{node.dimensions .filter((d: string) => d !== group.column.dimension) .slice(0, 2) .map(dim => ( {dim} ))}
)}
))}
))}
); } // Grid view with dimension grouping if (viewMode === 'grid') { const groups = getColumnsWithNodes(); return (
{groups.map(group => (
{/* Left side: dimension name + AND filter button */}
{group.label} ({group.nodes.length}) {showColumnFilterPicker === group.column.id && ( <>
setShowColumnFilterPicker(null)} />
Filter by (AND)
{group.hasFilter && ( )} {dimensions .filter(d => d.dimension !== group.column.dimension && d.dimension !== group.column.filterBy) .map(dim => ( ))}
)}
{/* Right side: remove button */}
{group.nodes.map((node: Node, index: number) => (
{ setReorderDrag({ nodeId: node.id, dimension: group.column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { setReorderDrag(null); setReorderDropIndex(null); }} onDragOver={(e) => { e.preventDefault(); if (reorderDrag && reorderDrag.dimension === group.column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} onDrop={(e) => { e.preventDefault(); if (reorderDrag && reorderDrag.dimension === group.column.dimension) { handleReorderDrop(group.column.dimension, reorderDrag.index, index, group.nodes); } }} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ background: reorderDrag?.nodeId === node.id ? '#1a2a1f' : '#0a0a0a', border: reorderDropIndex === index && reorderDrag?.dimension === group.column.dimension && reorderDrag?.nodeId !== node.id ? '2px dashed #22c55e' : '1px solid #161616', borderRadius: '10px', padding: '14px', display: 'flex', flexDirection: 'column', gap: '8px', cursor: 'grab', transition: 'all 0.15s ease', minHeight: '120px', maxHeight: '140px', overflow: 'hidden', opacity: reorderDrag?.nodeId === node.id ? 0.5 : 1 }} onMouseEnter={(e) => { if (!reorderDrag) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = '#222'; } }} onMouseLeave={(e) => { if (!reorderDrag) { e.currentTarget.style.background = '#0a0a0a'; e.currentTarget.style.borderColor = '#161616'; } }} >
⋮⋮ {node.id} {getNodeIcon(node)}
{node.title || 'Untitled'}
{node.content && (
{getContentPreview(node.content)}
)} {node.dimensions && node.dimensions.length > 1 && (
{node.dimensions.filter((d: string) => d !== group.column.dimension).slice(0, 2).map((dim: string) => ( {dim} ))}
)}
))}
))}
); } // Kanban view - group by columns with drag-and-drop if (viewMode === 'kanban') { return (
{columns.map(column => { const unsortedColumnNodes = getNodesForColumnById(column.id); const columnNodes = sortNodesByDimensionOrder(unsortedColumnNodes, column.dimension); const columnLabel = getColumnLabel(column); const hasSecondaryFilter = !!column.filterBy; const isDropTarget = dropTargetDimension === column.id; const isColumnDropTarget = columnDropTarget === column.id && draggedColumn !== column.id; const isBeingDragged = draggedColumn === column.id; return (
{ e.preventDefault(); // If dragging a column, set column drop target if (draggedColumn) { setColumnDropTarget(column.id); } else { setDropTargetDimension(column.id); } }} onDragLeave={(e) => { // Only clear if leaving the column entirely if (!e.currentTarget.contains(e.relatedTarget as HTMLElement)) { setDropTargetDimension(null); setColumnDropTarget(null); } }} onDrop={(e) => { e.preventDefault(); // Handle column reorder if (draggedColumn && draggedColumn !== column.id) { handleColumnReorder(draggedColumn, column.id); setDraggedColumn(null); setColumnDropTarget(null); } // Handle node drop else if (draggedNode && draggedNode.fromDimension !== column.dimension) { handleKanbanDrop(draggedNode.id, draggedNode.fromDimension, column.dimension); } setDraggedNode(null); setDropTargetDimension(null); }} style={{ width: '280px', minWidth: '280px', display: 'flex', flexDirection: 'column', background: isColumnDropTarget ? '#0d1a12' : (isDropTarget ? '#0d1a12' : '#0a0a0a'), border: isColumnDropTarget ? '1px solid #22c55e' : (isDropTarget ? '1px solid #22c55e' : '1px solid #1a1a1a'), borderRadius: '12px', transition: 'all 0.15s ease', opacity: isBeingDragged ? 0.5 : 1 }} > {/* Column header - draggable for reordering */}
{ setDraggedColumn(column.id); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { setDraggedColumn(null); setColumnDropTarget(null); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px', borderBottom: '1px solid #1a1a1a', cursor: 'grab', userSelect: 'none' }} >
⋮⋮ {columnLabel} {columnNodes.length} {/* AND filter button - inline with column name */} {showColumnFilterPicker === column.id && ( <>
setShowColumnFilterPicker(null)} />
Filter by (AND)
{hasSecondaryFilter && ( )} {dimensions .filter(d => d.dimension !== column.dimension && d.dimension !== column.filterBy) .map(dim => ( ))}
)}
{columnNodes.map((node, index) => { const isReorderTarget = reorderDropIndex === index && reorderDrag?.dimension === column.dimension && reorderDrag?.nodeId !== node.id; return (
{ setDraggedNode({ id: node.id, fromDimension: column.dimension }); setReorderDrag({ nodeId: node.id, dimension: column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { setDraggedNode(null); setDropTargetDimension(null); setReorderDrag(null); setReorderDropIndex(null); }} onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); // If dragging within same column, show reorder indicator if (reorderDrag && reorderDrag.dimension === column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); // Handle within-column reorder if (reorderDrag && reorderDrag.dimension === column.dimension && reorderDrag.nodeId !== node.id) { handleReorderDrop(column.dimension, reorderDrag.index, index, columnNodes); } }} onClick={() => { onNodeOpen(node.id); onClose(); }} style={{ padding: '10px', marginBottom: '6px', background: draggedNode?.id === node.id ? '#1a2a1f' : '#111', border: isReorderTarget ? '2px dashed #22c55e' : (draggedNode?.id === node.id ? '1px solid #22c55e' : '1px solid #222'), borderRadius: '8px', cursor: 'grab', transition: 'all 0.15s', userSelect: 'none', opacity: draggedNode?.id === node.id ? 0.5 : 1 }} onMouseEnter={(e) => { if (!draggedNode) { e.currentTarget.style.background = '#1a1a1a'; e.currentTarget.style.borderColor = '#333'; } }} onMouseLeave={(e) => { if (!draggedNode) { e.currentTarget.style.background = '#111'; e.currentTarget.style.borderColor = '#222'; } }} >
⋮⋮ {node.id}
{node.title || 'Untitled'}
{node.dimensions && node.dimensions.length > 1 && (
{node.dimensions .filter((d: string) => d !== column.dimension) .slice(0, 2) .map(dim => ( {dim} ))}
)}
);})} {columnNodes.length === 0 && (
{isDropTarget ? 'Drop here' : 'No nodes'}
)}
); })}
); } return null; }; return ( <>
{/* Header */}
{/* Top row: Mode tabs + Actions */}
{/* Back button when viewing nodes in a dimension */} {view === 'nodes' && ( )} {/* Mode tabs - only show when not drilling into a specific dimension */} {view === 'dimensions' && (
)} {/* Title when viewing nodes in a dimension */} {view === 'nodes' && (
Nodes – {selectedDimension?.dimension ?? ''}
)} {/* Saved views dropdown */} {view === 'dimensions' && savedViews.length > 0 && (
{showSavedViewsDropdown && ( <>
setShowSavedViewsDropdown(false)} />
{savedViews.map(sv => (
{ if (activeViewId !== sv.id) e.currentTarget.style.background = '#252525'; }} onMouseLeave={(e) => { if (activeViewId !== sv.id) e.currentTarget.style.background = 'transparent'; }} > loadSavedView(sv)} style={{ flex: 1, fontSize: '12px', color: '#ccc' }} > {sv.name}
))}
)}
)}
{/* Add Dimension button - only in folders mode */} {view === 'dimensions' && overlayMode === 'folders' && ( )} {/* View mode toggle - only in filtered mode with filters */} {view === 'dimensions' && overlayMode === 'filtered' && selectedFilters.length > 0 && (
)} {/* Save View button - only in filtered mode with filters */} {view === 'dimensions' && overlayMode === 'filtered' && selectedFilters.length > 0 && ( )}
{/* Filter bar - only show in filtered mode */} {view === 'dimensions' && overlayMode === 'filtered' && (
{/* Filter chips */}
{selectedFilters.map(filter => ( {filter} ))} {/* Add filter button */}
{showFilterPicker && ( <>
setShowFilterPicker(false)} />
setFilterSearchQuery(e.target.value)} style={{ width: '100%', padding: '8px 12px', background: '#0a0a0a', border: 'none', borderBottom: '1px solid #333', color: '#fff', fontSize: '12px', outline: 'none' }} autoFocus />
{dimensions .filter(d => d.dimension.toLowerCase().includes(filterSearchQuery.toLowerCase()) ) .map(dim => ( ))} {dimensions.filter(d => d.dimension.toLowerCase().includes(filterSearchQuery.toLowerCase()) && !selectedFilters.includes(d.dimension) ).length === 0 && (
No dimensions available
)}
)}
{selectedFilters.length > 0 && ( )}
)}
{/* Content */} {view === 'nodes' ? ( renderNodeGrid() ) : overlayMode === 'folders' ? ( renderDimensionGrid() ) : ( renderFilteredView() )}
{ if (dimensionPendingDelete) { handleDeleteDimension(dimensionPendingDelete); } }} onCancel={() => setDimensionPendingDelete(null)} /> setShowAddDimensionDialog(false)} /> setShowSaveViewDialog(false)} /> {/* Dimension Edit Modal */} {editingDimensionModal && (
{ if (e.target === e.currentTarget) closeDimensionEditModal(); }} >
{/* Header */}
Edit Dimension
{editingDimensionModal.dimension}
{/* Content */}
{/* Description */}