Forcing file download from GridFieldDetailForm

Hey all, I would like to generate a file download on the back of a user editing a record in a GridField and clicking a “Download SpreadSheet” button.

I have added a new GridFieldDetailForm_ItemRequest to add the extra button (which works fine), but I am unsure how to prompt a file download.

I have tried the following which appears to return the file data, but then just renders that info in the browser. Do I need to add some extra headers to the response in order to get SS to not try and render the returned data?

namespace App\Forms;

use SilverStripe\Forms\FormAction;
use SilverStripe\Control\HTTPRequest;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;

class SubmissionDetailForm_ItemRequest extends GridFieldDetailForm_ItemRequest
{
    public function getFormActions()
    {
        $actions = parent::getFormActions();

        $actions->push(
            FormAction::create("doDownloadSpreadsheet", "SpreadSheet")
                ->setUseButtonTag(true)
                ->addExtraClass('btn-outline-primary btn-hide-outline')
                ->addExtraClass('font-icon-down-circled')
        );

        return $actions;
    }

    public function doDownloadSpreadsheet()
    {
        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();
        $sheet->setCellValue('A1', 'Hello World !');

        $writer = new Xlsx($spreadsheet);

        return HTTPRequest::send_file(
            $writer->save("php://output"),
            "file.xlsx",
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        );
    }
}

Had to make several changes to get this working. The main issue was that I have to change the FormAction into a link to an action on the custom GridFieldDetailForm_ItemRequest. I then had to disable the Ajax on the button click.

class SubmissionDetailForm_ItemRequest extends GridFieldDetailForm_ItemRequest
{

    private static $allowed_actions = [
        "DownloadSpreadsheet"
    ];

    public function getFormActions()
    {
        $actions = parent::getFormActions();

        $classes = [
            "btn",
            "btn-outline-primary",
            "btn-hide-outline",
            "font-icon-down-circled",
            "no-ajax" // Class to disable ajax
        ];

        $sheet_button = HTML::createTag(
            "a",
            [
                'class' => implode(" ", $classes),
                'href' => $this->Link('DownloadSpreadsheet')
            ],
            "Spreadsheet"
        );

        $actions->push(
            LiteralField::create("DownloadSpreadsheet", $sheet_button)
        );

        return $actions;
    }

    public function DownloadSpreadsheet()
    {
        $spreadsheet = new Spreadsheet();
        $record = $this->getRecord();

        $sheet = $spreadsheet->getActiveSheet();
        $sheet->setCellValue('A1', 'Hello World !');

        $this->returnObjData(
            "Export-".date('d-m-Y').".xlsx",
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            SpreadsheetFactory::createWriter($spreadsheet, 'Xlsx')
        );
    }

    /**
     * Generate the correct headers and output the file data to the browser
     * 
     * @param string    $filename The name of the file to output
     * @param string    $mime     The mimetype of the file
     * @param IOFactory $write    An instance of PHPOffice IO Writer
     * 
     * @return void
     */
    protected function returnObjData($filename, $mime, $writer)
    {
        // Manually return file data as PHPOffice does not appear to support streaming 
        ob_clean();

        // Redirect output to a client’s web browser (Excel2007)
        header('Content-Type: ' . $mime);
        header('Content-Disposition: attachment;filename="'.$filename.'"');
        header('Cache-Control: max-age=0');

        $writer->save('php://output');

        //terminate php
        exit;
    }
}

Next, I had to add this JS to disable AJAX (thanks to @kinglozzer on Slack for this).

(function($) {
    $.entwine('ss', function($) {
        $('#Form_ItemEditForm a.no-ajax').entwine({
            onclick: function(event) {
                var anchor = $(this);

                if(anchor.attr('target') === '_blank') {
                    window.open(anchor.attr('href'));
                } else {
                    window.location = anchor.attr('href');
                }

                return false;
            }
        });
    });
})(jQuery);

Config.yml

# Extra CSS for Admin
SilverStripe\Admin\LeftAndMain:
 extra_requirements_javascript:
   - 'app/client/dist/js/admin.js'

Thanks to everyone on the Slack group for getting this sorted

1 Like

Would you care creating this as a gridfield extension module? I can see it being useful for others if it can be found on GitHub, via Packagist and SS modules directory.

What would the module be for? Just disabling ajax support? If so, I kind of feel that some way of doing this should be in the GridField JS (maybe via a data-attribute)?