SuiteCRM 8 Extensions

Hey there,

I am trying to develop a simple extension that would add a button to Accounts/detail view, that would call, upon being clicked, an external API on the backend. I was trying to follow Developer Guide here on your website and also SugarCrm website, but to no luck. I even tried a simple example from your website Metadata :: SuiteCRM Documentation, but that did not work for me either ? The suiteCRM 8 folder structure is also quite different to the suiteCRM 7 folder structure, are you supposed to place these extensions somewhere else ?

Thanks

2 Likes

Hi @rumburak

Yes, the SuiteCRM 8 folder structure has been modified quite a bit. Now, you need to place in the extensions for adding a button in the detail view in the following path.

public<legacy<modules<accounts<view<detail view

Step: 1

image

Step: 2

image (1)

And so on…

So am I supposed to directly modify the detailviewdefs.php ? No custom/extensions folder like here Adding Buttons to the Record View - SugarCRM Support Site ?

SuiteCRM structure has changed abit from 7 and 8 but even more so from Suga. Don’t rely on SugarCRM documentation regarding to front end extension especially if it’s Sugar 7+

The aspect of adding buttons to the view defs hasn’t changed too much from 7 to 8. Lets review what you’ve got.

If you don’t have a file in legacy/custom/modules/Accounts/metadata then you can copy one that exists already in the modules folder: legacy/modules/Accounts/metadata/detailviewdefs

In that file (copied to the custom folder if not already there) you need to add a button to the templateMeta => form => buttons section of the array.

There you can add an addition button.

This post has some good tutorials to help, you may need to change the pathnames since you’ll be looking into legacy/ etc

Thank you for the answer @samus-aran, I copied the file from legacy/modules/Accounts/metadata/detailviewdefs.php and put it into my local folder custom/modules/Accounts/metadata/detailviewdefs.php and I added a simple alert button, that was copied from the forum here Add button to Detail View of Module. Afterward the files got copied to public/legacy/custom (I am using Docker, but I checked and the file is definitely there, see screenshot).


Then, I did the “Quick repair and repair” and went to a random account’s detail, but I don’t see any new button in the actions
Is there something I am missing ?

Bellow is the added button section in detailviedefs.php

          3 => 'FIND_DUPLICATES',
          4 => array (
            'customCode' => '<input type="submit" class="button" title="genFile" onclick="alert(\'Bean ID: \' + \'{$bean->id}\');" name="genFile" value="genFile" />',
            'sugar_html' => 
            array (
              'type' => 'submit',
              'value' => 'genFile',
              'htmlOptions' => 
              array (
                'class' => 'button',
                'id' => 'genFile_button',
                'title' => 'genFile',
                'onclick' => 'alert(\'Bean ID: \' + \'{$bean->id}\');',
                'name' => 'genFile',
              ),
            ),
          ),
          'AOS_GENLET' =>

And if interested, this is how/where the local custom folder is copied in Docker
COPY custom/ /var/www/html/public/legacy/custom/

Hi @rumburak,

Welcome to the community :wave:! and thanks for trying out SuiteCRM 8.

The documentation on how to do this on SuiteCRM 8 has not been added yet.

However, there is already one example of something similar in the core app. Here are some steps to look into.

Note: To do this customization you’ll need to understand how Symfony dependency injection works.

Make the button appear on the front end

Note the following example comes from core but you should add it to the public/legacy/custom/<module>/metadata

To add the button you need to add a new entry to the new recordActions in detailviewdefs.

Next you can see an example that already exists on core

From the example there are important things to having in mind:

  • the key (print-as-pdf) should be a unique name, as it will also be used to identify the process that is going to handle that action.
  • Make sure to set 'asyncProcess' => true, as you want a process that is handled in the backend. By the way by asyncProcess, it means a process that is handled in the backend.
  • the modes defines if the button should be displayed in detail and edit or just in one.
  • You can remove the params

Example: public/legacy/modules/Accounts/metadata/detailviewdefs.php

      'recordActions' => [
          'actions' => [
              'print-as-pdf' => [
                  'key' => 'print-as-pdf',
                  'labelKey' => 'LBL_PRINT_AS_PDF',
                  'asyncProcess' => true,
                  'modes' => ['detail'],
                  'acl' => ['view'],
                  'aclModule' => 'AOS_PDF_Templates',
                  'params' => [
                      'selectModal' => [
                          'module' => 'AOS_PDF_Templates'
                      ]
                  ]
              ]
          ]
      ],

Add the service to handle the backend

Add your service to something like:

  • for services used in multiple modules
    • extensions/<extension-name>/backend/<domain>/Services
      • domain is the scope of the change you want to do ex: its languages, statistics,etc
  • for services that are module specific
    • extensions/<extension-name>/modules/<Module>/Services

You can find an example of an action in core/backend/Process/Service/RecordActions/PrintAsPdfAction.php

