saveErrorHandlers - how to close edit mode after handling the error

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • bandtank
    Active Community Member
    • Mar 2017
    • 419

    #1

    saveErrorHandlers - how to close edit mode after handling the error

    Using the information here: https://docs.espocrm.com/development...rror-handlers/

    In my system, users need to create entities called Sessions that are based on client availability. The beforeSave hook processes complex business rules to determine availablity, and then throws an error if the client is not avaiable. Using a saveErrorHandler, I give the user an option to save anyway, which mostly works.

    However, if the user decides to save the record, the page ends up in a weird state that is not quite edit mode and not quite detail mode. I don't know how to handle a successful save and the documentation does not give any information about this situation at all.

    This is what happens when the user clicks "Save Anyway" in a modal after being informed about the availability problem:

    Click image for larger version  Name:	CleanShot 2026-05-22 at 18.55.23@2x.png Views:	0 Size:	71.9 KB ID:	126523

    The record is successfully saved and most of the fields are put into read-only mode, but the "Cancel" button is still visible and the URL still shows /create. Normally the URL would be something like .../#Session/view/<id> after creating or saving a record. Here is my error handler with the irrelevant parts removed:
    Code:
    define('custom:error-handlers/client-unavailable', [], function () {
      return class {
        constructor(view) {
          this.view = view;
        }
    
        process(data) {
          ...
    
          this.view.createView('dialog', 'views/modal', {
            ...
            buttonList: [
              {
                ...
                onClick: dialog => {
                  dialog.close();
    
                  return this.view.save({
                    headers: {'X-Skip-Availability': 'true'}
                  }).then(() => {
                    this.view.actionCancelEdit();
                    this.view.reRender();
                  });
                }
              },
            ...
    });
    Notably, the X-Skip-Availability header is the only way I have figured out how to pass data to the backend. Setting data.options does not work and neither does anything else I've tried. Here is how the backend uses the header:

    PHP Code:
    
    $headers = getallheaders();\
    $skipAvailability = filter_var(
      $headers['X-Skip-Availability'] ?? false,
      FILTER_VALIDATE_BOOLEAN
    ); 
    
    Last edited by bandtank; Yesterday, 01:18 AM.
  • bandtank
    Active Community Member
    • Mar 2017
    • 419

    #2
    The following code does not work, but the behavior is a little bit better:
    Code:
                  return this.view.save({
                    headers: {
                      'X-Skip-Availability': 'true'
                    }
                  }).then(() => {
    
                    this.view.actionCancelEdit();
                    this.view.setDetailMode();
                    this.view.exit(this.view.isNew ? 'create' : 'save');
                  });
    When events are created on the calendar, the modal does not disappear and the calendar does not refresh. When events are created from the list view, the call to exit causes the app to navigate back to the list view instead of the newly created record.

    Comment

    • bandtank
      Active Community Member
      • Mar 2017
      • 419

      #3
      Alright, I figured out part of the issue I had originally. actionSave from views/edit.ts works differently than actionSave in modals/edit.ts. The former passes headers to the backend while the latter does not. In effect, you either can't use custom headers or you have to override the modals/edit.ts file. I chose the latter method.

      A few notes:
      • When using actionSave without checking for the modal/parent/etc (as seen below), the wrong actionSave is used when Espo's small view is shown (e.g. from the calendar or quick add menu). That causes the post-save code in modals/edit.ts to not execute, which means the original modal won't close correctly.
      • Overriding the save method in modas/edit.ts (as seen below) allows custom headers to be used, which is better than faking an attribute in the model for use as a flag in the backend.
      • When I first overrode the save function in modals/edit.ts, the modal disappeared before the save completed, which is wrong. The original modal needs to be visible until the save completes.

      Code:
      define('custom:error-handlers/client-unavailable', [], function () {
      
          return class {
      
              constructor(view) {
                  this.view = view;
              }
      
              process(data) {
                  if (!this.shouldHandle(data)) {
                      return;
                  }
      
                  this.hideSavingMessage();
                  this.showConfirmModal();
              }
      
              shouldHandle(data) {
                  return data && data.message === 'unavailable';
              }
      
              hideSavingMessage() {
                  this.view.notify(false);
              }
      
              showSavingMessage() {
                  this.view.notify('Saving...');
              }
      
              showConfirmModal() {
                  this.view.createView('dialog', 'views/modal', {
                      headerText: 'Client Unavailable',
                      templateContent: `
                          <p>The client is unavailable during this time.</p>
                          <p>Do you want to save anyway?</p>
                      `,
                      backdrop: true,
                      buttonList: this.getButtonList()
                  }, dialog => {
                      dialog.render();
                  });
              }
      
              getButtonList() {
                  return [
                      {
                          name: 'saveAnyway',
                          label: 'Save Anyway',
                          style: 'danger',
                          onClick: dialog => this.onSaveAnyway(dialog)
                      },
                      {
                          name: 'cancel',
                          label: 'Cancel'
                      }
                  ];
              }
      
              onSaveAnyway(dialog) {
                  dialog.close();
                  this.showSavingMessage();
      
                  if (this.isModalEdit()) {
                      return this.forceSaveModalEdit();
                  }
      
                  return this.forceSaveRecordEdit();
              }
      
              getHeaders() {
                  return {
                      'X-Skip-Availability': 'true'
                  };
              }
      
              getParentView() {
                  return this.view.getParentView && this.view.getParentView();
              }
      
              isModalEdit() {
                  const parent = this.getParentView();
      
                  return parent && parent.template === 'modals/edit';
              }
      
              forceSaveRecordEdit() {
                  return this.view.actionSave({
                      options: {
                          headers: this.getHeaders()
                      }
                  });
              }
      
              forceSaveModalEdit() {
                  const parent = this.getParentView();
                  const editView = parent.getRecordView();
      
                  const originalSave = editView.save.bind(editView);
      
                  editView.save = options => {
                      options = options || {};
      
                      options.headers = Object.assign(
                          {},
                          options.headers || {},
                          this.getHeaders()
                      );
      
                      return originalSave(options);
                  };
      
                  return parent.actionSave()
                      .finally(() => {
                          editView.save = originalSave;
                      });
              }
          };
      });
      This is not a great long-term solution because it requires working around an issue in modals/edit.ts. I would call it a bug, but I'm not sure.
      Last edited by bandtank; Today, 01:58 PM.

      Comment

      • bandtank
        Active Community Member
        • Mar 2017
        • 419

        #4
        I found another issue. Hopefully the code is completely correct now. The above code seems to be right except isModalEdit.

        Editing an event from the calendar would open a modal, allow save, show the error handler modal, allow save again, and everything worked fine. Creating an event from the calendar or quick add menu, however, did not work correctly. The record would save and error handler would fire, but the original modal would remain active and it would be in edit mode. The calendar would also not refresh if the calendar was visible when the original modal appeared. Using the following code appears to fix the lifecycle of the modal:
        Code:
              isModalEdit() {
                const parent = this.getParentView();
        
                return parent &&
                  typeof parent.actionSave === 'function' &&
                  typeof parent.getRecordView === 'function' &&
                  parent.dialog;
              }

        Comment

        Working...