diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
index ea08d77c20..2910a1ba0a 100644
--- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
+++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
@@ -59,7 +59,7 @@ const { $e } = useNuxtApp()
const { appInfo } = useGlobal()
-const { betaFeatureToggleState } = useBetaFeatureToggle()
+const { isFeatureEnabled } = useBetaFeatureToggle()
const { openedViewsTab } = storeToRefs(useViewsStore())
@@ -122,7 +122,9 @@ const isColumnTypeOpen = ref(false)
const geoDataToggleCondition = (t: { name: UITypes }) => {
if (!appInfo.value.ee) return true
- return betaFeatureToggleState.show ? betaFeatureToggleState.show : !t.name.includes(UITypes.GeoData)
+ const isColEnabled = isFeatureEnabled(FEATURE_FLAG.GEODATA_COLUMN)
+
+ return isColEnabled || !t.name.includes(UITypes.GeoData)
}
const showDeprecated = ref(false)
diff --git a/packages/nc-gui/components/smartsheet/form/field-settings.vue b/packages/nc-gui/components/smartsheet/form/field-settings.vue
index de8441e703..285a36a583 100644
--- a/packages/nc-gui/components/smartsheet/form/field-settings.vue
+++ b/packages/nc-gui/components/smartsheet/form/field-settings.vue
@@ -3,7 +3,7 @@ import { UITypes, isSelectTypeCol } from 'nocodb-sdk'
const { formState, activeField, updateColMeta, isRequired } = useFormViewStoreOrThrow()
-const { betaFeatureToggleState } = useBetaFeatureToggle()
+const { isFeatureEnabled } = useBetaFeatureToggle()
const updateSelectFieldLayout = (value: boolean) => {
if (!activeField.value) return
@@ -13,7 +13,7 @@ const updateSelectFieldLayout = (value: boolean) => {
}
const columnSupportsScanning = (elementType: UITypes) =>
- betaFeatureToggleState.show &&
+ isFeatureEnabled(FEATURE_FLAG.FORM_SUPPORT_COLUMN_SCANNING) &&
[UITypes.SingleLineText, UITypes.Number, UITypes.Email, UITypes.URL, UITypes.LongText].includes(elementType)
diff --git a/packages/nc-gui/components/smartsheet/grid/InfiniteTable.vue b/packages/nc-gui/components/smartsheet/grid/InfiniteTable.vue
new file mode 100644
index 0000000000..a17fa04c20
--- /dev/null
+++ b/packages/nc-gui/components/smartsheet/grid/InfiniteTable.vue
@@ -0,0 +1,2848 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Row filtered
+
+
+
+ This record will be hidden as it does not match the filters applied to this view.
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.rowMeta.rowIndex + 1 }}
+
+
+
+
+
+
+
+
+ {{ row.rowMeta.commentCount }}
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('title.updateSelectedRows') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New Record
+
+
+
+
+
+
+ {{ $t('activity.newRecord') }}
+
+ {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.grid') }}
+
+
+
+
+
+
+
+ {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/smartsheet/grid/PaginationV2.vue b/packages/nc-gui/components/smartsheet/grid/PaginationV2.vue
index f4cab5c0ac..0e5dc34685 100644
--- a/packages/nc-gui/components/smartsheet/grid/PaginationV2.vue
+++ b/packages/nc-gui/components/smartsheet/grid/PaginationV2.vue
@@ -4,11 +4,13 @@ import { type PaginatedType, UITypes } from 'nocodb-sdk'
const props = defineProps<{
scrollLeft?: number
- paginationData: PaginatedType
- changePage: (page: number) => void
+ paginationData?: PaginatedType
+ changePage?: (page: number) => void
showSizeChanger?: boolean
customLabel?: string
+ totalRows?: number
depth?: number
+ disablePagination?: boolean
}>()
const emits = defineEmits(['update:paginationData'])
@@ -23,6 +25,8 @@ const showSizeChanger = toRef(props, 'showSizeChanger')
const vPaginationData = useVModel(props, 'paginationData', emits)
+const disablePagination = toRef(props, 'disablePagination')
+
const { updateAggregate, getAggregations, visibleFieldsComputed, displayFieldComputed } = useViewAggregateOrThrow()
const scrollLeft = toRef(props, 'scrollLeft')
@@ -46,6 +50,9 @@ const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
const page = computed({
get: () => vPaginationData?.value?.page ?? 1,
set: async (p) => {
+ if (disablePagination.value) {
+ return
+ }
isPaginationLoading.value = true
try {
await changePage?.(p)
@@ -118,21 +125,36 @@ const renderAltOrOptlKey = () => {
}"
>
-
-
-
- {{ count }} {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
-
-
- {{ Intl.NumberFormat('en', { notation: 'compact' }).format(count) }}
- {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
-
-
+
+
+
+
+ {{ count }} {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
+
+
+ {{ Intl.NumberFormat('en', { notation: 'compact' }).format(count) }}
+ {{ customLabel ? customLabel : count !== 1 ? $t('objects.records') : $t('objects.record') }}
+
+
+
+
+
+
+ {{ totalRows }} {{ totalRows !== 1 ? $t('objects.records') : $t('objects.record') }}
+
+ {{ Intl.NumberFormat('en', { notation: 'compact' }).format(totalRows) }}
+ {{ totalRows !== 1 ? $t('objects.records') : $t('objects.record') }}
+
+
+
{
overlay-class-name="max-h-96 relative scroll-container nc-scrollbar-md overflow-auto"
>
‎
-
+
scrollParent.value || gridWrapper.value)
const scrollLeft = ref()
-function scrollToCell(row?: number | null, col?: number | null) {
+function scrollToCell(row?: number | null, col?: number | null, scrollBehaviour: ScrollBehavior = 'instant') {
row = row ?? activeCell.row
col = col ?? activeCell.col
@@ -1020,32 +1021,38 @@ function scrollToCell(row?: number | null, col?: number | null) {
}
if (row === dataRef.value.length - 1) {
- scrollWrapper.value.scrollTo({
- top: isGroupBy.value ? scrollWrapper.value.scrollTop : scrollWrapper.value.scrollHeight,
- left:
- col === fields.value.length - 1 // if corner cell
- ? scrollWrapper.value.scrollWidth
- : tdScroll.left,
- behavior: 'smooth',
+ requestAnimationFrame(() => {
+ scrollWrapper.value.scrollTo({
+ top: isGroupBy.value ? scrollWrapper.value.scrollTop : scrollWrapper.value.scrollHeight,
+ left:
+ col === fields.value.length - 1 // if corner cell
+ ? scrollWrapper.value.scrollWidth
+ : tdScroll.left,
+ behavior: 'instant',
+ })
})
return
}
if (col === fields.value.length - 1) {
// if last column make 'Add New Column' visible
- scrollWrapper.value.scrollTo({
- top: tdScroll.top,
- left: scrollWrapper.value.scrollWidth,
- behavior: 'smooth',
+ requestAnimationFrame(() => {
+ scrollWrapper.value.scrollTo({
+ top: tdScroll.top,
+ left: scrollWrapper.value.scrollWidth,
+ behavior: 'instant',
+ })
})
return
}
// scroll into the active cell
- scrollWrapper.value.scrollTo({
- top: tdScroll.top,
- left: tdScroll.left,
- behavior: 'smooth',
+ requestAnimationFrame(() => {
+ scrollWrapper.value.scrollTo({
+ top: tdScroll.top,
+ left: tdScroll.left,
+ behavior: 'instant',
+ })
})
}
}
diff --git a/packages/nc-gui/components/smartsheet/grid/index.vue b/packages/nc-gui/components/smartsheet/grid/index.vue
index 3e4e9af7c6..433db3a6d5 100644
--- a/packages/nc-gui/components/smartsheet/grid/index.vue
+++ b/packages/nc-gui/components/smartsheet/grid/index.vue
@@ -1,5 +1,6 @@
@@ -249,22 +252,47 @@ onMounted(() => {
:style="`background-color: ${isGroupBy ? `${baseColor}` : 'var(--nc-grid-bg)'};`"
>
+
@@ -305,24 +333,20 @@ onMounted(() => {
:view="view"
show-next-prev-icons
:first-row="isFirstRow"
- :last-row="islastRow"
+ :last-row="isLastRow"
:expand-form="expandForm"
@next="goToNextRow()"
@prev="goToPreviousRow()"
@update-row-comment-count="updateRowCommentCount"
/>
-
diff --git a/packages/nc-gui/composables/useBetaFeatureToggle.ts b/packages/nc-gui/composables/useBetaFeatureToggle.ts
index f40cbd5c41..751f28f7ba 100644
--- a/packages/nc-gui/composables/useBetaFeatureToggle.ts
+++ b/packages/nc-gui/composables/useBetaFeatureToggle.ts
@@ -1,22 +1,121 @@
-import { reactive } from 'vue'
+import { onMounted, ref } from 'vue'
+import { createSharedComposable } from '@vueuse/core'
-const storedValue = localStorage.getItem('betaFeatureToggleState')
+const FEATURES = [
+ {
+ id: 'infinite_scrolling',
+ title: 'Infinite scrolling',
+ description: 'Effortlessly browse large datasets with infinite scrolling.',
+ enabled: true,
+ },
+ {
+ id: 'geodata_column',
+ title: 'Geodata column',
+ description: 'Enable the geodata column.',
+ enabled: false,
+ isEngineering: true,
+ },
+ {
+ id: 'form_support_column_scanning',
+ title: 'Scanner for filling data in forms',
+ description: 'Enable scanner to fill data in forms.',
+ enabled: false,
+ isEngineering: true,
+ },
+ {
+ id: 'extensions',
+ title: 'Extensions',
+ description: 'Extensions allows you to add new features or functionalities to the NocoDB platform.',
+ enabled: false,
+ isEngineering: true,
+ },
+]
-const initialToggleState = storedValue ? JSON.parse(storedValue) : false
+export const FEATURE_FLAG = Object.fromEntries(FEATURES.map((feature) => [feature.id.toUpperCase(), feature.id])) as Record<
+ Uppercase<(typeof FEATURES)[number]['id']>,
+ (typeof FEATURES)[number]['id']
+>
-const betaFeatureToggleState = reactive({ show: initialToggleState })
+type FeatureId = (typeof FEATURES)[number]['id']
+type Feature = (typeof FEATURES)[number]
-const toggleBetaFeature = () => {
- betaFeatureToggleState.show = !betaFeatureToggleState.show
- localStorage.setItem('betaFeatureToggleState', JSON.stringify(betaFeatureToggleState.show))
-}
+const STORAGE_KEY = 'featureToggleStates'
-const _useBetaFeatureToggle = () => {
- return {
- betaFeatureToggleState,
- toggleBetaFeature,
+export const useBetaFeatureToggle = createSharedComposable(() => {
+ const features = ref
(structuredClone(FEATURES))
+
+ const featureStates = computed(() => {
+ return features.value.reduce((acc, feature) => {
+ acc[feature.id] = feature.enabled
+ return acc
+ }, {} as Record)
+ })
+
+ const { $e } = useNuxtApp()
+
+ const isEngineeringModeOn = ref(false)
+
+ const saveFeatures = () => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(features.value))
+ window.dispatchEvent(new StorageEvent('storage', { key: STORAGE_KEY }))
+ } catch (error) {
+ console.error('Failed to save features:', error)
+ }
}
-}
-const useBetaFeatureToggle = createSharedComposable(_useBetaFeatureToggle)
-export { useBetaFeatureToggle }
+ const toggleFeature = (id: FeatureId) => {
+ const feature = features.value.find((f) => f.id === id)
+ if (feature) {
+ feature.enabled = !feature.enabled
+ $e(`a:feature-preview:${id}:${feature.enabled ? 'on' : 'off'}`)
+ saveFeatures()
+ } else {
+ console.error(`Feature ${id} not found`)
+ }
+ }
+
+ const isFeatureEnabled = (id: FeatureId) => featureStates.value[id] ?? false
+
+ const initializeFeatures = () => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY)
+ if (stored) {
+ const parsedFeatures = JSON.parse(stored) as Partial[]
+ features.value = FEATURES.map((defaultFeature) => ({
+ ...defaultFeature,
+ enabled: parsedFeatures.find((f) => f.id === defaultFeature.id)?.enabled ?? defaultFeature.enabled,
+ }))
+ }
+ } catch (error) {
+ console.error('Failed to initialize features:', error)
+ }
+ saveFeatures()
+ }
+
+ const handleStorageEvent = (event: StorageEvent) => {
+ if (event.key === STORAGE_KEY && event.newValue !== null) {
+ if (JSON.parse(event.newValue) !== features.value) {
+ initializeFeatures()
+ }
+ }
+ }
+
+ onMounted(() => {
+ initializeFeatures()
+ window.addEventListener('storage', handleStorageEvent)
+ })
+
+ onUnmounted(() => {
+ window.removeEventListener('storage', handleStorageEvent)
+ })
+
+ onMounted(initializeFeatures)
+
+ return {
+ features,
+ toggleFeature,
+ isFeatureEnabled,
+ isEngineeringModeOn,
+ }
+})
diff --git a/packages/nc-gui/composables/useExtensions.ts b/packages/nc-gui/composables/useExtensions.ts
index 1cbaa77887..f0a29befec 100644
--- a/packages/nc-gui/composables/useExtensions.ts
+++ b/packages/nc-gui/composables/useExtensions.ts
@@ -3,12 +3,7 @@ import { ExtensionsEvents } from '#imports'
const extensionsState = createGlobalState(() => {
const baseExtensions = ref>({})
- // Egg
- const extensionsEgg = ref(false)
-
- const extensionsEggCounter = ref(0)
-
- return { baseExtensions, extensionsEgg, extensionsEggCounter }
+ return { baseExtensions }
})
export interface ExtensionManifest {
@@ -61,7 +56,7 @@ abstract class ExtensionType {
export { ExtensionType }
export const useExtensions = createSharedComposable(() => {
- const { baseExtensions, extensionsEgg, extensionsEggCounter } = extensionsState()
+ const { baseExtensions } = extensionsState()
const { $api } = useNuxtApp()
@@ -488,13 +483,6 @@ export const useExtensions = createSharedComposable(() => {
// Extension market modal
const isMarketVisible = ref(false)
- const onEggClick = () => {
- extensionsEggCounter.value++
- if (extensionsEggCounter.value >= 2) {
- extensionsEgg.value = true
- }
- }
-
return {
extensionsLoaded,
availableExtensions,
@@ -514,8 +502,6 @@ export const useExtensions = createSharedComposable(() => {
detailsFrom,
showExtensionDetails,
isMarketVisible,
- onEggClick,
- extensionsEgg,
extensionPanelSize,
eventBus,
}
diff --git a/packages/nc-gui/composables/useGridViewData.ts b/packages/nc-gui/composables/useGridViewData.ts
new file mode 100644
index 0000000000..6c9efcd74c
--- /dev/null
+++ b/packages/nc-gui/composables/useGridViewData.ts
@@ -0,0 +1,303 @@
+import type { Api, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
+import type { ComputedRef, Ref } from 'vue'
+import type { EventHook } from '@vueuse/core'
+import { NavigateDir, type Row } from '#imports'
+
+const formatData = (list: Record[], pageInfo: PaginatedType) =>
+ list.map((row, index) => ({
+ row: { ...row },
+ oldRow: { ...row },
+ rowMeta: {
+ // Calculate the rowIndex based on the offset and the index of the row
+ rowIndex: (pageInfo.page - 1) * pageInfo.pageSize + index,
+ },
+ }))
+
+export function useGridViewData(
+ _meta: Ref | ComputedRef,
+ viewMeta: Ref | ComputedRef<(ViewType & { id: string }) | undefined>,
+ where?: ComputedRef,
+ reloadVisibleDataHook?: EventHook,
+) {
+ const tablesStore = useTablesStore()
+ const { activeTableId, activeTable } = storeToRefs(tablesStore)
+
+ const meta = computed(() => _meta.value || activeTable.value)
+
+ const metaId = computed(() => _meta.value?.id || activeTableId.value)
+
+ const { t } = useI18n()
+
+ const optimisedQuery = useState('optimisedQuery', () => true)
+
+ const router = useRouter()
+
+ const route = router.currentRoute
+
+ const { appInfo, gridViewPageSize } = useGlobal()
+
+ const appInfoDefaultLimit = gridViewPageSize.value || appInfo.value.defaultLimit || 25
+
+ const _paginationData = ref({ page: 1, pageSize: appInfoDefaultLimit })
+
+ const excludePageInfo = ref(false)
+
+ const isPublic = inject(IsPublicInj, ref(false))
+
+ const { base } = storeToRefs(useBase())
+
+ const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
+
+ const { $api } = useNuxtApp()
+
+ const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
+
+ const { isUIAllowed } = useRoles()
+
+ const routeQuery = computed(() => route.value.query as Record)
+
+ const paginationData = computed({
+ get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
+ set: (value) => {
+ if (isPublic.value) {
+ sharedPaginationData.value = value
+ } else {
+ _paginationData.value = value
+ }
+ },
+ })
+
+ const {
+ insertRow,
+ updateRowProperty,
+ addEmptyRow,
+ deleteRow,
+ deleteRowById,
+ deleteSelectedRows,
+ deleteRangeOfRows,
+ updateOrSaveRow,
+ cachedRows,
+ clearCache,
+ totalRows,
+ bulkUpdateRows,
+ bulkUpdateView,
+ removeRowIfNew,
+ syncCount,
+ selectedRows,
+ chunkStates,
+ isRowSortRequiredRows,
+ clearInvalidRows,
+ applySorting,
+ } = useInfiniteData({
+ meta,
+ viewMeta,
+ callbacks: {
+ loadData,
+ syncVisibleData,
+ },
+ where,
+ })
+
+ function syncVisibleData() {
+ reloadVisibleDataHook?.trigger()
+ }
+
+ function getExpandedRowIndex(): number {
+ const rowId = routeQuery.value.rowId
+ if (!rowId) return -1
+
+ for (const [_index, row] of cachedRows.value.entries()) {
+ if (extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) === rowId) {
+ return row.rowMeta.rowIndex!
+ }
+ }
+ return -1
+ }
+
+ const isLastRow = computed(() => {
+ const expandedRowIndex = getExpandedRowIndex()
+ if (expandedRowIndex === -1) return false
+
+ return expandedRowIndex === totalRows.value - 1
+ })
+
+ const isFirstRow = computed(() => {
+ const expandedRowIndex = getExpandedRowIndex()
+ if (expandedRowIndex === -1) return false
+
+ return expandedRowIndex === 0
+ })
+
+ const queryParams = computed(() => ({
+ offset: ((paginationData.value.page ?? 0) - 1) * (paginationData.value.pageSize ?? appInfoDefaultLimit),
+ limit: paginationData.value.pageSize ?? appInfoDefaultLimit,
+ where: where?.value ?? '',
+ }))
+
+ async function loadAggCommentsCount(formattedData: Array) {
+ if (!isUIAllowed('commentCount') || isPublic.value) return
+
+ const ids = formattedData
+ .filter(({ rowMeta: { new: isNew } }) => !isNew)
+ .map(({ row }) => extractPkFromRow(row, meta?.value?.columns as ColumnType[]))
+ .filter(Boolean)
+
+ if (!ids.length) return
+
+ try {
+ const aggCommentCount = await $api.utils.commentCount({
+ ids,
+ fk_model_id: metaId.value as string,
+ })
+
+ formattedData.forEach((row) => {
+ const cachedRow = Array.from(cachedRows.value.values()).find(
+ (cachedRow) => cachedRow.rowMeta.rowIndex === row.rowMeta.rowIndex,
+ )
+ if (!cachedRow) return
+
+ const id = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
+ const count = aggCommentCount?.find((c: Record) => c.row_id === id)?.count || 0
+ cachedRow.rowMeta.commentCount = +count
+ })
+ } catch (e) {
+ console.error('Failed to load aggregate comment count:', e)
+ }
+ }
+
+ async function loadData(
+ params: Parameters['dbViewRow']['list']>[4] & {
+ limit?: number
+ offset?: number
+ } = {},
+ ): Promise {
+ if ((!base?.value?.id || !metaId.value || !viewMeta.value?.id) && !isPublic.value) return
+
+ try {
+ const response = !isPublic.value
+ ? await $api.dbViewRow.list('noco', base.value.id!, metaId.value!, viewMeta.value!.id!, {
+ ...queryParams.value,
+ ...params,
+ ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
+ ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
+ where: where?.value,
+ ...(excludePageInfo.value ? { excludeCount: 'true' } : {}),
+ } as any)
+ : await fetchSharedViewData(
+ {
+ sortsArr: sorts.value,
+ filtersArr: nestedFilters.value,
+ where: where?.value,
+ offset: params.offset,
+ limit: params.limit,
+ },
+ {
+ isInfiniteScroll: true,
+ },
+ )
+
+ const data = formatData(response.list, response.pageInfo)
+
+ if (response.pageInfo.totalRows) {
+ totalRows.value = response.pageInfo.totalRows
+ }
+
+ loadAggCommentsCount(data)
+
+ return data
+ } catch (error: any) {
+ if (error?.response?.data.error === 'INVALID_OFFSET_VALUE') {
+ return []
+ }
+ if (error?.response?.data?.error === 'FORMULA_ERROR') {
+ await tablesStore.reloadTableMeta(metaId.value as string)
+ return loadData(params)
+ }
+
+ console.error(error)
+ message.error(await extractSdkResponseErrorMsg(error))
+ }
+ }
+
+ const navigateToSiblingRow = async (dir: NavigateDir) => {
+ const expandedRowIndex = getExpandedRowIndex()
+ if (expandedRowIndex === -1) return
+
+ const sortedIndices = Array.from(cachedRows.value.keys()).sort((a, b) => a - b)
+ let siblingIndex = sortedIndices.findIndex((index) => index === expandedRowIndex) + (dir === NavigateDir.NEXT ? 1 : -1)
+
+ // Skip unsaved rows
+ while (
+ siblingIndex >= 0 &&
+ siblingIndex < sortedIndices.length &&
+ cachedRows.value.get(sortedIndices[siblingIndex])?.rowMeta?.new
+ ) {
+ siblingIndex += dir === NavigateDir.NEXT ? 1 : -1
+ }
+
+ // Check if we've gone out of bounds
+ if (siblingIndex < 0 || siblingIndex >= totalRows.value) {
+ return message.info(t('msg.info.noMoreRecords'))
+ }
+
+ // If the sibling row is not in cachedRows, load more data
+ if (siblingIndex >= sortedIndices.length) {
+ await loadData({
+ offset: sortedIndices[sortedIndices.length - 1] + 1,
+ limit: 10,
+ })
+ sortedIndices.push(
+ ...Array.from(cachedRows.value.keys())
+ .filter((key) => !sortedIndices.includes(key))
+ .sort((a, b) => a - b),
+ )
+ }
+
+ // Extract the row id of the sibling row
+ const siblingRow = cachedRows.value.get(sortedIndices[siblingIndex])
+ if (siblingRow) {
+ const rowId = extractPkFromRow(siblingRow.row, meta.value?.columns as ColumnType[])
+ if (rowId) {
+ await router.push({
+ query: {
+ ...routeQuery.value,
+ rowId,
+ },
+ })
+ }
+ }
+ }
+
+ return {
+ cachedRows,
+ loadData,
+ paginationData,
+ queryParams,
+ insertRow,
+ updateRowProperty,
+ addEmptyRow,
+ deleteRow,
+ deleteRowById,
+ deleteSelectedRows,
+ deleteRangeOfRows,
+ updateOrSaveRow,
+ bulkUpdateRows,
+ bulkUpdateView,
+ loadAggCommentsCount,
+ syncCount,
+ removeRowIfNew,
+ navigateToSiblingRow,
+ getExpandedRowIndex,
+ optimisedQuery,
+ isLastRow,
+ isFirstRow,
+ clearCache,
+ totalRows,
+ selectedRows,
+ syncVisibleData,
+ chunkStates,
+ clearInvalidRows,
+ applySorting,
+ isRowSortRequiredRows,
+ }
+}
diff --git a/packages/nc-gui/composables/useInfiniteData.ts b/packages/nc-gui/composables/useInfiniteData.ts
new file mode 100644
index 0000000000..d1ab66402d
--- /dev/null
+++ b/packages/nc-gui/composables/useInfiniteData.ts
@@ -0,0 +1,1371 @@
+import type { ComputedRef, Ref } from 'vue'
+import {
+ type Api,
+ type ColumnType,
+ type LinkToAnotherRecordType,
+ type RelationTypes,
+ type TableType,
+ UITypes,
+ type ViewType,
+ extractFilterFromXwhere,
+ isCreatedOrLastModifiedByCol,
+ isCreatedOrLastModifiedTimeCol,
+} from 'nocodb-sdk'
+import type { Row } from '../lib/types'
+import { validateRowFilters } from '../utils/dataUtils'
+import type { CellRange } from './useMultiSelect/cellRange'
+
+export function useInfiniteData(args: {
+ meta: Ref | ComputedRef
+ viewMeta: Ref | ComputedRef<(ViewType & { id: string }) | undefined>
+ callbacks: {
+ loadData?: (
+ params: Parameters['dbViewRow']['list']>[4] & {
+ limit?: number
+ offset?: number
+ },
+ ) => Promise
+ syncVisibleData?: () => void
+ }
+ where?: ComputedRef
+}) {
+ const { meta, viewMeta, callbacks, where } = args
+
+ const NOCO = 'noco'
+
+ const { $api } = useNuxtApp()
+
+ const { addUndo, clone, defineViewScope } = useUndoRedo()
+
+ const { t } = useI18n()
+
+ const { fetchCount } = useSharedView()
+
+ const { getBaseType } = useBase()
+
+ const { getMeta, metas } = useMetas()
+
+ const { base } = storeToRefs(useBase())
+
+ const isPublic = inject(IsPublicInj, ref(false))
+
+ const reloadAggregate = inject(ReloadAggregateHookInj)
+
+ const { nestedFilters, allFilters, xWhere, sorts } = useSmartsheetStoreOrThrow()
+
+ const columnsByAlias = computed(() => {
+ if (!meta.value?.columns?.length) return {}
+ return meta.value?.columns.reduce((acc, column) => {
+ acc[column.title!] = column
+ return acc
+ }, {} as Record)
+ })
+ const columnsById = computed(() => {
+ if (!meta.value?.columns?.length) return {}
+ return meta.value?.columns.reduce((acc, column) => {
+ acc[column.id!] = column
+ return acc
+ }, {} as Record)
+ })
+
+ const computedWhereFilter = computed(() => {
+ const filter = extractFilterFromXwhere(xWhere.value ?? '', columnsByAlias.value)
+
+ return filter.map((f) => {
+ return { ...f, value: f.value ? f.value?.toString().replace(/(^%)(.*?)(%$)/, '$2') : f.value }
+ })
+ })
+
+ const cachedRows = ref