fix(workspace): fix outliner cursor focus, enhanced task linking, nav order and Hauptebene drop zone
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 19s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 19s
This commit is contained in:
1 parent
d39d1d1122
commit
94acb2638e
7 files changed
+280
-162
No files matched your search
+55
-47
@@ -128,6 +128,7 @@ export const App: React.FC = () => {
|
|||||||
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
|
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
|
||||||
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
|
||||||
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
||||||
|
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
|
||||||
const [captureFocusCount, setCaptureFocusCount] = useState(0);
|
const [captureFocusCount, setCaptureFocusCount] = useState(0);
|
||||||
const [refreshRevision, setRefreshRevision] = useState(0);
|
const [refreshRevision, setRefreshRevision] = useState(0);
|
||||||
|
|
||||||
@@ -250,13 +251,6 @@ export const App: React.FC = () => {
|
|||||||
id: 'triage-nav',
|
id: 'triage-nav',
|
||||||
title: 'Bereiche wechseln',
|
title: 'Bereiche wechseln',
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
id: 'goto-tasks',
|
|
||||||
label: 'Aufgaben & Kanban',
|
|
||||||
icon: <CheckSquare className="w-4 h-4" />,
|
|
||||||
badge: stats.tasksOpenCount,
|
|
||||||
onClick: () => setActiveTab('tasks'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'goto-workspace',
|
id: 'goto-workspace',
|
||||||
label: 'Notizen & Workspace',
|
label: 'Notizen & Workspace',
|
||||||
@@ -264,6 +258,13 @@ export const App: React.FC = () => {
|
|||||||
badge: stats.docsCount,
|
badge: stats.docsCount,
|
||||||
onClick: () => setActiveTab('workspace'),
|
onClick: () => setActiveTab('workspace'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-tasks',
|
||||||
|
label: 'Aufgaben & Kanban',
|
||||||
|
icon: <CheckSquare className="w-4 h-4" />,
|
||||||
|
badge: stats.tasksOpenCount,
|
||||||
|
onClick: () => setActiveTab('tasks'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'goto-lists',
|
id: 'goto-lists',
|
||||||
label: 'Einkaufslisten',
|
label: 'Einkaufslisten',
|
||||||
@@ -298,18 +299,18 @@ export const App: React.FC = () => {
|
|||||||
variant: 'accent',
|
variant: 'accent',
|
||||||
onClick: () => setActiveTab('triage'),
|
onClick: () => setActiveTab('triage'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'goto-tasks',
|
|
||||||
label: 'Zu den Aufgaben',
|
|
||||||
icon: <CheckSquare className="w-4 h-4" />,
|
|
||||||
onClick: () => setActiveTab('tasks'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'goto-workspace',
|
id: 'goto-workspace',
|
||||||
label: 'Zum Workspace',
|
label: 'Zum Workspace',
|
||||||
icon: <FileText className="w-4 h-4" />,
|
icon: <FileText className="w-4 h-4" />,
|
||||||
onClick: () => setActiveTab('workspace'),
|
onClick: () => setActiveTab('workspace'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'goto-tasks',
|
||||||
|
label: 'Zu den Aufgaben',
|
||||||
|
icon: <CheckSquare className="w-4 h-4" />,
|
||||||
|
onClick: () => setActiveTab('tasks'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'goto-lists',
|
id: 'goto-lists',
|
||||||
label: 'Zu den Listen',
|
label: 'Zu den Listen',
|
||||||
@@ -556,6 +557,11 @@ export const App: React.FC = () => {
|
|||||||
setActiveTab('workspace');
|
setActiveTab('workspace');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenTask = useCallback((taskId: string) => {
|
||||||
|
setSelectedTaskId(taskId);
|
||||||
|
setActiveTab('tasks');
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleTriggerAddButton = () => {
|
const handleTriggerAddButton = () => {
|
||||||
triggerHaptic(20);
|
triggerHaptic(20);
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
@@ -874,24 +880,6 @@ export const App: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
triggerHaptic(10);
|
|
||||||
setActiveTab('tasks');
|
|
||||||
}}
|
|
||||||
className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`}
|
|
||||||
>
|
|
||||||
<CheckSquare className="w-4 h-4" />
|
|
||||||
<span className={`relative inline-flex items-center ${stats.tasksOpenCount > 0 ? 'pr-2' : ''}`}>
|
|
||||||
<span>Aufgaben</span>
|
|
||||||
{stats.tasksOpenCount > 0 && (
|
|
||||||
<span className="absolute -top-1.5 -right-1 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-sm pointer-events-none">
|
|
||||||
{stats.tasksOpenCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
@@ -910,6 +898,24 @@ export const App: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('tasks');
|
||||||
|
}}
|
||||||
|
className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`}
|
||||||
|
>
|
||||||
|
<CheckSquare className="w-4 h-4" />
|
||||||
|
<span className={`relative inline-flex items-center ${stats.tasksOpenCount > 0 ? 'pr-2' : ''}`}>
|
||||||
|
<span>Aufgaben</span>
|
||||||
|
{stats.tasksOpenCount > 0 && (
|
||||||
|
<span className="absolute -top-1.5 -right-1 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-sm pointer-events-none">
|
||||||
|
{stats.tasksOpenCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
@@ -962,6 +968,7 @@ export const App: React.FC = () => {
|
|||||||
onRefreshStats={refreshAll}
|
onRefreshStats={refreshAll}
|
||||||
onOpenDocument={handleOpenDocument}
|
onOpenDocument={handleOpenDocument}
|
||||||
refreshTrigger={refreshRevision}
|
refreshTrigger={refreshRevision}
|
||||||
|
initialSelectedTaskId={selectedTaskId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -971,6 +978,7 @@ export const App: React.FC = () => {
|
|||||||
initialDocId={selectedDocId}
|
initialDocId={selectedDocId}
|
||||||
initialBlockId={selectedBlockId}
|
initialBlockId={selectedBlockId}
|
||||||
onStatsChange={refreshAll}
|
onStatsChange={refreshAll}
|
||||||
|
onOpenTask={handleOpenTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1037,27 +1045,27 @@ export const App: React.FC = () => {
|
|||||||
<span className="text-[10px] leading-tight">Triage</span>
|
<span className="text-[10px] leading-tight">Triage</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Col 2: Aufgaben */}
|
{/* Col 2: Workspace */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
setActiveTab('tasks');
|
setActiveTab('workspace');
|
||||||
}}
|
}}
|
||||||
className={`min-h-[48px] rounded-xl flex flex-col items-center justify-center gap-0.5 transition-colors relative cursor-pointer active:scale-95 ${
|
className={`min-h-[48px] rounded-xl flex flex-col items-center justify-center gap-0.5 transition-colors relative cursor-pointer active:scale-95 ${
|
||||||
activeTab === 'tasks' ? 'text-accent font-semibold' : 'text-muted hover:text-main'
|
activeTab === 'workspace' ? 'text-accent font-semibold' : 'text-muted hover:text-main'
|
||||||
}`}
|
}`}
|
||||||
title="Aufgaben"
|
title="Workspace"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CheckSquare className="w-5 h-5" />
|
<FileText className="w-5 h-5" />
|
||||||
{stats.tasksOpenCount > 0 && (
|
{(stats.docsTodosOpenCount ?? 0) > 0 && (
|
||||||
<span className="absolute -top-1.5 -right-2.5 min-w-[16px] h-[16px] px-1 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
<span className="absolute -top-1.5 -right-2.5 min-w-[16px] h-[16px] px-1 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||||
{stats.tasksOpenCount}
|
{stats.docsTodosOpenCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] leading-tight">Aufgaben</span>
|
<span className="text-[10px] leading-tight">Workspace</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Col 3 (EXACT CENTER): + Quick-Add FAB */}
|
{/* Col 3 (EXACT CENTER): + Quick-Add FAB */}
|
||||||
@@ -1079,27 +1087,27 @@ export const App: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Col 4: Workspace */}
|
{/* Col 4: Aufgaben */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
setActiveTab('workspace');
|
setActiveTab('tasks');
|
||||||
}}
|
}}
|
||||||
className={`min-h-[48px] rounded-xl flex flex-col items-center justify-center gap-0.5 transition-colors relative cursor-pointer active:scale-95 ${
|
className={`min-h-[48px] rounded-xl flex flex-col items-center justify-center gap-0.5 transition-colors relative cursor-pointer active:scale-95 ${
|
||||||
activeTab === 'workspace' ? 'text-accent font-semibold' : 'text-muted hover:text-main'
|
activeTab === 'tasks' ? 'text-accent font-semibold' : 'text-muted hover:text-main'
|
||||||
}`}
|
}`}
|
||||||
title="Workspace"
|
title="Aufgaben"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<FileText className="w-5 h-5" />
|
<CheckSquare className="w-5 h-5" />
|
||||||
{(stats.docsTodosOpenCount ?? 0) > 0 && (
|
{stats.tasksOpenCount > 0 && (
|
||||||
<span className="absolute -top-1.5 -right-2.5 min-w-[16px] h-[16px] px-1 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
<span className="absolute -top-1.5 -right-2.5 min-w-[16px] h-[16px] px-1 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||||
{stats.docsTodosOpenCount}
|
{stats.tasksOpenCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] leading-tight">Workspace</span>
|
<span className="text-[10px] leading-tight">Aufgaben</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Col 5: Listen */}
|
{/* Col 5: Listen */}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface TasksViewProps {
|
|||||||
onRefreshStats: () => void;
|
onRefreshStats: () => void;
|
||||||
onOpenDocument: (docId: string, blockId?: string) => void;
|
onOpenDocument: (docId: string, blockId?: string) => void;
|
||||||
refreshTrigger?: number;
|
refreshTrigger?: number;
|
||||||
|
initialSelectedTaskId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TasksView: React.FC<TasksViewProps> = ({
|
export const TasksView: React.FC<TasksViewProps> = ({
|
||||||
@@ -55,6 +56,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
onRefreshStats,
|
onRefreshStats,
|
||||||
onOpenDocument,
|
onOpenDocument,
|
||||||
refreshTrigger = 0,
|
refreshTrigger = 0,
|
||||||
|
initialSelectedTaskId,
|
||||||
}) => {
|
}) => {
|
||||||
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
|
||||||
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
|
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
|
||||||
@@ -101,6 +103,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
// Detail modal state
|
// Detail modal state
|
||||||
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
|
||||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||||
|
const [extraTask, setExtraTask] = useState<Thought | null>(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
|
// Manage columns modal state
|
||||||
const [showManageColumnsModal, setShowManageColumnsModal] = useState(false);
|
const [showManageColumnsModal, setShowManageColumnsModal] = useState(false);
|
||||||
@@ -243,8 +257,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
|
|
||||||
const detailTask = useMemo(() => {
|
const detailTask = useMemo(() => {
|
||||||
if (!detailTaskId) return null;
|
if (!detailTaskId) return null;
|
||||||
return tasks.find((t) => t.id === detailTaskId) || null;
|
return tasks.find((t) => t.id === detailTaskId) || (extraTask?.id === detailTaskId ? extraTask : null);
|
||||||
}, [detailTaskId, tasks]);
|
}, [detailTaskId, tasks, extraTask]);
|
||||||
|
|
||||||
const safeTeams = Array.isArray(teams) ? teams : [];
|
const safeTeams = Array.isArray(teams) ? teams : [];
|
||||||
const safeCollaborators = Array.isArray(collaborators) ? collaborators : [];
|
const safeCollaborators = Array.isArray(collaborators) ? collaborators : [];
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Document, DocumentBlock, User } from '../../lib/types';
|
import { Document, DocumentBlock, User } from '../../lib/types';
|
||||||
import { api, getToken } from '../../lib/api';
|
import { api, getToken } from '../../lib/api';
|
||||||
import { OutlinerTree } from './OutlinerTree';
|
import { OutlinerTree, OutlinerTreeHandle } from './OutlinerTree';
|
||||||
|
|
||||||
export type DocSaveStatus = 'saved' | 'saving' | 'unsaved' | 'error';
|
export type DocSaveStatus = 'saved' | 'saving' | 'unsaved' | 'error';
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ export interface DocumentEditorProps {
|
|||||||
onSelectTag: (tag: string) => void;
|
onSelectTag: (tag: string) => void;
|
||||||
onWikiLinkClick?: (pageTitle: string) => void;
|
onWikiLinkClick?: (pageTitle: string) => void;
|
||||||
onBackMobile?: () => void;
|
onBackMobile?: () => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void;
|
onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +210,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onSelectTag,
|
onSelectTag,
|
||||||
onWikiLinkClick,
|
onWikiLinkClick,
|
||||||
onBackMobile,
|
onBackMobile,
|
||||||
|
onOpenTask,
|
||||||
onSaveStatusChange,
|
onSaveStatusChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [doc, setDoc] = useState<Document | null>(initialDoc || null);
|
const [doc, setDoc] = useState<Document | null>(initialDoc || null);
|
||||||
@@ -239,6 +241,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
const isInitialLoadRef = useRef<boolean>(true);
|
const isInitialLoadRef = useRef<boolean>(true);
|
||||||
|
|
||||||
const titleInputRef = useRef<HTMLTextAreaElement | HTMLInputElement | null>(null);
|
const titleInputRef = useRef<HTMLTextAreaElement | HTMLInputElement | null>(null);
|
||||||
|
const outlinerTreeRef = useRef<OutlinerTreeHandle | null>(null);
|
||||||
const docRef = useRef<Document | null>(doc);
|
const docRef = useRef<Document | null>(doc);
|
||||||
docRef.current = doc;
|
docRef.current = doc;
|
||||||
|
|
||||||
@@ -492,13 +495,20 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateBlock = useCallback(
|
const handleUpdateBlock = useCallback(
|
||||||
(updatedBlock: DocumentBlock) => {
|
(updatedBlock: DocumentBlock, immediate: boolean = false) => {
|
||||||
if (!canEdit) return;
|
if (!canEdit) return;
|
||||||
setBlocks((prev) =>
|
setBlocks((prev) => {
|
||||||
prev.map((b) => (b.id === updatedBlock.id ? updatedBlock : b))
|
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(
|
const handleUpdateBlocks = useCallback(
|
||||||
@@ -939,6 +949,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onCreateBlock={handleCreateBlock}
|
onCreateBlock={handleCreateBlock}
|
||||||
onDeleteBlock={handleDeleteBlock}
|
onDeleteBlock={handleDeleteBlock}
|
||||||
onMoveBlock={handleMoveBlock}
|
onMoveBlock={handleMoveBlock}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
onTagClick={onSelectTag}
|
onTagClick={onSelectTag}
|
||||||
onWikiLinkClick={onWikiLinkClick}
|
onWikiLinkClick={onWikiLinkClick}
|
||||||
initialFocusedBlockId={initialFocusedBlockId}
|
initialFocusedBlockId={initialFocusedBlockId}
|
||||||
@@ -977,6 +988,20 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
className="input-field w-full pl-7 pr-2 py-1 text-xs"
|
className="input-field w-full pl-7 pr-2 py-1 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{doc?.icon && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectIcon('');
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
setEmojiSearch('');
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-2 py-1 text-status-warn/90 hover:bg-status-warn/10 rounded text-xs font-medium flex items-center gap-1.5 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
<span>Icon entfernen</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{emojiSearch.trim() && (
|
{emojiSearch.trim() && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1034,6 +1059,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
e.target.style.height = 'auto';
|
e.target.style.height = 'auto';
|
||||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
outlinerTreeRef.current?.focusFirstBlock();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onBlur={handleTitleBlur}
|
onBlur={handleTitleBlur}
|
||||||
placeholder="Titel der Notiz…"
|
placeholder="Titel der Notiz…"
|
||||||
className="flex-1 min-w-0 text-xl sm:text-2xl font-bold text-main bg-transparent border-0 focus:outline-none focus:ring-0 tracking-tight resize-none leading-snug whitespace-pre-wrap break-words [overflow-wrap:anywhere]"
|
className="flex-1 min-w-0 text-xl sm:text-2xl font-bold text-main bg-transparent border-0 focus:outline-none focus:ring-0 tracking-tight resize-none leading-snug whitespace-pre-wrap break-words [overflow-wrap:anywhere]"
|
||||||
@@ -1042,6 +1073,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
|
|
||||||
{/* Logseq Outliner Tree */}
|
{/* Logseq Outliner Tree */}
|
||||||
<OutlinerTree
|
<OutlinerTree
|
||||||
|
ref={outlinerTreeRef}
|
||||||
docId={docId}
|
docId={docId}
|
||||||
blocks={blocks}
|
blocks={blocks}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
@@ -1052,6 +1084,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
onCreateBlock={handleCreateBlock}
|
onCreateBlock={handleCreateBlock}
|
||||||
onDeleteBlock={handleDeleteBlock}
|
onDeleteBlock={handleDeleteBlock}
|
||||||
onMoveBlock={handleMoveBlock}
|
onMoveBlock={handleMoveBlock}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
onTagClick={onSelectTag}
|
onTagClick={onSelectTag}
|
||||||
onWikiLinkClick={onWikiLinkClick}
|
onWikiLinkClick={onWikiLinkClick}
|
||||||
initialFocusedBlockId={initialFocusedBlockId}
|
initialFocusedBlockId={initialFocusedBlockId}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const SLASH_COMMANDS: SlashCommand[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface OutlinerBlockProps {
|
export interface OutlinerBlockProps {
|
||||||
|
docId: string;
|
||||||
block: DocumentBlock;
|
block: DocumentBlock;
|
||||||
index: number;
|
index: number;
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
@@ -58,6 +59,8 @@ export interface OutlinerBlockProps {
|
|||||||
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
|
onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void;
|
||||||
onIndentBlock: (blockId: string, delta: 1 | -1) => void;
|
onIndentBlock: (blockId: string, delta: 1 | -1) => void;
|
||||||
onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void;
|
onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void;
|
||||||
|
onFocusBlock?: (blockId: string) => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
onTagClick?: (tag: string) => void;
|
onTagClick?: (tag: string) => void;
|
||||||
onWikiLinkClick?: (pageTitle: string) => void;
|
onWikiLinkClick?: (pageTitle: string) => void;
|
||||||
onDragStartBlock?: (index: number) => void;
|
onDragStartBlock?: (index: number) => void;
|
||||||
@@ -69,6 +72,7 @@ export interface OutlinerBlockProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||||
|
docId,
|
||||||
block,
|
block,
|
||||||
index,
|
index,
|
||||||
isLast,
|
isLast,
|
||||||
@@ -83,6 +87,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
onDeleteBlock,
|
onDeleteBlock,
|
||||||
onIndentBlock,
|
onIndentBlock,
|
||||||
onNavigateFocus,
|
onNavigateFocus,
|
||||||
|
onFocusBlock,
|
||||||
|
onOpenTask,
|
||||||
onTagClick,
|
onTagClick,
|
||||||
onWikiLinkClick,
|
onWikiLinkClick,
|
||||||
onDragStartBlock,
|
onDragStartBlock,
|
||||||
@@ -93,6 +99,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
registerTextareaRef,
|
registerTextareaRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
||||||
|
const [isLocallyFocused, setIsLocallyFocused] = useState<boolean>(false);
|
||||||
|
const activeFocus = isFocused || isLocallyFocused;
|
||||||
const [slashSearch, setSlashSearch] = useState<string | null>(null);
|
const [slashSearch, setSlashSearch] = useState<string | null>(null);
|
||||||
const [slashIndex, setSlashIndex] = useState(0);
|
const [slashIndex, setSlashIndex] = useState(0);
|
||||||
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
|
const [showLinkModal, setShowLinkModal] = useState<boolean>(false);
|
||||||
@@ -199,7 +207,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
adjustHeight();
|
adjustHeight();
|
||||||
}, [localContent, block.type, block.level, adjustHeight]);
|
}, [localContent, block.type, block.level, activeFocus, adjustHeight]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', adjustHeight);
|
window.addEventListener('resize', adjustHeight);
|
||||||
@@ -299,10 +307,14 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
isFocusedRef.current = true;
|
isFocusedRef.current = true;
|
||||||
|
setIsLocallyFocused(true);
|
||||||
|
onFocusBlock?.(block.id);
|
||||||
|
setTimeout(adjustHeight, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
isFocusedRef.current = false;
|
isFocusedRef.current = false;
|
||||||
|
setIsLocallyFocused(false);
|
||||||
if (debounceTimerRef.current !== null) {
|
if (debounceTimerRef.current !== null) {
|
||||||
window.clearTimeout(debounceTimerRef.current);
|
window.clearTimeout(debounceTimerRef.current);
|
||||||
debounceTimerRef.current = null;
|
debounceTimerRef.current = null;
|
||||||
@@ -440,7 +452,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
? 'border-t-2 border-accent bg-accent/10'
|
? 'border-t-2 border-accent bg-accent/10'
|
||||||
: isTargeted
|
: isTargeted
|
||||||
? 'bg-accent/15 ring-2 ring-accent'
|
? 'bg-accent/15 ring-2 ring-accent'
|
||||||
: isFocused
|
: activeFocus
|
||||||
? 'bg-panel-elevated'
|
? 'bg-panel-elevated'
|
||||||
: 'hover:bg-panel'
|
: 'hover:bg-panel'
|
||||||
}`}
|
}`}
|
||||||
@@ -500,7 +512,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||||||
block.collapsed && hasChildren
|
block.collapsed && hasChildren
|
||||||
? 'bg-accent ring-2 ring-accent/30'
|
? 'bg-accent ring-2 ring-accent/30'
|
||||||
: isFocused
|
: activeFocus
|
||||||
? 'bg-accent scale-125'
|
? 'bg-accent scale-125'
|
||||||
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
|
: 'bg-faint group-hover/bullet:bg-accent group-hover/bullet:scale-125'
|
||||||
}`}
|
}`}
|
||||||
@@ -525,16 +537,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
<Check className="w-3 h-3 stroke-[3]" />
|
<Check className="w-3 h-3 stroke-[3]" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{block.taskId && (
|
|
||||||
<span
|
|
||||||
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1 py-0.2 rounded font-medium shrink-0"
|
|
||||||
title="Mit erweiterter Aufgabe synchronisiert"
|
|
||||||
>
|
|
||||||
<CheckSquare className="w-2.5 h-2.5" />
|
|
||||||
<span>Erweitert</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Block Content */}
|
{/* Main Block Content */}
|
||||||
@@ -589,9 +591,16 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
{!isFocused && localContent && (
|
{/* View Layer with Inline Markdown & Actions (3-dots or Erweitert Badge) directly behind text */}
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 cursor-text select-text pointer-events-none w-full leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
|
onClick={() => {
|
||||||
|
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'
|
block.type === 'h1'
|
||||||
? 'text-lg font-bold text-main tracking-tight'
|
? 'text-lg font-bold text-main tracking-tight'
|
||||||
: block.type === 'h2'
|
: block.type === 'h2'
|
||||||
@@ -603,9 +612,80 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
: 'text-sm text-main'
|
: 'text-sm text-main'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{renderInlineMarkdown(localContent)}
|
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
|
||||||
|
<span className={activeFocus ? 'invisible' : ''}>
|
||||||
|
{localContent ? renderInlineMarkdown(localContent) : ''}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Inline Badge or 3-Dots directly behind the text */}
|
||||||
|
{block.type === 'todo' && (
|
||||||
|
<span className="inline-flex items-center align-middle ml-1.5 gap-1 shrink-0 pointer-events-auto z-10 relative select-none">
|
||||||
|
{block.taskId ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenTask?.(block.taskId!);
|
||||||
|
}}
|
||||||
|
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
title="Zu dieser erweiterten Aufgabe wechseln"
|
||||||
|
>
|
||||||
|
<CheckSquare className="w-2.5 h-2.5" />
|
||||||
|
<span>Erweitert</span>
|
||||||
|
</button>
|
||||||
|
) : canEdit ? (
|
||||||
|
<span className="relative inline-flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowTodoMenu((prev) => !prev);
|
||||||
|
}}
|
||||||
|
className="p-0.5 text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
|
||||||
|
title="Aufgaben-Optionen"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
{showTodoMenu && (
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
setShowTodoMenu(false);
|
||||||
|
try {
|
||||||
|
const targetDocId = docId || block.documentId || block.docId || '';
|
||||||
|
const thought = await api.createEnhancedTask(targetDocId, block.id, {
|
||||||
|
content: localContent || 'Aufgabe',
|
||||||
|
checked: Boolean(block.checked),
|
||||||
|
});
|
||||||
|
const updatedBlock: DocumentBlock = {
|
||||||
|
...latestBlockRef.current,
|
||||||
|
content: localContent,
|
||||||
|
taskId: thought.id,
|
||||||
|
type: 'todo',
|
||||||
|
};
|
||||||
|
onUpdateBlockRef.current(updatedBlock, true);
|
||||||
|
onOpenTask?.(thought.id);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>In erweiterte Aufgabe umwandeln</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
textareaRef.current = el;
|
textareaRef.current = el;
|
||||||
@@ -624,7 +704,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
className={`w-full resize-none overflow-hidden bg-transparent border-0 focus:outline-none focus:ring-0 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
|
className={`w-full resize-none overflow-hidden bg-transparent border-0 focus:outline-none focus:ring-0 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
|
||||||
!isFocused && localContent ? 'text-transparent selection:text-transparent' : ''
|
activeFocus ? 'relative z-0 text-main' : 'absolute inset-0 text-transparent selection:text-transparent pointer-events-none'
|
||||||
} ${
|
} ${
|
||||||
block.type === 'h1'
|
block.type === 'h1'
|
||||||
? 'text-lg font-bold text-main tracking-tight'
|
? 'text-lg font-bold text-main tracking-tight'
|
||||||
@@ -749,50 +829,6 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Enhanced Task Option (3-dots) for To-Do Blocks */}
|
|
||||||
{canEdit && block.type === 'todo' && !block.taskId && (
|
|
||||||
<div className="relative shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowTodoMenu((prev) => !prev);
|
|
||||||
}}
|
|
||||||
className="opacity-0 group-hover:opacity-100 p-0.5 text-faint hover:text-accent rounded-btn transition-opacity cursor-pointer"
|
|
||||||
title="Aufgaben-Optionen"
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
{showTodoMenu && (
|
|
||||||
<div
|
|
||||||
className="absolute right-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
setShowTodoMenu(false);
|
|
||||||
try {
|
|
||||||
const docId = block.documentId || block.docId || '';
|
|
||||||
const thought = await api.createEnhancedTask(docId, block.id, {
|
|
||||||
content: localContent || 'Aufgabe',
|
|
||||||
checked: Boolean(block.checked),
|
|
||||||
});
|
|
||||||
flushDebounce(localContent, { taskId: thought.id });
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
|
||||||
<span>In erweiterte Aufgabe umwandeln</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Zoom / Focus button on hover */}
|
{/* Zoom / Focus button on hover */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { DocumentBlock, BlockType } from '../../lib/types';
|
import { DocumentBlock, BlockType } from '../../lib/types';
|
||||||
import { OutlinerBlock } from './OutlinerBlock';
|
import { OutlinerBlock } from './OutlinerBlock';
|
||||||
|
|
||||||
|
export interface OutlinerTreeHandle {
|
||||||
|
focusFirstBlock: () => void;
|
||||||
|
focusBlock: (blockId: string, cursor?: 'start' | 'end') => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface OutlinerTreeProps {
|
export interface OutlinerTreeProps {
|
||||||
docId: string;
|
docId: string;
|
||||||
blocks: DocumentBlock[];
|
blocks: DocumentBlock[];
|
||||||
@@ -14,12 +18,13 @@ export interface OutlinerTreeProps {
|
|||||||
onCreateBlock: (newBlock: DocumentBlock, afterBlockId?: string) => void;
|
onCreateBlock: (newBlock: DocumentBlock, afterBlockId?: string) => void;
|
||||||
onDeleteBlock: (blockId: string) => void;
|
onDeleteBlock: (blockId: string) => void;
|
||||||
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
|
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
onTagClick?: (tag: string) => void;
|
onTagClick?: (tag: string) => void;
|
||||||
onWikiLinkClick?: (pageTitle: string) => void;
|
onWikiLinkClick?: (pageTitle: string) => void;
|
||||||
initialFocusedBlockId?: string | null;
|
initialFocusedBlockId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
export const OutlinerTree = forwardRef<OutlinerTreeHandle, OutlinerTreeProps>(({
|
||||||
docId,
|
docId,
|
||||||
blocks,
|
blocks,
|
||||||
canEdit,
|
canEdit,
|
||||||
@@ -30,10 +35,11 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
onCreateBlock,
|
onCreateBlock,
|
||||||
onDeleteBlock,
|
onDeleteBlock,
|
||||||
onMoveBlock,
|
onMoveBlock,
|
||||||
|
onOpenTask,
|
||||||
onTagClick,
|
onTagClick,
|
||||||
onWikiLinkClick,
|
onWikiLinkClick,
|
||||||
initialFocusedBlockId,
|
initialFocusedBlockId,
|
||||||
}) => {
|
}, ref) => {
|
||||||
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
|
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
|
||||||
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
|
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
|
||||||
initialFocusedBlockId || null
|
initialFocusedBlockId || null
|
||||||
@@ -70,6 +76,32 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
focusFirstBlock: () => {
|
||||||
|
if (blocks.length > 0) {
|
||||||
|
focusBlockInput(blocks[0].id, 'start');
|
||||||
|
} else {
|
||||||
|
const firstBlock: DocumentBlock = {
|
||||||
|
id: `blk_${Date.now().toString(36)}_0`,
|
||||||
|
documentId: docId,
|
||||||
|
type: 'text',
|
||||||
|
content: '',
|
||||||
|
level: levelOffset || 0,
|
||||||
|
position: 1000,
|
||||||
|
};
|
||||||
|
pendingFocusRef.current = { blockId: firstBlock.id, cursor: 'start' };
|
||||||
|
onCreateBlock(firstBlock);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusBlock: (blockId: string, cursor: 'start' | 'end' = 'end') => {
|
||||||
|
focusBlockInput(blockId, cursor);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[blocks, docId, levelOffset, onCreateBlock, focusBlockInput]
|
||||||
|
);
|
||||||
|
|
||||||
const registerTextareaRef = useCallback((blockId: string, el: HTMLTextAreaElement | null) => {
|
const registerTextareaRef = useCallback((blockId: string, el: HTMLTextAreaElement | null) => {
|
||||||
if (el) {
|
if (el) {
|
||||||
textareaMapRef.current.set(blockId, el);
|
textareaMapRef.current.set(blockId, el);
|
||||||
@@ -380,6 +412,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
return (
|
return (
|
||||||
<OutlinerBlock
|
<OutlinerBlock
|
||||||
key={block.id}
|
key={block.id}
|
||||||
|
docId={docId}
|
||||||
block={block}
|
block={block}
|
||||||
index={index}
|
index={index}
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
@@ -394,6 +427,8 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
onDeleteBlock={handleDeleteBlock}
|
onDeleteBlock={handleDeleteBlock}
|
||||||
onIndentBlock={handleIndentBlock}
|
onIndentBlock={handleIndentBlock}
|
||||||
onNavigateFocus={handleNavigateFocus}
|
onNavigateFocus={handleNavigateFocus}
|
||||||
|
onFocusBlock={setFocusedBlockId}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
onTagClick={onTagClick}
|
onTagClick={onTagClick}
|
||||||
onWikiLinkClick={onWikiLinkClick}
|
onWikiLinkClick={onWikiLinkClick}
|
||||||
onDragStartBlock={handleDragStartBlock}
|
onDragStartBlock={handleDragStartBlock}
|
||||||
@@ -408,24 +443,14 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
|
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div
|
<div
|
||||||
className={`pt-2 pl-2 transition-colors ${
|
className={`min-h-[14rem] pt-2 pl-2 transition-colors cursor-text ${
|
||||||
dragOverIndex === blocks.length ? 'border-t-2 border-accent bg-accent/10 rounded' : ''
|
dragOverIndex === blocks.length ? 'border-t-2 border-accent bg-accent/10 rounded' : ''
|
||||||
}`}
|
}`}
|
||||||
onDragOver={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
if (e.target !== e.currentTarget && (e.target as HTMLElement).closest('button, a, input, textarea, select')) {
|
||||||
if (draggedIndex !== null) {
|
return;
|
||||||
setDragOverIndex(blocks.length);
|
|
||||||
}
|
}
|
||||||
}}
|
if (blocks.length === 0) {
|
||||||
onDrop={() => handleDropBlock(blocks.length)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
const lastBlock = blocks[blocks.length - 1];
|
|
||||||
if (lastBlock) {
|
|
||||||
handleCreateBlockBelow(lastBlock.id, lastBlock.content);
|
|
||||||
} else {
|
|
||||||
const firstBlock: DocumentBlock = {
|
const firstBlock: DocumentBlock = {
|
||||||
id: `blk_${Date.now().toString(36)}_0`,
|
id: `blk_${Date.now().toString(36)}_0`,
|
||||||
documentId: docId,
|
documentId: docId,
|
||||||
@@ -436,15 +461,24 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
|||||||
};
|
};
|
||||||
pendingFocusRef.current = { blockId: firstBlock.id, cursor: 'start' };
|
pendingFocusRef.current = { blockId: firstBlock.id, cursor: 'start' };
|
||||||
onCreateBlock(firstBlock);
|
onCreateBlock(firstBlock);
|
||||||
|
} else {
|
||||||
|
const lastBlock = blocks[blocks.length - 1];
|
||||||
|
if (lastBlock.content && lastBlock.content.trim() !== '') {
|
||||||
|
handleCreateBlockBelow(lastBlock.id, lastBlock.content);
|
||||||
|
} else {
|
||||||
|
focusBlockInput(lastBlock.id, 'end');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn-action inline-flex items-center gap-1.5 px-2.5 py-1 text-xs text-muted hover:text-main"
|
onDragOver={(e) => {
|
||||||
>
|
e.preventDefault();
|
||||||
<Plus className="w-3.5 h-3.5" />
|
if (draggedIndex !== null) {
|
||||||
<span>Neuer Punkt</span>
|
setDragOverIndex(blocks.length);
|
||||||
</button>
|
}
|
||||||
</div>
|
}}
|
||||||
|
onDrop={() => handleDropBlock(blocks.length)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
@@ -57,6 +57,7 @@ interface WorkspaceViewProps {
|
|||||||
initialDocId?: string | null;
|
initialDocId?: string | null;
|
||||||
initialBlockId?: string | null;
|
initialBlockId?: string | null;
|
||||||
onStatsChange: () => void;
|
onStatsChange: () => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags';
|
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags';
|
||||||
@@ -274,6 +275,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
initialDocId,
|
initialDocId,
|
||||||
initialBlockId,
|
initialBlockId,
|
||||||
onStatsChange,
|
onStatsChange,
|
||||||
|
onOpenTask,
|
||||||
}) => {
|
}) => {
|
||||||
const [docs, setDocs] = useState<Document[]>([]);
|
const [docs, setDocs] = useState<Document[]>([]);
|
||||||
const [folders, setFolders] = useState<string[]>([]);
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
@@ -1315,21 +1317,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* Scrollable Navigation Tree */}
|
{/* Scrollable Navigation Tree */}
|
||||||
<div className="flex-1 overflow-y-auto p-2 space-y-3">
|
<div className="flex-1 overflow-y-auto p-2 space-y-3">
|
||||||
{/* Top drop target for moving to root */}
|
|
||||||
{(draggingDocId || draggingFolderPath) && (
|
|
||||||
<div
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDragOverFolder('__root_top__');
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setDragOverFolder(null)}
|
|
||||||
onDrop={(e) => handleDropOnFolder('', e)}
|
|
||||||
className={`h-2.5 -mx-1 my-0.5 rounded transition-all ${
|
|
||||||
dragOverFolder === '__root_top__' ? 'bg-accent/40 ring-1 ring-accent h-4' : 'opacity-0'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pinned Notes */}
|
{/* Pinned Notes */}
|
||||||
{pinnedDocs.length > 0 && !searchQuery && (
|
{pinnedDocs.length > 0 && !searchQuery && (
|
||||||
<div>
|
<div>
|
||||||
@@ -1447,10 +1434,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}}
|
}}
|
||||||
onDragLeave={() => setDragOverFolder(null)}
|
onDragLeave={() => setDragOverFolder(null)}
|
||||||
onDrop={(e) => handleDropOnFolder('', e)}
|
onDrop={(e) => handleDropOnFolder('', e)}
|
||||||
className={`min-h-[2.5rem] flex-1 rounded transition-all ${
|
className={`p-3 rounded-card border-2 border-dashed transition-all flex items-center justify-center text-xs font-medium text-center ${
|
||||||
dragOverFolder === '__root_bottom__' ? 'bg-accent/20 ring-1 ring-accent' : ''
|
dragOverFolder === '__root_bottom__'
|
||||||
|
? 'border-accent bg-accent/20 text-accent ring-2 ring-accent/30'
|
||||||
|
: 'border-border/60 bg-panel-elevated/40 text-muted hover:border-accent/40'
|
||||||
}`}
|
}`}
|
||||||
/>
|
>
|
||||||
|
<span>Auf Hauptebene verschieben</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags Explorer */}
|
{/* Tags Explorer */}
|
||||||
@@ -1502,6 +1493,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
onSelectTag={handleSelectTag}
|
onSelectTag={handleSelectTag}
|
||||||
onWikiLinkClick={handleWikiLinkClick}
|
onWikiLinkClick={handleWikiLinkClick}
|
||||||
onBackMobile={() => setMobileShowDetail(false)}
|
onBackMobile={() => setMobileShowDetail(false)}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
onSaveStatusChange={(status, err) => {
|
onSaveStatusChange={(status, err) => {
|
||||||
setDocSaveStatus(status);
|
setDocSaveStatus(status);
|
||||||
setDocSaveError(err || null);
|
setDocSaveError(err || null);
|
||||||
|
|||||||
@@ -2364,7 +2364,7 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
|||||||
if title != nil {
|
if title != nil {
|
||||||
doc.Title = strings.TrimSpace(*title)
|
doc.Title = strings.TrimSpace(*title)
|
||||||
}
|
}
|
||||||
if icon != nil && *icon != "" {
|
if icon != nil {
|
||||||
doc.Icon = *icon
|
doc.Icon = *icon
|
||||||
}
|
}
|
||||||
if tags != nil {
|
if tags != nil {
|
||||||
@@ -4185,12 +4185,13 @@ func (s *Store) CreateTaskFromDocBlock(userID, docID, blockID, content string, c
|
|||||||
t.DocID = docID
|
t.DocID = docID
|
||||||
t.BlockID = blockID
|
t.BlockID = blockID
|
||||||
t.IsCompleted = checked
|
t.IsCompleted = checked
|
||||||
|
t.Status = domain.StatusKept
|
||||||
doneInt := 0
|
doneInt := 0
|
||||||
if checked {
|
if checked {
|
||||||
doneInt = 1
|
doneInt = 1
|
||||||
t.KanbanStatus = "done"
|
t.KanbanStatus = "done"
|
||||||
}
|
}
|
||||||
_, _ = s.db.Exec(`UPDATE thoughts SET converted_type = 'document_block', converted_ref_id = ?, is_completed = ?, kanban_status = ? WHERE id = ?`,
|
_, _ = s.db.Exec(`UPDATE thoughts SET converted_type = 'document_block', converted_ref_id = ?, is_completed = ?, kanban_status = ?, status = 'kept' WHERE id = ?`,
|
||||||
t.ConvertedRefID, doneInt, t.KanbanStatus, t.ID)
|
t.ConvertedRefID, doneInt, t.KanbanStatus, t.ID)
|
||||||
|
|
||||||
var updatedBlocks []domain.DocumentBlock
|
var updatedBlocks []domain.DocumentBlock
|
||||||
|
|||||||
Reference in new issue
Block a user