Skip to content

[BUG] Reordering the choice list deletes any choice the render filter dropped #1608

Description

@chhoumann

Found while fixing #1593, deliberately not grown into that PR: the same class of bug, on the choice list rather than the command list, and a live data-loss path on master.

The shape

ChoiceList.svelte:62-66 filters the rendered list to entries that can be keyed:

const renderable = $derived(
    rootChoicesOf(choices).filter(
        (choice) => isChoiceLike(choice) && typeof choice.id === "string" && choice.id !== "",
    ),
);

The comment above it (:58-61) says this is a "Render-time view only. Nothing here rewrites the tree." That is not true, because renderable is also what seeds the drop zone (:152) and what the persist path commits:

  • :152 use:dndzone={baseDndOptions({items: renderable, ...})}
  • :110 choices = next (from stripShadow(e.detail.items))
  • :114 actions.onReorderChoices(choices)
  • :133 moveChoice does the same from stripShadow(renderable)
  • ChoiceView.svelte:328-331 -> save() -> :101-103 saveChoices(snapshot(choices))

So the first drag or ArrowDown persists the filtered list, and every entry the filter dropped is gone from data.json.

Why it matters

For a null hole this is harmless - it carries nothing. But the filter also drops any entry whose id is not a non-empty string, and

{ "name": "Daily note", "type": "Template", "templatePath": "Templates/Daily.md", "format": {...}, "id": 12 }

is a complete, working choice whose id was written as a JSON number by a bad hand-edit, a script, or a merge. It is invisible in the settings list, so the user cannot miss it - and one reorder deletes it, with no prompt and no undo. That undoes exactly what dedupeChoicesById (choiceUtils.ts:218-221) goes out of its way to preserve.

The second half of the comment is wrong for the same reason: "It cannot be hiding a choice - only a Multi's choices value can" is false, because :64 drops id-less entries that are otherwise whole choices.

Suggested shape

Two options, in order of preference:

  1. Mirror what [BUG] A macro's commands has no read-accessor guard, so a malformed one is unrecoverable through the UI #1593 shipped for commands: repair rather than hide. A choice with a missing/non-string/duplicate id gets a fresh uuid at the seam and stays visible, so the user can see and delete it. dedupeChoicesById already does the duplicate half at load; this is the missing/non-string half.
  2. Failing that, reconcile the persist back onto the raw array (the setFolderChildrenById by-id idiom, choiceService.ts:589-635) so a reorder can only reorder, never delete.

Either way ChoiceView.malformed.test.ts needs the missing ratchet: reorder a list containing a hole and an id-less choice, and assert the persisted array is byte-identical apart from the reorder.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions