Как скачать сгенерированный файл runComponentAction

PHP
/**
 * Экспортирует данные в файл
 * @param int $id
 * @param string $type
 * @param null|array $params
 * @return null|BFile
 */
public function exportAction(int $id, string $type, ?array $params = []): ?BFile
{
   try {
      $pFilename = \TEMP_DIR . '/' . 'item.xls';
      \file_put_contents($pFilename, $data);
      $arFile = \CFile::MakeFileArray($pFilename, false, true);
      $file = new BFile($arFile);
   } catch (\Throwable $th) {
      $this->addError(new Error($th->getMessage(), $th->getCode()));
   }
   return $file;
}
JS
BX.ajax.runComponentAction('company:sale.basket.detail', 'export', { 
   mode: 'ajax',
   data: {id, type},
   method: 'POST',
})
?.then(response => {
   if (response.status === 'success' && response.data?.url) {
     window.location.href = response.data.url;
   }
})
.catch(response => {...})

Как скачать файл runComponentAction

PHP
/**
 * Экспортирует данные в файл
 * @param int $id
 * @param string $type
 * @param null|array $params
 * @return null|BFile
 */
public function exportAction(int $id, string $type, ?array $params = []): ?BFile
{
   try {
      $file = \Bitrix\Main\Engine\Response\BFile::createByFileId(985);
   } catch (\Throwable $th) {
      $this->addError(new Error($th->getMessage(), $th->getCode()));
   }
   return $file;
}
JS
BX.ajax.runComponentAction('company:sale.basket.detail', 'export', { 
   mode: 'ajax',
   data: {id, type},
   method: 'POST',
})
?.then(response => {
   if (response.status === 'success' && response.data?.url) {
     window.location.href = response.data.url;
   }
})
.catch(response => {...})

Все JS события в Bitrix

Свет на поток событий

Скрипт, которым можно пропустить через себя вызовы JS битриксовских событий.
Выполняем в консоли и начинаем видеть поток событий. Благодаря замыканиям, все объекты в логе доступны для инспекции.
let originalBxOnCustomEvent = BX.onCustomEvent;

BX.onCustomEvent = function (eventObject, eventName, eventParams, secureParams)
{
    // onMenuItemHover например выбрасывает в другом порядке
    let realEventName = BX.type.isString(eventName) ?
        eventName : BX.type.isString(eventObject) ? eventObject : null;

    if (realEventName) {
        console.log(
            '%c' + realEventName, 
            'background: #222; color: #bada55; font-weight: bold; padding: 3px 4px;'
        );
    }

    console.dir({
        eventObject: eventObject,
        eventParams: eventParams,
        secureParams: secureParams
    });

    originalBxOnCustomEvent.apply(
        null, arguments
    );
};