多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
8.5 KiB

feat: infinite scroll (#9403) * feat: infinite scroll wip * feat: implement column create * feat: improve scroll performance and minor bugs * fix: optimize cache clear fix: preserver selected items from cache clear * feat: add keyboard support * fix: get rid of unwanted data * feat: infinite scroll * fix: reload visible data * fix: rowIndex Sync * fix: row height fix * fix: performance issues * fix: small issues * fix: stablize scrolling * fix: scroll fails to load new data * fix: best part is no part remove bunch of manual handling and move to computedProperty * fix: load data as chunks instead of offset * fix: deboucne 500 ms * feat: safe chunk clearing * feat: working infinite table(wip) * fix: handle delete selected range of rows * fix: update types * fix: nuxt warnings * fix: table fixes * feat: undo-redo support for infiniteTable * fix: fix addEmptyRow * fix: groupby fixes * fix: refactor visibleDtaa computed * fix: cache clear * fix: invalid offset error * fix: add empty row elem * fix: rows not loading at end * fix: refactor * fix: more tests passing * fix: perf optimizations * fix: couple tests * fix: row height tests * fix: row height tests * fix: row height tests * fix: sync row comment count * fix: fixes * fix: lot of tests * fix: update the row placeholder columns size calculation * fix: update the totalRows on loadData * fix: tests when count is 0 * fix: hide placeholder if rowHeight is small * fix: not required imo as infinite scroll is implemented * fix: links tests * fix: filter tests * fix: insert after test fix: Row: Create, Update, Delete fix: Row height fix: Create column tests * fix: error, timezone bug fix: shared view data not loading after 100 * fix: ignore shifting. this fixes errors in rows, which has some mandatory required cells * fix: keyboardShortcuts test * fix: project collaboration test * fix: increase local cache fix: records empty on switching to full screen mode fix: links issue on new records * fix: row and col margin for improved data rendering * fix: addEmptyRow to table bottom * fix: update gridShare test fix: shared view grid feat: new count endpoint public * fix: undo-redo test failing * fix: bulkUpdate chore: disabled bulkUpdate tests for now * fix: slow searchBtn * fix: limit max selection * fix: limit selection to 100 * fix: initial chunk load to 100 * fix: couple fixes * fix: couple fixes * fix: row expand * fix: scrollto Top and scrollTo Bottom on Shift Cmd Down/Up * fix: drop support for cmd A * fix: failing tests * fix: error on clicking resize col * fix: premature fetching * fix: deleteSelected not working properly * fix: test build * fix: test build * fix: throttled 500 * fix: scroll related issues fix: added transitions * fix: scroll related issues * fix: decrease col margin * fix: increase local cache and update Buffer * fix: decrease throttle * fix: improve scroll performance * fix: improve scroll performance * fix: improve scroll performance * fix: fixes * feat: beta toggle show * feat: beta toggle show * fix: hold scroll action * fix: sync visible data reloadVisibleDataHook * fix: refactor useBetaFeatureToggle fix: useMultiSelect in table * fix: dynamically reduce margin while loading records * fix: some bugs * fix: data loading in infinitescroll * fix: shared view and search issues * feat: betaToggles menu * fix: scroll showing up in aggregation * fix: text * fix: implement shifting in addEmptyRow * fix: calculate slices on rowHeight modified * fix: keep invalid cells until another row selected * fix: remove row if filter gets failed * fix: update styles * fix: move filter handling to nocodb-sdk * fix: user field filter * fix: sort order * fix:user field sorting * fix: update virtual cols * fix: updated sort handling * fix: updated sort handling * fix: updated sort handling for bulkUpdate and undo-redo * fix: unit tests * fix: deleteSelectedRecords fails * fix: chunkstates errors * fix: sort bugs * fix: scroll position * fix: delete selectedrange of records * fix: improved chunk management * fix: sync toggle states across tabs * fix: sync between tabs * fix: limit issues * fix: update issues * fix: zIndex * fix: minor fixes * fix: cmd arrow issue * fix: bulkdelete index issues * fix: empty rows at top * fix: queue add new record behaviour * fix: resolve rowObj addEmptyRow * fix: typo * fix: clear indexes * fix: reload if width is zero * fix: manual handling * fix: remove console * fix: prefetch when scroll from below * fix: refactor fixes * fix:undo-redo with sort --------- Co-authored-by: mertmit <mertmit99@gmail.com>
2 weeks ago
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<string, any>[], 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<TableType | undefined> | ComputedRef<TableType | undefined>,
viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>,
where?: ComputedRef<string | undefined>,
reloadVisibleDataHook?: EventHook<void>,
) {
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<PaginatedType>({ 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<string, string>)
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<Row>) {
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<string, any>) => 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<Api<any>['dbViewRow']['list']>[4] & {
limit?: number
offset?: number
} = {},
): Promise<Row[] | undefined> {
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,
}
}