mirror of https://github.com/nocodb/nocodb
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.
1072 lines
37 KiB
1072 lines
37 KiB
import { |
|
type Api, |
|
type ColumnType, |
|
type LinkToAnotherRecordType, |
|
type PaginatedType, |
|
type RelationTypes, |
|
type TableType, |
|
type ViewType, |
|
isVirtualCol, |
|
} from 'nocodb-sdk' |
|
import { UITypes, isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk' |
|
import type { ComputedRef, Ref } from 'vue' |
|
import type { CellRange, Row } from '#imports' |
|
|
|
export function useData(args: { |
|
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined> |
|
viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined> |
|
formattedData: Ref<Row[]> |
|
paginationData: Ref<PaginatedType> |
|
callbacks?: { |
|
changePage?: (page: number) => Promise<void> |
|
loadData?: ( |
|
param?: Parameters<Api<any>['dbViewRow']['list']>[4], |
|
shouldShowLoading?: boolean, |
|
updateLocalState?: boolean, |
|
) => Promise<void> | Promise<Row[]> |
|
globalCallback?: (...args: any[]) => Promise<void> |
|
syncCount?: () => Promise<void> |
|
syncPagination?: () => Promise<void> |
|
} |
|
}) { |
|
const { meta, viewMeta, formattedData, paginationData, callbacks } = args |
|
|
|
const { t } = useI18n() |
|
|
|
const { getMeta, metas } = useMetas() |
|
|
|
const { addUndo, clone, defineViewScope } = useUndoRedo() |
|
|
|
const { base } = storeToRefs(useBase()) |
|
|
|
const { $api } = useNuxtApp() |
|
|
|
const { isPaginationLoading } = storeToRefs(useViewsStore()) |
|
|
|
const reloadAggregate = inject(ReloadAggregateHookInj) |
|
|
|
const selectedAllRecords = computed({ |
|
get() { |
|
return !!formattedData.value.length && formattedData.value.every((row: Row) => row.rowMeta.selected) |
|
}, |
|
set(selected: boolean) { |
|
formattedData.value.forEach((row: Row) => (row.rowMeta.selected = selected)) |
|
}, |
|
}) |
|
|
|
function addEmptyRow(addAfter = formattedData.value.length, metaValue = meta.value) { |
|
formattedData.value.splice(addAfter, 0, { |
|
row: { ...rowDefaultData(metaValue?.columns) }, |
|
oldRow: {}, |
|
rowMeta: { new: true }, |
|
}) |
|
|
|
return formattedData.value[addAfter] |
|
} |
|
|
|
async function insertRow( |
|
currentRow: Row, |
|
ltarState: Record<string, any> = {}, |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
undo = false, |
|
) { |
|
const row = currentRow.row |
|
if (currentRow.rowMeta) currentRow.rowMeta.saving = true |
|
try { |
|
const { missingRequiredColumns, insertObj } = await populateInsertObject({ |
|
meta: metaValue!, |
|
ltarState, |
|
getMeta, |
|
row, |
|
undo, |
|
}) |
|
|
|
if (missingRequiredColumns.size) return |
|
|
|
const insertedData = await $api.dbViewRow.create( |
|
NOCO, |
|
base?.value.id as string, |
|
metaValue?.id as string, |
|
viewMetaValue?.id as string, |
|
{ ...insertObj, ...(ltarState || {}) }, |
|
) |
|
|
|
await reloadAggregate?.trigger() |
|
|
|
if (!undo) { |
|
Object.assign(currentRow, { |
|
row: { ...insertedData, ...row }, |
|
rowMeta: { ...(currentRow.rowMeta || {}), new: undefined }, |
|
oldRow: { ...insertedData }, |
|
}) |
|
|
|
const id = extractPkFromRow(insertedData, metaValue?.columns as ColumnType[]) |
|
const pkData = rowPkData(insertedData, metaValue?.columns as ColumnType[]) |
|
const rowIndex = findIndexByPk(pkData, formattedData.value) |
|
|
|
addUndo({ |
|
redo: { |
|
fn: async function redo( |
|
this: UndoRedoAction, |
|
row: Row, |
|
ltarState: Record<string, any>, |
|
pg: { page: number; pageSize: number }, |
|
) { |
|
row.row = { ...pkData, ...row.row } |
|
const insertedData = await insertRow(row, ltarState, undefined, true) |
|
|
|
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) { |
|
if (pg.page === paginationData.value.page) { |
|
formattedData.value.splice(rowIndex, 0, { |
|
row: { ...row, ...insertedData }, |
|
rowMeta: row.rowMeta, |
|
oldRow: row.oldRow, |
|
}) |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
}, |
|
args: [ |
|
clone(currentRow), |
|
clone(ltarState), |
|
{ page: paginationData.value.page, pageSize: paginationData.value.pageSize }, |
|
], |
|
}, |
|
undo: { |
|
fn: async function undo(this: UndoRedoAction, id: string) { |
|
await deleteRowById(id) |
|
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1) |
|
paginationData.value.totalRows = paginationData.value.totalRows! - 1 |
|
}, |
|
args: [id], |
|
}, |
|
scope: defineViewScope({ view: viewMeta.value }), |
|
}) |
|
} |
|
|
|
await callbacks?.syncCount?.() |
|
return insertedData |
|
} catch (error: any) { |
|
message.error(await extractSdkResponseErrorMsg(error)) |
|
} finally { |
|
if (currentRow.rowMeta) currentRow.rowMeta.saving = false |
|
await callbacks?.globalCallback?.() |
|
} |
|
} |
|
|
|
async function bulkUpsertRows( |
|
insertRows: Row[], |
|
updateRows: Row[], |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
undo = false, |
|
) { |
|
isPaginationLoading.value = true |
|
|
|
try { |
|
// Identify the rows that need to be fetched from the server |
|
const rowsToFetch = updateRows.filter((row) => row.rowMeta?.isExistingRow) |
|
const pagesToFetch = Array.from(new Set(rowsToFetch.map((row) => row.rowMeta.page))).filter( |
|
(page): page is number => page !== undefined && page !== paginationData.value.page, |
|
) |
|
const fetchedData: Record<number, Row[]> = {} |
|
|
|
for (const pageToFetch of pagesToFetch) { |
|
fetchedData[pageToFetch] = (await callbacks?.loadData?.( |
|
{ |
|
offset: (pageToFetch - 1) * paginationData.value.pageSize!, |
|
}, |
|
false, |
|
false, |
|
)) as Row[] |
|
} |
|
|
|
const getPk = (row: Row) => extractPkFromRow(row.row, metaValue?.columns as ColumnType[]) |
|
|
|
const ogUpdateRows = updateRows.map((row) => { |
|
if (row.rowMeta?.isExistingRow) { |
|
const fetchedRow = fetchedData[row.rowMeta.page!]?.[row.rowMeta.rowInPage!] |
|
return fetchedRow ? clone(fetchedRow) : row |
|
} |
|
const currentPageRow = formattedData.value.find((formattedRow) => getPk(formattedRow) === getPk(row)) |
|
return currentPageRow ? clone(currentPageRow) : 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) => { |
|
if (row.rowMeta?.page && row.rowMeta.page !== paginationData.value.page) { |
|
const fetchedRow = fetchedData[row.rowMeta.page]?.[row.rowMeta.rowInPage!] |
|
if (fetchedRow) { |
|
return { |
|
...fetchedRow, |
|
row: { ...fetchedRow.row, ...row.row }, |
|
oldRow: fetchedRow.row, |
|
} |
|
} |
|
} |
|
return row |
|
}) |
|
|
|
const rowsToUpsert = { |
|
insert: insertRows.map((row) => cleanRow(row.row)), |
|
update: updateRows.map((row) => cleanRow(row.row)), |
|
} |
|
|
|
const bulkUpsertedRows = await $api.dbTableRow.bulkUpsert( |
|
NOCO, |
|
base.value?.id as string, |
|
metaValue?.id as string, |
|
rowsToUpsert, |
|
{}, |
|
) |
|
|
|
const originalPks = new Set(formattedData.value.map((row) => extractPkFromRow(row.row, metaValue?.columns as ColumnType[]))) |
|
const [insertedRows, updatedRows] = bulkUpsertedRows.reduce( |
|
([inserted, updated], row) => { |
|
const isPkInOriginal = originalPks.has(extractPkFromRow(row, metaValue?.columns as ColumnType[])) |
|
return isPkInOriginal |
|
? [inserted, [...updated, { row, rowMeta: {}, oldRow: row }]] |
|
: [[...inserted, { row, rowMeta: {}, oldRow: {} }], updated] |
|
}, |
|
[[], []] as [Row[], Row[]], |
|
) |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: async (insertRows: Row[], updateRows: Row[], pg: { page: number; pageSize: number }) => { |
|
await bulkUpsertRows(insertRows, updateRows, { metaValue, viewMetaValue }, true) |
|
await (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize |
|
? callbacks?.loadData?.() |
|
: callbacks?.changePage?.(pg.page)) |
|
}, |
|
args: [ |
|
clone(insertedRows), |
|
clone(updatedRows), |
|
{ page: paginationData.value.page, pageSize: paginationData.value.pageSize }, |
|
], |
|
}, |
|
undo: { |
|
fn: async (insertedRows: Row[], ogUpdateRows: Row[], pg: { page: number; pageSize: number }) => { |
|
await bulkDeleteRows( |
|
insertedRows.map((row) => rowPkData(row.row, metaValue?.columns as ColumnType[]) as Record<string, any>), |
|
) |
|
await $api.dbTableRow.bulkUpdate( |
|
NOCO, |
|
metaValue?.base_id as string, |
|
metaValue?.id as string, |
|
ogUpdateRows.map((row) => cleanRow(row.oldRow)), |
|
) |
|
await (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize |
|
? callbacks?.loadData?.() |
|
: callbacks?.changePage?.(pg.page)) |
|
}, |
|
args: [ |
|
clone(insertedRows), |
|
clone(ogUpdateRows), |
|
{ page: paginationData.value.page, pageSize: paginationData.value.pageSize }, |
|
], |
|
}, |
|
}) |
|
} |
|
|
|
await callbacks?.loadData?.() |
|
} catch (error: any) { |
|
message.error(await extractSdkResponseErrorMsg(error)) |
|
} finally { |
|
await callbacks?.globalCallback?.() |
|
isPaginationLoading.value = false |
|
} |
|
} |
|
async function bulkInsertRows( |
|
rows: Row[], |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
undo = false, |
|
) { |
|
isPaginationLoading.value = true |
|
|
|
const autoGeneratedKeys = clone(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) { |
|
autoGeneratedKeys.forEach((key) => delete insertObj[key!]) |
|
return insertObj |
|
} |
|
}), |
|
) |
|
)?.filter(Boolean) ?? [] // Filter out undefined values (if any) |
|
|
|
const bulkInsertedIds = await $api.dbDataTableRow.create(metaValue?.id as string, rowsToInsert, { |
|
viewId: viewMetaValue?.id as string, |
|
}) |
|
|
|
await callbacks?.syncCount?.() |
|
return bulkInsertedIds |
|
} catch (error: any) { |
|
message.error(await extractSdkResponseErrorMsg(error)) |
|
} finally { |
|
await callbacks?.globalCallback?.() |
|
isPaginationLoading.value = false |
|
} |
|
} |
|
|
|
// inside this method use metaValue and viewMetaValue to refer meta |
|
// since sometimes we need to pass old metas |
|
async function updateRowProperty( |
|
toUpdate: Row, |
|
property: string, |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
undo = false, |
|
) { |
|
if (toUpdate.rowMeta) toUpdate.rowMeta.saving = true |
|
|
|
try { |
|
const id = extractPkFromRow(toUpdate.row, metaValue?.columns as ColumnType[]) |
|
|
|
const updatedRowData: Record<string, any> = await $api.dbViewRow.update( |
|
NOCO, |
|
base?.value.id as string, |
|
metaValue?.id as string, |
|
viewMetaValue?.id as string, |
|
encodeURIComponent(id), |
|
{ |
|
// if value is undefined treat it as null |
|
[property]: toUpdate.row[property] ?? null, |
|
}, |
|
// todo: |
|
// { |
|
// query: { ignoreWebhook: !saved } |
|
// } |
|
) |
|
await reloadAggregate?.trigger({ fields: [{ title: property }] }) |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: async function redo(toUpdate: Row, property: string, pg: { page: number; pageSize: number }) { |
|
const updatedData = await updateRowProperty(toUpdate, property, undefined, true) |
|
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { |
|
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value) |
|
if (rowIndex !== -1) { |
|
const row = formattedData.value[rowIndex] |
|
Object.assign(row.row, updatedData) |
|
Object.assign(row.oldRow, updatedData) |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
}, |
|
args: [clone(toUpdate), property, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
undo: { |
|
fn: async function undo(toUpdate: Row, property: string, pg: { page: number; pageSize: number }) { |
|
const updatedData = await updateRowProperty( |
|
{ row: toUpdate.oldRow, oldRow: toUpdate.row, rowMeta: toUpdate.rowMeta }, |
|
property, |
|
undefined, |
|
true, |
|
) |
|
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { |
|
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value) |
|
if (rowIndex !== -1) { |
|
const row = formattedData.value[rowIndex] |
|
Object.assign(row.row, updatedData) |
|
Object.assign(row.oldRow, updatedData) |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
}, |
|
args: [clone(toUpdate), property, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
scope: defineViewScope({ view: viewMeta.value }), |
|
}) |
|
|
|
/** update row data(to sync formula and other related columns) |
|
* update only formula, rollup and auto updated datetime columns data to avoid overwriting any changes made by user |
|
* update attachment as well since id is required for further operations |
|
*/ |
|
Object.assign( |
|
toUpdate.row, |
|
metaValue!.columns!.reduce<Record<string, any>>((acc: Record<string, any>, col: ColumnType) => { |
|
if ( |
|
col.title in updatedRowData && |
|
(col.uidt === UITypes.Formula || |
|
col.uidt === UITypes.QrCode || |
|
col.uidt === UITypes.Barcode || |
|
col.uidt === UITypes.Rollup || |
|
col.uidt === UITypes.Checkbox || |
|
col.uidt === UITypes.User || |
|
col.uidt === UITypes.LastModifiedTime || |
|
col.uidt === UITypes.LastModifiedBy || |
|
col.uidt === UITypes.Lookup || |
|
col.uidt === UITypes.Button || |
|
col.uidt === UITypes.Attachment || |
|
col.au || |
|
(isValidValue(col?.cdf) && / on update /i.test(col.cdf))) |
|
) |
|
acc[col.title!] = updatedRowData[col.title!] |
|
return acc |
|
}, {} as Record<string, any>), |
|
) |
|
Object.assign(toUpdate.oldRow, updatedRowData) |
|
} |
|
|
|
await callbacks?.globalCallback?.() |
|
|
|
return updatedRowData |
|
} catch (e: any) { |
|
toUpdate.row[property] = toUpdate.oldRow[property] |
|
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} finally { |
|
if (toUpdate.rowMeta) toUpdate.rowMeta.saving = false |
|
} |
|
} |
|
|
|
async function updateOrSaveRow( |
|
row: Row, |
|
property?: string, |
|
ltarState?: Record<string, any>, |
|
args: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
) { |
|
// update changed status |
|
if (row.rowMeta) row.rowMeta.changed = false |
|
|
|
// if new row and save is in progress then wait until the save is complete |
|
await until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v) |
|
|
|
if (row.rowMeta.new) { |
|
return await insertRow(row, ltarState, args) |
|
} else { |
|
// if the field name is missing skip update |
|
if (property) { |
|
await updateRowProperty(row, property, args) |
|
} |
|
} |
|
} |
|
|
|
async function bulkUpdateRows( |
|
rows: Row[], |
|
props: string[], |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
undo = false, |
|
) { |
|
const promises = [] |
|
|
|
for (const row of rows) { |
|
// update changed status |
|
if (row.rowMeta) row.rowMeta.changed = false |
|
|
|
// if new row and save is in progress then wait until the save is complete |
|
promises.push(until(() => !(row.rowMeta?.new && row.rowMeta?.saving)).toMatch((v) => v)) |
|
} |
|
|
|
await Promise.all(promises) |
|
|
|
const updateArray = [] |
|
|
|
for (const row of rows) { |
|
if (row.rowMeta) row.rowMeta.saving = true |
|
|
|
const pk = rowPkData(row.row, metaValue?.columns as ColumnType[]) |
|
|
|
const updateData = props.reduce((acc: Record<string, any>, prop) => { |
|
acc[prop] = row.row[prop] |
|
return acc |
|
}, {} as Record<string, any>) |
|
|
|
updateArray.push({ ...updateData, ...pk }) |
|
} |
|
|
|
await $api.dbTableRow.bulkUpdate(NOCO, metaValue?.base_id as string, metaValue?.id as string, updateArray) |
|
await reloadAggregate?.trigger({ fields: props.map((p) => ({ title: p })) }) |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: async function redo(redoRows: Row[], props: string[], pg: { page: number; pageSize: number }) { |
|
await bulkUpdateRows(redoRows, props, { metaValue, viewMetaValue }, true) |
|
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { |
|
for (const toUpdate of redoRows) { |
|
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value) |
|
if (rowIndex !== -1) { |
|
const row = formattedData.value[rowIndex] |
|
Object.assign(row.row, toUpdate.row) |
|
Object.assign(row.oldRow, toUpdate.row) |
|
} else { |
|
await callbacks?.loadData?.() |
|
break |
|
} |
|
} |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
}, |
|
args: [clone(rows), clone(props), { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
undo: { |
|
fn: async function undo(undoRows: Row[], props: string[], pg: { page: number; pageSize: number }) { |
|
await bulkUpdateRows(undoRows, props, { metaValue, viewMetaValue }, true) |
|
if (pg.page === paginationData.value.page && pg.pageSize === paginationData.value.pageSize) { |
|
for (const toUpdate of undoRows) { |
|
const rowIndex = findIndexByPk(rowPkData(toUpdate.row, meta?.value?.columns as ColumnType[]), formattedData.value) |
|
if (rowIndex !== -1) { |
|
const row = formattedData.value[rowIndex] |
|
Object.assign(row.row, toUpdate.row) |
|
Object.assign(row.oldRow, toUpdate.row) |
|
} else { |
|
await callbacks?.loadData?.() |
|
break |
|
} |
|
} |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
}, |
|
args: [ |
|
clone( |
|
rows.map((row) => { |
|
return { row: row.oldRow, oldRow: row.row, rowMeta: row.rowMeta } |
|
}), |
|
), |
|
props, |
|
{ page: paginationData.value.page, pageSize: paginationData.value.pageSize }, |
|
], |
|
}, |
|
scope: defineViewScope({ view: viewMetaValue }), |
|
}) |
|
} |
|
|
|
for (const row of rows) { |
|
if (row.rowMeta) row.rowMeta.saving = false |
|
} |
|
|
|
// reload data since row update may change the other columns data( Formula, QrCode, Barcode, Rollup, Checkbox, User, Auto Updated Datetime, etc...) |
|
if ( |
|
metaValue!.columns!.some((col) => |
|
[ |
|
UITypes.QrCode, |
|
UITypes.LastModifiedTime, |
|
UITypes.Barcode, |
|
UITypes.Formula, |
|
UITypes.Lookup, |
|
UITypes.Rollup, |
|
UITypes.LinkToAnotherRecord, |
|
UITypes.LastModifiedBy, |
|
UITypes.Button, |
|
].includes(col.uidt), |
|
) |
|
) { |
|
await callbacks?.loadData?.() |
|
} |
|
await callbacks?.globalCallback?.() |
|
} |
|
|
|
async function bulkUpdateView( |
|
data: Record<string, any>[], |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
) { |
|
if (!viewMetaValue) return |
|
|
|
await $api.dbTableRow.bulkUpdateAll(NOCO, metaValue?.base_id as string, metaValue?.id as string, data, { |
|
viewId: viewMetaValue.id, |
|
}) |
|
|
|
await reloadAggregate?.trigger() |
|
|
|
await callbacks?.loadData?.() |
|
await callbacks?.globalCallback?.() |
|
} |
|
|
|
const linkRecord = async ( |
|
rowId: string, |
|
relatedRowId: string, |
|
column: ColumnType, |
|
type: RelationTypes, |
|
{ metaValue = meta.value }: { metaValue?: TableType } = {}, |
|
) => { |
|
try { |
|
await $api.dbTableRow.nestedAdd( |
|
NOCO, |
|
base.value.id as string, |
|
metaValue?.id as string, |
|
encodeURIComponent(rowId), |
|
type as RelationTypes, |
|
column.title as string, |
|
encodeURIComponent(relatedRowId), |
|
) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
} |
|
|
|
// Recover LTAR relations for a row using the row data |
|
const recoverLTARRefs = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}) => { |
|
const id = extractPkFromRow(row, metaValue?.columns as ColumnType[]) |
|
for (const column of metaValue?.columns ?? []) { |
|
if (column.uidt !== UITypes.LinkToAnotherRecord) continue |
|
|
|
const colOptions = column.colOptions as LinkToAnotherRecordType |
|
|
|
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string] |
|
|
|
if (isHm(column) || isMm(column)) { |
|
const relatedRows = (row[column.title!] ?? []) as Record<string, any>[] |
|
|
|
for (const relatedRow of relatedRows) { |
|
await linkRecord( |
|
id, |
|
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]), |
|
column, |
|
colOptions.type as RelationTypes, |
|
{ metaValue }, |
|
) |
|
} |
|
} else if (isBt(column) && row[column.title!]) { |
|
await linkRecord( |
|
id, |
|
extractPkFromRow(row[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]), |
|
column, |
|
colOptions.type as RelationTypes, |
|
{ metaValue }, |
|
) |
|
} |
|
} |
|
} |
|
|
|
async function deleteRowById( |
|
id: string, |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
) { |
|
if (!id) { |
|
throw new Error("Delete not allowed for table which doesn't have primary Key") |
|
} |
|
|
|
const res: any = await $api.dbViewRow.delete( |
|
'noco', |
|
base.value.id as string, |
|
metaValue?.id as string, |
|
viewMetaValue?.id as string, |
|
encodeURIComponent(id), |
|
) |
|
|
|
await reloadAggregate?.trigger() |
|
|
|
if (res.message) { |
|
message.info( |
|
`Record delete failed: ${`Unable to delete record with ID ${id} because of the following: |
|
\n${res.message.join('\n')}.\n |
|
Clear the data first & try again`})}`, |
|
) |
|
return false |
|
} |
|
return true |
|
} |
|
|
|
async function deleteRow(rowIndex: number, undo?: boolean) { |
|
try { |
|
const row = formattedData.value[rowIndex] |
|
if (!row.rowMeta.new) { |
|
const id = meta?.value?.columns |
|
?.filter((c) => c.pk) |
|
.map((c) => row.row[c.title!]) |
|
.join('___') |
|
|
|
const fullRecord = await $api.dbTableRow.read( |
|
NOCO, |
|
// todo: base_id missing on view type |
|
base?.value.id as string, |
|
meta.value?.id as string, |
|
encodeURIComponent(id as string), |
|
{ |
|
getHiddenColumn: true, |
|
}, |
|
) |
|
|
|
row.row = fullRecord |
|
|
|
const deleted = await deleteRowById(id as string) |
|
if (!deleted) { |
|
return |
|
} |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: async function redo(this: UndoRedoAction, id: string) { |
|
await deleteRowById(id) |
|
const pk: Record<string, string> = rowPkData(row.row, meta?.value?.columns as ColumnType[]) |
|
const rowIndex = findIndexByPk(pk, formattedData.value) |
|
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1) |
|
paginationData.value.totalRows = paginationData.value.totalRows! - 1 |
|
}, |
|
args: [id], |
|
}, |
|
undo: { |
|
fn: async function undo( |
|
this: UndoRedoAction, |
|
row: Row, |
|
ltarState: Record<string, any>, |
|
pg: { page: number; pageSize: number }, |
|
) { |
|
const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[]) |
|
|
|
row.row = { ...pkData, ...row.row } |
|
await insertRow(row, ltarState, {}, true) |
|
recoverLTARRefs(row.row) |
|
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) { |
|
if (pg.page === paginationData.value.page) { |
|
formattedData.value.splice(rowIndex, 0, row) |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
}, |
|
args: [clone(row), {}, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
scope: defineViewScope({ view: viewMeta.value }), |
|
}) |
|
} |
|
} |
|
|
|
formattedData.value.splice(rowIndex, 1) |
|
|
|
await callbacks?.syncCount?.() |
|
} catch (e: any) { |
|
message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} |
|
|
|
await callbacks?.globalCallback?.() |
|
} |
|
|
|
async function deleteSelectedRows() { |
|
let row = formattedData.value.length |
|
const removedRowsData: Record<string, any>[] = [] |
|
let compositePrimaryKey = '' |
|
|
|
while (row--) { |
|
const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any> |
|
if (!rowMeta.selected) { |
|
continue |
|
} |
|
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 |
|
|
|
removedRowsData.push({ |
|
[compositePrimaryKey]: compositePkValue as string, |
|
pkData, |
|
row: clone(formattedData.value[row]) as Row, |
|
rowIndex: row as number, |
|
}) |
|
} |
|
} |
|
} |
|
|
|
if (!removedRowsData.length) return |
|
|
|
isPaginationLoading.value = true |
|
|
|
const { list } = await $api.dbTableRow.list(NOCO, base?.value.id as string, meta.value?.id as string, { |
|
pks: removedRowsData.map((row) => row[compositePrimaryKey]).join(','), |
|
}) |
|
|
|
try { |
|
for (const removedRow of removedRowsData) { |
|
const rowObj = removedRow.row |
|
const rowPk = rowPkData(rowObj.row, meta?.value?.columns as ColumnType[]) |
|
const fullRecord = list.find((r: Record<string, any>) => { |
|
return Object.keys(rowPk).every((key) => rowPk[key] === r[key]) |
|
}) |
|
rowObj.row = clone(fullRecord) |
|
} |
|
|
|
await bulkDeleteRows(removedRowsData.map((row) => row.pkData)) |
|
} catch (e: any) { |
|
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} |
|
|
|
if (!removedRowsData.length) { |
|
isPaginationLoading.value = false |
|
return |
|
} |
|
|
|
addUndo({ |
|
redo: { |
|
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) { |
|
isPaginationLoading.value = true |
|
|
|
const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData)) |
|
|
|
if (Array.isArray(removedRowIds)) { |
|
for (const { row } of removedRowsData) { |
|
const primaryKey: Record<string, string> = rowPkData(row.row, meta?.value?.columns as ColumnType[]) |
|
const rowIndex = findIndexByPk(primaryKey, formattedData.value) |
|
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1) |
|
paginationData.value.totalRows = paginationData.value.totalRows! - 1 |
|
} |
|
} |
|
|
|
await callbacks?.syncCount?.() |
|
await callbacks?.syncPagination?.() |
|
await callbacks?.globalCallback?.() |
|
}, |
|
args: [removedRowsData], |
|
}, |
|
undo: { |
|
fn: async function undo( |
|
this: UndoRedoAction, |
|
removedRowsData: Record<string, any>[], |
|
pg: { page: number; pageSize: number }, |
|
) { |
|
const rowsToInsert = removedRowsData |
|
.map((row) => { |
|
const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[]) |
|
row.row = { ...pkData, ...row.row } |
|
return row |
|
}) |
|
.reverse() |
|
|
|
const insertedRowIds = await bulkInsertRows( |
|
rowsToInsert.map((row) => row.row), |
|
undefined, |
|
true, |
|
) |
|
|
|
if (Array.isArray(insertedRowIds)) { |
|
for (const { row, rowIndex } of rowsToInsert) { |
|
recoverLTARRefs(row.row) |
|
|
|
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) { |
|
if (pg.page === paginationData.value.page) { |
|
formattedData.value.splice(rowIndex, 0, row) |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
} |
|
} |
|
}, |
|
args: [removedRowsData, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
scope: defineViewScope({ view: viewMeta.value }), |
|
}) |
|
|
|
await callbacks?.syncCount?.() |
|
await callbacks?.syncPagination?.() |
|
await callbacks?.globalCallback?.() |
|
} |
|
|
|
async function deleteRangeOfRows(cellRange: CellRange) { |
|
if (!cellRange._start || !cellRange._end) return |
|
const start = Math.max(cellRange._start.row, cellRange._end.row) |
|
const end = Math.min(cellRange._start.row, cellRange._end.row) |
|
|
|
// plus one because we want to include the end row |
|
let row = start + 1 |
|
|
|
const removedRowsData: Record<string, any>[] = [] |
|
let compositePrimaryKey = '' |
|
|
|
while (row--) { |
|
try { |
|
const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any> |
|
|
|
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 |
|
|
|
removedRowsData.push({ |
|
[compositePrimaryKey]: compositePkValue as string, |
|
pkData, |
|
row: clone(formattedData.value[row]) as Row, |
|
rowIndex: row as number, |
|
}) |
|
} |
|
} |
|
} catch (e: any) { |
|
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} |
|
|
|
if (row === end) break |
|
} |
|
|
|
if (!removedRowsData.length) return |
|
|
|
isPaginationLoading.value = true |
|
|
|
const { list } = await $api.dbTableRow.list(NOCO, base?.value.id as string, meta.value?.id as string, { |
|
pks: removedRowsData.map((row) => row[compositePrimaryKey]).join(','), |
|
}) |
|
|
|
try { |
|
for (const removedRow of removedRowsData) { |
|
const rowObj = removedRow.row |
|
const rowPk = rowPkData(rowObj.row, meta?.value?.columns as ColumnType[]) |
|
const fullRecord = list.find((r: Record<string, any>) => { |
|
return Object.keys(rowPk).every((key) => rowPk[key] === r[key]) |
|
}) |
|
rowObj.row = clone(fullRecord) |
|
} |
|
|
|
await bulkDeleteRows(removedRowsData.map((row) => row.pkData)) |
|
} catch (e: any) { |
|
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} |
|
|
|
if (!removedRowsData.length) { |
|
isPaginationLoading.value = false |
|
return |
|
} |
|
|
|
addUndo({ |
|
redo: { |
|
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) { |
|
isPaginationLoading.value = true |
|
|
|
const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData)) |
|
|
|
if (Array.isArray(removedRowIds)) { |
|
for (const { row } of removedRowsData) { |
|
const primaryKey: Record<string, string> = rowPkData(row.row, meta?.value?.columns as ColumnType[]) |
|
const rowIndex = findIndexByPk(primaryKey, formattedData.value) |
|
if (rowIndex !== -1) formattedData.value.splice(rowIndex, 1) |
|
paginationData.value.totalRows = paginationData.value.totalRows! - 1 |
|
} |
|
} |
|
|
|
await callbacks?.syncCount?.() |
|
await callbacks?.syncPagination?.() |
|
await callbacks?.globalCallback?.() |
|
}, |
|
args: [removedRowsData], |
|
}, |
|
undo: { |
|
fn: async function undo( |
|
this: UndoRedoAction, |
|
removedRowsData: Record<string, any>[], |
|
pg: { page: number; pageSize: number }, |
|
) { |
|
const rowsToInsert = removedRowsData |
|
.map((row) => { |
|
const pkData = rowPkData(row.row, meta.value?.columns as ColumnType[]) |
|
row.row = { ...pkData, ...row.row } |
|
return row |
|
}) |
|
.reverse() |
|
|
|
const insertedRowIds = await bulkInsertRows( |
|
rowsToInsert.map((row) => row.row), |
|
undefined, |
|
true, |
|
) |
|
|
|
if (Array.isArray(insertedRowIds)) { |
|
for (const { row, rowIndex } of rowsToInsert) { |
|
recoverLTARRefs(row.row) |
|
|
|
if (rowIndex !== -1 && pg.pageSize === paginationData.value.pageSize) { |
|
if (pg.page === paginationData.value.page) { |
|
formattedData.value.splice(rowIndex, 0, row) |
|
} else { |
|
await callbacks?.changePage?.(pg.page) |
|
} |
|
} else { |
|
await callbacks?.loadData?.() |
|
} |
|
} |
|
} |
|
}, |
|
args: [removedRowsData, { page: paginationData.value.page, pageSize: paginationData.value.pageSize }], |
|
}, |
|
scope: defineViewScope({ view: viewMeta.value }), |
|
}) |
|
|
|
await callbacks?.syncCount?.() |
|
await callbacks?.syncPagination?.() |
|
await callbacks?.globalCallback?.() |
|
} |
|
|
|
async function bulkDeleteRows( |
|
rows: Record<string, string>[], |
|
{ metaValue = meta.value, viewMetaValue = viewMeta.value }: { metaValue?: TableType; viewMetaValue?: ViewType } = {}, |
|
) { |
|
try { |
|
const bulkDeletedRowsData = await $api.dbDataTableRow.delete(metaValue?.id as string, rows.length === 1 ? rows[0] : rows, { |
|
viewId: viewMetaValue?.id as string, |
|
}) |
|
await reloadAggregate?.trigger() |
|
|
|
return rows.length === 1 && bulkDeletedRowsData ? [bulkDeletedRowsData] : bulkDeletedRowsData |
|
} catch (error: any) { |
|
message.error(await extractSdkResponseErrorMsg(error)) |
|
} |
|
} |
|
|
|
const removeRowIfNew = (row: Row) => { |
|
const index = formattedData.value.indexOf(row) |
|
|
|
if (index > -1 && row.rowMeta.new) { |
|
formattedData.value.splice(index, 1) |
|
return true |
|
} |
|
return false |
|
} |
|
|
|
return { |
|
insertRow, |
|
updateRowProperty, |
|
addEmptyRow, |
|
deleteRow, |
|
deleteRowById, |
|
deleteSelectedRows, |
|
deleteRangeOfRows, |
|
updateOrSaveRow, |
|
bulkUpdateRows, |
|
bulkUpdateView, |
|
selectedAllRecords, |
|
removeRowIfNew, |
|
bulkDeleteRows, |
|
bulkInsertRows, |
|
bulkUpsertRows, |
|
} |
|
}
|
|
|