mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-09 19:31:57 +02:00
refactor(import): give the preview one rule for what a checkbox carries
A parent's checkbox covered creates and updates but skipped deletions and local-edit reverts, so a folder holding only those got no checkbox at all and the ones it did get moved a subset of what sat under them. The labels already mark destructive rows, so a parent now carries everything beneath it that has a checkbox of its own. Checking a row also brings in the folders it needs, which is the same rule read upwards and leaves nothing to disable: a resource can no longer be checked inside a folder that will not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
82ee025cd3
commit
0dbefd4283
@@ -260,19 +260,19 @@ function LoadedImportDataDialog({
|
|||||||
|
|
||||||
const itemTree = useMemo(() => buildItemTree(items), [items]);
|
const itemTree = useMemo(() => buildItemTree(items), [items]);
|
||||||
|
|
||||||
// A folder row's checkbox aggregates its subtree the way the git commit tree does: creates and
|
// A folder row's checkbox carries everything beneath it, deletions included — the row labels
|
||||||
// updates toggle together, while removals only ever cascade beneath a removed folder. Checking
|
// say which of those are destructive. Checking anything also brings back the folders it needs
|
||||||
// anything also brings back the folders it needs to live in.
|
// to live in.
|
||||||
const toggleNode = (node: CheckboxTreeNode<ImportPlanItem>, checked: boolean) => {
|
const toggleNode = (node: CheckboxTreeNode<ImportPlanItem>, checked: boolean) => {
|
||||||
const targets = new Set(
|
const targets = new Set(
|
||||||
collectItems(node)
|
collectItems(node)
|
||||||
.filter((i) => togglesWith(node.data, i))
|
.filter(isTogglable)
|
||||||
.map((i) => i.modelId),
|
.map((i) => i.modelId),
|
||||||
);
|
);
|
||||||
if (checked) {
|
if (checked) {
|
||||||
const byId = new Map(items.map((i) => [i.modelId, i]));
|
const byId = new Map(items.map((i) => [i.modelId, i]));
|
||||||
for (const ancestor of ancestorsOf(node.data, byId)) {
|
for (const ancestor of ancestorsOf(node.data, byId)) {
|
||||||
if (ancestor.action === "ignored") targets.add(ancestor.modelId);
|
if (isMissingFolder(ancestor)) targets.add(ancestor.modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setItems((prev) => prev.map((i) => (targets.has(i.modelId) ? { ...i, selected: checked } : i)));
|
setItems((prev) => prev.map((i) => (targets.has(i.modelId) ? { ...i, selected: checked } : i)));
|
||||||
@@ -284,21 +284,14 @@ function LoadedImportDataDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// A row the user can't meaningfully toggle on its own: a planned resource inside a deselected
|
// Deleting a folder takes its contents with it, so those rows have nothing left to decide.
|
||||||
// new folder can't exist, and a removed folder takes its contents with it.
|
|
||||||
const disabledIds = useMemo(() => {
|
const disabledIds = useMemo(() => {
|
||||||
const disabled = new Set<string>();
|
const disabled = new Set<string>();
|
||||||
const byId = new Map(items.map((i) => [i.modelId, i]));
|
const byId = new Map(items.map((i) => [i.modelId, i]));
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
if (item.action !== "delete") continue;
|
||||||
for (const parent of ancestorsOf(item, byId)) {
|
for (const parent of ancestorsOf(item, byId)) {
|
||||||
if (parent.model !== "folder") break;
|
if (parent.action === "delete" && parent.selected) {
|
||||||
const missing =
|
|
||||||
(parent.action === "create" || parent.action === "ignored") && !parent.selected;
|
|
||||||
// An ignored row stays checkable: checking it brings its folders back with it
|
|
||||||
if (missing && item.action !== "delete" && item.action !== "ignored") {
|
|
||||||
disabled.add(item.modelId);
|
|
||||||
}
|
|
||||||
if (parent.action === "delete" && parent.selected && item.action === "delete") {
|
|
||||||
disabled.add(item.modelId);
|
disabled.add(item.modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,23 +700,24 @@ function collectItems(node: CheckboxTreeNode<ImportPlanItem>): ImportPlanItem[]
|
|||||||
return [node.data, ...node.children.flatMap(collectItems)];
|
return [node.data, ...node.children.flatMap(collectItems)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A folder that isn't there yet, so anything inside it needs it brought in first. */
|
||||||
|
function isMissingFolder(item: ImportPlanItem): boolean {
|
||||||
|
return item.model === "folder" && (item.action === "create" || item.action === "ignored");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether toggling `root`'s checkbox also toggles `item` in its subtree. Destructive decisions
|
* Whether a row's checkbox is the decision for it, and so whether a parent's checkbox carries it.
|
||||||
* (deletions, reverting local edits) never ride along with a parent toggle.
|
* An unchanged resource has nothing to decide, a conflict is decided by its own control, and the
|
||||||
|
* destination workspace is not a plan item.
|
||||||
*/
|
*/
|
||||||
function togglesWith(root: ImportPlanItem, item: ImportPlanItem): boolean {
|
function isTogglable(item: ImportPlanItem): boolean {
|
||||||
if (item.model === "workspace") return false;
|
return item.model !== "workspace" && item.action !== "unchanged" && item.action !== "conflict";
|
||||||
if (root.action === "delete") return item.action === "delete";
|
|
||||||
if (item.action === "keep_local") {
|
|
||||||
return root.modelId === item.modelId && item.model !== "folder";
|
|
||||||
}
|
|
||||||
return item.action === "create" || item.action === "update" || item.action === "ignored";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeCheckedStatus(
|
function nodeCheckedStatus(
|
||||||
node: CheckboxTreeNode<ImportPlanItem>,
|
node: CheckboxTreeNode<ImportPlanItem>,
|
||||||
): boolean | "indeterminate" | "hidden" {
|
): boolean | "indeterminate" | "hidden" {
|
||||||
const covered = collectItems(node).filter((i) => togglesWith(node.data, i));
|
const covered = collectItems(node).filter(isTogglable);
|
||||||
if (covered.length === 0) return "hidden";
|
if (covered.length === 0) return "hidden";
|
||||||
const selected = covered.filter((i) => i.selected).length;
|
const selected = covered.filter((i) => i.selected).length;
|
||||||
if (selected === covered.length) return true;
|
if (selected === covered.length) return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user