fix(grid): restore long cell values in the detail editor

This commit is contained in:
jearhe authored and GitHub committed 2026-09-11 10:52:19 +08:00
1 parent 15d451d43a
commit a216e0938e
6 files changed
+177 -7

No files matched your search

+11 -2
View File
@@ -5786,7 +5786,16 @@ watch(valueEditorContainer, async (el) => {
fontSize: editorFontSize,
fontFamily: detailEditorFontFamily,
});
await valueDetailEditor.create(el, detailEditValue.value, activeCellDetail.value?.type);
const editor = valueDetailEditor;
await editor.create(el, detailEditValue.value, activeCellDetail.value?.type);
// The editor initializes asynchronously (theme loading can yield here), so
// detailEditValue may have changed before CodeMirror owns the document.
// Reconcile the latest value after create and ignore an editor replaced by
// a fast tab unmount/remount.
if (valueDetailEditor !== editor) return;
if (editor.getValue() !== detailEditValue.value) {
editor.setValue(detailEditValue.value, activeCellDetail.value?.type);
}
} else if (!el && valueDetailEditor) {
valueDetailEditor.destroy();
valueDetailEditor = null;
@@ -5809,7 +5818,7 @@ const detailEdit = useDataGridCellDetailEdit({
restoreCellValue,
syncEditor: (value, columnType) => {
const editor = getDetailEditor();
if (editor) editor.setValue(value, columnType);
if (editor && editor.getValue() !== value) editor.setValue(value, columnType);
},
refreshDetail: () => {
detailCell.value = detailCell.value ? { ...detailCell.value } : null;
@@ -1392,6 +1392,14 @@ describe("cell detail surfaces", () => {
}
expect(dataGridSource).not.toContain("activeCellDetailTabsGridClass");
const valueEditorLifecycleStart = dataGridSource.indexOf("watch(valueEditorContainer");
const valueEditorLifecycleEnd = dataGridSource.indexOf("const detailEdit = useDataGridCellDetailEdit", valueEditorLifecycleStart);
const valueEditorLifecycle = dataGridSource.slice(valueEditorLifecycleStart, valueEditorLifecycleEnd);
expect(valueEditorLifecycle).toContain("const editor = valueDetailEditor;");
expect(valueEditorLifecycle).toContain("if (valueDetailEditor !== editor) return;");
expect(valueEditorLifecycle).toContain("if (editor.getValue() !== detailEditValue.value)");
expect(valueEditorLifecycle).toContain("editor.setValue(detailEditValue.value, activeCellDetail.value?.type);");
const valueEditorStart = dataGridSource.indexOf("<TabsContent v-if=\"activeCellDetailTabs.includes('valueEditor')\"");
const valueEditorEnd = dataGridSource.indexOf("</TabsContent>", valueEditorStart);
const valueEditor = dataGridSource.slice(valueEditorStart, valueEditorEnd);
@@ -106,4 +106,51 @@ describe("useDataGridCellDetail", () => {
await nextTick();
scope.stop();
});
it("reconciles the latest long value after asynchronous editor creation", async () => {
const scope = effectScope();
const editValue = ref("");
let finishCreate!: () => void;
mocks.create.mockImplementationOnce(() => new Promise<void>((resolve) => (finishCreate = resolve)));
const composable = scope.run(() => useDataGridCellDetail({ detail: ref(detail()), editValue, onCancel: vi.fn() }))!;
composable.detailsEditorContainer.value = document.createElement("div");
await nextTick();
expect(finishCreate).toBeTypeOf("function");
const longValue = "x".repeat(15000);
editValue.value = longValue;
await nextTick();
// The real editor ignores setValue until create has installed its view.
mocks.setValue.mockClear();
finishCreate();
await Promise.resolve();
await nextTick();
expect(mocks.setValue).toHaveBeenCalledWith(longValue, "VARCHAR");
composable.detailsEditorContainer.value = undefined;
await nextTick();
scope.stop();
});
it("does not reconcile an editor that was destroyed during creation", async () => {
const scope = effectScope();
let finishCreate!: () => void;
mocks.create.mockImplementationOnce(() => new Promise<void>((resolve) => (finishCreate = resolve)));
const composable = scope.run(() => useDataGridCellDetail({ detail: ref(detail()), editValue: ref(""), onCancel: vi.fn() }))!;
composable.detailsEditorContainer.value = document.createElement("div");
await nextTick();
mocks.setValue.mockClear();
composable.detailsEditorContainer.value = undefined;
await nextTick();
expect(mocks.destroy).toHaveBeenCalledOnce();
finishCreate();
await Promise.resolve();
expect(mocks.setValue).not.toHaveBeenCalled();
scope.stop();
});
});
@@ -6,7 +6,7 @@ import { useDataGridCellDetailEdit } from "@/composables/useDataGridCellDetailEd
import { MONGO_DOCUMENT_GRID_NULL } from "@/lib/mongo/mongoDocumentValues";
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
function detail(value: string): DataGridCellDetail {
function detail(value: string, patch: Partial<DataGridCellDetail> = {}): DataGridCellDetail {
return {
rowNumber: 1,
rowId: 3,
@@ -24,6 +24,7 @@ function detail(value: string): DataGridCellDetail {
length: value.length,
formattedJson: "",
isEditable: true,
...patch,
};
}
@@ -59,4 +60,96 @@ describe("useDataGridCellDetailEdit", () => {
editor.setDetailNull();
expect(applyCellValue).toHaveBeenLastCalledWith(3, 1, MONGO_DOCUMENT_GRID_NULL);
});
it("synchronizes the latest long value after asynchronous hydration", async () => {
const previewValue = "preview";
const longValue = "x".repeat(15000);
const activeDetail = ref<DataGridCellDetail | null>(detail(previewValue));
let finishHydration!: (result: boolean) => void;
const syncEditor = vi.fn();
const editor = useDataGridCellDetailEdit({
activeDetail: computed(() => activeDetail.value),
activeTab: ref("valueEditor"),
jsonFormatted: computed(() => false),
databaseType: computed(() => "mongodb"),
resultRows: computed(() => [["id", previewValue]]),
getColumnInfo: () => undefined,
getRowItem: () => ({ sourceIndex: 0, isNew: false, isDeleted: false }),
hydrateLargeValueCell: () => new Promise<boolean>((resolve) => (finishHydration = resolve)),
applyCellValue: vi.fn(),
restoreCellValue: vi.fn(),
syncEditor,
refreshDetail: vi.fn(),
warnFormattedJsonEdit: vi.fn(),
});
const start = editor.startDetailEdit();
activeDetail.value = detail(longValue);
finishHydration(true);
await start;
expect(editor.detailEditValue.value).toBe(longValue);
expect(syncEditor).toHaveBeenLastCalledWith(longValue, "");
});
it("updates an already mounted editor when the selected cell changes", async () => {
const activeDetail = ref<DataGridCellDetail | null>(detail("short"));
const activeTab = ref<"details" | "valueEditor">("details");
const syncEditor = vi.fn();
const editor = useDataGridCellDetailEdit({
activeDetail: computed(() => activeDetail.value),
activeTab,
jsonFormatted: computed(() => false),
databaseType: computed(() => "mongodb"),
resultRows: computed(() => [["id", "short"]]),
getColumnInfo: () => undefined,
getRowItem: () => ({ sourceIndex: 0, isNew: false, isDeleted: false }),
hydrateLargeValueCell: async () => true,
applyCellValue: vi.fn(),
restoreCellValue: vi.fn(),
syncEditor,
refreshDetail: vi.fn(),
warnFormattedJsonEdit: vi.fn(),
});
activeTab.value = "valueEditor";
await Promise.resolve();
await Promise.resolve();
syncEditor.mockClear();
const longValue = "x".repeat(15000);
activeDetail.value = detail(longValue, { rowId: 4 });
await Promise.resolve();
expect(editor.detailEditValue.value).toBe(longValue);
expect(syncEditor).toHaveBeenLastCalledWith(longValue, "");
});
it("ignores a late async start after the detail editor is reset", async () => {
const activeDetail = ref<DataGridCellDetail | null>(detail("preview"));
let finishHydration!: (result: boolean) => void;
const editor = useDataGridCellDetailEdit({
activeDetail: computed(() => activeDetail.value),
activeTab: ref("valueEditor"),
jsonFormatted: computed(() => false),
databaseType: computed(() => "mongodb"),
resultRows: computed(() => [["id", "preview"]]),
getColumnInfo: () => undefined,
getRowItem: () => ({ sourceIndex: 0, isNew: false, isDeleted: false }),
hydrateLargeValueCell: () => new Promise<boolean>((resolve) => (finishHydration = resolve)),
applyCellValue: vi.fn(),
restoreCellValue: vi.fn(),
syncEditor: vi.fn(),
refreshDetail: vi.fn(),
warnFormattedJsonEdit: vi.fn(),
});
const start = editor.startDetailEdit();
editor.resetDetailEdit();
finishHydration(true);
await start;
expect(editor.isEditingDetail.value).toBe(false);
expect(editor.detailEditValue.value).toBe("");
});
});
@@ -36,6 +36,9 @@ export function useDataGridCellDetail(options: { detail: Ref<DataGridCellDetail>
const editor = useCellDetailEditor({ onChange: (value) => (options.editValue.value = value), onEscape: options.onCancel, ...editorOptions() });
detailsEditor = editor;
await editor.create(element, options.editValue.value, options.detail.value.type);
if (detailsEditor === editor && editor.getValue() !== options.editValue.value) {
editor.setValue(options.editValue.value, options.detail.value.type);
}
if (detailsEditor === editor) editor.view.value?.focus();
} else if (!element && detailsEditor) {
detailsEditor.destroy();
@@ -45,8 +48,13 @@ export function useDataGridCellDetail(options: { detail: Ref<DataGridCellDetail>
watch(sideJsonPreviewContainer, async (element) => {
if (element && !sideJsonEditor) {
sideJsonEditor = useCellDetailEditor({ language: "json", readOnly: true, ...editorOptions() });
await sideJsonEditor.create(element, options.detail.value.formattedJson ?? "", "json");
const editor = useCellDetailEditor({ language: "json", readOnly: true, ...editorOptions() });
sideJsonEditor = editor;
await editor.create(element, options.detail.value.formattedJson ?? "", "json");
if (sideJsonEditor === editor) {
const value = options.detail.value.formattedJson ?? "";
if (editor.getValue() !== value) editor.setValue(value, "json");
}
} else if (!element && sideJsonEditor) {
sideJsonEditor.destroy();
sideJsonEditor = null;
@@ -39,6 +39,7 @@ export function useDataGridCellDetailEdit(options: UseDataGridCellDetailEditOpti
const detailValueDiffSnapshot = ref<Readonly<JsonValueDiffSnapshot> | null>(null);
let allowActiveCellDetailResync = false;
let lastSyncedDetailCellKey: string | null = null;
let detailEditGeneration = 0;
const hasPendingDetailEditorDraft = computed(() => isEditingDetail.value && detailEditValue.value !== detailEditOriginalValue.value);
const detailJsonDiffContext = computed<JsonValueDiffContext | null>(() => {
@@ -80,6 +81,7 @@ export function useDataGridCellDetailEdit(options: UseDataGridCellDetailEditOpti
}
function resetDetailEdit() {
detailEditGeneration += 1;
isEditingDetail.value = false;
detailEditValue.value = "";
detailEditOriginalValue.value = "";
@@ -116,16 +118,19 @@ export function useDataGridCellDetailEdit(options: UseDataGridCellDetailEditOpti
isEditingDetail.value = true;
});
async function startDetailEdit() {
async function startDetailEdit(generation = detailEditGeneration) {
const initialDetail = options.activeDetail.value;
if (!initialDetail || !initialDetail.isEditable) return;
const initialCellKey = `${initialDetail.rowId}:${initialDetail.colIndex}`;
if (!(await options.hydrateLargeValueCell(initialDetail.rowId, initialDetail.colIndex))) return;
if (generation !== detailEditGeneration) return;
const detail = options.activeDetail.value;
if (!detail || !detail.isEditable) return;
if (!detail || !detail.isEditable || `${detail.rowId}:${detail.colIndex}` !== initialCellKey) return;
options.warnFormattedJsonEdit(detail);
const value = activeDetailEditorText(detail);
detailEditValue.value = value;
detailEditOriginalValue.value = value;
syncEditorFromDetailEdit();
isEditingDetail.value = true;
}