Add Contact standard object, related lists, meilisearch, pagination, search, AI assistant

This commit is contained in:
Francisco Gaona
2026-01-16 18:01:26 +01:00
parent 51c82d3d95
commit 20fc90a3fb
62 changed files with 6613 additions and 278 deletions

View File

@@ -11,6 +11,8 @@ interface RelatedListConfig {
relationName: string // e.g., 'domains', 'users'
objectApiName: string // e.g., 'domains', 'users'
fields: FieldConfig[] // Fields to display in the list
lookupFieldApiName?: string // Used to filter by parentId when fetching
parentObjectApiName?: string // Parent object API name, used to derive lookup field if missing
canCreate?: boolean
createRoute?: string // Route to create new related record
}
@@ -19,11 +21,11 @@ interface Props {
config: RelatedListConfig
parentId: string
relatedRecords?: any[] // Can be passed in if already fetched
baseUrl?: string // Base API URL, defaults to '/central'
baseUrl?: string // Base API URL, defaults to runtime objects
}
const props = withDefaults(defineProps<Props>(), {
baseUrl: '/central',
baseUrl: '/runtime/objects',
relatedRecords: undefined,
})
@@ -53,14 +55,48 @@ const fetchRelatedRecords = async () => {
try {
// Replace :parentId placeholder in the API path
let apiPath = props.config.objectApiName.replace(':parentId', props.parentId)
const sanitizedBase = props.baseUrl.replace(/\/$/, '')
let apiPath = props.config.objectApiName.replace(':parentId', props.parentId).replace(/^\/+/, '')
const isRuntimeObjects = sanitizedBase.endsWith('/runtime/objects')
// Default runtime object routes expect /:objectApiName/records
if (isRuntimeObjects && !apiPath.includes('/')) {
apiPath = `${apiPath}/records`
}
const response = await api.get(`${props.baseUrl}/${apiPath}`, {
const findLookupKey = () => {
if (props.config.lookupFieldApiName) return props.config.lookupFieldApiName
const parentName = props.config.parentObjectApiName?.toLowerCase()
const fields = props.config.fields || []
const parentMatch = fields.find(field => {
const relation = (field as any).relationObject || (field as any).referenceObject
return relation && parentName && relation.toLowerCase() === parentName
})
if (parentMatch?.apiName) return parentMatch.apiName
const lookupMatch = fields.find(
field => (field.type || '').toString().toLowerCase() === 'lookup'
)
if (lookupMatch?.apiName) return lookupMatch.apiName
const idMatch = fields.find(field =>
field.apiName?.toLowerCase().endsWith('id')
)
if (idMatch?.apiName) return idMatch.apiName
return 'parentId'
}
const lookupKey = findLookupKey()
const response = await api.get(`${sanitizedBase}/${apiPath}`, {
params: {
parentId: props.parentId,
[lookupKey]: props.parentId,
},
})
records.value = response || []
records.value = response?.data || response || []
} catch (err: any) {
console.error('Error fetching related records:', err)
error.value = err.message || 'Failed to fetch related records'
@@ -77,20 +113,40 @@ const handleViewRecord = (recordId: string) => {
emit('navigate', props.config.objectApiName, recordId)
}
const formatValue = (value: any, field: FieldConfig): string => {
const formatValue = (record: any, field: FieldConfig): string => {
const value = record?.[field.apiName]
if (value === null || value === undefined) return '-'
const type = (field.type || '').toString().toLowerCase()
// Lookup fields: use related object display value when available
if (type === 'lookup' || type === 'belongsto') {
const relationName = field.apiName.replace(/Id$/i, '').toLowerCase()
const related = record?.[relationName]
if (related && typeof related === 'object') {
const displayField = (field as any).relationDisplayField || 'name'
if (related[displayField]) return String(related[displayField])
// Fallback: first string-ish property or ID
const firstStringKey = Object.keys(related).find(
key => typeof related[key] === 'string'
)
if (firstStringKey) return String(related[firstStringKey])
if (related.id) return String(related.id)
}
// If no related object, show raw value
}
// Handle different field types
if (field.type === 'date') {
if (type === 'date') {
return new Date(value).toLocaleDateString()
}
if (field.type === 'datetime') {
if (type === 'datetime' || type === 'date_time' || type === 'date-time') {
return new Date(value).toLocaleString()
}
if (field.type === 'boolean') {
if (type === 'boolean') {
return value ? 'Yes' : 'No'
}
if (field.type === 'select' && field.options) {
if (type === 'select' && field.options) {
const option = field.options.find(opt => opt.value === value)
return option?.label || value
}
@@ -163,7 +219,7 @@ onMounted(() => {
<TableBody>
<TableRow v-for="record in displayRecords" :key="record.id">
<TableCell v-for="field in config.fields" :key="field.id">
{{ formatValue(record[field.apiName], field) }}
{{ formatValue(record, field) }}
</TableCell>
<TableCell>
<Button