Announcement

Collapse
No announcement yet.

custom template for multiple fields?

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • custom template for multiple fields?

    Is it possible to create a custom template like the 'person-name' templates/js files?
    Or to customize the person-name template? E.g. to add first-letters and/or title?

    How would one do this and implement it in a custom resource?

    Thanks in advance for answers


  • #2
    Hello,

    Yes, it is possible to create a custom "person-name" field and customize its templates, although since "person-name" is really a combination of other fields and not a standard field which can be invoked directly from an entity's entityDefs metadata settings file, it involves some coding, basic knowledge of Backbone views, Handlebars templates and getting familiar with Espo's architecture.

    If this is OK with you, please follow the instructions below to modify this field for Contacts. Please note that I have not tested fully these modifications because we don't use them in our application, but they are based on other changes that we have made and should work fine.

    If you prefer a ready made solution you might want to hire a professional Espo developer, they are listed in this link: https://forum.espocrm.com/forum/job-.../find-customer

    Best wishes and welcome to this forum

    STEPS

    1) Create custom templates, modifying the content of the original templates:

    a) client/custom/res/templates/fields/person-name/detail.tpl (based on the content of 'client/res/templates/fields/person-name/detail.tpl)
    b) client/custom/res/templates/fields/person-name/edit.tpl (based on the content of 'client/res/templates/fields/person-name/edit.tpl)
    c) client/custom/res/templates/fields/person-name/edit-last-first.tpl (based on the content of 'client/res/templates/fields/person-name/edit-last-first.tpl)
    d) client/custom/res/templates/fields/person-name/edit-last-first-middle.tpl (based on the content of 'client/res/templates/fields/person-name/edit-last-first-middle.tpl)
    e) client/custom/res/templates/fields/person-name/edit-first-middle-last.tpl (based on the content of 'client/res/templates/fields/person-name/edit-first-middle-last.tpl)

    2) create script client/custom/src/views/fields/my-person-name.js extending from the core 'views/fields/person-name' script as follows:
    Code:
    define('custom:views/fields/my-person-name', 'views/fields/person-name', function (Dep) {
        return Dep.extend({
    
            // call your custom templates here:
    
            detailTemplate: 'custom:fields/person-name/detail',
    
            editTemplate: 'custom:fields/person-name/edit',
    
            editTemplateLastFirst: 'custom:fields/person-name/edit-last-first',
    
            editTemplateLastFirstMiddle: 'custom:fields/person-name/edit-last-first-middle',
    
            editTemplateFirstMiddleLast: 'custom:fields/person-name/edit-first-middle-last',
    
            // if you added new placeholders (for example, if you added a 'title' field), specify their values inside the data function
            data: function () {
                // call the protoype 'data' function
                var data = Dep.prototype.data.call(this);
                    // set the value for the 'title' placeholder in the templates
                    data.title = {{ specify where the value for 'title' comes from (see the prototype script to see how this can be done)}}    
            }
        });
    });
    3) Now you need to let Espo know that you want to use the custom "my-person-name" instead of the core "person-name" field, for this you need to create the following custom scripts:

    client/custom/src/views/contact/fields/name-for-account.js
    Code:
    define('custom:views/contact/fields/name-for-account', 'custom:views/fields/my-person-name', function (Dep) {
    
        return Dep.extend({
    
            afterRender: function () {
                Dep.prototype.afterRender.call(this);
                if (this.mode === 'listLink') {
                    if (this.model.get('accountIsInactive')) {
                        this.$el.find('a').css('text-decoration', 'line-through');
                    };
                }
            },
    
            getAttributeList: function () {
                return ['name', 'accountIsInactive'];
            }
    
        });
    });
    custom/Espo/Custom/Resources/layouts/Contact/myListForAccount.json
    Code:
    [
        {"name":"name", "width": 35,"link":"true", "view": "custom:views/contact/fields/name-for-account"},
        {"name":"accountRole", "width": 35, "notSortable": true},
        {"name":"emailAddress"}
    ]
    custom/Espo/Custom/Resources/metadata/entitydefs/Account.json
    Code:
    {
        "relationshipPanels": {
            "contacts": {
            "filterList": ["all", "accountActive"],
            "layout":"myListForAccount",
            "orderBy": "name"
        },
        "opportunities":{
            "layout":"myListForAccount"
        }
    }
    custom/Espo/Custom/Resources/metadata/clientDefs/Contact.json
    Code:
    {
        "additionalLayouts": {
            "detailConvert": {
                "type": "detail"
            },
            "myListForAccount": {
                "type": "listSmall"
            }
        }
    }
    custom/Espo/Custom/Resources/metadata/clientDefs/Opportunity.json
    Code:
    {
        "additionalLayouts": {
            "detailConvert": {
                "type": "detail"
            },
            "myListForAccount": {
                "type": "listSmall"
            },
            "listForContact": {
                "type": "listSmall"
            }
        }
    }
    custom/Espo/Custom/Resources/metadata/entityDefs/Contact.json
    Code:
    {
        "fields" : {
            "accountRole": {
                "type": "varchar",
                "notStorable": true,
                "directUpdateDisabled": true,
                "layoutDetailDisabled": true,
                "layoutMassUpdateDisabled": true,
                "layoutFiltersDisabled": true,
                "layoutAvailabilityList": ["myListForAccount"],
                "exportDisabled": true,
                "importDisabled": true,
                "view": "crm:views/contact/fields/account-role",
                "customizationOptionsDisabled": true,
                "textFilterDisabled": true
            }
        }
    }
    Last edited by telecastg; 05-18-2020, 07:47 PM.

    Comment


    • #3
      Thank you very much for your instructions!

      However, I'm a littlebit lost.

      I followed the documentation of EspoCRM (https://docs.espocrm.com/development/custom-views/).

      And I got this working, almost... What I did:

      1. I took the template for person-name and changed it:

      Code:
      define('custom:views/contact/fields/nvkh-name', 'views/fields/varchar', function (Dep) {
      
      return Dep.extend({
      
         type: 'personName',
      
         detailTemplate: 'custom:fields/person-name/detail',
      
         editTemplate: 'custom:fields/person-name/edit',
      
         editTemplateLastFirst: 'custom:fields/person-name/edit-last-first',
      
         editTemplateLastFirstMiddle: 'custom:fields/person-name/edit-last-first-middle',
      
         editTemplateFirstMiddleLast: 'custom:fields/person-name/edit-first-middle-last',
      
         data: function () {
            var data = Dep.prototype.data.call(this);
            data.ucName = Espo.Utils.upperCaseFirst(this.name);
            data.salutationValue = this.model.get(this.salutationField);
            data.firstValue = this.model.get(this.firstField);
            data.lastValue = this.model.get(this.lastField);
            data.voorlettersValue = this.model.get(this.voorlettersField);
            data.middleValue = this.model.get(this.middleField);
            data.salutationOptions = this.model.getFieldParam(this.salutationField, 'options');
      
           i f (this.model === 'edit') {
              data.firstMaxLength = this.model.getFieldParam(this.firstField, 'maxLength');
              data.lastMaxLength = this.model.getFieldParam(this.lastField, 'maxLength');
              data.middleMaxLength = this.model.getFieldParam(this.middleField, 'maxLength');
              data.voorlettersMaxLength = this.model.getFieldParam(this.voorlettersField, 'maxLength');
            }
      
            data.valueIsSet = this.model.has(this.firstField) || this.model.has(this.lastField);
            if (this.mode === 'detail') {
                data.isNotEmpty = !!data.firstValue || !!data.lastValue || !!data.salutationValue || !!data.middleValue || !!data.voorlettersValue;
            } else if (this.mode === 'list' || this.mode === 'listLink') {
                data.isNotEmpty = !!data.firstValue || !!data.lastValue || !!data.middleValue || !!data.voorlettersValue;
            }
      
            if (data.isNotEmpty && (this.mode === 'detail' || this.mode === 'list' || this.mode === 'listLink')) {
                console.log('getting formatted value for:' + this.mode);
                data.formattedValue = this.getFormattedValue();
            }
      
            return data;
        },
      
      
         setup: function () {
            Dep.prototype.setup.call(this);
            var ucName = Espo.Utils.upperCaseFirst(this.name)
            this.salutationField = 'salutation' + ucName;
            this.firstField = 'first' + ucName;
            this.lastField = 'last' + ucName;
            this.middleField = 'middle' + ucName;
            this.voorlettersField = 'voorletters';
         },
      
      
         afterRender: function () {
            Dep.prototype.afterRender.call(this);
            if (this.mode == 'edit') {
               this.$salutation = this.$el.find('[data-name="' + this.salutationField + '"]');
               this.$first = this.$el.find('[data-name="' + this.firstField + '"]');
               this.$last = this.$el.find('[data-name="' + this.lastField + '"]');
               this.$voorletters = this.$el.find('[data-name="' + this.voorlettersField + '"]');
      
               if (this.formatHasMiddle()) {
                  this.$middle = this.$el.find('[data-name="' + this.middleField + '"]');
               }
      
               this.$salutation.on('change', function () {
                  this.trigger('change');
               }.bind(this));
               this.$first.on('change', function () {
                  this.trigger('change');
               }.bind(this));
               this.$last.on('change', function () {
                  this.trigger('change');
               }.bind(this));
              this.$voorletters.on('change', function() {
                  this.trigger('change');
               }.bind(this));
            }
         },
      
         getFormattedValue: function () {
            var salutation = this.model.get(this.salutationField);
            var first = this.model.get(this.firstField);
            var last = this.model.get(this.lastField);
            var middle = this.model.get(this.middleField);
            var voorletters = this.model.get(this.voorlettersField);
      
            if (salutation) {
            salutation = this.getLanguage().translateOption(salutation, 'salutationName', this.model.entityType);
            }
      
            var value = '';
      
            var format = this.getFormat();
            console.log('formatting for: ' + format);
            console.log('vl = ' + voorletters + ', first=' + first + ', middle=' + middle);
      
            switch (format) {
               (...)
               default:
                  if (salutation) value += salutation;
                 if (voorletters) {
                     value += ' ' + voorletters + ' (';
                  } else {
                     value += ' ';
                  }
                  if (first) value += first;
                 if (voorletters) {
                     value += ')';
                  }
                  if (last) value += ' ' + last;
            }
      
            value = value.trim();
      
            return value;
         },
      
      (...)
      
         fetch: function (form) {
            var data = {};
            data[this.salutationField] = this.$salutation.val() || null;
            data[this.firstField] = this.$first.val().trim() || null;
            data[this.lastField] = this.$last.val().trim() || null;
           data[this.voorlettersField] = this.$voorletters.val().trim() || null;
      
            if (this.formatHasMiddle()) {
            data[this.middleField] = this.$middle.val().trim() || null;
         }
      
         return data;
         },
      });
      });
      I left out some stuff, but it gives the idea I think. I changed the templates.
      Next I set the custom view for the type in Contact.json:

      Code:
         (...)
         "fields": {
            "name": {
               "view": "custom:views/contact/fields/nvkh-name"
            },
            "agbcode": {
          (...)
      And it starts to work, i.e. for the editing part. For the detail-view some of the time. For list views none of the time. For Small Detail views none of the time.

      What I see on the javascript console is this:

      For lists of contacts:
      Code:
      getting formatted value for:listLink
      VM10125:129 formatting for: firstLast
      VM10125:130 vl = undefined, first=Jiska, middle=null
      VM10125:71 getting formatted value for:listLink
      VM10125:129 formatting for: firstLast
      VM10125:130 vl = undefined, first=Hans, middle=H.N.M.
      VM10125:71 getting formatted value for:listLink
      VM10125:129 formatting for: firstLast
      VM10125:130 vl = undefined, first=Liesbeth, middle=null
      VM10125:71 getting formatted value for:listLink
      VM10125:129 formatting for: firstLast
      VM10125:130 vl = undefined, first=Joop, middle=null
      For detailed view:
      Code:
      getting formatted value for:detail
      VM10125:129 formatting for: firstLast
      VM10125:130 vl = H.N.M., first=Hans, middle=H.N.M.
      And sometimes for detailed view:
      Code:
      getting formatted value for:detail
      VM10750:129 formatting for: firstLast
      VM10750:130 vl = undefined, first=Hans, middle=H.N.M.
      (should be vl = H.N.M.)

      Mostly for small detailed views:
      Code:
      getting formatted value for:detail
      VM10750:129 formatting for: firstLast
      VM10750:130 vl = undefined, first=Liesbeth, middle=null
      (should be vl = E.P.)


      So, somehow, the data is not completely available in the model for lists and not always for details.

      Should I point out somehow to EspoCRM that in order to get the complete fields for Record Name, it should also fetch 'voorletters' always?



      Thanks in advance.

      Comment


      • #4
        It looks like:

        ./application/Espo/Core/Utils/Database/Orm/Fields/PersonName.php

        may be involved in getting the right fields..

        Can I also customize this php class?

        Or need I alter the file itself?

        Comment


        • #5
          Hi,

          I believe the issue is that you need to define the nvkh-name custom field view by extending the existing person-name field view to inherit all its methods and properties, not by extending the basic varchar field, and this is why you are getting the "undefined" errors.

          As I mentioned I didn't fully test this implementation because we don't use it in our system but it should work based on other implementations that we have made.

          Try changing: define('custom:views/contact/fields/nvkh-name', 'views/fields/varchar', function (Dep) {
          To: define('custom:views/contact/fields/nvkh-name', 'views/fields/person-name', function (Dep) {
          in the client/custom/src/views/fields/nvkh-name.js script
          Last edited by telecastg; 05-22-2020, 04:23 PM.

          Comment


          • #6
            I'm sorry, I didn't update the code above. I was already a step further. My nvkh-name.js view now looks as follows.

            Code:
            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;
            
                     data.voorlettersValue = this.model.get(this.voorlettersField);
            
                     data.valueIsSet = data.valueIsSet || this.model.has(this.voorlettersFiel);
            
                     data.isNotEmpty = data.isNotEmpty || !!data.voorlettersValue;
            
                     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);
                  this.voorlettersField = 'voorletters';
               },
            
               afterRender: function () {
                  Dep.prototype.afterRender.call(this);
            
                  if (this.mode === 'edit') {
                     this.$voorletters = this.$el.find('[data-name="' + this.voorlettersField + '"]');
            
                     this.$voorletters.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 voorletters = this.model.get(this.voorlettersField);
            
                     if (salutation) {
                        salutation = this.getLanguage().translateOption(salutation, 'salutationName', this.model.entityType);
                     }
            
                     var open = '';
                     var close = '';
            
                     if (salutation) value += salutation;
                     if (voorletters) {
                        value += ' ' + voorletters;
                        open = '(';
                        close = ')';
                     }
                     if (first) value += ' ' + open + first + close;
                     if (last) value += ' ' + last;
            
                     value = value.trim();
            
                     console.log('first='+first+', last='+last+', vl='+voorletters);
                     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.voorlettersField] = this.$voorletters.val().trim() || null;
                  return data;
               },
            
               });
            });
            What I see happening, is that when displaying a list, this field-view 'nvkh-name.js' gets called, however, the 'voorletters' field seems not to part of the model, i.e. it looks like it has not been fetched from the backend at that moment.

            Listing the contacts, I see this on the console:

            Code:
            first=Jiska, last=Snul, vl=undefined
            VM6105:113 value=Mevr. Jiska Snul
            VM6105:112 first=Hans, last=Dijkema, vl=undefined
            VM6105:113 value=Ir. Hans Dijkema
            VM6105:112 first=Liesbeth, last=Kruyswijk, vl=undefined
            VM6105:113 value=Mevr. Liesbeth Kruyswijk
            VM6105:112 first=Joop, last=Knal, vl=undefined
            VM6105:113 value=Dhr. Joop Knal
            When viewing a contact for the first time (details), I see this on the console:

            Code:
            first=Jiska, last=Snul, vl=undefined
            VM6105:113 value=Mevr. Jiska Snul
            Now, I start to edit this Contact, and I see nothing, so probably the attributes of the Contact entry are fetched in the model.
            However, when I save the edit form, or I do an update, or even a cancel of the 'field-edit',
            I see this on the console:

            Code:
            first=Jiska, last=Snul, vl=J.D.
            VM6105:113 value=Mevr. J.D. (Jiska) Snul
            Probably all fields have been fetched from the backend, and are available in the model, and the right Detail display is indeed there.

            Now, strangely, in the list view, after I've edited some contacts, I see following:
            Code:
            first=Jiska, last=Snul, vl=J.D.
            VM6105:113 value=Mevr. J.D. (Jiska) Snul
            VM6105:112 first=Hans, last=Dijkema, vl=H.N.M.
            VM6105:113 value=Ir. H.N.M. (Hans) Dijkema
            VM6105:112 first=Liesbeth, last=Kruyswijk, vl=E.P.
            VM6105:113 value=Mevr. E.P. (Liesbeth) Kruyswijk
            VM6105:112 first=Joop, last=Knal, vl=undefined
            VM6105:113 value=Dhr. Joop Knal
            So, the name field gets formatted by nvkh-name.js; however, the list doesn't show it like that, it shows it with template 'firstname lastname'.

            Why would the formatting of this field view 'nvkh-name.js' be called and not shown?

            Again, thank you for any information.

            Comment


            • #7
              It looks like

              ./application/Espo/Core/ORM/Helper.php

              plays a role with getting data from the backend.
              It is a Helper in the entityManager and gets loaded, either customized or not customized.

              I customized this Helper this way:


              custom/Espo/Custom/Core/Loaders/EntityManagerHelper.php
              Code:
              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 added my own Helper as follows:

              custom/Espo/Custom/Core/ORM/NvkhHelper.php
              Code:
              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);
                    }
              
                 }
              }
              This solves some of the issues, although the detail view still fetches only the firstname and the lastname when initially created, an update almost immediately thereafter is done and fetches the name format from the Helper class.

              When I click on the name in the list, the name also gets updated.

              However, the initial formatting still is <firstName> <lastName>.

              Last edited by hans@dykema.nl; 05-22-2020, 07:33 PM.

              Comment


              • #8
                I don't understand why it takes an event (for example click on the list display or click the 'edit' button) for the 'voorletters' field to be fetched. As I mentioned previously I had not dealt with the personName type of field before.

                Not sure if someone else could assist, perhaps Maximus , item can help. I would love to learn more about this implementation and will do some more research on my own.

                In the meantime, see if the model includes the 'voorletters' value at the 'setup' function in nvkh-name.js typing console.log("this.model = ".this.model);

                If it doesn't, try to force a fresh fetch ( this.model.fetch() ) and run another console.log to see if the model is fetched correctly now.

                One quick and dirty solution that comes to mind too, could be to change the "name" field type at Contact from 'personName' to 'varchar' and use formula to create the "name" field in Contact by manually concatenating the firstName+(+voorletters+)+lastName values but that would be forgoing the benefits of the personName implementation
                Last edited by telecastg; 05-23-2020, 04:48 PM.

                Comment


                • #9
                  I finally got it to work !

                  The solution is implemented as a custom "PersonPlus" entity type that has the ability to include Mother Maiden name in the "Person" type entities, like Contact, Lead, etc which is preferred at least in many Spanish speaking countries.

                  I am working on releasing it as a free plug-in but in the meantime feel free to check the scripts here and adapt for a custom "initials" field:
                  https://github.com/telecastg/person-plus-for-espocrm

                  I also posted the answer in the Espo Issues repository. https://github.com/espocrm/espocrm/issues/1719
                  Last edited by telecastg; 05-26-2020, 08:58 PM.

                  Comment


                  • espcrm
                    espcrm commented
                    Editing a comment
                    Installing. Wait nevermind, I bookmark it for future since it look like need to manual install and do some editing.
                Working...
                X