From 94acb2638e1857653a882167d8540749f14f93b0 Mon Sep 17 00:00:00 2001 From: Sandro Date: Tue, 27 Oct 2026 20:26:45 +0100 Subject: [PATCH] fix(workspace): fix outliner cursor focus, enhanced task linking, nav order and Hauptebene drop zone --- frontend/src/App.tsx | 102 +++++----- frontend/src/features/triage/TasksView.tsx | 18 +- .../src/features/workspace/DocumentEditor.tsx | 45 ++++- .../src/features/workspace/OutlinerBlock.tsx | 186 +++++++++++------- .../src/features/workspace/OutlinerTree.tsx | 98 ++++++--- .../src/features/workspace/WorkspaceView.tsx | 28 +-- internal/storage/sqlite.go | 5 +- 7 files changed, 300 insertions(+), 182 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c3c0e2..0a2d73f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -128,6 +128,7 @@ export const App: React.FC = () => { const [previousTab, setPreviousTab] = useState('triage'); const [selectedDocId, setSelectedDocId] = useState(null); const [selectedBlockId, setSelectedBlockId] = useState(null); + const [selectedTaskId, setSelectedTaskId] = useState(null); const [captureFocusCount, setCaptureFocusCount] = useState(0); const [refreshRevision, setRefreshRevision] = useState(0); @@ -250,13 +251,6 @@ export const App: React.FC = () => { id: 'triage-nav', title: 'Bereiche wechseln', items: [ - { - id: 'goto-tasks', - label: 'Aufgaben & Kanban', - icon: , - badge: stats.tasksOpenCount, - onClick: () => setActiveTab('tasks'), - }, { id: 'goto-workspace', label: 'Notizen & Workspace', @@ -264,6 +258,13 @@ export const App: React.FC = () => { badge: stats.docsCount, onClick: () => setActiveTab('workspace'), }, + { + id: 'goto-tasks', + label: 'Aufgaben & Kanban', + icon: , + badge: stats.tasksOpenCount, + onClick: () => setActiveTab('tasks'), + }, { id: 'goto-lists', label: 'Einkaufslisten', @@ -298,18 +299,18 @@ export const App: React.FC = () => { variant: 'accent', onClick: () => setActiveTab('triage'), }, - { - id: 'goto-tasks', - label: 'Zu den Aufgaben', - icon: , - onClick: () => setActiveTab('tasks'), - }, { id: 'goto-workspace', label: 'Zum Workspace', icon: , onClick: () => setActiveTab('workspace'), }, + { + id: 'goto-tasks', + label: 'Zu den Aufgaben', + icon: , + onClick: () => setActiveTab('tasks'), + }, { id: 'goto-lists', label: 'Zu den Listen', @@ -556,6 +557,11 @@ export const App: React.FC = () => { setActiveTab('workspace'); }; + const handleOpenTask = useCallback((taskId: string) => { + setSelectedTaskId(taskId); + setActiveTab('tasks'); + }, []); + const handleTriggerAddButton = () => { triggerHaptic(20); if (window.innerWidth < 640) { @@ -874,24 +880,6 @@ export const App: React.FC = () => { - - + + - {/* Col 2: Aufgaben */} + {/* Col 2: Workspace */} {/* Col 3 (EXACT CENTER): + Quick-Add FAB */} @@ -1079,27 +1087,27 @@ export const App: React.FC = () => { - {/* Col 4: Workspace */} + {/* Col 4: Aufgaben */} {/* Col 5: Listen */} diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index b2bf88b..a26d6b1 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -48,6 +48,7 @@ interface TasksViewProps { onRefreshStats: () => void; onOpenDocument: (docId: string, blockId?: string) => void; refreshTrigger?: number; + initialSelectedTaskId?: string | null; } export const TasksView: React.FC = ({ @@ -55,6 +56,7 @@ export const TasksView: React.FC = ({ onRefreshStats, onOpenDocument, refreshTrigger = 0, + initialSelectedTaskId, }) => { const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open'); @@ -101,6 +103,18 @@ export const TasksView: React.FC = ({ // Detail modal state const [detailTaskId, setDetailTaskId] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); + const [extraTask, setExtraTask] = useState(null); + + useEffect(() => { + if (initialSelectedTaskId) { + setDetailTaskId(initialSelectedTaskId); + setShowDetailModal(true); + setFilter('open'); + api.getThought(initialSelectedTaskId).then((th) => { + if (th) setExtraTask(th); + }).catch(() => {}); + } + }, [initialSelectedTaskId]); // Manage columns modal state const [showManageColumnsModal, setShowManageColumnsModal] = useState(false); @@ -243,8 +257,8 @@ export const TasksView: React.FC = ({ const detailTask = useMemo(() => { if (!detailTaskId) return null; - return tasks.find((t) => t.id === detailTaskId) || null; - }, [detailTaskId, tasks]); + return tasks.find((t) => t.id === detailTaskId) || (extraTask?.id === detailTaskId ? extraTask : null); + }, [detailTaskId, tasks, extraTask]); const safeTeams = Array.isArray(teams) ? teams : []; const safeCollaborators = Array.isArray(collaborators) ? collaborators : []; diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index bca2245..28f7657 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -17,7 +17,7 @@ import { } from 'lucide-react'; import { Document, DocumentBlock, User } from '../../lib/types'; import { api, getToken } from '../../lib/api'; -import { OutlinerTree } from './OutlinerTree'; +import { OutlinerTree, OutlinerTreeHandle } from './OutlinerTree'; export type DocSaveStatus = 'saved' | 'saving' | 'unsaved' | 'error'; @@ -34,6 +34,7 @@ export interface DocumentEditorProps { onSelectTag: (tag: string) => void; onWikiLinkClick?: (pageTitle: string) => void; onBackMobile?: () => void; + onOpenTask?: (taskId: string) => void; onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void; } @@ -209,6 +210,7 @@ export const DocumentEditor: React.FC = ({ onSelectTag, onWikiLinkClick, onBackMobile, + onOpenTask, onSaveStatusChange, }) => { const [doc, setDoc] = useState(initialDoc || null); @@ -239,6 +241,7 @@ export const DocumentEditor: React.FC = ({ const isInitialLoadRef = useRef(true); const titleInputRef = useRef(null); + const outlinerTreeRef = useRef(null); const docRef = useRef(doc); docRef.current = doc; @@ -492,13 +495,20 @@ export const DocumentEditor: React.FC = ({ }; const handleUpdateBlock = useCallback( - (updatedBlock: DocumentBlock) => { + (updatedBlock: DocumentBlock, immediate: boolean = false) => { if (!canEdit) return; - setBlocks((prev) => - prev.map((b) => (b.id === updatedBlock.id ? updatedBlock : b)) - ); + setBlocks((prev) => { + const next = prev.map((b) => (b.id === updatedBlock.id ? updatedBlock : b)); + currentBlocksRef.current = next; + return next; + }); + if (immediate) { + window.setTimeout(() => { + executeAutoSave(true); + }, 10); + } }, - [canEdit] + [canEdit, executeAutoSave] ); const handleUpdateBlocks = useCallback( @@ -939,6 +949,7 @@ export const DocumentEditor: React.FC = ({ onCreateBlock={handleCreateBlock} onDeleteBlock={handleDeleteBlock} onMoveBlock={handleMoveBlock} + onOpenTask={onOpenTask} onTagClick={onSelectTag} onWikiLinkClick={onWikiLinkClick} initialFocusedBlockId={initialFocusedBlockId} @@ -977,6 +988,20 @@ export const DocumentEditor: React.FC = ({ className="input-field w-full pl-7 pr-2 py-1 text-xs" /> + {doc?.icon && ( + + )} {emojiSearch.trim() && ( )} - - {block.taskId && ( - - - Erweitert - - )} {/* Main Block Content */} @@ -589,23 +591,101 @@ export const OutlinerBlock: React.FC = ({ ) : (
- {!isFocused && localContent && ( -
- {renderInlineMarkdown(localContent)} -
- )} + {/* View Layer with Inline Markdown & Actions (3-dots or Erweitert Badge) directly behind text */} +
{ + if (!activeFocus && canEdit) { + textareaRef.current?.focus(); + } + }} + className={`w-full leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${ + activeFocus ? 'absolute inset-0 pointer-events-none' : 'relative cursor-text' + } ${ + block.type === 'h1' + ? 'text-lg font-bold text-main tracking-tight' + : block.type === 'h2' + ? 'text-base font-semibold text-main' + : block.type === 'h3' + ? 'text-sm font-semibold text-main' + : block.type === 'todo' && block.checked + ? 'text-sm text-faint line-through' + : 'text-sm text-main' + }`} + > + {/* When actively focused, ghost text is invisible so textarea renders text/caret directly */} + + {localContent ? renderInlineMarkdown(localContent) : ''} + + + {/* Inline Badge or 3-Dots directly behind the text */} + {block.type === 'todo' && ( + + {block.taskId ? ( + + ) : canEdit ? ( + + + {showTodoMenu && ( +
e.stopPropagation()} + > + +
+ )} +
+ ) : null} +
+ )} +
+