Files
nocodb/packages/nc-gui/composables/useExpandedFormStore.ts

966 lines
32 KiB
TypeScript

import type {
AuditType,
ColumnType,
CommentPayload,
DataPayload,
MetaType,
PlanLimitExceededDetailsType,
TableType,
} from 'nocodb-sdk'
import {
EventType,
PermissionEntity,
PermissionKey,
PlanLimitTypes,
ViewTypes,
isAIPromptCol,
isHiddenCol,
isReadOnlyColumn,
isSystemColumn,
isVirtualCol,
} from 'nocodb-sdk'
import type { Ref } from 'vue'
import dayjs from 'dayjs'
interface AuditTypeExtended extends AuditType {
created_display_name?: string
created_display_name_short?: string
created_by_email?: string
created_by_meta?: MetaType
}
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState(
(
meta: Ref<TableType>,
_row: Ref<Row>,
maintainDefaultViewOrder: Ref<boolean>,
useMetaFields: boolean,
allowNullFieldIds?: string[],
) => {
const { $e, $state, $api, $ncSocket } = useNuxtApp()
const { t } = useI18n()
const isPublic = inject(IsPublicInj, ref(false))
const audits = ref<Array<AuditTypeExtended>>([])
const isAuditLoading = ref(false)
const commentsDrawer = ref(true)
const saveRowAndStay = ref(0)
const changedColumns = ref<Set<string>>(new Set<string>())
const localOnlyChanges = ref<Record<string, any>>({})
const basesStore = useBases()
const { basesUser } = storeToRefs(basesStore)
const { base } = storeToRefs(useBase())
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : []))
const { sharedView } = useSharedView()
const row = ref<Row>(
!sharedView.value ||
sharedView.value?.type === ViewTypes.GALLERY ||
sharedView.value?.type === ViewTypes.KANBAN ||
_row.value?.rowMeta?.new
? _row.value
: ({ row: {}, oldRow: {}, rowMeta: {} } as Row),
)
if (row.value?.rowMeta?.fromExpandedForm) {
row.value.rowMeta.fromExpandedForm = true
}
const rowStore = useProvideSmartsheetRowStore(row, changedColumns)
const activeView = inject(ActiveViewInj, ref())
const { addUndo, clone, defineViewScope } = useUndoRedo()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { comments, resolveComment, loadComments, updateComment, deleteComment, saveComment, isCommentsLoading } =
useProvideRowComments(meta, row)
const { isUIAllowed } = useRoles()
const { handleUpgradePlan, isPaymentEnabled } = useEeConfig()
const { isAllowed } = usePermissions()
const isAllowedAddNewRecord = computed(() => {
if (!isEeUI) return true
return meta.value?.id && isAllowed(PermissionEntity.TABLE, meta.value.id, PermissionKey.TABLE_RECORD_ADD)
})
const getIsAllowedEditField = (fieldId: string) => {
if (!isEeUI) return true
return fieldId && isAllowed(PermissionEntity.FIELD, fieldId, PermissionKey.RECORD_FIELD_EDIT)
}
// getters
const displayValue = computed(() => {
if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv)
if (!col) {
return
}
return row.value.row?.[col.title as string]
}
})
const { fieldsMap, isLocalMode, showSystemFields, hasViewFieldDataEditPermission } = useViewColumnsOrThrow()
const isHiddenColumnInNewRecord = (col: ColumnType) => {
return isReadOnlyColumn(col) || isAIPromptCol(col)
}
/**
* Injects the fields from the parent component if available.
* Uses a ref to ensure reactivity.
*/
const fieldsFromParent = inject<Ref<ColumnType[] | null>>(FieldsInj, ref(null))
/**
* Computes the list of fields to be used based on the given conditions.
*
* - Prefers `props.useMetaFields` over `fieldsFromParent` if enabled.
* - Filters out system columns and readonly fields for new records.
* - Maintains default view order if `maintainDefaultViewOrder` is enabled.
*
* @returns {ColumnType[]} The computed list of fields.
*/
const fields = computed(() => {
// Give preference to props.useMetaFields instead of fieldsFromParent
if (useMetaFields) {
if (maintainDefaultViewOrder.value) {
return (meta.value.columns ?? [])
.filter(
(col) =>
!isHiddenCol(col, meta.value ?? {}) &&
!isSystemColumn(col) &&
!!(col.meta?.defaultViewColVisibility ?? true) &&
// if new record, then hide readonly fields
(!rowStore.isNew.value || !isHiddenColumnInNewRecord(col)),
)
.sort((a, b) => {
return (a.meta?.defaultViewColOrder ?? Infinity) - (b.meta?.defaultViewColOrder ?? Infinity)
})
}
return (meta.value.columns ?? []).filter(
(col) =>
!isHiddenCol(col, meta.value ?? {}) &&
// if new record, then hide readonly fields
(!rowStore.isNew.value || !isHiddenColumnInNewRecord(col)) &&
// exclude system columns
!isSystemColumn(col) &&
// exclude hidden columns
!!(col.meta?.defaultViewColVisibility ?? true),
)
}
// If `props.useMetaFields` is not enabled, use fields from the parent component
if (fieldsFromParent.value) {
if (rowStore.isNew.value) {
return fieldsFromParent.value.filter((col) => !isHiddenColumnInNewRecord(col))
}
return fieldsFromParent.value
}
return []
})
const hiddenFields = computed(() => {
// todo: figure out when meta.value is undefined
const _hiddenFields = (meta.value?.columns ?? []).filter(
(col) =>
!isHiddenCol(col, meta.value ?? {}) &&
(!useMetaFields || !isSystemColumn(col)) &&
!fields.value?.includes(col) &&
(isLocalMode.value && !hasViewFieldDataEditPermission.value && col?.id && fieldsMap.value[col.id]
? fieldsMap.value[col.id]?.initialShow
: true) &&
// exclude readonly fields from hidden fields if new record creation
(!rowStore.isNew.value || !isHiddenColumnInNewRecord(col)),
)
if (useMetaFields) {
return maintainDefaultViewOrder.value
? _hiddenFields.sort((a, b) => {
return (a.meta?.defaultViewColOrder ?? Infinity) - (b.meta?.defaultViewColOrder ?? Infinity)
})
: _hiddenFields
}
// record from same view and same table (not linked)
else {
return _hiddenFields
.filter((col) => {
if (rowStore.isNew.value || !showSystemFields.value) {
return !isSystemColumn(col)
}
return true
})
.sort((a, b) => {
return (fieldsMap.value[a.id]?.order ?? Infinity) - (fieldsMap.value[b.id]?.order ?? Infinity)
})
}
})
const auditToCursor = (audit: any) => {
return `${audit.id}|${audit.created_at}`
}
const primaryKey = computed(() => {
return extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
})
const currentAuditCursor = ref('')
const hasMoreAudits = ref(false)
const loadAudits = async (_rowId?: string, showLoading = true) => {
if (!isUIAllowed('recordAuditList') || (!row.value && !_rowId)) return
const rowId = _rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId || !meta.value.base_id) return
try {
if (showLoading) {
isAuditLoading.value = true
}
const response = await $api.internal.getOperation(
base.value.fk_workspace_id ?? NO_SCOPE,
(meta.value.base_id as string) ?? (base.value.id as string),
{
operation: 'recordAuditList',
fk_model_id: meta.value.id as string,
row_id: rowId,
cursor: currentAuditCursor.value,
},
)
const lastRecord = response.list?.[response.list.length - 1]
if (lastRecord) {
currentAuditCursor.value = auditToCursor(lastRecord)
}
hasMoreAudits.value = !response.pageInfo?.isLastPage
const res = response.list?.reverse?.() || []
audits.value.unshift(
...res.map((audit) => {
const user = baseUsers.value.find((u) => u.id === audit.fk_user_id || u.email === audit.user)
return {
...audit,
created_display_name: user?.display_name,
created_display_name_short: user?.display_name ?? extractNameFromEmail(user?.email),
created_by_email: user?.email,
created_by_meta: user?.meta,
}
}),
)
} catch (e: any) {
console.error(e)
const errorInfo = await extractSdkResponseErrorMsgv2(e)
if (isPaymentEnabled.value && errorInfo.error === NcErrorType.ERR_PLAN_LIMIT_EXCEEDED) {
const details = errorInfo.details as PlanLimitExceededDetailsType
handleUpgradePlan({
title: t('upgrade.updateToExtendRecordHistory'),
content: t('upgrade.updateToExtendRecordHistorySubtitle', {
activePlan: details.plan,
plan: details.higherPlan,
period: formatDurationFromDays(+(details.limit ?? 14)),
}),
limitOrFeature: PlanLimitTypes.LIMIT_AUDIT_RETENTION,
})
} else {
message.error(errorInfo.message)
}
} finally {
isAuditLoading.value = false
}
}
function formatDurationFromDays(days: number): string {
if (days === Infinity) {
return '3+ years'
}
if (days < 14) {
return `${days} day${days === 1 ? '' : 's'}`
} else if (days < 30) {
const weeks = Math.floor(days / 7)
return `${weeks} week${weeks === 1 ? '' : 's'}`
} else if (days < 365) {
const months = Math.floor(days / 30)
return `${months} month${months === 1 ? '' : 's'}`
} else {
const years = Math.floor(days / 365)
return years > 3 ? `${years}+ years` : `${years} year${years === 1 ? '' : 's'}`
}
}
const loadMoreAudits = async () => {
if (!hasMoreAudits.value) {
return
}
await loadAudits()
}
const resetAuditPages = async () => {
currentAuditCursor.value = ''
audits.value = []
hasMoreAudits.value = false
await loadAudits()
}
const isYou = (email: string) => {
return $state.user?.value?.email === email
}
const loadKanbanData = async () => {
if (activeView.value?.type === ViewTypes.KANBAN) {
const { loadKanbanData: _loadKanbanData } = useKanbanViewStoreOrThrow()
await _loadKanbanData()
}
}
const save = async (
ltarState: Record<string, any> = {},
undo = false,
// TODO: Hack. Remove this when kanban injection store issue is resolved
{
kanbanClbk,
}: {
kanbanClbk?: (row: Row, isNewRow: boolean) => void
} = {},
) => {
if (!meta.value.id) return
let data
const isNewRow = row.value.rowMeta?.new ?? false
if (isNewRow) {
const { getMeta } = useMetas()
const { missingRequiredColumns, insertObj } = await populateInsertObject({
meta: meta.value,
ltarState,
getMeta,
row: row.value.row,
throwError: true,
allowNullFieldIds,
})
if (missingRequiredColumns.size) return
data = await $api.dbTableRow.create('noco', meta.value.base_id, meta.value.id, {
...insertObj,
...(ltarState || {}),
})
Object.assign(row.value, {
row: data,
rowMeta: {
...row.value.rowMeta,
new: false,
},
oldRow: { ...data },
})
if (!undo) {
const id = extractPkFromRow(data, meta.value?.columns as ColumnType[])
const pkData = rowPkData(row.value.row, meta.value?.columns as ColumnType[])
// TODO remove linked record
addUndo({
redo: {
fn: async (rowData: any) => {
await $api.dbTableRow.create('noco', base.value.id as string, meta.value.id, { ...pkData, ...rowData })
await loadKanbanData()
reloadTrigger?.trigger()
},
args: [clone(insertObj)],
},
undo: {
fn: async (id: string) => {
const res: any = await $api.dbViewRow.delete(
'noco',
meta.value?.base_id ?? (base.value.id as string),
meta.value?.id as string,
activeView.value?.id as string,
encodeURIComponent(id),
)
if (res.message) {
throw new Error(res.message)
}
await loadKanbanData()
reloadTrigger?.trigger()
},
args: [id],
},
scope: defineViewScope({ view: activeView.value }),
})
}
} else {
const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => {
obj[col] = row.value.row[col]
return obj
}, {} as Record<string, any>)
if (Object.keys(updateOrInsertObj).length) {
const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!id) {
return message.info(t('msg.info.updateNotAllowedWithoutPK'))
}
const updatedData = await $api.dbTableRow.update(
NOCO,
meta.value.base_id ?? (base.value.id as string),
meta.value.id,
encodeURIComponent(id),
updateOrInsertObj,
)
// If the updated row is now hidden by RLS policy, mark it
if (updatedData?.__nc_rls_hidden) {
row.value.row.__nc_rls_hidden = true
}
if (!undo) {
const undoObject = [...changedColumns.value].reduce((obj, col) => {
obj[col] = row.value.oldRow[col]
return obj
}, {} as Record<string, any>)
addUndo({
redo: {
fn: async (id: string, data: Record<string, any>) => {
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data)
await loadKanbanData()
reloadTrigger?.trigger()
},
args: [id, clone(updateOrInsertObj)],
},
undo: {
fn: async (id: string, data: Record<string, any>) => {
await $api.dbTableRow.update(NOCO, base.value.id as string, meta.value.id!, encodeURIComponent(id), data)
await loadKanbanData()
reloadTrigger?.trigger()
},
args: [id, clone(undoObject)],
},
scope: defineViewScope({ view: activeView.value }),
})
}
if (commentsDrawer.value) {
await Promise.all([loadComments()])
}
} else {
// No columns to update
message.info(t('msg.info.noColumnsToUpdate'))
return
}
}
if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
kanbanClbk(row.value, isNewRow)
}
changedColumns.value = new Set()
$e('a:row-expand:add')
return data
}
const clearColumns = () => {
changedColumns.value = new Set()
}
const loadRow = async (rowId?: string, onlyVirtual = false, onlyNewColumns = false) => {
if (row?.value?.rowMeta?.new || isPublic.value || !meta.value?.id) return
// Row is hidden by RLS policy — skip read to avoid 404
if (row?.value?.row?.__nc_rls_hidden) return
const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!recordId) return
let record: Record<string, any> = {}
try {
record = await $api.dbTableRow.read(
NOCO,
// todo: base_id missing on view type
((meta.value?.base_id ?? base?.value?.id) || (sharedView.value?.view as any)?.base_id) as string,
meta.value.id as string,
encodeURIComponent(recordId),
{
getHiddenColumn: true,
},
)
} catch (err: any) {
if (err.response?.status === 404) {
const router = useRouter()
message.error(t('msg.noRecordFound'))
router.replace({ query: {} })
} else {
message.error(`${await extractSdkResponseErrorMsg(err)}`)
}
}
try {
// update only virtual columns value if `onlyVirtual` is true
if (onlyVirtual) {
record = {
...row.value.row,
...(meta.value.columns ?? []).reduce((partialRecord, col) => {
if (isVirtualCol(col) && col.title && col.title in record) {
partialRecord[col.title] = (record as Record<string, any>)[col.title as string]
}
return partialRecord
}, {} as Record<string, any>),
}
}
// update only new/duplicated/renamed columns value if `onlyNewColumns` is true
if (onlyNewColumns) {
record = Object.keys(record).reduce((acc, curr) => {
if (!Object.prototype.hasOwnProperty.call(row.value.row, curr)) {
acc[curr] = record[curr]
} else {
acc[curr] = row.value.row[curr]
}
return acc
}, {} as Record<string, any>)
}
Object.assign(row.value, {
row: record,
oldRow: { ...record },
rowMeta: {
...row.value.rowMeta,
},
})
} catch (e: any) {
message.error(`${t('msg.error.errorLoadingRecord')}`)
}
}
const deleteRowById = async (rowId?: string) => {
try {
const recordId = rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
const res: { message?: string[] } | number = await $api.dbTableRow.delete(
NOCO,
meta.value?.base_id ?? (base.value.id as string),
meta.value.id as string,
encodeURIComponent(recordId),
)
if (res.message) {
message.info(
`Record delete failed: ${`Unable to delete record with ID ${rowId} because of the following:
\n${res.message.join('\n')}.\n
Clear the data first & try again`})}`,
)
return false
}
} catch (e: any) {
message.error(`${t('msg.error.deleteFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
}
const processedAudits = computed(() => {
const result: typeof audits.value = []
try {
const allAudits = JSON.parse(JSON.stringify(audits.value))
for (const audit of allAudits) {
if (audit.op_type !== 'DATA_UPDATE' && audit.op_type !== 'DATA_CASCADE_UPDATE') {
result.push(audit)
continue
}
const details = JSON.parse(audit.details)
for (const columnKey of Object.keys(details.data || {})) {
if (!details.column_meta?.[columnKey]) {
delete details.data[columnKey]
delete details.old_data[columnKey]
delete details.column_meta[columnKey]
continue
}
if (
['CreatedTime', 'CreatedBy', 'LastModifiedTime', 'LastModifiedBy'].includes(details.column_meta?.[columnKey]?.type)
) {
delete details.data[columnKey]
delete details.old_data[columnKey]
delete details.column_meta[columnKey]
continue
}
}
if (Object.values(details.column_meta || {}).length > 0) {
audit.details = JSON.stringify(details)
result.push(audit)
}
}
} catch (e) {
console.error(e)
}
return result
})
const consolidatedAudits = computed(() => {
const result: typeof audits.value = []
const applyLinkAuditValue = (detail: any, refRowId: string, value: string, type: 'link' | 'unlink') => {
if (!detail.consolidated_ref_display_values_links) detail.consolidated_ref_display_values_links = []
if (!detail.consolidated_ref_display_values_unlinks) detail.consolidated_ref_display_values_unlinks = []
if (type === 'link') {
if (!detail.consolidated_ref_display_values_unlinks.find((it: any) => it.refRowId === refRowId)) {
if (!detail.consolidated_ref_display_values_links.find((it: any) => it.refRowId === refRowId)) {
detail.consolidated_ref_display_values_links.push({ refRowId, value })
}
} else {
detail.consolidated_ref_display_values_unlinks.splice(
detail.consolidated_ref_display_values_unlinks.findIndex((it: any) => it.refRowId === refRowId),
1,
)
}
} else {
if (!detail.consolidated_ref_display_values_links.find((it: any) => it.refRowId === refRowId)) {
if (!detail.consolidated_ref_display_values_unlinks.find((it: any) => it.refRowId === refRowId)) {
detail.consolidated_ref_display_values_unlinks.push({ refRowId, value })
}
} else {
detail.consolidated_ref_display_values_links.splice(
detail.consolidated_ref_display_values_links.findIndex((it: any) => it.refRowId === refRowId),
1,
)
}
}
}
try {
const allAudits = JSON.parse(JSON.stringify(processedAudits.value))
while (allAudits.length > 0) {
const current = allAudits.shift()!
if (current.op_type === 'DATA_LINK' || current.op_type === 'DATA_UNLINK') {
const last = result.findLast((it) => it.op_type === 'DATA_LINK' || it.op_type === 'DATA_UNLINK')
const details = JSON.parse(current.details)
if (!last) {
applyLinkAuditValue(
details,
details.ref_row_id,
details.ref_display_value,
current.op_type === 'DATA_LINK' ? 'link' : 'unlink',
)
current.details = JSON.stringify(details)
result.push(current)
} else {
const lastDetails = JSON.parse(last.details)
if (
last.user === current.user &&
dayjs(current.created_at).diff(dayjs(last.created_at), 'second') <= 30 &&
lastDetails.link_field_id === details.link_field_id &&
lastDetails.ref_table_title === details.ref_table_title
) {
applyLinkAuditValue(
lastDetails,
details.ref_row_id,
details.ref_display_value,
current.op_type === 'DATA_LINK' ? 'link' : 'unlink',
)
if (
lastDetails.consolidated_ref_display_values_links?.length > 0 ||
lastDetails.consolidated_ref_display_values_unlinks?.length
) {
last.details = JSON.stringify(lastDetails)
} else {
result.pop()
}
} else {
applyLinkAuditValue(
details,
details.ref_row_id,
details.ref_display_value,
current.op_type === 'DATA_LINK' ? 'link' : 'unlink',
)
current.details = JSON.stringify(details)
result.push(current)
}
}
} else if (current.op_type === 'DATA_UPDATE' || current.op_type === 'DATA_CASCADE_UPDATE') {
const last = result.findLast((it) => it.op_type === current.op_type)
if (!last || last.user !== current.user || dayjs(current.created_at).diff(dayjs(last.created_at), 'second') > 30) {
result.push(current)
continue
}
const details = JSON.parse(current.details)
const lastDetails = JSON.parse(last.details)
for (const field of Object.values(details.column_meta ?? {}) as any[]) {
if (['MultiSelect', 'SingleSelect'].includes(field.type) && lastDetails?.column_meta?.[field?.title]) {
lastDetails.data[field.title] = details.data[field.title]
for (const option of details.column_meta[field.title]?.options?.choices ?? []) {
if (!lastDetails.column_meta[field.title]?.options.choices.find((it: any) => it.id === option.id)) {
lastDetails.column_meta[field.title].options.choices.push(option)
}
}
last.details = JSON.stringify(lastDetails)
delete details.old_data[field.title]
delete details.data[field.title]
delete details.column_meta[field.title]
current.details = JSON.stringify(details)
} else if (lastDetails?.column_meta?.[field?.title] && lastDetails.old_data[field.title]) {
lastDetails.data[field.title] = details.data[field.title]
last.details = JSON.stringify(lastDetails)
delete details.old_data[field.title]
delete details.data[field.title]
delete details.column_meta[field.title]
current.details = JSON.stringify(details)
} else if (details?.column_meta?.[field?.title] && !lastDetails?.column_meta?.[field?.title]) {
if (!lastDetails.column_meta) lastDetails.column_meta = {}
if (!lastDetails.old_data) lastDetails.old_data = {}
if (!lastDetails.data) lastDetails.data = {}
lastDetails.column_meta[field.title] = details.column_meta[field.title]
lastDetails.old_data[field.title] = details.old_data[field.title]
lastDetails.data[field.title] = details.data[field.title]
last.details = JSON.stringify(lastDetails)
delete details.old_data[field.title]
delete details.data[field.title]
delete details.column_meta[field.title]
current.details = JSON.stringify(details)
}
}
if (Object.values(details.column_meta).length > 0) {
result.push(current)
}
} else {
result.push(current)
}
}
} catch (e) {
console.error(e)
}
return result
})
const auditCommentGroups = computed(() => {
const adts = [...consolidatedAudits.value].map((it) => ({
user: it.user,
displayName: it.created_display_name,
displayNameShort: it.created_display_name_short,
created_at: it.created_at,
type: 'audit',
audit: it,
}))
const cmnts = [...comments.value].map((it) => ({
...it,
user: it.created_by_email,
displayName: it.created_display_name,
displayNameShort: it.created_display_name_short,
type: 'comment',
}))
const groups = [...adts, ...cmnts]
return groups.sort((a, b) => {
return dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1
})
})
const baseRoles = computedAsync(async () => {
// if active base id and meta base id is different, then extract the base roles of the meta base
if (meta.value?.base_id !== base.value?.id) {
return await basesStore.getBaseRoles(meta.value?.base_id, {
skipUpdatingUser: true,
})
}
})
const activeDataListener = ref<string | null>(null)
const activeCommentListener = ref<string | null>(null)
watch(
meta,
(newMeta, oldMeta) => {
if (newMeta?.fk_workspace_id && newMeta?.base_id && newMeta?.id) {
if (oldMeta?.id && oldMeta.id === newMeta.id) return
if (activeDataListener.value) {
$ncSocket.offMessage(activeDataListener.value)
}
if (activeCommentListener.value) {
$ncSocket.offMessage(activeCommentListener.value)
}
activeDataListener.value = $ncSocket.onMessage(
`${EventType.DATA_EVENT}:${newMeta.fk_workspace_id}:${newMeta.base_id}:${newMeta.id}`,
(data: DataPayload) => {
const { id, action, payload } = data
const activePk = extractPkFromRow(row.value.row, meta.value?.columns as ColumnType[])
if (`${id}` === activePk) {
if (action === 'update') {
try {
if (payload) {
// Merge payload with local row, but preserve locally changed columns
const mergedRow = { ...row.value.row, ...payload }
for (const col of changedColumns.value) {
if (Object.prototype.hasOwnProperty.call(row.value.row, col)) {
mergedRow[col] = row.value.row[col]
if (row.value.row[col] !== payload[col]) {
localOnlyChanges.value[col] = payload[col]
}
}
}
Object.assign(row.value, {
row: mergedRow,
oldRow: { ...mergedRow },
})
// Do NOT clear changedColumns here, as we want to preserve local changes
} else {
console.warn('No payload provided for update action')
}
} catch (e) {
console.error('Failed to update cached row on socket event', e)
}
} else if (action === 'delete') {
try {
//
} catch (e) {
console.error('Failed to delete cached row on socket event', e)
}
}
}
},
)
activeCommentListener.value = $ncSocket.onMessage(
`${EventType.COMMENT_EVENT}:${newMeta.fk_workspace_id}:${newMeta.base_id}:${newMeta.id}`,
(data: CommentPayload) => {
const { action, id, payload } = data
if (primaryKey.value && `${id}` === `${primaryKey.value}`) {
const commentId = payload.id
const user = baseUsers.value.find((u) => u.id === payload.created_by)
const finalPayload = {
...payload,
created_display_name: user?.display_name,
created_display_name_short: user?.display_name ?? extractNameFromEmail(user?.email),
created_by_email: user?.email,
created_by_meta: user?.meta,
}
if (action === 'add') {
comments.value.push(finalPayload)
} else if (action === 'update') {
const index = comments.value.findIndex((comment) => comment.id === commentId)
if (index !== -1) {
comments.value[index] = finalPayload
}
} else if (action === 'delete') {
comments.value = comments.value.filter((comment) => comment.id !== commentId)
}
}
},
)
}
},
{ immediate: true },
)
const unsubscribeActiveChannels = (): void => {
;[activeDataListener.value, activeCommentListener.value].filter(Boolean).forEach((channel) => {
$ncSocket.offMessage(channel!)
})
}
onBeforeUnmount(() => {
unsubscribeActiveChannels()
})
return {
...rowStore,
loadComments,
deleteComment,
loadAudits,
comments,
audits,
isAuditLoading,
clearColumns,
auditCommentGroups,
consolidatedAudits,
hasMoreAudits,
loadMoreAudits,
resetAuditPages,
resolveComment,
isCommentsLoading,
saveComment,
isYou,
commentsDrawer,
row,
deleteRowById,
displayValue,
save,
changedColumns,
localOnlyChanges,
loadRow,
primaryKey,
saveRowAndStay,
updateComment,
baseRoles,
fieldsFromParent,
fields,
hiddenFields,
isAllowedAddNewRecord,
getIsAllowedEditField,
meta,
}
},
'expanded-form-store',
)
export { useProvideExpandedFormStore }
export function useExpandedFormStoreOrThrow() {
const expandedFormStore = useExpandedFormStore()
if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component')
return expandedFormStore
}