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.
609 lines
21 KiB
609 lines
21 KiB
import type { |
|
type ColumnType, |
|
type LinkToAnotherRecordType, |
|
type PaginatedType, |
|
type RequestParams, |
|
type TableType, |
|
} from 'nocodb-sdk' |
|
import { RelationTypes, UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk' |
|
import type { ComputedRef, Ref } from 'vue' |
|
|
|
interface DataApiResponse { |
|
list: Record<string, any> |
|
pageInfo: PaginatedType |
|
} |
|
|
|
/** Store for managing Link to another cells */ |
|
const [useProvideLTARStore, useLTARStore] = useInjectionState( |
|
( |
|
column: Ref<Required<ColumnType>>, |
|
row: Ref<Row>, |
|
isNewRow: ComputedRef<boolean> | Ref<boolean>, |
|
_reloadData = (_params: { shouldShowLoading?: boolean }) => {}, |
|
) => { |
|
// state |
|
const { metas, getMeta } = useMetas() |
|
|
|
const { base } = storeToRefs(useBase()) |
|
|
|
const { $api, $e } = useNuxtApp() |
|
|
|
const activeView = inject(ActiveViewInj, ref()) |
|
const isForm = inject(IsFormInj, ref(false)) |
|
|
|
const { addUndo, clone, defineViewScope } = useUndoRedo() |
|
|
|
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null)) |
|
|
|
const childrenExcludedList = ref<DataApiResponse | undefined>() |
|
const childrenList = ref<DataApiResponse | undefined>() |
|
|
|
const childrenExcludedListPagination = reactive({ |
|
page: 1, |
|
query: '', |
|
size: 10, |
|
}) |
|
|
|
const childrenExcludedOffsetCount = ref(0) |
|
|
|
const childrenListPagination = reactive({ |
|
page: 1, |
|
query: '', |
|
size: 10, |
|
}) |
|
|
|
const childrenListOffsetCount = ref(0) |
|
|
|
const isChildrenLoading = ref(false) |
|
|
|
const isChildrenListLoading = ref<Array<boolean>>([]) |
|
|
|
const isChildrenListLinked = ref<Array<boolean>>([]) |
|
|
|
const isChildrenExcludedListLoading = ref<Array<boolean>>([]) |
|
|
|
const isChildrenExcludedLoading = ref(false) |
|
|
|
const isChildrenExcludedListLinked = ref<Array<boolean>>([]) |
|
|
|
const newRowState = reactive({ |
|
state: null, |
|
}) |
|
|
|
const childrenListCount = ref(0) |
|
|
|
const { t } = useI18n() |
|
|
|
const isPublic: Ref<boolean> = inject(IsPublicInj, ref(false)) |
|
|
|
const colOptions = computed(() => column.value?.colOptions as LinkToAnotherRecordType) |
|
|
|
const { sharedView } = useSharedView() |
|
|
|
const baseId = base.value?.id || (sharedView.value?.view as any)?.base_id |
|
|
|
// getters |
|
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string]) |
|
const relatedTableMeta = computed<TableType>(() => { |
|
return metas.value?.[colOptions.value?.fk_related_model_id as string] |
|
}) |
|
|
|
const rowId = computed(() => extractPkFromRow(row.value.row, meta.value.columns)) |
|
|
|
const getRelatedTableRowId = (row: Record<string, any>) => { |
|
return extractPkFromRow(row, relatedTableMeta.value?.columns) |
|
} |
|
|
|
// actions |
|
|
|
const loadRelatedTableMeta = async () => { |
|
await getMeta(colOptions.value.fk_related_model_id as string) |
|
} |
|
|
|
const relatedTableDisplayValueProp = computed(() => { |
|
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title || '' |
|
}) |
|
|
|
// todo: temp fix, handle in backend |
|
const relatedTableDisplayValuePropId = computed(() => { |
|
return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.id || '' |
|
}) |
|
|
|
const relatedTablePrimaryKeyProps = computed(() => { |
|
return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? [] |
|
}) |
|
|
|
const displayValueProp = computed(() => { |
|
return (meta.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title |
|
}) |
|
|
|
const displayValueTypeAndFormatProp = computed(() => { |
|
let displayValueTypeAndFormat = { |
|
type: '', |
|
format: '', |
|
} |
|
const currentColumn = relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0] |
|
|
|
if (currentColumn) { |
|
if (currentColumn?.uidt === UITypes.DateTime) { |
|
displayValueTypeAndFormat = { |
|
type: currentColumn?.uidt, |
|
format: `${parseProp(currentColumn?.meta)?.date_format ?? dateFormats[0]} ${ |
|
parseProp(currentColumn?.meta)?.time_format ?? timeFormats[0] |
|
}`, |
|
} |
|
} |
|
if (currentColumn?.uidt === UITypes.Time) { |
|
displayValueTypeAndFormat = { |
|
type: currentColumn?.uidt, |
|
format: `${timeFormats[0]}`, |
|
} |
|
} |
|
} |
|
return displayValueTypeAndFormat |
|
}) |
|
|
|
const headerDisplayValue = computed(() => { |
|
if ( |
|
row.value.row[displayValueProp.value] && |
|
displayValueTypeAndFormatProp.value.type && |
|
displayValueTypeAndFormatProp.value.format |
|
) { |
|
return parseStringDateTime( |
|
row.value.row[displayValueProp.value], |
|
displayValueTypeAndFormatProp.value.format, |
|
!(displayValueTypeAndFormatProp.value.format === UITypes.Time), |
|
) |
|
} |
|
return row.value.row[displayValueProp.value] |
|
}) |
|
|
|
const loadChildrenExcludedList = async (activeState?: any, resetOffset: boolean = false) => { |
|
if (activeState) newRowState.state = activeState |
|
try { |
|
let offset = |
|
childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value |
|
|
|
if (offset < 0 || resetOffset) { |
|
offset = 0 |
|
childrenExcludedOffsetCount.value = 0 |
|
childrenExcludedListPagination.page = 1 |
|
} |
|
isChildrenExcludedLoading.value = true |
|
if (isPublic.value) { |
|
const router = useRouter() |
|
|
|
const route = router.currentRoute |
|
|
|
let row |
|
// if shared form extract the current form state |
|
if (isForm.value) { |
|
const { formState } = useSharedFormStoreOrThrow() |
|
row = formState?.value |
|
} |
|
|
|
childrenExcludedList.value = await $api.public.dataRelationList( |
|
route.value.params.viewId as string, |
|
column.value.id, |
|
{}, |
|
{ |
|
headers: { |
|
'xc-password': sharedViewPassword.value, |
|
}, |
|
query: { |
|
limit: childrenExcludedListPagination.size, |
|
offset, |
|
where: |
|
childrenExcludedListPagination.query && |
|
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`, |
|
fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value], |
|
|
|
// todo: include only required fields |
|
rowData: JSON.stringify(row), |
|
} as RequestParams, |
|
}, |
|
) |
|
|
|
/** if new row load all records */ |
|
} else if (isNewRow?.value) { |
|
childrenExcludedList.value = await $api.dbTableRow.list( |
|
NOCO, |
|
baseId, |
|
relatedTableMeta?.value?.id as string, |
|
{ |
|
limit: childrenExcludedListPagination.size, |
|
offset, |
|
where: |
|
childrenExcludedListPagination.query && |
|
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`, |
|
// fields: [relatedTableDisplayValueProp.value, ...relatedTablePrimaryKeyProps.value], |
|
|
|
// todo: include only required fields |
|
linkColumnId: column.value.fk_column_id || column.value.id, |
|
linkRowData: JSON.stringify(row.value.row), |
|
} as any, |
|
) |
|
} else { |
|
// extract changed data and include with the api call if any |
|
let changedRowData |
|
try { |
|
if (row.value?.row) { |
|
changedRowData = Object.keys(row.value?.row).reduce((acc: Record<string, any>, key: string) => { |
|
if (row.value.row[key] !== row.value.oldRow[key]) acc[key] = row.value.row[key] |
|
return acc |
|
}, {}) |
|
} |
|
} catch {} |
|
|
|
childrenExcludedList.value = await $api.dbTableRow.nestedChildrenExcludedList( |
|
NOCO, |
|
baseId, |
|
meta.value.id, |
|
encodeURIComponent(rowId.value), |
|
colOptions.value.type as RelationTypes, |
|
column?.value?.id, |
|
{ |
|
limit: String(childrenExcludedListPagination.size), |
|
offset: String(offset), |
|
// todo: where clause is missing from type |
|
where: |
|
childrenExcludedListPagination.query && |
|
`(${relatedTableDisplayValueProp.value},like,${childrenExcludedListPagination.query})`, |
|
linkRowData: changedRowData ? JSON.stringify(changedRowData) : undefined, |
|
} as any, |
|
) |
|
} |
|
|
|
childrenExcludedList.value?.list.forEach((row: Record<string, any>, index: number) => { |
|
isChildrenExcludedListLinked.value[index] = false |
|
isChildrenExcludedListLoading.value[index] = false |
|
}) |
|
|
|
if (childrenExcludedList.value?.list && activeState && activeState[column.value.title]) { |
|
// Mark out exact same objects in activeState[column.value.title] as Linked |
|
// compare all keys and values |
|
childrenExcludedList.value.list.forEach((row: any, index: number) => { |
|
const found = ( |
|
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type) |
|
? [activeState[column.value.title]] |
|
: activeState[column.value.title] |
|
).find((a: any) => { |
|
let isSame = true |
|
|
|
for (const key in a) { |
|
if (a[key] !== row[key]) { |
|
isSame = false |
|
} |
|
} |
|
return isSame |
|
}) |
|
|
|
if (found) { |
|
isChildrenExcludedListLinked.value[index] = true |
|
} |
|
}) |
|
} |
|
} catch (e: any) { |
|
// temporary fix to handle when offset is beyond limit |
|
const error = await extractSdkResponseErrorMsgv2(e) |
|
|
|
if (error.error === NcErrorType.INVALID_OFFSET_VALUE) { |
|
childrenExcludedListPagination.page = 0 |
|
return loadChildrenExcludedList(activeState, true) |
|
} |
|
|
|
message.error(`${t('msg.error.failedToLoadList')}: ${error.message}`) |
|
} finally { |
|
isChildrenExcludedLoading.value = false |
|
} |
|
} |
|
|
|
const loadChildrenList = async (resetOffset: boolean = false) => { |
|
try { |
|
isChildrenLoading.value = true |
|
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)) return |
|
if (!rowId.value || !column.value) return |
|
let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value |
|
if (offset < 0 || resetOffset) { |
|
offset = 0 |
|
childrenListOffsetCount.value = 0 |
|
childrenListPagination.page = 1 |
|
} else if (offset >= childrenListCount.value) { |
|
offset = 0 |
|
} |
|
|
|
if (isPublic.value) { |
|
childrenList.value = await $api.public.dataNestedList( |
|
sharedView.value?.uuid as string, |
|
encodeURIComponent(rowId.value), |
|
colOptions.value.type as RelationTypes, |
|
column.value.id, |
|
{ |
|
limit: String(childrenListPagination.size), |
|
offset: String(offset), |
|
where: |
|
childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`, |
|
} as any, |
|
{ |
|
headers: { |
|
'xc-password': sharedViewPassword.value, |
|
}, |
|
}, |
|
) |
|
} else { |
|
childrenList.value = await $api.dbTableRow.nestedList( |
|
NOCO, |
|
(base?.value?.id || (sharedView.value?.view as any)?.base_id) as string, |
|
meta.value.id, |
|
encodeURIComponent(rowId.value), |
|
colOptions.value.type as RelationTypes, |
|
column?.value?.id, |
|
{ |
|
limit: String(childrenListPagination.size), |
|
offset: String(offset), |
|
where: |
|
childrenListPagination.query && `(${relatedTableDisplayValueProp.value},like,${childrenListPagination.query})`, |
|
} as any, |
|
) |
|
} |
|
childrenList.value?.list.forEach((row: Record<string, any>, index: number) => { |
|
isChildrenListLinked.value[index] = true |
|
isChildrenListLoading.value[index] = false |
|
}) |
|
|
|
if (!childrenListPagination.query) { |
|
childrenListCount.value = childrenList.value?.pageInfo.totalRows ?? 0 |
|
} |
|
} catch (e: any) { |
|
message.error(`${t('msg.error.failedToLoadChildrenList')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} finally { |
|
isChildrenLoading.value = false |
|
} |
|
} |
|
|
|
const deleteRelatedRow = async (row: Record<string, any>, onSuccess?: (row: Record<string, any>) => void) => { |
|
Modal.confirm({ |
|
title: 'Do you want to delete the record?', |
|
type: 'warning', |
|
onOk: async () => { |
|
const id = getRelatedTableRowId(row) |
|
try { |
|
const res: { message?: string[] } | number = await $api.dbTableRow.delete( |
|
NOCO, |
|
baseId, |
|
relatedTableMeta.value.id as string, |
|
encodeURIComponent(id as string), |
|
) |
|
|
|
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 |
|
} |
|
|
|
_reloadData?.({ shouldShowLoading: false }) |
|
|
|
/** reload child list if not a new row */ |
|
if (!isNewRow?.value) { |
|
await loadChildrenList() |
|
} |
|
onSuccess?.(row) |
|
|
|
$e('a:links:delete-related-row') |
|
} catch (e: any) { |
|
message.error(`${t('msg.error.deleteFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} |
|
}, |
|
}) |
|
} |
|
|
|
const unlink = async ( |
|
row: Record<string, any>, |
|
{ metaValue = meta.value }: { metaValue?: TableType } = {}, |
|
undo = false, |
|
index: number, // Index is For Loading and Linked State of Row |
|
) => { |
|
// const column = meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id); |
|
// todo: handle if new record |
|
// if (this.isNew) { |
|
// this.$emit('updateCol', this.row, _cn, null); |
|
// this.localState = null; |
|
// this.$emit('update:localState', this.localState); |
|
// return; |
|
// } |
|
// todo: handle bt column if required |
|
// if (column.rqd) { |
|
// this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000); |
|
// return; |
|
// } |
|
try { |
|
// todo: audit |
|
|
|
childrenListOffsetCount.value = childrenListOffsetCount.value - 1 |
|
childrenExcludedOffsetCount.value = childrenExcludedOffsetCount.value - 1 |
|
|
|
isChildrenExcludedListLoading.value[index] = true |
|
isChildrenListLoading.value[index] = true |
|
await $api.dbTableRow.nestedRemove( |
|
NOCO, |
|
base.value.id as string, |
|
metaValue.id!, |
|
encodeURIComponent(rowId.value), |
|
colOptions.value.type as RelationTypes, |
|
column?.value?.id, |
|
encodeURIComponent(getRelatedTableRowId(row) as string), |
|
) |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: (row: Record<string, any>) => unlink(row, {}, true, index), |
|
args: [clone(row)], |
|
}, |
|
undo: { |
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define |
|
fn: (row: Record<string, any>) => link(row, {}, true, index), |
|
args: [clone(row)], |
|
}, |
|
scope: defineViewScope({ view: activeView.value }), |
|
}) |
|
} |
|
isChildrenExcludedListLinked.value[index] = false |
|
isChildrenListLinked.value[index] = false |
|
if (colOptions.value.type !== RelationTypes.BELONGS_TO && colOptions.value.type !== RelationTypes.ONE_TO_ONE) { |
|
childrenListCount.value = childrenListCount.value - 1 |
|
} |
|
} catch (e: any) { |
|
message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`) |
|
} finally { |
|
isChildrenExcludedListLoading.value[index] = false |
|
isChildrenListLoading.value[index] = false |
|
} |
|
|
|
_reloadData?.({ shouldShowLoading: false }) |
|
$e('a:links:unlink') |
|
} |
|
|
|
const link = async ( |
|
row: Record<string, any>, |
|
{ metaValue = meta.value }: { metaValue?: TableType } = {}, |
|
undo = false, |
|
index: number, // Index is For Loading and Linked State of Row |
|
) => { |
|
// todo: handle new record |
|
// const pid = this._extractRowId(parent, this.parentMeta); |
|
// const id = this._extractRowId(this.row, this.meta); |
|
// const _cn = this.meta.columns.find(c => c.id === this.column.colOptions.fk_child_column_id).title; |
|
// |
|
// if (this.isNew) { |
|
// const _rcn = this.parentMeta.columns.find(c => c.id === this.column.colOptions.fk_parent_column_id).title; |
|
// this.localState = parent; |
|
// this.$emit('update:localState', this.localState); |
|
// this.$emit('updateCol', this.row, _cn, parent[_rcn]); |
|
// this.newRecordModal = false; |
|
// return; |
|
// } |
|
try { |
|
isChildrenExcludedListLoading.value[index] = true |
|
isChildrenListLoading.value[index] = true |
|
|
|
childrenListOffsetCount.value = childrenListOffsetCount.value + 1 |
|
childrenExcludedOffsetCount.value = childrenExcludedOffsetCount.value + 1 |
|
|
|
await $api.dbTableRow.nestedAdd( |
|
NOCO, |
|
base.value.id as string, |
|
metaValue.id as string, |
|
encodeURIComponent(rowId.value), |
|
colOptions.value.type as RelationTypes, |
|
column?.value?.id, |
|
encodeURIComponent(getRelatedTableRowId(row) as string) as string, |
|
) |
|
// await loadChildrenList() |
|
|
|
if (!undo) { |
|
addUndo({ |
|
redo: { |
|
fn: (row: Record<string, any>) => link(row, {}, true, index), |
|
args: [clone(row)], |
|
}, |
|
undo: { |
|
fn: (row: Record<string, any>) => unlink(row, {}, true, index), |
|
args: [clone(row)], |
|
}, |
|
scope: defineViewScope({ view: activeView.value }), |
|
}) |
|
} |
|
isChildrenExcludedListLinked.value[index] = true |
|
isChildrenListLinked.value[index] = true |
|
|
|
if (colOptions.value.type !== RelationTypes.BELONGS_TO && colOptions.value.type !== RelationTypes.ONE_TO_ONE) { |
|
childrenListCount.value = childrenListCount.value + 1 |
|
} else { |
|
isChildrenExcludedListLinked.value = Array(childrenExcludedList.value?.list.length).fill(false) |
|
isChildrenExcludedListLinked.value[index] = true |
|
} |
|
} catch (e: any) { |
|
message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`) |
|
} finally { |
|
// To Keep the Loading State for Minimum 600ms |
|
|
|
isChildrenExcludedListLoading.value[index] = false |
|
isChildrenListLoading.value[index] = false |
|
} |
|
|
|
_reloadData?.({ shouldShowLoading: false }) |
|
$e('a:links:link') |
|
} |
|
|
|
// watchers |
|
watch(childrenExcludedListPagination, async () => { |
|
await loadChildrenExcludedList(newRowState.state) |
|
}) |
|
|
|
watch(childrenListPagination, async () => { |
|
await loadChildrenList() |
|
}) |
|
|
|
watch(childrenList, async () => { |
|
childrenList.value?.list.forEach((row: Record<string, any>, index: number) => { |
|
isChildrenListLinked.value[index] = true |
|
isChildrenListLoading.value[index] = false |
|
}) |
|
}) |
|
|
|
const resetChildrenExcludedOffsetCount = () => { |
|
childrenExcludedOffsetCount.value = 0 |
|
} |
|
|
|
const resetChildrenListOffsetCount = () => { |
|
childrenListOffsetCount.value = 0 |
|
} |
|
|
|
return { |
|
relatedTableMeta, |
|
loadRelatedTableMeta, |
|
relatedTableDisplayValueProp, |
|
displayValueTypeAndFormatProp, |
|
childrenExcludedList, |
|
childrenList, |
|
childrenListCount, |
|
childrenListOffsetCount, |
|
childrenExcludedOffsetCount, |
|
rowId, |
|
childrenExcludedListPagination, |
|
childrenListPagination, |
|
displayValueProp, |
|
meta, |
|
unlink, |
|
link, |
|
loadChildrenExcludedList, |
|
loadChildrenList, |
|
isChildrenExcludedListLinked, |
|
isChildrenListLinked, |
|
isChildrenListLoading, |
|
isChildrenExcludedListLoading, |
|
row, |
|
isChildrenLoading, |
|
isChildrenExcludedLoading, |
|
deleteRelatedRow, |
|
getRelatedTableRowId, |
|
headerDisplayValue, |
|
relatedTableDisplayValuePropId, |
|
resetChildrenExcludedOffsetCount, |
|
resetChildrenListOffsetCount, |
|
} |
|
}, |
|
'ltar-store', |
|
) |
|
|
|
export { useProvideLTARStore } |
|
|
|
export function useLTARStoreOrThrow() { |
|
const ltarStore = useLTARStore() |
|
if (ltarStore == null) throw new Error('Please call `useLTARStore` on the appropriate parent component') |
|
return ltarStore |
|
}
|
|
|