diff --git a/.travis.yml b/.travis.yml index a8d8436..4e18a3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ matrix: env: DB=PGSQL PHPUNIT_TEST=1 - php: 7.1 env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1 + - php: 7.2 + env: DB=MYSQL PHPUNIT_TEST=1 before_script: - phpenv rehash @@ -25,7 +27,7 @@ before_script: script: - if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit tests/; fi - if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi - - if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=vendor/silverstripe/framework/phpcs.xml.dist src/ tests/ ; fi + - if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs src/ tests/ *.php; fi after_success: - if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi diff --git a/css/GridFieldExtensions.css b/css/GridFieldExtensions.css index f49c814..498d478 100644 --- a/css/GridFieldExtensions.css +++ b/css/GridFieldExtensions.css @@ -14,7 +14,7 @@ } .add-existing-search-dialog .add-existing-search-form .field label { - padding-bottom: 4px; + padding: 4px 0; } .add-existing-search-dialog .add-existing-search-form .Actions { @@ -22,38 +22,12 @@ padding: 0; } -.add-existing-search-dialog .add-existing-search-items li a { - background: #FFF; - border: solid #CCC; - border-right-width: 1px; - border-bottom-width: 1px; - border-left-width: 1px; - display: block; - padding: 6px; +.add-existing-search-dialog .list-group-item { + min-height: 32px; } -.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; +.add-existing-search-dialog .btn-toolbar { margin-top: 12px; - padding: 6px; } /** @@ -97,7 +71,6 @@ display: inline-block; margin: 0; min-width: 150px; - width: calc(100% - 20px); } .ss-gridfield-add-new-multi-class .form-group:after { diff --git a/javascript/GridFieldExtensions.js b/javascript/GridFieldExtensions.js index 2c2f478..d196464 100644 --- a/javascript/GridFieldExtensions.js +++ b/javascript/GridFieldExtensions.js @@ -44,6 +44,15 @@ } }); + // Allow the list item to be clickable as well as the anchor + $('.add-existing-search-dialog .add-existing-search-items .list-group-item-action').entwine({ + onclick: function() { + if (this.children('a').length > 0) { + this.children('a').first().trigger('click'); + } + } + }); + $(".add-existing-search-dialog .add-existing-search-items a").entwine({ onclick: function() { var link = this.closest(".add-existing-search-items").data("add-link"); @@ -264,6 +273,82 @@ */ $(".ss-gridfield-orderable tbody").entwine({ + // reload the gridfield without triggering the change event + // this is because the change has already been saved by reorder action + reload: function (ajaxOpts, successCallback) { + var self = this.getGridField(), form = this.closest('form'), + focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh + data = form.find(':input').serializeArray(); + + if (!ajaxOpts) { + ajaxOpts = {}; + } + if (!ajaxOpts.data) { + ajaxOpts.data = []; + } + ajaxOpts.data = ajaxOpts.data.concat(data); + + // Include any GET parameters from the current URL, as the view state might depend on it. + // For example, a list prefiltered through external search criteria might be passed to GridField. + if (window.location.search) { + ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data); + } + + form.addClass('loading'); + + $.ajax($.extend({}, { + headers: {"X-Pjax": 'CurrentField'}, + type: "POST", + url: this.data('url'), + dataType: 'html', + success: function (data) { + // Replace the grid field with response, not the form. + // TODO Only replaces all its children, to avoid replacing the current scope + // of the executing method. Means that it doesn't retrigger the onmatch() on the main container. + self.empty().append($(data).children()); + + // Refocus previously focused element. Useful e.g. for finding+adding + // multiple relationships via keyboard. + if (focusedElName) self.find(':input[name="' + focusedElName + '"]').focus(); + + // Update filter + if (self.find('.grid-field__filter-header').length) { + var content; + if (ajaxOpts.data[0].filter == "show") { + content = ''; + self.addClass('show-filter').find('.grid-field__filter-header').show(); + } else { + content = ''; + self.removeClass('show-filter').find('.grid-field__filter-header').hide(); + } + + self.find('.sortable-header th:last').html(content); + } + + form.removeClass('loading'); + if (successCallback) { + successCallback.apply(this, arguments); + } + self.trigger('reload', self); + + // update publish button if necessary + const publish = $('#Form_EditForm_action_publish'); + + // button needs to be updated only if it's in published state + if (publish.length > 0 && publish.hasClass('btn-outline-primary')) { + publish.removeClass('btn-outline-primary'); + publish.removeClass('font-icon-tick'); + publish.addClass('btn-primary'); + publish.addClass('font-icon-rocket'); + publish.find('.btn__title').html('Save & publish'); + } + }, + error: function (e) { + alert(i18n._t('Admin.ERRORINTRANSACTION')); + form.removeClass('loading'); + } + }, ajaxOpts)); + }, rebuildSort: function() { var grid = this.getGridField(); @@ -320,7 +405,7 @@ var grid = self.getGridField(); if (grid.data("immediate-update") && postback) { - grid.reload({ + self.reload({ url: grid.data("url-reorder") }); } diff --git a/lang/en.yml b/lang/en.yml new file mode 100644 index 0000000..5792708 --- /dev/null +++ b/lang/en.yml @@ -0,0 +1,14 @@ +en: + GridFieldExtensions: + ADD: Add + ADDEXISTING: 'Add Existing' + BACK: Back + CURRENT: (current) + NOITEMS: 'There are no items.' + Next: Next + PREVIOUS: Previous + RESULTS: Results + SEARCH: Search + SELECTTYPETOCREATE: '(Select type to create)' + Symbiote\GridFieldExtensions\GridFieldConfigurablePaginator: + SHOW: Show diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..1b984f8 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,9 @@ + + + CodeSniffer ruleset for SilverStripe coding conventions. + + + + + + diff --git a/src/GridFieldAddExistingSearchButton.php b/src/GridFieldAddExistingSearchButton.php index 7e991cb..630a2c3 100755 --- a/src/GridFieldAddExistingSearchButton.php +++ b/src/GridFieldAddExistingSearchButton.php @@ -91,14 +91,15 @@ class GridFieldAddExistingSearchButton implements GridField_HTMLProvider, GridFi { GridFieldExtensions::include_requirements(); - $data = new ArrayData(array( + $data = ArrayData::create([ 'Title' => $this->getTitle(), - 'Link' => $grid->Link('add-existing-search') - )); + 'Classes' => 'action btn btn-primary font-icon-search add-existing-search', + 'Link' => $grid->Link('add-existing-search'), + ]); - return array( - $this->fragment => $data->renderWith('Symbiote\\GridFieldExtensions\\GridFieldAddExistingSearchButton'), - ); + return [ + $this->fragment => $data->renderWith(__CLASS__), + ]; } public function getURLHandlers($grid) diff --git a/src/GridFieldAddExistingSearchHandler.php b/src/GridFieldAddExistingSearchHandler.php index 8f025fd..58de381 100644 --- a/src/GridFieldAddExistingSearchHandler.php +++ b/src/GridFieldAddExistingSearchHandler.php @@ -7,8 +7,10 @@ use SilverStripe\Control\RequestHandler; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; +use SilverStripe\Forms\GridField\GridField; use SilverStripe\ORM\DataList; use SilverStripe\ORM\PaginatedList; +use SilverStripe\ORM\Search\SearchContext; /** * Used by {@link GridFieldAddExistingSearchButton} to provide the searching @@ -49,7 +51,7 @@ class GridFieldAddExistingSearchHandler extends RequestHandler public function index() { - return $this->renderWith('Symbiote\\GridFieldExtensions\\GridFieldAddExistingSearchHandler'); + return $this->renderWith(__CLASS__); } public function add($request) @@ -73,19 +75,18 @@ class GridFieldAddExistingSearchHandler extends RequestHandler */ public function SearchForm() { - $form = new Form( + $form = Form::create( $this, 'SearchForm', $this->context->getFields(), - new FieldList( + FieldList::create( FormAction::create('doSearch', _t('GridFieldExtensions.SEARCH', 'Search')) ->setUseButtonTag(true) - ->addExtraClass('ss-ui-button') - ->setAttribute('data-icon', 'magnifier') + ->addExtraClass('btn btn-primary font-icon-search') ) ); - $form->addExtraClass('stacked add-existing-search-form'); + $form->addExtraClass('stacked add-existing-search-form form--no-dividers'); $form->setFormMethod('GET'); return $form; diff --git a/src/GridFieldConfigurablePaginator.php b/src/GridFieldConfigurablePaginator.php index f3af8d5..3655ae9 100644 --- a/src/GridFieldConfigurablePaginator.php +++ b/src/GridFieldConfigurablePaginator.php @@ -5,8 +5,8 @@ namespace Symbiote\GridFieldExtensions; use Exception; use SilverStripe\Core\Config\Configurable; use SilverStripe\Forms\GridField\GridField; -use SilverStripe\Forms\GridField\GridFieldPaginator; 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; @@ -269,7 +269,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator * {@inheritDoc} * * @param GridField $gridField - * @return ArrayList|null + * @return ArrayData|null */ public function getTemplateParameters(GridField $gridField) { @@ -281,7 +281,7 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator // Figure out which page and record range we're on if (!$arguments['total-rows']) { - return; + return null; } // Define a list of the FormActions that should be generated for pager controls (see getPagerActions()) @@ -289,13 +289,15 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator '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', + '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', + 'extra-class' => 'btn btn-secondary btn--hide-text btn-sm font-icon-angle-left ' + . 'ss-gridfield-previouspage', 'disable-previous' => ($this->getCurrentPage() == 1) ), 'next' => array( @@ -307,7 +309,8 @@ class GridFieldConfigurablePaginator extends GridFieldPaginator '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', + '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( diff --git a/src/GridFieldOrderableRows.php b/src/GridFieldOrderableRows.php index 91cb6d3..79f1b24 100755 --- a/src/GridFieldOrderableRows.php +++ b/src/GridFieldOrderableRows.php @@ -11,17 +11,18 @@ use SilverStripe\Forms\GridField\GridField_DataManipulator; use SilverStripe\Forms\GridField\GridField_HTMLProvider; use SilverStripe\Forms\GridField\GridField_SaveHandler; use SilverStripe\Forms\GridField\GridField_URLHandler; +use SilverStripe\Forms\GridField\GridFieldPaginator; use SilverStripe\Forms\HiddenField; use SilverStripe\ORM\ArrayList; -use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\DB; use SilverStripe\ORM\DataList; +use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\DB; use SilverStripe\ORM\ManyManyList; +use SilverStripe\ORM\Map; use SilverStripe\ORM\SS_List; -use SilverStripe\ORM\SS_Map; +use SilverStripe\Versioned\Versioned; use SilverStripe\View\ViewableData; -use Exception; /** * Allows grid field rows to be re-ordered via drag and drop. Both normal data @@ -171,6 +172,35 @@ class GridFieldOrderableRows extends RequestHandler implements return $this; } + /** + * Validates sortable list + * + * @param SS_List $list + * @throws Exception + */ + public function validateSortField(SS_List $list) + { + $field = $this->getSortField(); + + if ($list instanceof ManyManyList) { + $extra = $list->getExtraFields(); + + if ($extra && array_key_exists($field, $extra)) { + return; + } + } + + $classes = ClassInfo::dataClassesFor($list->dataClass()); + + foreach ($classes as $class) { + if (singleton($class)->hasDataBaseField($field)) { + return; + } + } + + throw new \Exception("Couldn't find the sort field '" . $field . "'"); + } + /** * Gets the table which contains the sort field. * @@ -363,7 +393,7 @@ class GridFieldOrderableRows extends RequestHandler implements */ public function handleMoveToPage(GridField $grid, $request) { - if (!$paginator = $grid->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldPaginator')) { + if (!$paginator = $grid->getConfig()->getComponentByType(GridFieldPaginator::class)) { $this->httpError(404, 'Paginator component not found'); } @@ -499,16 +529,14 @@ class GridFieldOrderableRows extends RequestHandler implements /** @var SS_List $map */ $map = $list->map('ID', $sortField); //fix for versions of SS that return inconsistent types for `map` function - if ($map instanceof SS_Map) { + if ($map instanceof Map) { $map = $map->toArray(); } // If not a ManyManyList and using versioning, detect it. - $isVersioned = false; + $this->validateSortField($list); $class = $list->dataClass(); - if ($class == $this->getSortTable($list)) { - $isVersioned = $class::has_extension('SilverStripe\\ORM\\Versioning\\Versioned'); - } + $isVersioned = $class::has_extension(Versioned::class); // Loop through each item, and update the sort values which do not // match to order the objects. diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchButton.ss b/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchButton.ss index 3efa1ec..ddebae0 100644 --- a/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchButton.ss +++ b/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchButton.ss @@ -1,3 +1,3 @@ - - $Title - + + $Title + diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchHandler.ss b/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchHandler.ss index eaa531a..969fc94 100644 --- a/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchHandler.ss +++ b/templates/Symbiote/GridFieldExtensions/GridFieldAddExistingSearchHandler.ss @@ -1,36 +1,56 @@ -$SearchForm - -

<% _t("RESULTS", "Results") %>

-
- <% if $Items %> - - <% else %> -

<% _t("NOITEMS", "There are no items.") %>

- <% end_if %> - - <% if $Items.MoreThanOnePage %> - - <% end_if %> -
+$SearchForm + +

<%t GridFieldExtensions.RESULTS "Results" %>

+
+ <% if $Items %> + + <% else %> +

<%t GridFieldExtensions.NOITEMS "There are no items." %>

+ <% end_if %> + + <% if $Items.MoreThanOnePage %> + + <% end_if %> +