diff --git a/.travis.yml b/.travis.yml index 9d45ae8..7e09073 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,10 @@ sudo: false language: php php: - - 5.5 - 5.6 - 7.0 + - 7.1 + - 7.2 env: - DB=MYSQL CORE_RELEASE=4 diff --git a/README.md b/README.md index e9c90b8..ab0fe47 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,30 @@ -# SilverStripe Grid Field Extensions Module - -[![Build Status](https://travis-ci.org/symbiote/silverstripe-gridfieldextensions.svg?branch=master)](https://travis-ci.org/symbiote/silverstripe-gridfieldextensions) -[![Latest Stable Version](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/version.svg)](https://github.com/symbiote/silverstripe-gridfieldextensions/releases) -[![Latest Unstable Version](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/v/unstable.svg)](https://packagist.org/packages/symbiote/silverstripe-gridfieldextensions) -[![Total Downloads](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/downloads.svg)](https://packagist.org/packages/symbiote/silverstripe-gridfieldextensions) -[![License](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/license.svg)](https://github.com/symbiote/silverstripe-gridfieldextensions/blob/master/LICENSE.md) - -This module provides a number of useful grid field components: - -* `GridFieldAddExistingSearchButton` - a more advanced search form for adding - items. -* `GridFieldAddNewInlineButton` - builds on `GridFieldEditableColumns` to allow - inline creation of records. -* `GridFieldAddNewMultiClass` - lets the user select from a list of classes to - create a new record from. -* `GridFieldEditableColumns` - allows inline editing of records. -* `GridFieldOrderableRows` - drag and drop re-ordering of rows. -* `GridFieldRequestHandler` - a basic utility class which can be used to build - custom grid field detail views including tabs, breadcrumbs and other CMS - features. -* `GridFieldTitleHeader` - a simple header which displays column titles. - -This branch will aim for compatibility with SilverStripe 4.x. - -For SilverStripe 3.x, please see the [compatible branch](https://github.com/symbiote/silverstripe-gridfieldextensions/tree/1). - - -See [docs/en/index.md](docs/en/index.md) for documentation and examples. +# SilverStripe Grid Field Extensions Module + +[![Build Status](https://travis-ci.org/symbiote/silverstripe-gridfieldextensions.svg?branch=master)](https://travis-ci.org/symbiote/silverstripe-gridfieldextensions) +[![Latest Stable Version](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/version.svg)](https://github.com/symbiote/silverstripe-gridfieldextensions/releases) +[![Latest Unstable Version](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/v/unstable.svg)](https://packagist.org/packages/symbiote/silverstripe-gridfieldextensions) +[![Total Downloads](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/downloads.svg)](https://packagist.org/packages/symbiote/silverstripe-gridfieldextensions) +[![License](https://poser.pugx.org/symbiote/silverstripe-gridfieldextensions/license.svg)](https://github.com/symbiote/silverstripe-gridfieldextensions/blob/master/LICENSE.md) + +This module provides a number of useful grid field components: + +* `GridFieldAddExistingSearchButton` - a more advanced search form for adding + items. +* `GridFieldAddNewInlineButton` - builds on `GridFieldEditableColumns` to allow + inline creation of records. +* `GridFieldAddNewMultiClass` - lets the user select from a list of classes to + create a new record from. +* `GridFieldEditableColumns` - allows inline editing of records. +* `GridFieldOrderableRows` - drag and drop re-ordering of rows. +* `GridFieldRequestHandler` - a basic utility class which can be used to build + custom grid field detail views including tabs, breadcrumbs and other CMS + features. +* `GridFieldTitleHeader` - a simple header which displays column titles. +* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes. + +This branch will aim for compatibility with SilverStripe 4.x. + +For SilverStripe 3.x, please see the [compatible branch](https://github.com/symbiote/silverstripe-gridfieldextensions/tree/1). + + +See [docs/en/index.md](docs/en/index.md) for documentation and examples. diff --git a/css/GridFieldExtensions.css b/css/GridFieldExtensions.css index 9d43e84..a3d11c5 100644 --- a/css/GridFieldExtensions.css +++ b/css/GridFieldExtensions.css @@ -1,184 +1,206 @@ -/** - * GridFieldAddExistingSearchButton - */ - -.add-existing-search-dialog { - min-width: inherit !important; -} - -.add-existing-search-dialog .add-existing-search-form .field { - border: none; - box-shadow: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.add-existing-search-dialog .add-existing-search-form .field label { - padding-bottom: 4px; -} - -.add-existing-search-dialog .add-existing-search-form .Actions { - margin-top: 10px; - padding: 0; -} - -.add-existing-search-dialog .add-existing-search-items li a { - background: #FFF; - border-bottom-width: 1px; - border-color: #CCC; - border-left-width: 1px; - border-right-width: 1px; - border-style: solid; - display: block; - padding: 6px; -} - -.add-existing-search-dialog .add-existing-search-items li:first-child a { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-top-width: 1px; -} - -.add-existing-search-dialog .add-existing-search-items li:last-child a { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} - -.add-existing-search-dialog .add-existing-search-items li a:hover { - background: #F4F4F4; -} - -.add-existing-search-dialog .add-existing-search-pagination li { - background: #FFF; - display: block; - float: left; - margin-right: 2px; - margin-top: 12px; - padding: 6px; -} - -/** - * GridFieldAddNewInlineButton - */ - -.ss-gridfield-inline-new { - background: #EFE; -} - -.ss-gridfield-inline-new:nth-child(2n) { - background: #DFD; -} - -/** - * GridFieldAddNewMultiClass - */ - -.ss-gridfield-add-new-multi-class { - margin-bottom: 8px !important; -} - -.ss-gridfield-add-new-multi-class .field { - border: none; - box-shadow: none; - float: left; - margin: 0 4px 0 0; -} - -/** - * GridFieldEditableColumns - */ - -.ss-gridfield-editable .readonly { - padding-top: 0 !important; -} - -.ss-gridfield-editable input.text, -.ss-gridfield-editable textarea, -.ss-gridfield-editable select, -.ss-gridfield-editable .TreeDropdownField { - margin: 0 !important; - max-width: none !important; -} - -.ss-gridfield-editable select.dropdown { - border: 1px solid #b3b3b3; - background-color: #fff; - padding: 7px 7px; - padding-left: 4px; - line-height: 16px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; -} - -.ss-gridfield-add-new-inline { - margin-bottom: 12px; -} - -.ss-gridfield-add-new-inline span.readonly { - color: #FFF !important; -} - -.ss-gridfield-add-new-inline .col-buttons { - text-align: right; -} - -/** - * GridFieldOrderableRows - */ - -.ss-gridfield-orderable thead tr th.col-Reorder span { - padding: 0 !important; - margin-left: 8px; -} - -.ss-gridfield-orderable .col-reorder { - position: relative; - padding: 0 !important; - width: 16px !important; -} - -.ss-gridfield-orderable .col-reorder .handle { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: move; -} - -.ss-gridfield-orderable .col-reorder .handle .icon { - position: absolute; - top: 50%; - left: 50%; - width: 5px; - height: 11px; - margin: -5px 0 0 -2px; - background-image: url('../../framework/admin/thirdparty/jquery-ui-themes/smoothness/images/ui-icons_222222_256x240.png'); - background-position: -5px -227px; -} - -.ss-gridfield-orderhelper { - border-bottom: 1px solid rgba(0, 0, 0, .1); - border-top: 1px solid rgba(0, 0, 0, .1); - box-shadow: 0 0 8px rgba(0, 0, 0, .4); -} - -.ss-gridfield-orderable tfoot .ui-droppable { - padding-left: 12px; - padding-right: 12px; -} - -.ss-gridfield-orderable tfoot .ui-droppable-active { - background-color: #D4CF90 !important; -} - -.ss-gridfield-orderable tfoot .ss-gridfield-previouspage { - background-position: -16px 9px !important; - margin-left: 0; -} - -.ss-gridfield-orderable tfoot .ss-gridfield-nextpage { - background-position: -40px 9px !important; - margin-right: 0; -} +/** + * GridFieldAddExistingSearchButton + */ + +.add-existing-search-dialog { + min-width: inherit !important; +} + +.add-existing-search-dialog .add-existing-search-form .field { + border: none; + box-shadow: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.add-existing-search-dialog .add-existing-search-form .field label { + padding-bottom: 4px; +} + +.add-existing-search-dialog .add-existing-search-form .Actions { + margin-top: 10px; + padding: 0; +} + +.add-existing-search-dialog .add-existing-search-items li a { + background: #FFF; + border-bottom-width: 1px; + border-color: #CCC; + border-left-width: 1px; + border-right-width: 1px; + border-style: solid; + display: block; + padding: 6px; +} + +.add-existing-search-dialog .add-existing-search-items li:first-child a { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-top-width: 1px; +} + +.add-existing-search-dialog .add-existing-search-items li:last-child a { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.add-existing-search-dialog .add-existing-search-items li a:hover { + background: #F4F4F4; +} + +.add-existing-search-dialog .add-existing-search-pagination li { + background: #FFF; + display: block; + float: left; + margin-right: 2px; + margin-top: 12px; + padding: 6px; +} + +/** + * GridFieldAddNewInlineButton + */ + +.ss-gridfield-inline-new { + background: #EFE; +} + +.ss-gridfield-inline-new:nth-child(2n) { + background: #DFD; +} + +/** + * GridFieldAddNewMultiClass + */ + +.ss-gridfield-add-new-multi-class { + margin-bottom: 8px !important; +} + +.ss-gridfield-add-new-multi-class .field { + border: none; + box-shadow: none; + float: left; + margin: 0 4px 0 0; +} + +/** + * GridFieldEditableColumns + */ + +.ss-gridfield-editable .readonly { + padding-top: 0 !important; +} + +.ss-gridfield-editable input.text, +.ss-gridfield-editable textarea, +.ss-gridfield-editable select, +.ss-gridfield-editable .TreeDropdownField { + margin: 0 !important; + max-width: none !important; +} + +.ss-gridfield-editable select.dropdown { + border: 1px solid #b3b3b3; + background-color: #fff; + padding: 7px 7px; + padding-left: 4px; + line-height: 16px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +.ss-gridfield-add-new-inline { + margin-bottom: 12px; +} + +.ss-gridfield-add-new-inline span.readonly { + color: #FFF !important; +} + +.ss-gridfield-add-new-inline .col-buttons { + text-align: right; +} + +/** + * GridFieldOrderableRows + */ + +.ss-gridfield-orderable thead tr th.col-Reorder span { + padding: 0 !important; + margin-left: 8px; +} + +.ss-gridfield-orderable .col-reorder { + position: relative; + padding: 0 !important; + width: 16px !important; +} + +.ss-gridfield-orderable .col-reorder .handle { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: move; +} + +.ss-gridfield-orderable .col-reorder .handle .icon { + position: absolute; + top: 50%; + left: 50%; + width: 5px; + height: 11px; + margin: -5px 0 0 -2px; + background-image: url('../../framework/thirdparty/jquery-ui-themes/smoothness/images/ui-icons_222222_256x240.png'); + background-position: -5px -227px; +} + +.ss-gridfield-orderhelper { + border-bottom: 1px solid rgba(0, 0, 0, .1); + border-top: 1px solid rgba(0, 0, 0, .1); + box-shadow: 0 0 8px rgba(0, 0, 0, .4); +} + +.ss-gridfield-orderable tfoot .ui-droppable { + padding-left: 12px; + padding-right: 12px; +} + +.ss-gridfield-orderable tfoot .ui-droppable-active { + background-color: #D4CF90 !important; +} + +.ss-gridfield-orderable tfoot .ss-gridfield-previouspage { + background-position: -16px 9px !important; + margin-left: 0; +} + +.ss-gridfield-orderable tfoot .ss-gridfield-nextpage { + background-position: -40px 9px !important; + margin-right: 0; +} + +/** + * GridFieldConfigurablePaginator + */ +.ss-gridfield-configurable-paginator .pagination-page-size { + color: #fff; + float: left; + padding: 6px 0; +} + +.ss-gridfield-configurable-paginator .pagination-page-size-select { + margin-left: 0; + width: auto; +} + +.ss-gridfield-configurable-paginator .ss-gridfield-pagesize-submit { + display: none; +} + +.ss-gridfield-configurable-paginator .pagination-page-number input { + text-align: center; +} diff --git a/docs/en/index.md b/docs/en/index.md index fb1e3be..5afe558 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -106,3 +106,36 @@ class Item extends DataObject { **Please NOTE:** There is a limitation when using `GridFieldOrderableRows` on unsaved data objects; namely, that it doesn't work as without data being saved, the list of related objects has no context. Please check `$this->ID` before adding the `GridFieldOrderableRows` component to the grid field config (or even, before adding the gridfield at all). +Configurable Paginator +---------------------- + +The `GridFieldConfigurablePaginator` component allows you to have a page size dropdown added to your GridField +pagination controls. The page sizes are configurable via the configuration system, or at call time using the public API. +To use this component you should remove the original paginator component first: + +```php +$gridField->getConfig() + ->removeComponentsByType('GridFieldPaginator') + ->addComponent(new GridFieldConfigurablePaginator()); +``` + +You can configure the page sizes with the configuration system. Note that merging is the default strategy, so to replace +the default sizes with your own you will need to unset the original first, for example: + +```php +# File: mysite/_config.php +Config::inst()->remove('GridFieldConfigurablePaginator', 'default_page_sizes'); +Config::inst()->update('GridFieldConfigurablePaginator', 'default_page_sizes', array(100, 200, 500)); +``` + +You can also override these at call time: + +```php +$paginator = new GridFieldConfigurablePaginator(100, array(100, 200, 500)); + +$paginator->setPageSizes(array(200, 500, 1000)); +$paginator->setItemsPerPage(500); +``` + +The first shown record will be maintained across page size changes, and the number of pages and current page will be +recalculated on each request, based on the current first shown record and page size. diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 3498c49..3463bdb 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -387,5 +387,14 @@ if(this.hasClass("ui-droppable")) this.droppable("destroy"); } }); + + /** + * GridFieldConfigurablePaginator + */ + $('.ss-gridfield-configurable-paginator .pagination-page-size-select').entwine({ + onchange: function () { + this.parent().find('.ss-gridfield-pagesize-submit').trigger('click'); + } + }); }); })(jQuery); diff --git a/src/GridFieldAddNewInlineButton.php b/src/GridFieldAddNewInlineButton.php index 9ee05d9..4d0bf88 100755 --- a/src/GridFieldAddNewInlineButton.php +++ b/src/GridFieldAddNewInlineButton.php @@ -3,7 +3,7 @@ namespace Symbiote\GridFieldExtensions; use SilverStripe\Core\Convert; -use SilverStripe\Core\Object; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\Form; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridField_HTMLProvider; @@ -109,7 +109,7 @@ class GridFieldAddNewInlineButton implements GridField_HTMLProvider, GridField_S $handled = array_keys($editable->getDisplayFields($grid)); if ($grid->getList()) { - $record = Object::create($grid->getModelClass()); + $record = Injector::inst()->create($grid->getModelClass()); } else { $record = null; } @@ -168,7 +168,6 @@ class GridFieldAddNewInlineButton implements GridField_HTMLProvider, GridField_S $editable = $grid->getConfig()->getComponentByType('Symbiote\\GridFieldExtensions\\GridFieldEditableColumns'); /** @var GridFieldOrderableRows $sortable */ $sortable = $grid->getConfig()->getComponentByType('Symbiote\\GridFieldExtensions\\GridFieldOrderableRows'); - $form = $editable->getForm($grid, $record); if (!singleton($class)->canCreate()) { return; @@ -178,6 +177,7 @@ class GridFieldAddNewInlineButton implements GridField_HTMLProvider, GridField_S $item = $class::create(); $extra = array(); + $form = $editable->getForm($grid, $record); $form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING); $form->saveInto($item); diff --git a/src/GridFieldAddNewMultiClass.php b/src/GridFieldAddNewMultiClass.php index b428c46..875ee37 100755 --- a/src/GridFieldAddNewMultiClass.php +++ b/src/GridFieldAddNewMultiClass.php @@ -3,6 +3,7 @@ namespace Symbiote\GridFieldExtensions; use SilverStripe\Control\Controller; +use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; @@ -145,7 +146,12 @@ class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URL } } - return $result; + $sanitised = array(); + foreach($result as $class=>$title) { + $sanitised[$this->sanitiseClassName($class)] = $title; + } + + return $sanitised; } /** @@ -179,7 +185,7 @@ class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URL * Handles adding a new instance of a selected class. * * @param GridField $grid - * @param SS_HTTPRequest $request + * @param HTTPRequest $request * @return GridFieldAddNewMultiClassHandler */ public function handleAdd($grid, $request) @@ -196,11 +202,12 @@ class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URL throw new HTTPResponse_Exception(400); } + $unsanitisedClass = $this->unsanitiseClassName($class); $handler = Injector::inst()->create( $this->itemRequestClass, $grid, $component, - new $class(), + new $unsanitisedClass(), $grid->getForm()->getController(), 'add-multi-class' ); @@ -222,7 +229,7 @@ class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URL GridFieldExtensions::include_requirements(); - $field = new DropdownField(sprintf('%s[ClassName]', __CLASS__), '', $classes, $this->defaultClass); + $field = new DropdownField(sprintf('%s[%s]', __CLASS__, $grid->getName()), '', $classes, $this->defaultClass); if (Config::inst()->get(__CLASS__, 'showEmptyString')) { $field->setEmptyString(_t('GridFieldExtensions.SELECTTYPETOCREATE', '(Select type to create)')); } @@ -254,4 +261,20 @@ class GridFieldAddNewMultiClass implements GridField_HTMLProvider, GridField_URL $this->itemRequestClass = $class; return $this; } + + /** + * Sanitise a model class' name for inclusion in a link + * @return string + */ + protected function sanitiseClassName($class) { + return str_replace('\\', '-', $class); + } + + /** + * Unsanitise a model class' name from a URL param + * @return string + */ + protected function unsanitiseClassName($class) { + return str_replace('-', '\\', $class); + } } diff --git a/src/GridFieldAddNewMultiClassHandler.php b/src/GridFieldAddNewMultiClassHandler.php index 4d4f3b3..036b739 100644 --- a/src/GridFieldAddNewMultiClassHandler.php +++ b/src/GridFieldAddNewMultiClassHandler.php @@ -19,8 +19,16 @@ class GridFieldAddNewMultiClassHandler extends GridFieldDetailForm_ItemRequest return Controller::join_links( $this->gridField->Link(), 'add-multi-class', - get_class($this->record) + $this->sanitiseClassName(get_class($this->record)) ); } } + + /** + * Sanitise a model class' name for inclusion in a link + * @return string + */ + protected function sanitiseClassName($class) { + return str_replace('\\', '-', $class); + } } diff --git a/src/GridFieldConfigurablePaginator.php b/src/GridFieldConfigurablePaginator.php new file mode 100644 index 0000000..0902e02 --- /dev/null +++ b/src/GridFieldConfigurablePaginator.php @@ -0,0 +1,458 @@ +setPageSizes($pageSizes ?: self::config()->get('default_page_sizes')); + + if (!$itemsPerPage) { + $itemsPerPage = $this->pageSizes[0]; + } + + parent::__construct($itemsPerPage); + } + + /** + * Get the total number of records in the list + * + * @return int + */ + public function getTotalRecords() + { + return (int) $this->getGridField()->getList()->count(); + } + + /** + * Get the first shown record number + * + * @return int + */ + public function getFirstShown() + { + $firstShown = $this->getGridPagerState()->firstShown ?: 1; + // Prevent visiting a page with an offset higher than the total number of items + if ($firstShown > $this->getTotalRecords()) { + $this->getGridPagerState()->firstShown = $firstShown = 1; + } + return $firstShown; + } + + /** + * Set the first shown record number. Will be stored in the state. + * + * @param int $firstShown + * @return $this + */ + public function setFirstShown($firstShown = 1) + { + $this->getGridPagerState()->firstShown = (int) $firstShown; + return $this; + } + + /** + * Get the last shown record number + * + * @return int + */ + public function getLastShown() + { + return min($this->getTotalRecords(), $this->getFirstShown() + $this->getItemsPerPage() - 1); + } + + /** + * Get the total number of pages, given the current number of items per page. The total + * pages might be higher than / if the first shown record + * is half way through a standard page break point. + * + * @return int + */ + public function getTotalPages() + { + // Pages before + $pages = ceil(($this->getFirstShown() - 1) / $this->getItemsPerPage()); + + // Current page + $pages++; + + // Pages after + $pages += ceil(($this->getTotalRecords() - $this->getLastShown()) / $this->getItemsPerPage()); + + return (int) $pages; + } + + + /** + * Get the page currently active. This is calculated by adding one to the previous number + * of pages calculated via the "first shown record" position. + * + * @return int + */ + public function getCurrentPage() + { + return (int) ceil(($this->getFirstShown() - 1) / $this->getItemsPerPage()) + 1; + } + + /** + * Get the next page number + * + * @return int + */ + public function getNextPage() + { + return min($this->getTotalPages(), $this->getCurrentPage() + 1); + } + + /** + * Get the previous page number + * + * @return int + */ + public function getPreviousPage() + { + return max(1, $this->getCurrentPage() - 1); + } + + /** + * Set the page sizes to use in the "Show x" dropdown + * + * @param array $pageSizes + * @return $this + */ + public function setPageSizes(array $pageSizes) + { + $this->pageSizes = $pageSizes; + + // Reset items per page + $this->setItemsPerPage(current($pageSizes)); + + return $this; + } + + /** + * Get the sizes for the "Show x" dropdown + * + * @return array + */ + public function getPageSizes() + { + return $this->pageSizes; + } + + /** + * Gets a list of page sizes for use in templates as a dropdown + * + * @return ArrayList + */ + public function getPageSizesAsList() + { + $pageSizes = ArrayList::create(); + $perPage = $this->getItemsPerPage(); + foreach ($this->getPageSizes() as $pageSize) { + $pageSizes->push(array( + 'Size' => $pageSize, + 'Selected' => $pageSize == $perPage + )); + } + return $pageSizes; + } + + /** + * Get the GridField used in this request + * + * @return GridField + * @throws Exception If the GridField has not been previously set + */ + public function getGridField() + { + if ($this->gridField) { + return $this->gridField; + } + throw new Exception('No GridField available yet for this request!'); + } + + /** + * Set the GridField so it can be used in other parts of the component during this request + * + * @param GridField $gridField + * @return $this + */ + public function setGridField(GridField $gridField) + { + $this->gridField = $gridField; + return $this; + } + + public function handleAction(GridField $gridField, $actionName, $arguments, $data) + { + $this->setGridField($gridField); + + if ($actionName !== 'paginate') { + return; + } + $state = $this->getGridPagerState(); + + $state->firstShown = (int) $arguments['first-shown']; + $state->pageSize = $data[$gridField->getName()]['page-sizes']; + + $this->setItemsPerPage($state->pageSize); + } + + public function getManipulatedData(GridField $gridField, SS_List $dataList) + { + // Assign the GridField to the class so it can be used later in the request + $this->setGridField($gridField); + + // Retain page sizes during actions provided by other components + $state = $this->getGridPagerState(); + if (is_numeric($state->pageSize)) { + $this->setItemsPerPage($state->pageSize); + } + + if (!($dataList instanceof Limitable) || ($dataList instanceof UnsavedRelationList)) { + return $dataList; + } + + return $dataList->limit($this->getItemsPerPage(), $this->getFirstShown() - 1); + } + + /** + * Add the configurable page size options to the template data + * + * {@inheritDoc} + * + * @param GridField $gridField + * @return ArrayList|null + */ + public function getTemplateParameters(GridField $gridField) + { + $state = $this->getGridPagerState(); + if (is_numeric($state->pageSize)) { + $this->setItemsPerPage($state->pageSize); + } + $arguments = $this->getPagerArguments(); + + // Figure out which page and record range we're on + if (!$arguments['total-rows']) { + return; + } + + // Define a list of the FormActions that should be generated for pager controls (see getPagerActions()) + $controls = array( + 'first' => array( + 'title' => 'First', + 'args' => array('first-shown' => 1), + 'extra-class' => 'ss-gridfield-firstpage', + 'disable-previous' => ($this->getCurrentPage() == 1) + ), + 'prev' => array( + 'title' => 'Previous', + 'args' => array('first-shown' => $arguments['first-shown'] - $this->getItemsPerPage()), + 'extra-class' => 'ss-gridfield-previouspage', + 'disable-previous' => ($this->getCurrentPage() == 1) + ), + 'next' => array( + 'title' => 'Next', + 'args' => array('first-shown' => $arguments['first-shown'] + $this->getItemsPerPage()), + 'extra-class' => 'ss-gridfield-nextpage', + 'disable-next' => ($this->getCurrentPage() == $arguments['total-pages']) + ), + 'last' => array( + 'title' => 'Last', + 'args' => array('first-shown' => ($this->getTotalPages() - 1) * $this->getItemsPerPage() + 1), + 'extra-class' => 'ss-gridfield-lastpage', + 'disable-next' => ($this->getCurrentPage() == $arguments['total-pages']) + ), + 'pagesize' => array( + 'title' => 'Page Size', + 'args' => array('first-shown' => $arguments['first-shown']), + 'extra-class' => 'ss-gridfield-pagesize-submit' + ), + ); + + if ($controls['prev']['args']['first-shown'] < 1) { + $controls['prev']['args']['first-shown'] = 1; + } + + $actions = $this->getPagerActions($controls, $gridField); + + // Render in template + return ArrayData::create(array( + 'OnlyOnePage' => ($arguments['total-pages'] == 1), + 'FirstPage' => $actions['first'], + 'PreviousPage' => $actions['prev'], + 'NextPage' => $actions['next'], + 'LastPage' => $actions['last'], + 'PageSizesSubmit' => $actions['pagesize'], + 'CurrentPageNum' => $this->getCurrentPage(), + 'NumPages' => $arguments['total-pages'], + 'FirstShownRecord' => $arguments['first-shown'], + 'LastShownRecord' => $arguments['last-shown'], + 'NumRecords' => $arguments['total-rows'], + 'PageSizes' => $this->getPageSizesAsList(), + 'PageSizesName' => $gridField->getName() . '[page-sizes]', + )); + } + + public function getHTMLFragments($gridField) + { + GridFieldExtensions::include_requirements(); + + $gridField->addExtraClass('ss-gridfield-configurable-paginator'); + + $forTemplate = $this->getTemplateParameters($gridField); + if ($forTemplate) { + return array( + 'footer' => $forTemplate->renderWith( + $this->itemClass, + array('Colspan' => count($gridField->getColumns())) + ) + ); + } + } + + /** + * Returns an array containing the arguments for the pagination: total rows, pages, first record etc + * + * @return array + */ + protected function getPagerArguments() + { + return array( + 'total-rows' => $this->getTotalRecords(), + 'total-pages' => $this->getTotalPages(), + 'items-per-page' => $this->getItemsPerPage(), + 'first-shown' => $this->getFirstShown(), + 'last-shown' => $this->getLastShown(), + ); + } + + /** + * Returns FormActions for each of the pagination actions, in an array + * + * @param array $controls + * @param GridField $gridField + * @return GridField_FormAction[] + */ + public function getPagerActions(array $controls, GridField $gridField) + { + $actions = array(); + + foreach ($controls as $key => $arguments) { + $action = GridField_FormAction::create( + $gridField, + 'pagination_' . $key, + $arguments['title'], + 'paginate', + $arguments['args'] + ); + + if (isset($arguments['extra-class'])) { + $action->addExtraClass($arguments['extra-class']); + } + + if (isset($arguments['disable-previous']) && $arguments['disable-previous']) { + $action = $action->performDisabledTransformation(); + } elseif (isset($arguments['disable-next']) && $arguments['disable-next']) { + $action = $action->performDisabledTransformation(); + } + + $actions[$key] = $action; + } + + return $actions; + } + + public function getActions($gridField) + { + return array('paginate'); + } + + /** + * Gets the state from the current request's GridField and sets some default values on it + * + * @param GridField $gridField Not used, but present for parent method compatibility + * @return GridState_Data + */ + protected function getGridPagerState(GridField $gridField = null) + { + if (!$this->gridFieldState) { + $state = $this->getGridField()->State->GridFieldConfigurablePaginator; + + // SS 3.1 compatibility (missing __call) + if (is_object($state->firstShown)) { + $state->firstShown = 1; + } + if (is_object($state->pageSize)) { + $state->pageSize = $this->getItemsPerPage(); + } + + // Handle free input in the page number field + $parentState = $this->getGridField()->State->GridFieldPaginator; + if (is_object($parentState->currentPage)) { + $parentState->currentPage = 0; + } + + if ($parentState->currentPage >= 1) { + $state->firstShown = ($parentState->currentPage - 1) * $this->getItemsPerPage() + 1; + $parentState->currentPage = null; + } + + $this->gridFieldState = $state; + } + + return $this->gridFieldState; + } +} diff --git a/src/GridFieldEditableColumns.php b/src/GridFieldEditableColumns.php index ef709db..e2c5f85 100644 --- a/src/GridFieldEditableColumns.php +++ b/src/GridFieldEditableColumns.php @@ -4,7 +4,7 @@ namespace Symbiote\GridFieldExtensions; use SilverStripe\Control\Controller; use SilverStripe\Control\HTTPResponse_Exception; -use SilverStripe\Core\Object; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; use SilverStripe\Forms\FormField; @@ -16,6 +16,7 @@ use SilverStripe\Forms\GridField\GridField_URLHandler; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\ManyManyList; use Exception; @@ -114,8 +115,6 @@ class GridFieldEditableColumns extends GridFieldDataColumns implements /** @var GridFieldOrderableRows $sortable */ $sortable = $grid->getConfig()->getComponentByType('Symbiote\\GridFieldExtensions\\GridFieldOrderableRows'); - $form = $this->getForm($grid, $record); - foreach ($value[__CLASS__] as $id => $fields) { if (!is_numeric($id) || !is_array($fields)) { continue; @@ -129,6 +128,7 @@ class GridFieldEditableColumns extends GridFieldDataColumns implements $extra = array(); + $form = $this->getForm($grid, $record); $form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING); $form->saveInto($item); @@ -219,7 +219,7 @@ class GridFieldEditableColumns extends GridFieldDataColumns implements $extra = $list->getExtraFields(); if ($extra && array_key_exists($col, $extra)) { - $field = Object::create_from_string($extra[$col], $col)->scaffoldFormField(); + $field = Injector::inst()->create($extra[$col], $col)->scaffoldFormField(); } } diff --git a/src/GridFieldOrderableRows.php b/src/GridFieldOrderableRows.php index b2ac9e9..a02d1d9 100755 --- a/src/GridFieldOrderableRows.php +++ b/src/GridFieldOrderableRows.php @@ -15,6 +15,7 @@ use SilverStripe\Forms\HiddenField; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; +use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\ManyManyList; @@ -514,7 +515,8 @@ class GridFieldOrderableRows extends RequestHandler implements // match to order the objects. if (!$isVersioned) { $sortTable = $this->getSortTable($list); - $additionalSQL = (!$list instanceof ManyManyList) ? ', "LastEdited" = NOW()' : ''; + $now = DBDatetime::now()->Rfc2822(); + $additionalSQL = (!$list instanceof ManyManyList) ? ", \"LastEdited\" = '$now'" : ''; foreach ($sortedIDs as $sortValue => $id) { if ($map[$id] != $sortValue) { DB::query(sprintf( @@ -550,7 +552,8 @@ class GridFieldOrderableRows extends RequestHandler implements $field = $this->getSortField(); $table = $this->getSortTable($list); $clause = sprintf('"%s"."%s" = 0', $table, $this->getSortField()); - $additionalSQL = (!$list instanceof ManyManyList) ? ', "LastEdited" = NOW()' : ''; + $now = DBDatetime::now()->Rfc2822(); + $additionalSQL = (!$list instanceof ManyManyList) ? ", \"LastEdited\" = '$now'" : ''; foreach ($list->where($clause)->column('ID') as $id) { $max = DB::query(sprintf('SELECT MAX("%s") + 1 FROM "%s"', $field, $table)); diff --git a/templates/GridFieldConfigurablePaginator.ss b/templates/GridFieldConfigurablePaginator.ss new file mode 100644 index 0000000..adb2490 --- /dev/null +++ b/templates/GridFieldConfigurablePaginator.ss @@ -0,0 +1,30 @@ + + + + <%t GridFieldConfigurablePaginator.SHOW 'Show' %> + + $PageSizesSubmit + + <% if not $OnlyOnePage %> +
+ $FirstPage $PreviousPage + + <%t Pagination.Page 'Page' %> + + <%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %> + $NumPages + + $NextPage $LastPage +
+ <% end_if %> + + {$FirstShownRecord}–{$LastShownRecord} + <%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %> + $NumRecords + + + diff --git a/tests/GridFieldAddNewMultiClassWithNamespacesTest.php b/tests/GridFieldAddNewMultiClassWithNamespacesTest.php new file mode 100644 index 0000000..ee6fbd1 --- /dev/null +++ b/tests/GridFieldAddNewMultiClassWithNamespacesTest.php @@ -0,0 +1,48 @@ +setModelClass(NamespacedClass::class); + + $component = new GridFieldAddNewMultiClass(); + + $this->assertEquals( + array( + 'Symbiote-GridFieldExtensions-Tests-Stub-NamespacedClass' => 'NamespacedClass' + ), + $component->getClasses($grid), + 'Namespaced classes are sanitised' + ); + } + + public function testHandleAddWithNamespaces() { + $grid = new GridField('TestGridField'); + $grid->getConfig()->addComponent(new GridFieldDetailForm()); + $grid->setModelClass(NamespacedClass::class); + $grid->setForm(Form::create(Controller::create(), 'test', FieldList::create(), FieldList::create())); + + $request = new HTTPRequest('POST', 'test'); + $request->setRouteParams(array('ClassName' => 'Symbiote-GridFieldExtensions-Tests-Stub-NamespacedClass')); + + $component = new GridFieldAddNewMultiClass(); + $response = $component->handleAdd($grid, $request); + + $record = new \ReflectionProperty(GridFieldAddNewMultiClassHandler::class, 'record'); + $record->setAccessible(true); + $this->assertInstanceOf(NamespacedClass::class, $record->getValue($response)); + } + +} diff --git a/tests/GridFieldConfigurablePaginatorTest.php b/tests/GridFieldConfigurablePaginatorTest.php new file mode 100644 index 0000000..9638dac --- /dev/null +++ b/tests/GridFieldConfigurablePaginatorTest.php @@ -0,0 +1,238 @@ +push(array('ID' => $i)); + } + + $this->gridField = GridField::create('Mock', null, $data); + } + + public function testGetTotalRecords() + { + $paginator = new GridFieldConfigurablePaginator; + $paginator->setGridField($this->gridField); + + $this->assertSame(130, $paginator->getTotalRecords()); + } + + public function testGetFirstShown() + { + $paginator = new GridFieldConfigurablePaginator; + $paginator->setGridField($this->gridField); + + // No state + $this->assertSame(1, $paginator->getFirstShown()); + + // With a state + $paginator->setFirstShown(123); + $this->assertSame(123, $paginator->getFirstShown()); + + // Too high! + $paginator->setFirstShown(234); + $this->assertSame(1, $paginator->getFirstShown()); + } + + public function testGetLastShown() + { + $paginator = new GridFieldConfigurablePaginator(20, array(10, 20, 30)); + $paginator->setGridField($this->gridField); + + $this->assertSame(20, $paginator->getLastShown()); + + $paginator->setFirstShown(5); + $this->assertSame(24, $paginator->getLastShown()); + } + + public function testGetTotalPages() + { + $paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60)); + $paginator->setGridField($this->gridField); + + // Default calculation + $this->assertSame(7, $paginator->getTotalPages()); + + // With a standard "first shown" record number, e.g. page 2 + $paginator->setFirstShown(21); + $this->assertSame(7, $paginator->getTotalPages()); + + // Non-standard "first shown", e.g. when a page size is changed at page 3. In this case the first page is + // 20 records, the second page is 7 records, third page 20 records, etc + $paginator->setFirstShown(27); + $this->assertSame(8, $paginator->getTotalPages()); + + // ... and when the page size has also been changed. In this case the first page is 57 records, second page + // 60 records and last page is 13 records + $paginator->setFirstShown(57); + $paginator->setItemsPerPage(60); + $this->assertSame(3, $paginator->getTotalPages()); + } + + public function testItemsPerPageIsSetToFirstInPageSizesListWhenChanged() + { + $paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60)); + $paginator->setGridField($this->gridField); + + // Initial state, should be what was provided to the constructor + $this->assertSame(20, $paginator->getItemsPerPage()); + + $paginator->setPageSizes(array(50, 100, 200)); + + // Set via public API, should now be set to 50 + $this->assertSame(50, $paginator->getItemsPerPage()); + } + + public function testGetCurrentPreviousAndNextPages() + { + $paginator = new GridFieldConfigurablePaginator(20, array(20, 40, 60)); + $paginator->setGridField($this->gridField); + + // No page selected (first page) + $this->assertSame(1, $paginator->getCurrentPage()); + $this->assertSame(1, $paginator->getPreviousPage()); + $this->assertSame(2, $paginator->getNextPage()); + + // Second page + $paginator->setFirstShown(21); + $this->assertSame(2, $paginator->getCurrentPage()); + $this->assertSame(1, $paginator->getPreviousPage()); + $this->assertSame(3, $paginator->getNextPage()); + + // Third page + $paginator->setFirstShown(41); + $this->assertSame(3, $paginator->getCurrentPage()); + $this->assertSame(2, $paginator->getPreviousPage()); + $this->assertSame(4, $paginator->getNextPage()); + + // Fourth page, partial record count + $paginator->setFirstShown(42); + $this->assertSame(4, $paginator->getCurrentPage()); + $this->assertSame(3, $paginator->getPreviousPage()); + $this->assertSame(5, $paginator->getNextPage()); + + // Last page (default paging) + $paginator->setFirstShown(121); + $this->assertSame(7, $paginator->getCurrentPage()); + $this->assertSame(6, $paginator->getPreviousPage()); + $this->assertSame(7, $paginator->getNextPage()); + + // Non-standard page size should recalculate the page numbers to be relative to the page size + $paginator->setFirstShown(121); + $paginator->setItemsPerPage(60); + $this->assertSame(3, $paginator->getCurrentPage()); + $this->assertSame(2, $paginator->getPreviousPage()); + $this->assertSame(3, $paginator->getNextPage()); + } + + public function testPageSizesAreConfigurable() + { + // Via constructor + $paginator = new GridFieldConfigurablePaginator(3, array(2, 4, 6)); + $this->assertSame(3, $paginator->getItemsPerPage()); + $this->assertSame(array(2, 4, 6), $paginator->getPageSizes()); + + // Via public API + $paginator->setPageSizes(array(10, 20, 30)); + $this->assertSame(array(10, 20, 30), $paginator->getPageSizes()); + + // Via default configuration + $paginator = new GridFieldConfigurablePaginator; + $default = Config::inst()->get(GridFieldConfigurablePaginator::class, 'default_page_sizes'); + $this->assertSame($default, $paginator->getPageSizes()); + } + + public function testGetPageSizesAsList() + { + $paginator = new GridFieldConfigurablePaginator(10, array(10, 20, 30)); + $this->assertListEquals(array( + array('Size' => '10', 'Selected' => true), + array('Size' => '20', 'Selected' => false), + array('Size' => '30', 'Selected' => false), + ), $paginator->getPageSizesAsList()); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage No GridField available yet for this request! + */ + public function testGetGridFieldThrowsExceptionWhenNotSet() + { + $paginator = new GridFieldConfigurablePaginator; + $paginator->getGridField(); + } + + public function testGetPagerActions() + { + $controls = array( + 'prev' => array( + 'title' => 'Previous', + 'args' => array( + 'next-page' => 123, + 'first-shown' => 234 + ), + 'extra-class' => 'ss-gridfield-previouspage', + 'disable-previous' => false + ), + 'next' => array( + 'title' => 'Next', + 'args' => array( + 'next-page' => 234, + 'first-shown' => 123 + ), + 'extra-class' => 'ss-gridfield-nextpage', + 'disable-next' => true + ) + ); + + $gridField = $this->getMockBuilder(GridField::class)->disableOriginalConstructor()->getMock(); + $paginator = new GridFieldConfigurablePaginator; + $result = $paginator->getPagerActions($controls, $gridField); + + $this->assertCount(2, $result); + $this->assertArrayHasKey('next', $result); + $this->assertContainsOnlyInstancesOf(GridField_FormAction::class, $result); + + $this->assertFalse($result['prev']->isDisabled()); + + $this->assertTrue((bool) $result['next']->hasClass('ss-gridfield-nextpage')); + $this->assertTrue($result['next']->isDisabled()); + } + + public function testSinglePageWithLotsOfItems() + { + $paginator = new GridFieldConfigurablePaginator(null, array(100, 200, 300)); + $this->assertSame(100, $paginator->getItemsPerPage()); + } + + /** + * Set something to the GridField's paginator state data + * + * @param string $key + * @param mixed $value + * @return $this + */ + protected function setState($key, $value) + { + $this->gridField->State->GridFieldConfigurablePaginator->$key = $value; + return $this; + } +} diff --git a/tests/Stub/NamespacedClass.php b/tests/Stub/NamespacedClass.php new file mode 100644 index 0000000..cb6d666 --- /dev/null +++ b/tests/Stub/NamespacedClass.php @@ -0,0 +1,17 @@ +