espocrm
espocrm copied to clipboard
Extend personName with 'initials'
personName is a field type in EspoCRM. Suppose I'd like to extend my Contact's name field type 'personName' with 'initials' varchar.
I've tried doing that today and I got a long way. This is what I did: (please read 'initials' where I wrote 'letters')
I added a view to my name of entity Contact:
"name": {
"type": "nvkhName",
"view": "custom:views/contact/fields/nvkh-name"
},
Forget about the type 'nvkhName' for the moment.
So I created the view:
define('custom:views/contact/fields/nvkh-name', 'views/fields/person-name', function (Dep) {
return Dep.extend({
detailTemplate: 'custom:fields/nvkh-name/detail',
editTemplate: 'custom:fields/nvkh-name/edit',
editTemplateLastFirst: 'custom:fields/nvkh-name/edit-last-first',
editTemplateLastFirstMiddle: 'custom:fields/nvkh-name/edit-last-first-middle',
editTemplateFirstMiddleLast: 'custom:fields/nvkh-name/edit-first-middle-last',
data: function () {
this.inDataCalc = true;
var data = Dep.prototype.data.call(this);
this.inDataCalc = false;
if (this.model === 'edit') {
data.lettersMaxlength = this.model.getFieldParam(this.lettersField, 'maxLength');
}
data.lettersValue = this.model.get(this.lettersField);
data.valueIsSet = data.valueIsSet || this.model.has(this.lettersField);
data.isNotEmpty = data.isNotEmpty || !!data.lettersValue;
if (data.isNotEmpty && (this.mode === 'detail' || this.mode === 'list' || this.mode === 'listLink')) {
data.formattedValue = this.getFormattedValue();
}
return data;
},
setup: function () {
Dep.prototype.setup.call(this);
var ucName = Espo.Utils.upperCaseFirst(this.name)
this.lettersField = 'letters' + ucName;
},
afterRender: function () {
Dep.prototype.afterRender.call(this);
if (this.mode === 'edit') {
this.$letters = this.$el.find('[data-name="' + this.lettersField + '"]');
this.$letters.on('change', function () {
this.trigger('change');
}.bind(this));
}
},
getFormattedValue: function () {
if (this.inDataCalc) return '';
var format = this.getFormat();
var value = '';
if (format === 'firstLast') {
var salutation = this.model.get(this.salutationField);
var first = this.model.get(this.firstField);
var last = this.model.get(this.lastField);
var letters = this.model.get(this.lettersField);
if (salutation) {
salutation = this.getLanguage().translateOption(salutation, 'salutationName', this.model.entityType);
}
var open = '';
var close = '';
if (salutation) value += salutation;
if (letters) {
value += ' ' + letters;
open = '(';
close = ')';
}
if (first) value += ' ' + open + first + close;
if (last) value += ' ' + last;
value = value.trim();
console.log('first='+first+', last='+last+', vl='+letters);
console.log('value='+value);
} else {
value = Dep.prototype.getFormattedValue.call(this);
}
return value;
},
fetch: function (form) {
var data = Dep.prototype.fetch.call(this, form);
data[this.lettersField] = this.$letters.val().trim() || null;
return data;
},
});
});
Also not to difficult. However, this doesn't create the behaviour that I wanted. What it doesn't do is:
- Write the right name format "letters (first) last" in the list.
- Write the right name format when details are initially shown "letters (first) last".
Why? Because 'letters' do not get fetched. Strange... Oke. the definition in personName type doesn't contain 'letters' (./application/Espo/Resources/metadata/fields/personName.json).
So I looked in the code and found './application/Espo/Core/ORM/Helper.php'. It contains code to format a personName. And it is customizable, because it gets loaded and the custom loader gets in first. So I created:
custom/Espo/Custom/Core/Loaders/EntityManagerHelper.php:
namespace Espo\Custom\Core\Loaders;
class EntityManagerHelper extends \Espo\Core\Loaders\EntityManagerHelper
{
public function load()
{
return new \Espo\Custom\Core\ORM\NvkhHelper(
$this->getContainer()->get('config')
);
}
}
And a custom helper:
custom/Espo/Custom/Core/ORM/NvkhHelper.php
namespace Espo\Custom\Core\ORM;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
class NvkhHelper extends \Espo\Core\ORM\Helper
{
public function __construct(Config $config)
{
parent::__construct($config);
}
public function formatPersonName(Entity $entity, string $field)
{
if ($entity->getEntityType() === 'Contact') {
$first = $entity->get('first' . ucfirst($field));
$last = $entity->get('last' . ucfirst($field));
$letters = $entity->get('letters' . ucfirst($field));
if (!$first && !$last && !$letters) {
return null;
}
$arr = [];
if ($letters) $arr[] = $letters;
if ($first) {
if ($letters) $arr[] = "($first)";
else $arr[] = $first;
}
if ($last) $arr[] = $last;
return implode(' ', $arr);
} else {
return parent::formatPersonName($entity, $field);
}
}
}
Oke, now we're getting somewhere. When I click on a Contact, the details are shown. Initially with only firstname, lastname; but then a refresh is made and it shows 'letters (firstname) lastname'. Yes!
However, I still don't get the 'letters (firstname) lastname' entries in a list.
Oke. let's create a new type then. nvkhName. I created it and put it here: ./custom/Espo/Custom/Resources/metadata/fields/nvkhName.json. I added 'letters' as a type to this nvkhName.json file. It wasn't difficult to do.
Now, I refer to this nvkhName type. See json file on top of this issue. But then, the backend starts complaining with a code 500. Why?
After some searching I find "application/Espo/Core/Utils/Database/Orm/Base.php". It contains a function: protected function getForeignField(string $name, string $entityType)
And this actually has a hard reference to 'personType', which converts the 'name' field into a list of fields. When I add this code:
(...)
if (isset($foreignField['type']) && $foreignField['type'] == 'nvkhName') {
return [ 'letters' . ucfirst($name), ' ', 'first' . ucfirst($name), ' ', 'last' . ucfirst($name) ];
}
(...)
All starts working smoothly. Error 500 disappears. However, now I've made changes to the core of EspoCrm. This is no customization. This is a patch. I don't like that.
Any advice? What can I do in this case? I don't think I can customize this class...
After some thinking, I would like to propose a different implementation of the getForeignField()
function in application/Espo/Core/Utils/Database/Orm/Base.php
:
protected function getForeignField(string $name, string $entityType)
{
$foreignField = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $name]);
if (isset($foreignField['type'])) {
if (isset($foreignField['fieldOrder'])) {
$fieldDefs = $foreignField['fieldOrder'];
} else {
$foreignType = $foreignField['type'];
$personNameFormat = $this->config->get('personNameFormat');
$fieldsMeta = $this->getMetadata()->get('fields');
if (isset($fieldsMeta[$foreignType])) {
$fieldMeta = $fieldsMeta[$foreignType];
if (isset($fieldMeta['fieldOrders'])) {
$fieldOrders = $fieldMeta['fieldOrders'];
if (isset($fieldOrders[$personNameFormat])) {
$fieldDefs = $fieldOrders[$personNameFormat];
} else if (isset($fieldOrders['default'])) {
$fieldDefs = $fieldOrders['default'];
}
}
}
}
if ($fieldDefs) {
$retval = array();
$space = '';
foreach($fieldDefs as $field) {
if ($space != '') { array_push($retval, $space); }
array_push($retval, $field . ucfirst($name));
$space = ' ';
}
return $retval;
}
}
return $name;
}
So instead of hard-coding personName as a type in getForeignField, I'm proposing to solve this in the metadata of fields. In this metadata I added a section 'fieldOrders':
(...)
"fieldOrders": {
"default": [ "letters", "first", "last" ],
"lastFirst": [ "last", "letters", "first" ],
"lastFirstMiddle": [ "last", "letters", "first", "middle" ],
"firstMiddleLast": [ "letters", "first", "middle", "last" ],
"firstLast": [ "letters", "first", "last" ]
}
(...)
And in the entity definitions, I'm proposing a json field for a type, e.g. my Contact.json, which overrides all "fieldOrders" of the field itself:
(...)
"fields": {
"name": {
"type": "nvkhName",
"view": "custom:views/contact/fields/nvkh-name",
"fieldOrder": [ "letters", "first", "last" ]
},
(...)
So, I created a new PersonName that can be used with Entities. It is the IpersonName. It not only has firstName, lastName, middleName; but also 'initialsName'. To use it, one would create an entity derived from Person (e.g. Contact); extend the entity with 'initialsName' and put the field type for 'name' in the metadata of the entity (e.g. Contact.json). "type" = "ipersonName"
.
I made following changes to EspoCRM:
- application/Espo/Core/Utils/Database/Orm/Fields/IpersonName.php
- application/Espo/Resources/metadata/fields/ipersonName.json
- Templates in client/res/templates/fields/iperson-name
- View in client/src/views/fields/iperson-name.js
- Changed the personName Helper functions: application/Espo/Core/Loaders/EntityManagerHelper.php and application/Espo/Core/ORM/Helper.php
- Changed the 'getForeignField()' function in application/Espo/Core/Utils/Database/Orm/Base.php
And now it works.
The only thing that doesn't work yet, is when I update the initials in the the editing form.
When I go back to the list, the initials aren't updated in the list of Contacts.
Any idea's?
I got it to work without modifying any of the original Espo scripts.
I created a custom "PersonPlus" entity extended from "Person" that has an additional "Mother Maiden Name" field (this is needed for many Spanish speaking countries) and the required custom field definitions and front end scripts and templates.
Almost everything is encapsulated in a "PersonPlus" module namespace (because I don't like dumping every customization in a single "custom" namespace) except the custom loader which at least until I find a workaround, requires to exist in the Custom namespace.
To apply it to "Contact" all you need to do is to create a custom "Contact" entity and extend from "PersonPlus" instead of from "Person" then add a varchar field "motherMaidenName".
I am planning to package it as a free plug in but you are welcome to check the code and adapt for your needs here: https://github.com/telecastg/person-plus-for-espocrm
Thank you for telling this.
To apply it to "Contact" all you need to do is to create a custom "Contact" entity and extend from "PersonPlus" instead of from "Person" then add a varchar field "motherMaidenName".
I'm not sure what you mean by this. I somehow don't get my IPerson entity working. Problems with ORDER BY...
See comment below.
I got it to work without modifying any of the original Espo scripts.
I implemented your solution, but See issue telecastg/person-plus-for-espocrm#3 and issue telecastg/person-plus-for-espocrm#4.
Either you have no use for Contacts as foreignEntities in other Entities, or I'm missing something here.
I'm creating a new issue with a change request for Base.php#L225
I'm still missing something...
When I use a normal list view, I see:
In a list of related contacts, I see:
Also, in the details view I see:
Which is all good.
However, when using a many-to-one relation, I get to see the foreign field for the Contact. And than, I see:
No Brackets around the firstName field.
Also, when getting a list of related contacts in a detail view, I get no brackets:
Also, the entity helper hook doesn't get called. What I see, is that
public function getForeignField(string $fieldName, string $entityType)
{
$format = $this->config->get('personNameFormat');
return $this->getFields($format, $fieldName);
}
gets called (see #1751, the change request for getForeignField).
So, somehow, the Contacts with type IPersonName, doesn't get formatted when the getForeignField() function is used.
Am I missing something?
As if v7.4, a custom field converter can be used instead of Espo\Core\Utils\Database\Orm\FieldConverters\PersonName
.