feat(grid): load all remaining rows with confirmation guard
This commit is contained in:
25 files changed
+336
-27
No files matched your search
@@ -2957,6 +2957,7 @@ defineExpose({ focusSearch });
|
||||
:total-row-count-is-exact="totalIsExact"
|
||||
:inexact-total-row-count-mode="documentStoreProvider.kind === 'mongodb' ? 'estimated' : 'at-least'"
|
||||
:pagination-total-row-count="pageTotal"
|
||||
:load-all-rows-enabled="false"
|
||||
:count-total-rows="countExactDocumentTotal"
|
||||
:full-export-result="documentStoreProvider.kind === 'mongodb' || documentStoreProvider.kind === 'dynamodb' || documentStoreProvider.kind === 'elasticsearch' ? exportAllDocumentStoreDocuments : undefined"
|
||||
@sort="onSort"
|
||||
|
||||
@@ -208,6 +208,7 @@ import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid
|
||||
import {
|
||||
canFetchNextDataGridSegment,
|
||||
canGoNextDataGridPage,
|
||||
dataGridLoadAllSegment,
|
||||
dataGridTotalRowCountLabelKey,
|
||||
dataGridTruncationHintKey,
|
||||
ELASTICSEARCH_PAGE_JUMP_WARNING_REQUESTS,
|
||||
@@ -493,6 +494,7 @@ interface DataGridProps {
|
||||
inexactTotalRowCountMode?: DataGridInexactTotalRowCountMode;
|
||||
paginationTotalRowCount?: number;
|
||||
paginationEnabled?: boolean;
|
||||
loadAllRowsEnabled?: boolean;
|
||||
totalRowCountLoading?: boolean;
|
||||
pageJumpProgress?: QueryPageJumpProgress;
|
||||
/** Document stores (e.g. MongoDB) count exactly on demand without SQL tableMeta/countSql. */
|
||||
@@ -556,6 +558,7 @@ const props = withDefaults(defineProps<DataGridProps>(), {
|
||||
totalRowCountIsExact: true,
|
||||
inexactTotalRowCountMode: "at-least",
|
||||
paginationEnabled: true,
|
||||
loadAllRowsEnabled: true,
|
||||
// Omitted row-action limits must keep normal table-data editing.
|
||||
allowInsertRows: undefined,
|
||||
allowDeleteRows: undefined,
|
||||
@@ -588,7 +591,7 @@ function logDataGridTiming(message: string, payload?: Record<string, unknown>) {
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent];
|
||||
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string];
|
||||
paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean];
|
||||
sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode];
|
||||
"update:whereInput": [value: string];
|
||||
"update:orderByInput": [value: string];
|
||||
@@ -3009,6 +3012,8 @@ let infiniteScrollCheckScheduled = false;
|
||||
let infiniteScrollAllLoaded = false;
|
||||
let infiniteScrollRequestedOffset: number | undefined;
|
||||
let infiniteScrollRequestedLimit: number | undefined;
|
||||
let infiniteScrollLoadAllPending = false;
|
||||
const loadAllRowsActive = ref(false);
|
||||
// Tracks whether the current loading cycle was triggered by a refresh/rollback
|
||||
// (as opposed to a normal paginate). Used to decide whether to auto-redirect
|
||||
// when the current page no longer exists after data was deleted.
|
||||
@@ -3046,11 +3051,14 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
// Clear infinite-scroll loading when the parent finishes loading new data
|
||||
// Complete an append only after both the loading state and appended result have
|
||||
// propagated. Large results can update those props in separate render cycles.
|
||||
watch(
|
||||
() => props.loading,
|
||||
(loading, prevLoading) => {
|
||||
if (prevLoading && !loading && infiniteScrollLoading.value) {
|
||||
() => [props.loading, props.result.rows.length, props.result.appended_from_row_count] as const,
|
||||
([loading]) => {
|
||||
if (!loading && infiniteScrollLoading.value) {
|
||||
const shouldSelectLastRow = infiniteScrollLoadAllPending;
|
||||
infiniteScrollLoadAllPending = false;
|
||||
infiniteScrollLoading.value = false;
|
||||
isInfiniteScrollPaginating.value = false;
|
||||
const requestedOffset = infiniteScrollRequestedOffset;
|
||||
@@ -3062,19 +3070,31 @@ watch(
|
||||
// optimistic page marker so a later scroll can retry the same segment.
|
||||
currentPage.value = Math.max(1, currentPage.value - 1);
|
||||
lastInfiniteScrollPage = Math.max(0, currentPage.value - 1);
|
||||
loadAllRowsActive.value = false;
|
||||
return;
|
||||
}
|
||||
const appendedRows = props.result.rows.length - requestedOffset;
|
||||
if (props.result.rows.length >= infiniteScrollMaxRows.value || appendedRows < (requestedLimit ?? pageSize.value)) {
|
||||
infiniteScrollAllLoaded = true;
|
||||
}
|
||||
if (shouldSelectLastRow) selectAndRevealLastLoadedRow();
|
||||
}
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
const manualTotalRowCount = ref<number | undefined>(undefined);
|
||||
const manualTotalRowCountLoading = ref(false);
|
||||
const esDeepPageJumpConfirmOpen = ref(false);
|
||||
const pendingEsDeepPageJump = ref<{ targetPage: number; requestCount: number; updateCurrentPage: boolean }>();
|
||||
// One "load all" click fetches the whole remaining segment in a single request;
|
||||
// with the result-row cap disabled that segment is effectively unbounded, so a
|
||||
// large shot needs an explicit confirmation the way ES deep page jumps do.
|
||||
const LOAD_ALL_ROWS_CONFIRM_ROW_THRESHOLD = 100_000;
|
||||
const loadAllRowsConfirmOpen = ref(false);
|
||||
const pendingLoadAllRows = ref<{ remaining: number }>();
|
||||
watch(loadAllRowsConfirmOpen, (open) => {
|
||||
if (!open) pendingLoadAllRows.value = undefined;
|
||||
});
|
||||
watch(esDeepPageJumpConfirmOpen, (open) => {
|
||||
if (!open) {
|
||||
pendingEsDeepPageJump.value = undefined;
|
||||
@@ -3318,8 +3338,6 @@ watch(
|
||||
(values, previousValues) => {
|
||||
if (!didDataGridInfiniteScrollContextChange(values, previousValues)) return;
|
||||
manualTotalRowCount.value = undefined;
|
||||
// Reset infinite-scroll allLoaded when query context changes
|
||||
infiniteScrollAllLoaded = false;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3402,6 +3420,79 @@ function infiniteScrollNextPage() {
|
||||
// row identities, which would invalidate pending edits while the user scrolls.
|
||||
emit("paginate", nextOffset, nextLimit, currentWhereInput(), currentOrderBy());
|
||||
}
|
||||
|
||||
function selectAndRevealLastLoadedRow() {
|
||||
nextTick(() => {
|
||||
let rowIndex = displayRowRefs.value.length - 1;
|
||||
while (rowIndex >= 0 && !("sourceIndex" in displayRowRefs.value[rowIndex])) rowIndex--;
|
||||
if (rowIndex < 0) return;
|
||||
selectRow(rowIndex);
|
||||
if (showTranspose.value) {
|
||||
transposeRowIndex.value = rowIndex;
|
||||
nextTick(() => scrollTransposeRecordIntoView(rowIndex, "nearest"));
|
||||
return;
|
||||
}
|
||||
|
||||
const rowHeight = useCanvasGridRows.value ? CANVAS_DATA_GRID_ROW_HEIGHT : DOM_DATA_GRID_ROW_HEIGHT;
|
||||
const expectedRowBottom = (rowIndex + 1) * rowHeight;
|
||||
let remainingFrames = 12;
|
||||
const revealWhenReady = () => {
|
||||
const scroller = gridScrollerElement();
|
||||
if (scroller) {
|
||||
if (useCanvasGridRows.value) scrollCanvasRowIntoView(rowIndex, "end");
|
||||
else scrollDomRowIntoView(rowIndex, "end");
|
||||
const expectedScrollTop = Math.max(0, expectedRowBottom - scroller.clientHeight);
|
||||
if (scroller.scrollTop >= expectedScrollTop - 1) return;
|
||||
}
|
||||
remainingFrames--;
|
||||
if (remainingFrames > 0) requestAnimationFrame(revealWhenReady);
|
||||
};
|
||||
requestAnimationFrame(revealWhenReady);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAllRowsAndGoToLast() {
|
||||
if (!props.loadAllRowsEnabled || gridSurfaceBusy.value || infiniteScrollLoading.value || props.result.rows.length === 0) return;
|
||||
// search_after cursor paging fetches page by page; a single giant append
|
||||
// against those cursors is untested, so ES/Easysearch grids keep the
|
||||
// reveal-only shortcut instead of loading everything.
|
||||
if (isResultsContext.value && (resolvedDatabaseType.value === "elasticsearch" || resolvedDatabaseType.value === "easysearch")) return;
|
||||
const segment = dataGridLoadAllSegment(props.result.rows.length, infiniteScrollMaxRows.value, !infiniteScrollAllLoaded && canFetchNextInfiniteScrollSegment.value);
|
||||
if (!segment) {
|
||||
loadAllRowsActive.value = true;
|
||||
infiniteScrollAllLoaded = true;
|
||||
selectAndRevealLastLoadedRow();
|
||||
return;
|
||||
}
|
||||
const knownTotal = displayedTotalRowCount.value;
|
||||
const remaining = typeof knownTotal === "number" && Number.isFinite(knownTotal) && knownTotal >= props.result.rows.length ? knownTotal - props.result.rows.length : segment.limit;
|
||||
if (remaining > LOAD_ALL_ROWS_CONFIRM_ROW_THRESHOLD) {
|
||||
pendingLoadAllRows.value = { remaining };
|
||||
loadAllRowsConfirmOpen.value = true;
|
||||
return;
|
||||
}
|
||||
startLoadAllRows(segment);
|
||||
}
|
||||
|
||||
function startLoadAllRows(segment: { offset: number; limit: number }) {
|
||||
loadAllRowsActive.value = true;
|
||||
infiniteScrollLoadAllPending = true;
|
||||
infiniteScrollLoading.value = true;
|
||||
isInfiniteScrollPaginating.value = true;
|
||||
infiniteScrollRequestedOffset = segment.offset;
|
||||
infiniteScrollRequestedLimit = segment.limit;
|
||||
currentPage.value++;
|
||||
emit("paginate", segment.offset, segment.limit, currentWhereInput(), currentOrderBy(), true);
|
||||
}
|
||||
|
||||
function confirmLoadAllRows() {
|
||||
const pending = pendingLoadAllRows.value;
|
||||
if (!pending) return;
|
||||
pendingLoadAllRows.value = undefined;
|
||||
loadAllRowsConfirmOpen.value = false;
|
||||
const segment = dataGridLoadAllSegment(props.result.rows.length, infiniteScrollMaxRows.value, !infiniteScrollAllLoaded && canFetchNextInfiniteScrollSegment.value);
|
||||
if (segment) startLoadAllRows(segment);
|
||||
}
|
||||
function checkInfiniteScroll(scroller: HTMLElement) {
|
||||
if (!infiniteScrollEnabled.value || infiniteScrollLoading.value || props.loading) return;
|
||||
if (infiniteScrollAllLoaded) return;
|
||||
@@ -3423,6 +3514,7 @@ function changePageSize(size: number) {
|
||||
currentPage.value = 1;
|
||||
lastInfiniteScrollPage = 0;
|
||||
infiniteScrollAllLoaded = false;
|
||||
loadAllRowsActive.value = false;
|
||||
infiniteScrollPositions = new WeakMap();
|
||||
resetGridVerticalScroll(true);
|
||||
emit("paginate", 0, normalizedSize, currentWhereInput(), currentOrderBy());
|
||||
@@ -4199,13 +4291,15 @@ function resetInfiniteScrollState() {
|
||||
infiniteScrollRequestedLimit = undefined;
|
||||
isInfiniteScrollPaginating.value = false;
|
||||
infiniteScrollLoading.value = false;
|
||||
infiniteScrollLoadAllPending = false;
|
||||
loadAllRowsActive.value = false;
|
||||
infiniteScrollPositions = new WeakMap();
|
||||
resetGridVerticalScroll(true);
|
||||
}
|
||||
|
||||
function prepareFullReload() {
|
||||
const viewportAnchor = captureViewportAnchorForRefresh();
|
||||
if (infiniteScrollEnabled.value) {
|
||||
if (infiniteScrollEnabled.value || loadAllRowsActive.value) {
|
||||
resetInfiniteScrollState();
|
||||
}
|
||||
const selection = captureCurrentSelectionForRefresh();
|
||||
@@ -6114,7 +6208,7 @@ function applyColumnSort(column: string, columnIndex: number, direction: "asc" |
|
||||
toast(t("grid.largeValueLocalSortUnavailable"), 5000);
|
||||
return;
|
||||
}
|
||||
if (mode === "database" && infiniteScrollEnabled.value) {
|
||||
if (mode === "database" && (infiniteScrollEnabled.value || loadAllRowsActive.value)) {
|
||||
resetInfiniteScrollState();
|
||||
} else {
|
||||
currentPage.value = 1;
|
||||
@@ -9983,8 +10077,21 @@ watch(
|
||||
infiniteScrollLoading.value = false;
|
||||
isInfiniteScrollPaginating.value = false;
|
||||
}
|
||||
// The append completion above already reset `infiniteScrollLoading`, so the
|
||||
// post-flush loading watcher cannot observe this append; a "load all" run
|
||||
// must still reveal its last row from here.
|
||||
if (infiniteScrollLoadAllPending) {
|
||||
infiniteScrollLoadAllPending = false;
|
||||
selectAndRevealLastLoadedRow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A non-append result replaces the whole data set, so a running "load all" is over.
|
||||
loadAllRowsActive.value = false;
|
||||
// The replacement also invalidates the all-loaded marker: a filter change or
|
||||
// page jump swaps in a fresh first page whose remaining segments must be
|
||||
// re-derived instead of being silently skipped.
|
||||
infiniteScrollAllLoaded = false;
|
||||
if (getResetScrollAfterResult()) {
|
||||
clearResetScrollAfterResult();
|
||||
resetGridVerticalScroll();
|
||||
@@ -13872,9 +13979,12 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
:selection-summary="selectionSummary"
|
||||
:selection-summary-sum-text="selectionSummarySumText"
|
||||
:selection-summary-average-text="selectionSummaryAverageText"
|
||||
:loading="gridPaginationBusy"
|
||||
:loading="gridPaginationBusy || infiniteScrollLoading"
|
||||
:infinite-scroll-enabled="infiniteScrollEnabled"
|
||||
:infinite-scroll-all-loaded="infiniteScrollAllLoaded"
|
||||
:load-all-rows-active="loadAllRowsActive"
|
||||
:load-all-rows-enabled="loadAllRowsEnabled"
|
||||
:can-load-all-rows="result.rows.length > 0"
|
||||
:page-size="pageSize"
|
||||
:default-page-size="defaultPageSize"
|
||||
:page-size-menu-items="pageSizeMenuItems"
|
||||
@@ -13891,6 +14001,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
@next-page="nextPage"
|
||||
@jump-page="jumpPage"
|
||||
@last-page="lastPage"
|
||||
@load-all-rows="loadAllRowsAndGoToLast"
|
||||
@select-export="selectExportMenuItem"
|
||||
/>
|
||||
</div>
|
||||
@@ -13999,6 +14110,24 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="loadAllRowsConfirmOpen">
|
||||
<DialogContent class="sm:max-w-[480px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
{{ t("grid.loadAllRowsConfirmTitle") }}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="py-3 text-sm leading-6 text-muted-foreground">
|
||||
{{ t("grid.loadAllRowsConfirmMessage", { count: pendingLoadAllRows?.remaining ?? 0 }) }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="loadAllRowsConfirmOpen = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button @click="confirmLoadAllRows">{{ t("grid.loadAllRowsContinue") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="generateIncrementDialogOpen">
|
||||
<DialogContent class="sm:max-w-[380px]">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2 } from "@lucide/vue";
|
||||
import { Check, ChevronLeft, ChevronRight, ChevronsDown, ChevronsLeft, ChevronsRight, Loader2 } from "@lucide/vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -20,6 +20,9 @@ const props = withDefaults(
|
||||
loading: boolean;
|
||||
infiniteScrollEnabled: boolean;
|
||||
infiniteScrollAllLoaded: boolean;
|
||||
loadAllRowsActive: boolean;
|
||||
loadAllRowsEnabled: boolean;
|
||||
canLoadAllRows: boolean;
|
||||
pageSize: number;
|
||||
defaultPageSize: number;
|
||||
pageSizeMenuItems: LightDropdownItem[];
|
||||
@@ -29,7 +32,7 @@ const props = withDefaults(
|
||||
canGoNextPage: boolean;
|
||||
canJumpLastPage: boolean;
|
||||
}>(),
|
||||
{ paginationEnabled: true },
|
||||
{ paginationEnabled: true, loadAllRowsActive: false, loadAllRowsEnabled: true, canLoadAllRows: false },
|
||||
);
|
||||
|
||||
const customPageSizeInput = defineModel<string>("customPageSizeInput", { default: "" });
|
||||
@@ -49,6 +52,7 @@ const emit = defineEmits<{
|
||||
nextPage: [];
|
||||
jumpPage: [page: number];
|
||||
lastPage: [];
|
||||
loadAllRows: [];
|
||||
selectExport: [value: string];
|
||||
applyCustomPageSizeAndSetDefault: [];
|
||||
}>();
|
||||
@@ -96,10 +100,10 @@ function handlePageInputKeydown(event: KeyboardEvent) {
|
||||
</div>
|
||||
</div>
|
||||
<Loader2 class="w-3 h-3 text-muted-foreground" :class="loading ? 'animate-spin' : 'invisible'" aria-hidden="true" />
|
||||
<template v-if="paginationEnabled && infiniteScrollEnabled">
|
||||
<template v-if="paginationEnabled && (infiniteScrollEnabled || loadAllRowsActive)">
|
||||
<span v-if="infiniteScrollAllLoaded" class="text-xs text-muted-foreground shrink-0">{{ t("grid.allLoaded") }}</span>
|
||||
</template>
|
||||
<template v-if="paginationEnabled && !infiniteScrollEnabled">
|
||||
<template v-if="paginationEnabled && !infiniteScrollEnabled && !loadAllRowsActive">
|
||||
<LightDropdown
|
||||
:model-value="String(pageSize)"
|
||||
:items="pageSizeMenuItems"
|
||||
@@ -152,6 +156,14 @@ function handlePageInputKeydown(event: KeyboardEvent) {
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canGoNextPage" @click="emit('nextPage')"><ChevronRight class="h-3 w-3" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canJumpLastPage" @click="emit('lastPage')"><ChevronsRight class="h-3 w-3" /></Button>
|
||||
</template>
|
||||
<Tooltip v-if="paginationEnabled && loadAllRowsEnabled">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 shrink-0" :disabled="loading || !canLoadAllRows" :aria-label="t('grid.loadAllAndGoToLastRow')" @click="emit('loadAllRows')">
|
||||
<ChevronsDown class="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{{ t("grid.loadAllAndGoToLastRow") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<DataGridExportMenu :items="exportMenuItems" :label="t('grid.export')" :on-select="(value) => emit('selectExport', value)" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,6 +30,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
ChevronsLeft: icon,
|
||||
ChevronsRight: icon,
|
||||
Download: icon,
|
||||
ChevronsDown: icon,
|
||||
Filter: icon,
|
||||
Focus: icon,
|
||||
FileDiff: icon,
|
||||
@@ -288,7 +289,7 @@ describe("DataGridPagination", () => {
|
||||
onNextPage: nextPage,
|
||||
onLastPage: lastPage,
|
||||
});
|
||||
const navigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
|
||||
const navigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
|
||||
|
||||
expect(navigation.map((node) => node.props.disabled)).toEqual([true, true, true, true]);
|
||||
navigation.forEach((node) => dispatch(node, "click"));
|
||||
@@ -298,7 +299,7 @@ describe("DataGridPagination", () => {
|
||||
expect(lastPage).not.toHaveBeenCalled();
|
||||
|
||||
await mounted.setProps({ currentPage: 2, canGoNextPage: true, canJumpLastPage: true });
|
||||
const enabledNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
|
||||
const enabledNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
|
||||
expect(enabledNavigation.map((node) => node.props.disabled)).toEqual([false, false, false, false]);
|
||||
enabledNavigation.forEach((node) => dispatch(node, "click"));
|
||||
expect(firstPage).toHaveBeenCalledOnce();
|
||||
@@ -307,7 +308,7 @@ describe("DataGridPagination", () => {
|
||||
expect(lastPage).toHaveBeenCalledOnce();
|
||||
|
||||
await mounted.setProps({ loading: true });
|
||||
const busyNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0");
|
||||
const busyNavigation = findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0" && node.props["aria-label"] !== "grid.loadAllAndGoToLastRow");
|
||||
expect(busyNavigation.map((node) => node.props.disabled)).toEqual([true, true, true, true]);
|
||||
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.jumpToPage").props.disabled).toBe(true);
|
||||
});
|
||||
@@ -375,6 +376,36 @@ describe("DataGridPagination", () => {
|
||||
|
||||
expect(findAll(mounted.root, (node) => node.props["data-stub"] === "Button" && node.props.class === "h-5 w-5 shrink-0")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("loads all paginated rows and moves to the last row", async () => {
|
||||
const loadAllRows = vi.fn();
|
||||
const mounted = mountComponent(DataGridPagination, {
|
||||
selectionSummary: null,
|
||||
selectionSummarySumText: "",
|
||||
loading: false,
|
||||
infiniteScrollEnabled: false,
|
||||
infiniteScrollAllLoaded: false,
|
||||
canLoadAllRows: true,
|
||||
pageSize: 100,
|
||||
customPageSizeInput: "",
|
||||
pageSizeMenuItems: [],
|
||||
exportMenuItems: [],
|
||||
currentPage: 1,
|
||||
canGoNextPage: true,
|
||||
canJumpLastPage: true,
|
||||
onLoadAllRows: loadAllRows,
|
||||
});
|
||||
const loadAllButton = findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow");
|
||||
|
||||
expect(loadAllButton.props.disabled).toBe(false);
|
||||
dispatch(loadAllButton, "click");
|
||||
expect(loadAllRows).toHaveBeenCalledOnce();
|
||||
|
||||
await mounted.setProps({ loading: true });
|
||||
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow").props.disabled).toBe(true);
|
||||
await mounted.setProps({ loading: false, canLoadAllRows: false });
|
||||
expect(findOne(mounted.root, (node) => node.props["aria-label"] === "grid.loadAllAndGoToLastRow").props.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataGridColumnHeader", () => {
|
||||
|
||||
@@ -2301,7 +2301,7 @@ defineExpose({
|
||||
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
|
||||
@local-column-filters-change="(filters: Record<string, string[]>) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)"
|
||||
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', activeTab.id, sql, searchText, whereInput, orderBy, limit, offset, intent)"
|
||||
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy)"
|
||||
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy, appendResult)"
|
||||
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', activeTab.id, column, columnIndex, direction, whereInput, mode)"
|
||||
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
|
||||
>
|
||||
@@ -2727,7 +2727,7 @@ defineExpose({
|
||||
@update:order-by-input="(v: string) => (activeTab.orderByInput = v)"
|
||||
@local-column-filters-change="(filters: Record<string, string[]>) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)"
|
||||
@reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', activeTab.id, sql, searchText, whereInput, orderBy, limit, offset, intent)"
|
||||
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy)"
|
||||
@paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean) => emit('paginate', activeTab.id, offset, limit, whereInput, orderBy, appendResult)"
|
||||
@sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', activeTab.id, column, columnIndex, direction, whereInput, mode)"
|
||||
@change-query-timeout="(connectionId: string) => emit('openConnectionSettings', connectionId, 'advanced')"
|
||||
>
|
||||
|
||||
@@ -91,7 +91,7 @@ export interface ContentAreaSurfaceEmits {
|
||||
editorStateFlushed: [tabId: string];
|
||||
formatError: [tabId: string];
|
||||
reload: [tabId: string, sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent];
|
||||
paginate: [tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string];
|
||||
paginate: [tabId: string, offset: number, limit: number, whereInput?: string, orderBy?: string, appendResult?: boolean];
|
||||
sort: [tabId: string, column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode];
|
||||
executeSql: [tabId: string, sql: string];
|
||||
clickTable: [tabId: string, target: SqlObjectNavigationTarget];
|
||||
|
||||
Reference in new issue
Block a user