Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions client/web/compose/src/components/Common/FieldPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
<template
v-if="field.label"
>
{{ field.label }} ({{ field.name }})
{{ field.label }}
<template v-if="field.originalName">
({{ field.originalName }})
</template>
</template>
<template
v-else
>
{{ field.name }}
{{ field.originalName || field.name }}
</template>
<template
v-if="field.isRequired"
Expand All @@ -40,6 +43,12 @@
>
{{ $t('selector.systemField') }}
</small>
<small
v-if="field.isParentField"
class="cursor-default ml-1 text-truncate text-primary"
>
{{ field.parentModuleName }}
</small>
</template>
</c-item-picker>
</div>
Expand Down Expand Up @@ -95,6 +104,18 @@ export default {
type: Array,
default: null,
},

// NEW: optional parent module object
extraModule: {
type: Object,
default: null,
},

// NEW: array of field objects from the parent module
extraModuleFields: {
type: Array,
default: () => [],
},
},

computed: {
Expand Down Expand Up @@ -153,8 +174,10 @@ export default {
mFields.sort((a, b) => a.label.localeCompare(b.label))
}

// Build base options from child module fields
let baseOptions = []
if (mFields && sysFields) {
return [
baseOptions = [
...[...mFields],
...sysFields,
].map(field => ({
Expand All @@ -169,7 +192,7 @@ export default {
field,
}))
} else {
return Object.keys(this.module).map(key => {
baseOptions = Object.keys(this.module).map(key => {
return this.module[key]
}).map(f => ({
...f,
Expand All @@ -178,6 +201,32 @@ export default {
},
}))
}

// Build parent module field options (if extraModule and extraModuleFields are provided)
const extraFieldOptions = (this.extraModule && this.extraModuleFields.length > 0)
? this.extraModuleFields.map(field => {
// Use moduleID::fieldName as the unique key to avoid name collisions
const uniqueName = `${this.extraModule.moduleID}::${field.name}`
return {
value: uniqueName,
text: [field.name, field.label, field.kind, this.extraModule.name].join(' '),
field: {
...field,
// Override name with the namespaced version for storage/lookup
name: uniqueName,
label: field.label || field.name,
// Flags and metadata for downstream use
isParentField: true,
parentModuleID: this.extraModule.moduleID,
parentModuleName: this.extraModule.name,
originalName: field.name,
},
}
})
: []

// Concat extra options after the base options
return [...baseOptions, ...extraFieldOptions]
},
},
}
Expand Down
169 changes: 161 additions & 8 deletions client/web/compose/src/components/PageBlocks/RecordBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<template v-for="field in fields">
<b-form-group
v-if="canDisplay(field)"
:key="`${field.fieldID}-${field.name}`"
:key="`${field.fieldID || field.originalName}-${field.name}`"
:data-test-id="getFieldCypressId(field.label || field.name)"
:label-cols-md="options.horizontalFieldLayoutEnabled && '6'"
:label-cols-xl="options.horizontalFieldLayoutEnabled && '5'"
Expand All @@ -43,7 +43,7 @@
<c-hint :tooltip="((field.options.hint || {}).view || '')" />

<div
v-if="!record.deletedAt && options.inlineRecordEditEnabled && isFieldEditable(field)"
v-if="!field.isParentField && !record.deletedAt && options.inlineRecordEditEnabled && isFieldEditable(field)"
class="inline-actions ml-1"
>
<b-button
Expand All @@ -70,7 +70,27 @@
</template>

<div
v-if="field.canReadRecordValue"
v-if="field.isParentField"
class="value align-self-center"
>
<template v-if="resolvedParentRecord">
<field-viewer
:field="field"
:record="resolvedParentRecord"
:module="getModuleByID(field.parentModuleID)"
:namespace="namespace"
:extra-options="options"
value-only
/>
</template>
<span
v-else
class="text-muted"
>&mdash;</span>
</div>

<div
v-else-if="field.canReadRecordValue"
class="value align-self-center"
>
<field-viewer
Expand Down Expand Up @@ -108,7 +128,7 @@
</template>
<script>
import { compose, NoID } from '@cortezaproject/corteza-js'
import { mapActions } from 'vuex'
import { mapActions, mapGetters } from 'vuex'
import axios from 'axios'
import base from './base'
import FieldViewer from 'corteza-webapp-compose/src/components/ModuleFields/Viewer'
Expand Down Expand Up @@ -141,6 +161,7 @@ export default {
return {
referenceRecord: undefined,
referenceModule: undefined,
resolvedParentRecord: null,
inlineEdit: {
fields: [],
recordIDs: [],
Expand All @@ -152,22 +173,74 @@ export default {
},

computed: {
...mapGetters({
getModuleByID: 'module/getByID',
}),

fields () {
if (!this.fieldModule) {
// No module, no fields
return []
}

if (!this.options.fields || this.options.fields.length === 0) {
// No fields defined in the options, show all (buy system)
return this.fieldModule.fields
}

// Show filtered & ordered list of fields
return this.fieldModule.filterFields(this.options.fields).map(f => {
const childFieldConfigs = (this.options.fields || []).filter(f => !f.isParentField)
const parentFieldConfigs = (this.options.fields || []).filter(f => f.isParentField)

let moduleFields = []
if (childFieldConfigs.length > 0) {
moduleFields = this.fieldModule.filterFields(childFieldConfigs)
} else {
moduleFields = this.fieldModule.fields
}

const configured = moduleFields.map(f => {
f.label = f.isSystem ? this.$t(`field:system.${f.name}`) : f.label || f.name
f.isParentField = false
return f
})

const parentModule = this.parentModule
const parentConfigured = parentFieldConfigs
.map(pf => {
const actualField = parentModule ? parentModule.fields.find(f => f.name === pf.originalName) : null
if (!actualField) return null
return {
...actualField,
isParentField: true,
parentModuleID: pf.parentModuleID,
originalName: pf.originalName,
label: actualField.label || actualField.name,
}
})
.filter(Boolean)

if (parentFieldConfigs.length > 0 && this.options.fields.length > 0) {
const allConfigured = [...configured, ...parentConfigured]
const orderedFields = []

this.options.fields.forEach(configField => {
const match = allConfigured.find(c => {
if (configField.isParentField) {
return c.isParentField && c.originalName === configField.originalName && c.parentModuleID === configField.parentModuleID
}
return !c.isParentField && (c.name === configField.name || c.fieldID === configField.name)
})
if (match) orderedFields.push(match)
})

allConfigured.forEach(c => {
if (!orderedFields.find(o => o === c)) {
orderedFields.push(c)
}
})

return orderedFields
}

return [...configured, ...parentConfigured]
},

fieldLayoutClass () {
Expand All @@ -188,6 +261,30 @@ export default {
return this.options.referenceField ? this.referenceRecord : this.record
},

parentFieldConfigs () {
if (!this.options.fields || !this.options.fields.length) {
return []
}
return this.options.fields.filter(f => f.isParentField && f.parentModuleID)
},

parentModule () {
if (!this.options.includeParentFields || !this.options.parentField) {
return null
}

const linkField = this.module.fields.find(f => f.name === this.options.parentField)
if (!linkField || linkField.kind !== 'Record' || !linkField.options || !linkField.options.moduleID) {
return null
}

return this.getModuleByID(linkField.options.moduleID) || null
},

parentLinkFieldName () {
return this.options.parentField || null
},

isProcessing () {
return this.loadingRecord || !this.fieldRecord || this.evaluating
},
Expand Down Expand Up @@ -230,6 +327,10 @@ export default {
if (this.options.referenceModuleID) {
this.fetchReferenceModule(this.options.referenceModuleID)
}

if (this.options.includeParentFields) {
this.fetchParentRecord()
}
},
},

Expand Down Expand Up @@ -376,10 +477,62 @@ export default {
setDefaultValues () {
this.referenceRecord = undefined
this.referenceModule = undefined
this.resolvedParentRecord = null
this.inlineEdit = {}
this.abortableRequests = []
},

async fetchParentRecord () {
if (!this.options.includeParentFields || !this.parentLinkFieldName || !this.parentFieldConfigs.length) {
return
}

if (!this.record || !this.record.recordID || this.record.recordID === NoID) {
return
}

const linkField = this.module.fields.find(f => f.name === this.parentLinkFieldName)
if (!linkField || linkField.kind !== 'Record' || !linkField.options || !linkField.options.moduleID) {
return
}

const parentModuleID = linkField.options.moduleID
const parentRecordID = this.record.values[this.parentLinkFieldName]

if (!parentRecordID || parentRecordID === '0') {
this.resolvedParentRecord = null
return
}

const parentModule = this.getModuleByID(parentModuleID)
if (!parentModule) return

try {
const { response } = this.$ComposeAPI.recordReadCancellable({
namespaceID: this.namespace.namespaceID,
moduleID: parentModuleID,
recordID: parentRecordID,
})

const record = await response()
this.resolvedParentRecord = new compose.Record(parentModule, { ...record })

const selectedParentFields = this.parentFieldConfigs
.map(pf => parentModule.fields.find(f => f.name === pf.originalName))
.filter(Boolean)

await Promise.all([
this.fetchRecords(this.namespace.namespaceID, selectedParentFields, [this.resolvedParentRecord]),
this.fetchUsers(selectedParentFields, [this.resolvedParentRecord]),
])
} catch (e) {
if (!axios.isCancel(e)) {
console.warn('[RecordBlock] Failed to fetch parent record:', e)
this.resolvedParentRecord = null
}
}
},

abortRequests () {
this.abortableRequests.forEach((cancel) => {
cancel()
Expand Down
Loading