diff --git a/.travis.yml b/.travis.yml index ab30ad9..cc29166 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ sudo: false language: php +dist: precise + php: - 5.6 - 7.0 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/composer.json b/composer.json index 6025b84..0f5133c 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ "require": { "silverstripe/framework": "~4.0" }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, "extra": { "installer-name": "gridfieldextensions", "branch-alias": { diff --git a/css/GridFieldExtensions.css b/css/GridFieldExtensions.css index 617823f..6b9ef60 100644 --- a/css/GridFieldExtensions.css +++ b/css/GridFieldExtensions.css @@ -73,8 +73,7 @@ */ .ss-gridfield-add-new-multi-class { - margin-bottom: 8px !important; - white-space: nowrap; + margin-bottom: 8px !important;white-space: nowrap; } .ss-gridfield-add-new-multi-class .field { @@ -131,7 +130,7 @@ .ss-gridfield-editable select.dropdown { border: 1px solid #b3b3b3; background-color: #fff; - padding: 7px 7px 7px 4px; + padding: 7px 7px7px 4px; line-height: 16px; -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -166,12 +165,13 @@ } .ss-gridfield-orderable .col-reorder .handle { - cursor: move; - padding: 16px 0 11px; + + cursor: move;padding: 16px 0 11px; } .ss-gridfield-orderable .col-reorder .handle .icon { - line-height: 100%; + line- + height: 100%; font-size: 1.5em; } @@ -199,3 +199,25 @@ 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 ec822ea..f7f416b 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 3baf298..abb05dc 100755 --- a/src/GridFieldAddNewInlineButton.php +++ b/src/GridFieldAddNewInlineButton.php @@ -181,7 +181,6 @@ class GridFieldAddNewInlineButton implements GridField_HTMLProvider, GridField_S $editable = $grid->getConfig()->getComponentByType(GridFieldEditableColumns::class); /** @var GridFieldOrderableRows $sortable */ $sortable = $grid->getConfig()->getComponentByType(GridFieldOrderableRows::class); - $form = $editable->getForm($grid, $record); if (!singleton($class)->canCreate()) { return; @@ -192,6 +191,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/GridFieldConfigurablePaginator.php b/src/GridFieldConfigurablePaginator.php new file mode 100644 index 0000000..f3af8d5 --- /dev/null +++ b/src/GridFieldConfigurablePaginator.php @@ -0,0 +1,453 @@ +setPageSizes($pageSizes ?: $this->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' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-double-left ss-gridfield-firstpage', + 'disable-previous' => ($this->getCurrentPage() == 1) + ), + 'prev' => array( + 'title' => 'Previous', + 'args' => array('first-shown' => $arguments['first-shown'] - $this->getItemsPerPage()), + 'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-left ss-gridfield-previouspage', + 'disable-previous' => ($this->getCurrentPage() == 1) + ), + 'next' => array( + 'title' => 'Next', + 'args' => array('first-shown' => $arguments['first-shown'] + $this->getItemsPerPage()), + 'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-right 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' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-double-right 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( + __CLASS__, + 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 0e7128a..9b7d705 100644 --- a/src/GridFieldEditableColumns.php +++ b/src/GridFieldEditableColumns.php @@ -124,8 +124,6 @@ class GridFieldEditableColumns extends GridFieldDataColumns implements /** @var GridFieldOrderableRows $sortable */ $sortable = $grid->getConfig()->getComponentByType(GridFieldOrderableRows::class); - $form = $this->getForm($grid, $record); - foreach ($value[self::POST_KEY] as $id => $fields) { if (!is_numeric($id) || !is_array($fields)) { continue; @@ -139,6 +137,7 @@ class GridFieldEditableColumns extends GridFieldDataColumns implements $extra = array(); + $form = $this->getForm($grid, $record); $form->loadDataFrom($fields, Form::MERGE_CLEAR_MISSING); $form->saveInto($item); diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldConfigurablePaginator.ss b/templates/Symbiote/GridFieldExtensions/GridFieldConfigurablePaginator.ss new file mode 100644 index 0000000..adb2490 --- /dev/null +++ b/templates/Symbiote/GridFieldExtensions/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/GridFieldConfigurablePaginatorTest.php b/tests/GridFieldConfigurablePaginatorTest.php new file mode 100644 index 0000000..7681d78 --- /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->assertDOSEquals(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; + } +}