fix(grid): decouple where and order by undo histories

This commit is contained in:
zipg authored and GitHub committed 2026-08-31 16:58:32 +08:00
1 parent 0e9f92c917
commit d144e1bc08
2 files changed
+88 -2

No files matched your search

@@ -55,6 +55,9 @@ const suggestionPosition = ref({ left: 0, top: 0, width: 180 });
const historyPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null); const historyPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null);
const pointerMovedSuggestionIndex = ref(-1); const pointerMovedSuggestionIndex = ref(-1);
const editorFocused = ref(false); const editorFocused = ref(false);
const conditionUndoStack = ref<string[]>([]);
const conditionRedoStack = ref<string[]>([]);
let conditionLastValue = modelValue.value;
let collapseTimer: ReturnType<typeof setTimeout> | undefined; let collapseTimer: ReturnType<typeof setTimeout> | undefined;
let resizeObserver: ResizeObserver | undefined; let resizeObserver: ResizeObserver | undefined;
let expandAfterComposition = false; let expandAfterComposition = false;
@@ -370,6 +373,43 @@ function onInput(event: Event) {
scheduleCaretIntoView(); scheduleCaretIntoView();
} }
function isConditionUndoRedoShortcut(event: KeyboardEvent) {
const key = event.key.toLowerCase();
return ((event.metaKey || event.ctrlKey) && !event.altKey && key === "z") || (event.ctrlKey && !event.metaKey && !event.altKey && key === "y");
}
function isConditionRedoShortcut(event: KeyboardEvent) {
return ((event.metaKey || event.ctrlKey) && !event.altKey && event.shiftKey && event.key.toLowerCase() === "z") || (event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "y");
}
function applyConditionHistoryValue(value: string) {
conditionLastValue = value;
modelValue.value = value;
void nextTick(() => {
const target = activeEditor.value;
if (!target) return;
target.focus({ preventScroll: true });
target.setSelectionRange(value.length, value.length);
syncSelection(target);
});
}
function handleConditionUndoRedo(event: KeyboardEvent) {
if (!isConditionUndoRedoShortcut(event)) return false;
event.preventDefault();
event.stopPropagation();
const source = isConditionRedoShortcut(event) ? conditionRedoStack.value : conditionUndoStack.value;
const targetValue = source.pop();
if (targetValue === undefined) return true;
const currentValue = modelValue.value;
const destination = isConditionRedoShortcut(event) ? conditionUndoStack.value : conditionRedoStack.value;
destination.push(currentValue);
applyConditionHistoryValue(targetValue);
return true;
}
async function applyCondition() { async function applyCondition() {
editor.dismiss(); editor.dismiss();
const applied = props.apply ? await props.apply(modelValue.value) : emit("apply", modelValue.value); const applied = props.apply ? await props.apply(modelValue.value) : emit("apply", modelValue.value);
@@ -385,6 +425,7 @@ async function clearCondition() {
} }
function onKeydown(event: KeyboardEvent) { function onKeydown(event: KeyboardEvent) {
if (handleConditionUndoRedo(event)) return;
if (completeQuote(event)) return; if (completeQuote(event)) return;
const action = editor.handleKeydown(event); const action = editor.handleKeydown(event);
if (action === "apply") void applyCondition(); if (action === "apply") void applyCondition();
@@ -471,7 +512,14 @@ function hideHistoryPreview() {
historyPreview.value = null; historyPreview.value = null;
} }
watch(modelValue, () => resizeEditor()); watch(modelValue, (value) => {
if (value !== conditionLastValue) {
conditionUndoStack.value.push(conditionLastValue);
conditionRedoStack.value = [];
conditionLastValue = value;
}
resizeEditor();
});
watch(suggestionPreferredWidth, () => { watch(suggestionPreferredWidth, () => {
if (editor.dropdownOpen.value) updateSuggestionPosition(); if (editor.dropdownOpen.value) updateSuggestionPosition();
}); });
@@ -30,7 +30,7 @@ function mountEditor(kind: DataGridConditionHistoryKind, initialValue: string, o
); );
app.mount(host); app.mount(host);
mountedApps.push({ app, host }); mountedApps.push({ app, host });
return { value, input: host.querySelector("textarea") as HTMLTextAreaElement }; return { value, input: host.querySelector("textarea") as HTMLTextAreaElement, host };
} }
function mockTextareaMetrics(input: HTMLTextAreaElement, options: { clientWidth: number; scrollWidth?: number; clientHeight?: number; scrollHeight?: number }) { function mockTextareaMetrics(input: HTMLTextAreaElement, options: { clientWidth: number; scrollWidth?: number; clientHeight?: number; scrollHeight?: number }) {
@@ -121,6 +121,44 @@ describe("DataGridConditionEditor quote completion", () => {
expect(value.value).toBe("name"); expect(value.value).toBe("name");
}); });
it.each([
["where", "z", { ctrlKey: true }],
["orderBy", "z", { metaKey: true }],
["where", "z", { ctrlKey: true, shiftKey: true }],
["orderBy", "y", { ctrlKey: true }],
] as const)("keeps %s undo/redo shortcuts in the condition editor", (kind, key, modifiers) => {
const { input, host } = mountEditor(kind, "id = 123");
let bubbled = 0;
host.addEventListener("keydown", () => bubbled++);
const event = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, ...modifiers });
input.dispatchEvent(event);
expect(event.defaultPrevented).toBe(true);
expect(bubbled).toBe(0);
});
it("keeps WHERE and ORDER BY undo history independent", async () => {
const where = mountEditor("where", "id = 123");
const orderBy = mountEditor("orderBy", "id ASC");
where.input.value = "id = 456";
where.input.dispatchEvent(new Event("input", { bubbles: true }));
orderBy.input.value = "id DESC";
orderBy.input.dispatchEvent(new Event("input", { bubbles: true }));
await nextTick();
orderBy.input.dispatchEvent(new KeyboardEvent("keydown", { key: "z", metaKey: true, bubbles: true, cancelable: true }));
await nextTick();
expect(where.value.value).toBe("id = 456");
expect(orderBy.value.value).toBe("id ASC");
orderBy.input.dispatchEvent(new KeyboardEvent("keydown", { key: "z", metaKey: true, shiftKey: true, bubbles: true, cancelable: true }));
await nextTick();
expect(where.value.value).toBe("id = 456");
expect(orderBy.value.value).toBe("id DESC");
});
it("passes the textarea caret range through when accepting a suggestion", async () => { it("passes the textarea caret range through when accepting a suggestion", async () => {
const { value, input } = mountEditor("where", "status = cus AND enabled = 1", { columns: ["customer_id"] }); const { value, input } = mountEditor("where", "status = cus AND enabled = 1", { columns: ["customer_id"] });
input.focus(); input.focus();