Merge branch '2.0' into 3.0

This commit is contained in:
Daniel Hensby 2018-02-21 09:49:04 +00:00
commit 51dd4cab65
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
15 changed files with 1119 additions and 228 deletions

View File

@ -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

View File

@ -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.

View File

@ -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;
}

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,458 @@
<?php
namespace Symbiote\GridFieldExtensions;
use Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridField_FormAction;
use SilverStripe\Forms\GridField\GridFieldPaginator;
use SilverStripe\Forms\GridField\GridState_Data;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Limitable;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\View\ArrayData;
/**
* GridFieldConfigurablePaginator paginates the {@link GridField} list and adds controls to the bottom of
* the {@link GridField}. The page sizes are configurable.
*/
class GridFieldConfigurablePaginator extends GridFieldPaginator
{
/**
* Specifies default page sizes
*
* @config
* @var int
*/
private static $default_page_sizes = array(15, 30, 60);
/**
* Which template to use for rendering
*
* @var string
*/
protected $itemClass = GridFieldConfigurablePaginator::class;
/**
* @var GridField
*/
protected $gridField;
/**
* @var GridState_Data
*/
protected $gridFieldState;
/**
* @var int[]
*/
protected $pageSizes = array();
/**
* @param int $itemsPerPage How many items should be displayed per page
* @param int $pageSizes The page sizes to show in the dropdown
*/
public function __construct($itemsPerPage = null, $pageSizes = null)
{
$this->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 <totalitems> / <itemsperpage> 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;
}
}

View File

@ -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();
}
}

View File

@ -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));

View File

@ -0,0 +1,30 @@
<tr>
<td class="bottom-all" colspan="$Colspan">
<span class="pagination-page-size">
<%t GridFieldConfigurablePaginator.SHOW 'Show' %>
<select name="$PageSizesName" class="pagination-page-size-select" data-skip-autofocus="true">
<% loop $PageSizes %>
<option <% if $Selected %>selected="selected"<% end_if %>>$Size</option>
<% end_loop %>
</select>
$PageSizesSubmit
</span>
<% if not $OnlyOnePage %>
<div class="datagrid-pagination">
$FirstPage $PreviousPage
<span class="pagination-page-number">
<%t Pagination.Page 'Page' %>
<input class="text" value="$CurrentPageNum" data-skip-autofocus="true" />
<%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %>
$NumPages
</span>
$NextPage $LastPage
</div>
<% end_if %>
<span class="pagination-records-number">
{$FirstShownRecord}&ndash;{$LastShownRecord}
<%t TableListField_PageControls_ss.OF 'of' is 'Example: View 1 of 2' %>
$NumRecords
</span>
</td>
</tr>

View File

@ -0,0 +1,48 @@
<?php
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClass;
use Symbiote\GridFieldExtensions\GridFieldAddNewMultiClassHandler;
use Symbiote\GridFieldExtensions\Tests\Stub\NamespacedClass;
class GridFieldAddNewMultiClassWithNamespacesTest extends SapphireTest {
public function testGetClassesWithNamespaces() {
$grid = new GridField('TestGridField');
$grid->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));
}
}

View File

@ -0,0 +1,238 @@
<?php
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridField_FormAction;
use SilverStripe\ORM\ArrayList;
use Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator;
class GridFieldConfigurablePaginatorTest extends SapphireTest
{
/**
* @var GridField
*/
protected $gridField;
public function setUp()
{
parent::setUp();
// Some dummy GridField list data
$data = ArrayList::create();
for ($i = 1; $i <= 130; $i++) {
$data->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;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symbiote\GridFieldExtensions\Tests\Stub;
use Silverstripe\Dev\TestOnly;
class NamespacedClass implements TestOnly
{
public function i18n_singular_name()
{
return 'NamespacedClass';
}
public function canCreate()
{
return true;
}
}