Harden quick-add against review findings

Independent review of the quick-add range surfaced real holes:

- The emptyLabel guard suppressed creation for any name that happened
  to be a substring of the label — typing "Rent" in the parent picker
  (a substring of "No parent (root)") silently offered nothing. The
  guard is gone; the exact-match rule already suppresses creates when
  the full label is typed.
- Async creates resolved against click-time snapshots, so a checkbox
  toggled or chip removed during the server round trip was silently
  reverted. Consumers now apply functional updates or a latest-value
  ref.
- Created names are capped at 200 characters, matching the registry
  forms; over-long text fails inline instead of minting a permanent
  multi-kilobyte name.
- A create failing after the user blurred mid-flight reopens the list
  so the error is never invisible, and option rows are locked while a
  create is in flight so a race cannot override an explicit pick.
This commit is contained in:
Lars Nolden
2026-09-14 12:20:33 +02:00
parent c569ae7dbf
commit 676065292e
3 changed files with 38 additions and 16 deletions
+14 -8
View File
@@ -695,6 +695,11 @@ function CorrectionEditor({
mutate: Mutate;
onChange: (value: CorrectionValue) => void;
}) {
// Async creates resolve against the freshest correction, not the snapshot
// captured when the create row was clicked: a chip removed during the
// server round trip must survive the create landing.
const latest = useRef(value);
latest.current = value;
const addable = data.tags
.filter((t) => !value.tag_ids.includes(t.id))
.map((t) => ({ value: t.id, label: t.name }));
@@ -739,7 +744,9 @@ function CorrectionEditor({
mutate={mutate}
value={value.category_id}
disabled={disabled}
onChange={(category_id) => onChange({ ...value, category_id })}
onChange={(category_id) =>
onChange({ ...latest.current, category_id })
}
/>
</dd>
</div>
@@ -783,14 +790,13 @@ function CorrectionEditor({
{
key: "tag",
label: `Create tag "${text}"`,
run: async () =>
run: async () => {
const id = await createTag(mutate, data, text);
onChange({
...value,
tag_ids: [
...value.tag_ids,
await createTag(mutate, data, text),
],
}),
...latest.current,
tag_ids: [...latest.current.tag_ids, id],
});
},
},
]
}