Dev #7

Merged
sandrodach merged 33 commits from dev into main 2026-10-27 11:42:12 +01:00
6 changed files with 811 additions and 232 deletions
Showing only changes of commit 4850f9dfad - Show all commits

No files matched your search

@@ -26,6 +26,8 @@ export interface DocumentEditorProps {
currentUser?: User | null; currentUser?: User | null;
initialDoc?: Document | null; initialDoc?: Document | null;
availableFolders: string[]; availableFolders: string[];
autoFocusTitle?: boolean;
initialFocusedBlockId?: string | null;
onDocMetadataChange: (updated: Document) => void; onDocMetadataChange: (updated: Document) => void;
onDeleteDoc: (docId: string) => void; onDeleteDoc: (docId: string) => void;
onOpenShareModal: (doc: Document) => void; onOpenShareModal: (doc: Document) => void;
@@ -143,6 +145,8 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
currentUser, currentUser,
initialDoc, initialDoc,
availableFolders, availableFolders,
autoFocusTitle,
initialFocusedBlockId,
onDocMetadataChange, onDocMetadataChange,
onDeleteDoc, onDeleteDoc,
onOpenShareModal, onOpenShareModal,
@@ -161,6 +165,18 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
const [showFolderMenu, setShowFolderMenu] = useState<boolean>(false); const [showFolderMenu, setShowFolderMenu] = useState<boolean>(false);
const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null); const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null);
useEffect(() => {
if (autoFocusTitle || (doc?.title === 'Neue Notiz' && !doc?.markdownCache)) {
const timer = setTimeout(() => {
if (titleInputRef.current) {
titleInputRef.current.focus();
titleInputRef.current.select();
}
}, 60);
return () => clearTimeout(timer);
}
}, [autoFocusTitle]);
useEffect(() => { useEffect(() => {
if (zoomedBlockId && !blocks.some((b) => b.id === zoomedBlockId)) { if (zoomedBlockId && !blocks.some((b) => b.id === zoomedBlockId)) {
setZoomedBlockId(null); setZoomedBlockId(null);
@@ -932,6 +948,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
onMoveBlock={handleMoveBlock} onMoveBlock={handleMoveBlock}
onTagClick={onSelectTag} onTagClick={onSelectTag}
onWikiLinkClick={onWikiLinkClick} onWikiLinkClick={onWikiLinkClick}
initialFocusedBlockId={initialFocusedBlockId}
/> />
)} )}
</div> </div>
@@ -941,12 +958,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
========================================================================= */ ========================================================================= */
<> <>
{/* Document Icon + Title Header */} {/* Document Icon + Title Header */}
<div className="flex items-center gap-3 mb-3 relative"> <div className="flex items-center gap-3 mb-4 relative">
<div className="relative"> <div className="relative">
<button <button
type="button" type="button"
onClick={() => canEdit && setShowEmojiPicker((prev) => !prev)} onClick={() => canEdit && setShowEmojiPicker((prev) => !prev)}
className="w-10 h-10 rounded-btn bg-canvas border border-border hover:border-strong flex items-center justify-center text-xl transition-colors" className="text-2xl sm:text-3xl hover:opacity-80 transition-opacity p-0.5 rounded cursor-pointer select-none"
title="Icon ändern" title="Icon ändern"
> >
{doc?.icon || '📝'} {doc?.icon || '📝'}
@@ -958,7 +975,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
key={emoji} key={emoji}
type="button" type="button"
onClick={() => handleSelectIcon(emoji)} onClick={() => handleSelectIcon(emoji)}
className="p-1.5 rounded-btn hover:bg-panel-elevated text-base flex items-center justify-center" className="p-1.5 rounded-btn hover:bg-panel-elevated text-base flex items-center justify-center cursor-pointer"
> >
{emoji} {emoji}
</button> </button>
@@ -979,23 +996,6 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
/> />
</div> </div>
{/* Extracted #Tags Summary Bar */}
{extractedTags.length > 0 && (
<div className="flex flex-wrap items-center gap-1.5 mb-4 pb-2 border-b border-border">
<Hash className="w-3.5 h-3.5 text-faint mr-0.5" />
{extractedTags.map((tag) => (
<button
key={tag}
type="button"
onClick={() => onSelectTag(tag)}
className="badge-accent text-[11px] font-medium px-2 py-0.5 rounded-btn hover:opacity-85 transition-opacity"
>
#{tag}
</button>
))}
</div>
)}
{/* Logseq Outliner Tree */} {/* Logseq Outliner Tree */}
<OutlinerTree <OutlinerTree
docId={docId} docId={docId}
@@ -1011,6 +1011,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
onMoveBlock={handleMoveBlock} onMoveBlock={handleMoveBlock}
onTagClick={onSelectTag} onTagClick={onSelectTag}
onWikiLinkClick={onWikiLinkClick} onWikiLinkClick={onWikiLinkClick}
initialFocusedBlockId={initialFocusedBlockId}
/> />
</> </>
)} )}
+38 -31
View File
@@ -111,22 +111,29 @@ export const LinkModal: React.FC<LinkModalProps> = ({
return ( return (
<div <div
className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4 backdrop-blur-xs animate-in fade-in duration-100" className="fixed inset-0 !m-0 modal-overlay z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
onClick={(e) => { onClick={(e) => {
if (e.target === e.currentTarget) onClose(); if (e.target === e.currentTarget) onClose();
}} }}
> >
<div className="panel-surface shadow-panel border border-border rounded-card max-w-md w-full overflow-hidden flex flex-col max-h-[85vh]"> <div className="panel-surface w-full max-w-lg p-5 shadow-panel rounded-card border border-border space-y-4 animate-in fade-in zoom-in-95 duration-150">
{/* Modal Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel"> <div className="flex items-center justify-between pb-3 border-b border-border">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5 min-w-0">
<LinkIcon className="w-4 h-4 text-accent" /> <div className="w-8 h-8 rounded-btn bg-accent/15 text-accent flex items-center justify-center shrink-0">
<h3 className="text-sm font-bold text-main">Link einfügen</h3> <LinkIcon className="w-4 h-4" />
</div>
<div className="min-w-0">
<h3 className="text-sm font-bold text-main truncate">Link einfügen</h3>
<p className="text-[11px] text-muted truncate">
Notiz intern verknüpfen oder externen Web-Link einbetten
</p>
</div>
</div> </div>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="p-1 rounded-btn text-muted hover:text-main hover:bg-panel-elevated transition-colors" className="p-1.5 text-muted hover:text-main rounded-btn hover:bg-panel transition-colors cursor-pointer"
title="Schließen (Esc)" title="Schließen (Esc)"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
@@ -134,14 +141,14 @@ export const LinkModal: React.FC<LinkModalProps> = ({
</div> </div>
{/* Tab Switcher */} {/* Tab Switcher */}
<div className="flex border-b border-border bg-panel-elevated/40 p-1 gap-1"> <div className="flex rounded-btn bg-panel p-0.5 border border-border">
<button <button
type="button" type="button"
onClick={() => setTab('note')} onClick={() => setTab('note')}
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-semibold flex items-center justify-center gap-1.5 transition-colors ${ className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs font-semibold rounded-btn transition-colors cursor-pointer ${
tab === 'note' tab === 'note'
? 'bg-accent text-on-accent shadow-sm' ? 'bg-panel-elevated text-main shadow-xs'
: 'text-muted hover:text-main hover:bg-panel-elevated' : 'text-muted hover:text-main'
}`} }`}
> >
<FileText className="w-3.5 h-3.5" /> <FileText className="w-3.5 h-3.5" />
@@ -150,10 +157,10 @@ export const LinkModal: React.FC<LinkModalProps> = ({
<button <button
type="button" type="button"
onClick={() => setTab('web')} onClick={() => setTab('web')}
className={`flex-1 py-1.5 px-3 rounded-btn text-xs font-semibold flex items-center justify-center gap-1.5 transition-colors ${ className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs font-semibold rounded-btn transition-colors cursor-pointer ${
tab === 'web' tab === 'web'
? 'bg-accent text-on-accent shadow-sm' ? 'bg-panel-elevated text-main shadow-xs'
: 'text-muted hover:text-main hover:bg-panel-elevated' : 'text-muted hover:text-main'
}`} }`}
> >
<Globe className="w-3.5 h-3.5" /> <Globe className="w-3.5 h-3.5" />
@@ -162,7 +169,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
</div> </div>
{/* Modal Body */} {/* Modal Body */}
<div className="p-4 flex-1 overflow-y-auto"> <div>
{tab === 'note' ? ( {tab === 'note' ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="relative"> <div className="relative">
@@ -172,8 +179,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Notiz suchen…" placeholder="Notiz nach Titel oder Ordner filtern…"
className="w-full bg-input border border-border focus:border-accent text-main text-xs pl-8 pr-3 py-1.5 rounded-btn focus:outline-none transition-colors" className="input-field w-full text-xs pl-8 pr-3 py-2 text-main"
/> />
</div> </div>
@@ -183,22 +190,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
<span>Notizen werden geladen…</span> <span>Notizen werden geladen…</span>
</div> </div>
) : ( ) : (
<div className="max-h-60 overflow-y-auto space-y-1 pr-1"> <div className="max-h-64 overflow-y-auto space-y-1 pr-1">
{filteredDocs.map((d) => ( {filteredDocs.map((d) => (
<button <button
key={d.id} key={d.id}
type="button" type="button"
onClick={() => handleSelectNote(d)} onClick={() => handleSelectNote(d)}
className="w-full text-left px-3 py-2 rounded-btn hover:bg-panel-elevated flex items-center justify-between text-xs transition-colors group" className="w-full text-left px-3 py-2 rounded-btn hover:bg-panel border border-transparent hover:border-border flex items-center justify-between text-xs transition-colors group cursor-pointer"
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="text-base shrink-0">{d.icon || '📝'}</span> <span className="text-base shrink-0">{d.icon || '📝'}</span>
<span className="font-medium text-main truncate group-hover:text-accent"> <span className="font-semibold text-main truncate group-hover:text-accent">
{d.title || 'Unbenannte Notiz'} {d.title || 'Unbenannte Notiz'}
</span> </span>
</div> </div>
{d.folderPath && ( {d.folderPath && (
<span className="text-[10px] text-faint ml-2 shrink-0 truncate max-w-[100px]"> <span className="badge-neutral text-[10px] ml-2 shrink-0 truncate max-w-[120px]">
📁 {d.folderPath} 📁 {d.folderPath}
</span> </span>
)} )}
@@ -206,7 +213,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
))} ))}
{filteredDocs.length === 0 && ( {filteredDocs.length === 0 && (
<div className="py-8 text-center text-xs text-muted"> <div className="py-8 text-center text-xs text-muted border border-dashed border-border rounded-card">
{search {search
? `Keine Notiz für "${search}" gefunden.` ? `Keine Notiz für "${search}" gefunden.`
: 'Keine Notizen im Workspace vorhanden.'} : 'Keine Notizen im Workspace vorhanden.'}
@@ -218,8 +225,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
) : ( ) : (
<form onSubmit={handleWebLinkSubmit} className="space-y-3"> <form onSubmit={handleWebLinkSubmit} className="space-y-3">
<div> <div>
<label className="block text-[11px] font-semibold text-muted mb-1"> <label className="text-[10px] font-bold uppercase tracking-wider text-faint mb-1 block">
Link-Text / Titel Link-Text / Label
</label> </label>
<input <input
ref={webTitleInputRef} ref={webTitleInputRef}
@@ -227,12 +234,12 @@ export const LinkModal: React.FC<LinkModalProps> = ({
value={webTitle} value={webTitle}
onChange={(e) => setWebTitle(e.target.value)} onChange={(e) => setWebTitle(e.target.value)}
placeholder="z. B. MindSift Dokumentation" placeholder="z. B. MindSift Dokumentation"
className="w-full bg-input border border-border focus:border-accent text-main text-xs px-3 py-1.5 rounded-btn focus:outline-none transition-colors" className="input-field w-full p-2 text-xs text-main"
/> />
</div> </div>
<div> <div>
<label className="block text-[11px] font-semibold text-muted mb-1"> <label className="text-[10px] font-bold uppercase tracking-wider text-faint mb-1 block">
Web-Adresse (URL) * Web-Adresse (URL) *
</label> </label>
<input <input
@@ -241,22 +248,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
value={webUrl} value={webUrl}
onChange={(e) => setWebUrl(e.target.value)} onChange={(e) => setWebUrl(e.target.value)}
placeholder="https://example.com" placeholder="https://example.com"
className="w-full bg-input border border-border focus:border-accent text-main text-xs px-3 py-1.5 rounded-btn focus:outline-none transition-colors" className="input-field w-full p-2 text-xs text-main"
/> />
</div> </div>
<div className="pt-2 flex justify-end gap-2"> <div className="pt-2 flex justify-end gap-2 border-t border-border">
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="btn-action px-3 py-1.5 text-xs text-muted hover:text-main" className="btn-action px-3 py-1.5 text-xs text-main cursor-pointer"
> >
Abbrechen Abbrechen
</button> </button>
<button <button
type="submit" type="submit"
disabled={!webUrl.trim()} disabled={!webUrl.trim()}
className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-on-accent rounded-btn disabled:opacity-50 transition-opacity" className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold text-on-accent rounded-btn disabled:opacity-50 transition-opacity cursor-pointer"
> >
<ExternalLink className="w-3.5 h-3.5" /> <ExternalLink className="w-3.5 h-3.5" />
<span>Link einfügen</span> <span>Link einfügen</span>
@@ -63,6 +63,7 @@ export interface OutlinerBlockProps {
onDragOverBlock?: (e: React.DragEvent, index: number) => void; onDragOverBlock?: (e: React.DragEvent, index: number) => void;
onDropBlock?: (index: number) => void; onDropBlock?: (index: number) => void;
isDragTarget?: boolean; isDragTarget?: boolean;
isTargeted?: boolean;
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void; registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
} }
@@ -88,6 +89,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
onDragOverBlock, onDragOverBlock,
onDropBlock, onDropBlock,
isDragTarget, isDragTarget,
isTargeted,
registerTextareaRef, registerTextareaRef,
}) => { }) => {
const [localContent, setLocalContent] = useState<string>(block.content || ''); const [localContent, setLocalContent] = useState<string>(block.content || '');
@@ -439,9 +441,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
return ( return (
<div <div
className={`group relative flex items-start gap-1 py-1 rounded-card transition-colors ${ className={`group relative flex items-start gap-1 py-1 rounded-card transition-all duration-300 ${
isDragTarget isDragTarget
? 'border-t-2 border-accent bg-accent/10' ? 'border-t-2 border-accent bg-accent/10'
: isTargeted
? 'bg-accent/15 ring-2 ring-accent'
: isFocused : isFocused
? 'bg-panel-elevated' ? 'bg-panel-elevated'
: 'hover:bg-panel' : 'hover:bg-panel'
@@ -18,6 +18,7 @@ export interface OutlinerTreeProps {
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void; onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
onTagClick?: (tag: string) => void; onTagClick?: (tag: string) => void;
onWikiLinkClick?: (pageTitle: string) => void; onWikiLinkClick?: (pageTitle: string) => void;
initialFocusedBlockId?: string | null;
} }
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
@@ -34,14 +35,38 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
onMoveBlock, onMoveBlock,
onTagClick, onTagClick,
onWikiLinkClick, onWikiLinkClick,
initialFocusedBlockId,
}) => { }) => {
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null); const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
initialFocusedBlockId || null
);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null); const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null); const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map()); const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map());
const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' } | null>(null); const pendingFocusRef = useRef<{ blockId: string; cursor: 'start' | 'end' } | null>(null);
useEffect(() => {
if (initialFocusedBlockId) {
setHighlightedBlockId(initialFocusedBlockId);
const timer = setTimeout(() => {
focusBlockInput(initialFocusedBlockId, 'end');
const el = textareaMapRef.current.get(initialFocusedBlockId);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 80);
const fadeTimer = setTimeout(() => {
setHighlightedBlockId(null);
}, 2500);
return () => {
clearTimeout(timer);
clearTimeout(fadeTimer);
};
}
}, [initialFocusedBlockId]);
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);
@@ -304,6 +329,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
onDragOverBlock={handleDragOverBlock} onDragOverBlock={handleDragOverBlock}
onDropBlock={handleDropBlock} onDropBlock={handleDropBlock}
isDragTarget={dragOverIndex === index && draggedIndex !== index} isDragTarget={dragOverIndex === index && draggedIndex !== index}
isTargeted={highlightedBlockId === block.id}
registerTextareaRef={registerTextareaRef} registerTextareaRef={registerTextareaRef}
/> />
))} ))}
File diff suppressed because it is too large. Load diff
+19 -5
View File
@@ -2404,11 +2404,18 @@ func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
JOIN team_members tm ON tm.team_id = dts2.team_id JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ? WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id ) dts ON d.id = dts.doc_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL) LEFT JOIN workspace_shared_folders wsf ON d.shared_folder_id = wsf.id
LEFT JOIN (
SELECT wfs2.folder_id
FROM workspace_folder_shares wfs2
WHERE (wfs2.target_type = 'user' AND wfs2.target_id = ?)
OR (wfs2.target_type = 'team' AND wfs2.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
) wfs ON d.shared_folder_id = wfs.folder_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL OR (wsf.id IS NOT NULL AND (wsf.owner_id = ? OR wfs.folder_id IS NOT NULL)))
AND d.is_archived = 0 AND d.is_archived = 0
GROUP BY t.tag GROUP BY t.tag
ORDER BY count DESC, t.tag ASC`, ORDER BY count DESC, t.tag ASC`,
userID, userID, userID, userID, userID, userID, userID, userID, userID, userID, userID,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2460,7 +2467,7 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
rows, err := s.db.Query( rows, err := s.db.Query(
`SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id, `SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id,
COALESCE(u.display_name, '') as owner_name, COALESCE(u.display_name, '') as owner_name,
COALESCE(s.permission, dts.permission, '') as shared_permission COALESCE(s.permission, dts.permission, wfs.permission, '') as shared_permission
FROM block_tags t FROM block_tags t
JOIN documents d ON t.document_id = d.id JOIN documents d ON t.document_id = d.id
LEFT JOIN users u ON d.user_id = u.id LEFT JOIN users u ON d.user_id = u.id
@@ -2471,11 +2478,18 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
JOIN team_members tm ON tm.team_id = dts2.team_id JOIN team_members tm ON tm.team_id = dts2.team_id
WHERE tm.user_id = ? WHERE tm.user_id = ?
) dts ON d.id = dts.doc_id ) dts ON d.id = dts.doc_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL) LEFT JOIN workspace_shared_folders wsf ON d.shared_folder_id = wsf.id
LEFT JOIN (
SELECT wfs2.folder_id, wfs2.permission
FROM workspace_folder_shares wfs2
WHERE (wfs2.target_type = 'user' AND wfs2.target_id = ?)
OR (wfs2.target_type = 'team' AND wfs2.target_id IN (SELECT team_id FROM team_members WHERE user_id = ?))
) wfs ON d.shared_folder_id = wfs.folder_id
WHERE (d.user_id = ? OR s.user_id = ? OR dts.doc_id IS NOT NULL OR (wsf.id IS NOT NULL AND (wsf.owner_id = ? OR wfs.folder_id IS NOT NULL)))
AND t.tag = ? AND t.tag = ?
AND d.is_archived = 0 AND d.is_archived = 0
ORDER BY d.updated_at DESC`, ORDER BY d.updated_at DESC`,
userID, userID, userID, userID, cleanTag, userID, userID, userID, userID, userID, userID, userID, cleanTag,
) )
if err != nil { if err != nil {
return nil, err return nil, err