feat(infinite-scroll): implement infinite scroll feature with customizable max rows

This commit is contained in:
Libon
2026-06-15 23:05:35 +08:00
committed by GitHub
parent d053facdd5
commit 1991bf118b
9 changed files with 260 additions and 62 deletions
@@ -106,6 +106,8 @@ const debugLogDownloaded = ref(false);
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editInfiniteScroll = ref(settingsStore.editorSettings.infiniteScroll);
const editInfiniteScrollMaxRows = ref(settingsStore.editorSettings.infiniteScrollMaxRows);
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const editSqlFormatter = ref<SqlFormatterSettings>(normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter));
@@ -347,6 +349,8 @@ watch(
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
editInfiniteScroll.value = settingsStore.editorSettings.infiniteScroll;
editInfiniteScrollMaxRows.value = settingsStore.editorSettings.infiniteScrollMaxRows;
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
editSqlFormatter.value = normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter);
@@ -401,6 +405,8 @@ function hasChanges(): boolean {
editShowColumnCommentsInHeader.value !== settingsStore.editorSettings.showColumnCommentsInHeader ||
editShowColumnTypesInHeader.value !== settingsStore.editorSettings.showColumnTypesInHeader ||
editCompactColumnHeaderActions.value !== settingsStore.editorSettings.compactColumnHeaderActions ||
editInfiniteScroll.value !== settingsStore.editorSettings.infiniteScroll ||
editInfiniteScrollMaxRows.value !== settingsStore.editorSettings.infiniteScrollMaxRows ||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
JSON.stringify(editSqlFormatter.value) !== JSON.stringify(normalizeSqlFormatterSettings(settingsStore.editorSettings.sqlFormatter)) ||
@@ -436,6 +442,8 @@ async function persistSettings() {
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
infiniteScroll: editInfiniteScroll.value,
infiniteScrollMaxRows: editInfiniteScrollMaxRows.value,
redisScanPageSize: editRedisScanPageSize.value,
shortcuts: editShortcuts.value,
sqlFormatter: normalizeSqlFormatterSettings(editSqlFormatter.value),
@@ -488,6 +496,8 @@ function resetDefaults() {
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
editInfiniteScroll.value = DEFAULT_EDITOR_SETTINGS.infiniteScroll;
editInfiniteScrollMaxRows.value = DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
editSqlFormatter.value = normalizeSqlFormatterSettings(DEFAULT_EDITOR_SETTINGS.sqlFormatter);
@@ -1801,6 +1811,36 @@ watch(
</div>
<Switch id="compact-column-header-actions" v-model="editCompactColumnHeaderActions" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="infinite-scroll">
{{ t("settings.infiniteScroll") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.infiniteScrollDescription") }}
</p>
</div>
<Switch id="infinite-scroll" v-model="editInfiniteScroll" />
</div>
<div v-if="editInfiniteScroll" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="infinite-scroll-max-rows">
{{ t("settings.infiniteScrollMaxRows") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.infiniteScrollMaxRowsDescription") }}
</p>
</div>
<Input
id="infinite-scroll-max-rows"
v-model="editInfiniteScrollMaxRows"
type="number"
inputmode="numeric"
:min="1000"
:max="50000"
class="h-7 w-24 px-2 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
</div>
</div>
<Separator />
+165 -50
View File
@@ -277,6 +277,8 @@ const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.s
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions);
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
const infiniteScrollEnabled = computed(() => settingsStore.editorSettings.infiniteScroll);
const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows);
function headerColumnComment(column: string): string {
if (!showColumnCommentsInHeader.value) return "";
@@ -1883,7 +1885,10 @@ function markGridScrolling() {
function onScrollerScroll(e: Event) {
syncHeaderScroll(e);
const target = e.target;
recordScrollPosition(target instanceof HTMLElement ? { top: target.scrollTop, left: target.scrollLeft } : undefined);
if (target instanceof HTMLElement) {
recordScrollPosition({ top: target.scrollTop, left: target.scrollLeft });
checkInfiniteScroll(target);
}
markGridScrolling();
}
@@ -1931,6 +1936,11 @@ const pageSize = ref(normalizeResultPageSize(settingsStore.editorSettings.pageSi
const currentPage = ref(1);
const pageSizeOptions = computed(() => resultPageSizeMenuOptions(pageSize.value));
const customPageSizeInput = ref(String(pageSize.value));
const infiniteScrollLoading = ref(false);
const isInfiniteScrollPaginating = ref(false);
let lastInfiniteScrollPage = 0;
let infiniteScrollCheckScheduled = false;
let infiniteScrollAllLoaded = false;
watch(pageSize, (value) => {
customPageSizeInput.value = String(value);
});
@@ -1940,16 +1950,43 @@ watch(
pageSize.value = normalizeResultPageSize(value, pageSize.value);
},
);
watch(
() => infiniteScrollEnabled.value,
(enabled, prevEnabled) => {
// Switched between paginated and infinite scroll: reset to first page
if (enabled !== prevEnabled) {
resetInfiniteScrollState();
emit("paginate", 0, pageSize.value, currentWhereInput(), currentOrderBy());
}
},
);
watch(
() => [props.pageOffset, props.pageLimit],
([offset, limit]) => {
if (typeof offset !== "number" || typeof limit !== "number" || limit <= 0) return;
// Skip resetting pagination state during infinite scroll pagination
if (isInfiniteScrollPaginating.value) return;
const normalizedLimit = normalizeResultPageSize(limit);
pageSize.value = normalizedLimit;
currentPage.value = Math.floor(offset / normalizedLimit) + 1;
},
{ immediate: true },
);
// Clear infinite-scroll loading when the parent finishes loading new data
watch(
() => props.loading,
(loading, prevLoading) => {
if (prevLoading && !loading && infiniteScrollLoading.value) {
infiniteScrollLoading.value = false;
isInfiniteScrollPaginating.value = false;
// Detect if the backend returned no new data for this page
const expectedRows = currentPage.value * pageSize.value;
if (props.result.rows.length < expectedRows) {
infiniteScrollAllLoaded = true;
}
}
},
);
const manualTotalRowCount = ref<number | undefined>(undefined);
const manualTotalRowCountLoading = ref(false);
const showTruncationWarning = computed(() => props.result.truncated === true && typeof props.pageLimit !== "number" && props.result.has_more !== true);
@@ -2042,6 +2079,8 @@ watch(
() => [props.countSql ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? "", currentWhereInput() ?? "", props.database ?? "", props.connectionId ?? "", props.result],
() => {
manualTotalRowCount.value = undefined;
// Reset infinite-scroll allLoaded when query context changes
infiniteScrollAllLoaded = false;
},
);
@@ -2071,6 +2110,7 @@ watch(
function firstPage() {
if (currentPage.value <= 1) return;
currentPage.value = 1;
lastInfiniteScrollPage = 0;
resetGridVerticalScroll(true);
emit("paginate", 0, pageSize.value, currentWhereInput(), currentOrderBy());
}
@@ -2086,11 +2126,50 @@ function nextPage() {
resetGridVerticalScroll(true);
emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy());
}
function infiniteScrollNextPage() {
if (infiniteScrollLoading.value || props.loading) return;
const nextPageNum = currentPage.value + 1;
const cumulativeLimit = nextPageNum * pageSize.value;
if (cumulativeLimit > infiniteScrollMaxRows.value) return;
// Stop if we already know all data is loaded
if (infiniteScrollAllLoaded) return;
// Skip if we already have this many rows loaded (e.g. cached data)
if (props.result.rows.length >= cumulativeLimit) {
currentPage.value = nextPageNum;
return;
}
infiniteScrollLoading.value = true;
isInfiniteScrollPaginating.value = true;
currentPage.value = nextPageNum;
// Load cumulative data (all rows up to current page) to append instead of replace
emit("paginate", 0, cumulativeLimit, currentWhereInput(), currentOrderBy());
}
function checkInfiniteScroll(scroller: HTMLElement) {
if (!infiniteScrollEnabled.value || infiniteScrollLoading.value || props.loading) return;
if (infiniteScrollAllLoaded) return;
if (infiniteScrollCheckScheduled) return;
infiniteScrollCheckScheduled = true;
requestAnimationFrame(() => {
infiniteScrollCheckScheduled = false;
const { scrollTop, scrollHeight, clientHeight } = scroller;
const threshold = 100;
const distanceToBottom = scrollHeight - scrollTop - clientHeight;
// Only trigger when near bottom AND page has changed since last trigger
if (distanceToBottom < threshold && currentPage.value !== lastInfiniteScrollPage) {
lastInfiniteScrollPage = currentPage.value;
infiniteScrollNextPage();
}
});
}
function changePageSize(size: number) {
const normalizedSize = normalizeResultPageSize(size);
pageSize.value = normalizedSize;
settingsStore.updateEditorSettings({ pageSize: normalizedSize });
currentPage.value = 1;
lastInfiniteScrollPage = 0;
infiniteScrollAllLoaded = false;
resetGridVerticalScroll(true);
emit("paginate", 0, normalizedSize, currentWhereInput(), currentOrderBy());
}
@@ -2100,6 +2179,7 @@ function applyCustomPageSize() {
}
async function lastPage() {
if (infiniteScrollEnabled.value) return;
if (hasKnownTotalRowCount.value) {
const total = displayedTotalRowCount.value ?? 0;
if (total <= 0) return;
@@ -2394,12 +2474,25 @@ function canDeleteRowItem(item: RowItem | undefined): boolean {
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
}
function resetInfiniteScrollState() {
currentPage.value = 1;
lastInfiniteScrollPage = 0;
infiniteScrollAllLoaded = false;
isInfiniteScrollPaginating.value = false;
infiniteScrollLoading.value = false;
resetGridVerticalScroll(true);
}
async function onToolbarRefresh() {
if (transactionActive.value) {
discardChanges();
}
// Reset infinite scroll state on refresh
if (infiniteScrollEnabled.value) {
resetInfiniteScrollState();
}
preserveTransposeOnNextResult.value = showTranspose.value;
emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, (currentPage.value - 1) * pageSize.value);
emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, 0);
}
async function onToolbarCommit() {
@@ -2409,7 +2502,11 @@ async function onToolbarCommit() {
function onToolbarRollback() {
preserveTransposeOnNextResult.value = showTranspose.value;
discardChanges();
emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, (currentPage.value - 1) * pageSize.value);
// Reset infinite scroll state on rollback
if (infiniteScrollEnabled.value) {
resetInfiniteScrollState();
}
emit("reload", props.sql, searchText.value, currentWhereInput(), currentOrderBy(), pageSize.value, 0);
}
function addRow() {
@@ -3682,6 +3779,7 @@ function onCanvasScroll(event: Event) {
recordScrollPosition({ top: scrollTop, left: scrollLeft });
markGridScrolling();
scheduleCanvasDraw();
checkInfiniteScroll(scroller);
}
function canvasWheelDeltaToPixels(delta: number, deltaMode: number, pageSize: number): number {
@@ -6819,7 +6917,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</div>
<div v-else-if="useCanvasGridRows" ref="scrollerRef" class="data-grid-scroller canvas-grid-scroller flex-1 overflow-auto overscroll-none bg-background" :class="{ 'is-scrolling': isScrolling }" @scroll="onCanvasScroll" @wheel="onCanvasWheel">
<div v-else-if="useCanvasGridRows" ref="scrollerRef" class="data-grid-scroller canvas-grid-scroller flex-1 overflow-auto overscroll-none bg-background relative" :class="{ 'is-scrolling': isScrolling }" @scroll="onCanvasScroll" @wheel="onCanvasWheel">
<div class="relative" :style="{ width: `${totalWidth}px`, height: `${canvasContentHeight}px` }">
<canvas
ref="canvasRef"
@@ -6883,6 +6981,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</div>
</div>
<!-- Infinite scroll loading indicator for Canvas -->
<div v-if="infiniteScrollEnabled && infiniteScrollLoading" class="absolute bottom-0 left-0 right-0 flex items-center justify-center py-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm z-10">
<Loader2 class="w-3 h-3 animate-spin mr-1" />
{{ t("grid.loadingMore") }}
</div>
</div>
<!-- Virtual scrolled rows -->
@@ -6997,6 +7100,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</div>
</template>
</RecycleScroller>
<!-- Infinite scroll loading indicator for RecycleScroller -->
<div v-if="infiniteScrollEnabled && infiniteScrollLoading && !loading" class="flex items-center justify-center py-2 text-xs text-muted-foreground">
<Loader2 class="w-3 h-3 animate-spin mr-1" />
{{ t("grid.loadingMore") }}
</div>
<div v-if="hasGridHorizontalOverflow" ref="gridHorizontalScrollbarTrackRef" class="data-grid-horizontal-scrollbar" :class="{ 'data-grid-horizontal-scrollbar--dragging': gridHorizontalScrollbarDragging }" @pointerdown="startGridHorizontalScrollbarDrag">
<div class="data-grid-horizontal-scrollbar__thumb" :style="gridHorizontalScrollbarThumbStyle" />
</div>
@@ -7415,52 +7523,59 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
<div class="flex min-w-0 items-center justify-end gap-1">
<Loader2 v-if="loading" class="w-3 h-3 animate-spin text-muted-foreground" />
<LightDropdown
:model-value="String(pageSize)"
:items="pageSizeMenuItems"
:trigger-label="`${pageSize}${t('grid.rowsPerPageShort')}`"
trigger-class="inline-flex h-5 items-center justify-center rounded-md px-1.5 text-xs hover:bg-accent hover:text-accent-foreground"
content-class="w-36"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="selectPageSizeMenuItem"
>
<div class="bg-border -mx-1 my-1 h-px" />
<div class="text-muted-foreground px-1.5 py-1 text-xs">{{ t("grid.customRowsPerPage") }}</div>
<div class="flex items-center gap-1 px-1.5 pb-1" @click.stop @keydown.stop>
<Input
v-model="customPageSizeInput"
type="number"
inputmode="numeric"
:min="MIN_RESULT_PAGE_SIZE"
:max="MAX_RESULT_PAGE_SIZE"
class="h-6 w-20 px-1.5 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
@keydown.enter.prevent.stop="applyCustomPageSize"
/>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-6 w-6 shrink-0" :aria-label="t('grid.applyPageSize')" @click.stop="applyCustomPageSize">
<Check class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.applyPageSize") }}</TooltipContent>
</Tooltip>
</div>
</LightDropdown>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="currentPage <= 1" @click="firstPage">
<ChevronsLeft class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="currentPage <= 1" @click="prevPage">
<ChevronLeft class="h-3 w-3" />
</Button>
<span>{{ currentPage }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canGoNextPage" @click="nextPage">
<ChevronRight class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canJumpLastPage" @click="lastPage">
<ChevronsRight class="h-3 w-3" />
</Button>
<template v-if="infiniteScrollEnabled">
<span v-if="infiniteScrollAllLoaded" class="text-xs text-muted-foreground shrink-0">
{{ t("grid.allLoaded") }}
</span>
</template>
<template v-if="!infiniteScrollEnabled">
<LightDropdown
:model-value="String(pageSize)"
:items="pageSizeMenuItems"
:trigger-label="`${pageSize}${t('grid.rowsPerPageShort')}`"
trigger-class="inline-flex h-5 items-center justify-center rounded-md px-1.5 text-xs hover:bg-accent hover:text-accent-foreground"
content-class="w-36"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="selectPageSizeMenuItem"
>
<div class="bg-border -mx-1 my-1 h-px" />
<div class="text-muted-foreground px-1.5 py-1 text-xs">{{ t("grid.customRowsPerPage") }}</div>
<div class="flex items-center gap-1 px-1.5 pb-1" @click.stop @keydown.stop>
<Input
v-model="customPageSizeInput"
type="number"
inputmode="numeric"
:min="MIN_RESULT_PAGE_SIZE"
:max="MAX_RESULT_PAGE_SIZE"
class="h-6 w-20 px-1.5 text-xs tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
@keydown.enter.prevent.stop="applyCustomPageSize"
/>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-6 w-6 shrink-0" :aria-label="t('grid.applyPageSize')" @click.stop="applyCustomPageSize">
<Check class="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{{ t("grid.applyPageSize") }}</TooltipContent>
</Tooltip>
</div>
</LightDropdown>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="currentPage <= 1" @click="firstPage">
<ChevronsLeft class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="currentPage <= 1" @click="prevPage">
<ChevronLeft class="h-3 w-3" />
</Button>
<span>{{ currentPage }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canGoNextPage" @click="nextPage">
<ChevronRight class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canJumpLastPage" @click="lastPage">
<ChevronsRight class="h-3 w-3" />
</Button>
</template>
<LightDropdown
model-value=""
:items="exportMenuItems"
+6
View File
@@ -489,6 +489,8 @@
totalRows: "Total {count} rows",
totalRowCount: "({count} total)",
totalRowCountLoading: "(counting...)",
loadingMore: "Loading more data...",
allLoaded: "all loaded",
calculateTotalRows: "Count total rows",
calculateTotalRowsInline: "(count total rows)",
calculateTotalRowsFailed: "Count failed: {message}",
@@ -2159,6 +2161,10 @@
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
compactColumnHeaderActions: "Compact column header tools",
compactColumnHeaderActionsDescription: "Move formatter and local filter tools into a more menu so column names get priority.",
infiniteScroll: "Infinite scroll loading",
infiniteScrollDescription: "Automatically load the next page of data when scrolling to the bottom of the table.",
infiniteScrollMaxRows: "Infinite scroll max rows",
infiniteScrollMaxRowsDescription: "Maximum number of rows to load in infinite scroll mode (1000–50000).",
sidebarActivation: "Sidebar activation",
sidebarActivationSingle: "Single click",
sidebarActivationSingleDescription: "Open actionable sidebar items with one click.",
+6
View File
@@ -435,6 +435,8 @@
totalRows: "Total {count} filas",
totalRowCount: "({count} en total)",
totalRowCountLoading: "(contando...)",
loadingMore: "Cargando más datos...",
allLoaded: "todo cargado",
calculateTotalRows: "Contar filas totales",
calculateTotalRowsInline: "(contar filas totales)",
calculateTotalRowsFailed: "Error al contar: {message}",
@@ -1859,6 +1861,10 @@
showColumnTypesInHeaderDescription: "Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
compactColumnHeaderActions: "Compactar herramientas del encabezado",
compactColumnHeaderActionsDescription: "Mueve formatear y filtrar a un menú de más acciones para priorizar el nombre de columna.",
infiniteScroll: "Carga de desplazamiento infinito",
infiniteScrollDescription: "Carga automáticamente la siguiente página de datos al desplazarse hasta el final de la tabla.",
infiniteScrollMaxRows: "Máximo de filas en desplazamiento infinito",
infiniteScrollMaxRowsDescription: "Número máximo de filas a cargar en modo desplazamiento infinito (1000–50000).",
sidebarActivation: "Activación de la barra lateral",
sidebarActivationSingle: "Un clic",
sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.",
+6
View File
@@ -440,6 +440,8 @@
totalRows: "Totale {count} righe",
totalRowCount: "({count} in totale)",
totalRowCountLoading: "(conteggio...)",
loadingMore: "Caricamento altri dati...",
allLoaded: "tutto caricato",
calculateTotalRows: "Conta righe totali",
calculateTotalRowsInline: "(conta righe totali)",
calculateTotalRowsFailed: "Conteggio non riuscito: {message}",
@@ -1926,6 +1928,10 @@
showColumnTypesInHeaderDescription: "Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
compactColumnHeaderActions: "Strumenti intestazione colonna compatti",
compactColumnHeaderActionsDescription: "Sposta gli strumenti del formattatore e dei filtri locali in un menu 'altro' in modo che i nomi delle colonne abbiano la priorità.",
infiniteScroll: "Caricamento a scorrimento infinito",
infiniteScrollDescription: "Carica automaticamente la pagina successiva dei dati quando scorri fino in fondo alla tabella.",
infiniteScrollMaxRows: "Righe max a scorrimento infinito",
infiniteScrollMaxRowsDescription: "Numero massimo di righe da caricare in modalità scorrimento infinito (1000–50000).",
sidebarActivation: "Attivazione barra laterale",
sidebarActivationSingle: "Singolo clic",
sidebarActivationSingleDescription: "Apri gli elementi della barra laterale con un solo clic.",
+6
View File
@@ -440,6 +440,8 @@
totalRows: "Total de {count} linhas",
totalRowCount: "({count} no total)",
totalRowCountLoading: "(contando...)",
loadingMore: "Carregando mais dados...",
allLoaded: "tudo carregado",
calculateTotalRows: "Contar total de linhas",
calculateTotalRowsInline: "(contar total de linhas)",
calculateTotalRowsFailed: "Falha ao contar: {message}",
@@ -1937,6 +1939,10 @@
showColumnTypesInHeaderDescription: "Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna",
compactColumnHeaderActionsDescription: "Mover as ferramentas de formatação e filtro local para um menu de mais opções para que os nomes das colunas tenham prioridade.",
infiniteScroll: "Carregamento por rolagem infinita",
infiniteScrollDescription: "Carregar automaticamente a próxima página de dados ao rolar até o final da tabela.",
infiniteScrollMaxRows: "Máximo de linhas em rolagem infinita",
infiniteScrollMaxRowsDescription: "Número máximo de linhas a carregar no modo de rolagem infinita (1000–50000).",
sidebarActivation: "Ativação da barra lateral",
sidebarActivationSingle: "Clique único",
sidebarActivationSingleDescription: "Abrir itens acionáveis da barra lateral com um clique.",
+6
View File
@@ -490,6 +490,8 @@
totalRows: "共 {count} 行",
totalRowCount: "(总计 {count} 行)",
totalRowCountLoading: "(统计中...)",
loadingMore: "加载更多数据...",
allLoaded: "已全部加载",
calculateTotalRows: "统计总行数",
calculateTotalRowsInline: "(统计总行数)",
calculateTotalRowsFailed: "统计失败:{message}",
@@ -2183,6 +2185,10 @@
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
compactColumnHeaderActions: "收起字段表头工具",
compactColumnHeaderActionsDescription: "将格式化、本地筛选收进更多菜单,优先显示字段名称。",
infiniteScroll: "无限滚动加载",
infiniteScrollDescription: "滚动到表格底部时自动加载下一页数据,无需手动翻页。",
infiniteScrollMaxRows: "无限滚动最大行数",
infiniteScrollMaxRowsDescription: "无限滚动模式下最多加载的行数(1000–50000)。",
sidebarActivation: "侧边栏打开方式",
sidebarActivationSingle: "单击打开",
sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。",
+6
View File
@@ -440,6 +440,8 @@
totalRows: "共 {count} 筆",
totalRowCount: "(總計 {count} 筆)",
totalRowCountLoading: "(統計中...)",
loadingMore: "載入更多資料...",
allLoaded: "已全部載入",
calculateTotalRows: "統計總筆數",
calculateTotalRowsInline: "(統計總筆數)",
calculateTotalRowsFailed: "統計失敗:{message}",
@@ -1963,6 +1965,10 @@
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
compactColumnHeaderActions: "收起欄位表頭工具",
compactColumnHeaderActionsDescription: "將格式設定和本機篩選工具移到更多選單,讓欄位名稱優先顯示。",
infiniteScroll: "無限滾動載入",
infiniteScrollDescription: "滾動到表格底部時自動載入下一頁資料,無需手動翻頁。",
infiniteScrollMaxRows: "無限滾動最大筆數",
infiniteScrollMaxRowsDescription: "無限滾動模式下最多載入的筆數(1000–50000)。",
sidebarActivation: "側邊欄開啟方式",
sidebarActivationSingle: "單擊開啟",
sidebarActivationSingleDescription: "單擊即可開啟側邊欄中的可操作項目。",
+19 -12
View File
@@ -279,6 +279,8 @@ export interface EditorSettings {
compactTabTitle: boolean;
appLayout: "separated" | "classic";
pageSize: number;
infiniteScroll: boolean;
infiniteScrollMaxRows: number;
redisScanPageSize: number;
mongoViewMode: "document" | "table";
showColumnCommentsInHeader: boolean;
@@ -375,6 +377,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
compactTabTitle: false,
appLayout: "classic",
pageSize: 100,
infiniteScroll: false,
infiniteScrollMaxRows: 5000,
redisScanPageSize: 1000,
mongoViewMode: "document",
showColumnCommentsInHeader: false,
@@ -518,18 +522,16 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
};
});
}
return [
...(settings.customThemeColors
? [
{
id: "migrated",
name: "Migrated",
colors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...settings.customThemeColors },
ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS },
},
]
: []),
];
return settings.customThemeColors
? [
{
id: "migrated",
name: "Migrated",
colors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...settings.customThemeColors },
ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS },
},
]
: [];
})(),
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
@@ -538,6 +540,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
pageSize: normalizeResultPageSize(settings.pageSize),
infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll,
infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows,
redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize,
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
@@ -693,6 +697,9 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize);
if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll;
if (partial.infiniteScrollMaxRows !== undefined)
editorSettings.value.infiniteScrollMaxRows = typeof partial.infiniteScrollMaxRows === "number" && partial.infiniteScrollMaxRows >= 1000 && partial.infiniteScrollMaxRows <= 50000 ? Math.round(partial.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows;
if (partial.redisScanPageSize !== undefined) editorSettings.value.redisScanPageSize = partial.redisScanPageSize;
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;