Dev #7
No files matched your search
@@ -26,6 +26,8 @@ export interface DocumentEditorProps {
|
||||
currentUser?: User | null;
|
||||
initialDoc?: Document | null;
|
||||
availableFolders: string[];
|
||||
autoFocusTitle?: boolean;
|
||||
initialFocusedBlockId?: string | null;
|
||||
onDocMetadataChange: (updated: Document) => void;
|
||||
onDeleteDoc: (docId: string) => void;
|
||||
onOpenShareModal: (doc: Document) => void;
|
||||
@@ -143,6 +145,8 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
currentUser,
|
||||
initialDoc,
|
||||
availableFolders,
|
||||
autoFocusTitle,
|
||||
initialFocusedBlockId,
|
||||
onDocMetadataChange,
|
||||
onDeleteDoc,
|
||||
onOpenShareModal,
|
||||
@@ -161,6 +165,18 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
const [showFolderMenu, setShowFolderMenu] = useState<boolean>(false);
|
||||
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(() => {
|
||||
if (zoomedBlockId && !blocks.some((b) => b.id === zoomedBlockId)) {
|
||||
setZoomedBlockId(null);
|
||||
@@ -932,6 +948,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onTagClick={onSelectTag}
|
||||
onWikiLinkClick={onWikiLinkClick}
|
||||
initialFocusedBlockId={initialFocusedBlockId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -941,12 +958,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
========================================================================= */
|
||||
<>
|
||||
{/* 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">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{doc?.icon || '📝'}
|
||||
@@ -958,7 +975,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
key={emoji}
|
||||
type="button"
|
||||
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}
|
||||
</button>
|
||||
@@ -979,23 +996,6 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
/>
|
||||
</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 */}
|
||||
<OutlinerTree
|
||||
docId={docId}
|
||||
@@ -1011,6 +1011,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onTagClick={onSelectTag}
|
||||
onWikiLinkClick={onWikiLinkClick}
|
||||
initialFocusedBlockId={initialFocusedBlockId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -111,22 +111,29 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
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]">
|
||||
{/* Modal Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border bg-panel">
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkIcon className="w-4 h-4 text-accent" />
|
||||
<h3 className="text-sm font-bold text-main">Link einfügen</h3>
|
||||
<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">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-border">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-8 h-8 rounded-btn bg-accent/15 text-accent flex items-center justify-center shrink-0">
|
||||
<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>
|
||||
<button
|
||||
type="button"
|
||||
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)"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
@@ -134,14 +141,14 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-accent text-on-accent shadow-sm'
|
||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||
? 'bg-panel-elevated text-main shadow-xs'
|
||||
: 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
@@ -150,10 +157,10 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-accent text-on-accent shadow-sm'
|
||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||
? 'bg-panel-elevated text-main shadow-xs'
|
||||
: 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
@@ -162,7 +169,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="p-4 flex-1 overflow-y-auto">
|
||||
<div>
|
||||
{tab === 'note' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
@@ -172,8 +179,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Notiz suchen…"
|
||||
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"
|
||||
placeholder="Notiz nach Titel oder Ordner filtern…"
|
||||
className="input-field w-full text-xs pl-8 pr-3 py-2 text-main"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -183,22 +190,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
<span>Notizen werden geladen…</span>
|
||||
</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) => (
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
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">
|
||||
<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'}
|
||||
</span>
|
||||
</div>
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -206,7 +213,7 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
))}
|
||||
|
||||
{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
|
||||
? `Keine Notiz für "${search}" gefunden.`
|
||||
: 'Keine Notizen im Workspace vorhanden.'}
|
||||
@@ -218,8 +225,8 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
) : (
|
||||
<form onSubmit={handleWebLinkSubmit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-muted mb-1">
|
||||
Link-Text / Titel
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-faint mb-1 block">
|
||||
Link-Text / Label
|
||||
</label>
|
||||
<input
|
||||
ref={webTitleInputRef}
|
||||
@@ -227,12 +234,12 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
value={webTitle}
|
||||
onChange={(e) => setWebTitle(e.target.value)}
|
||||
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>
|
||||
<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) *
|
||||
</label>
|
||||
<input
|
||||
@@ -241,22 +248,22 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
||||
value={webUrl}
|
||||
onChange={(e) => setWebUrl(e.target.value)}
|
||||
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 className="pt-2 flex justify-end gap-2">
|
||||
<div className="pt-2 flex justify-end gap-2 border-t border-border">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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" />
|
||||
<span>Link einfügen</span>
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface OutlinerBlockProps {
|
||||
onDragOverBlock?: (e: React.DragEvent, index: number) => void;
|
||||
onDropBlock?: (index: number) => void;
|
||||
isDragTarget?: boolean;
|
||||
isTargeted?: boolean;
|
||||
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
|
||||
}
|
||||
|
||||
@@ -88,6 +89,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
onDragOverBlock,
|
||||
onDropBlock,
|
||||
isDragTarget,
|
||||
isTargeted,
|
||||
registerTextareaRef,
|
||||
}) => {
|
||||
const [localContent, setLocalContent] = useState<string>(block.content || '');
|
||||
@@ -439,9 +441,11 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
||||
|
||||
return (
|
||||
<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
|
||||
? 'border-t-2 border-accent bg-accent/10'
|
||||
: isTargeted
|
||||
? 'bg-accent/15 ring-2 ring-accent'
|
||||
: isFocused
|
||||
? 'bg-panel-elevated'
|
||||
: 'hover:bg-panel'
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface OutlinerTreeProps {
|
||||
onMoveBlock: (blockId: string, newIndex: number, level?: number) => void;
|
||||
onTagClick?: (tag: string) => void;
|
||||
onWikiLinkClick?: (pageTitle: string) => void;
|
||||
initialFocusedBlockId?: string | null;
|
||||
}
|
||||
|
||||
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
||||
@@ -34,14 +35,38 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
||||
onMoveBlock,
|
||||
onTagClick,
|
||||
onWikiLinkClick,
|
||||
initialFocusedBlockId,
|
||||
}) => {
|
||||
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
|
||||
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
|
||||
initialFocusedBlockId || null
|
||||
);
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
|
||||
const textareaMapRef = useRef<Map<string, HTMLTextAreaElement>>(new Map());
|
||||
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) => {
|
||||
if (el) {
|
||||
textareaMapRef.current.set(blockId, el);
|
||||
@@ -304,6 +329,7 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
|
||||
onDragOverBlock={handleDragOverBlock}
|
||||
onDropBlock={handleDropBlock}
|
||||
isDragTarget={dragOverIndex === index && draggedIndex !== index}
|
||||
isTargeted={highlightedBlockId === block.id}
|
||||
registerTextareaRef={registerTextareaRef}
|
||||
/>
|
||||
))}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2404,11 +2404,18 @@ func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
|
||||
JOIN team_members tm ON tm.team_id = dts2.team_id
|
||||
WHERE tm.user_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
|
||||
GROUP BY t.tag
|
||||
ORDER BY count DESC, t.tag ASC`,
|
||||
userID, userID, userID, userID,
|
||||
userID, userID, userID, userID, userID, userID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2460,7 +2467,7 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
|
||||
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,
|
||||
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
|
||||
JOIN documents d ON t.document_id = d.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
|
||||
WHERE tm.user_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 d.is_archived = 0
|
||||
ORDER BY d.updated_at DESC`,
|
||||
userID, userID, userID, userID, cleanTag,
|
||||
userID, userID, userID, userID, userID, userID, userID, cleanTag,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in new issue
Block a user