MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
- Extract: button on the bullet (desktop) or /extract (/notiz). The bullet becomes the title (hashtags stay at the link), its children the content of a new note in the same folder with the source's shares (a foreign source owner gets write access); the branch becomes [[title]] through the operation log, so open editors follow. - Merge: drop a note onto another in the sidebar or use "Mit anderer Notiz zusammenführen…" in the note header. The own source is appended as an h2 section with its bullets below and goes to the trash; the toast offers a complete undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
import React, { useMemo, useState } from 'react';
|
|
import { GitMerge, Search } from 'lucide-react';
|
|
import { FormSheet } from '../../components/FormSheet';
|
|
import { api } from '../../lib/api';
|
|
import { Document, MergeResult } from '../../lib/types';
|
|
|
|
interface MergeDialogProps {
|
|
docs: Document[];
|
|
sourceId: string;
|
|
/** Known after a drop in the sidebar; otherwise the user picks it here */
|
|
targetId?: string | null;
|
|
onClose: () => void;
|
|
onMerged: (result: MergeResult) => void;
|
|
}
|
|
|
|
const canWrite = (d: Document) => d.isOwner || d.permission === 'owner' || d.permission === 'write';
|
|
|
|
// Merging a note into another: pick the target (when not dropped onto one), confirm, merge. The source
|
|
// is appended to the target under a heading with its title and goes to the trash.
|
|
export const MergeDialog: React.FC<MergeDialogProps> = ({ docs, sourceId, targetId, onClose, onMerged }) => {
|
|
const [target, setTarget] = useState<string | null>(targetId || null);
|
|
const [query, setQuery] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const source = docs.find((d) => d.id === sourceId);
|
|
const targetDoc = docs.find((d) => d.id === target);
|
|
|
|
const candidates = useMemo(() => {
|
|
const q = query.trim().toLowerCase();
|
|
return docs
|
|
.filter((d) => d.id !== sourceId && !d.isArchived && canWrite(d))
|
|
.filter((d) => !q || (d.title || '').toLowerCase().includes(q))
|
|
.slice(0, 50);
|
|
}, [docs, sourceId, query]);
|
|
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!target) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
onMerged(await api.mergeDocuments(target, sourceId));
|
|
} catch (err: any) {
|
|
setError(err?.message || 'Zusammenführen fehlgeschlagen');
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const title = (d?: Document) => `„${d?.title || 'Unbenannte Notiz'}“`;
|
|
|
|
return (
|
|
<FormSheet
|
|
title="Notizen zusammenführen"
|
|
icon={<GitMerge className="w-4 h-4" />}
|
|
onClose={onClose}
|
|
onSubmit={submit}
|
|
submitLabel={target ? 'Zusammenführen' : 'Ziel wählen'}
|
|
busy={busy || !target}
|
|
>
|
|
{targetDoc ? (
|
|
<div className="space-y-2 text-xs">
|
|
<p className="text-main">
|
|
Notiz {title(source)} in {title(targetDoc)} zusammenführen?
|
|
</p>
|
|
<p className="text-muted">
|
|
Die Punkte von {title(source)} werden als eigener Abschnitt an {title(targetDoc)} angehängt.{' '}
|
|
{title(source)} wandert danach in den Papierkorb; „Rückgängig“ macht beides wieder rückgängig.
|
|
</p>
|
|
{!targetId && (
|
|
<button type="button" onClick={() => setTarget(null)} className="text-accent hover:underline min-h-[44px] sm:min-h-0">
|
|
Anderes Ziel wählen
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-muted">In welche Notiz soll {title(source)} eingefügt werden?</p>
|
|
<div className="relative">
|
|
<Search className="w-3.5 h-3.5 text-faint absolute left-2.5 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
autoFocus
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder="Notiz suchen…"
|
|
className="w-full h-11 sm:h-9 pl-8 pr-2 input-field text-sm"
|
|
/>
|
|
</div>
|
|
<ul className="max-h-64 overflow-y-auto divide-y divide-border rounded-card border border-border">
|
|
{candidates.map((d) => (
|
|
<li key={d.id}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setTarget(d.id)}
|
|
className="w-full min-h-[44px] sm:min-h-0 px-3 py-2 flex items-center gap-2 text-left text-xs text-main hover:bg-panel-elevated"
|
|
>
|
|
<span className="shrink-0">{d.icon || '📝'}</span>
|
|
<span className="truncate flex-1">{d.title || 'Unbenannte Notiz'}</span>
|
|
{d.folderPath && <span className="text-[10px] text-faint truncate max-w-[40%]">{d.folderPath}</span>}
|
|
</button>
|
|
</li>
|
|
))}
|
|
{candidates.length === 0 && <li className="px-3 py-3 text-xs text-faint">Keine passende Notiz</li>}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
{error && <p className="text-xs text-status-err">{error}</p>}
|
|
</FormSheet>
|
|
);
|
|
};
|