mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #11049 from creative-commoners/pulls/5/gridfield-with-viewabledata
Make GridField components work with ViewableData where possible
This commit is contained in:
commit
6c69d32367
@ -12,7 +12,6 @@ use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Control\Session;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
@ -136,7 +135,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
/**
|
||||
* Populated by {@link loadDataFrom()}.
|
||||
*
|
||||
* @var DataObject|null
|
||||
* @var ViewableData|null
|
||||
*/
|
||||
protected $record;
|
||||
|
||||
@ -1223,10 +1222,10 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DataObject that has given this form its data
|
||||
* Returns the record that has given this form its data
|
||||
* through {@link loadDataFrom()}.
|
||||
*
|
||||
* @return DataObject
|
||||
* @return ViewableData
|
||||
*/
|
||||
public function getRecord()
|
||||
{
|
||||
@ -1285,7 +1284,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
const MERGE_AS_SUBMITTED_VALUE = 0b1000;
|
||||
|
||||
/**
|
||||
* Load data from the given DataObject or array.
|
||||
* Load data from the given record or array.
|
||||
*
|
||||
* It will call $object->MyField to get the value of MyField.
|
||||
* If you passed an array, it will call $object[MyField].
|
||||
@ -1306,7 +1305,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
* @uses FormField::setSubmittedValue()
|
||||
* @uses FormField::setValue()
|
||||
*
|
||||
* @param array|DataObject $data
|
||||
* @param array|ViewableData $data
|
||||
* @param int $mergeStrategy
|
||||
* For every field, {@link $data} is interrogated whether it contains a relevant property/key, and
|
||||
* what that property/key's value is.
|
||||
@ -1351,7 +1350,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
|
||||
// If an object is passed, save it for historical reference through {@link getRecord()}
|
||||
// Also use this to determine if we are loading a submitted form, or loading
|
||||
// from a dataobject
|
||||
// from a record
|
||||
$submitted = true;
|
||||
if (is_object($data)) {
|
||||
$this->record = $data;
|
||||
@ -1480,7 +1479,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
* Save the contents of this form into the given data object.
|
||||
* It will make use of setCastedField() to do this.
|
||||
*
|
||||
* @param DataObjectInterface $dataObject The object to save data into
|
||||
* @param ViewableData&DataObjectInterface $dataObject The object to save data into
|
||||
* @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a
|
||||
* form that has some fields that save to one object, and some that save to another.
|
||||
*/
|
||||
@ -1523,7 +1522,7 @@ class Form extends ViewableData implements HasRequestHandler
|
||||
* {@link FieldList->dataFields()}, which filters out
|
||||
* any form-specific data like form-actions.
|
||||
* Calls {@link FormField->dataValue()} on each field,
|
||||
* which returns a value suitable for insertion into a DataObject
|
||||
* which returns a value suitable for insertion into a record
|
||||
* property.
|
||||
*
|
||||
* @return array
|
||||
|
@ -8,13 +8,13 @@ use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\View\AttributesHTML;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Represents a field in a form.
|
||||
@ -454,11 +454,11 @@ class FormField extends RequestHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to save this form field into the given {@link DataObject}.
|
||||
* Method to save this form field into the given record.
|
||||
*
|
||||
* By default, makes use of $this->dataValue()
|
||||
*
|
||||
* @param DataObject|DataObjectInterface $record DataObject to save data into
|
||||
* @param ViewableData|DataObjectInterface $record Record to save data into
|
||||
*/
|
||||
public function saveInto(DataObjectInterface $record)
|
||||
{
|
||||
@ -697,7 +697,7 @@ class FormField extends RequestHandler
|
||||
* or a submitted form value they should override setSubmittedValue() instead.
|
||||
*
|
||||
* @param mixed $value Either the parent object, or array of source data being loaded
|
||||
* @param array|DataObject $data {@see Form::loadDataFrom}
|
||||
* @param array|ViewableData $data {@see Form::loadDataFrom}
|
||||
* @return $this
|
||||
*/
|
||||
public function setValue($value, $data = null)
|
||||
@ -712,7 +712,7 @@ class FormField extends RequestHandler
|
||||
* data formats.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array|DataObject $data
|
||||
* @param array|ViewableData $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setSubmittedValue($value, $data = null)
|
||||
|
@ -20,11 +20,14 @@ use SilverStripe\Forms\GridField\FormAction\SessionStore;
|
||||
use SilverStripe\Forms\GridField\FormAction\StateStore;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\ORM\Limitable;
|
||||
use SilverStripe\ORM\Sortable;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\View\HTML;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Displays a {@link SS_List} in a grid format.
|
||||
@ -83,12 +86,12 @@ class GridField extends FormField
|
||||
/**
|
||||
* Data source.
|
||||
*
|
||||
* @var SS_List
|
||||
* @var SS_List&Filterable&Sortable&Limitable
|
||||
*/
|
||||
protected $list = null;
|
||||
|
||||
/**
|
||||
* Class name of the DataObject that the GridField will display.
|
||||
* Class name of the records that the GridField will display.
|
||||
*
|
||||
* Defaults to the value of $this->list->dataClass.
|
||||
*
|
||||
@ -205,7 +208,7 @@ class GridField extends FormField
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a data class that is a DataObject type that this GridField should look like.
|
||||
* Returns the class name of the record type that this GridField should contain.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
@ -374,7 +377,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* Set the data source.
|
||||
*
|
||||
* @param SS_List $list
|
||||
* @param SS_List&Filterable&Sortable&Limitable $list
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
@ -388,7 +391,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* Get the data source.
|
||||
*
|
||||
* @return SS_List
|
||||
* @return SS_List&Filterable&Sortable&Limitable
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
@ -398,7 +401,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* Get the data source after applying every {@link GridField_DataManipulator} to it.
|
||||
*
|
||||
* @return SS_List
|
||||
* @return SS_List&Filterable&Sortable&Limitable
|
||||
*/
|
||||
public function getManipulatedList()
|
||||
{
|
||||
@ -461,7 +464,7 @@ class GridField extends FormField
|
||||
if (($request instanceof NullHTTPRequest) && Controller::has_curr()) {
|
||||
$request = Controller::curr()->getRequest();
|
||||
}
|
||||
|
||||
|
||||
$stateStr = $this->getStateManager()->getStateFromRequest($this, $request);
|
||||
if ($stateStr) {
|
||||
$oldState = $this->getState(false);
|
||||
@ -744,7 +747,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* @param int $total
|
||||
* @param int $index
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param array $attributes
|
||||
* @param string $content
|
||||
*
|
||||
@ -762,7 +765,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* @param int $total
|
||||
* @param int $index
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param array $attributes
|
||||
* @param string $content
|
||||
*
|
||||
@ -780,7 +783,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* @param int $total
|
||||
* @param int $index
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@ -798,7 +801,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* @param int $total
|
||||
* @param int $index
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@ -869,7 +872,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* Get the value from a column.
|
||||
*
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $column
|
||||
*
|
||||
* @return string
|
||||
@ -922,7 +925,7 @@ class GridField extends FormField
|
||||
* Use of this method ensures that any special rules around the data for this gridfield are
|
||||
* followed.
|
||||
*
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return mixed
|
||||
@ -949,7 +952,7 @@ class GridField extends FormField
|
||||
/**
|
||||
* Get extra columns attributes used as HTML attributes.
|
||||
*
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $column
|
||||
*
|
||||
* @return array
|
||||
|
@ -32,6 +32,8 @@ use LogicException;
|
||||
*
|
||||
* For easier setup, have a look at a sample configuration in
|
||||
* {@link GridFieldConfig_RelationEditor}.
|
||||
*
|
||||
* The modelClass of the GridField this component is in must be a DataObject subclass.
|
||||
*/
|
||||
class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent implements GridField_HTMLProvider, GridField_ActionProvider, GridField_DataManipulator, GridField_URLHandler
|
||||
{
|
||||
@ -106,6 +108,10 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
||||
{
|
||||
$dataClass = $gridField->getModelClass();
|
||||
|
||||
if (!is_a($dataClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$dataClass'");
|
||||
}
|
||||
|
||||
$forTemplate = new ArrayData([]);
|
||||
$forTemplate->Fields = new FieldList();
|
||||
|
||||
@ -191,11 +197,17 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
||||
*/
|
||||
public function getManipulatedData(GridField $gridField, SS_List $dataList)
|
||||
{
|
||||
$dataClass = $gridField->getModelClass();
|
||||
|
||||
if (!is_a($dataClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$dataClass'");
|
||||
}
|
||||
|
||||
$objectID = $gridField->State->GridFieldAddRelation(null);
|
||||
if (empty($objectID)) {
|
||||
return $dataList;
|
||||
}
|
||||
$object = DataObject::get_by_id($gridField->getModelClass(), $objectID);
|
||||
$object = DataObject::get_by_id($dataClass, $objectID);
|
||||
if ($object) {
|
||||
$dataList->add($object);
|
||||
}
|
||||
@ -227,6 +239,10 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
||||
$searchStr = $request->getVar('gridfield_relationsearch');
|
||||
$dataClass = $gridField->getModelClass();
|
||||
|
||||
if (!is_a($dataClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$dataClass'");
|
||||
}
|
||||
|
||||
$searchFields = ($this->getSearchFields())
|
||||
? $this->getSearchFields()
|
||||
: $this->scaffoldSearchFields($dataClass);
|
||||
@ -337,6 +353,10 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
||||
*/
|
||||
public function scaffoldSearchFields($dataClass)
|
||||
{
|
||||
if (!is_a($dataClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$dataClass'");
|
||||
}
|
||||
|
||||
$obj = DataObject::singleton($dataClass);
|
||||
$fields = null;
|
||||
if ($fieldSpecs = $obj->searchableFields()) {
|
||||
@ -387,6 +407,10 @@ class GridFieldAddExistingAutocompleter extends AbstractGridFieldComponent imple
|
||||
*/
|
||||
public function getPlaceholderText($dataClass)
|
||||
{
|
||||
if (!is_a($dataClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$dataClass'");
|
||||
}
|
||||
|
||||
$searchFields = ($this->getSearchFields())
|
||||
? $this->getSearchFields()
|
||||
: $this->scaffoldSearchFields($dataClass);
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\RelationList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
@ -12,8 +14,7 @@ use SilverStripe\View\SSViewer;
|
||||
* This component provides a button for opening the add new form provided by
|
||||
* {@link GridFieldDetailForm}.
|
||||
*
|
||||
* Only returns a button if {@link DataObject->canCreate()} for this record
|
||||
* returns true.
|
||||
* Only returns a button if canCreate() for this record returns true.
|
||||
*/
|
||||
class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridField_HTMLProvider
|
||||
{
|
||||
@ -36,7 +37,16 @@ class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridFi
|
||||
|
||||
public function getHTMLFragments($gridField)
|
||||
{
|
||||
$singleton = singleton($gridField->getModelClass());
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$singleton = singleton($modelClass);
|
||||
|
||||
if (!$singleton->hasMethod('canCreate')) {
|
||||
throw new LogicException(
|
||||
__CLASS__ . ' cannot be used with models that do not implement canCreate().'
|
||||
. " Remove this component from your GridField or implement canCreate() on $modelClass"
|
||||
);
|
||||
}
|
||||
|
||||
$context = [];
|
||||
if ($gridField->getList() instanceof RelationList) {
|
||||
$record = $gridField->getForm()->getRecord();
|
||||
@ -51,7 +61,7 @@ class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridFi
|
||||
|
||||
if (!$this->buttonName) {
|
||||
// provide a default button name, can be changed by calling {@link setButtonName()} on this component
|
||||
$objectName = $singleton->i18n_singular_name();
|
||||
$objectName = $singleton->hasMethod('i18n_singular_name') ? $singleton->i18n_singular_name() : ClassInfo::shortName($singleton);
|
||||
$this->buttonName = _t('SilverStripe\\Forms\\GridField\\GridField.Add', 'Add {name}', ['name' => $objectName]);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
* A simple readonly, paginated view of records, with sortable and searchable
|
||||
* headers.
|
||||
@ -23,9 +25,11 @@ class GridFieldConfig_Base extends GridFieldConfig
|
||||
$this->addComponent(GridFieldPageCount::create('toolbar-header-right'));
|
||||
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
|
||||
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) {
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
});
|
||||
|
||||
$this->extend('updateConfig');
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
* Allows editing of records contained within the GridField, instead of only allowing the ability to view records in
|
||||
* the GridField.
|
||||
@ -30,9 +32,11 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig
|
||||
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
|
||||
$this->addComponent(GridFieldDetailForm::create(null, $showPagination, $showAdd));
|
||||
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) {
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
});
|
||||
|
||||
$this->extend('updateConfig');
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
* Similar to {@link GridFieldConfig_RecordEditor}, but adds features to work
|
||||
* on has-many or many-many relationships.
|
||||
@ -43,9 +45,11 @@ class GridFieldConfig_RelationEditor extends GridFieldConfig
|
||||
$this->addComponent($pagination = GridFieldPaginator::create($itemsPerPage));
|
||||
$this->addComponent(GridFieldDetailForm::create());
|
||||
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
Deprecation::withNoReplacement(function () use ($sort, $filter, $pagination) {
|
||||
$sort->setThrowExceptionOnBadDataType(false);
|
||||
$filter->setThrowExceptionOnBadDataType(false);
|
||||
$pagination->setThrowExceptionOnBadDataType(false);
|
||||
});
|
||||
|
||||
$this->extend('updateConfig');
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Core\Convert;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use LogicException;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* @see GridField
|
||||
@ -87,7 +88,15 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
|
||||
public function getDisplayFields($gridField)
|
||||
{
|
||||
if (!$this->displayFields) {
|
||||
return singleton($gridField->getModelClass())->summaryFields();
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$singleton = singleton($modelClass);
|
||||
if (!$singleton->hasMethod('summaryFields')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine columns. Pass the column names to setDisplayFields()'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
}
|
||||
return $singleton->summaryFields();
|
||||
}
|
||||
return $this->displayFields;
|
||||
}
|
||||
@ -146,7 +155,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
|
||||
* HTML for the column, content of the <td> element.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record Record displayed in this row
|
||||
* @param ViewableData $record Record displayed in this row
|
||||
* @param string $columnName
|
||||
* @return string HTML for the column. Return NULL to skip.
|
||||
*/
|
||||
@ -180,7 +189,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
|
||||
* Attributes for the element containing the content returned by {@link getColumnContent()}.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record displayed in this row
|
||||
* @param ViewableData $record displayed in this row
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
@ -216,7 +225,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
|
||||
/**
|
||||
* Translate a Object.RelationName.ColumnName $columnName into the value that ColumnName returns
|
||||
*
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return string|null - returns null if it could not found a value
|
||||
*/
|
||||
@ -269,7 +278,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie
|
||||
/**
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $item
|
||||
* @param ViewableData $item
|
||||
* @param string $fieldName
|
||||
* @param string $value
|
||||
* @return string
|
||||
|
@ -2,9 +2,12 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\ValidationException;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* This class is a {@link GridField} component that adds a delete action for
|
||||
@ -72,13 +75,12 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
/**
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param DataObjectInterface&ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return string|null the attribles for the action
|
||||
*/
|
||||
public function getExtraData($gridField, $record, $columnName)
|
||||
{
|
||||
|
||||
$field = $this->getRemoveAction($gridField, $record, $columnName);
|
||||
|
||||
if ($field) {
|
||||
@ -105,7 +107,7 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
* Return any special attributes that will be used for FormField::create_tag()
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param DataObjectInterface&ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
@ -153,7 +155,7 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
/**
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param DataObjectInterface&ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return string|null the HTML for the column
|
||||
*/
|
||||
@ -179,29 +181,39 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
*/
|
||||
public function handleAction(GridField $gridField, $actionName, $arguments, $data)
|
||||
{
|
||||
$list = $gridField->getList();
|
||||
if ($actionName == 'deleterecord' || $actionName == 'unlinkrelation') {
|
||||
/** @var DataObject $item */
|
||||
$item = $gridField->getList()->byID($arguments['RecordID']);
|
||||
/** @var DataObjectInterface&ViewableData $item */
|
||||
$item = $list->byID($arguments['RecordID']);
|
||||
if (!$item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($actionName == 'deleterecord') {
|
||||
$this->checkForRequiredMethod($item, 'canDelete');
|
||||
|
||||
if (!$item->canDelete()) {
|
||||
throw new ValidationException(
|
||||
_t(__CLASS__ . '.DeletePermissionsFailure', "No delete permissions")
|
||||
);
|
||||
}
|
||||
|
||||
if (!($list instanceof DataList)) {
|
||||
// We need to make sure to exclude the item since the list items have already been determined.
|
||||
// This must happen before deletion while the item still has its ID set.
|
||||
$gridField->setList($list->exclude(['ID' => $item->ID]));
|
||||
}
|
||||
$item->delete();
|
||||
} else {
|
||||
$this->checkForRequiredMethod($item, 'canEdit');
|
||||
|
||||
if (!$item->canEdit()) {
|
||||
throw new ValidationException(
|
||||
_t(__CLASS__ . '.EditPermissionsFailure', "No permission to unlink record")
|
||||
);
|
||||
}
|
||||
|
||||
$gridField->getList()->remove($item);
|
||||
$list->remove($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -209,16 +221,19 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
/**
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param DataObjectInterface&ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return GridField_FormAction|null
|
||||
*/
|
||||
private function getRemoveAction($gridField, $record, $columnName)
|
||||
{
|
||||
if ($this->getRemoveRelation()) {
|
||||
$this->checkForRequiredMethod($record, 'canEdit');
|
||||
|
||||
if (!$record->canEdit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = _t(__CLASS__ . '.UnlinkRelation', "Unlink");
|
||||
|
||||
$field = GridField_FormAction::create(
|
||||
@ -233,9 +248,12 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
->setDescription($title)
|
||||
->setAttribute('aria-label', $title);
|
||||
} else {
|
||||
$this->checkForRequiredMethod($record, 'canDelete');
|
||||
|
||||
if (!$record->canDelete()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = _t(__CLASS__ . '.Delete', "Delete");
|
||||
|
||||
$field = GridField_FormAction::create(
|
||||
@ -274,4 +292,20 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
|
||||
$this->removeRelation = (bool) $removeRelation;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a required method exists - and if not, throws an exception.
|
||||
*
|
||||
* @throws LogicException if the required method doesn't exist
|
||||
*/
|
||||
private function checkForRequiredMethod($record, string $method): void
|
||||
{
|
||||
if (!$record->hasMethod($method)) {
|
||||
$modelClass = get_class($record);
|
||||
throw new LogicException(
|
||||
__CLASS__ . " cannot be used with models that don't implement {$method}()."
|
||||
. " Remove this component from your GridField or implement {$method}() on $modelClass"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use Closure;
|
||||
use LogicException;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
@ -13,10 +14,11 @@ use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Extensible;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FieldsValidator;
|
||||
use SilverStripe\Forms\Validator;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Provides view and edit forms at GridField-specific URLs.
|
||||
@ -62,7 +64,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @var FieldList Falls back to {@link DataObject->getCMSFields()} if not defined.
|
||||
* @var FieldList Falls back to {@link $record->getCMSFields()} if not defined.
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
@ -143,28 +145,31 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
|
||||
// if no validator has been set on the GridField then use the Validators from the record.
|
||||
if (!$this->getValidator()) {
|
||||
$this->setValidator($record->getCMSCompositeValidator());
|
||||
if ($record->hasMethod('getCMSCompositeValidator')) {
|
||||
$validator = $record->getCMSCompositeValidator();
|
||||
} else {
|
||||
$validator = FieldsValidator::create();
|
||||
}
|
||||
$this->setValidator($validator);
|
||||
}
|
||||
|
||||
return $handler->handleRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param GridField $gridField
|
||||
* @param HTTPRequest $request
|
||||
* @return DataObject|null
|
||||
*/
|
||||
protected function getRecordFromRequest(GridField $gridField, HTTPRequest $request): ?DataObject
|
||||
protected function getRecordFromRequest(GridField $gridField, HTTPRequest $request): ?ViewableData
|
||||
{
|
||||
/** @var DataObject $record */
|
||||
/** @var ViewableData $record */
|
||||
if (is_numeric($request->param('ID'))) {
|
||||
/** @var Filterable $dataList */
|
||||
$dataList = $gridField->getList();
|
||||
$record = $dataList->byID($request->param('ID'));
|
||||
} else {
|
||||
$record = Injector::inst()->create($gridField->getModelClass());
|
||||
}
|
||||
|
||||
if ($record && !$record->hasField('ID')) {
|
||||
throw new LogicException(get_class($record) . ' must have an ID field.');
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
@ -174,7 +179,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
* This only works when the list passed to the GridField is a {@link DataList}.
|
||||
*
|
||||
* @param $gridField The current GridField
|
||||
* @param $id The ID of the DataObject to open
|
||||
* @param $id The ID of the record to open
|
||||
*/
|
||||
public function getLostRecordRedirection(GridField $gridField, HTTPRequest $request, ?int $id = null): ?string
|
||||
{
|
||||
@ -216,7 +221,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
* Build a request handler for the given record
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param RequestHandler $requestHandler
|
||||
* @return GridFieldDetailForm_ItemRequest
|
||||
*/
|
||||
@ -248,7 +253,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
@ -266,7 +271,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
@ -276,8 +281,8 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel
|
||||
/**
|
||||
* Enable redirection to missing records.
|
||||
*
|
||||
* If a GridField shows a filtered list, and the DataObject is not in the list but exists in the
|
||||
* database, and the DataObject has a CMSEditLink method, then the system will redirect to the
|
||||
* If a GridField shows a filtered list, and the record is not in the list but exists in the
|
||||
* database, and the record has a CMSEditLink method, then the system will redirect to the
|
||||
* URL returned by that method.
|
||||
*/
|
||||
public function setRedirectMissingRecords(bool $redirectMissingRecords): self
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Control\RequestHandler;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Forms\CompositeField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
@ -17,6 +18,7 @@ use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\LiteralField;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\ORM\HasManyList;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
@ -28,6 +30,7 @@ use SilverStripe\ORM\ValidationResult;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\HTML;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
{
|
||||
@ -64,7 +67,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
protected $component;
|
||||
|
||||
/**
|
||||
* @var DataObject
|
||||
* @var ViewableData
|
||||
*/
|
||||
protected $record;
|
||||
|
||||
@ -96,7 +99,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param GridFieldDetailForm $component
|
||||
* @param DataObject $record
|
||||
* @param ViewableData&DataObjectInterface $record
|
||||
* @param RequestHandler $requestHandler
|
||||
* @param string $popupFormName
|
||||
*/
|
||||
@ -125,11 +128,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
*/
|
||||
public function view($request)
|
||||
{
|
||||
if (!$this->record->canView()) {
|
||||
// Assume item can be viewed if canView() isn't implemented
|
||||
if ($this->record->hasMethod('canView') && !$this->record->canView()) {
|
||||
$this->httpError(403, _t(
|
||||
__CLASS__ . '.ViewPermissionsFailure',
|
||||
'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
|
||||
['ObjectTitle' => $this->record->singular_name()]
|
||||
['ObjectTitle' => $this->getModelName()]
|
||||
));
|
||||
}
|
||||
|
||||
@ -215,17 +219,25 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->record->canView()) {
|
||||
// Assume item can be viewed if canView() isn't implemented
|
||||
if ($this->record->hasMethod('canView') && !$this->record->canView()) {
|
||||
$controller = $this->getToplevelController();
|
||||
return $controller->httpError(403, _t(
|
||||
__CLASS__ . '.ViewPermissionsFailure',
|
||||
'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"',
|
||||
['ObjectTitle' => $this->record->singular_name()]
|
||||
['ObjectTitle' => $this->getModelName()]
|
||||
));
|
||||
}
|
||||
|
||||
$fields = $this->component->getFields();
|
||||
if (!$fields) {
|
||||
if (!$this->record->hasMethod('getCMSFields')) {
|
||||
$modelClass = get_class($this->record);
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine form fields. Pass the fields to GridFieldDetailForm::setFields()'
|
||||
. " or implement a getCMSFields() method on {$modelClass}"
|
||||
);
|
||||
}
|
||||
$fields = $this->record->getCMSFields();
|
||||
}
|
||||
|
||||
@ -249,15 +261,15 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
|
||||
$form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT);
|
||||
|
||||
if ($this->record->ID && !$this->record->canEdit()) {
|
||||
if ($this->record->ID && (!$this->record->hasMethod('canEdit') || !$this->record->canEdit())) {
|
||||
// Restrict editing of existing records
|
||||
$form->makeReadonly();
|
||||
// Hack to re-enable delete button if user can delete
|
||||
if ($this->record->canDelete()) {
|
||||
if ($this->record->hasMethod('canDelete') && $this->record->canDelete()) {
|
||||
$form->Actions()->fieldByName('action_doDelete')->setReadonly(false);
|
||||
}
|
||||
} elseif (!$this->record->ID
|
||||
&& !$this->record->canCreate(null, $this->getCreateContext())
|
||||
&& (!$this->record->hasMethod('canCreate') || !$this->record->canCreate(null, $this->getCreateContext()))
|
||||
) {
|
||||
// Restrict creation of new records
|
||||
$form->makeReadonly();
|
||||
@ -367,7 +379,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
|
||||
$rightGroup->push($previousAndNextGroup);
|
||||
|
||||
if ($component && $component->getShowAdd() && $this->record->canCreate()) {
|
||||
if ($component && $component->getShowAdd() && $this->record->hasMethod('canCreate') && $this->record->canCreate()) {
|
||||
$rightGroup->push(
|
||||
LiteralField::create(
|
||||
'new-record',
|
||||
@ -386,7 +398,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the set of form field actions for this DataObject
|
||||
* Build the set of form field actions for the record being handled
|
||||
*
|
||||
* @return FieldList
|
||||
*/
|
||||
@ -399,8 +411,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
$majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
|
||||
$actions->push($majorActions);
|
||||
|
||||
if ($this->record->ID !== 0) { // existing record
|
||||
if ($this->record->canEdit()) {
|
||||
if ($this->record->ID !== null && $this->record->ID !== 0) { // existing record
|
||||
if ($this->record->hasMethod('canEdit') && $this->record->canEdit()) {
|
||||
if (!($this->record instanceof DataObjectInterface)) {
|
||||
throw new LogicException(get_class($this->record) . ' must implement ' . DataObjectInterface::class);
|
||||
}
|
||||
|
||||
$noChangesClasses = 'btn-outline-primary font-icon-tick';
|
||||
$majorActions->push(FormAction::create('doSave', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Save', 'Save'))
|
||||
->addExtraClass($noChangesClasses)
|
||||
@ -410,7 +426,10 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
->setAttribute('data-text-alternate', _t('SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT', 'Save')));
|
||||
}
|
||||
|
||||
if ($this->record->canDelete()) {
|
||||
if ($this->record->hasMethod('canDelete') && $this->record->canDelete()) {
|
||||
if (!($this->record instanceof DataObjectInterface)) {
|
||||
throw new LogicException(get_class($this->record) . ' must implement ' . DataObjectInterface::class);
|
||||
}
|
||||
$actions->insertAfter('MajorActions', FormAction::create('doDelete', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Delete', 'Delete'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('btn-outline-danger btn-hide-outline font-icon-trash-bin action--delete'));
|
||||
@ -490,9 +509,9 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
* {@see Form::saveInto()}
|
||||
*
|
||||
* Handles detection of falsey values explicitly saved into the
|
||||
* DataObject by formfields
|
||||
* record by formfields
|
||||
*
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param SS_List $list
|
||||
* @return array List of data to write to the relation
|
||||
*/
|
||||
@ -518,11 +537,11 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
$isNewRecord = $this->record->ID == 0;
|
||||
|
||||
// Check permission
|
||||
if (!$this->record->canEdit()) {
|
||||
if (!$this->record->hasMethod('canEdit') || !$this->record->canEdit()) {
|
||||
$this->httpError(403, _t(
|
||||
__CLASS__ . '.EditPermissionsFailure',
|
||||
'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"',
|
||||
['ObjectTitle' => $this->record->singular_name()]
|
||||
['ObjectTitle' => $this->getModelName()]
|
||||
));
|
||||
return null;
|
||||
}
|
||||
@ -537,7 +556,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved',
|
||||
'Saved {name} {link}',
|
||||
[
|
||||
'name' => $this->record->i18n_singular_name(),
|
||||
'name' => $this->getModelName(),
|
||||
'link' => $link
|
||||
]
|
||||
);
|
||||
@ -744,12 +763,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given form data into the underlying dataobject and relation
|
||||
* Loads the given form data into the underlying record and relation
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @throws ValidationException On error
|
||||
* @return DataObject Saved record
|
||||
* @return ViewableData&DataObjectInterface Saved record
|
||||
*/
|
||||
protected function saveFormIntoRecord($data, $form)
|
||||
{
|
||||
@ -766,7 +785,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
$this->record = $this->record->newClassInstance($newClassName);
|
||||
}
|
||||
|
||||
// Save form and any extra saved data into this dataobject.
|
||||
// Save form and any extra saved data into this record.
|
||||
// Set writeComponents = true to write has-one relations / join records
|
||||
$form->saveInto($this->record);
|
||||
// https://github.com/silverstripe/silverstripe-assets/issues/365
|
||||
@ -788,7 +807,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
public function doDelete($data, $form)
|
||||
{
|
||||
$title = $this->record->Title;
|
||||
if (!$this->record->canDelete()) {
|
||||
if (!$this->record->hasMethod('canDelete') || !$this->record->canDelete()) {
|
||||
throw new ValidationException(
|
||||
_t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions")
|
||||
);
|
||||
@ -799,7 +818,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted',
|
||||
'Deleted {type} "{name}"',
|
||||
[
|
||||
'type' => $this->record->i18n_singular_name(),
|
||||
'type' => $this->getModelName(),
|
||||
'name' => htmlspecialchars($title ?? '', ENT_QUOTES)
|
||||
]
|
||||
);
|
||||
@ -870,7 +889,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataObject
|
||||
* @return ViewableData
|
||||
*/
|
||||
public function getRecord()
|
||||
{
|
||||
@ -906,7 +925,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
]));
|
||||
} else {
|
||||
$items->push(ArrayData::create([
|
||||
'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]),
|
||||
'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->getModelName()]),
|
||||
'Link' => false
|
||||
]));
|
||||
}
|
||||
@ -920,4 +939,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
$this->extend('updateBreadcrumbs', $items);
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function getModelName(): string
|
||||
{
|
||||
if ($this->record->hasMethod('i18n_singular_name')) {
|
||||
return $this->record->i18n_singular_name();
|
||||
}
|
||||
return ClassInfo::shortName($this->record);
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Provides the entry point to editing a single record presented by the
|
||||
@ -91,7 +91,7 @@ class GridFieldEditButton extends AbstractGridFieldComponent implements GridFiel
|
||||
* Return any special attributes that will be used for FormField::create_tag()
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
@ -139,7 +139,7 @@ class GridFieldEditButton extends AbstractGridFieldComponent implements GridFiel
|
||||
|
||||
/**
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
* @param string $columnName
|
||||
* @return string The HTML for the column
|
||||
*/
|
||||
|
@ -3,12 +3,13 @@
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use League\Csv\Writer;
|
||||
use LogicException;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Adds an "Export list" button to the bottom of a {@link GridField}.
|
||||
@ -155,7 +156,15 @@ class GridFieldExportButton extends AbstractGridFieldComponent implements GridFi
|
||||
return $dataCols->getDisplayFields($gridField);
|
||||
}
|
||||
|
||||
return DataObject::singleton($gridField->getModelClass())->summaryFields();
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$singleton = singleton($modelClass);
|
||||
if (!$singleton->hasMethod('summaryFields')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
}
|
||||
return $singleton->summaryFields();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -225,8 +234,9 @@ class GridFieldExportButton extends AbstractGridFieldComponent implements GridFi
|
||||
// Remove limit as the list may be paginated, we want the full list for the export
|
||||
$items = $items->limit(null);
|
||||
|
||||
/** @var DataObject $item */
|
||||
/** @var ViewableData $item */
|
||||
foreach ($items as $item) {
|
||||
// Assume item can be viewed if canView() isn't implemented
|
||||
if (!$item->hasMethod('canView') || $item->canView()) {
|
||||
$columnData = [];
|
||||
|
||||
|
@ -7,10 +7,12 @@ use SilverStripe\Admin\LeftAndMain;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPResponse;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\Schema\FormSchema;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\ORM\Search\SearchContext;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
@ -27,11 +29,12 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
*
|
||||
* @var bool
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
protected $throwExceptionOnBadDataType = true;
|
||||
|
||||
/**
|
||||
* @var \SilverStripe\ORM\Search\SearchContext
|
||||
* @var SearchContext
|
||||
*/
|
||||
protected $searchContext = null;
|
||||
|
||||
@ -66,17 +69,21 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
* {@link GridFieldConfig} subclasses set this to false for flexibility.
|
||||
*
|
||||
* @param bool $throwExceptionOnBadDataType
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function setThrowExceptionOnBadDataType($throwExceptionOnBadDataType)
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
$this->throwExceptionOnBadDataType = $throwExceptionOnBadDataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function getThrowExceptionOnBadDataType()
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
return $this->throwExceptionOnBadDataType;
|
||||
}
|
||||
|
||||
@ -103,6 +110,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
if ($dataList instanceof Filterable) {
|
||||
return true;
|
||||
} else {
|
||||
// This will be changed to always throw an exception in a future major release.
|
||||
if ($this->throwExceptionOnBadDataType) {
|
||||
throw new LogicException(
|
||||
static::class . " expects an SS_Filterable list to be passed to the GridField."
|
||||
@ -243,17 +251,35 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
* Generate a search context based on the model class of the of the GridField
|
||||
*
|
||||
* @param GridField $gridfield
|
||||
* @return \SilverStripe\ORM\Search\SearchContext
|
||||
* @return SearchContext
|
||||
*/
|
||||
public function getSearchContext(GridField $gridField)
|
||||
{
|
||||
if (!$this->searchContext) {
|
||||
$this->searchContext = singleton($gridField->getModelClass())->getDefaultSearchContext();
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$singleton = singleton($modelClass);
|
||||
if (!$singleton->hasMethod('getDefaultSearchContext')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically instantiate SearchContext. Pass the SearchContext to setSearchContext()'
|
||||
. " or implement a getDefaultSearchContext() method on $modelClass"
|
||||
);
|
||||
}
|
||||
$this->searchContext = $singleton->getDefaultSearchContext();
|
||||
}
|
||||
|
||||
return $this->searchContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific SearchContext instance for this component to use, instead of the default
|
||||
* context provided by the ModelClass.
|
||||
*/
|
||||
public function setSearchContext(SearchContext $context): static
|
||||
{
|
||||
$this->searchContext = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search field schema for the component
|
||||
*
|
||||
@ -280,8 +306,6 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$searchField = $searchField && property_exists($searchField, 'name') ? $searchField->name : null;
|
||||
}
|
||||
|
||||
$name = $gridField->Title ?: $inst->i18n_plural_name();
|
||||
|
||||
// Prefix "Search__" onto the filters for the React component
|
||||
$filters = $context->getSearchParams();
|
||||
if (!empty($filters)) {
|
||||
@ -295,7 +319,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$schema = [
|
||||
'formSchemaUrl' => $schemaUrl,
|
||||
'name' => $searchField,
|
||||
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $name]),
|
||||
'placeholder' => _t(__CLASS__ . '.Search', 'Search "{name}"', ['name' => $this->getTitle($gridField, $inst)]),
|
||||
'filters' => $filters ?: new \stdClass, // stdClass maps to empty json object '{}'
|
||||
'gridfield' => $gridField->getName(),
|
||||
'searchAction' => $searchAction->getAttribute('name'),
|
||||
@ -307,6 +331,19 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
return json_encode($schema);
|
||||
}
|
||||
|
||||
private function getTitle(GridField $gridField, object $inst): string
|
||||
{
|
||||
if ($gridField->Title) {
|
||||
return $gridField->Title;
|
||||
}
|
||||
|
||||
if (ClassInfo::hasMethod($inst, 'i18n_plural_name')) {
|
||||
return $inst->i18n_plural_name();
|
||||
}
|
||||
|
||||
return ClassInfo::shortName($inst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search form for the component
|
||||
*
|
||||
@ -350,7 +387,7 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi
|
||||
$field->addExtraClass('stacked no-change-track');
|
||||
}
|
||||
|
||||
$name = $gridField->Title ?: singleton($gridField->getModelClass())->i18n_plural_name();
|
||||
$name = $this->getTitle($gridField, singleton($gridField->getModelClass()));
|
||||
|
||||
$this->searchForm = $form = new Form(
|
||||
$gridField,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\ORM\Hierarchy\Hierarchy;
|
||||
@ -13,6 +14,8 @@ use SilverStripe\View\SSViewer;
|
||||
* Adds a "level up" link to a GridField table, which is useful when viewing
|
||||
* hierarchical data. Requires the managed record to have a "getParent()"
|
||||
* method or has_one relationship called "Parent".
|
||||
*
|
||||
* The modelClass of the GridField this component is in must be a DataObject subclass.
|
||||
*/
|
||||
class GridFieldLevelup extends AbstractGridFieldComponent implements GridField_HTMLProvider
|
||||
{
|
||||
@ -53,6 +56,10 @@ class GridFieldLevelup extends AbstractGridFieldComponent implements GridField_H
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$parentID = 0;
|
||||
|
||||
if (!is_a($modelClass, DataObject::class, true)) {
|
||||
throw new LogicException(__CLASS__ . " must be used with DataObject subclasses. Found '$modelClass'");
|
||||
}
|
||||
|
||||
if (!$this->currentID) {
|
||||
return null;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use SilverStripe\ORM\UnsavedRelationList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use LogicException;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
* GridFieldPaginator paginates the {@link GridField} list and adds controls
|
||||
@ -33,6 +34,7 @@ class GridFieldPaginator extends AbstractGridFieldComponent implements GridField
|
||||
|
||||
/**
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
protected $throwExceptionOnBadDataType = true;
|
||||
|
||||
@ -57,9 +59,11 @@ class GridFieldPaginator extends AbstractGridFieldComponent implements GridField
|
||||
*
|
||||
* @param bool $throwExceptionOnBadDataType
|
||||
* @return $this
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function setThrowExceptionOnBadDataType($throwExceptionOnBadDataType)
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
$this->throwExceptionOnBadDataType = $throwExceptionOnBadDataType;
|
||||
return $this;
|
||||
}
|
||||
@ -68,9 +72,11 @@ class GridFieldPaginator extends AbstractGridFieldComponent implements GridField
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function getThrowExceptionOnBadDataType()
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
return $this->throwExceptionOnBadDataType;
|
||||
}
|
||||
|
||||
@ -86,6 +92,7 @@ class GridFieldPaginator extends AbstractGridFieldComponent implements GridField
|
||||
if ($dataList instanceof Limitable) {
|
||||
return true;
|
||||
} else {
|
||||
// This will be changed to always throw an exception in a future major release.
|
||||
if ($this->throwExceptionOnBadDataType) {
|
||||
throw new LogicException(
|
||||
static::class . " expects an SS_Limitable list to be passed to the GridField."
|
||||
|
@ -2,16 +2,17 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Core\Extensible;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\FieldType\DBHTMLText;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\Requirements;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Adds an "Print" button to the bottom or top of a GridField.
|
||||
@ -161,7 +162,15 @@ class GridFieldPrintButton extends AbstractGridFieldComponent implements GridFie
|
||||
return $dataCols->getDisplayFields($gridField);
|
||||
}
|
||||
|
||||
return DataObject::singleton($gridField->getModelClass())->summaryFields();
|
||||
$modelClass = $gridField->getModelClass();
|
||||
$singleton = singleton($modelClass);
|
||||
if (!$singleton->hasMethod('summaryFields')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
}
|
||||
return $singleton->summaryFields();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,8 +235,9 @@ class GridFieldPrintButton extends AbstractGridFieldComponent implements GridFie
|
||||
/** @var GridFieldDataColumns $gridFieldColumnsComponent */
|
||||
$gridFieldColumnsComponent = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class);
|
||||
|
||||
/** @var DataObject $item */
|
||||
/** @var ViewableData $item */
|
||||
foreach ($items->limit(null) as $item) {
|
||||
// Assume item can be viewed if canView() isn't implemented
|
||||
if (!$item->hasMethod('canView') || $item->canView()) {
|
||||
$itemRow = new ArrayList();
|
||||
|
||||
|
@ -11,7 +11,9 @@ use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use SilverStripe\View\SSViewer;
|
||||
use LogicException;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
|
||||
/**
|
||||
* GridFieldSortableHeader adds column headers to a {@link GridField} that can
|
||||
@ -26,6 +28,7 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
*
|
||||
* @var bool
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
protected $throwExceptionOnBadDataType = true;
|
||||
|
||||
@ -45,9 +48,11 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
|
||||
*
|
||||
* @param bool $throwExceptionOnBadDataType
|
||||
* @return $this
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function setThrowExceptionOnBadDataType($throwExceptionOnBadDataType)
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
$this->throwExceptionOnBadDataType = $throwExceptionOnBadDataType;
|
||||
return $this;
|
||||
}
|
||||
@ -56,9 +61,11 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
|
||||
* See {@link setThrowExceptionOnBadDataType()}
|
||||
*
|
||||
* @return bool
|
||||
* @deprecated 5.2.0 Will be removed without equivalent functionality
|
||||
*/
|
||||
public function getThrowExceptionOnBadDataType()
|
||||
{
|
||||
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
|
||||
return $this->throwExceptionOnBadDataType;
|
||||
}
|
||||
|
||||
@ -74,6 +81,7 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
|
||||
if ($dataList instanceof Sortable) {
|
||||
return true;
|
||||
} else {
|
||||
// This will be changed to always throw an exception in a future major release.
|
||||
if ($this->throwExceptionOnBadDataType) {
|
||||
throw new LogicException(
|
||||
static::class . " expects an SS_Sortable list to be passed to the GridField."
|
||||
@ -147,7 +155,7 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid
|
||||
if ($tmpItem instanceof SS_List) {
|
||||
// It's impossible to sort on a HasManyList/ManyManyList
|
||||
break;
|
||||
} elseif ($tmpItem && method_exists($tmpItem, 'hasMethod') && $tmpItem->hasMethod($methodName)) {
|
||||
} elseif ($tmpItem && ClassInfo::hasMethod($tmpItem, $methodName)) {
|
||||
// The part is a relation name, so get the object/list from it
|
||||
$tmpItem = $tmpItem->$methodName();
|
||||
} elseif ($tmpItem instanceof DataObject
|
||||
|
@ -62,7 +62,8 @@ class GridFieldViewButton extends AbstractGridFieldComponent implements GridFiel
|
||||
|
||||
public function getColumnContent($field, $record, $col)
|
||||
{
|
||||
if (!$record->canView()) {
|
||||
// Assume item can be viewed if canView() isn't implemented
|
||||
if ($record->hasMethod('canView') && !$record->canView()) {
|
||||
return null;
|
||||
}
|
||||
$data = new ArrayData([
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* GridField action menu item interface, this provides data so the action
|
||||
@ -21,7 +21,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent
|
||||
* @see {@link GridField_ActionMenu->getColumnContent()}
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return string $title
|
||||
*/
|
||||
@ -33,7 +33,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent
|
||||
* @see {@link GridField_ActionMenu->getColumnContent()}
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return array $data
|
||||
*/
|
||||
@ -46,7 +46,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent
|
||||
* @see {@link GridField_ActionMenu->getColumnContent()}
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return string|null $group
|
||||
*/
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* Allows GridField_ActionMenuItem to act as a link
|
||||
@ -15,7 +15,7 @@ interface GridField_ActionMenuLink extends GridField_ActionMenuItem
|
||||
* @see {@link GridField_ActionMenu->getColumnContent()}
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param ViewableData $record
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
|
@ -34,7 +34,7 @@ interface GridField_ColumnProvider extends GridFieldComponent
|
||||
* HTML for the column, content of the <td> element.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record - Record displayed in this row
|
||||
* @param ViewableData $record - Record displayed in this row
|
||||
* @param string $columnName
|
||||
* @return string - HTML for the column. Return NULL to skip.
|
||||
*/
|
||||
@ -44,7 +44,7 @@ interface GridField_ColumnProvider extends GridFieldComponent
|
||||
* Attributes for the element containing the content returned by {@link getColumnContent()}.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record displayed in this row
|
||||
* @param ViewableData $record displayed in this row
|
||||
* @param string $columnName
|
||||
* @return array
|
||||
*/
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\Forms\GridField;
|
||||
|
||||
use SilverStripe\ORM\DataObjectInterface;
|
||||
use SilverStripe\View\ViewableData;
|
||||
|
||||
/**
|
||||
* A component which is used to handle when a {@link GridField} is saved into
|
||||
@ -15,7 +16,7 @@ interface GridField_SaveHandler extends GridFieldComponent
|
||||
* Called when a grid field is saved - i.e. the form is submitted.
|
||||
*
|
||||
* @param GridField $grid
|
||||
* @param DataObjectInterface $record
|
||||
* @param DataObjectInterface&ViewableData $record
|
||||
*/
|
||||
public function handleSave(GridField $grid, DataObjectInterface $record);
|
||||
}
|
||||
|
188
src/ORM/Search/BasicSearchContext.php
Normal file
188
src/ORM/Search/BasicSearchContext.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Search;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\ORM\Filterable;
|
||||
use SilverStripe\ORM\Filters\PartialMatchFilter;
|
||||
use SilverStripe\ORM\Filters\SearchFilter;
|
||||
use SilverStripe\ORM\Limitable;
|
||||
use SilverStripe\ORM\Sortable;
|
||||
|
||||
/**
|
||||
* A SearchContext that can be used with non-ORM data.
|
||||
* This class isn't guaranteed to respect the full searchable fields spec defined on DataObject classes.
|
||||
*/
|
||||
class BasicSearchContext extends SearchContext
|
||||
{
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* Name of the field which, if included in search forms passed to this object, will be used
|
||||
* to search across all searchable fields.
|
||||
*/
|
||||
private static $general_search_field_name = 'q';
|
||||
|
||||
/**
|
||||
* Returns a list which has been limited, sorted, and filtered by the given parameters.
|
||||
*
|
||||
* @param array $searchParams Map of search criteria, mostly taken from $_REQUEST.
|
||||
* If a filter is applied to a relationship in dot notation,
|
||||
* the parameter name should have the dots replaced with double underscores,
|
||||
* for example "Comments__Name" instead of the filter name "Comments.Name".
|
||||
* @param array|bool|string $sort Field to sort on.
|
||||
* @param array|null|string $limit
|
||||
* @param Filterable&Sortable&Limitable $existingQuery
|
||||
*/
|
||||
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null): Filterable&Sortable&Limitable
|
||||
{
|
||||
if (!$existingQuery || !($existingQuery instanceof Filterable) || !($existingQuery instanceof Sortable) || !($existingQuery instanceof Limitable)) {
|
||||
throw new InvalidArgumentException('getQuery requires a pre-existing filterable/sortable/limitable list to be passed as $existingQuery.');
|
||||
}
|
||||
|
||||
if ((count(func_get_args()) >= 3) && (!in_array(gettype($limit), ['array', 'NULL', 'string']))) {
|
||||
Deprecation::notice(
|
||||
'5.1.0',
|
||||
'$limit should be type of array|string|null'
|
||||
);
|
||||
$limit = null;
|
||||
}
|
||||
|
||||
$searchParams = $this->applySearchFilters($this->normaliseSearchParams($searchParams));
|
||||
$result = $this->applyGeneralSearchField($searchParams, $existingQuery);
|
||||
|
||||
// Filter the list by the requested filters.
|
||||
if (!empty($searchParams)) {
|
||||
$result = $result->filter($searchParams);
|
||||
}
|
||||
|
||||
// Only sort if a sort value is provided - sort by "false" just means use the existing sort.
|
||||
if ($sort) {
|
||||
$result = $result->sort($sort);
|
||||
}
|
||||
|
||||
// Limit must be last so that ArrayList results don't have an applied limit before they can be filtered/sorted.
|
||||
$result = $result->limit($limit);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function normaliseSearchParams(array $searchParams): array
|
||||
{
|
||||
$normalised = [];
|
||||
foreach ($searchParams as $field => $searchTerm) {
|
||||
if ($this->clearEmptySearchFields($searchTerm)) {
|
||||
$normalised[str_replace('__', '.', $field)] = $searchTerm;
|
||||
}
|
||||
}
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
private function applySearchFilters(array $searchParams): array
|
||||
{
|
||||
$applied = [];
|
||||
foreach ($searchParams as $fieldName => $searchTerm) {
|
||||
// Ignore the general search field - we'll deal with that in a special way.
|
||||
if ($fieldName === static::config()->get('general_search_field_name')) {
|
||||
$applied[$fieldName] = $searchTerm;
|
||||
continue;
|
||||
}
|
||||
$filterTerm = $this->getFilterTerm($fieldName);
|
||||
$applied["{$fieldName}:{$filterTerm}"] = $searchTerm;
|
||||
}
|
||||
return $applied;
|
||||
}
|
||||
|
||||
private function applyGeneralSearchField(array &$searchParams, Filterable $existingQuery): Filterable
|
||||
{
|
||||
$generalFieldName = static::config()->get('general_search_field_name');
|
||||
if (array_key_exists($generalFieldName, $searchParams)) {
|
||||
$searchTerm = $searchParams[$generalFieldName];
|
||||
if (Config::inst()->get($this->modelClass, 'general_search_split_terms') !== false) {
|
||||
$searchTerm = explode(' ', $searchTerm);
|
||||
}
|
||||
$generalFilter = [];
|
||||
foreach ($this->getSearchFields()->dataFieldNames() as $fieldName) {
|
||||
if ($fieldName === $generalFieldName) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->getCanGeneralSearch($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
$filterTerm = $this->getGeneralSearchFilterTerm($fieldName);
|
||||
$generalFilter["{$fieldName}:{$filterTerm}"] = $searchTerm;
|
||||
}
|
||||
$result = $existingQuery->filterAny($generalFilter);
|
||||
unset($searchParams[$generalFieldName]);
|
||||
}
|
||||
|
||||
return $result ?? $existingQuery;
|
||||
}
|
||||
|
||||
private function getCanGeneralSearch(string $fieldName): bool
|
||||
{
|
||||
$singleton = singleton($this->modelClass);
|
||||
|
||||
// Allowed if we're dealing with arbitrary data.
|
||||
if (!ClassInfo::hasMethod($singleton, 'searchableFields')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fields = $singleton->searchableFields();
|
||||
|
||||
// Not allowed if the field isn't searchable.
|
||||
if (!isset($fields[$fieldName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allowed if 'general' isn't part of the spec, or is explicitly truthy.
|
||||
return !isset($fields[$fieldName]['general']) || $fields[$fieldName]['general'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search filter for the given fieldname when searched from the general search field.
|
||||
*/
|
||||
private function getGeneralSearchFilterTerm(string $fieldName): string
|
||||
{
|
||||
$filterClass = Config::inst()->get($this->modelClass, 'general_search_field_filter');
|
||||
if ($filterClass) {
|
||||
return $this->getTermFromFilter(Injector::inst()->create($filterClass, $fieldName));
|
||||
}
|
||||
|
||||
if ($filterClass === '') {
|
||||
return $this->getFilterTerm($fieldName);
|
||||
}
|
||||
|
||||
return 'PartialMatch:nocase';
|
||||
}
|
||||
|
||||
private function getFilterTerm(string $fieldName): string
|
||||
{
|
||||
$filter = $this->getFilter($fieldName) ?? PartialMatchFilter::create($fieldName);
|
||||
return $this->getTermFromFilter($filter);
|
||||
}
|
||||
|
||||
private function getTermFromFilter(SearchFilter $filter): string
|
||||
{
|
||||
$modifiers = $filter->getModifiers() ?? [];
|
||||
|
||||
// Get the string used to refer to the filter, e.g. "PartialMatch"
|
||||
// Ask the injector for it first - but for any not defined there, fall back to string manipulation.
|
||||
$filterTerm = Injector::inst()->getServiceName(get_class($filter));
|
||||
if (!$filterTerm) {
|
||||
$filterTerm = preg_replace('/Filter$/', '', ClassInfo::shortName($filter));
|
||||
}
|
||||
|
||||
// Add modifiers to filter
|
||||
foreach ($modifiers as $modifier) {
|
||||
$filterTerm .= ":{$modifier}";
|
||||
}
|
||||
|
||||
return $filterTerm;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ use SilverStripe\Forms\SelectField;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
@ -104,7 +105,18 @@ class SearchContext
|
||||
*/
|
||||
public function getSearchFields()
|
||||
{
|
||||
return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
|
||||
if ($this->fields?->exists()) {
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
$singleton = singleton($this->modelClass);
|
||||
if (!$singleton->hasMethod('scaffoldSearchFields')) {
|
||||
throw new LogicException(
|
||||
'Cannot dynamically determine search fields. Pass the fields to setFields()'
|
||||
. " or implement a scaffoldSearchFields() method on {$this->modelClass}"
|
||||
);
|
||||
}
|
||||
return $singleton->scaffoldSearchFields();
|
||||
}
|
||||
|
||||
protected function applyBaseTableFields()
|
||||
@ -431,7 +443,7 @@ class SearchContext
|
||||
*/
|
||||
public function addField($field)
|
||||
{
|
||||
$this->fields->push($field);
|
||||
$this->fields?->push($field);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -441,7 +453,7 @@ class SearchContext
|
||||
*/
|
||||
public function removeFieldByName($fieldName)
|
||||
{
|
||||
$this->fields->removeByName($fieldName);
|
||||
$this->fields?->removeByName($fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -488,7 +500,7 @@ class SearchContext
|
||||
continue;
|
||||
}
|
||||
|
||||
$field = $this->fields->fieldByName($filter->getFullName());
|
||||
$field = $this->fields?->fieldByName($filter->getFullName());
|
||||
if (!$field) {
|
||||
continue;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ use SilverStripe\BehatExtension\Context\MainContextAwareTrait;
|
||||
use SilverStripe\BehatExtension\Utility\StepHelper;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Facebook\WebDriver\WebDriverExpectedCondition;
|
||||
use SilverStripe\SiteConfig\SiteConfig;
|
||||
|
||||
/**
|
||||
@ -445,6 +446,23 @@ JS;
|
||||
$button->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^I click the "([^"]*)" button in the "([^"]*)" gridfield for the "([^"]*)" row, confirming the dialog$/
|
||||
* @param string $buttonLabel
|
||||
* @param string $gridFieldName
|
||||
* @param string $rowName
|
||||
*/
|
||||
public function stepIClickTheGridFieldButtonForRowConfirmingDialog($buttonLabel, $gridFieldName, $rowName)
|
||||
{
|
||||
$this->stepIClickTheGridFieldButtonForRow($buttonLabel, $gridFieldName, $rowName);
|
||||
$session = $this->getSession()->getDriver()->getWebDriver();
|
||||
$session->wait()->until(
|
||||
WebDriverExpectedCondition::alertIsPresent(),
|
||||
"Alert is expected"
|
||||
);
|
||||
$session->switchTo()->alert()->accept();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a button in the gridfield row
|
||||
*
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\Convert;
|
||||
use SilverStripe\Dev\CSSContentParser;
|
||||
@ -17,6 +18,7 @@ use SilverStripe\Forms\Tests\GridField\GridFieldTest\Player;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Stadium;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Team;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldAddExistingAutocompleterTest extends FunctionalTest
|
||||
{
|
||||
@ -167,6 +169,118 @@ class GridFieldAddExistingAutocompleterTest extends FunctionalTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetHTMLFragmentsNeedsDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddExistingAutocompleter::class
|
||||
. " must be used with DataObject subclasses. Found '$dataClass'"
|
||||
);
|
||||
// Calling the method will throw an exception.
|
||||
$component->getHTMLFragments($gridField);
|
||||
}
|
||||
|
||||
public function testGetManipulatedDataNeedsDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddExistingAutocompleter::class
|
||||
. " must be used with DataObject subclasses. Found '$dataClass'"
|
||||
);
|
||||
|
||||
// Calling the method will throw an exception.
|
||||
$component->getManipulatedData($gridField, $list);
|
||||
}
|
||||
|
||||
public function testDoSearchNeedsDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddExistingAutocompleter::class
|
||||
. " must be used with DataObject subclasses. Found '$dataClass'"
|
||||
);
|
||||
|
||||
// Calling the method will throw an exception.
|
||||
$component->doSearch($gridField, new HTTPRequest('GET', ''));
|
||||
}
|
||||
|
||||
public function testScaffoldSearchFieldsNeedsDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddExistingAutocompleter::class
|
||||
. " must be used with DataObject subclasses. Found '$dataClass'"
|
||||
);
|
||||
|
||||
// Calling the method will either throw an exception or not.
|
||||
// The test pass/failure is explicitly about whether an exception is thrown.
|
||||
$component->scaffoldSearchFields($dataClass);
|
||||
}
|
||||
|
||||
public function testGetPlaceholderTextNeedsDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddExistingAutocompleter::class
|
||||
. " must be used with DataObject subclasses. Found '$dataClass'"
|
||||
);
|
||||
|
||||
// Calling the method will either throw an exception or not.
|
||||
// The test pass/failure is explicitly about whether an exception is thrown.
|
||||
$component->getPlaceholderText($dataClass);
|
||||
}
|
||||
|
||||
public function testSetPlaceholderTextDoesntNeedDataObject()
|
||||
{
|
||||
$component = new GridFieldAddExistingAutocompleter();
|
||||
$gridField = $this->getGridFieldForComponent($component);
|
||||
$list = new ArrayList();
|
||||
$dataClass = ArrayData::class;
|
||||
$list->setDataClass($dataClass);
|
||||
$gridField->setList($list);
|
||||
|
||||
// Prevent from being marked risky.
|
||||
// This test passes if there's no exception thrown.
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
$component->setPlaceholderText('');
|
||||
}
|
||||
|
||||
protected function getGridFieldForComponent($component)
|
||||
{
|
||||
$config = GridFieldConfig::create()->addComponents(
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
@ -9,11 +10,14 @@ use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldAddNewButton;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PeopleGroup;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\TestController;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldAddNewButtonTest extends SapphireTest
|
||||
{
|
||||
@ -76,6 +80,24 @@ class GridFieldAddNewButtonTest extends SapphireTest
|
||||
$this->mockButtonFragments($list, null);
|
||||
}
|
||||
|
||||
public function testGetHTMLFragmentsThrowsException()
|
||||
{
|
||||
$component = new GridFieldAddNewButton();
|
||||
$config = new GridFieldConfig_Base();
|
||||
$config->addComponent($component);
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList(), $config);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldAddNewButton::class . ' cannot be used with models that do not implement canCreate().'
|
||||
. " Remove this component from your GridField or implement canCreate() on $modelClass"
|
||||
);
|
||||
|
||||
$component->getHTMLFragments($gridField);
|
||||
}
|
||||
|
||||
protected function mockButtonFragments(SS_List $list, $parent = null)
|
||||
{
|
||||
$form = Form::create(
|
||||
|
@ -3,15 +3,18 @@
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use SilverStripe\Forms\GridField\GridFieldDataColumns;
|
||||
use SilverStripe\Security\Member;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
use stdClass;
|
||||
|
||||
class GridFieldDataColumnsTest extends SapphireTest
|
||||
{
|
||||
|
||||
/**
|
||||
* @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields
|
||||
*/
|
||||
@ -23,6 +26,19 @@ class GridFieldDataColumnsTest extends SapphireTest
|
||||
$this->assertEquals($expected, $columns->getDisplayFields($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields
|
||||
*/
|
||||
public function testGridFieldGetDisplayFieldsWithArrayList()
|
||||
{
|
||||
$list = new ArrayList([new ArrayData(['Title' => 'My Item'])]);
|
||||
$obj = new GridField('testfield', 'testfield', $list);
|
||||
$expected = ['Title' => 'Title'];
|
||||
$columns = $obj->getConfig()->getComponentByType(GridFieldDataColumns::class);
|
||||
$columns->setDisplayFields($expected);
|
||||
$this->assertEquals($expected, $columns->getDisplayFields($obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::setDisplayFields
|
||||
* @covers \SilverStripe\Forms\GridField\GridFieldDataColumns::getDisplayFields
|
||||
@ -76,4 +92,22 @@ class GridFieldDataColumnsTest extends SapphireTest
|
||||
$columns->getFieldFormatting()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetDisplayFieldsThrowsException()
|
||||
{
|
||||
$component = new GridFieldDataColumns();
|
||||
$config = new GridFieldConfig_Base();
|
||||
$config->addComponent($component);
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList(), $config);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically determine columns. Pass the column names to setDisplayFields()'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
|
||||
$component->getDisplayFields($gridField);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Control\HTTPResponse_Exception;
|
||||
@ -12,6 +14,7 @@ use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
|
||||
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Cheerleader;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldTest\Permissions;
|
||||
@ -22,6 +25,7 @@ use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\ValidationException;
|
||||
use SilverStripe\Security\Security;
|
||||
use SilverStripe\Security\SecurityToken;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldDeleteActionTest extends SapphireTest
|
||||
{
|
||||
@ -230,4 +234,70 @@ class GridFieldDeleteActionTest extends SapphireTest
|
||||
$group = $action->getGroup($gridField, $this->list->first(), 'dummy');
|
||||
$this->assertNull($group, 'A menu group does not exist when the user cannot delete');
|
||||
}
|
||||
|
||||
public function provideHandleActionThrowsException()
|
||||
{
|
||||
return [
|
||||
'unlinks relation' => [true],
|
||||
'deletes related record' => [false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideHandleActionThrowsException
|
||||
*/
|
||||
public function testHandleActionThrowsException(bool $unlinkRelation)
|
||||
{
|
||||
$component = new GridFieldDeleteAction();
|
||||
$config = new GridFieldConfig_Base();
|
||||
$config->addComponent($component);
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList([new ArrayData(['ID' => 1])]), $config);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$permissionMethod = $unlinkRelation ? 'canEdit' : 'canDelete';
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldDeleteAction::class . " cannot be used with models that don't implement {$permissionMethod}()."
|
||||
. " Remove this component from your GridField or implement {$permissionMethod}() on $modelClass"
|
||||
);
|
||||
|
||||
// Calling the method will throw an exception.
|
||||
$secondArg = $unlinkRelation ? 'unlinkrelation' : 'deleterecord';
|
||||
$component->handleAction($gridField, $secondArg, ['RecordID' => 1], []);
|
||||
}
|
||||
|
||||
public function provideGetRemoveActionThrowsException()
|
||||
{
|
||||
return [
|
||||
'removes relation' => [true],
|
||||
'deletes related record' => [false],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetRemoveActionThrowsException
|
||||
*/
|
||||
public function testGetRemoveActionThrowsException(bool $removeRelation)
|
||||
{
|
||||
$component = new GridFieldDeleteAction();
|
||||
$component->setRemoveRelation($removeRelation);
|
||||
$config = new GridFieldConfig_Base();
|
||||
$config->addComponent($component);
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList([new ArrayData(['ID' => 1])]), $config);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$permissionMethod = $removeRelation ? 'canEdit' : 'canDelete';
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldDeleteAction::class . " cannot be used with models that don't implement {$permissionMethod}()."
|
||||
. " Remove this component from your GridField or implement {$permissionMethod}() on $modelClass"
|
||||
);
|
||||
|
||||
// Calling the method will throw an exception.
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getRemoveAction');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$reflectionMethod->invokeArgs($component, [$gridField, new ArrayData(), '']);
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,20 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Dev\CSSContentParser;
|
||||
use SilverStripe\Dev\FunctionalTest;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\ArrayDataWithID;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\CategoryController;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\GroupController;
|
||||
@ -18,6 +24,8 @@ use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PeopleGroup;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PolymorphicPeopleGroup;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\TestController;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldDetailFormTest extends FunctionalTest
|
||||
{
|
||||
@ -487,4 +495,121 @@ class GridFieldDetailFormTest extends FunctionalTest
|
||||
|
||||
$this->autoFollowRedirection = $origAutoFollow;
|
||||
}
|
||||
|
||||
public function provideGetRecordFromRequestFindExisting()
|
||||
{
|
||||
return [
|
||||
'No records' => [
|
||||
'data' => [],
|
||||
'hasRecord' => false,
|
||||
],
|
||||
'Records exist but without ID field' => [
|
||||
'data' => [new ArrayDataWithID()],
|
||||
'hasRecord' => false,
|
||||
],
|
||||
'Record exists with matching ID' => [
|
||||
'data' => [new ArrayDataWithID(['ID' => 32])],
|
||||
'hasRecord' => true,
|
||||
],
|
||||
'Record exists, no matching ID' => [
|
||||
'data' => [new ArrayDataWithID(['ID' => 1])],
|
||||
'hasRecord' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetRecordFromRequestFindExisting
|
||||
*/
|
||||
public function testGetRecordFromRequestFindExisting(array $data, bool $hasRecord)
|
||||
{
|
||||
$controller = new TestController();
|
||||
$form = $controller->Form(null, new ArrayList($data));
|
||||
$gridField = $form->Fields()->dataFieldByName('testfield');
|
||||
if (empty($data)) {
|
||||
$gridField->setModelClass(ArrayDataWithID::class);
|
||||
}
|
||||
$component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
|
||||
$request = new HTTPRequest('GET', $gridField->Link('item/32'));
|
||||
$request->match(Controller::join_links($gridField->Link(), 'item/$ID'));
|
||||
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$this->assertSame($hasRecord, (bool) $reflectionMethod->invoke($component, $gridField, $request));
|
||||
}
|
||||
|
||||
public function provideGetRecordFromRequestCreateNew()
|
||||
{
|
||||
// Note that in all of these scenarios a new record gets created, so it *shouldn't* matter what's already in there.
|
||||
return [
|
||||
'No records' => [
|
||||
'data' => [],
|
||||
],
|
||||
'Records exist but without ID field' => [
|
||||
'data' => [new ArrayDataWithID()],
|
||||
],
|
||||
'Record exists with ID field' => [
|
||||
'data' => [new ArrayDataWithID(['ID' => 32])],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetRecordFromRequestCreateNew
|
||||
*/
|
||||
public function testGetRecordFromRequestCreateNew(array $data)
|
||||
{
|
||||
$controller = new TestController();
|
||||
$form = $controller->Form(null, new ArrayList($data));
|
||||
$gridField = $form->Fields()->dataFieldByName('testfield');
|
||||
if (empty($data)) {
|
||||
$gridField->setModelClass(ArrayDataWithID::class);
|
||||
}
|
||||
$component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
|
||||
$request = new HTTPRequest('GET', $gridField->Link('item/new'));
|
||||
$request->match(Controller::join_links($gridField->Link(), 'item/$ID'));
|
||||
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$this->assertEquals(new ArrayDataWithID(['ID' => 0]), $reflectionMethod->invoke($component, $gridField, $request));
|
||||
}
|
||||
|
||||
public function provideGetRecordFromRequestWithoutData()
|
||||
{
|
||||
// Note that in all of these scenarios a new record gets created, so it *shouldn't* matter what's already in there.
|
||||
return [
|
||||
'No records' => [
|
||||
'data' => [],
|
||||
],
|
||||
'Records exist but without ID field' => [
|
||||
'data' => [new ArrayData()],
|
||||
],
|
||||
'Record exists with ID field' => [
|
||||
'data' => [new ArrayData(['ID' => 32])],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetRecordFromRequestWithoutData
|
||||
*/
|
||||
public function testGetRecordFromRequestWithoutData(array $data)
|
||||
{
|
||||
$controller = new TestController();
|
||||
$form = $controller->Form(null, new ArrayList($data));
|
||||
$gridField = $form->Fields()->dataFieldByName('testfield');
|
||||
if (empty($data)) {
|
||||
$gridField->setModelClass(ArrayData::class);
|
||||
}
|
||||
$component = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
|
||||
$request = new HTTPRequest('GET', $gridField->Link('item/new'));
|
||||
$request->match(Controller::join_links($gridField->Link(), 'item/$ID'));
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(ArrayData::class . ' must have an ID field.');
|
||||
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getRecordFromRequest');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$reflectionMethod->invoke($component, $gridField, $request);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class ArrayDataWithID extends ArrayData implements TestOnly
|
||||
{
|
||||
public function __construct($value = [])
|
||||
{
|
||||
$value['ID'] ??= 0;
|
||||
parent::__construct($value);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest;
|
||||
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\Form;
|
||||
@ -12,6 +13,7 @@ use SilverStripe\Forms\GridField\GridFieldDetailForm;
|
||||
use SilverStripe\Forms\GridField\GridFieldEditButton;
|
||||
use SilverStripe\Forms\GridField\GridFieldToolbarHeader;
|
||||
use SilverStripe\Forms\GridField\GridFieldViewButton;
|
||||
use SilverStripe\ORM\SS_List;
|
||||
|
||||
class TestController extends Controller implements TestOnly
|
||||
{
|
||||
@ -32,14 +34,17 @@ class TestController extends Controller implements TestOnly
|
||||
|
||||
protected $template = 'BlankPage';
|
||||
|
||||
public function Form()
|
||||
public function Form(?HTTPRequest $request = null, ?SS_List $list = null)
|
||||
{
|
||||
$group = PeopleGroup::get()
|
||||
->filter('Name', 'My Group')
|
||||
->sort('Name')
|
||||
->First();
|
||||
if (!$list) {
|
||||
$group = PeopleGroup::get()
|
||||
->filter('Name', 'My Group')
|
||||
->sort('Name')
|
||||
->First();
|
||||
$list = $group->People();
|
||||
}
|
||||
|
||||
$field = new GridField('testfield', 'testfield', $group->People());
|
||||
$field = new GridField('testfield', 'testfield', $list);
|
||||
$field->getConfig()->addComponent(new GridFieldToolbarHeader());
|
||||
$field->getConfig()->addComponent(new GridFieldAddNewButton('toolbar-header-right'));
|
||||
$field->getConfig()->addComponent(new GridFieldViewButton());
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig_Base;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm;
|
||||
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldDetailForm_ItemRequestTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = false;
|
||||
|
||||
public function testItemEditFormThrowsException()
|
||||
{
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList(), new GridFieldConfig_Base());
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
$itemRequest = new GridFieldDetailForm_ItemRequest($gridField, new GridFieldDetailForm(), new ArrayData(), new Controller(), '');
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically determine form fields. Pass the fields to GridFieldDetailForm::setFields()'
|
||||
. " or implement a getCMSFields() method on $modelClass"
|
||||
);
|
||||
|
||||
$itemRequest->ItemEditForm();
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use League\Csv\Reader;
|
||||
use LogicException;
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldExportButtonTest\NoView;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldExportButtonTest\Team;
|
||||
use SilverStripe\ORM\DataList;
|
||||
@ -12,8 +14,10 @@ use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldExportButton;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldDataColumns;
|
||||
use SilverStripe\Forms\GridField\GridFieldPaginator;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldExportButtonTest extends SapphireTest
|
||||
{
|
||||
@ -155,13 +159,16 @@ class GridFieldExportButtonTest extends SapphireTest
|
||||
public function testArrayListInput()
|
||||
{
|
||||
$button = new GridFieldExportButton();
|
||||
$columns = new GridFieldDataColumns();
|
||||
$columns->setDisplayFields(['ID' => 'ID']);
|
||||
$this->gridField->getConfig()->addComponent($columns);
|
||||
$this->gridField->getConfig()->addComponent(new GridFieldPaginator());
|
||||
|
||||
//Create an ArrayList 1 greater the Paginator's default 15 rows
|
||||
$arrayList = new ArrayList();
|
||||
for ($i = 1; $i <= 16; $i++) {
|
||||
$dataobject = new DataObject(['ID' => $i]);
|
||||
$arrayList->add($dataobject);
|
||||
$datum = new ArrayData(['ID' => $i]);
|
||||
$arrayList->add($datum);
|
||||
}
|
||||
$this->gridField->setList($arrayList);
|
||||
|
||||
@ -192,6 +199,25 @@ class GridFieldExportButtonTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetExportColumnsForGridFieldThrowsException()
|
||||
{
|
||||
$component = new GridFieldExportButton();
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList());
|
||||
$gridField->getConfig()->removeComponentsByType(GridFieldDataColumns::class);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getExportColumnsForGridField');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$reflectionMethod->invoke($component, $gridField);
|
||||
}
|
||||
|
||||
protected function createReader($string)
|
||||
{
|
||||
$reader = Reader::createFromString($string);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
@ -20,6 +21,7 @@ use SilverStripe\Forms\Tests\GridField\GridFieldFilterHeaderTest\TeamGroup;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\DataList;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldFilterHeaderTest extends SapphireTest
|
||||
{
|
||||
@ -226,4 +228,20 @@ class GridFieldFilterHeaderTest extends SapphireTest
|
||||
|
||||
$this->assertNull($htmlFragment);
|
||||
}
|
||||
|
||||
public function testGetDisplayFieldsThrowsException()
|
||||
{
|
||||
$component = new GridFieldFilterHeader();
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList());
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically instantiate SearchContext. Pass the SearchContext to setSearchContext()'
|
||||
. " or implement a getDefaultSearchContext() method on $modelClass"
|
||||
);
|
||||
|
||||
$component->getSearchContext($gridField);
|
||||
}
|
||||
}
|
||||
|
29
tests/php/Forms/GridField/GridFieldLevelupTest.php
Normal file
29
tests/php/Forms/GridField/GridFieldLevelupTest.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldLevelup;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldLevelupTest extends SapphireTest
|
||||
{
|
||||
protected $usesDatabase = false;
|
||||
|
||||
public function testGetHTMLFragmentsThrowsException()
|
||||
{
|
||||
$component = new GridFieldLevelup(0);
|
||||
$gridField = new GridField('dummy');
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
GridFieldLevelup::class . " must be used with DataObject subclasses. Found '$modelClass'"
|
||||
);
|
||||
|
||||
$component->getHTMLFragments($gridField);
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace SilverStripe\Forms\Tests\GridField;
|
||||
|
||||
use LogicException;
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Control\Controller;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
@ -10,7 +12,10 @@ use SilverStripe\Forms\GridField\GridFieldPrintButton;
|
||||
use SilverStripe\Forms\GridField\GridFieldConfig;
|
||||
use SilverStripe\Forms\GridField\GridFieldPaginator;
|
||||
use SilverStripe\Forms\GridField\GridField;
|
||||
use SilverStripe\Forms\GridField\GridFieldDataColumns;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldPrintButtonTest\TestObject;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class GridFieldPrintButtonTest extends SapphireTest
|
||||
{
|
||||
@ -33,21 +38,19 @@ class GridFieldPrintButtonTest extends SapphireTest
|
||||
|
||||
public function testLimit()
|
||||
{
|
||||
$this->assertEquals(42, $this->getTestableRows()->count());
|
||||
$this->assertEquals(42, $this->getTestableRows(TestObject::get())->count());
|
||||
}
|
||||
|
||||
public function testCanViewIsRespected()
|
||||
{
|
||||
$orig = TestObject::$canView;
|
||||
TestObject::$canView = false;
|
||||
$this->assertEquals(0, $this->getTestableRows()->count());
|
||||
$this->assertEquals(0, $this->getTestableRows(TestObject::get())->count());
|
||||
TestObject::$canView = $orig;
|
||||
}
|
||||
|
||||
private function getTestableRows()
|
||||
private function getTestableRows($list)
|
||||
{
|
||||
$list = TestObject::get();
|
||||
|
||||
$button = new GridFieldPrintButton();
|
||||
$button->setPrintColumns(['Name' => 'My Name']);
|
||||
|
||||
@ -62,4 +65,50 @@ class GridFieldPrintButtonTest extends SapphireTest
|
||||
$printData = $button->generatePrintData($gridField);
|
||||
return $printData->ItemRows;
|
||||
}
|
||||
|
||||
public function testGeneratePrintData()
|
||||
{
|
||||
$names = [
|
||||
'Bob',
|
||||
'Alice',
|
||||
'John',
|
||||
'Jane',
|
||||
'Sam',
|
||||
];
|
||||
|
||||
$list = new ArrayList();
|
||||
foreach ($names as $name) {
|
||||
$list->add(new ArrayData(['Name' => $name]));
|
||||
}
|
||||
|
||||
$rows = $this->getTestableRows($list);
|
||||
|
||||
$foundNames = [];
|
||||
foreach ($rows as $row) {
|
||||
foreach ($row->ItemRow as $column) {
|
||||
$foundNames[] = $column->CellString;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame($names, $foundNames);
|
||||
}
|
||||
|
||||
public function testGetPrintColumnsForGridFieldThrowsException()
|
||||
{
|
||||
$component = new GridFieldPrintButton();
|
||||
$gridField = new GridField('dummy', 'dummy', new ArrayList());
|
||||
$gridField->getConfig()->removeComponentsByType(GridFieldDataColumns::class);
|
||||
$modelClass = ArrayData::class;
|
||||
$gridField->setModelClass($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField'
|
||||
. " or implement a summaryFields() method on $modelClass"
|
||||
);
|
||||
|
||||
$reflectionMethod = new ReflectionMethod($component, 'getPrintColumnsForGridField');
|
||||
$reflectionMethod->setAccessible(true);
|
||||
$reflectionMethod->invoke($component, $gridField);
|
||||
}
|
||||
}
|
||||
|
304
tests/php/ORM/Search/BasicSearchContextTest.php
Normal file
304
tests/php/ORM/Search/BasicSearchContextTest.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests\Search;
|
||||
|
||||
use ReflectionMethod;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Forms\TextField;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\ORM\ArrayList;
|
||||
use SilverStripe\ORM\Filters\ExactMatchFilter;
|
||||
use SilverStripe\ORM\Filters\SearchFilter;
|
||||
use SilverStripe\ORM\Filters\StartsWithFilter;
|
||||
use SilverStripe\ORM\Search\BasicSearchContext;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class BasicSearchContextTest extends SapphireTest
|
||||
{
|
||||
protected static $fixture_file = 'BasicSearchContextTest.yml';
|
||||
|
||||
protected static $extra_dataobjects = [
|
||||
SearchContextTest\GeneralSearch::class,
|
||||
];
|
||||
|
||||
private function getList(): ArrayList
|
||||
{
|
||||
$data = [
|
||||
[
|
||||
'Name' => 'James',
|
||||
'Email' => 'james@example.com',
|
||||
'HairColor' => 'brown',
|
||||
'EyeColor' => 'brown',
|
||||
],
|
||||
[
|
||||
'Name' => 'John',
|
||||
'Email' => 'john@example.com',
|
||||
'HairColor' => 'blond',
|
||||
'EyeColor' => 'blue',
|
||||
],
|
||||
[
|
||||
'Name' => 'Jane',
|
||||
'Email' => 'jane@example.com',
|
||||
'HairColor' => 'brown',
|
||||
'EyeColor' => 'green',
|
||||
],
|
||||
[
|
||||
'Name' => 'Hemi',
|
||||
'Email' => 'hemi@example.com',
|
||||
'HairColor' => 'black',
|
||||
'EyeColor' => 'brown eyes',
|
||||
],
|
||||
[
|
||||
'Name' => 'Sara',
|
||||
'Email' => 'sara@example.com',
|
||||
'HairColor' => 'black',
|
||||
'EyeColor' => 'green',
|
||||
],
|
||||
[
|
||||
'Name' => 'MatchNothing',
|
||||
'Email' => 'MatchNothing',
|
||||
'HairColor' => 'MatchNothing',
|
||||
'EyeColor' => 'MatchNothing',
|
||||
],
|
||||
];
|
||||
|
||||
$list = new ArrayList();
|
||||
foreach ($data as $datum) {
|
||||
$list->add(new ArrayData($datum));
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function getSearchableFields(string $generalField): FieldList
|
||||
{
|
||||
return new FieldList([
|
||||
new HiddenField($generalField),
|
||||
new TextField('Name'),
|
||||
new TextField('Email'),
|
||||
new TextField('HairColor'),
|
||||
new TextField('EyeColor'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testResultSetFilterReturnsExpectedCount()
|
||||
{
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$results = $context->getQuery(['Name' => ''], existingQuery: $this->getList());
|
||||
|
||||
$this->assertEquals(6, $results->Count());
|
||||
|
||||
$results = $context->getQuery(['EyeColor' => 'green'], existingQuery: $this->getList());
|
||||
$this->assertEquals(2, $results->Count());
|
||||
|
||||
$results = $context->getQuery(['EyeColor' => 'green', 'HairColor' => 'black'], existingQuery: $this->getList());
|
||||
$this->assertEquals(1, $results->Count());
|
||||
}
|
||||
|
||||
public function provideApplySearchFilters()
|
||||
{
|
||||
$idFilter = new ExactMatchFilter('ID');
|
||||
$idFilter->setModifiers(['nocase']);
|
||||
return [
|
||||
'defaults to PartialMatch' => [
|
||||
'searchParams' => [
|
||||
'q' => 'This one gets ignored',
|
||||
'ID' => 47,
|
||||
'Name' => 'some search term',
|
||||
],
|
||||
'filters' => null,
|
||||
'expected' => [
|
||||
'q' => 'This one gets ignored',
|
||||
'ID:PartialMatch' => 47,
|
||||
'Name:PartialMatch' => 'some search term',
|
||||
],
|
||||
],
|
||||
'respects custom filters and modifiers' => [
|
||||
'searchParams' => [
|
||||
'q' => 'This one gets ignored',
|
||||
'ID' => 47,
|
||||
'Name' => 'some search term',
|
||||
],
|
||||
'filters' => ['ID' => $idFilter],
|
||||
'expected' => [
|
||||
'q' => 'This one gets ignored',
|
||||
'ID:ExactMatch:nocase' => 47,
|
||||
'Name:PartialMatch' => 'some search term',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideApplySearchFilters
|
||||
*/
|
||||
public function testApplySearchFilters(array $searchParams, ?array $filters, array $expected)
|
||||
{
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$reflectionApplySearchFilters = new ReflectionMethod($context, 'applySearchFilters');
|
||||
$reflectionApplySearchFilters->setAccessible(true);
|
||||
|
||||
if ($filters) {
|
||||
$context->setFilters($filters);
|
||||
}
|
||||
|
||||
$this->assertSame($expected, $reflectionApplySearchFilters->invoke($context, $searchParams));
|
||||
}
|
||||
|
||||
public function provideGetGeneralSearchFilterTerm()
|
||||
{
|
||||
return [
|
||||
'defaults to case-insensitive partial match' => [
|
||||
'filterType' => null,
|
||||
'fieldFilter' => null,
|
||||
'expected' => 'PartialMatch:nocase',
|
||||
],
|
||||
'uses default even when config is explicitly "null"' => [
|
||||
'filterType' => null,
|
||||
'fieldFilter' => new StartsWithFilter('MyField'),
|
||||
'expected' => 'PartialMatch:nocase',
|
||||
],
|
||||
'uses configuration filter over field-specific filter' => [
|
||||
'filterType' => ExactMatchFilter::class,
|
||||
'fieldFilter' => new StartsWithFilter(),
|
||||
'expected' => 'ExactMatch',
|
||||
],
|
||||
'uses field-specific filter if provided and config is empty string' => [
|
||||
'filterType' => '',
|
||||
'fieldFilter' => new StartsWithFilter('MyField'),
|
||||
'expected' => 'StartsWith',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetGeneralSearchFilterTerm
|
||||
*/
|
||||
public function testGetGeneralSearchFilterTerm(?string $filterType, ?SearchFilter $fieldFilter, string $expected)
|
||||
{
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$reflectionGetGeneralSearchFilterTerm = new ReflectionMethod($context, 'getGeneralSearchFilterTerm');
|
||||
$reflectionGetGeneralSearchFilterTerm->setAccessible(true);
|
||||
|
||||
if ($fieldFilter) {
|
||||
$context->setFilters(['MyField' => $fieldFilter]);
|
||||
}
|
||||
|
||||
Config::modify()->set(ArrayData::class, 'general_search_field_filter', $filterType);
|
||||
|
||||
$this->assertSame($expected, $reflectionGetGeneralSearchFilterTerm->invoke($context, 'MyField'));
|
||||
}
|
||||
|
||||
public function provideGetQuery()
|
||||
{
|
||||
// Note that the search TERM is the same for both scenarios,
|
||||
// but because the search FIELD is different, we get different results.
|
||||
return [
|
||||
'search against hair' => [
|
||||
'searchParams' => [
|
||||
'HairColor' => 'brown',
|
||||
],
|
||||
'expected' => [
|
||||
'James',
|
||||
'Jane',
|
||||
],
|
||||
],
|
||||
'search against eyes' => [
|
||||
'searchParams' => [
|
||||
'EyeColor' => 'brown',
|
||||
],
|
||||
'expected' => [
|
||||
'James',
|
||||
'Hemi',
|
||||
],
|
||||
],
|
||||
'search against all' => [
|
||||
'searchParams' => [
|
||||
'q' => 'brown',
|
||||
],
|
||||
'expected' => [
|
||||
'James',
|
||||
'Jane',
|
||||
'Hemi',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetQuery
|
||||
*/
|
||||
public function testGetQuery(array $searchParams, array $expected)
|
||||
{
|
||||
$list = $this->getList();
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$context->setFields($this->getSearchableFields(BasicSearchContext::config()->get('general_search_field_name')));
|
||||
|
||||
$results = $context->getQuery($searchParams, existingQuery: $list);
|
||||
$this->assertSame($expected, $results->column('Name'));
|
||||
}
|
||||
|
||||
public function testGeneralSearch()
|
||||
{
|
||||
$list = $this->getList();
|
||||
$generalField = BasicSearchContext::config()->get('general_search_field_name');
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$context->setFields($this->getSearchableFields($generalField));
|
||||
|
||||
$results = $context->getQuery([$generalField => 'brown'], existingQuery: $list);
|
||||
$this->assertSame(['James', 'Jane', 'Hemi'], $results->column('Name'));
|
||||
$results = $context->getQuery([$generalField => 'b'], existingQuery: $list);
|
||||
$this->assertSame(['James', 'John', 'Jane', 'Hemi', 'Sara'], $results->column('Name'));
|
||||
}
|
||||
|
||||
public function testGeneralSearchSplitTerms()
|
||||
{
|
||||
$list = $this->getList();
|
||||
$generalField = BasicSearchContext::config()->get('general_search_field_name');
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$context->setFields($this->getSearchableFields($generalField));
|
||||
|
||||
// These terms don't exist in a single field in this order on any object, but they do exist in separate fields.
|
||||
$results = $context->getQuery([$generalField => 'john blue'], existingQuery: $list);
|
||||
$this->assertSame(['John'], $results->column('Name'));
|
||||
$results = $context->getQuery([$generalField => 'eyes sara'], existingQuery: $list);
|
||||
$this->assertSame(['Hemi', 'Sara'], $results->column('Name'));
|
||||
}
|
||||
|
||||
public function testGeneralSearchNoSplitTerms()
|
||||
{
|
||||
Config::modify()->set(ArrayData::class, 'general_search_split_terms', false);
|
||||
$list = $this->getList();
|
||||
$generalField = BasicSearchContext::config()->get('general_search_field_name');
|
||||
$context = new BasicSearchContext(ArrayData::class);
|
||||
$context->setFields($this->getSearchableFields($generalField));
|
||||
|
||||
// These terms don't exist in a single field in this order on any object
|
||||
$results = $context->getQuery([$generalField => 'john blue'], existingQuery: $list);
|
||||
$this->assertCount(0, $results);
|
||||
|
||||
// These terms exist in a single field, but not in this order.
|
||||
$results = $context->getQuery([$generalField => 'eyes brown'], existingQuery: $list);
|
||||
$this->assertCount(0, $results);
|
||||
|
||||
// These terms exist in a single field in this order.
|
||||
$results = $context->getQuery([$generalField => 'brown eyes'], existingQuery: $list);
|
||||
$this->assertSame(['Hemi'], $results->column('Name'));
|
||||
}
|
||||
|
||||
public function testSpecificFieldsCanBeSkipped()
|
||||
{
|
||||
$general1 = $this->objFromFixture(SearchContextTest\GeneralSearch::class, 'general1');
|
||||
$list = new ArrayList();
|
||||
$list->merge(SearchContextTest\GeneralSearch::get());
|
||||
$generalField = BasicSearchContext::config()->get('general_search_field_name');
|
||||
$context = new BasicSearchContext(SearchContextTest\GeneralSearch::class);
|
||||
|
||||
// We're searching for a value that DOES exist in a searchable field,
|
||||
// but that field is set to be skipped by general search.
|
||||
$results = $context->getQuery([$generalField => $general1->ExcludeThisField], existingQuery: $list);
|
||||
$this->assertNotEmpty($general1->ExcludeThisField);
|
||||
$this->assertCount(0, $results);
|
||||
}
|
||||
}
|
28
tests/php/ORM/Search/BasicSearchContextTest.yml
Normal file
28
tests/php/ORM/Search/BasicSearchContextTest.yml
Normal file
@ -0,0 +1,28 @@
|
||||
SilverStripe\ORM\Tests\Search\SearchContextTest\GeneralSearch:
|
||||
general0:
|
||||
Name: General Zero
|
||||
DoNotUseThisField: omitted
|
||||
HairColor: blue
|
||||
ExcludeThisField: excluded
|
||||
ExactMatchField: Some specific value here
|
||||
PartialMatchField: A partial match is allowed for this field
|
||||
MatchAny1: Some match any field
|
||||
MatchAny2: Another match any field
|
||||
general1:
|
||||
Name: General One
|
||||
DoNotUseThisField: omitted
|
||||
HairColor: brown
|
||||
ExcludeThisField: excluded
|
||||
ExactMatchField: This requires an exact match
|
||||
PartialMatchField: This explicitly allows partial matches
|
||||
MatchAny1: first match
|
||||
MatchAny2: second match
|
||||
general2:
|
||||
Name: MatchNothing
|
||||
DoNotUseThisField: MatchNothing
|
||||
HairColor: MatchNothing
|
||||
ExcludeThisField: MatchNothing
|
||||
ExactMatchField: MatchNothing
|
||||
PartialMatchField: MatchNothing
|
||||
MatchAny1: MatchNothing
|
||||
MatchAny2: MatchNothing
|
@ -18,6 +18,7 @@ use SilverStripe\ORM\Filters\EndsWithFilter;
|
||||
use SilverStripe\ORM\Filters\ExactMatchFilter;
|
||||
use SilverStripe\ORM\Filters\PartialMatchFilter;
|
||||
use SilverStripe\ORM\Search\SearchContext;
|
||||
use SilverStripe\View\ArrayData;
|
||||
|
||||
class SearchContextTest extends SapphireTest
|
||||
{
|
||||
@ -527,4 +528,18 @@ class SearchContextTest extends SapphireTest
|
||||
$results = $context->getResults(['PartialMatchField' => 'an']);
|
||||
$this->assertCount(1, $results);
|
||||
}
|
||||
|
||||
public function testGetSearchFieldsThrowsException()
|
||||
{
|
||||
$modelClass = ArrayData::class;
|
||||
$context = new SearchContext($modelClass);
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Cannot dynamically determine search fields. Pass the fields to setFields()'
|
||||
. " or implement a scaffoldSearchFields() method on {$modelClass}"
|
||||
);
|
||||
|
||||
$context->getSearchFields();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user