diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index d8c8409..1bbd009 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -106,6 +106,9 @@ export const WorkspaceView: React.FC = ({ const [workspaceTags, setWorkspaceTags] = useState([]); const [isLoadingTag, setIsLoadingTag] = useState(false); const [tagSearchQuery, setTagSearchQuery] = useState(''); + const [tagNoteContent, setTagNoteContent] = useState(''); + const [tagNoteSaveState, setTagNoteSaveState] = useState<'saved' | 'saving'>('saved'); + const tagNoteSaveTimerRef = useRef(null); // Collapsible sidebar sections const [isPinnedCollapsed, setIsPinnedCollapsed] = useState(false); @@ -412,12 +415,18 @@ export const WorkspaceView: React.FC = ({ const openTagPage = async (tag: string) => { const clean = tag.replace(/^#/, '').toLowerCase(); + if (tagNoteSaveTimerRef.current) { + clearTimeout(tagNoteSaveTimerRef.current); + tagNoteSaveTimerRef.current = null; + } setSelectedTag(clean); setWorkspaceMode('tags'); setIsLoadingTag(true); try { const details = await api.getTagDetails(clean); setTagDetails(details); + setTagNoteContent(details.noteContent || ''); + setTagNoteSaveState('saved'); } catch (err) { console.error('Failed to load tag details', err); } finally { @@ -425,6 +434,40 @@ export const WorkspaceView: React.FC = ({ } }; + const handleTagNoteChange = (newContent: string) => { + setTagNoteContent(newContent); + if (!selectedTag) return; + const targetTag = selectedTag; + setTagNoteSaveState('saving'); + if (tagNoteSaveTimerRef.current) { + clearTimeout(tagNoteSaveTimerRef.current); + } + tagNoteSaveTimerRef.current = setTimeout(async () => { + try { + await api.updateTagPageNote(targetTag, newContent); + setTagDetails((prev) => + prev && prev.tag === targetTag ? { ...prev, noteContent: newContent } : prev + ); + setTagNoteSaveState('saved'); + } catch (err) { + console.error('Failed to save tag note', err); + setTagNoteSaveState('saved'); + } + }, 450); + }; + + const handleExportTagMarkdown = () => { + if (!selectedTag) return; + const md = `# #${selectedTag}\n\n${tagNoteContent || ''}\n`; + const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `tag-${selectedTag}.md`; + a.click(); + URL.revokeObjectURL(url); + }; + useEffect(() => { loadDocs(initialDocId); }, [initialDocId]); @@ -3605,43 +3648,45 @@ export const WorkspaceView: React.FC = ({
{/* Header Toolbar */}
+ {selectedTag && ( +
+ +
+ )} +
-
+
{selectedTag ? ( <> - -
-
-
- -
-
-

- #{selectedTag} -

-

- {isLoadingTag - ? 'Wird geladen…' - : `${tagDetails?.totalMatches || 0} ${ - tagDetails?.totalMatches === 1 ? 'Abschnitt' : 'Abschnitte' - } in ${tagDetails?.documents.length || 0} ${ - tagDetails?.documents.length === 1 ? 'Notiz' : 'Notizen' - }`} -

-
+
+ +
+
+

+ #{selectedTag} +

+

+ {isLoadingTag + ? 'Wird geladen…' + : `${tagDetails?.totalMatches || 0} ${ + tagDetails?.totalMatches === 1 ? 'Abschnitt' : 'Abschnitte' + } in ${tagDetails?.documents.length || 0} ${ + tagDetails?.documents.length === 1 ? 'Notiz' : 'Notizen' + }`} +

) : ( -
-
+ <> +
@@ -3650,7 +3695,7 @@ export const WorkspaceView: React.FC = ({ {workspaceTags.length} {workspaceTags.length === 1 ? 'Thema' : 'Themen'} im gesamten Workspace

-
+ )}
@@ -3760,147 +3805,203 @@ export const WorkspaceView: React.FC = ({ Hashtag-Verknüpfungen werden geladen…
- ) : !tagDetails || tagDetails.documents.length === 0 ? ( -
- -

Keine Bullet Points mit #{selectedTag} gefunden

-

- Füge einfach in irgendeiner Notiz #{selectedTag} in einen Bullet Point ein. -

-
) : (
- {tagDetails.documents.map((docGroup) => ( -
- {/* Document Header */} -
-
- {docGroup.icon || '📄'} - {docGroup.title || 'Unbenannt'} - {docGroup.folderPath && ( - - - {docGroup.folderPath} - - )} - - • {docGroup.matchedBlocks.length}{' '} - {docGroup.matchedBlocks.length === 1 ? 'Treffer' : 'Treffer'} - -
- + {/* Dedicated Logseq-Style Hashtag Markdown Note Card (#tag.md) */} +
+
+
+ + + Eigene Notiz zu #{selectedTag} + + + #{selectedTag}.md + +
+
+ + {tagNoteSaveState === 'saving' ? 'Speichert…' : 'Gespeichert'} +
+
- {/* Matched Bullet Points and their children */} -
- {docGroup.matchedBlocks.map((mb, idx) => ( -
- {/* Root Bullet Point */} -
- {mb.rootBlock.type === 'todo' ? ( - handleToggleRollupTodo(docGroup.docId, mb.rootBlock.id)} - className="w-4 h-4 mt-0.5 checkbox-square cursor-pointer flex-shrink-0" - /> - ) : ( -
-
-
+