fix(workspace): restore theme design-system, fix 5x note creation, enforce privacy isolation, fix shared write permission, add drag-drop & slash-commands
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
This commit is contained in:
1 parent
86867b0d6f
commit
85e4010435
7 files changed
+791
-490
No files matched your search
@@ -25,6 +25,7 @@ export interface DocumentEditorProps {
|
||||
onDeleteDoc: (docId: string) => void;
|
||||
onOpenShareModal: (doc: Document) => void;
|
||||
onSelectTag: (tag: string) => void;
|
||||
onWikiLinkClick?: (pageTitle: string) => void;
|
||||
onBackMobile?: () => void;
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
onDeleteDoc,
|
||||
onOpenShareModal,
|
||||
onSelectTag,
|
||||
onWikiLinkClick,
|
||||
onBackMobile,
|
||||
}) => {
|
||||
const [doc, setDoc] = useState<Document | null>(initialDoc || null);
|
||||
@@ -78,9 +80,11 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
const docRef = useRef<Document | null>(doc);
|
||||
docRef.current = doc;
|
||||
|
||||
// Crucial fix: Collaborator with write permission can edit!
|
||||
const canEdit = useMemo(() => {
|
||||
if (!doc) return true;
|
||||
return doc.isOwner !== false && doc.permission !== 'read';
|
||||
if (doc.isOwner) return true;
|
||||
return doc.permission === 'owner' || doc.permission === 'write';
|
||||
}, [doc]);
|
||||
|
||||
// Handle incoming WebSocket events for THIS document room
|
||||
@@ -400,6 +404,8 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
lines.push(`${indent}- ### ${b.content}`);
|
||||
} else if (b.type === 'code') {
|
||||
lines.push(`${indent}- \`\`\`${b.lang || ''}\n${b.content}\n${indent} \`\`\``);
|
||||
} else if (b.type === 'divider') {
|
||||
lines.push(`${indent}- ---`);
|
||||
} else {
|
||||
lines.push(`${indent}- ${b.content}`);
|
||||
}
|
||||
@@ -415,9 +421,9 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center p-12 text-slate-400">
|
||||
<Loader2 className="w-6 h-6 animate-spin mr-2 text-indigo-500" />
|
||||
<span className="text-sm">Notiz wird geladen…</span>
|
||||
<div className="flex-1 flex items-center justify-center p-12 text-muted">
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2 text-accent" />
|
||||
<span className="text-xs">Notiz wird geladen…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -425,12 +431,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center">
|
||||
<p className="text-sm text-rose-500 font-medium mb-2">{error}</p>
|
||||
<p className="text-xs text-status-err font-medium mb-2">{error}</p>
|
||||
{onBackMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBackMobile}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300"
|
||||
className="btn-action px-3 py-1.5 text-xs text-main"
|
||||
>
|
||||
Zurück zur Übersicht
|
||||
</button>
|
||||
@@ -440,15 +446,15 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full min-h-0 bg-white dark:bg-slate-900 overflow-y-auto">
|
||||
<div className="flex-1 flex flex-col h-full min-h-0 bg-canvas overflow-y-auto">
|
||||
{/* Top Document Action Bar */}
|
||||
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-2 px-4 sm:px-8 py-3 bg-white/90 dark:bg-slate-900/90 backdrop-blur-md border-b border-slate-200/70 dark:border-slate-800/80">
|
||||
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-2 px-4 sm:px-6 py-2.5 bg-panel border-b border-border shadow-sm">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{onBackMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBackMobile}
|
||||
className="lg:hidden p-1.5 rounded-lg text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
className="lg:hidden p-1.5 rounded-btn text-muted hover:text-main hover:bg-panel-elevated"
|
||||
title="Zurück zur Seitenleiste"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
@@ -460,24 +466,22 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canEdit && setShowFolderMenu((prev) => !prev)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200/70 dark:hover:bg-slate-700/70 transition-colors"
|
||||
className="btn-action inline-flex items-center gap-1.5 px-2 py-1 text-xs text-muted hover:text-main"
|
||||
title="Ordner wechseln"
|
||||
>
|
||||
<FolderIcon className="w-3.5 h-3.5 text-indigo-500" />
|
||||
<span className="truncate max-w-[160px]">
|
||||
<FolderIcon className="w-3.5 h-3.5 text-accent" />
|
||||
<span className="truncate max-w-[140px]">
|
||||
{doc?.folderPath ? doc.folderPath : 'Hauptebene'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showFolderMenu && canEdit && (
|
||||
<div className="absolute left-0 mt-1 w-52 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl shadow-xl py-1 z-30 max-h-60 overflow-y-auto">
|
||||
<div className="absolute left-0 mt-1 w-52 panel-surface shadow-panel border border-border rounded-card py-1 z-30 max-h-60 overflow-y-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMoveToFolder('')}
|
||||
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
||||
!doc?.folderPath
|
||||
? 'font-semibold text-indigo-600 dark:text-indigo-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-panel-elevated ${
|
||||
!doc?.folderPath ? 'font-bold text-accent' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
/ (Hauptebene)
|
||||
@@ -487,10 +491,8 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
key={folder}
|
||||
type="button"
|
||||
onClick={() => handleMoveToFolder(folder)}
|
||||
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-slate-100 dark:hover:bg-slate-800 truncate ${
|
||||
doc?.folderPath === folder
|
||||
? 'font-semibold text-indigo-600 dark:text-indigo-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-panel-elevated truncate ${
|
||||
doc?.folderPath === folder ? 'font-bold text-accent' : 'text-main'
|
||||
}`}
|
||||
>
|
||||
📁 {folder}
|
||||
@@ -500,9 +502,9 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Read-only indicator if shared with read permission */}
|
||||
{/* Read-only indicator */}
|
||||
{!canEdit && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium bg-amber-50 dark:bg-amber-950/60 text-amber-700 dark:text-amber-300">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-btn text-[11px] font-medium bg-status-warn/10 text-status-warn border border-status-warn/30">
|
||||
<Lock className="w-3 h-3" />
|
||||
Nur Lesen
|
||||
</span>
|
||||
@@ -510,36 +512,36 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Right Status & Action Buttons */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Sync Status Pill */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Sync Status Badge */}
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-medium ${
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-btn text-[10px] font-mono font-medium ${
|
||||
syncStatus === 'connected'
|
||||
? 'bg-emerald-50 dark:bg-emerald-950/50 text-emerald-600 dark:text-emerald-400'
|
||||
? 'bg-status-ok/10 text-status-ok border border-status-ok/30'
|
||||
: syncStatus === 'connecting' || syncStatus === 'reconnecting'
|
||||
? 'bg-amber-50 dark:bg-amber-950/50 text-amber-600 dark:text-amber-400'
|
||||
: 'bg-slate-100 dark:bg-slate-800 text-slate-500 dark:text-slate-400'
|
||||
? 'bg-status-warn/10 text-status-warn border border-status-warn/30'
|
||||
: 'bg-panel-elevated text-muted border border-border'
|
||||
}`}
|
||||
title={
|
||||
syncStatus === 'connected'
|
||||
? 'Echtzeit-Synchronisation aktiv (1.5s Auto-Flush in SQLite)'
|
||||
: 'Änderungen werden automatisch per REST gespeichert'
|
||||
? 'Echtzeit-Synchronisation aktiv'
|
||||
: 'Verbindung wird hergestellt…'
|
||||
}
|
||||
>
|
||||
{syncStatus === 'connected' ? (
|
||||
<>
|
||||
<Wifi className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Live</span>
|
||||
<Wifi className="w-2.5 h-2.5" />
|
||||
<span className="hidden sm:inline">LIVE</span>
|
||||
</>
|
||||
) : syncStatus === 'connecting' || syncStatus === 'reconnecting' ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span className="hidden sm:inline">Sync…</span>
|
||||
<Loader2 className="w-2.5 h-2.5 animate-spin" />
|
||||
<span className="hidden sm:inline">SYNC…</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOff className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">REST-Sync</span>
|
||||
<WifiOff className="w-2.5 h-2.5" />
|
||||
<span className="hidden sm:inline">OFFLINE</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -547,10 +549,10 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTogglePin}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
className={`p-1.5 rounded-btn transition-colors ${
|
||||
doc?.isPinned
|
||||
? 'bg-amber-50 dark:bg-amber-950/50 text-amber-500'
|
||||
: 'text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800'
|
||||
? 'text-status-warn bg-status-warn/10'
|
||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||
}`}
|
||||
title={doc?.isPinned ? 'Anheftung lösen' : 'Notiz anheften'}
|
||||
>
|
||||
@@ -561,7 +563,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenShareModal(doc)}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
className="p-1.5 rounded-btn text-muted hover:text-accent hover:bg-panel-elevated transition-colors"
|
||||
title="Notiz teilen / Berechtigungen"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
@@ -571,17 +573,17 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExportMarkdown}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
className="p-1.5 rounded-btn text-muted hover:text-main hover:bg-panel-elevated transition-colors"
|
||||
title="Als Markdown (.md) exportieren"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{canEdit && doc?.isOwner !== false && (
|
||||
{canEdit && (doc?.isOwner || doc?.permission === 'owner') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDeleteDoc(docId)}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors"
|
||||
className="p-1.5 rounded-btn text-muted hover:text-status-err hover:bg-status-err/10 transition-colors"
|
||||
title="Notiz löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -590,27 +592,27 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Outliner Content Container */}
|
||||
<div className="max-w-4xl w-full mx-auto px-4 sm:px-10 py-6 sm:py-8 flex-1">
|
||||
{/* Main Content Area */}
|
||||
<div className="max-w-4xl w-full mx-auto px-4 sm:px-8 py-5 flex-1">
|
||||
{/* Document Icon + Title Header */}
|
||||
<div className="flex items-center gap-3 mb-4 relative">
|
||||
<div className="flex items-center gap-3 mb-3 relative">
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canEdit && setShowEmojiPicker((prev) => !prev)}
|
||||
className="w-11 h-11 rounded-2xl bg-slate-100 dark:bg-slate-800/90 hover:bg-slate-200/70 dark:hover:bg-slate-700 flex items-center justify-center text-2xl transition-colors"
|
||||
className="w-10 h-10 rounded-btn bg-panel-elevated border border-border hover:border-strong flex items-center justify-center text-xl transition-colors"
|
||||
title="Icon ändern"
|
||||
>
|
||||
{doc?.icon || '📝'}
|
||||
</button>
|
||||
{showEmojiPicker && canEdit && (
|
||||
<div className="absolute left-0 mt-2 p-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-xl grid grid-cols-6 gap-1 z-30 w-56">
|
||||
<div className="absolute left-0 mt-1 p-2 panel-surface shadow-panel border border-border rounded-card grid grid-cols-6 gap-1 z-30 w-52">
|
||||
{EMOJI_PALETTE.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => handleSelectIcon(emoji)}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 text-lg flex items-center justify-center"
|
||||
className="p-1.5 rounded-btn hover:bg-panel-elevated text-base flex items-center justify-center"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
@@ -626,20 +628,20 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
readOnly={!canEdit}
|
||||
onChange={handleTitleChange}
|
||||
placeholder="Titel der Notiz…"
|
||||
className="flex-1 min-w-0 text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white bg-transparent border-0 focus:outline-none focus:ring-0 tracking-tight"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Document #Tags Summary Bar */}
|
||||
{/* Extracted #Tags Summary Bar */}
|
||||
{extractedTags.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5 mb-6 pb-4 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<Hash className="w-3.5 h-3.5 text-slate-400 mr-0.5" />
|
||||
<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="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-50 dark:bg-indigo-950/60 text-indigo-600 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/80 transition-colors"
|
||||
className="badge-accent text-[11px] font-medium px-2 py-0.5 rounded-btn hover:opacity-85 transition-opacity"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
@@ -647,7 +649,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logseq Hierarchical Outliner Tree */}
|
||||
{/* Logseq Outliner Tree */}
|
||||
<OutlinerTree
|
||||
docId={docId}
|
||||
blocks={blocks}
|
||||
@@ -657,6 +659,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
onDeleteBlock={handleDeleteBlock}
|
||||
onMoveBlock={handleMoveBlock}
|
||||
onTagClick={onSelectTag}
|
||||
onWikiLinkClick={onWikiLinkClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user