From e76c541627a42a3d99a7ee5719bcb3f063ae6593 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sun, 12 Mar 2023 16:04:17 +0000 Subject: [PATCH] Reset scrolling when datasource changes and fix wasted pagination calls --- .../src/components/sheet/Sheet.svelte | 10 +- .../src/components/sheet/stores/max-scroll.js | 86 +++++++++++++++ .../src/components/sheet/stores/pagination.js | 26 +++++ .../src/components/sheet/stores/rows.js | 29 +++-- .../src/components/sheet/stores/scroll.js | 100 +----------------- .../src/components/sheet/stores/viewport.js | 15 +-- 6 files changed, 151 insertions(+), 115 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/max-scroll.js create mode 100644 packages/frontend-core/src/components/sheet/stores/pagination.js diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index ceb158b529..ec717ba824 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -15,6 +15,8 @@ import { createUserStores } from "./stores/users" import { createResizeStores } from "./stores/resize" import { createMenuStores } from "./stores/menu" + import { createMaxScrollStores } from "./stores/max-scroll" + import { createPaginationStores } from "./stores/pagination" import DeleteButton from "./DeleteButton.svelte" import SheetBody from "./SheetBody.svelte" import ResizeOverlay from "./ResizeOverlay.svelte" @@ -57,16 +59,18 @@ config, } context = { ...context, ...createEventManagers() } - context = { ...context, ...createRowsStore(context) } - context = { ...context, ...createColumnsStores(context) } - context = { ...context, ...createResizeStores(context) } context = { ...context, ...createBoundsStores(context) } context = { ...context, ...createScrollStores(context) } + context = { ...context, ...createRowsStore(context) } + context = { ...context, ...createColumnsStores(context) } + context = { ...context, ...createMaxScrollStores(context) } + context = { ...context, ...createResizeStores(context) } context = { ...context, ...createViewportStores(context) } context = { ...context, ...createReorderStores(context) } context = { ...context, ...createUIStores(context) } context = { ...context, ...createUserStores(context) } context = { ...context, ...createMenuStores(context) } + context = { ...context, ...createPaginationStores(context) } // Reference some stores for local use const { isResizing, isReordering, ui, loaded } = context diff --git a/packages/frontend-core/src/components/sheet/stores/max-scroll.js b/packages/frontend-core/src/components/sheet/stores/max-scroll.js new file mode 100644 index 0000000000..8c368e0d8e --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/max-scroll.js @@ -0,0 +1,86 @@ +import { derived, get } from "svelte/store" + +export const createMaxScrollStores = context => { + const { rows, visibleColumns, stickyColumn, bounds, cellHeight, scroll } = + context + const padding = 180 + + // Memoize store primitives + const scrollTop = derived(scroll, $scroll => $scroll.top, 0) + const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) + + // Derive vertical limits + const height = derived(bounds, $bounds => $bounds.height, 0) + const width = derived(bounds, $bounds => $bounds.width, 0) + const contentHeight = derived( + rows, + $rows => $rows.length * cellHeight + padding, + 0 + ) + const maxScrollTop = derived( + [height, contentHeight], + ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0), + 0 + ) + + // Derive horizontal limits + const contentWidth = derived( + [visibleColumns, stickyColumn], + ([$visibleColumns, $stickyColumn]) => { + let width = 40 + padding + ($stickyColumn?.width || 0) + $visibleColumns.forEach(col => { + width += col.width + }) + return width + }, + 0 + ) + const screenWidth = derived( + [width, stickyColumn], + ([$width, $stickyColumn]) => $width + 40 + ($stickyColumn?.width || 0), + 0 + ) + const maxScrollLeft = derived( + [contentWidth, screenWidth], + ([$contentWidth, $screenWidth]) => { + return Math.max($contentWidth - $screenWidth, 0) + }, + 0 + ) + + // Ensure scroll state never goes invalid, which can happen when changing + // rows or tables + const overscrollTop = derived( + [scrollTop, maxScrollTop], + ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop, + false + ) + const overscrollLeft = derived( + [scrollLeft, maxScrollLeft], + ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft, + false + ) + overscrollTop.subscribe(overscroll => { + if (overscroll) { + scroll.update(state => ({ + ...state, + top: get(maxScrollTop), + })) + } + }) + overscrollLeft.subscribe(overscroll => { + if (overscroll) { + scroll.update(state => ({ + ...state, + left: get(maxScrollLeft), + })) + } + }) + + return { + contentHeight, + contentWidth, + maxScrollTop, + maxScrollLeft, + } +} diff --git a/packages/frontend-core/src/components/sheet/stores/pagination.js b/packages/frontend-core/src/components/sheet/stores/pagination.js new file mode 100644 index 0000000000..24bd10f19e --- /dev/null +++ b/packages/frontend-core/src/components/sheet/stores/pagination.js @@ -0,0 +1,26 @@ +import { derived } from "svelte/store" + +export const createPaginationStores = context => { + const { scrolledRowCount, rows, visualRowCapacity } = context + + // Derive how many rows we have in total + const rowCount = derived(rows, $rows => $rows.length, 0) + + // Derive how many rows we have available to scroll + const remainingRows = derived( + [scrolledRowCount, rowCount, visualRowCapacity], + ([$scrolledRowCount, $rowCount, $visualRowCapacity]) => { + return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity) + }, + 100 + ) + + // Fetch next page when fewer than 25 remaining rows to scroll + remainingRows.subscribe(remaining => { + if (remaining < 25) { + rows.actions.loadNextPage() + } + }) + + return null +} diff --git a/packages/frontend-core/src/components/sheet/stores/rows.js b/packages/frontend-core/src/components/sheet/stores/rows.js index 958fd1e569..6c6fc7e439 100644 --- a/packages/frontend-core/src/components/sheet/stores/rows.js +++ b/packages/frontend-core/src/components/sheet/stores/rows.js @@ -3,13 +3,14 @@ import { fetchData } from "../../../fetch/fetchData" import { notifications } from "@budibase/bbui" export const createRowsStore = context => { - const { config, API } = context + const { config, API, scroll } = context const tableId = derived(config, $config => $config.tableId) const rows = writable([]) const schema = writable({}) const table = writable(null) const filter = writable([]) const loaded = writable(false) + const instanceLoaded = writable(false) const fetch = writable(null) const initialSortState = { column: null, @@ -51,6 +52,7 @@ export const createRowsStore = context => { // Unsub from previous fetch if one exists unsubscribe?.() fetch.set(null) + instanceLoaded.set(false) // Reset state sort.set(initialSortState) @@ -75,10 +77,16 @@ export const createRowsStore = context => { // Subscribe to changes of this fetch model unsubscribe = newFetch.subscribe($fetch => { if ($fetch.loaded && !$fetch.loading) { - if ($fetch.pageNumber === 0) { - // Hydrate initial data - rowCacheMap = {} - rows.set([]) + const resetRows = $fetch.pageNumber === 0 + + // Reset scroll state when data changes + if (!get(instanceLoaded)) { + // Reset both top and left for a new table ID + instanceLoaded.set(true) + scroll.set({ top: 0, left: 0 }) + } else if (resetRows) { + // Only reset top scroll position when resetting rows + scroll.update(state => ({ ...state, top: 0 })) } // Update schema and enrich primary display into schema @@ -91,7 +99,7 @@ export const createRowsStore = context => { table.set($fetch.definition) // Process new rows - handleNewRows($fetch.rows) + handleNewRows($fetch.rows, resetRows) // Notify that we're loaded loaded.set(true) @@ -222,7 +230,10 @@ export const createRowsStore = context => { // Local handler to process new rows inside the fetch, and append any new // rows to state that we haven't encountered before - const handleNewRows = newRows => { + const handleNewRows = (newRows, resetRows) => { + if (resetRows) { + rowCacheMap = {} + } let rowsToAppend = [] let newRow for (let i = 0; i < newRows.length; i++) { @@ -232,7 +243,9 @@ export const createRowsStore = context => { rowsToAppend.push(newRow) } } - if (rowsToAppend.length) { + if (resetRows) { + rows.set(rowsToAppend) + } else if (rowsToAppend.length) { rows.update(state => [...state, ...rowsToAppend]) } } diff --git a/packages/frontend-core/src/components/sheet/stores/scroll.js b/packages/frontend-core/src/components/sheet/stores/scroll.js index fa88ac46b4..e0f33c7d87 100644 --- a/packages/frontend-core/src/components/sheet/stores/scroll.js +++ b/packages/frontend-core/src/components/sheet/stores/scroll.js @@ -1,107 +1,11 @@ -import { derived, get, writable } from "svelte/store" +import { writable } from "svelte/store" -export const createScrollStores = context => { - const { rows, visibleColumns, stickyColumn, bounds, cellHeight } = context - const padding = 180 +export const createScrollStores = () => { const scroll = writable({ left: 0, top: 0, }) - - // Memoize store primitives - const scrollTop = derived(scroll, $scroll => $scroll.top, 0) - const scrollLeft = derived(scroll, $scroll => $scroll.left, 0) - - // Derive vertical limits - const height = derived(bounds, $bounds => $bounds.height, 0) - const width = derived(bounds, $bounds => $bounds.width, 0) - const contentHeight = derived( - rows, - $rows => $rows.length * cellHeight + padding, - 0 - ) - const maxScrollTop = derived( - [height, contentHeight], - ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0), - 0 - ) - - // Derive horizontal limits - const contentWidth = derived( - [visibleColumns, stickyColumn], - ([$visibleColumns, $stickyColumn]) => { - let width = 40 + padding + ($stickyColumn?.width || 0) - $visibleColumns.forEach(col => { - width += col.width - }) - return width - }, - 0 - ) - const screenWidth = derived( - [width, stickyColumn], - ([$width, $stickyColumn]) => $width + 40 + ($stickyColumn?.width || 0), - 0 - ) - const maxScrollLeft = derived( - [contentWidth, screenWidth], - ([$contentWidth, $screenWidth]) => { - return Math.max($contentWidth - $screenWidth, 0) - }, - 0 - ) - - // Ensure scroll state never goes invalid, which can happen when changing - // rows or tables - const overscrollTop = derived( - [scrollTop, maxScrollTop], - ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop, - false - ) - const overscrollLeft = derived( - [scrollLeft, maxScrollLeft], - ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft, - false - ) - overscrollTop.subscribe(overscroll => { - if (overscroll) { - scroll.update(state => ({ - ...state, - top: get(maxScrollTop), - })) - } - }) - overscrollLeft.subscribe(overscroll => { - if (overscroll) { - scroll.update(state => ({ - ...state, - left: get(maxScrollLeft), - })) - } - }) - - // Fetch next page when fewer than 50 scrollable rows remaining - const scrollableRows = derived( - [scrollTop, maxScrollTop], - ([$scrollTop, $maxScrollTop]) => { - if (!$maxScrollTop) { - return 100 - } - return ($maxScrollTop - $scrollTop) / cellHeight - }, - 100 - ) - scrollableRows.subscribe(count => { - if (count < 25) { - rows.actions.loadNextPage() - } - }) - return { scroll, - contentHeight, - contentWidth, - maxScrollTop, - maxScrollLeft, } } diff --git a/packages/frontend-core/src/components/sheet/stores/viewport.js b/packages/frontend-core/src/components/sheet/stores/viewport.js index 79a075dee9..27341a7cfe 100644 --- a/packages/frontend-core/src/components/sheet/stores/viewport.js +++ b/packages/frontend-core/src/components/sheet/stores/viewport.js @@ -12,14 +12,14 @@ export const createViewportStores = context => { // Derive visible rows // Split into multiple stores containing primitives to optimise invalidation // as mich as possible - const firstRowIdx = derived( + const scrolledRowCount = derived( scrollTop, $scrollTop => { return Math.floor($scrollTop / cellHeight) }, 0 ) - const renderedRowCount = derived( + const visualRowCapacity = derived( height, $height => { return Math.ceil($height / cellHeight) @@ -27,9 +27,12 @@ export const createViewportStores = context => { 0 ) const renderedRows = derived( - [rows, firstRowIdx, renderedRowCount], - ([$rows, $firstRowIdx, $visibleRowCount]) => { - return $rows.slice($firstRowIdx, $firstRowIdx + $visibleRowCount) + [rows, scrolledRowCount, visualRowCapacity], + ([$rows, $scrolledRowCount, $visualRowCapacity]) => { + return $rows.slice( + $scrolledRowCount, + $scrolledRowCount + $visualRowCapacity + ) }, [] ) @@ -74,5 +77,5 @@ export const createViewportStores = context => { [] ) - return { renderedRows, renderedColumns } + return { scrolledRowCount, visualRowCapacity, renderedRows, renderedColumns } }