1

Goal:

I have a DataObject called "Event". This is in a managed_model for "EventsAdmin" (extending ModelAdmin). When editing an Event, I want a tab on the record called "Moderation" that has a few fields and two buttons: "Approve" and "Reject". These two buttons call an action each that performs relevant actions.

Event extends DataObject

public function getCMSFields() {
    $fields = parent::getCMSFields();

    $eventStatus = $fields->dataFieldByName("EventStatus")
        ->setTitle('Current Status')
        ->setDisabled(true);

    $approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
        ->setUseButtonTag(true)
        ->addExtraClass('btn-outline-success font-icon-check-mark-circle');

    $rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
        ->setUseButtonTag(true)
        ->addExtraClass('btn-outline-danger font-icon-cancel-circled');

    $fields->addFieldsToTab('Root.Moderation', [
        $eventStatus,
        $approveButton,
        $rejectButton
    ]);

    return $fields;
}

This displays the buttons just fine. But they don't do anything. So I am trying to work out how they can plug into action methods doApproveEvent and doRejectEvent (And where they should go)

I did find docs that led me to adding the buttons to the action bar at the bottom of the CMS page via updateFormActions(). But this isn't what I want as the other fields I am adding above the buttons are part of the Approve/Reject process. Here is the code for this method. This works fine barring the buttons are not in a logical place for the process I'm trying to create.

class CMSActionButtonExtension extends DataExtension
{
    public function updateFormActions(FieldList $actions)
    {
        $record = $this->owner->getRecord();

        if (!$record instanceof Event || !$record->exists()) {
            return;
        }

        $approveButton = FormAction::create('doApproveEvent', _t('SiteBlockAdmin.Approve', 'Approve'))
            ->setUseButtonTag(true)
            ->addExtraClass('btn-outline-success font-icon-check-mark-circle');

        $rejectButton = FormAction::create('doRejectEvent', _t('SiteBlockAdmin.Reject', 'Reject'))
            ->setUseButtonTag(true)
            ->addExtraClass('btn-outline-danger font-icon-cancel-circled');

        $actions->push($approveButton);
        $actions->push($rejectButton);

    }

    public function doApproveEvent($data, $form) {
        $record = $this->owner->getRecord();

        // Approve logic
    }

    public function doRejectEvent($data, $form) {
        $record = $this->owner->getRecord();

        // Reject logic
    }

}

The above Extension is attached to GridFieldDetailForm_ItemRequest

extension.yml

SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest:
  extensions:
    - My\Namespace\CMSActionButtonExtension

Interestingly, if I have both sets of buttons on the page at the same time, the updateFormActions option works while my desired option still doesn't. Despite the buttons being of identical markup and sitting inside the exact same form tag. I assume that has something to do with how Silverstripe loads the main content panel and the DOM.

Any thoughts on achieving this? Anyone seen a button added to the main CMS panel in a module that I could take a look at? I found this post from 5 years ago, but it's for SS3 and the answer doesn't work for me.

Aaryn
  • 1,601
  • 2
  • 18
  • 31
  • I assume CMSActionButtonExtension is attached to the form or gridfield component? Can you also include the config.yml that you used for that please – Zauberfisch Aug 07 '20 at 06:20
  • @Zauberfisch Yes that's right. GridFieldDetailForm_ItemRequest to be exact. I have updated my post with the config. – Aaryn Aug 11 '20 at 01:33

1 Answers1

1

Short answer:
you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself

Long Answer:

A bit of background on how SilverStripe does forms:

Generally speaking, forms are always served through Controllers/RequestHandlers (they need to be accessible on some route, usually that's an Action on a Controller that is often named Form, EditForm, ItemEditoForm, ...).

  1. Fields
    Inside the CMS you rarely ever have to create your own form, that's done by the CMSs built in Controllers/RequestHandlers for the admin area (GridFieldDetailForm_ItemRequest in this case).

    Basically (pseudo code here), what those controllers do is:

    public function EditForm() {
       $fields = $myCurrentlyEditingDataObject->getCMSFields();
       $actions = ...;
       $validator = ...;
       $this->updateFormActions(&$actions);
       $form = new Form('ItemRequestForm', $fields, $actions, $validator);
       $this->updateItemEditForm(&$form); // or $this->updateEditForm()
       return $form;
    }
    

    So, getCMSFields() and in some cases getCMSActions()/getCMSValidator() (not sure if those 2 are still used in SilverStripe 4.x), you can add things to the form, without ever seeing the form object.

    Also, the getCMSFields() will always be put into the ``` section of the Form, that's why your button is somewhere in the middle with all the fields and not with the other actions.

  2. Submission
    When a form is submitted (eg to /admin/pages/edit/EditForm/265/field/NameOfMyGridField/item/542/ItemEditForm), it will call the action GridFieldDetailForm_ItemRequest->ItemEditForm() which returns the Form object where subsequently FormRequestHandler->httpSubmission() is called. This will then look at the submitted data to figure out what action was clicked (eg $_REQUEST['action_doApproveEvent']) and try to find that action.
    The way it tries to find that, is checking if it itself has a method called doApproveEvent, if that fails, it will try Form->getController()->doApproveEvent() or something like that. In the case of a GridField, that controller is GridFieldDetailForm_ItemRequest which means it will try to call GridFieldDetailForm_ItemRequest->doApproveEvent()


So, that means DataObject->getCMSFields() lets you easily add FormFields (and FormActions) into your form body.
But it does not provide a means of adding a method to handle the submission.

That's why, for custom actions you need to modify the Controller (GridFieldDetailForm_ItemRequest in this case).
You are doing this by creating a Extension which you attached to GridFieldDetailForm_ItemRequest. Any method in your Extension is added to the thing it's attached to, so if you add a method called updateFormActions, it will kind of become GridFieldDetailForm_ItemRequest->updateFormActions().
And if you recall from earlier, the controller will call $this->updateFormActions() during the creation of the form.
Additionally, as I explained earlier, when a FormAction is named doApproveEvent it will look for a GridFieldDetailForm_ItemRequest->doApproveEvent(), which now exists because you added it through that Extension.

So, in summary: you have to add custom FormActions through an Extension on the Controller that controls the form (or on the form itself


PS: the old post from bummzack you linked to worked in 3.x, because the Controller in his example that created the form was an instance of LeftAndMain.

Zauberfisch
  • 3,870
  • 18
  • 25