feat(delete): clarify "Delete also all clones" based on actual number of clones (closes #2362)

This commit is contained in:
Elian Doran
2026-04-11 11:39:05 +03:00
parent fb33921308
commit ccf95ad885
2 changed files with 97 additions and 5 deletions

View File

@@ -98,7 +98,10 @@
"broken_relations_to_be_deleted": "Following relations will be broken and deleted ({{ relationCount}})",
"cancel": "Cancel",
"ok": "OK",
"deleted_relation_text": "Note {{- note}} (to be deleted) is referenced by relation {{- relation}} originating from {{- source}}."
"deleted_relation_text": "Note {{- note}} (to be deleted) is referenced by relation {{- relation}} originating from {{- source}}.",
"no_clones_message": "The selected notes have no clones.",
"delete_clones_with_count": "Also delete {{count}} other clone(s) (can be undone in recent changes)",
"and_more_clones": "and {{count}} more..."
},
"export": {
"export_note_title": "Export note",

View File

@@ -11,6 +11,11 @@ import Button from "../react/Button.jsx";
import Alert from "../react/Alert.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
interface CloneInfo {
totalCloneCount: number;
clonePaths: string[];
}
export interface ResolveOptions {
proceed: boolean;
deleteAllClones?: boolean;
@@ -34,8 +39,9 @@ export default function DeleteNotesDialog() {
const [ deleteAllClones, setDeleteAllClones ] = useState(false);
const [ eraseNotes, setEraseNotes ] = useState(!!opts.forceDeleteAllClones);
const [ brokenRelations, setBrokenRelations ] = useState<DeleteNotesPreview["brokenRelations"]>([]);
const [ noteIdsToBeDeleted, setNoteIdsToBeDeleted ] = useState<DeleteNotesPreview["noteIdsToBeDeleted"]>([]);
const [ noteIdsToBeDeleted, setNoteIdsToBeDeleted ] = useState<DeleteNotesPreview["noteIdsToBeDeleted"]>([]);
const [ shown, setShown ] = useState(false);
const [ cloneInfo, setCloneInfo ] = useState<CloneInfo>({ totalCloneCount: 0, clonePaths: [] });
const okButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEvent("showDeleteNotesDialog", (opts) => {
@@ -43,11 +49,53 @@ export default function DeleteNotesDialog() {
setShown(true);
})
// Calculate clone information when branches change
useEffect(() => {
const { branchIdsToDelete } = opts;
if (!branchIdsToDelete || branchIdsToDelete.length === 0) {
setCloneInfo({ totalCloneCount: 0, clonePaths: [] });
return;
}
async function calculateCloneInfo() {
const branches = froca.getBranches(branchIdsToDelete!, true);
const uniqueNoteIds = [...new Set(branches.map(b => b.noteId))];
const notes = await froca.getNotes(uniqueNoteIds);
let totalCloneCount = 0;
const clonePaths: string[] = [];
for (const note of notes) {
const parentBranches = note.getParentBranches();
// Clones are additional parent branches beyond the one being deleted
const otherBranches = parentBranches.filter(b => !branchIdsToDelete!.includes(b.branchId));
if (otherBranches.length > 0) {
totalCloneCount += otherBranches.length;
// Get paths for preview (limit to first 5 total)
for (const branch of otherBranches) {
if (clonePaths.length >= 5) break;
const pathHtml = (await link.createLink(note.noteId, {
showNotePath: true,
referenceLink: false
})).html();
clonePaths.push(pathHtml);
}
}
}
setCloneInfo({ totalCloneCount, clonePaths });
}
calculateCloneInfo();
}, [opts.branchIdsToDelete]);
useEffect(() => {
const { branchIdsToDelete, forceDeleteAllClones } = opts;
if (!branchIdsToDelete || branchIdsToDelete.length === 0) {
return;
}
}
server.post<DeleteNotesPreview>("delete-notes-preview", {
branchIdsToDelete,
@@ -81,8 +129,10 @@ export default function DeleteNotesDialog() {
</>}
show={shown}
>
<FormCheckbox name="delete-all-clones" label={t("delete_notes.delete_all_clones_description")}
currentValue={deleteAllClones} onChange={setDeleteAllClones}
<DeleteAllClonesOption
cloneInfo={cloneInfo}
deleteAllClones={deleteAllClones}
setDeleteAllClones={setDeleteAllClones}
/>
<FormCheckbox
name="erase-notes" label={t("delete_notes.erase_notes_warning")}
@@ -170,3 +220,42 @@ function BrokenRelations({ brokenRelations }: { brokenRelations: DeleteNotesPrev
return <></>;
}
}
interface DeleteAllClonesOptionProps {
cloneInfo: CloneInfo;
deleteAllClones: boolean;
setDeleteAllClones: (value: boolean) => void;
}
function DeleteAllClonesOption({ cloneInfo, deleteAllClones, setDeleteAllClones }: DeleteAllClonesOptionProps) {
const { totalCloneCount, clonePaths } = cloneInfo;
if (totalCloneCount === 0) {
return (
<div className="clone-info-message" style={{ marginBottom: "10px", color: "var(--muted-text-color)" }}>
<em>{t("delete_notes.no_clones_message")}</em>
</div>
);
}
return (
<div className="clone-option-wrapper">
<FormCheckbox
name="delete-all-clones"
label={t("delete_notes.delete_clones_with_count", { count: totalCloneCount })}
currentValue={deleteAllClones}
onChange={setDeleteAllClones}
/>
{clonePaths.length > 0 && (
<ul className="clone-paths-preview" style={{ marginLeft: "24px", marginTop: "4px", fontSize: "0.9em", color: "var(--muted-text-color)" }}>
{clonePaths.map((path, index) => (
<li key={index} dangerouslySetInnerHTML={{ __html: path }} />
))}
{totalCloneCount > clonePaths.length && (
<li><em>{t("delete_notes.and_more_clones", { count: totalCloneCount - clonePaths.length })}</em></li>
)}
</ul>
)}
</div>
);
}