{"id":15286419,"url":"https://github.com/lekoala/silverstripe-cms-actions","last_synced_at":"2025-04-04T14:07:44.121Z","repository":{"id":43013588,"uuid":"320704569","full_name":"lekoala/silverstripe-cms-actions","owner":"lekoala","description":"Add actions to your models in SilverStripe","archived":false,"fork":false,"pushed_at":"2024-12-04T08:11:29.000Z","size":294,"stargazers_count":41,"open_issues_count":2,"forks_count":13,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-28T13:08:59.880Z","etag":null,"topics":["betterbuttons","dataobjects","hacktoberfest","hacktoberfest2021","php","silverstripe","silverstripe-4","silverstripe-5","silverstripe-cms-actions","silverstripe-module"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lekoala.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"lekoala"}},"created_at":"2020-12-11T23:12:36.000Z","updated_at":"2025-01-09T20:55:20.000Z","dependencies_parsed_at":"2024-06-18T19:48:43.668Z","dependency_job_id":"a0d71faa-8dc3-4f96-be10-b66f055bad44","html_url":"https://github.com/lekoala/silverstripe-cms-actions","commit_stats":{"total_commits":144,"total_committers":13,"mean_commits":"11.076923076923077","dds":0.1527777777777778,"last_synced_commit":"70f6ecdc61e7dac0fffad016d6b30a9c829b9596"},"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lekoala%2Fsilverstripe-cms-actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lekoala%2Fsilverstripe-cms-actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lekoala%2Fsilverstripe-cms-actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lekoala%2Fsilverstripe-cms-actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lekoala","download_url":"https://codeload.github.com/lekoala/silverstripe-cms-actions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247190250,"owners_count":20898702,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["betterbuttons","dataobjects","hacktoberfest","hacktoberfest2021","php","silverstripe","silverstripe-4","silverstripe-5","silverstripe-cms-actions","silverstripe-module"],"created_at":"2024-09-30T15:13:56.358Z","updated_at":"2025-04-04T14:07:44.101Z","avatar_url":"https://github.com/lekoala.png","language":"PHP","funding_links":["https://github.com/sponsors/lekoala"],"categories":[],"sub_categories":[],"readme":"# SilverStripe Cms Actions module\n\n![Build Status](https://github.com/lekoala/silverstripe-cms-actions/actions/workflows/ci.yml/badge.svg)\n[![Build Status](https://app.travis-ci.com/lekoala/silverstripe-cms-actions.svg?branch=master)](https://app.travis-ci.com/lekoala/silverstripe-cms-actions)\n[![scrutinizer](https://scrutinizer-ci.com/g/lekoala/silverstripe-cms-actions/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lekoala/silverstripe-cms-actions/)\n[![Code coverage](https://codecov.io/gh/lekoala/silverstripe-cms-actions/branch/master/graph/badge.svg)](https://codecov.io/gh/lekoala/silverstripe-cms-actions)\n\n[![Latest Stable Version](https://poser.pugx.org/lekoala/silverstripe-cms-actions/version)](https://packagist.org/packages/lekoala/silverstripe-cms-actions)\n[![Latest Unstable Version](https://poser.pugx.org/lekoala/silverstripe-cms-actions/v/unstable)](//packagist.org/packages/lekoala/silverstripe-cms-actions)\n[![Total Downloads](https://poser.pugx.org/lekoala/silverstripe-cms-actions/downloads)](https://packagist.org/packages/lekoala/silverstripe-cms-actions)\n[![License](https://poser.pugx.org/lekoala/silverstripe-cms-actions/license)](https://packagist.org/packages/lekoala/silverstripe-cms-actions)\n[![Monthly Downloads](https://poser.pugx.org/lekoala/silverstripe-cms-actions/d/monthly)](https://packagist.org/packages/lekoala/silverstripe-cms-actions)\n[![Daily Downloads](https://poser.pugx.org/lekoala/silverstripe-cms-actions/d/daily)](https://packagist.org/packages/lekoala/silverstripe-cms-actions)\n\n## Intro\n\nFor those of you missing betterbuttons :-) Because let's face it, adding custom actions in SilverStripe is a real pain.\n\n## How does it work ?\n\nWell it's actually quite simple. First of all, we improve the `GridFieldItemRequest` with our `ActionsGridFieldItemRequest`.\nThis is heavily inspired by betterbuttons module. Thanks to this extension, our actions defined in `getCMSActions` will appear properly.\n\nThen, we forward our requests to the model (a button declared on Member call the ItemRequest handler which forwards the action to the Member model).\n\nWe can declare things in two functions:\n\n-   As actions in getCMSActions : these are displayed next to the regular \"save\" button\n\n![custom action](docs/custom-action.png \"custom action\")\n\n-   As utilities in getCMSUtils : these are displayed on the top right corner, next to the tabs\n\n![cms utils](docs/cms-utils.png \"cms utils\")\n\n## LeftAndMain support\n\nThis module is mainly targeted at dealing with actions on regular DataObjects. However, in order to support actions on pages, the `ActionsGridFieldItemRequest`\nis also applied on `LeftAndMain`. Therefore, actions defined on pages should work properly as well.\n\n## Add your buttons\n\n### Actions\n\nSimply use getCMSActions on your DataObjects and add new buttons for your DataObjects!\nFor this, simply push new actions. The CustomAction class is responsible of calling the\naction defined on your DataObject.\n\nIn the following example, we call doCustomAction. The return string is displayed as a notification.\nIf not return string is specified, we display a generic message \"Action {action} done on {record}\".\n\n```php\npublic function getCMSActions()\n{\n    $actions = parent::getCMSActions();\n\n    $actions-\u003epush(new CustomAction(\"doCustomAction\", \"My custom action\"));\n\n    return $actions;\n}\n\npublic function doCustomAction() {\n    return 'Done!';\n}\n```\n\nIf it throws an exception or return a false bool, it will show an error message\n\n```php\npublic function doCustomAction() {\n    throw new Exception(\"Show this error\");\n    return false;\n}\n```\n\nCustomActions are buttons and submitted through ajax. If it changes the state of your record you\nmay need to refresh the UI, but be careful of not losing any unsaved data.\n\n```php\n$myAction-\u003esetShouldRefresh(true);\n```\n\nYou can also redirect to a custom URL (eg: to the main list) after the action\n\n```php\n$fields-\u003epush($doRedirect = new CustomAction('doRedirect', 'Redirect'));\n$doRedirect-\u003esetRedirectURL(\"/admin/my_section/my_model\");\n```\n\nSometimes, you don't want buttons, but links. Use CustomLink instead. This is useful to, say,\ndownload an excel report or a pdf file.\n\n```php\npublic function getCMSActions()\n{\n    $actions = parent::getCMSActions();\n\n    $actions-\u003epush($downloadExcelReport = new CustomLink('downloadExcelReport','Download report'));\n    $downloadExcelReport-\u003esetButtonIcon(SilverStripeIcons::ICON_EXPORT);\n\n    return $actions;\n}\n\npublic function downloadExcelReport() {\n    echo \"This is the report\";\n    die();\n}\n```\n\nPlease note that are we use a die pattern that is not very clean, but you can very well return\na HTTPResponse object instead.\n\nCustomLink use by default ajax navigation. You can use `setNoAjax(true)` to prevent this.\nCustomLink can open links in a new window. You can use `setNewWindow(true)` to enable this.\nCustomLink calls by default an action on the model matching its name. But really you can point it to anything, even an external link using `setLink('https//www.silverstripe.org')`.\n\n#### Confirm actions\n\nIf an action is potentially dangerous or avoid misclicks, you can set a confirmation message using `setConfirmation('Are you sure')` or simply pass `true` for a generic message.\n\n### Decoration \u0026 Placement\n\nYou can set icon. See SilverStripeIcons class for available icons. We use base silverstripe icons.\n\n```php\n$downloadExcelReport-\u003esetButtonIcon(SilverStripeIcons::ICON_EXPORT);\n```\n\nThe native `setIcon` from SilverStripe is also supported for `FormAction`.\n\nYou can also put buttons into a drop-up menu.\n\n![Drop-up example](docs/drop-up.gif \"Drop-up example\")\n\n```php\n$myAction-\u003esetDropUp(true);\n```\n\n### Last icon\n\nYou can use an unlimited set of icons using [last-icon](https://github.com/lekoala/last-icon).\n\nSimply make sure you included the required javascript (the css is already included by default as it is limited and has no side-effect):\n\n```php\nRequirements::javascript(\"https://cdn.jsdelivr.net/npm/last-icon@2/last-icon.min.js\");\n```\n\nor with yml\n\n```yml\nSilverStripe\\Admin\\LeftAndMain:\n  extra_requirements_js:\n    - \"https://cdn.jsdelivr.net/npm/last-icon@2/last-icon.min.js\"\n```\n\nAnd add the icon of your choice:\n\n```php\n$my_action-\u003esetLastIcon('star');\n```\n\nYou can pass additional parameters or pass directly an array.\n\n#### Grouping\nButtons can be grouped in the same manner as the \"Save\"/\"Save and close\" usually are when enclosed in `ActionButtonsGroup` container.\n\n![Grouping example](docs/grouping.gif \"Grouping example\")\n\n```php\n$groupedButtons = [\n    CustomAction::create(\"doAction1\", \"Action1\")\n        -\u003eaddExtraClass('btn-outline-info')\n        -\u003eremoveExtraClass('btn-info'),\n    CustomAction::create(\"doAction2\", \"Action2\")\n        -\u003eaddExtraClass('btn-outline-info')\n        -\u003eremoveExtraClass('btn-info'),\n];\n$btnGroup = ActionButtonsGroup::create($groupedButtons);\n$actions-\u003epush($btnGroup);\n```\n\n### Utils\n\nDeclare getCMSUtils or use updateCMSUtils in your extensions. These utilities will\nappear next to the tabs. They are ideal to provide some extra information or navigation.\nI've used these to add shortcuts, timers, dropdowns navs...\n\n```php\npublic function getCMSUtils()\n{\n    $fields = new FieldList();\n    $fields-\u003epush(new LiteralField(\"LastLoginInfo\", \"Last login at \" . $this-\u003eLastLogin));\n    return $fields;\n}\n```\n\n### Save and close\n\nAdd a default \"save and close\" or \"create and close\" button to quickly add DataObjects.\n\nThis feature can be disabled with the `enable_save_close` config flag\n\n![save and close](docs/save-and-close.png \"save and close\")\n\n### Delete action is on the right\n\nReally I don't know who thought that having delete button next to a save button was a good idea, but I'd rather have it on the right end side.\n\nThis feature can be disabled with the `enable_delete_right` config flag\n\n![delete btn](docs/delete-btn.png \"delete btn\")\n\n### Prev/next support\n\nSilverStripe 4.4 introduced a more refined UI for prev/next records. However, it only allows\nnavigation and does not support \"save and next\" or \"previous and next\" which is useful\nwhen you edit records in a row.\n\nThis feature can be disabled with the `enable_save_prev_next` config flag\n\n![save prev next](docs/save-prev-next.png \"save prev next\")\n\nYou can also use the HasPrevNextUtils trait to add navigation in your utils as well.\n\n### Configure UI options per record\n\nInstead of using the global config flags, you can configure the form based on the record being edited.\n\nYour DataObject needs to implement `getCMSActionsOptions`. This function should return an array with the following keys:\n- save_close: true/false\n- save_prev_next: true/false\n- delete_right: true/false\n\nThis will be use instead of the default global options if provided.\n\n### Custom prev/next record\n\nBy implementing PrevRecord and NextRecord and your DataObject, you can override the\ndefault logic provided by SilverStripe.\n\nPrev/next records can be tricky to manage, if you have grid state issues, using nested\ngridfields and stuff like that. Using PrevRecord and NextRecord provide a simple mean\nwithout dealing with state problems.\n\n## Adding actions to a GridField row\n\nYou can create new row actions by extending the `GridFieldRowButton`\n\nAll actions will be stored in a new \"Actions\" column that supports multiple actions.\nThis can be used, for example, to download files, like invoices, etc directly from a GridField.\n\nFor example, you can do this:\n\n```php\nclass MyRowAction extends GridFieldRowButton\n{\n    protected $fontIcon = 'torso';\n\n    public function getActionName()\n    {\n        return 'my_action';\n    }\n\n    public function getButtonLabel()\n    {\n        return 'My Action';\n    }\n\n    public function doHandle(GridField $gridField, $actionName, $arguments, $data)\n    {\n        $item = $gridField-\u003egetList()-\u003ebyID($arguments['RecordID']);\n        if (!$item) {\n            return;\n        }\n        // Do something with item\n        // Or maybe download a file...\n        return Controller::curr()-\u003eredirectBack();\n    }\n}\n```\n\nAnd use it in your ModelAdmin like so:\n\n```php\npublic function getGridFieldFrom(Form $form)\n{\n    return $form-\u003eFields()-\u003edataFieldByName($this-\u003egetSanitisedModelClass());\n}\n\npublic function getEditForm($id = null, $fields = null)\n{\n    $form = parent::getEditForm($id, $fields);\n\n    $gridfield = $this-\u003egetGridFieldFrom($form);\n\n    if ($this-\u003emodelClass == MyModel::class) {\n        $gridfield-\u003egetConfig()-\u003eaddComponent(new MyRowAction());\n    }\n\n    return $form;\n}\n```\n\n## Adding links to GridField\n\nIf actions are not your cup of tea, you can also add links to your GridField.\n\nAgain, it will be added to the Actions column.\n\nThis acts like a CustomLink describe above, so if we go back to our report example, we get this:\n\n```php\n$gridfield-\u003egetConfig()-\u003eaddComponent(new GridFieldCustomLink('downloadExcelReport', 'Download Report'));\n```\n\n![gridfield row actions](docs/gridfield-row-actions.png \"gridfield row actions\")\n\nFor security reasons, the action MUST be declared in getCMSActions. Failing to do so will return a\nhelpful error message. If you do not want to display the button in the detail form, simply\nset a d-none on it:\n\n```php\n$actions-\u003epush($downloadExcelReport = new CustomLink('downloadExcelReport', 'Download report'));\n$downloadExcelReport-\u003eaddExtraClass('d-none');\n//or simply...\n//$downloadExcelReport-\u003esetHidden();\n```\n\n## Adding buttons to a whole GridField\n\nThis is done using GridFieldTableButton\n\n```php\nclass MyGridButton extends GridFieldTableButton\n{\n    protected $buttonLabel = 'Do stuff';\n    protected $fontIcon = 'do_stuff';\n\n    public function handle(GridField $gridField, Controller $controller)\n    {\n    }\n}\n```\n\nThis class can then be added as a regular GridField component\n\n## Adding actions in getCMSFields\n\nIf you have a lot of actions, sometimes it might make more sense to add it to your cms fields.\nI've used this to provide template files for instance that needs to be uploaded.\n\nThis is done using the `CmsInlineFormAction` class. Please note that the `doCustomAction` function must be declared on your controller, not on the model.\n\nThis is due to the fact that we are not submitting the form, therefore we are not processing the record with our `ActionsGridFieldItemRequest`.\n\n```php\npublic function getCMSFields()\n{\n    $fields = parent::getCMSFields();\n\n    $fields-\u003eaddFieldToTab('Root.Actions', new CmsInlineFormAction('doCustomAction', 'Do this'));\n\n    return $fields;\n}\n```\n\nIn your admin class\n\n```php\n// don't forget to add me to allowed_actions as well\nfunction doCustomAction()\n{\n    // do something here\n    return $this-\u003eredirectBack();\n}\n```\n\n### Posting with inline actions\n\nYou can also post the whole form to another location with `CmsInlineFormAction`.\n\nSimply do this\n\n```php\n    $fields-\u003eaddFieldToTab('Root.MyTab', $myAction = new CmsInlineFormAction(\"myAction\", \"My Action\"));\n    $myAction-\u003esetPost(true);\n```\n\nAnd add to your admin class\n\n```php\n    public function myAction()\n    {\n        $RecordID = $this-\u003egetRequest()-\u003egetVar(\"ID\");\n\n        $message = \"My action done\";\n        $response = Controller::curr()-\u003egetResponse();\n        $response-\u003eaddHeader('X-Status', rawurlencode($message));\n\n        return $response;\n    }\n```\n\nThis will trigger the Save action (through ajax) do your new endpoint, allowing custom behaviour and feedback messages.\n\n## Show messages instead of actions\n\nIf an action is not available/visible, the user may wonder why. Obviously you can display a disabled button, but you can also\ndisplay a message instead of the button. This can be done like so:\n\n```php\n$actions-\u003epush(LiteralField::create('MyCustomAction', \"\u003cspan class=\\\"bb-align\\\"\u003eAction not available\u003c/span\u003e\"));\n```\n\nThe `bb-align` class ensure the text is properly aligned with the buttons.\n\n## Extensions support\n\nIf your extensions depend on this module, you can play with `DataObject::onBeforeUpdateCMSActions` and `DataObject::onAfterUpdateCMSActions` extension hook to add your own buttons.\nThis is called after all buttons have been defined.\n\nSee for instance how it's done in my [softdelete module](https://github.com/lekoala/silverstripe-softdelete).\n\n## Tab tracking\n\nThis extension will also track the active tab when you call save and next / prev and next. This allows editing stuff in a row and keep the same tab.\n\nWhen clicking on the main tabs, it will also update the url. This way, when you reload the page, the good tab will reopen.\n\nThis also allows targeting specific pages with a given tab with links.\n\n## Profile and LeftAndMain extension support\n\nSince we apply our extension on `SilverStripe\\Admin\\LeftAndMain` actions declared in updateCMSActions/getCMSActions are visible in the profile for example.\n\nThe issue here is that the `updateItemEditForm` is never called (this is only called by `GridFieldDetailForm_ItemRequest`, so when you are in a GridField item... in ModelAdmin for instance).\n\nFor instance, this means that you actions are displayed before the 'save' button provided by the Profile controller. Currently, this module fixes this with a bit of css.\n\n## Progressive actions\n\nSince version 1.2, this module supports progressive actions. Progressive actions are buttons that use a progress bar to display what is happening. Under the hood,\nit translates to multiple ajax calls to the same handler function and passing the following post parameters:\n\n-   progress_step: the current step\n-   progress_total: this can be either set in advance or provided by the handler function\n\n![progressive action](docs/progressive-action.gif \"progressive action\")\n\nProgressive actions are supported for `GridFieldTableButton` and `CustomLink`.\n\nHere is a sample implementation. The action needs to return an array with the following keys:\n\n-   progress_step: the updated step. Usually +1.\n-   progress_total: the total number of records. It should only be computed once (on the initial run) when none is provided.\n-   progress_id: you can return a unique id that will be passed along on each call\n-   reload: should we reload at the end ?\n-   message: each run can display a short lived notification with specific text\n-   label: the end label (by default : Completed).\n\n```php\nclass MyProgressiveTableButton extends GridFieldTableButton\n{\n    public function __construct($targetFragment = \"buttons-before-right\", $buttonLabel = null)\n    {\n        $this-\u003eprogressive = true;\n        parent::__construct($targetFragment, $buttonLabel);\n    }\n\n    public function handle(GridField $gridField, Controller $controller)\n    {\n        $step = (int) $controller-\u003egetRequest()-\u003epostVar(\"progress_step\");\n        $total = (int) $controller-\u003egetRequest()-\u003epostVar(\"progress_total\");\n        if (!$total) {\n            $total = $list-\u003ecount();\n        }\n        $i = 0;\n        $res = null;\n        foreach ($list as $rec) {\n            if ($i \u003c $step) {\n                $i++;\n                continue;\n            }\n            $res = \"Processed record $i\";\n            break;\n        }\n        $step++;\n        return [\n            'progress_step' =\u003e $step,\n            'progress_total' =\u003e $total,\n            'reload' =\u003e true,\n            'message' =\u003e $res,\n        ];\n    }\n}\n```\n\n## Modal action\n\nWhat if you need to ask some input from your user when clicking a button ? Like a 'Send Message' button which would display\na nice textarea ?\n\nThis is covered as part of another module: https://github.com/lekoala/silverstripe-pure-modal\n\nIt requires a modal to be displayed (and modals are a bit of a pain to setup in SilverStripe 4, so I created a module to make\nthat much easier). When using both modules it's easy to have actions that open a modal.\n\n## Collapsing icons\n\nThe icons collapse in mobile view. If you have your own buttons, you can add the `btn-mobile-collapse` class so that they\ndo the same. This will be added by default if you set an icon on your buttons.\n\nYou can also hide buttons completely with `btn-mobile-hidden`\n\n![btn mobile collapse](docs/btn-mobile-collapse.png \"btn mobile collapse\")\n\n## Sponsored by\n\nThis module is kindly sponsored by [RESTRUCT](restruct.nl)\n\n## Compatibility\n\nTested with 5.1 but should work on any ^5.1 projects\n\n## Maintainer\n\nLeKoala - thomas@lekoala.be\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flekoala%2Fsilverstripe-cms-actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flekoala%2Fsilverstripe-cms-actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flekoala%2Fsilverstripe-cms-actions/lists"}