mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 03:35:37 +00:00
1053 lines
32 KiB
TypeScript
1053 lines
32 KiB
TypeScript
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<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 { 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<number>) => where?.value ?? '',
|
|
getWhereFilterArr: getGroupFilterArr,
|
|
reloadAggregate: triggerAggregateReload,
|
|
findGroupByPath: (path?: Array<number>) => {
|
|
return findGroupByPath(cachedGroups.value, path)
|
|
},
|
|
},
|
|
groupByColumns,
|
|
where,
|
|
isPublic,
|
|
})
|
|
|
|
function triggerAggregateReload(params: {
|
|
fields?: Array<{ title: string; aggregation?: string | undefined }>
|
|
path: Array<number>
|
|
}) {
|
|
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<string, any> = {}) => {
|
|
const { path, fields } = v
|
|
if (!path?.length && isGroupBy.value) {
|
|
const allGroups: CanvasGroup[] = []
|
|
|
|
function collectAllGroups(groups: Map<number, CanvasGroup>) {
|
|
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<number>) {
|
|
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<number> = [], _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<number> = []): Promise<void> {
|
|
let removedRowsData: Record<string, any>[] = []
|
|
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<string, any>) => {
|
|
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<string, any>[], path: Array<number>) => {
|
|
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<string, any>[], path: Array<number>) => {
|
|
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<number> = [],
|
|
): Promise<string[]> {
|
|
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<string, any>; 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<number, Row>()
|
|
|
|
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<number>()
|
|
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<number> = [],
|
|
): Promise<void> {
|
|
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<string, any>
|
|
|
|
triggerAggregateReload({ fields: props.map((p) => ({ title: p })), path })
|
|
|
|
newRows.forEach((newRow: Record<string, any>) => {
|
|
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,
|
|
}
|
|
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<number>) => {
|
|
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<number>) => {
|
|
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<ColumnType>[],
|
|
undo = false,
|
|
path: Array<number> = [],
|
|
) {
|
|
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<number>) => {
|
|
try {
|
|
isBulkOperationInProgress.value = true
|
|
|
|
await bulkDeleteRows(
|
|
insertedRows.map((row) => rowPkData(row.row, metaValue?.columns as ColumnType[]) as Record<string, any>),
|
|
)
|
|
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<number>) => {
|
|
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<string, any>[],
|
|
nested = true,
|
|
path: Array<number> = [],
|
|
): Promise<void> {
|
|
const dataCache = getDataCache(path)
|
|
const maxCachedIndex = Math.max(...dataCache.cachedRows.value.keys())
|
|
const newCachedRows = new Map<number, Row>()
|
|
|
|
const deleteSet = new Set(rowsToDelete.map((row) => (nested ? row.row : row).rowMeta.rowIndex))
|
|
const affectedChunks = new Set<number>()
|
|
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<number, number>()
|
|
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<number>()
|
|
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<number> = []): Promise<void> {
|
|
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<string, any>[] = []
|
|
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<string, any>) => {
|
|
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<string, any>[], path: Array<number>) => {
|
|
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<string, any>[], path: Array<number>) => {
|
|
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<string, string>[],
|
|
{
|
|
metaValue = meta.value,
|
|
viewMetaValue = viewMeta.value,
|
|
}: {
|
|
metaValue?: TableType
|
|
viewMetaValue?: ViewType
|
|
} = {},
|
|
): Promise<any> {
|
|
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<number> = []) {
|
|
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,
|
|
}
|
|
}
|