mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 09:23:26 +00:00
feat(infinite-scroll): implement infinite scroll feature with customizable max rows
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: "单击即可打开侧边栏中的可操作项目。",
|
||||
|
||||
@@ -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: "單擊即可開啟側邊欄中的可操作項目。",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user