fix(import): close three holes found in review

Checking an ignored folder selected every deletion beneath it, including
the one the folder caused: a resource the source had moved into it was
offered for deletion precisely because the folder was not imported, so
checking the folder deleted the resource instead of keeping it for the
move. A checkbox that means "keep these" can never mean "delete that".

Binding a re-import to a linked source took a single shared key as proof,
and two API documents naming one operation the same way is not proof —
that let an unrelated import remap onto somebody else's resources. A
document keeps its keys, so it now has to account for most of both sides,
and only against a source from the same importer.

A key the user turned down was forgotten as soon as the source stopped
listing it, so the question came back the next time the source did. The
row outlives the absence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-09-10 10:05:16 -07:00
co-authored by Claude Opus 5
parent b6818b4f52
commit a73cb245ea
2 changed files with 105 additions and 15 deletions
@@ -264,7 +264,7 @@ function LoadedImportDataDialog({
// say which of those are destructive. Checking anything also brings back the folders it needs
// to live in.
const toggleNode = (node: CheckboxTreeNode<TreeRow>, checked: boolean) => {
const targets = new Set(togglableItems(node).map((i) => i.modelId));
const targets = new Set(carriedItems(node).map((i) => i.modelId));
if (checked && node.data.kind === "item") {
const byId = new Map(items.map((i) => [i.modelId, i]));
for (const ancestor of ancestorsOf(node.data.item, byId)) {
@@ -776,8 +776,20 @@ function togglableItems(node: CheckboxTreeNode<TreeRow>): ImportPlanItem[] {
.filter((i) => i != null);
}
/**
* What a row's checkbox carries: its subtree, less any deletion that exists only because a folder
* above it isn't imported. Checking that folder is how the user keeps those resources, so it must
* not be how they delete them — the move follows on the next import.
*/
function carriedItems(node: CheckboxTreeNode<TreeRow>): ImportPlanItem[] {
const self = node.data.kind === "item" ? node.data.item.modelId : null;
return togglableItems(node).filter(
(i) => i.modelId === self || i.reason !== "moved_into_ignored_folder",
);
}
function nodeCheckedStatus(node: CheckboxTreeNode<TreeRow>): boolean | "indeterminate" | "hidden" {
const covered = togglableItems(node);
const covered = carriedItems(node);
if (covered.length === 0) return "hidden";
const selected = covered.filter((i) => i.selected).length;
if (selected === covered.length) return true;