fix(grid): restore long cell values in the detail editor
This commit is contained in:
1 parent
15d451d43a
commit
a216e0938e
6 files changed
+177
-7
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user