fix(grid): decouple where and order by undo histories
This commit is contained in:
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();
|
||||||
});
|
});
|
||||||
|
|||||||
+39
-1
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user