fix(grid): remove virtual row memoization
This commit is contained in:
parent
370701a82b
commit
f68edb2516
|
|
@ -382,24 +382,6 @@ const savedCustomFormatters = computed(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const columnFormatterMemoKey = computed(() =>
|
||||
JSON.stringify({
|
||||
columns: props.result.columns,
|
||||
columnFormatters: settingsStore.editorSettings.columnFormatters,
|
||||
customColumnFormatters: settingsStore.editorSettings.customColumnFormatters,
|
||||
}),
|
||||
);
|
||||
|
||||
const cellEditabilityMemoKey = computed(() =>
|
||||
JSON.stringify({
|
||||
editable: props.editable,
|
||||
canEditExistingRows: canEditExistingRows.value,
|
||||
databaseType: props.databaseType,
|
||||
sourceColumns: props.sourceColumns,
|
||||
tableColumns: props.tableMeta?.columns.map((column) => [column.name, column.data_type]),
|
||||
}),
|
||||
);
|
||||
|
||||
function localFilterKey(value: CellValue): string {
|
||||
if (value === null) return "__dbx_null__";
|
||||
if (typeof value === "boolean") return `bool:${value}`;
|
||||
|
|
@ -1407,6 +1389,7 @@ async function lastPage() {
|
|||
|
||||
interface RowItem {
|
||||
id: number;
|
||||
displayIndex: number;
|
||||
sourceIndex?: number;
|
||||
newIndex?: number;
|
||||
data: CellValue[];
|
||||
|
|
@ -1577,7 +1560,7 @@ const sortedRows = computed(() => {
|
|||
const displayItems = computed<RowItem[]>(() => {
|
||||
const cols = props.result.columns;
|
||||
const rows = props.result.rows;
|
||||
const items: RowItem[] = sortedRows.value.map((sourceIndex) => {
|
||||
const items: Omit<RowItem, "displayIndex">[] = sortedRows.value.map((sourceIndex) => {
|
||||
const row = rows[sourceIndex];
|
||||
const dirty = dirtyRows.value.get(sourceIndex);
|
||||
const data = rowDataWithChanges(row, sourceIndex);
|
||||
|
|
@ -1598,7 +1581,9 @@ const displayItems = computed<RowItem[]>(() => {
|
|||
status: "new",
|
||||
});
|
||||
});
|
||||
return items.filter((item) => matchesRowStatusFilter(item.status, rowStatusFilter.value));
|
||||
return items
|
||||
.filter((item) => matchesRowStatusFilter(item.status, rowStatusFilter.value))
|
||||
.map((item, displayIndex) => ({ ...item, displayIndex }));
|
||||
});
|
||||
|
||||
watch(
|
||||
|
|
@ -1834,36 +1819,6 @@ function isRowActive(index: number): boolean {
|
|||
return index >= range.startRow && index <= range.endRow;
|
||||
}
|
||||
|
||||
function rowSelectionMemoKey(item: RowItem, index: number): string {
|
||||
const rowSelected = selectedRowIds.value.has(item.id) ? "row" : "";
|
||||
const range = selectedRange.value;
|
||||
if (!range || index < range.startRow || index > range.endRow) return rowSelected;
|
||||
return `${rowSelected}|cells:${range.startCol}:${range.endCol}`;
|
||||
}
|
||||
|
||||
function rowHoverMemoKey(index: number): string {
|
||||
return hoveredDetailCell.value?.rowIndex === index ? String(hoveredDetailCell.value.col) : "";
|
||||
}
|
||||
|
||||
function rowEditMemoKey(item: RowItem): string {
|
||||
return editingCell.value?.rowId === item.id ? `${editingCell.value.col}:${editValue.value}` : "";
|
||||
}
|
||||
|
||||
function rowRenderMemoDeps(item: RowItem, index: number) {
|
||||
return [
|
||||
item.data,
|
||||
item.status,
|
||||
item.isDirtyCol,
|
||||
rowSelectionMemoKey(item, index),
|
||||
rowSearchMemoKey(index),
|
||||
rowHoverMemoKey(index),
|
||||
rowEditMemoKey(item),
|
||||
visibleColumnIndexes.value,
|
||||
columnFormatterMemoKey.value,
|
||||
cellEditabilityMemoKey.value,
|
||||
];
|
||||
}
|
||||
|
||||
const contextRowItem = computed(() => (contextCell.value ? getRowItem(contextCell.value.rowId) : undefined));
|
||||
const contextColumn = computed(() => {
|
||||
if (!contextCell.value || contextCell.value.col < 0) return null;
|
||||
|
|
@ -2079,6 +2034,9 @@ function setDetailNull() {
|
|||
|
||||
function toggleSort(colName: string, colIdx: number) {
|
||||
if (getIsResizing()) return;
|
||||
orderByInput.value = "";
|
||||
currentPage.value = 1;
|
||||
resetGridVerticalScroll(true);
|
||||
if (sortCol.value === colName && sortColIndex.value === colIdx) {
|
||||
if (sortDir.value === "asc") {
|
||||
sortDir.value = "desc";
|
||||
|
|
@ -4083,18 +4041,21 @@ defineExpose({
|
|||
key-field="id"
|
||||
@scroll="onScrollerScroll"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
class="flex text-xs border-b border-border"
|
||||
v-memo="rowRenderMemoDeps(item, index)"
|
||||
:class="{
|
||||
'bg-destructive/5 opacity-70': item.isDeleted,
|
||||
'bg-primary/5': item.isNew && !isRowActive(index),
|
||||
'bg-muted/30': !item.isNew && !item.isDeleted && !isRowActive(index) && index % 2 === 1,
|
||||
'active-row': isRowActive(index) && !item.isDeleted,
|
||||
'bg-primary/5': item.isNew && !isRowActive(item.displayIndex),
|
||||
'bg-muted/30':
|
||||
!item.isNew &&
|
||||
!item.isDeleted &&
|
||||
!isRowActive(item.displayIndex) &&
|
||||
item.displayIndex % 2 === 1,
|
||||
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
|
||||
}"
|
||||
:style="{ height: '26px', width: 'var(--total-w)' }"
|
||||
:data-row-index="index"
|
||||
:data-row-index="item.displayIndex"
|
||||
>
|
||||
<div
|
||||
class="shrink-0 px-2 py-1 border-r border-border text-center select-none cursor-default hover:bg-accent/50"
|
||||
|
|
@ -4109,11 +4070,11 @@ defineExpose({
|
|||
},
|
||||
]"
|
||||
:style="{ width: 'var(--row-num-w)' }"
|
||||
@click="handleRowClick(index, item.id, $event)"
|
||||
@dblclick.stop="toggleTranspose(index)"
|
||||
@contextmenu="onRowContext(item.id, index)"
|
||||
@click="handleRowClick(item.displayIndex, item.id, $event)"
|
||||
@dblclick.stop="toggleTranspose(item.displayIndex)"
|
||||
@contextmenu="onRowContext(item.id, item.displayIndex)"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
{{ item.displayIndex + 1 }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(actualColIdx, visibleColIdx) in visibleColumnIndexes"
|
||||
|
|
@ -4123,21 +4084,23 @@ defineExpose({
|
|||
:class="{
|
||||
'text-muted-foreground italic': isNull(item.data[actualColIdx]),
|
||||
'bg-yellow-500/10 cell-dirty': item.isDirtyCol[actualColIdx],
|
||||
'cell-selected': cellIsSelected(index, visibleColIdx) && !item.isDirtyCol[actualColIdx],
|
||||
'cell-selected-dirty': cellIsSelected(index, visibleColIdx) && item.isDirtyCol[actualColIdx],
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(index, actualColIdx),
|
||||
'cell-selected':
|
||||
cellIsSelected(item.displayIndex, visibleColIdx) && !item.isDirtyCol[actualColIdx],
|
||||
'cell-selected-dirty':
|
||||
cellIsSelected(item.displayIndex, visibleColIdx) && item.isDirtyCol[actualColIdx],
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, actualColIdx),
|
||||
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40':
|
||||
cellIsCurrentMatch(index, actualColIdx),
|
||||
cellIsCurrentMatch(item.displayIndex, actualColIdx),
|
||||
'tabular-nums': typeof item.data[actualColIdx] === 'number',
|
||||
'cursor-text hover:bg-accent/50': canEditCellItem(item, actualColIdx),
|
||||
'line-through': item.isDeleted,
|
||||
}"
|
||||
@mousedown="handleDataCellMousedown(index, visibleColIdx, item.id, $event)"
|
||||
@mouseenter="onCellMouseenter(index, visibleColIdx, actualColIdx)"
|
||||
@mouseleave="onCellMouseleave(index, actualColIdx)"
|
||||
@mousedown="handleDataCellMousedown(item.displayIndex, visibleColIdx, item.id, $event)"
|
||||
@mouseenter="onCellMouseenter(item.displayIndex, visibleColIdx, actualColIdx)"
|
||||
@mouseleave="onCellMouseleave(item.displayIndex, actualColIdx)"
|
||||
@dblclick="canEditCellItem(item, actualColIdx) && startEdit(item.id, actualColIdx)"
|
||||
:data-visible-col-index="visibleColIdx"
|
||||
@contextmenu="onCellContext(item.id, index, actualColIdx, visibleColIdx)"
|
||||
@contextmenu="onCellContext(item.id, item.displayIndex, actualColIdx, visibleColIdx)"
|
||||
>
|
||||
<template v-if="editingCell?.rowId === item.id && editingCell?.col === actualColIdx">
|
||||
<TemporalCellEditor
|
||||
|
|
@ -4162,11 +4125,11 @@ defineExpose({
|
|||
<template v-else>
|
||||
{{ formatCell(item.data[actualColIdx], actualColIdx) }}
|
||||
<button
|
||||
v-if="cellDetailButtonVisible(index, actualColIdx)"
|
||||
v-if="cellDetailButtonVisible(item.displayIndex, actualColIdx)"
|
||||
class="absolute right-0.5 top-0.5 flex h-5 w-5 items-center justify-center rounded bg-background/90 text-muted-foreground shadow-sm ring-1 ring-border hover:text-foreground"
|
||||
:title="t('grid.cellDetails')"
|
||||
@mousedown.stop
|
||||
@click.stop="showCellDetailsForVisibleCell(index, visibleColIdx, actualColIdx)"
|
||||
@click.stop="showCellDetailsForVisibleCell(item.displayIndex, visibleColIdx, actualColIdx)"
|
||||
>
|
||||
<Info class="h-3 w-3" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -679,6 +679,23 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_sorted_query_sql_for_first_result_column() {
|
||||
let result = build_sorted_query_sql(SortedQuerySqlOptions {
|
||||
original_sql: "SELECT iso3, year, gdp_pc FROM country_gdp".to_string(),
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
result_columns: vec!["iso3".to_string(), "year".to_string(), "gdp_pc".to_string()],
|
||||
column_index: 0,
|
||||
column: "iso3".to_string(),
|
||||
direction: QuerySortDirection::Asc,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result.sql.unwrap(),
|
||||
"SELECT * FROM (SELECT iso3, year, gdp_pc FROM country_gdp) t(\"iso3\", \"year\", \"gdp_pc\") ORDER BY \"iso3\" ASC;"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_sorted_query_without_alias_list() {
|
||||
let result = build_sorted_query_sql(SortedQuerySqlOptions {
|
||||
|
|
|
|||
|
|
@ -443,6 +443,26 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_table_data_order_overrides_default_key_order() {
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("public".to_string()),
|
||||
table_name: "country_gdp".to_string(),
|
||||
primary_keys: vec!["year".to_string()],
|
||||
columns: vec!["iso3".to_string(), "year".to_string(), "gdp_pc".to_string()],
|
||||
fallback_order_columns: Vec::new(),
|
||||
order_by: Some("\"iso3\" ASC".to_string()),
|
||||
limit: Some(100),
|
||||
offset: None,
|
||||
where_input: None,
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT * FROM \"public\".\"country_gdp\" ORDER BY \"iso3\" ASC LIMIT 100;"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_table_data_special_column_queries() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,21 @@ test("toolbar refresh preserves header sort order", () => {
|
|||
assert.doesNotMatch(match[1], /orderByInput\.value\.trim\(\) \|\| undefined/);
|
||||
});
|
||||
|
||||
test("header sort starts from first page and top row", () => {
|
||||
const match = source.match(/function toggleSort\(colName: string, colIdx: number\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define toggleSort");
|
||||
assert.match(match[1], /orderByInput\.value = ""/);
|
||||
assert.match(match[1], /currentPage\.value = 1/);
|
||||
assert.match(match[1], /resetGridVerticalScroll\(true\)/);
|
||||
});
|
||||
|
||||
test("visible row numbers use display index after sorting", () => {
|
||||
assert.match(source, /displayIndex: number/);
|
||||
assert.match(source, /\.map\(\(item, displayIndex\) => \(\{ \.\.\.item, displayIndex \}\)\)/);
|
||||
assert.match(source, /<template #default="\{ item \}">/);
|
||||
assert.match(source, /\{\{ item\.displayIndex \+ 1 \}\}/);
|
||||
});
|
||||
|
||||
test("rollback refresh preserves header sort order", () => {
|
||||
const match = source.match(/function onToolbarRollback\(\) \{([\s\S]*?)\n\}/);
|
||||
assert.ok(match, "DataGrid should define onToolbarRollback");
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ import test from "node:test";
|
|||
|
||||
const source = readFileSync("apps/desktop/src/components/grid/DataGrid.vue", "utf8");
|
||||
|
||||
test("data grid memoizes virtual rows with render-affecting dependencies", () => {
|
||||
assert.match(source, /function rowRenderMemoDeps\(item: RowItem, index: number\)/);
|
||||
assert.match(source, /function rowSelectionMemoKey\(item: RowItem, index: number\)/);
|
||||
assert.match(source, /function rowSearchMemoKey\(index: number\)/);
|
||||
assert.match(source, /function rowHoverMemoKey\(index: number\)/);
|
||||
assert.match(source, /const columnFormatterMemoKey = computed/);
|
||||
assert.match(source, /v-memo="rowRenderMemoDeps\(item, index\)"/);
|
||||
test("data grid does not memoize virtual rows", () => {
|
||||
assert.doesNotMatch(source, /v-memo=/);
|
||||
assert.doesNotMatch(source, /function rowRenderMemoDeps/);
|
||||
assert.match(source, /\{\{ item\.displayIndex \+ 1 \}\}/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue