fix(grid): replace matches in truncated large values

This commit is contained in:
ccq authored and GitHub committed 2026-09-18 22:48:16 +08:00
1 parent 4699295ed6
commit 8bafe0916c
6 files changed
+157 -24

No files matched your search

+30 -19
View File
@@ -311,7 +311,7 @@ import { createDataGridRuntimeScope } from "@/lib/dataGrid/dataGridRuntime";
import { useDataGridEditor } from "@/composables/useDataGridEditor";
import { useDataGridSort } from "@/composables/useDataGridSort";
import { useDataGridSearch, type DataGridSearchMatch } from "@/composables/useDataGridSearch";
import { findDataGridReplacementMatches, replaceDataGridText, type DataGridReplaceScope } from "@/lib/dataGrid/dataGridReplace";
import { findDataGridReplacementMatches, prepareDataGridCellReplacements, type DataGridReplaceScope } from "@/lib/dataGrid/dataGridReplace";
import { useDataGridResultLifecycle } from "@/composables/useDataGridResultLifecycle";
import { useDataGridAutoRefresh } from "@/composables/useDataGridAutoRefresh";
import { useDataGridAsyncSurface } from "@/composables/useDataGridAsyncSurface";
@@ -8160,29 +8160,19 @@ function selectedRangeTargetsOnlyDraftRow(): boolean {
}
const replaceAvailable = computed(() => !!props.editable && hasDataGridSaveTarget.value && canEditExistingRows.value && !resolvedConnectionConfig.value?.read_only && !isConditionalUpdateActive.value);
const replaceBusy = computed(() => isSaving.value || gridSurfaceBusy.value || props.loading === true);
const replaceResolving = ref(false);
const replaceBusy = computed(() => replaceResolving.value || isSaving.value || gridSurfaceBusy.value || props.loading === true);
function replacementRowItem(rowId: number): RowItem | undefined {
const row = props.result.rows[rowId];
if (!row || rowId < 0) return undefined;
return { id: rowId, displayIndex: displayRowIndexById(rowId), sourceIndex: rowId, data: rowDataWithChanges(row, rowId), isNew: false, isDeleted: deletedRows.value.has(rowId), isDirtyCol: [], status: dirtyRows.value.has(rowId) ? "edited" : "clean" };
const dirty = dirtyRows.value.get(rowId);
return { id: rowId, displayIndex: displayRowIndexById(rowId), sourceIndex: rowId, data: rowDataWithChanges(row, rowId), isNew: false, isDeleted: deletedRows.value.has(rowId), isDirtyCol: dirtyColumnsForRow(dirty, props.result.columns.length), status: dirty?.size ? "edited" : "clean" };
}
function canReplaceGridCell(item: RowItem | undefined, col: number): boolean {
const type = allColumnTypes.value[col];
return (
replaceAvailable.value &&
!!item &&
item.sourceIndex !== undefined &&
!item.isNew &&
!item.isDraft &&
typeof item.data[col] === "string" &&
canEditCellItem(item, col) &&
!isLargeValuePreview(item, col) &&
!isBinaryCellColumnType(type) &&
!isNumericColumnType(type) &&
!isBooleanGridCell(item, col)
);
return replaceAvailable.value && !!item && item.sourceIndex !== undefined && !item.isNew && !item.isDraft && typeof item.data[col] === "string" && canEditCellItem(item, col) && !isBinaryCellColumnType(type) && !isNumericColumnType(type) && !isBooleanGridCell(item, col);
}
function replacementCellInScope(rowId: number, col: number): boolean {
@@ -8227,14 +8217,35 @@ watch(
},
);
function replaceGridMatches(currentOnly = false) {
async function replaceGridMatches(currentOnly = false) {
if (!replaceAvailable.value || replaceBusy.value) return;
if (currentOnly && !canReplaceCurrent.value) return;
const current = currentSearchMatch.value;
const currentRowId = current?.kind === "cell" ? displayItemAt(current.displayRow)?.id : undefined;
const matches = replacementMatches.value.filter((match) => !currentOnly || (match.rowId === currentRowId && match.col === current?.col));
const count = stageCellReplacements(matches.map((match) => ({ ...match, previousValue: match.value, value: replaceDataGridText(match.value, deferredClientSearchText.value, replacementText.value, replaceCaseSensitive.value) })));
if (count > 0) toast(t("grid.replaceStagedCells", { count }), 5000);
if (!matches.length) return;
const sourceResult = props.result;
const search = deferredClientSearchText.value;
const replacement = replacementText.value;
const caseSensitive = replaceCaseSensitive.value;
replaceResolving.value = true;
try {
const changes = await prepareDataGridCellReplacements({
matches,
search,
replacement,
caseSensitive,
needsResolution: (rowId, col) => isLargeValuePreview(replacementRowItem(rowId), col),
resolveValues: resolveLargeValueCells,
});
if (props.result !== sourceResult) return;
const count = stageCellReplacements(changes);
if (count > 0) toast(t("grid.replaceStagedCells", { count }), 5000);
} catch (error) {
if (props.result === sourceResult) reportLargeValueLoadError(error);
} finally {
replaceResolving.value = false;
}
}
function fillSelectionWithValue(value: string | null, options: { preserveEmptyString?: boolean; emptyStringAsNull?: boolean } = {}): boolean {
@@ -0,0 +1,23 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
function functionSource(name: string, nextName: string): string {
const start = dataGridSource.indexOf(`function ${name}`);
const relativeEnd = dataGridSource.slice(start).search(new RegExp(`\\n(?:async\\s+)?function\\s+${nextName}\\b`));
expect(start).toBeGreaterThanOrEqual(0);
expect(relativeEnd).toBeGreaterThan(0);
return dataGridSource.slice(start, start + relativeEnd);
}
describe("DataGrid large-value replacement", () => {
it("counts preview matches and resolves full values before staging replacements", () => {
const eligibilitySource = functionSource("canReplaceGridCell", "replacementCellInScope");
const replaceSource = functionSource("replaceGridMatches", "fillSelectionWithValue");
expect(eligibilitySource).not.toContain("!isLargeValuePreview");
expect(replaceSource).toContain("await prepareDataGridCellReplacements");
expect(replaceSource).toContain("resolveLargeValueCells");
});
});