import { type ColumnType, type TableType, type ViewType, isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol, isVirtualCol, } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' import type { EventHook } from '@vueuse/core' import { findGroupByPath } from '../components/smartsheet/grid/canvas/utils/groupby' import type { CanvasGroup } from '../lib/types' import { useInfiniteGroups } from './useInfiniteGroups' import { type CellRange, type Row } from '#imports' export function useGridViewData( _meta: Ref | ComputedRef, viewMeta: Ref | ComputedRef<(ViewType & { id: string }) | undefined>, where?: ComputedRef, reloadVisibleDataHook?: EventHook, ) { const tablesStore = useTablesStore() const { activeTable } = storeToRefs(tablesStore) const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook()) const meta = computed(() => _meta.value || activeTable.value) const { t } = useI18n() const optimisedQuery = useState('optimisedQuery', () => true) const isPublic = inject(IsPublicInj, ref(false)) const { getMeta } = useMetas() const reloadAggregate = inject(ReloadAggregateHookInj) const { addUndo, clone, defineViewScope } = useUndoRedo() const { base } = storeToRefs(useBase()) const { $api } = useNuxtApp() const isBulkOperationInProgress = ref(false) const { cachedGroups, totalGroups, toggleExpand, groupByColumns, isGroupBy, buildNestedFilterArr, clearGroupCache, syncCount: groupSyncCount, fetchMissingGroupChunks, updateGroupAggregations, toggleExpandAll, } = useInfiniteGroups(viewMeta, meta, where, { syncVisibleData, }) const { insertRow, updateRowProperty, addEmptyRow, deleteRow, deleteRowById, updateOrSaveRow, cachedRows, clearCache, totalRows, actualTotalRows, bulkUpdateView, removeRowIfNew, syncCount, fetchChunk, recoverLTARRefs, getChunkIndex, selectedRows, chunkStates, isRowSortRequiredRows, clearInvalidRows, applySorting, CHUNK_SIZE, isLastRow, isFirstRow, getExpandedRowIndex, loadData, updateRecordOrder, selectedAllRecords, selectedAllRecordsSkipPks, loadAggCommentsCount, navigateToSiblingRow, getRows, getDataCache, groupDataCache, } = useInfiniteData({ meta, viewMeta, callbacks: { syncVisibleData, getCount, getWhereFilter: async (_path?: Array) => where?.value ?? '', getWhereFilterArr: getGroupFilterArr, reloadAggregate: triggerAggregateReload, findGroupByPath: (path?: Array) => { return findGroupByPath(cachedGroups.value, path) }, }, groupByColumns, where, isPublic, }) function triggerAggregateReload(params: { fields?: Array<{ title: string; aggregation?: string | undefined }> path: Array }) { const { fields, path } = params reloadAggregate?.trigger(params) if (!isGroupBy.value) { return } function collectChildGroups(group: CanvasGroup): CanvasGroup[] { const result: CanvasGroup[] = [group] if (group.groups && group.groups.size > 0) { for (const childGroup of group.groups.values()) { result.push(...collectChildGroups(childGroup)) } } return result } const targetGroup = findGroupByPath(cachedGroups.value, path) if (!targetGroup) return const groupsToUpdate = collectChildGroups(targetGroup) updateGroupAggregations(groupsToUpdate, fields) if (path.length > 1) { for (let i = path.length - 1; i > 0; i--) { const parentPath = path.slice(0, i) const parentGroup = findGroupByPath(cachedGroups.value, parentPath) if (parentGroup) { const parentAndChildren = collectChildGroups(parentGroup) updateGroupAggregations(parentAndChildren, fields) } } } } const reloadAggregateListener = (v: Record = {}) => { const { path, fields } = v if (!path?.length && isGroupBy.value) { const allGroups: CanvasGroup[] = [] function collectAllGroups(groups: Map) { const groupArray = Array.from(groups.values()) allGroups.push(...groupArray) for (const group of groupArray) { if (group.groups && group.groups.size > 0) { collectAllGroups(group.groups) } } } collectAllGroups(cachedGroups.value) if (allGroups.length) { updateGroupAggregations(allGroups, fields) } } } reloadAggregate?.on(reloadAggregateListener) onBeforeUnmount(() => { reloadAggregate?.off(reloadAggregateListener) }) function getCount(path?: Array) { if (!path?.length) return let currentGroups = cachedGroups.value const pathCopy = [...path] for (let i = 0; i < path.length - 1; i++) { const groupIndex = pathCopy[i] const group = currentGroups.get(groupIndex) if (!group || !group.groups) { console.warn(`Invalid path: Group at index ${groupIndex} not found or has no subgroups`) return undefined } currentGroups = group.groups } const finalIndex = pathCopy[path.length - 1] const targetGroup = currentGroups.get(finalIndex) if (!targetGroup) { console.warn(`Target group at path [${path}] not found`) return undefined } return targetGroup.count } async function getGroupFilterArr(path: Array = [], _ignoreWhereFilter = false) { let group = findGroupByPath(cachedGroups.value, path) if (!group) { try { let currentGroups = cachedGroups.value let parentGroup: CanvasGroup | undefined let targetIndex: number | undefined for (let depth = 0; depth < path.length; depth++) { const groupIndex = path[depth] const currentGroup = currentGroups.get(groupIndex) if (!currentGroup) { targetIndex = groupIndex break } if (depth === path.length - 1) { targetIndex = groupIndex break } if (!currentGroup.isExpanded || !currentGroup.groups) { return [] } parentGroup = currentGroup currentGroups = currentGroup.groups } if (targetIndex !== undefined) { await fetchMissingGroupChunks(targetIndex, targetIndex, parentGroup) } group = findGroupByPath(cachedGroups.value, path) } catch (error) { console.error(`Failed to load group for path ${path}:`, error) return [] } } return buildNestedFilterArr(group) } function syncVisibleData() { reloadVisibleDataHook?.trigger() } async function deleteSelectedRows(path: Array = []): Promise { let removedRowsData: Record[] = [] let compositePrimaryKey = '' isBulkOperationInProgress.value = true const dataCache = getDataCache(path) for (const row of dataCache.selectedRows.value) { const { row: rowData, rowMeta } = row if (!rowMeta.selected || rowMeta.new) { continue } const extractedPk = extractPk(meta?.value?.columns as ColumnType[]) const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[]) as string const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[]) if (extractedPk && compositePkValue) { if (!compositePrimaryKey) compositePrimaryKey = extractedPk removedRowsData.push({ [compositePrimaryKey]: compositePkValue as string, pkData, row: clone(rowData), rowMeta, }) } } if (!removedRowsData.length) return try { const { list } = await $api.internal.getOperation((meta.value as any).fk_workspace_id!, meta.value!.base_id!, { operation: 'dataList', tableId: meta.value?.id as string, pks: removedRowsData.map((row) => row[compositePrimaryKey]).join(','), getHiddenColumns: true, limit: removedRowsData.length, }) removedRowsData = removedRowsData.map((row) => { const rowObj = row.row const rowPk = rowPkData(rowObj, meta.value?.columns as ColumnType[]) const fullRecord = list.find((r: Record) => { return Object.keys(rowPk).every((key) => r[key] === rowPk[key]) }) if (!fullRecord) return { ...row } return { ...row, row: { ...fullRecord }, } }) await bulkDeleteRows(removedRowsData.map((row) => row.pkData)) } catch (e: any) { const errorMessage = await extractSdkResponseErrorMsg(e) isBulkOperationInProgress.value = false return message.error(`${t('msg.error.deleteRowFailed')}: ${errorMessage}`) } await updateCacheAfterDelete(removedRowsData, false, path) addUndo({ undo: { fn: async (removedRowsData: Record[], path: Array) => { const rowsToInsert = removedRowsData.reverse() const insertedRowIds = await bulkInsertRows(rowsToInsert as Row[], undefined, true, path) if (Array.isArray(insertedRowIds)) { await Promise.all(rowsToInsert.map((row, _index) => recoverLTARRefs(row.row))) } }, args: [removedRowsData, clone(path)], }, redo: { fn: async (toBeRemovedData: Record[], path: Array) => { try { isBulkOperationInProgress.value = true await bulkDeleteRows(toBeRemovedData.map((row) => row.pkData)) await updateCacheAfterDelete(toBeRemovedData, false, path) await syncCount(path) } finally { isBulkOperationInProgress.value = false } }, args: [removedRowsData, clone(path)], }, scope: defineViewScope({ view: viewMeta.value }), }) isBulkOperationInProgress.value = false await syncCount(path) } async function bulkInsertRows( rows: Row[], { metaValue = meta.value, viewMetaValue = viewMeta.value, }: { metaValue?: TableType viewMetaValue?: ViewType } = {}, undo = false, path: Array = [], ): Promise { if (!metaValue || !viewMetaValue) { throw new Error('Meta value or view meta value is undefined') } const dataCache = getDataCache(path) isBulkOperationInProgress.value = true const autoGeneratedKeys = new Set( metaValue.columns ?.filter((c) => !c.pk && (isCreatedOrLastModifiedTimeCol(c) || isCreatedOrLastModifiedByCol(c))) .map((c) => c.title!), ) try { const rowsToInsert = await Promise.all( rows.map(async (currentRow) => { const { missingRequiredColumns, insertObj } = await populateInsertObject({ meta: metaValue, ltarState: {}, getMeta, row: currentRow.row, undo, }) if (missingRequiredColumns.size === 0) { const newInsertObj = { ...insertObj } for (const key of autoGeneratedKeys) { delete newInsertObj[key] } return { insertObj: newInsertObj, rowIndex: currentRow.rowMeta.rowIndex, } } return null }), ) const validRowsToInsert = rowsToInsert.filter(Boolean) as { insertObj: Record; rowIndex: number }[] const bulkInsertedIds = await $api.dbDataTableRow.create( metaValue.id!, validRowsToInsert.map((row) => row!.insertObj), { viewId: viewMetaValue.id, undo, }, ) validRowsToInsert.sort((a, b) => (a!.rowIndex ?? 0) - (b!.rowIndex ?? 0)) const newCachedRows = new Map() for (const [index, row] of dataCache.cachedRows.value) { newCachedRows.set(index, { ...row, rowMeta: { ...row.rowMeta, rowIndex: index } }) } for (const { insertObj, rowIndex } of validRowsToInsert) { // If there's already a row at this index, shift it and all subsequent rows if (newCachedRows.has(rowIndex!)) { const rowsToShift = Array.from(newCachedRows.entries()) .filter(([index]) => index >= rowIndex!) .sort((a, b) => b[0] - a[0]) // Sort in descending order for (const [index, row] of rowsToShift) { const newIndex = index + 1 newCachedRows.set(newIndex, { ...row, rowMeta: { ...row.rowMeta, rowIndex: newIndex } }) } } const newRow = { row: { ...insertObj, id: bulkInsertedIds[validRowsToInsert.indexOf({ insertObj, rowIndex })] }, oldRow: {}, rowMeta: { rowIndex: rowIndex!, new: false, path }, } newCachedRows.set(rowIndex!, newRow) } const indices = new Set() for (const [_, row] of newCachedRows) { if (indices.has(row.rowMeta.rowIndex)) { console.error(`Op: bulkInsertRows ${undo}: Duplicate index detected:`, row.rowMeta.rowIndex) break } indices.add(row.rowMeta.rowIndex) } dataCache.cachedRows.value = newCachedRows dataCache.totalRows.value += validRowsToInsert.length await syncCount(path, true, false) syncVisibleData() return bulkInsertedIds } catch (error: any) { const errorMessage = await extractSdkResponseErrorMsg(error) message.error(`Failed to bulk insert rows: ${errorMessage}`) throw error } finally { isBulkOperationInProgress.value = false } } async function bulkUpdateRows( rows: Row[], props: string[], { metaValue = meta.value, onError }: { metaValue?: TableType; viewMetaValue?: ViewType; onError?: (e: any) => void } = {}, undo = false, path: Array = [], ): Promise { isBulkOperationInProgress.value = true const dataCache = getDataCache(path) await Promise.all( rows.map(async (row) => { if (row.rowMeta) { row.rowMeta.changed = false await until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v) row.rowMeta.saving = true } }), ) const pksIndex = [] as { pk: string; rowIndex: number }[] const updateArray = rows.map((row) => { const pk = rowPkData(row.row, metaValue?.columns as ColumnType[]) const updateData = props.reduce((acc, prop) => ({ ...acc, [prop]: row.row[prop] }), {}) pksIndex.push({ pk: extractPkFromRow(row.row, metaValue?.columns as ColumnType[]) as string, rowIndex: row.rowMeta.rowIndex!, }) return { ...updateData, ...pk } }) try { const newRows = (await $api.dbTableRow.bulkUpdate( NOCO, metaValue?.base_id as string, metaValue?.id as string, updateArray, )) as Record triggerAggregateReload({ fields: props.map((p) => ({ title: p })), path }) newRows.forEach((newRow: Record) => { const pk = extractPkFromRow(newRow, metaValue?.columns as ColumnType[]) const rowIndex = pksIndex.find((pkIndex) => pkIndex.pk === pk)?.rowIndex if (rowIndex !== undefined && rowIndex !== null) { const row = dataCache.cachedRows.value.get(rowIndex) if (row) { row.rowMeta.saving = false row.row = { ...row.row, ...newRow, } // Mark row as hidden if it moved out of user's RLS scope after update if (newRow.__nc_rls_hidden) { row.rowMeta.isRlsHidden = true } dataCache.cachedRows.value.set(rowIndex, row) } } }) } catch (e) { console.error(e) onError?.(e) message.error(await extractSdkResponseErrorMsg(e as any)) isBulkOperationInProgress.value = false return } finally { rows.forEach((row) => { if (row.rowMeta) row.rowMeta.saving = false }) } if (!undo) { addUndo({ undo: { fn: async (undoRows: Row[], props: string[], path: Array) => { await bulkUpdateRows( undoRows.map((r) => ({ ...r, row: r.oldRow, oldRow: r.row, })), props, undefined, true, path, ) }, args: [clone(rows), props, clone(path)], }, redo: { fn: async (redoRows: Row[], props: string[], path: Array) => { await bulkUpdateRows(redoRows, props, undefined, true, path) }, args: [clone(rows), props, clone(path)], }, scope: defineViewScope({ view: viewMeta.value }), }) } applySorting(rows) syncVisibleData() reloadViewDataHook?.trigger() isBulkOperationInProgress.value = false } async function bulkUpsertRows( insertRows: Row[], updateRows: Row[], props: string[], { metaValue = meta.value, viewMetaValue = viewMeta.value, }: { metaValue?: TableType viewMetaValue?: ViewType } = {}, columns: Partial[], undo = false, path: Array = [], ) { const dataCache = getDataCache(path) try { isBulkOperationInProgress.value = true const newCols = (meta.value.columns ?? []).filter((col: ColumnType) => columns.some((c) => c.title === col.title)) const rowsToFetch = updateRows.filter((row) => !dataCache.cachedRows.value.has(row.rowMeta.rowIndex!)) const chunksToFetch = new Set(rowsToFetch.map((row) => Math.floor(row.rowMeta.rowIndex! / CHUNK_SIZE))) await Promise.all(Array.from(chunksToFetch).map((chunkId) => fetchChunk(chunkId, path))) const getPk = (row: Row) => extractPkFromRow(row.row, metaValue?.columns as ColumnType[]) const ogUpdateRows = updateRows.map((_row) => { const row = _row ?? dataCache.cachedRows.value.get((_row as Row).rowMeta.rowIndex!) newCols.forEach((col: ColumnType) => { row.oldRow[col.title!] = undefined }) return clone(row) }) const cleanRow = (row: any) => { const cleanedRow = { ...row } metaValue?.columns?.forEach((col) => { if (col.system || isVirtualCol(col)) delete cleanedRow[col.title!] }) return cleanedRow } updateRows = updateRows.map((row) => { const cachedRow = dataCache.cachedRows.value.get(row.rowMeta.rowIndex!) if (cachedRow) { return { ...cachedRow, row: { ...cachedRow.row, ...row.row }, oldRow: cachedRow.row, } } return row }) const bulkUpsertedRows = await $api.dbTableRow.bulkUpsert( NOCO, metaValue?.base_id ?? (base.value?.id as string), metaValue?.id as string, [...insertRows.map((row) => cleanRow(row.row)), ...updateRows.map((row) => cleanRow(row.row))], {}, ) const existingPks = new Set(Array.from(dataCache.cachedRows.value.values()).map((row) => getPk(row))) const [insertedRows, updatedRows] = bulkUpsertedRows.reduce( ([inserted, updated], row) => { const isPkExisting = existingPks.has(extractPkFromRow(row, metaValue?.columns as ColumnType[])) return isPkExisting ? [inserted, [...updated, { row, rowMeta: {}, oldRow: row }]] : [[...inserted, { row, rowMeta: {}, oldRow: {} }], updated] }, [[], []] as [Row[], Row[]], ) insertedRows.forEach((row: Row, index: number) => { const newIndex = dataCache.totalRows.value + index row.rowMeta.rowIndex = newIndex dataCache.cachedRows.value.set(newIndex, { ...row, rowMeta: { ...row.rowMeta, rowIndex: newIndex } }) }) updatedRows.forEach((row: Row) => { const existingRow = Array.from(dataCache.cachedRows.value.entries()).find(([_, r]) => getPk(r) === getPk(row)) if (existingRow) { dataCache.cachedRows.value.set(existingRow[0], { ...row, rowMeta: { ...row.rowMeta, rowIndex: existingRow[0] }, }) } }) dataCache.totalRows.value += insertedRows.length if (!undo) { addUndo({ undo: { fn: async (insertedRows: Row[], ogUpdateRows: Row[], path: Array) => { try { isBulkOperationInProgress.value = true await bulkDeleteRows( insertedRows.map((row) => rowPkData(row.row, metaValue?.columns as ColumnType[]) as Record), ) await bulkUpdateRows( ogUpdateRows.map((r) => ({ ...r, row: r.oldRow, oldRow: r.row, })), props, { metaValue }, true, path, ) isBulkOperationInProgress.value = true const columnsHash = ( await $api.internal.getOperation(meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnsHash', tableId: meta.value?.id as string, }) ).hash await $api.internal.postOperation( meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnsBulk', tableId: meta.value?.id as string }, { hash: columnsHash, ops: newCols.map((col: ColumnType) => ({ op: 'delete', column: col, })), }, ) insertedRows.forEach((row) => { dataCache.cachedRows.value.delete(row.rowMeta.rowIndex!) }) dataCache.totalRows.value = dataCache.totalRows.value - insertedRows.length syncVisibleData() await getMeta(meta.value!.base_id!, meta.value?.id as string, true) } catch (e) { } finally { isBulkOperationInProgress.value = false } }, args: [clone(insertedRows), clone(ogUpdateRows), clone(path)], }, redo: { fn: async (insertRows: Row[], updateRows: Row[], path: Array) => { try { isBulkOperationInProgress.value = true const columnsHash = ( await $api.internal.getOperation(meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnsHash', tableId: meta.value?.id as string, }) ).hash await $api.internal.postOperation( meta.value!.fk_workspace_id!, meta.value!.base_id!, { operation: 'columnsBulk', tableId: meta.value?.id as string }, { hash: columnsHash, ops: newCols.map((col: ColumnType) => ({ op: 'add', column: col, })), }, ) await bulkUpsertRows(insertRows, updateRows, props, { metaValue, viewMetaValue }, columns, true, path) isBulkOperationInProgress.value = true await getMeta(meta.value!.base_id!, meta.value?.id as string, true) syncVisibleData() } finally { isBulkOperationInProgress.value = false } }, args: [clone(insertedRows), clone(updatedRows), clone(path)], }, scope: defineViewScope({ view: viewMeta.value }), }) } reloadViewDataHook?.trigger() syncVisibleData() await syncCount(path, true, false) } catch (error: any) { message.error(await extractSdkResponseErrorMsg(error)) } finally { isBulkOperationInProgress.value = false } } async function updateCacheAfterDelete( rowsToDelete: Record[], nested = true, path: Array = [], ): Promise { const dataCache = getDataCache(path) const maxCachedIndex = Math.max(...dataCache.cachedRows.value.keys()) const newCachedRows = new Map() const deleteSet = new Set(rowsToDelete.map((row) => (nested ? row.row : row).rowMeta.rowIndex)) const affectedChunks = new Set() let deletionCount = 0 for (let i = 0; i <= maxCachedIndex + 1; i++) { if (deleteSet.has(i)) { deletionCount++ affectedChunks.add(getChunkIndex(i)) continue } if (dataCache.cachedRows.value.has(i)) { const row = dataCache.cachedRows.value.get(i) if (row) { const newIndex = i - deletionCount row.rowMeta.rowIndex = newIndex newCachedRows.set(newIndex, row) affectedChunks.add(getChunkIndex(i)) affectedChunks.add(getChunkIndex(newIndex)) } } } const rowsByChunk = new Map() for (const [_, row] of newCachedRows) { const chunkIndex = getChunkIndex(row.rowMeta.rowIndex) rowsByChunk.set(chunkIndex, (rowsByChunk.get(chunkIndex) || 0) + 1) } for (const chunkIndex of affectedChunks) { if (!rowsByChunk.has(chunkIndex) || rowsByChunk.get(chunkIndex) < CHUNK_SIZE) { dataCache.chunkStates.value[chunkIndex] = undefined } } const indices = new Set() for (const [_, row] of newCachedRows) { if (indices.has(row.rowMeta.rowIndex)) { console.error(`Op: updateCacheAfterDelete: Duplicate index detected:`, row.rowMeta.rowIndex) break } indices.add(row.rowMeta.rowIndex) } dataCache.cachedRows.value = newCachedRows dataCache.totalRows.value = Math.max(0, dataCache.totalRows.value - rowsToDelete.length) await syncCount(path) syncVisibleData() } async function deleteRangeOfRows(cellRange: CellRange, path: Array = []): Promise { if (!cellRange._start || !cellRange._end) return isBulkOperationInProgress.value = true const dataCache = getDataCache(path) const start = Math.min(cellRange._start.row, cellRange._end.row) const end = Math.max(cellRange._start.row, cellRange._end.row) let rowsToDelete: Record[] = [] let compositePrimaryKey = '' await getRows(start, end, path) for (let i = start; i <= end; i++) { const cachedRow = dataCache.cachedRows.value.get(i) if (!cachedRow) { console.warn(`Record at index ${i} not found in local cache`) continue } const { row: rowData, rowMeta } = cachedRow if (!rowMeta.new) { const extractedPk = extractPk(meta?.value?.columns as ColumnType[]) const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[]) const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[]) if (extractedPk && compositePkValue) { if (!compositePrimaryKey) compositePrimaryKey = extractedPk rowsToDelete.push({ [compositePrimaryKey]: compositePkValue, pkData, ...cachedRow, rowIndex: i, }) } } } if (!rowsToDelete.length) return const { list } = await $api.internal.getOperation((meta.value as any).fk_workspace_id!, meta.value!.base_id!, { operation: 'dataList', tableId: meta.value?.id as string, pks: rowsToDelete.map((row) => row[compositePrimaryKey]).join(','), getHiddenColumns: 'true', limit: rowsToDelete.length, }) try { rowsToDelete = rowsToDelete.map((row) => { const rowObj = row.row const rowPk = rowPkData(rowObj, meta.value?.columns as ColumnType[]) const fullRecord = list.find((r: Record) => { return Object.keys(rowPk).every((key) => r[key] === rowPk[key]) }) if (!fullRecord) { console.warn(`Full record not found for row with index ${row.rowMeta.rowIndex}`) return row } row.row = fullRecord return row }) await bulkDeleteRows(rowsToDelete.map((row) => row.pkData)) } catch (e: any) { const errorMessage = await extractSdkResponseErrorMsg(e) message.error(`${t('msg.error.deleteRowFailed')}: ${errorMessage}`) isBulkOperationInProgress.value = false throw e } addUndo({ undo: { fn: async (deletedRows: Record[], path: Array) => { const rowsToInsert = deletedRows.reverse() const insertedRowIds = await bulkInsertRows(rowsToInsert, undefined, true, path) if (Array.isArray(insertedRowIds)) { await Promise.all(rowsToInsert.map((row, _index) => recoverLTARRefs(row.row))) } }, args: [rowsToDelete, clone(path)], }, redo: { fn: async (rowsToDelete: Record[], path: Array) => { await bulkDeleteRows(rowsToDelete.map((row) => row.pkData)) await updateCacheAfterDelete(rowsToDelete, false, path) }, args: [rowsToDelete, clone(path)], }, scope: defineViewScope({ view: viewMeta.value }), }) await updateCacheAfterDelete(rowsToDelete, false, path) isBulkOperationInProgress.value = false } async function bulkDeleteRows( rows: Record[], { metaValue = meta.value, viewMetaValue = viewMeta.value, }: { metaValue?: TableType viewMetaValue?: ViewType } = {}, ): Promise { try { const bulkDeletedRowsData = await $api.internal.postOperation( (metaValue as any).fk_workspace_id!, metaValue!.base_id!, { operation: 'dataDelete', tableId: metaValue?.id as string, viewId: viewMetaValue?.id as string, }, rows.length === 1 ? rows[0] : rows, ) triggerAggregateReload({ path: [] }) return rows.length === 1 && bulkDeletedRowsData ? [bulkDeletedRowsData] : bulkDeletedRowsData } catch (error: any) { const errorMessage = await extractSdkResponseErrorMsg(error) message.error(`Bulk delete failed: ${errorMessage}`) } } async function bulkDeleteAll(path: Array = []) { try { isBulkOperationInProgress.value = true await $api.internal.postOperation( (meta.value as any).fk_workspace_id!, meta.value!.base_id!, { operation: 'bulkDataDeleteAll', tableId: meta.value.id!, where: where?.value, viewId: viewMeta.value?.id, skipPks: Object.values(selectedAllRecordsSkipPks.value).join(','), }, {}, ) } catch (error) { } finally { clearCache(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, path) await syncCount(path) syncVisibleData?.() isBulkOperationInProgress.value = false } } return { cachedRows, loadData, insertRow, updateRowProperty, addEmptyRow, deleteRow, deleteRowById, deleteSelectedRows, deleteRangeOfRows, updateOrSaveRow, bulkUpdateRows, bulkUpsertRows, bulkUpdateView, bulkDeleteAll, loadAggCommentsCount, syncCount, removeRowIfNew, navigateToSiblingRow, getExpandedRowIndex, optimisedQuery, isLastRow, isFirstRow, clearCache, totalRows, actualTotalRows, selectedRows, syncVisibleData, chunkStates, clearInvalidRows, applySorting, isRowSortRequiredRows, isBulkOperationInProgress, updateRecordOrder, selectedAllRecords, selectedAllRecordsSkipPks, getRows, getDataCache, groupDataCache, // Groupby cachedGroups, totalGroups, toggleExpand, groupByColumns, isGroupBy, groupSyncCount, fetchMissingGroupChunks, clearGroupCache, toggleExpandAll, } }