Keep edited list values when an item is deleted (#235)
The list tab read the list once when it built its rows. Editing a value does not rebuild the rows, so the delete button later filtered that old copy and wrote it back. Every edit since the last rebuild was lost. Adding an item only triggered it, because adding rebuilds the rows and that state became the old copy. - ListAccess.value in src/editor/list/slot.ts is now read on every access instead of once when the access is built. - The kind switch of a row and the struct field buttons (field type, rename, remove field, + Field) read the item when clicked, not when the row was built. They had the same stale copy. - New test hook __gm.list.removeAt(i) clicks the delete button of one row, because the tab shows many buttons with the same label. Not covered by a test of their own: the struct field buttons and the kind switch after an edit. The cause is the same, the fix is in code. Tests: - ./check.sh: 2435 tests, CHECK EXIT 0. - Unit test in tests/editor/list-slot.test.ts keeps one access, edits a value, adds and edits an item, deletes another. All edits stay. It fails without the fix. - e2e 21b-ii with real clicks: set row 2 to 66, delete row 0, expect ["11","66"], two undos take both back. e2e 21: 52 checks passed. - Counter probe: with the old snapshot the check fails with ["11","44"], the reported bug. - Check total in README is now 1400 (+2), counted, not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d18862ed6
commit
085e6c8cce
6 files changed
+64
-9
No files matched your search
@@ -88,6 +88,7 @@ export function documentHooks({ state, editor }: HookContext) {
|
||||
/** The kind mark of a row, or of one field of a struct item – both are pickers. */
|
||||
markAt: (index: number, field = '') => editor.listEditor.markAt(index, field),
|
||||
nameAt: (index: number, field: string) => editor.listEditor.nameAt(index, field),
|
||||
removeAt: (index: number) => editor.listEditor.removeAt(index),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -54,6 +54,7 @@ const INDEX_WIDTH = 40;
|
||||
interface RowWidgets {
|
||||
value: Widget;
|
||||
mark: Widget;
|
||||
remove?: Widget;
|
||||
parts: Map<string, Widget>;
|
||||
marks: Map<string, Widget>;
|
||||
names: Map<string, Widget>;
|
||||
@@ -127,6 +128,11 @@ export class ListEditor extends VStack {
|
||||
return centreOf(field ? row?.marks.get(field) : row?.mark);
|
||||
}
|
||||
|
||||
/** For the check scripts: the button that deletes a row – every row has the same `×`. */
|
||||
removeAt(index: number): { x: number; y: number } | null {
|
||||
return centreOf(this.rowWidgets[index]?.remove);
|
||||
}
|
||||
|
||||
/** For the check scripts: the name field of one field of a struct item. */
|
||||
nameAt(index: number, field: string): { x: number; y: number } | null {
|
||||
return centreOf(this.rowWidgets[index]?.names.get(field));
|
||||
@@ -211,7 +217,7 @@ export class ListEditor extends VStack {
|
||||
// Through `untag`, so a color keeps its number when it becomes one.
|
||||
kindMenu(
|
||||
access.kinds.filter((next) => next !== kind),
|
||||
(next) => this.replaceItem(access, index, makeItem(next, untag(item))),
|
||||
(next) => this.replaceItem(access, index, makeItem(next, untag(access.value[index]))),
|
||||
),
|
||||
);
|
||||
if (tall) mark.minHeight = Theme.rowHeight;
|
||||
@@ -228,7 +234,10 @@ export class ListEditor extends VStack {
|
||||
this.setters.push((next) => field.setValue(untag(next) ?? ''));
|
||||
this.rowWidgets.push(rowWidgets(mark, field.widget));
|
||||
}
|
||||
row.add(removeButton(() => access.replace(access.value.filter((_, i) => i !== index))));
|
||||
const remove = removeButton(() => access.replace(access.value.filter((_, i) => i !== index)));
|
||||
row.add(remove);
|
||||
const widgets = this.rowWidgets[index];
|
||||
if (widgets) widgets.remove = remove;
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -242,6 +251,8 @@ export class ListEditor extends VStack {
|
||||
): void {
|
||||
const bundle = structItemOf(item);
|
||||
const fields = structItemFields(item);
|
||||
// A field value edit does not rebuild the row, so every button reads the item as it is now.
|
||||
const now = () => structItemOf(access.value[index]);
|
||||
const stack = new VStack({ gap: Theme.gap });
|
||||
const widgets = rowWidgets(mark, stack);
|
||||
const setters = new Map<string, (value: PropertyValue) => void>();
|
||||
@@ -255,13 +266,13 @@ export class ListEditor extends VStack {
|
||||
FIELD_KINDS.filter((next) => next !== field.kind),
|
||||
(next) =>
|
||||
this.replaceItem(access, index, {
|
||||
...bundle,
|
||||
[field.key]: makeFlatItem(next, untag(bundle[field.key])),
|
||||
...now(),
|
||||
[field.key]: makeFlatItem(next, untag(now()[field.key])),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const name = nameField(field.key, (to) => {
|
||||
if (to && to !== field.key) this.replaceItem(access, index, renamedField(bundle, field.key, to));
|
||||
if (to && to !== field.key) this.replaceItem(access, index, renamedField(now(), field.key, to));
|
||||
});
|
||||
line.add(fieldMark);
|
||||
line.add(name);
|
||||
@@ -275,12 +286,12 @@ export class ListEditor extends VStack {
|
||||
widgets.parts.set(field.key, sub.widget);
|
||||
widgets.marks.set(field.key, fieldMark);
|
||||
widgets.names.set(field.key, name);
|
||||
line.add(removeButton(() => this.replaceItem(access, index, withoutField(bundle, field.key))));
|
||||
line.add(removeButton(() => this.replaceItem(access, index, withoutField(now(), field.key))));
|
||||
stack.add(line);
|
||||
}
|
||||
const foot = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
foot.minHeight = Theme.rowHeight;
|
||||
foot.add(addFieldMenu((kind) => this.addField(access, index, bundle, kind)));
|
||||
foot.add(addFieldMenu((kind) => this.addField(access, index, now(), kind)));
|
||||
foot.add(new Spacer(), 1);
|
||||
stack.add(foot);
|
||||
row.add(stack, 1);
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ListDef = Extract<PropertyDef, { kind: 'list' }>;
|
||||
/** What the tab needs of a slot: what is in it, and the two ways of writing it back. */
|
||||
export interface ListAccess {
|
||||
readonly def: ListDef;
|
||||
/** What is in the list now – re-read on every access. */
|
||||
readonly value: ListValue;
|
||||
/**
|
||||
* The kinds this list may hold. A list in a struct field takes primitives only – `struct → list →
|
||||
@@ -119,7 +120,11 @@ function access<Target>(
|
||||
def,
|
||||
kinds,
|
||||
title,
|
||||
value: current(),
|
||||
// Re-read, not a snapshot: an item edit does not rebuild the rows, so a delete or kind change
|
||||
// clicked later has to build on the list with that edit in it, not on the one the rows showed.
|
||||
get value() {
|
||||
return current();
|
||||
},
|
||||
itemHandlers: (index) => ({
|
||||
...previewingHandlers<Target>(
|
||||
{ state },
|
||||
|
||||
Reference in new issue
Block a user