From c9d85998c1ade504f7646cc013ebb480ea9ab63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Thu, 25 Dec 2025 10:05:02 +1100 Subject: [PATCH] sync: Phase 3 Agent Features from private repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features synced: - LucideIconPicker component (115 icons for dimension customization) - Dimension editing modal (descriptions + icons) - Compound filters (per-column AND filters) - Improved dimension assignment with keyword tagging - queryDimensions and getDimension agent tools New files: - src/components/common/LucideIconPicker.tsx - src/tools/database/queryDimensions.ts - src/tools/database/getDimension.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/api/nodes/route.ts | 13 +- src/components/common/LucideIconPicker.tsx | 418 +++++++++ src/components/nodes/FolderViewOverlay.tsx | 929 ++++++++++++++++++--- src/components/nodes/NodesPanel.tsx | 13 +- src/services/database/dimensionService.ts | 225 +++-- src/tools/database/getDimension.ts | 94 +++ src/tools/database/queryDimensions.ts | 108 +++ src/tools/infrastructure/registry.ts | 4 + 8 files changed, 1630 insertions(+), 174 deletions(-) create mode 100644 src/components/common/LucideIconPicker.tsx create mode 100644 src/tools/database/getDimension.ts create mode 100644 src/tools/database/queryDimensions.ts diff --git a/app/api/nodes/route.ts b/app/api/nodes/route.ts index 886b961..69c2ada 100644 --- a/app/api/nodes/route.ts +++ b/app/api/nodes/route.ts @@ -66,15 +66,20 @@ export async function POST(request: NextRequest) { .filter(Boolean) .slice(0, 5); - // Auto-assign locked dimensions for all new nodes - const autoAssignedDimensions = await DimensionService.assignLockedDimensions({ + // Auto-assign locked dimensions + keyword dimensions for all new nodes + const { locked, keywords } = await DimensionService.assignDimensions({ title: body.title, content: rawContent || undefined, link: body.link }); - // Combine provided and auto-assigned dimensions, remove duplicates - const finalDimensions = [...new Set([...trimmedProvidedDimensions, ...autoAssignedDimensions])] + // Ensure keyword dimensions exist in the database (create if new) + for (const keyword of keywords) { + await DimensionService.ensureKeywordDimension(keyword); + } + + // Combine provided, locked, and keyword dimensions, remove duplicates + const finalDimensions = [...new Set([...trimmedProvidedDimensions, ...locked, ...keywords])] .slice(0, 5); // Ensure we don't exceed 5 total dimensions const rawChunk = typeof body.chunk === 'string' ? body.chunk : null; let chunkToStore = rawChunk; diff --git a/src/components/common/LucideIconPicker.tsx b/src/components/common/LucideIconPicker.tsx new file mode 100644 index 0000000..ac6cb49 --- /dev/null +++ b/src/components/common/LucideIconPicker.tsx @@ -0,0 +1,418 @@ +"use client"; + +import { useState, useMemo } from 'react'; +import { + // Core + Folder, + BookOpen, + Lightbulb, + Star, + Heart, + Briefcase, + Code, + FileText, + Globe, + Hash, + Inbox, + Layers, + Link, + MessageSquare, + Music, + Paperclip, + Search, + Settings, + Tag, + User, + Video, + Zap, + Archive, + Award, + Bell, + Bookmark, + Calendar, + Camera, + Clock, + Cloud, + Coffee, + Compass, + Database, + Flag, + Gift, + Home, + Image, + Key, + Mail, + Map, + Mic, + Package, + Smile, + Target, + // Thinking & Ideas + Brain, + Sparkles, + Wand2, + FlaskConical, + Atom, + // Content & Media + Newspaper, + Podcast, + Youtube, + Twitter, + Rss, + Radio, + Tv, + Film, + // Work & Business + Rocket, + TrendingUp, + PieChart, + Presentation, + LineChart, + BarChart3, + CircleDollarSign, + Wallet, + // People & Social + Users, + UserCircle, + Contact, + MessagesSquare, + Share2, + // Nature & Environment + Leaf, + Sun, + Moon, + Mountain, + Trees, + Flower2, + // Objects & Tools + Wrench, + Hammer, + Paintbrush, + Pencil, + Scissors, + Ruler, + // Tech & Development + Terminal, + GitBranch, + Bug, + Cpu, + Server, + Wifi, + Smartphone, + Laptop, + // Documents & Files + FileCode, + FilePlus, + FolderOpen, + ClipboardList, + ScrollText, + StickyNote, + // Health & Wellness + HeartPulse, + Activity, + Dumbbell, + Apple, + // Navigation & Location + Navigation, + MapPin, + Route, + Signpost, + // Misc Popular + Crown, + Diamond, + Gem, + Shield, + Lock, + Eye, + ThumbsUp, + CheckCircle, + AlertCircle, + Info, + HelpCircle, + Play, + Pause, + SkipForward, + type LucideIcon +} from 'lucide-react'; + +// Static icon registry - no dynamic imports, works perfectly in Tauri WebView +export const ICON_REGISTRY: Record = { + // Core + Folder, + BookOpen, + Lightbulb, + Star, + Heart, + Briefcase, + Code, + FileText, + Globe, + Hash, + Inbox, + Layers, + Link, + MessageSquare, + Music, + Paperclip, + Search, + Settings, + Tag, + User, + Video, + Zap, + Archive, + Award, + Bell, + Bookmark, + Calendar, + Camera, + Clock, + Cloud, + Coffee, + Compass, + Database, + Flag, + Gift, + Home, + Image, + Key, + Mail, + Map, + Mic, + Package, + Smile, + Target, + // Thinking & Ideas + Brain, + Sparkles, + Wand2, + FlaskConical, + Atom, + // Content & Media + Newspaper, + Podcast, + Youtube, + Twitter, + Rss, + Radio, + Tv, + Film, + // Work & Business + Rocket, + TrendingUp, + PieChart, + Presentation, + LineChart, + BarChart3, + CircleDollarSign, + Wallet, + // People & Social + Users, + UserCircle, + Contact, + MessagesSquare, + Share2, + // Nature & Environment + Leaf, + Sun, + Moon, + Mountain, + Trees, + Flower2, + // Objects & Tools + Wrench, + Hammer, + Paintbrush, + Pencil, + Scissors, + Ruler, + // Tech & Development + Terminal, + GitBranch, + Bug, + Cpu, + Server, + Wifi, + Smartphone, + Laptop, + // Documents & Files + FileCode, + FilePlus, + FolderOpen, + ClipboardList, + ScrollText, + StickyNote, + // Health & Wellness + HeartPulse, + Activity, + Dumbbell, + Apple, + // Navigation & Location + Navigation, + MapPin, + Route, + Signpost, + // Misc Popular + Crown, + Diamond, + Gem, + Shield, + Lock, + Eye, + ThumbsUp, + CheckCircle, + AlertCircle, + Info, + HelpCircle, + Play, + Pause, + SkipForward, +}; + +// Helper to get icon component by name +export function getIconByName(name: string): LucideIcon { + return ICON_REGISTRY[name] || Folder; +} + +// Render icon by name - useful for dynamic rendering +export function DynamicIcon({ + name, + size = 16, + className, + style +}: { + name: string; + size?: number; + className?: string; + style?: React.CSSProperties; +}) { + const IconComponent = getIconByName(name); + return ; +} + +interface LucideIconPickerProps { + selectedIcon: string; + onSelect: (iconName: string) => void; + onClose?: () => void; +} + +export default function LucideIconPicker({ selectedIcon, onSelect, onClose }: LucideIconPickerProps) { + const [searchQuery, setSearchQuery] = useState(''); + + const iconNames = Object.keys(ICON_REGISTRY); + + const filteredIcons = useMemo(() => { + if (!searchQuery.trim()) return iconNames; + const query = searchQuery.toLowerCase(); + return iconNames.filter(name => name.toLowerCase().includes(query)); + }, [searchQuery, iconNames]); + + return ( +
+ {/* Search input */} +
+ + setSearchQuery(e.target.value)} + placeholder="Search icons..." + style={{ + width: '100%', + padding: '8px 12px 8px 32px', + background: '#111', + border: '1px solid #333', + borderRadius: '6px', + color: '#e5e5e5', + fontSize: '13px', + outline: 'none' + }} + onFocus={(e) => { + e.currentTarget.style.borderColor = '#22c55e'; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = '#333'; + }} + /> +
+ + {/* Icon grid */} +
+ {filteredIcons.map((iconName) => { + const IconComponent = ICON_REGISTRY[iconName]; + const isSelected = selectedIcon === iconName; + + return ( + + ); + })} +
+ + {filteredIcons.length === 0 && ( +
+ No icons match "{searchQuery}" +
+ )} +
+ ); +} diff --git a/src/components/nodes/FolderViewOverlay.tsx b/src/components/nodes/FolderViewOverlay.tsx index c46b934..dce578d 100644 --- a/src/components/nodes/FolderViewOverlay.tsx +++ b/src/components/nodes/FolderViewOverlay.tsx @@ -1,20 +1,29 @@ "use client"; import { useEffect, useMemo, useState, useRef, type DragEvent } from 'react'; -import { Folder, Check, X, ArrowLeft, Plus, Trash2, Edit2, LayoutGrid, List, Columns3, Save, Filter, ChevronDown } from 'lucide-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[]; + filters: string[]; // Legacy: simple dimension names + columnFilters?: ColumnFilter[]; // NEW: columns with optional secondary filters viewMode: DimensionViewMode; kanbanColumns?: { dimension: string; order: number }[]; createdAt: string; @@ -79,7 +88,8 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o // Filter/View system state const [overlayMode, setOverlayMode] = useState('folders'); - const [selectedFilters, setSelectedFilters] = useState([]); + // 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); @@ -89,6 +99,16 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o 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); @@ -100,6 +120,15 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o // 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); @@ -185,38 +214,79 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o }; // Filter helper functions - const addFilter = (dimension: string) => { - if (!selectedFilters.includes(dimension)) { - setSelectedFilters([...selectedFilters, dimension]); - } + // 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) => { - setSelectedFilters(selectedFilters.filter(f => f !== dimension)); + const idx = columns.findIndex(c => c.dimension === dimension); + if (idx !== -1) { + setColumns(columns.filter((_, i) => i !== idx)); + } }; const clearFilters = () => { - setSelectedFilters([]); + setColumns([]); setActiveViewId(null); }; - // Kanban column reorder handler - const handleColumnReorder = (draggedDim: string, targetDim: string) => { - if (draggedDim === targetDim) return; + // Column filter helper functions (per-column AND filters) + const getColumn = (columnId: string): ColumnFilter | undefined => { + return columns.find(c => c.id === columnId); + }; - const currentFilters = [...selectedFilters]; - const draggedIndex = currentFilters.indexOf(draggedDim); - const targetIndex = currentFilters.indexOf(targetDim); + 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 - currentFilters.splice(draggedIndex, 1); - currentFilters.splice(targetIndex, 0, draggedDim); + const [draggedItem] = currentColumns.splice(draggedIndex, 1); + currentColumns.splice(targetIndex, 0, draggedItem); - setSelectedFilters(currentFilters); + setColumns(currentColumns); }; // Kanban drag-and-drop handler @@ -258,7 +328,8 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o const newView: SavedView = { id: `view-${Date.now()}`, name: name.trim(), - filters: [...selectedFilters], + filters: selectedFilters, // Legacy: unique dimensions + columnFilters: columns.length > 0 ? [...columns] : undefined, viewMode, kanbanColumns: viewMode === 'kanban' ? [...kanbanColumns] : undefined, createdAt: new Date().toISOString() @@ -270,7 +341,16 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o const loadSavedView = (view: SavedView) => { setOverlayMode('filtered'); - setSelectedFilters(view.filters); + // 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); @@ -332,18 +412,18 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o setReorderDropIndex(null); }; - // Get nodes grouped by dimension for list view - const getNodesGroupedByDimension = () => { - const groups: { dimension: string; nodes: Node[] }[] = []; - for (const dim of selectedFilters) { - const nodesInDim = filteredNodes.filter(n => n.dimensions?.includes(dim)); - if (nodesInDim.length > 0) { - // Sort by dimension order - const sortedNodes = sortNodesByDimensionOrder(nodesInDim, dim); - groups.push({ dimension: dim, nodes: sortedNodes }); - } - } - return groups; + // 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(() => { @@ -641,6 +721,56 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o 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); @@ -769,7 +899,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o } }} > - {/* Folder icon */} + {/* Dimension icon */}
- +
{/* Name and count */} @@ -824,6 +958,28 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o {/* Action buttons - show on hover via CSS would be ideal, but inline for now */}
+ + {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.dimension, index }); + setReorderDrag({ nodeId: node.id, dimension: group.column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { @@ -2163,14 +2446,14 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o }} onDragOver={(e) => { e.preventDefault(); - if (reorderDrag && reorderDrag.dimension === group.dimension && reorderDrag.nodeId !== node.id) { + if (reorderDrag && reorderDrag.dimension === group.column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} onDrop={(e) => { e.preventDefault(); - if (reorderDrag && reorderDrag.dimension === group.dimension) { - handleReorderDrop(group.dimension, reorderDrag.index, index, group.nodes); + if (reorderDrag && reorderDrag.dimension === group.column.dimension) { + handleReorderDrop(group.column.dimension, reorderDrag.index, index, group.nodes); } }} onClick={() => { @@ -2185,7 +2468,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o padding: '8px 10px', marginBottom: '2px', background: reorderDrag?.nodeId === node.id ? '#1a2a1f' : 'transparent', - border: reorderDropIndex === index && reorderDrag?.dimension === group.dimension && reorderDrag?.nodeId !== node.id + border: reorderDropIndex === index && reorderDrag?.dimension === group.column.dimension && reorderDrag?.nodeId !== node.id ? '1px dashed #22c55e' : '1px solid transparent', borderRadius: '6px', @@ -2241,7 +2524,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o {node.dimensions && node.dimensions.length > 1 && (
{node.dimensions - .filter(d => d !== group.dimension) + .filter((d: string) => d !== group.column.dimension) .slice(0, 2) .map(dim => ( {groups.map(group => ( -
+
- {group.dimension} ({group.nodes.length}) + {/* 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) => ( + {group.nodes.map((node: Node, index: number) => (
{ - setReorderDrag({ nodeId: node.id, dimension: group.dimension, index }); + setReorderDrag({ nodeId: node.id, dimension: group.column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { @@ -2308,14 +2718,14 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o }} onDragOver={(e) => { e.preventDefault(); - if (reorderDrag && reorderDrag.dimension === group.dimension && reorderDrag.nodeId !== node.id) { + if (reorderDrag && reorderDrag.dimension === group.column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} onDrop={(e) => { e.preventDefault(); - if (reorderDrag && reorderDrag.dimension === group.dimension) { - handleReorderDrop(group.dimension, reorderDrag.index, index, group.nodes); + if (reorderDrag && reorderDrag.dimension === group.column.dimension) { + handleReorderDrop(group.column.dimension, reorderDrag.index, index, group.nodes); } }} onClick={() => { @@ -2324,7 +2734,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o }} style={{ background: reorderDrag?.nodeId === node.id ? '#1a2a1f' : '#0a0a0a', - border: reorderDropIndex === index && reorderDrag?.dimension === group.dimension && reorderDrag?.nodeId !== node.id + border: reorderDropIndex === index && reorderDrag?.dimension === group.column.dimension && reorderDrag?.nodeId !== node.id ? '2px dashed #22c55e' : '1px solid #161616', borderRadius: '10px', @@ -2398,7 +2808,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o )} {node.dimensions && node.dimensions.length > 1 && (
- {node.dimensions.filter(d => d !== group.dimension).slice(0, 2).map((dim) => ( + {node.dimensions.filter((d: string) => d !== group.column.dimension).slice(0, 2).map((dim: string) => ( - {selectedFilters.map(dimension => { - const unsortedColumnNodes = filteredNodes.filter(n => n.dimensions?.includes(dimension)); - const columnNodes = sortNodesByDimensionOrder(unsortedColumnNodes, dimension); - const isDropTarget = dropTargetDimension === dimension; - const isColumnDropTarget = columnDropTarget === dimension && draggedColumn !== dimension; - const isBeingDragged = draggedColumn === dimension; + {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(dimension); + setColumnDropTarget(column.id); } else { - setDropTargetDimension(dimension); + setDropTargetDimension(column.id); } }} onDragLeave={(e) => { @@ -2463,14 +2875,14 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o onDrop={(e) => { e.preventDefault(); // Handle column reorder - if (draggedColumn && draggedColumn !== dimension) { - handleColumnReorder(draggedColumn, dimension); + if (draggedColumn && draggedColumn !== column.id) { + handleColumnReorder(draggedColumn, column.id); setDraggedColumn(null); setColumnDropTarget(null); } // Handle node drop - else if (draggedNode && draggedNode.fromDimension !== dimension) { - handleKanbanDrop(draggedNode.id, draggedNode.fromDimension, dimension); + else if (draggedNode && draggedNode.fromDimension !== column.dimension) { + handleKanbanDrop(draggedNode.id, draggedNode.fromDimension, column.dimension); } setDraggedNode(null); setDropTargetDimension(null); @@ -2480,8 +2892,8 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o minWidth: '280px', display: 'flex', flexDirection: 'column', - background: isColumnDropTarget ? '#1a1a2a' : (isDropTarget ? '#0d1a12' : '#0a0a0a'), - border: isColumnDropTarget ? '1px solid #6366f1' : (isDropTarget ? '1px solid #22c55e' : '1px solid #1a1a1a'), + 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 @@ -2491,7 +2903,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
{ - setDraggedColumn(dimension); + setDraggedColumn(column.id); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { @@ -2508,7 +2920,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o userSelect: 'none' }} > -
+
- {dimension} + {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 === dimension && reorderDrag?.nodeId !== node.id; + const isReorderTarget = reorderDropIndex === index && reorderDrag?.dimension === column.dimension && reorderDrag?.nodeId !== node.id; return (
{ - setDraggedNode({ id: node.id, fromDimension: dimension }); - setReorderDrag({ nodeId: node.id, dimension, index }); + setDraggedNode({ id: node.id, fromDimension: column.dimension }); + setReorderDrag({ nodeId: node.id, dimension: column.dimension, index }); e.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { @@ -2556,7 +3070,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o e.preventDefault(); e.stopPropagation(); // If dragging within same column, show reorder indicator - if (reorderDrag && reorderDrag.dimension === dimension && reorderDrag.nodeId !== node.id) { + if (reorderDrag && reorderDrag.dimension === column.dimension && reorderDrag.nodeId !== node.id) { setReorderDropIndex(index); } }} @@ -2564,8 +3078,8 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o e.preventDefault(); e.stopPropagation(); // Handle within-column reorder - if (reorderDrag && reorderDrag.dimension === dimension && reorderDrag.nodeId !== node.id) { - handleReorderDrop(dimension, reorderDrag.index, index, columnNodes); + if (reorderDrag && reorderDrag.dimension === column.dimension && reorderDrag.nodeId !== node.id) { + handleReorderDrop(column.dimension, reorderDrag.index, index, columnNodes); } }} onClick={() => { @@ -2628,7 +3142,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o {node.dimensions && node.dimensions.length > 1 && (
{node.dimensions - .filter(d => d !== dimension) + .filter((d: string) => d !== column.dimension) .slice(0, 2) .map(dim => ( - + Folders {showFilterPicker && ( <> @@ -3107,13 +3641,12 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
{dimensions .filter(d => - d.dimension.toLowerCase().includes(filterSearchQuery.toLowerCase()) && - !selectedFilters.includes(d.dimension) + d.dimension.toLowerCase().includes(filterSearchQuery.toLowerCase()) ) .map(dim => ( +
+ + {/* Content */} +
+ {/* Description */} +
+ +