One thing to have in mind is that the namespace must match the path were you added the file, and its case sensitive. If you add a file to extensions/myExt/modules/Accounts/Services/AccountSpecialPdfPrintAction.php the namespace need to be something like

namespace App\Extension\myext\module\Accounts\Service;

Hope this helps

1 Like

Thank you very much @clemente.raposo, very helpful post! The button was actually added, so I am very thankful. However, the rest is still not working.

I added the button to recordActions like so:

'send-through-api' => [
    'key' => 'send-through-api',
    'labelKey' => 'LBL_SEND_THROUGH_API',
    'asyncProcess' => true,
    'modes' => ['detail']
]

I removed params like you said and did not add acl nor aclModule because, you did not mention them and I have no idea what they do.

The button appeared under action, but there is no text, I assume that is because I need to add the label LBL_SEND_THROUGH_API somewhere ? Most likely, need to copy Accounts/language/en_us.php and add it at the very end ? Is that right ? If so where does this need to be copied to ?

Next problem, is the action, I have a module specific action, so I should follow extensions/<extension-name>/modules/<Module>/Services, I created folder extensions locally and created folder structure like so: extensions/myExt/modules/Accounts/Service/SendThroughApi.php.

What I am not sure about is, should it be .../Accounts/Services or .../Accounts/Service ? (or should there also be RecordActions after the Service(s) ? And where should this folder be copied to, just /extensions in root or somewhere to public/legacy/... ?

Also the namespace, that I should use, should it be namespace App\Extension\myext\Accounts\Service ? With myext in lowercase or myExt ? And should it end with Serivce or Services ? (the “s” letter seems to come and go so I am not sure)

I assumed the run function is the main function and put my code there.

Hi @rumburak,

Glad to hear you were able to make some progress.

Regarding the path where to place the it “doesn’t really matter”, it will be autowired by symfony. Its more a matter of convention and having a standard, to make the code easier to find and maintain. The only rules that need to be followed are:

  • Add it to extensions/<your-extension>
  • The namespace needs to start with App\Extension\
  • The namespace needs to follow the psr rules. as I explained before.
  • Your class needs to ProcessHandlerInterface
  • getProcessType() on your class needs to return the same value that you set on the key, in your case send-through-api. (EDIT: this should be prefixed with record-, so it should be record-send-through-api)

In your case if the class is in extensions/myExt/modules/Accounts/Service/SendThroughApi.php.

the namespace should be namespace App\Extension\myExt\modules\Accounts\Service;

Regarding the method yes, it should implement all in the interface. and the one that runs the code is run

Regarding acl and aclModule are just need if you want to hide/show according to acls

1 Like

Thank you for keeping up with me @clemente.raposo. I tried to follow your advice precisely, but I still get a toast “Unexpected error when calling action”.

This is my SendThroughApi.php file (copied from PrintAsPdfAction.php):

<?php
namespace App\Extension\myExt\modules\Accounts\Service;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use App\Process\Entity\Process;
use App\Module\Service\ModuleNameMapperInterface;
use App\Process\Service\ProcessHandlerInterface;

class SendThroughApi implements ProcessHandlerInterface
{
    protected const MSG_OPTIONS_NOT_FOUND = 'Process options is not defined';
    protected const PROCESS_TYPE = 'send-through-api';

    private $moduleNameMapper;

    public function __construct(ModuleNameMapperInterface $moduleNameMapper) {
        $this->moduleNameMapper = $moduleNameMapper;
    }

    public function getProcessType(): string {
        return self::PROCESS_TYPE;
    }

    public function requiredAuthRole(): string {
        return 'ROLE_USER';
    }

    public function configure(Process $process): void {
        //This process is synchronous
        //We aren't going to store a record on db
        //thus we will use process type as the id
        $process->setId(self::PROCESS_TYPE);
        $process->setAsync(false);
    }

    public function validate(Process $process): void {
        // ...
    }

    public function run(Process $process) {
        // ...
    }
}

And it’s located in /extensions/myExt/modules/Accounts/Service/.

I still can’t see the button label, but that’s not as important right now.

Hi @rumburak,

Regarding the issues you’ve mentioned:

Creating a process

Sorry forgot to mention one thing. You need to run ./bin/console cache:clear or in alternative delete the suite 8 cache folder.

Anyway, if that still does not work, could you try the following please?

  • change APP_ENV=prod to APP_ENV=qa in the .env file
  • Open you browser’s dev tools in the network tab
  • Go to the accounts module detail view
  • Clear the network tab
  • Click on the button to trigger the action
  • Check the network tab. It should have a graphql call. if you check the request that is sent it should have CreateProcess and the value of type should be the same as the key you configured on the recordAction
  • If there is an error, on the response for that request you should have an error entry and a stack trace with more details

The request and response look something similar to:

image

Adding a label

At the legacy way of adding labels still applies.
So in your case you be adding something something like:

public/legacy/custom/Extension/modules/Accounts/Ext/Language/en_us.<some-meaningfull-name>.php

with contents like

<?php

$mod_strings['LBL_SEND_THROUGH_API'] = '<your-label>';

After adding:

  • Run Repair and Rebuild
  • Refresh your browser

Hope this helps

@clemente.raposo feels like we are slowly writing a SuiteCRM 8 documentation :smiley: Anyway, seems like the cache:clear command did something.

However, I had to rename my PROCESS_TYPE (constant that is being returned on getProcessType() from 'send-through-api' to 'record-send-through-api', because otherwise I was getting an error in grapql "Process is not defined".

After doing that, it seems like it gets even to the run() function!
At first I had a very simple run function like so:

public function run(Process $process) {
    $process->setStatus('success');
    $process->setMessages([]);
    $process->setData([]);
}

However, I am still getting a toast saying "Unexpected error when calling action", even though I don’t do anything else in the code. (The graphql response does not show any error either, it even says "status": "success")

Next problem is BeanFactory, I need the account’s data, so I have a function (I figured out $options['id'] is the account’s id):

protected function callApi(array $options) {
    $accountId = $options['id'];

    $bean = BeanFactory::getBean('Accounts', $accountId);
}

But I get an error, saying "Class BeanFactory not found" (I have use BeanFactory at the very top)

Lastly, I was not sucessful with the label either :confused: I have a file:

<?php

$mod_strings['LBL_SEND_THROUGH_API'] = 'Send through API';

in custom/Extension/modules/Accounts/Ext/Language/en_us.lang.php (and I am copying it to public/legacy/custom/Extension/modules/Accounts/Ext/Language)

I cleared cache, run repair & rebuild.

(this is how the action without label looks like)
image

1 Like

Hi @rumburak,

:grin: yeah seems so.

So regarding the questions above.

If the graphql calls returns “status”: “success”. I’m not sure why you are getting the “Unexpected error when calling action”. Could there be another call that is failing?

Calling legacy

To call legacy from suite 8 side you need to use a “Legacy Handler”. In this case you need to convert your service into a legacy handler.
In order to do that you need to extend LegacyHandler and implement the interface or the abstract methods.

One thing to note with legacy handlers: You need to call init() and close() and call all the legacy code within those calls.
You can look at the following as an example : core/backend/Process/LegacyHandler/LinkRelationHandler.php

Label

Hm… strange. Lets try adding to AppStrings then. The following guide explains how to do:

Alright, we are almost there @clemente.raposo ! :smiley:

  1. Status
    For some reason, I think I might have been getting the errors, because I tried logging out some things and suiteCrm didn’t like it ? I tried logging to terminal, and also via logger, but it felt like any sort of logging caused an error, so I am not sure what is the right way to log something. But I deleted the logs and the toast is green!

  2. BeanFactory
    This advice helped, and seems like for now it is doing everything I want it to do!

  3. Lables
    This is probably the only problem persisting :confused: I tried $mod_strings and $app_strings, but neither did anything. I set the button (in detailviewdefs.php) to a different existing label and it worked, but I need my own label, well I actually need 2 labels now, because I figured out you need to pass a label to $process->setMessages(['<label>']) for the toast to display anything at all.

The language file is for sure in path /public/legacy/custom/Extension/modules/Accounts/Ext/Language with name en_us.<name>.php with contents like so:

<?php

$mod_strings['LBL_SEND_THROUGH_API'] = 'Send';

But it doesn’t populate anywhere (I don’t need to create anything through the studio right ?)

Also is there a way to access env variables ? There is something mentioned here Automated Testing :: SuiteCRM Documentation and I’d assume you can access them via getenv maybe ?

Hi @rumburak,

Sorry for the delay in replying. Was busy with the maintenance release yesterday.

Labels

Regarding the labels, that should work. Getting a bit puzzled. Could you try the following, please?

  1. Option 1. Check permissions. Maybe the new files are set to a different user:group, and the apache user is not able to pick them up.

    • Don’t forget to Repair and Rebuild and refresh the browser, after re-setting permissions
  2. Option 2. If option 1 does not work, could you try adding them directly to $app_strings in public/legacy/include/language/en_us.lang.php. I know this is a core change, but its just to try to understand if there is anything else going on. As this file should be picked up.

Logger

To use the suite 8 side logger you need to:

  • implement the LoggerAwareInterface in your class
  • Add logger property, like so:
    /**
     * @var LoggerInterface
     */
    protected $logger;

  • Add the method setLogger
    /**
     * @inheritDoc
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
  • Call logger like this $this->logger->error('My message');

You can see an example here:

  • core/backend/Statistics/LegacyHandler/SubpanelDefault.php

Hope this helps

Hey @clemente.raposo,

Labels
I’ve tried both methods:

  1. Permissions

    The last file is mine, the others are extensions or whatever. I cleared the cache, Repaired and rebuilt and refreshed the browser but nothing. Does the <name> in en_us.<name>.php play any role ?
  2. Option 2, core change, this one actually did something! I got the label displayed, but as you said, this is a core change.

ENV Variables
Is there any way to get env variables to config so I can access them in the code ?

Hi @rumburak,

Thank you for the feedback.

Labels

The file was added to Contacts. Is the button on the contacts module also?

Regarding the labels, would you mind creating a bug on the SuiteCRM-Core project

Env variables

What variables would you like to have access to? the ones on the .env file?

Labels
Yeah the button was added to Contacts (in /public/legacy/custom/modules/Contacts/metadata/detailviewdefs.php), we talked about Accounts before, sorry for the confusion. Therefore, I will create the bug.

ENV Variables
I need to pass them from docker, but yeah, let’s say the ones in .env (as you can have .env.prod and etc)

I created an issue for the labels https://github.com/salesagility/SuiteCRM-Core/issues/45

And I got the env variables working like so:

config_override.php

'variabale' => $_SERVER['MY_VARIABLE'],

So thank you @clemente.raposo for huge support! And happy holidays.

Hi @rumburak,

Glad you were able to get it working.
Many thanks for creating the issue.

Happy holidays.

Hello,

I have added the custom button into the Contacts module. However when I am click on it, SuiteCRM gives an error “Unexpected error when calling action”. I have checked the console log an it says the “Internal server error”.

I have added the code as follows for my custom button.

[1] custom/modules/Contacts/metadata/detailviewdefs.php

'add-custom-button' => 
    array (
      'key' => 'add-custom-button',
      'labelKey' => 'LBL_ADD_CUSTOM_BUTTON',
      'asyncProcess' => true,
      'modes' => 
      array (
        0 => 'detail',
      ),
      'acl' => 
      array (
        0 => 'view',
      ),
    ),

[2] extensions/cstmButton/modules/Contacts/Service/AddCustomButton.php

namespace App\Extension\cstmButton\modules\Contacts\Service;

namespace App\Process\Service\RecordActions;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use App\Process\Entity\Process;
use App\Module\Service\ModuleNameMapperInterface;
use App\Process\Service\ProcessHandlerInterface;

class AddCustomButton implements ProcessHandlerInterface
{
protected const MSG_OPTIONS_NOT_FOUND = ‘Process options is not defined’;
protected const PROCESS_TYPE = ‘add-custom-button’;

/**
 * @var ModuleNameMapperInterface
 */
private $moduleNameMapper;

/**
 * PrintAsPdfAction constructor.
 * @param ModuleNameMapperInterface $moduleNameMapper
 */
public function __construct(ModuleNameMapperInterface $moduleNameMapper)
{
    $this->moduleNameMapper = $moduleNameMapper;
}

/**
 * @inheritDoc
 */
public function getProcessType(): string
{
    return self::PROCESS_TYPE;
}

/**
 * @inheritDoc
 */
public function requiredAuthRole(): string
{
    return 'ROLE_USER';
}

/**
 * @inheritDoc
 */
public function configure(Process $process): void
{
    //This process is synchronous
    //We aren't going to store a record on db
    //thus we will use process type as the id
    $process->setId(self::PROCESS_TYPE);
    $process->setAsync(false);
}

/**
 * @inheritDoc
 *
 */
public function validate(Process $process): void
{
    if (empty($process->getOptions())) {
        throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
    }

    $options = $process->getOptions();
    [
        'module' => $baseModule,
        'id' => $id
    ] = $options;

    ['modalRecord' => $modalRecord] = $options['params'];
    [
        'module' => $modalModule,
        'id' => $modalId
    ] = $modalRecord;

    if (empty($baseModule) || empty($id) || empty($modalModule) || empty($modalId)) {
        throw new InvalidArgumentException(self::MSG_OPTIONS_NOT_FOUND);
    }
}

/**
 * @inheritDoc
 */
public function run(Process $process)
{
    $GLOBALS['log']->fatal(" Comes into custom button run");
    

    $process->setStatus('success');
    $process->setMessages([]);
    $process->setData($responseData);
    
    
}

}

[3] Following is the error screenshot.

[4] While clearing the cache from command > php ./bin/console cache:clear
I am getting the following error.

Expected to find class “App\Extension\cstmButton\modules\Contacts\Service\AddCustomButton” in file “extensions/cstmButton\modules\Contacts\Service\AddCustomButton.php” while importing services from resource “…/extensions/*”, but it was not found! Check the names pace prefix used with the resource in config\core_services.yaml (which is being imported from “config/services.yaml”).

Am I missing something to work it? Is there any build process or something remaining to working this button?

Thanks.