mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #236 from silverstripe-big-o/pull/SSF-25
SSF-25: ModelAdmin
This commit is contained in:
commit
603a984ad4
@ -30,7 +30,7 @@
|
||||
*/
|
||||
abstract class ModelAdmin extends LeftAndMain {
|
||||
|
||||
static $url_rule = '/$Action';
|
||||
static $url_rule = '/$ModelClass/$Action';
|
||||
|
||||
/**
|
||||
* List of all managed {@link DataObject}s in this interface.
|
||||
@ -49,51 +49,24 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
*
|
||||
* Available options:
|
||||
* - 'title': Set custom titles for the tabs or dropdown names
|
||||
* - 'collection_controller': Set a custom class to use as a collection controller for this model
|
||||
* - 'record_controller': Set a custom class to use as a record controller for this model
|
||||
*
|
||||
* @var array|string
|
||||
*/
|
||||
public static $managed_models = null;
|
||||
|
||||
/**
|
||||
* More actions are dynamically added in {@link defineMethods()} below.
|
||||
*/
|
||||
public static $allowed_actions = array(
|
||||
'add',
|
||||
'edit',
|
||||
'delete',
|
||||
'import',
|
||||
'renderimportform',
|
||||
'handleList',
|
||||
'handleItem',
|
||||
'ImportForm'
|
||||
'ImportForm',
|
||||
'SearchForm',
|
||||
);
|
||||
|
||||
/**
|
||||
* @param string $collection_controller_class Override for controller class
|
||||
*/
|
||||
public static $collection_controller_class = "ModelAdmin_CollectionController";
|
||||
|
||||
/**
|
||||
* @param string $collection_controller_class Override for controller class
|
||||
*/
|
||||
public static $record_controller_class = "ModelAdmin_RecordController";
|
||||
|
||||
/**
|
||||
* Forward control to the default action handler
|
||||
*/
|
||||
public static $url_handlers = array(
|
||||
'$Action' => 'handleAction'
|
||||
'$ModelClass/$Action' => 'handleAction'
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Model object currently in manipulation queue. Used for updating Link to point
|
||||
* to the correct generic data object in generated URLs.
|
||||
*
|
||||
* @var string
|
||||
* @var String
|
||||
*/
|
||||
private $currentModel = false;
|
||||
protected $modelClass;
|
||||
|
||||
/**
|
||||
* Change this variable if you don't want the Import from CSV form to appear.
|
||||
@ -120,20 +93,7 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
* @var int
|
||||
*/
|
||||
public static $page_length = 30;
|
||||
|
||||
/**
|
||||
* Class name of the form field used for the results list. Overloading this in subclasses
|
||||
* can let you customise the results table field.
|
||||
*/
|
||||
protected $resultsTableClassName = 'GridField';
|
||||
|
||||
/**
|
||||
* Return {@link $this->resultsTableClassName}
|
||||
*/
|
||||
public function resultsTableClassName() {
|
||||
return $this->resultsTableClassName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins.
|
||||
*
|
||||
@ -141,103 +101,101 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
*/
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
|
||||
$models = $this->getManagedModels();
|
||||
$this->modelClass = (isset($this->urlParams['ModelClass'])) ? $this->urlParams['ModelClass'] : $models[0];
|
||||
|
||||
// security check for valid models
|
||||
if(isset($this->urlParams['Action']) && !in_array($this->urlParams['Action'], $this->getManagedModels())) {
|
||||
//user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR);
|
||||
if(!in_array($this->modelClass, $models)) {
|
||||
user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR);
|
||||
}
|
||||
|
||||
Requirements::css(SAPPHIRE_ADMIN_DIR . '/css/silverstripe.tabs.css'); // follows the jQuery UI theme conventions
|
||||
|
||||
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js');
|
||||
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-livequery/jquery.livequery.js');
|
||||
Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
|
||||
Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ModelAdmin.js');
|
||||
Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ModelAdmin.History.js');
|
||||
}
|
||||
|
||||
/**
|
||||
* overwrite the static page_length of the admin panel,
|
||||
* should be called in the project _config file.
|
||||
*/
|
||||
static function set_page_length($length){
|
||||
self::$page_length = $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the static page_length of the admin, default as 30
|
||||
*/
|
||||
static function get_page_length(){
|
||||
return self::$page_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the class name of the collection controller
|
||||
*
|
||||
* @param string $model model name to get the controller for
|
||||
* @return string the collection controller class
|
||||
*/
|
||||
function getCollectionControllerClass($model) {
|
||||
$models = $this->getManagedModels();
|
||||
|
||||
if(isset($models[$model]['collection_controller'])) {
|
||||
$class = $models[$model]['collection_controller'];
|
||||
} else {
|
||||
$class = $this->stat('collection_controller_class');
|
||||
|
||||
function getEditForm($id = null) {
|
||||
$list = $this->getList();
|
||||
$listField = Object::create('GridField',
|
||||
$this->modelClass,
|
||||
false,
|
||||
$list,
|
||||
$fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length'))
|
||||
->addComponent(new GridFieldExportButton())
|
||||
->removeComponentsByType('GridFieldFilter')
|
||||
);
|
||||
|
||||
// Validation
|
||||
if(singleton($this->modelClass)->hasMethod('getCMSValidator')) {
|
||||
$detailValidator = singleton($this->modelClass)->getCMSValidator();
|
||||
$listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator);
|
||||
}
|
||||
|
||||
$form = new Form(
|
||||
$this,
|
||||
'EditForm',
|
||||
new FieldList($listField),
|
||||
new FieldList()
|
||||
);
|
||||
$form->addExtraClass('cms-edit-form cms-panel-padded center');
|
||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'EditForm'));
|
||||
|
||||
$this->extend('updateEditForm', $form);
|
||||
|
||||
return $class;
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SearchContext
|
||||
*/
|
||||
public function getSearchContext() {
|
||||
$context = singleton($this->modelClass)->getDefaultSearchContext();
|
||||
|
||||
// Namespace fields, for easier detection if a search is present
|
||||
foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName()));
|
||||
foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName()));
|
||||
|
||||
$this->extend('updateSearchContext', $context);
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Form
|
||||
*/
|
||||
public function SearchForm() {
|
||||
$context = $this->getSearchContext();
|
||||
$form = new Form($this, "SearchForm",
|
||||
$context->getSearchFields(),
|
||||
new FieldList(
|
||||
Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search'))
|
||||
->setUseButtonTag(true)->addExtraClass('ss-ui-action-minor'),
|
||||
Object::create('FormAction', 'search', _t('MemberTableField.SEARCH', 'Search'))
|
||||
->setUseButtonTag(true)
|
||||
),
|
||||
new RequiredFields()
|
||||
);
|
||||
$form->setFormMethod('get');
|
||||
$form->setFormAction($this->Link($this->modelClass));
|
||||
$form->addExtraClass('cms-search-form');
|
||||
$form->disableSecurityToken();
|
||||
$form->loadDataFrom($this->request->getVars());
|
||||
|
||||
$this->extend('updateSearchForm', $form);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the class name of the record controller
|
||||
*
|
||||
* @param string $model model name to get the controller for
|
||||
* @return string the record controller class
|
||||
*/
|
||||
function getRecordControllerClass($model) {
|
||||
$models = $this->getManagedModels();
|
||||
|
||||
if(isset($models[$model]['record_controller'])) {
|
||||
$class = $models[$model]['record_controller'];
|
||||
} else {
|
||||
$class = $this->stat('record_controller_class');
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add mappings for generic form constructors to automatically delegate to a scaffolded form object.
|
||||
*/
|
||||
function defineMethods() {
|
||||
parent::defineMethods();
|
||||
foreach($this->getManagedModels() as $class => $options) {
|
||||
if(is_numeric($class)) $class = $options;
|
||||
$this->addWrapperMethod($class, 'bindModelController');
|
||||
self::$allowed_actions[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base scaffolding method for returning a generic model instance.
|
||||
*/
|
||||
public function bindModelController($model, $request = null) {
|
||||
$class = $this->getCollectionControllerClass($model);
|
||||
return new $class($this, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method can be overloaded to specify the UI by which the search class is chosen.
|
||||
*
|
||||
* It can create a tab strip or a dropdown. The dropdown is useful when there are a large number of classes.
|
||||
* By default, it will show a tabs for 1-3 classes, and a dropdown for 4 or more classes.
|
||||
*
|
||||
* @return String: 'tabs' or 'dropdown'
|
||||
*/
|
||||
public function SearchClassSelector() {
|
||||
return sizeof($this->getManagedModels()) > 3 ? 'dropdown' : 'tabs';
|
||||
public function getList() {
|
||||
$context = $this->getSearchContext();
|
||||
$params = $this->request->requestVar('q');
|
||||
$list = $context->getResults($params);
|
||||
|
||||
$this->extend('updateList', $list);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns managed models' create, search, and import forms
|
||||
@ -245,7 +203,7 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
* @uses SearchFilter
|
||||
* @return SS_List of forms
|
||||
*/
|
||||
protected function getModelForms() {
|
||||
protected function getManagedModelTabs() {
|
||||
$models = $this->getManagedModels();
|
||||
$forms = new ArrayList();
|
||||
|
||||
@ -254,7 +212,8 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
$forms->push(new ArrayData(array (
|
||||
'Title' => (is_array($options) && isset($options['title'])) ? $options['title'] : singleton($class)->i18n_singular_name(),
|
||||
'ClassName' => $class,
|
||||
'Content' => $this->$class()->getModelSidebar()
|
||||
'Link' => $this->Link($class),
|
||||
'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link'
|
||||
)));
|
||||
}
|
||||
|
||||
@ -287,177 +246,28 @@ abstract class ModelAdmin extends LeftAndMain {
|
||||
* with a default {@link CsvBulkLoader} class. In this case the column names of the first row
|
||||
* in the CSV file are assumed to have direct mappings to properties on the object.
|
||||
*
|
||||
* @return array
|
||||
* @return array Map of model class names to importer instances
|
||||
*/
|
||||
function getModelImporters() {
|
||||
$importers = $this->stat('model_importers');
|
||||
$importerClasses = $this->stat('model_importers');
|
||||
|
||||
// fallback to all defined models if not explicitly defined
|
||||
if(is_null($importers)) {
|
||||
if(is_null($importerClasses)) {
|
||||
$models = $this->getManagedModels();
|
||||
foreach($models as $modelName => $options) {
|
||||
if(is_numeric($modelName)) $modelName = $options;
|
||||
$importers[$modelName] = 'CsvBulkLoader';
|
||||
$importerClasses[$modelName] = 'CsvBulkLoader';
|
||||
}
|
||||
}
|
||||
|
||||
$importers = array();
|
||||
foreach($importerClasses as $modelClass => $importerClass) {
|
||||
$importers[$modelClass] = new $importerClass($modelClass);
|
||||
}
|
||||
|
||||
return $importers;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a managed model class and provides default collection filtering behavior.
|
||||
*
|
||||
* @package cms
|
||||
* @subpackage core
|
||||
*/
|
||||
class ModelAdmin_CollectionController extends Controller {
|
||||
public $parentController;
|
||||
protected $modelClass;
|
||||
|
||||
public $showImportForm = null;
|
||||
|
||||
static $url_handlers = array(
|
||||
'$Action' => 'handleActionOrID'
|
||||
);
|
||||
|
||||
function __construct($parent, $model) {
|
||||
$this->parentController = $parent;
|
||||
$this->modelClass = $model;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the model class to the URL.
|
||||
*
|
||||
* @param string $action
|
||||
* @return string
|
||||
*/
|
||||
function Link($action = null) {
|
||||
return $this->parentController->Link(Controller::join_links($this->modelClass, $action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the class name of the model being managed.
|
||||
*
|
||||
* @return unknown
|
||||
*/
|
||||
function getModelClass() {
|
||||
return $this->modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to different control flow, depending on whether the
|
||||
* URL parameter is a number (record id) or string (action).
|
||||
*
|
||||
* @param unknown_type $request
|
||||
* @return unknown
|
||||
*/
|
||||
function handleActionOrID($request) {
|
||||
if (is_numeric($request->param('Action'))) {
|
||||
return $this->handleID($request);
|
||||
} else {
|
||||
return $this->handleAction($request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to the RecordController if a valid numeric ID appears in the URL
|
||||
* segment.
|
||||
*
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return RecordController
|
||||
*/
|
||||
public function handleID($request) {
|
||||
$class = $this->parentController->getRecordControllerClass($this->getModelClass());
|
||||
return new $class($this, $request);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get a combination of the Search, Import and Create forms that can be inserted into a {@link ModelAdmin} sidebar.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getModelSidebar() {
|
||||
return $this->renderWith('ModelSidebar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a search form for a single {@link DataObject} subclass.
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function SearchForm() {
|
||||
$SNG_model = singleton($this->modelClass);
|
||||
$context = $SNG_model->getDefaultSearchContext();
|
||||
$fields = $context->getSearchFields();
|
||||
$columnSelectionField = $this->ColumnSelectionField();
|
||||
$fields->push($columnSelectionField);
|
||||
|
||||
$validator = ($SNG_model->hasMethod('getCMSValidator')) ? $SNG_model->getCMSValidator() : new RequiredFields();
|
||||
$clearAction = new ResetFormAction('clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search'));
|
||||
|
||||
$form = new Form($this, "SearchForm",
|
||||
$fields,
|
||||
new FieldList(
|
||||
new FormAction('search', _t('MemberTableField.SEARCH', 'Search')),
|
||||
$clearAction
|
||||
),
|
||||
$validator
|
||||
);
|
||||
//$form->setFormAction(Controller::join_links($this->Link(), "search"));
|
||||
$form->setFormMethod('get');
|
||||
$form->setHTMLID("Form_SearchForm_" . $this->modelClass);
|
||||
$form->disableSecurityToken();
|
||||
$clearAction->setUseButtonTag(true);
|
||||
$clearAction->addExtraClass('ss-ui-action-minor');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a form that consists of one button
|
||||
* that directs to a give model's Add form
|
||||
*/
|
||||
public function CreateForm() {
|
||||
$modelName = $this->modelClass;
|
||||
$SNG_model = singleton($modelName);
|
||||
|
||||
if($this->hasMethod('alternatePermissionCheck')) {
|
||||
if(!$this->alternatePermissionCheck()) return false;
|
||||
} else {
|
||||
if(!$SNG_model->canCreate(Member::currentUser())) return false;
|
||||
}
|
||||
|
||||
$buttonLabel = sprintf(_t('ModelAdmin.CREATEBUTTON', "Create '%s'", PR_MEDIUM, "Create a new instance from a model class"), $SNG_model->i18n_singular_name());
|
||||
|
||||
$validator = ($SNG_model->hasMethod('getCMSValidator')) ? $SNG_model->getCMSValidator() : new RequiredFields();
|
||||
$createButton = FormAction::create('add', $buttonLabel)->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept');
|
||||
|
||||
$form = new Form($this, "CreateForm",
|
||||
new FieldList(),
|
||||
new FieldList($createButton),
|
||||
$validator
|
||||
);
|
||||
|
||||
$createButton->dontEscape = true;
|
||||
$form->setHTMLID("Form_CreateForm_" . $this->modelClass);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a CSV import form should be generated by a className criteria or in general for ModelAdmin.
|
||||
*/
|
||||
function showImportForm() {
|
||||
if($this->showImportForm === null) return $this->parentController->showImportForm;
|
||||
else return $this->showImportForm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a CSV import form for a single {@link DataObject} subclass.
|
||||
*
|
||||
@ -466,8 +276,11 @@ class ModelAdmin_CollectionController extends Controller {
|
||||
public function ImportForm() {
|
||||
$modelName = $this->modelClass;
|
||||
// check if a import form should be generated
|
||||
if(!$this->showImportForm() || (is_array($this->showImportForm()) && !in_array($modelName,$this->showImportForm()))) return false;
|
||||
$importers = $this->parentController->getModelImporters();
|
||||
if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($modelName,$this->showImportForm))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$importers = $this->getModelImporters();
|
||||
if(!$importers || !isset($importers[$modelName])) return false;
|
||||
|
||||
if(!singleton($modelName)->canCreate(Member::currentUser())) return false;
|
||||
@ -508,7 +321,10 @@ class ModelAdmin_CollectionController extends Controller {
|
||||
$fields,
|
||||
$actions
|
||||
);
|
||||
$form->setHTMLID("Form_ImportForm_" . $this->modelClass);
|
||||
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'ImportForm'));
|
||||
|
||||
$this->extend('updateImportForm', $form);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
@ -524,14 +340,12 @@ class ModelAdmin_CollectionController extends Controller {
|
||||
* @param SS_HTTPRequest $request
|
||||
*/
|
||||
function import($data, $form, $request) {
|
||||
if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($this->modelClass,$this->showImportForm))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$modelName = $data['ClassName'];
|
||||
|
||||
if(!$this->showImportForm() || (is_array($this->showImportForm()) && !in_array($modelName,$this->showImportForm()))) return false;
|
||||
$importers = $this->parentController->getModelImporters();
|
||||
$importerClass = $importers[$modelName];
|
||||
|
||||
$loader = new $importerClass($data['ClassName']);
|
||||
$importers = $this->getModelImporters();
|
||||
$loader = $importers[$this->modelClass];
|
||||
|
||||
// File wasn't properly uploaded, show a reminder to the user
|
||||
if(
|
||||
@ -566,467 +380,38 @@ class ModelAdmin_CollectionController extends Controller {
|
||||
$form->sessionMessage($message, 'good');
|
||||
$this->redirectBack();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the columns available in the column selection field.
|
||||
* Overload this to make other columns available
|
||||
*/
|
||||
public function columnsAvailable() {
|
||||
return singleton($this->modelClass)->summaryFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the columns selected by default in the column selection field.
|
||||
* Overload this to make other columns selected by default
|
||||
*/
|
||||
public function columnsSelectedByDefault() {
|
||||
return array_keys(singleton($this->modelClass)->summaryFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the flexibilility to show variouse combination of columns in the search result table
|
||||
*/
|
||||
public function ColumnSelectionField() {
|
||||
$model = singleton($this->modelClass);
|
||||
$source = $this->columnsAvailable();
|
||||
|
||||
// select all fields by default
|
||||
$value = $this->columnsSelectedByDefault();
|
||||
|
||||
// Reorder the source so that you read items down the column and then across
|
||||
$columnisedSource = array();
|
||||
$keys = array_keys($source);
|
||||
$midPoint = ceil(sizeof($source)/2);
|
||||
for($i=0;$i<$midPoint;$i++) {
|
||||
$key1 = $keys[$i];
|
||||
$columnisedSource[$key1] = $model->fieldLabel($source[$key1]);
|
||||
// If there are an odd number of items, the last item will be unset
|
||||
if(isset($keys[$i+$midPoint])) {
|
||||
$key2 = $keys[$i+$midPoint];
|
||||
$columnisedSource[$key2] = $model->fieldLabel($source[$key2]);
|
||||
}
|
||||
}
|
||||
|
||||
$checkboxes = new CheckboxSetField("ResultAssembly", false, $columnisedSource, $value);
|
||||
|
||||
$field = new CompositeField(
|
||||
new LiteralField(
|
||||
"ToggleResultAssemblyLink",
|
||||
sprintf("<a class=\"form_frontend_function toggle_result_assembly\" href=\"#\">%s</a>",
|
||||
_t('ModelAdmin.CHOOSE_COLUMNS', 'Select result columns...')
|
||||
)
|
||||
),
|
||||
$checkboxesBlock = new CompositeField(
|
||||
$checkboxes,
|
||||
new LiteralField("ClearDiv", "<div class=\"clear\"></div>"),
|
||||
new LiteralField(
|
||||
"TickAllAssemblyLink",
|
||||
sprintf(
|
||||
"<a class=\"form_frontend_function tick_all_result_assembly\" href=\"#\">%s</a>",
|
||||
_t('ModelAdmin.SELECTALL', 'select all')
|
||||
)
|
||||
),
|
||||
new LiteralField(
|
||||
"UntickAllAssemblyLink",
|
||||
sprintf(
|
||||
"<a class=\"form_frontend_function untick_all_result_assembly\" href=\"#\">%s</a>",
|
||||
_t('ModelAdmin.SELECTNONE', 'select none')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$field->addExtraClass("ResultAssemblyBlock");
|
||||
$checkboxesBlock->addExtraClass("hidden");
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to render a data object collection, using the model context to provide filters
|
||||
* and paging.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function search($request, $form) {
|
||||
// Get the results form to be rendered
|
||||
$resultsForm = $this->ResultsForm(array_merge($form->getData(), $request));
|
||||
return $resultsForm->forTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the search query generated on the SearchContext from
|
||||
* {@link DataObject::getDefaultSearchContext()},
|
||||
* and the current GET parameters on the request.
|
||||
*
|
||||
* @return SQLQuery
|
||||
*/
|
||||
function getSearchQuery($searchCriteria) {
|
||||
$context = singleton($this->modelClass)->getDefaultSearchContext();
|
||||
return $context->getQuery($searchCriteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all columns used for tabular search results display.
|
||||
* Defaults to all fields specified in {@link DataObject->summaryFields()}.
|
||||
*
|
||||
* @param array $searchCriteria Limit fields by populating the 'ResultsAssembly' key
|
||||
* @param boolean $selectedOnly Limit by 'ResultsAssempty
|
||||
*/
|
||||
function getResultColumns($searchCriteria, $selectedOnly = true) {
|
||||
$model = singleton($this->modelClass);
|
||||
|
||||
$summaryFields = $this->columnsAvailable();
|
||||
|
||||
if($selectedOnly && isset($searchCriteria['ResultAssembly'])) {
|
||||
$resultAssembly = $searchCriteria['ResultAssembly'];
|
||||
if(!is_array($resultAssembly)) {
|
||||
$explodedAssembly = split(' *, *', $resultAssembly);
|
||||
$resultAssembly = array();
|
||||
foreach($explodedAssembly as $item) $resultAssembly[$item] = true;
|
||||
}
|
||||
return array_intersect_key($summaryFields, $resultAssembly);
|
||||
} else {
|
||||
return $summaryFields;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns the result table field for resultsForm.
|
||||
* Uses {@link resultsTableClassName()} to initialise the formfield.
|
||||
* Method is called from {@link ResultsForm}.
|
||||
*
|
||||
* @param array $searchCriteria passed through from ResultsForm
|
||||
*
|
||||
* @return GridField
|
||||
*/
|
||||
function getResultsTable($searchCriteria) {
|
||||
|
||||
$className = $this->parentController->resultsTableClassName();
|
||||
$datalist = $this->getSearchQuery($searchCriteria);
|
||||
$numItemsPerPage = $this->parentController->stat('page_length');
|
||||
$tf = Object::create($className,
|
||||
$this->modelClass,
|
||||
false,
|
||||
$datalist,
|
||||
$fieldConfig = GridFieldConfig_RecordEditor::create($numItemsPerPage)
|
||||
->addComponent(new GridFieldExportButton())->removeComponentsByType('GridFieldFilterHeader')
|
||||
)->setDisplayFields($this->getResultColumns($searchCriteria));
|
||||
|
||||
return $tf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows results from the "search" action in a TableListField.
|
||||
*
|
||||
* @uses getResultsTable()
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
function ResultsForm($searchCriteria) {
|
||||
if($searchCriteria instanceof SS_HTTPRequest) $searchCriteria = $searchCriteria->getVars();
|
||||
|
||||
$tf = $this->getResultsTable($searchCriteria);
|
||||
|
||||
// implemented as a form to enable further actions on the resultset
|
||||
// (serverside sorting, export as CSV, etc)
|
||||
$form = new Form(
|
||||
$this,
|
||||
'ResultsForm',
|
||||
new FieldList(
|
||||
new HeaderField('SearchResults', _t('ModelAdmin.SEARCHRESULTS','Search Results'), 2),
|
||||
$tf
|
||||
),
|
||||
new FieldList()
|
||||
);
|
||||
|
||||
// Include the search criteria on the results form URL, but not dodgy variables like those below
|
||||
$filteredCriteria = $searchCriteria;
|
||||
unset($filteredCriteria['ctf']);
|
||||
unset($filteredCriteria['url']);
|
||||
unset($filteredCriteria['action_search']);
|
||||
|
||||
$form->setFormAction($this->Link() . '/ResultsForm?' . http_build_query($filteredCriteria));
|
||||
return $form;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Create a new model record.
|
||||
*
|
||||
* @param unknown_type $request
|
||||
* @return unknown
|
||||
*/
|
||||
function add($request) {
|
||||
return new SS_HTTPResponse(
|
||||
$this->AddForm()->forTemplate(),
|
||||
200,
|
||||
sprintf(
|
||||
_t('ModelAdmin.ADDFORM', "Fill out this form to add a %s to the database."),
|
||||
$this->modelClass
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a form suitable for adding a new model, falling back on the default edit form.
|
||||
*
|
||||
* Caution: The add-form shows a DataObject's {@link DataObject->getCMSFields()} method on a record
|
||||
* that doesn't exist in the database yet, hence has no ID. This means the {@link DataObject->getCMSFields()}
|
||||
* implementation has to ensure that no fields are added which would rely on a
|
||||
* record ID being present, e.g. {@link HasManyComplexTableField}.
|
||||
*
|
||||
* Example:
|
||||
* <code>
|
||||
* function getCMSFields() {
|
||||
* $fields = parent::getCMSFields();
|
||||
* if($this->exists()) {
|
||||
* $ctf = new HasManyComplexTableField($this, 'MyRelations', 'MyRelation');
|
||||
* $fields->addFieldToTab('Root.Main', $ctf);
|
||||
* }
|
||||
* return $fields;
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function AddForm() {
|
||||
$newRecord = new $this->modelClass();
|
||||
|
||||
if($newRecord->canCreate()){
|
||||
if($newRecord->hasMethod('getCMSAddFormFields')) {
|
||||
$fields = $newRecord->getCMSAddFormFields();
|
||||
} else {
|
||||
$fields = $newRecord->getCMSFields();
|
||||
}
|
||||
|
||||
$validator = ($newRecord->hasMethod('getCMSValidator')) ? $newRecord->getCMSValidator() : new RequiredFields();
|
||||
|
||||
$actions = new FieldList (
|
||||
FormAction::create("doCreate", _t('ModelAdmin.ADDBUTTON', "Add"))
|
||||
->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
|
||||
);
|
||||
|
||||
$form = new Form($this, "AddForm", $fields, $actions, $validator);
|
||||
$form->loadDataFrom($newRecord);
|
||||
$form->addExtraClass('cms-edit-form');
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
function doCreate($data, $form, $request) {
|
||||
$className = $this->getModelClass();
|
||||
$model = new $className();
|
||||
// We write before saveInto, since this will let us save has-many and many-many relationships :-)
|
||||
$model->write();
|
||||
$form->saveInto($model);
|
||||
$model->write();
|
||||
|
||||
if($this->isAjax()) {
|
||||
$class = $this->parentController->getRecordControllerClass($this->getModelClass());
|
||||
$recordController = new $class($this, $request, $model->ID);
|
||||
return new SS_HTTPResponse(
|
||||
$recordController->EditForm()->forTemplate(),
|
||||
200,
|
||||
sprintf(
|
||||
_t('ModelAdmin.LOADEDFOREDITING', "Loaded '%s' for editing."),
|
||||
$model->Title
|
||||
)
|
||||
);
|
||||
} else {
|
||||
Director::redirect(Controller::join_links($this->Link(), $model->ID , 'edit'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function Breadcrumbs(){
|
||||
return new ArrayList();
|
||||
}
|
||||
}
|
||||
public function Breadcrumbs($unlinked = false) {
|
||||
$items = parent::Breadcrumbs($unlinked);
|
||||
|
||||
/**
|
||||
* Handles operations on a single record from a managed model.
|
||||
*
|
||||
* @package cms
|
||||
* @subpackage core
|
||||
* @todo change the parent controller varname to indicate the model scaffolding functionality in ModelAdmin
|
||||
*/
|
||||
class ModelAdmin_RecordController extends Controller {
|
||||
protected $parentController;
|
||||
protected $currentRecord;
|
||||
|
||||
static $allowed_actions = array('edit', 'view', 'EditForm', 'ViewForm');
|
||||
|
||||
function __construct($parentController, $request, $recordID = null) {
|
||||
$this->parentController = $parentController;
|
||||
$modelName = $parentController->getModelClass();
|
||||
$recordID = ($recordID) ? $recordID : $request->param('Action');
|
||||
$this->currentRecord = DataObject::get_by_id($modelName, $recordID);
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Link fragment - appends the current record ID to the URL.
|
||||
*/
|
||||
public function Link($action = null) {
|
||||
return $this->parentController->Link(Controller::join_links($this->currentRecord->ID, $action));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Edit action - shows a form for editing this record
|
||||
*/
|
||||
function edit($request) {
|
||||
if ($this->currentRecord) {
|
||||
if($this->isAjax()) {
|
||||
$this->response->setBody($this->EditForm()->forTemplate());
|
||||
$this->response->setStatusCode(
|
||||
200,
|
||||
sprintf(
|
||||
_t('ModelAdmin.LOADEDFOREDITING', "Loaded '%s' for editing."),
|
||||
$this->currentRecord->Title
|
||||
)
|
||||
);
|
||||
return $this->response;
|
||||
} else {
|
||||
// This is really quite ugly; to fix will require a change in the way that customise() works. :-(
|
||||
return $this->parentController->parentController->customise(array(
|
||||
'Content' => $this->parentController->parentController->customise(array(
|
||||
'EditForm' => $this->EditForm()
|
||||
))->renderWith(array("{$this->class}_Content",'ModelAdmin_Content', 'LeftAndMain_Content'))
|
||||
))->renderWith(array('ModelAdmin', 'LeftAndMain'));
|
||||
}
|
||||
// Show the class name rather than ModelAdmin title as root node
|
||||
$models = $this->getManagedModels();
|
||||
$modelSpec = ArrayLib::is_associative($models) ? $models[$this->modelClass] : null;
|
||||
if(is_array($modelSpec) && isset($modelSpec['title'])) {
|
||||
$items[0]->Title = $modelSpec['title'];
|
||||
} else {
|
||||
return _t('ModelAdmin.ITEMNOTFOUND', "I can't find that item");
|
||||
$items[0]->Title = singleton($this->modelClass)->i18n_singular_name();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a form for editing the attached model
|
||||
* overwrite the static page_length of the admin panel,
|
||||
* should be called in the project _config file.
|
||||
*/
|
||||
public function EditForm() {
|
||||
$fields = $this->currentRecord->getCMSFields();
|
||||
$fields->push(new HiddenField("ID"));
|
||||
|
||||
if($this->currentRecord->hasMethod('Link')) {
|
||||
$fields->push(new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator()));
|
||||
}
|
||||
|
||||
$validator = ($this->currentRecord->hasMethod('getCMSValidator')) ? $this->currentRecord->getCMSValidator() : new RequiredFields();
|
||||
|
||||
$actions = $this->currentRecord->getCMSActions();
|
||||
if($this->currentRecord->canEdit(Member::currentUser())){
|
||||
if(!$actions->fieldByName('action_doSave') && !$actions->fieldByName('action_save')) {
|
||||
$actions->push(
|
||||
FormAction::create("doSave", _t('ModelAdmin.SAVE', "Save"))
|
||||
->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
|
||||
);
|
||||
}
|
||||
}else{
|
||||
$fields = $fields->makeReadonly();
|
||||
}
|
||||
|
||||
if($this->currentRecord->canDelete(Member::currentUser())) {
|
||||
if(!$actions->fieldByName('action_doDelete')) {
|
||||
$actions->unshift(
|
||||
FormAction::create('doDelete', _t('ModelAdmin.DELETE', 'Delete'))
|
||||
->addExtraClass('ss-ui-action-destructive')->setAttribute('data-icon', 'delete')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$form = new Form($this, "EditForm", $fields, $actions, $validator);
|
||||
$form->loadDataFrom($this->currentRecord);
|
||||
$form->addExtraClass('cms-edit-form');
|
||||
|
||||
return $form;
|
||||
static function set_page_length($length){
|
||||
self::$page_length = $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postback action to save a record
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
function doSave($data, $form, $request) {
|
||||
$form->saveInto($this->currentRecord);
|
||||
|
||||
try {
|
||||
$this->currentRecord->write();
|
||||
} catch(ValidationException $e) {
|
||||
$form->sessionMessage($e->getResult()->message(), 'bad');
|
||||
}
|
||||
|
||||
|
||||
// Behaviour switched on .
|
||||
if($this->parentController->isAjax()) {
|
||||
return $this->edit($request);
|
||||
} else {
|
||||
$this->parentController->redirectBack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current record
|
||||
* Return the static page_length of the admin, default as 30
|
||||
*/
|
||||
public function doDelete($data, $form, $request) {
|
||||
if($this->currentRecord->canDelete(Member::currentUser())) {
|
||||
$this->currentRecord->delete();
|
||||
Director::redirect($this->parentController->Link('SearchForm?action=search'));
|
||||
} else {
|
||||
$this->parentController->redirectBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
static function get_page_length(){
|
||||
return self::$page_length;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Renders the record view template.
|
||||
*
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return mixed
|
||||
*/
|
||||
function view($request) {
|
||||
if($this->currentRecord) {
|
||||
$form = $this->ViewForm();
|
||||
return $form->forTemplate();
|
||||
} else {
|
||||
return _t('ModelAdmin.ITEMNOTFOUND');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a form for viewing the attached model
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function ViewForm() {
|
||||
$fields = $this->currentRecord->getCMSFields();
|
||||
$form = new Form($this, "EditForm", $fields, new FieldList());
|
||||
$form->loadDataFrom($this->currentRecord);
|
||||
$form->makeReadonly();
|
||||
return $form;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function index() {
|
||||
Director::redirect(Controller::join_links($this->Link(), 'edit'));
|
||||
}
|
||||
|
||||
function getCurrentRecord(){
|
||||
return $this->currentRecord;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -265,11 +265,11 @@ body.cms { overflow: hidden; }
|
||||
.cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-state-default a, .cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-widget-content .ui-state-default a, .cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-widget-header .ui-state-default a { color: #1f1f1f; }
|
||||
|
||||
/** -------------------------------------------- Tabs -------------------------------------------- */
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav li, .cms-dialog .ui-tabs-nav li { margin: 0; }
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav li a, .cms-dialog .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; }
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-default, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-default, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-dialog .ui-tabs-nav .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-default { background-color: #b0bec7; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzhjYTFhZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #8ca1ae)); background-image: -webkit-linear-gradient(#b0bec7, #8ca1ae); background-image: -moz-linear-gradient(#b0bec7, #8ca1ae); background-image: -o-linear-gradient(#b0bec7, #8ca1ae); background-image: -ms-linear-gradient(#b0bec7, #8ca1ae); background-image: linear-gradient(#b0bec7, #8ca1ae); border-right-color: #a6a6a6; border-left-color: #d9d9d9; border-bottom: none; text-shadow: white 0 1px 0; }
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-active, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-dialog .ui-tabs-nav .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active { background: #eceff1; border-right-color: #a6a6a6; border-left-color: #a6a6a6; margin-right: -1px; margin-left: -1px; z-index: 2; }
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-active a, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active a, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active a { border-bottom: none; }
|
||||
.cms-content-header .ui-tabs-nav li, .cms-dialog .ui-tabs-nav li { margin: 0; }
|
||||
.cms-content-header .ui-tabs-nav li a, .cms-dialog .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; }
|
||||
.cms-content-header .ui-tabs-nav .ui-state-default, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-dialog .ui-tabs-nav .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-default { background-color: #b0bec7; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #8ca1ae)); background-image: -webkit-linear-gradient(#b0bec7, #8ca1ae); background-image: -moz-linear-gradient(#b0bec7, #8ca1ae); background-image: -o-linear-gradient(#b0bec7, #8ca1ae); background-image: -ms-linear-gradient(#b0bec7, #8ca1ae); background-image: linear-gradient(#b0bec7, #8ca1ae); border-right-color: #a6a6a6; border-left-color: #d9d9d9; border-bottom: none; text-shadow: white 0 1px 0; }
|
||||
.cms-content-header .ui-tabs-nav .ui-state-active, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-dialog .ui-tabs-nav .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active { background: #eceff1; border-right-color: #a6a6a6; border-left-color: #a6a6a6; margin-right: -1px; margin-left: -1px; z-index: 2; }
|
||||
.cms-content-header .ui-tabs-nav .ui-state-active a, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active a { border-bottom: none; }
|
||||
|
||||
.CMSPagesController .cms-content-header-tabs .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; text-indent: -9999em; }
|
||||
.CMSPagesController .cms-content-header-tabs .ui-tabs-nav li a.content-treeview { background: url(../images/content-header-tabs-sprite.png) no-repeat 2px 0px; }
|
||||
@ -338,7 +338,7 @@ body.cms { overflow: hidden; }
|
||||
|
||||
/* -------------------------------------------------------- Content Tools is the sidebar on the left of the main content panel */
|
||||
.cms-content-tools { background-color: #dde3e7; width: 192px; border-right: 1px solid #bfcad2; overflow-y: auto; overflow-x: hidden; z-index: 70; -moz-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; -webkit-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; -o-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; float: left; position: relative; }
|
||||
.cms-content-tools .cms-panel-header { margin: 0 0 7px; line-height: 24px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); }
|
||||
.cms-content-tools .cms-panel-header { clear: both; margin: 0 0 7px; line-height: 24px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); }
|
||||
.cms-content-tools .cms-panel-content { width: 176px; padding: 8px 8px; overflow: auto; height: 100%; }
|
||||
.cms-content-tools .cms-content-header { background-color: #748d9d; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzc0OGQ5ZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #748d9d)); background-image: -webkit-linear-gradient(#b0bec7, #748d9d); background-image: -moz-linear-gradient(#b0bec7, #748d9d); background-image: -o-linear-gradient(#b0bec7, #748d9d); background-image: -ms-linear-gradient(#b0bec7, #748d9d); background-image: linear-gradient(#b0bec7, #748d9d); }
|
||||
.cms-content-tools .cms-content-header h2 { text-shadow: #5c7382 -1px -1px 0; width: 176px; color: white; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; o-text-overflow: ellipsis; }
|
||||
@ -475,6 +475,9 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai
|
||||
.htmleditorfield-mediaform .ss-htmleditorfield-file .overview .action-delete { display: inline-block; }
|
||||
.htmleditorfield-mediaform .ss-htmleditorfield-file .details { padding: 16px; }
|
||||
|
||||
/** -------------------------------------------- Search forms (used in AssetAdmin, ModelAdmin, etc) -------------------------------------------- */
|
||||
.cms-search-form { overflow: auto; margin-bottom: 16px; }
|
||||
|
||||
/** -------------------------------------------- Step labels -------------------------------------------- */
|
||||
.step-label > * { display: inline-block; vertical-align: top; }
|
||||
.step-label .flyout { height: 18px; font-size: 14px; font-weight: bold; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; -o-border-top-left-radius: 3px; -ms-border-top-left-radius: 3px; -khtml-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; -o-border-bottom-left-radius: 3px; -ms-border-bottom-left-radius: 3px; -khtml-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; background-color: #667980; padding: 4px 3px 4px 6px; text-align: center; text-shadow: none; color: #fff; }
|
||||
@ -663,20 +666,5 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; }
|
||||
.cms-menu-list.collapsed li .text, .cms-menu-list.collapsed li .toggle-children { display: none; }
|
||||
.cms-menu-list.collapsed li > li { display: none; }
|
||||
|
||||
/** -------------------------------------------- ModelAdmin -------------------------------------------- */
|
||||
.ModelAdmin .ResultAssemblyBlock { display: none; }
|
||||
.ModelAdmin .cms-content-tools h3.cms-panel-header { display: none; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav { overflow: hidden; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav li { max-width: 99%; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav li a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 85%; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder #ModelClassSelector { margin-bottom: 2px; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder #ModelClassSelector select { width: 96%; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab { border: 1px solid #aaa; margin-top: -1px; background: #fff; padding: 8px; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab h3 { margin-top: 16px; margin-bottom: 10px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form input { margin: 0px; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .field { border-bottom: 0px; margin-bottom: 6px; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .Actions { max-width: 100%; overflow: hidden; }
|
||||
.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .Actions button.ss-ui-action-minor { display: none; }
|
||||
|
||||
.SecurityAdmin .cms-edit-form .cms-content-header h2 { display: none; }
|
||||
.SecurityAdmin .permissioncheckboxset .optionset li, .SecurityAdmin .permissioncheckboxsetfield_readonly .optionset li { float: none; width: auto; }
|
||||
|
@ -1,180 +0,0 @@
|
||||
/**
|
||||
* File: ModelAdmin.History.js
|
||||
*/
|
||||
(function($) {
|
||||
$.entwine('ss', function($){
|
||||
/**
|
||||
* Class: .ModelAdmin
|
||||
*
|
||||
* A simple ajax browser history implementation tailored towards
|
||||
* navigating through search results and different forms loaded into
|
||||
* the ModelAdmin right panels. The logic listens to search and form loading
|
||||
* events, keeps track of the loaded URLs, and will display graphical back/forward
|
||||
* buttons where appropriate. A search action will cause the history to be reset.
|
||||
*
|
||||
* Note: The logic does not replay save operations or hook into any form actions.
|
||||
*
|
||||
* Available Events:
|
||||
* - historyAdd
|
||||
* - historyStart
|
||||
* - historyGoFoward
|
||||
* - historyGoBack
|
||||
*
|
||||
* Todo:
|
||||
* Switch tab state when re-displaying search forms
|
||||
* Reload search parameters into forms
|
||||
*/
|
||||
$('.ModelAdmin').entwine({
|
||||
|
||||
/**
|
||||
* Variable: History
|
||||
*/
|
||||
History: [],
|
||||
|
||||
/**
|
||||
* Variable: Future
|
||||
*/
|
||||
Future: [],
|
||||
|
||||
onmatch: function() {
|
||||
var self = this;
|
||||
|
||||
this._super();
|
||||
|
||||
// generate markup
|
||||
this.find('#right').prepend(
|
||||
'<div class="historyNav">'
|
||||
+ '<a href="#" class="back">< ' + ss.i18n._t('ModelAdmin.HISTORYBACK', 'back') + '</a>'
|
||||
+ '<a href="#" class="forward">' + ss.i18n._t('ModelAdmin.HISTORYFORWARD', 'forward') + ' ></a>'
|
||||
+ '</div>'
|
||||
).find('.back,.forward').hide();
|
||||
|
||||
this.find('.historyNav .back').live('click', function() {
|
||||
self.goBack();
|
||||
return false;
|
||||
});
|
||||
|
||||
this.find('.historyNav .forward').live('click', function() {
|
||||
self.goForward();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Function: redraw
|
||||
*/
|
||||
redraw: function() {
|
||||
this.find('.historyNav .forward').toggle(Boolean(this.getFuture().length > 0));
|
||||
this.find('.historyNav .back').toggle(Boolean(this.getHistory().length > 1));
|
||||
|
||||
this._super();
|
||||
},
|
||||
|
||||
/**
|
||||
* Function: startHistory
|
||||
*
|
||||
* Parameters:
|
||||
* (String) url - ...
|
||||
* (Object) data - ...
|
||||
*/
|
||||
startHistory: function(url, data) {
|
||||
this.trigger('historyStart', {url: url, data: data});
|
||||
|
||||
this.setHistory([]);
|
||||
this.addHistory(url, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an item to the history, to be accessed by goBack and goForward
|
||||
*/
|
||||
addHistory: function(url, data) {
|
||||
this.trigger('historyAdd', {url: url, data: data});
|
||||
|
||||
// Combine data into URL
|
||||
if(data) {
|
||||
if(url.indexOf('?') == -1) url += '?' + $.param(data);
|
||||
else url += '&' + $.param(data);
|
||||
}
|
||||
// Add to history
|
||||
this.getHistory().push(url);
|
||||
// Reset future
|
||||
this.setFuture([]);
|
||||
|
||||
this.redraw();
|
||||
},
|
||||
|
||||
/**
|
||||
* Function: goBack
|
||||
*/
|
||||
goBack: function() {
|
||||
if(this.getHistory() && this.getHistory().length) {
|
||||
if(this.getFuture() == null) this.setFuture([]);
|
||||
|
||||
var currentPage = this.getHistory().pop();
|
||||
var previousPage = this.getHistory()[this.getHistory().length-1];
|
||||
|
||||
this.getFuture().push(currentPage);
|
||||
|
||||
this.trigger('historyGoBack', {url:previousPage});
|
||||
|
||||
// load new location
|
||||
$('.cms-content').loadForm(previousPage);
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function: goForward
|
||||
*/
|
||||
goForward: function() {
|
||||
if(this.getFuture() && this.getFuture().length) {
|
||||
if(this.getFuture() == null) this.setFuture([]);
|
||||
|
||||
var nextPage = this.getFuture().pop();
|
||||
|
||||
this.getHistory().push(nextPage);
|
||||
|
||||
this.trigger('historyGoForward', {url:nextPage});
|
||||
|
||||
// load new location
|
||||
$('.cms-content').loadForm(nextPage);
|
||||
|
||||
this.redraw();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Class: #SearchForm_holder form
|
||||
*
|
||||
* A search action will cause the history to be reset.
|
||||
*/
|
||||
$('#SearchForm_holder form').entwine({
|
||||
onmatch: function() {
|
||||
var self = this;
|
||||
this.bind('beforeSubmit', function(e) {
|
||||
$('.ModelAdmin').startHistory(
|
||||
self.attr('action'),
|
||||
self.serializeArray()
|
||||
);
|
||||
});
|
||||
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Class: form[name=Form_ResultsForm] tbody td a
|
||||
*
|
||||
* We have to apply this to the result table buttons instead of the
|
||||
* more generic form loading.
|
||||
*/
|
||||
$('form[name=Form_ResultsForm] tbody td a').entwine({
|
||||
onclick: function(e) {
|
||||
$('.ModelAdmin').addHistory(this.attr('href'));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
})(jQuery);
|
@ -15,7 +15,9 @@
|
||||
this.find('ul').addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all');
|
||||
this.find('li').addClass('ui-state-default ui-corner-top');
|
||||
// TODO Figure out selected tab
|
||||
this.find('li:first').selectIt();
|
||||
var selected = this.find('li.current');
|
||||
if(!selected.length) selected = this.find('li:first');
|
||||
selected.selectIt();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
/** --------------------------------------------
|
||||
* ModelAdmin
|
||||
* -------------------------------------------- */
|
||||
|
||||
.ModelAdmin {
|
||||
// Disable by default, will be replaced by more intuitive column selection in new data grid
|
||||
.ResultAssemblyBlock {
|
||||
display: none;
|
||||
}
|
||||
.cms-content-tools {
|
||||
h3.cms-panel-header {
|
||||
display:none;
|
||||
}
|
||||
#SearchForm_holder {
|
||||
ul.ui-tabs-nav {
|
||||
overflow:hidden;
|
||||
li {
|
||||
max-width:99%;
|
||||
a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow:ellipsis;
|
||||
//above 3 lines can also be achieved with mixin below
|
||||
//@include ellipsis;
|
||||
max-width:85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
#ModelClassSelector {
|
||||
margin-bottom:2px;
|
||||
select {
|
||||
width:96%;
|
||||
}
|
||||
}
|
||||
div.tab {
|
||||
border:1px solid #aaa; //following color from the jquery smoothness theme
|
||||
margin-top:-1px;
|
||||
background:#fff; //backround is kept white to follow tabs
|
||||
padding:$grid-x;
|
||||
|
||||
h3 {
|
||||
margin-top:16px;
|
||||
margin-bottom:10px;
|
||||
@include doubleborder(bottom, $color-light-separator, lighten($color-light-separator, 10%))
|
||||
}
|
||||
|
||||
form {
|
||||
input {
|
||||
margin:0px;
|
||||
}
|
||||
.field {
|
||||
border-bottom:0px;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.Actions {
|
||||
max-width:100%;
|
||||
overflow:hidden;
|
||||
button.ss-ui-action-minor {
|
||||
//removing the "clear search" button
|
||||
display:none;
|
||||
}
|
||||
input.action {
|
||||
//experimenting with text-overlow:ellipsis on action buttons
|
||||
//currently disabled as this ignores padding-left on the buttons in Firefox
|
||||
//and makes the text appear in front of the icon in Firefox 10
|
||||
//See a screenshot here: http://db.tt/iBi39rRt
|
||||
|
||||
//white-space: nowrap;
|
||||
//overflow: hidden;
|
||||
//text-overflow:ellipsis;
|
||||
//max-width:100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -179,7 +179,7 @@ body.cms {
|
||||
/** --------------------------------------------
|
||||
* Tabs
|
||||
* -------------------------------------------- */
|
||||
.ui-tabs .cms-content-header .ui-tabs-nav, .cms-dialog .ui-tabs-nav {
|
||||
.cms-content-header .ui-tabs-nav, .cms-dialog .ui-tabs-nav {
|
||||
li {
|
||||
margin:0;
|
||||
a {
|
||||
@ -568,6 +568,7 @@ body.cms {
|
||||
position: relative;
|
||||
|
||||
.cms-panel-header {
|
||||
clear: both;
|
||||
margin: 0 0 $grid-y - 1;
|
||||
line-height: $grid-y * 3;
|
||||
|
||||
@ -1237,6 +1238,14 @@ body.cms-dialog {
|
||||
|
||||
}
|
||||
|
||||
/** --------------------------------------------
|
||||
* Search forms (used in AssetAdmin, ModelAdmin, etc)
|
||||
* -------------------------------------------- */
|
||||
.cms-search-form {
|
||||
overflow: auto;
|
||||
margin-bottom: $grid-y*2;
|
||||
}
|
||||
|
||||
/** --------------------------------------------
|
||||
* Step labels
|
||||
* -------------------------------------------- */
|
||||
|
@ -1,18 +1,30 @@
|
||||
<div class="cms-content center $BaseCSSClasses" data-layout-type="border">
|
||||
|
||||
<div class="cms-content-header north">
|
||||
<div><h2>
|
||||
<% if SectionTitle %>
|
||||
$SectionTitle
|
||||
<% else %>
|
||||
<% _t('ModelAdmin.Title', 'Data Models') %>
|
||||
<% end_if %>
|
||||
</h2></div>
|
||||
<div>
|
||||
<h2>
|
||||
<% if SectionTitle %>
|
||||
$SectionTitle
|
||||
<% else %>
|
||||
<% _t('ModelAdmin.Title', 'Data Models') %>
|
||||
<% end_if %>
|
||||
</h2>
|
||||
|
||||
<div class="cms-content-header-tabs ss-ui-tabs-nav">
|
||||
<ul>
|
||||
<% control ManagedModelTabs %>
|
||||
<li class="tab-$ClassName $LinkOrCurrent">
|
||||
<a href="$Link" class="cms-panel-link">$Title</a>
|
||||
</li>
|
||||
<% end_control %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
$Tools
|
||||
|
||||
<div class="cms-content-fields center ui-widget-content">
|
||||
<div class="cms-content-fields center ui-widget-content" data-layout-type="border">
|
||||
$Tools
|
||||
$EditForm
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
<% if Results %>
|
||||
$Form
|
||||
<% else %>
|
||||
<p><% sprintf(_t('ModelAdmin.NORESULTS', 'No results'), $ModelPluralName) %></p>
|
||||
<% end_if %>
|
@ -1,33 +1,12 @@
|
||||
<div class="cms-content-tools west cms-panel cms-panel-layout" data-expandOnClick="true" data-layout-type="border">
|
||||
<div class="cms-content-tools west cms-panel cms-panel-layout collapsed" id="cms-content-tools" data-expandOnClick="true" data-layout-type="border">
|
||||
<div class="cms-panel-content center">
|
||||
<h3 class="cms-panel-header"><% _t('Filter', 'Filter') %></h3>
|
||||
|
||||
<div id="SearchForm_holder" class="leftbottom ss-tabset">
|
||||
<% if SearchClassSelector = tabs %>
|
||||
<ul>
|
||||
<% control ModelForms %>
|
||||
<li class="$FirstLast"><a id="tab-ModelAdmin_$Title.HTMLATT" href="#Form_$ClassName">$Title</a></li>
|
||||
<% end_control %>
|
||||
</ul>
|
||||
<% end_if %>
|
||||
<h3 class="cms-panel-header"><% _t('FILTER', 'Filter') %></h3>
|
||||
$SearchForm
|
||||
|
||||
<% if SearchClassSelector = dropdown %>
|
||||
<div id="ModelClassSelector" class="ui-widget-container">
|
||||
Search for:
|
||||
<select>
|
||||
<% control ModelForms %>
|
||||
<option value="Form_$ClassName">$Title</option>
|
||||
<% end_control %>
|
||||
</select>
|
||||
</div>
|
||||
<% end_if %>
|
||||
|
||||
<% control ModelForms %>
|
||||
<div class="tab" id="Form_$ClassName">
|
||||
$Content
|
||||
</div>
|
||||
<% end_control %>
|
||||
</div>
|
||||
<h3 class="cms-panel-header"><% _t('IMPORT', 'Import') %></h3>
|
||||
$ImportForm
|
||||
</div>
|
||||
<div class="cms-panel-content-collapsed">
|
||||
<h3 class="cms-panel-header"><% _t('FILTER', 'Filter') %></h3>
|
||||
</div>
|
||||
|
||||
</div>
|
@ -37,7 +37,8 @@ unfortunately there is no clear upgrade path for every interface detail.
|
||||
As a starting point, have a look at the new templates in `cms/templates`
|
||||
and `sapphire/admin/templates`, as well as the new [jQuery.entwine](https://github.com/hafriedlander/jquery.entwine)
|
||||
based JavaScript logic. Have a look at the new ["Extending the CMS" guide](../howto/extending-the-cms),
|
||||
["CSS" guide](../topics/css) and ["JavaScript" guide](../topics/javascript) to get you started.
|
||||
["CSS" guide](../topics/css), ["JavaScript" guide](../topics/javascript) and
|
||||
["CMS Architecture" guide](/reference/cms-architecture) to get you started.
|
||||
|
||||
### New tree library ###
|
||||
|
||||
@ -60,6 +61,17 @@ which will help users understand the new "Add page" dialog.
|
||||
For example, a `TeamPage` type could be described as "Lists all team members, linking to their profiles".
|
||||
Note: This property is optional (defaults to an empty string), but its usage is highly encouraged.
|
||||
|
||||
### New ModelAdmin interface, removed sub-controllers
|
||||
|
||||
ModelAdmin has been substanially rewritten to natively support the `[api:GridField]` API
|
||||
for more flexible data presentation (replacing `[api:ComplexTableField]`),
|
||||
and the `[api:DataList]` API for more expressive querying.
|
||||
|
||||
If you have overwritten any methods in the class, customized templates,
|
||||
or implemented your own `$collection_controller_class`/`$record_controller_class` controllers,
|
||||
please refer to the new [ModelAdmin documentation](/reference/modeladmin)
|
||||
on details for how to achieve the same goals in the new class.
|
||||
|
||||
### Stylesheet preprocessing via SCSS and the "compass" module ###
|
||||
|
||||
CSS files in the `cms` and `sapphire/admin` modules are now generated through
|
||||
|
@ -2,111 +2,221 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
*Replaces GenericDataAdmin in Silverstripe 2.3*
|
||||
Provides a simple way to utilize the SilverStripe CMS UI with your own data models,
|
||||
and create searchable list and edit views of them, and even providing import and export of your data.
|
||||
|
||||
The ModelAdmin provides a simple way to utilize the SilverStripe CMS UI with your own custom data models. The
|
||||
ModelAdmin uses the `[api:DataObject]`'s Scaffolding to create the search fields, forms, and displayed data within the
|
||||
CMS.
|
||||
It uses the framework's knowledge about the model to provide sensible defaults,
|
||||
allowing you to get started in a couple of lines of code,
|
||||
while still providing a solid base for customization.
|
||||
|
||||
In order to customize the ModelAdmin CMS interface you will need to understand how `[api:DataObject]` works.
|
||||
The interface is mainly powered by the `[GridField](/topics/grid-field)` class,
|
||||
which can also be used in other CMS areas (e.g. to manage a relation on a `SiteTree`
|
||||
record in the standard CMS interface).
|
||||
|
||||
## Requirements
|
||||
## Setup
|
||||
|
||||
*Requires Silverstripe 2.3*
|
||||
|
||||
## Usage
|
||||
|
||||
### Step 1
|
||||
Extend ModelAdmin with a custom class for your admin area, and edit the `$managed_models` property with the list of
|
||||
data objects you want to scaffold an interface for:
|
||||
|
||||
:::php
|
||||
class MyCatalogAdmin extends ModelAdmin {
|
||||
|
||||
public static $managed_models = array( //since 2.3.2
|
||||
'Product',
|
||||
'Category'
|
||||
);
|
||||
|
||||
static $url_segment = 'products'; // will be linked as /admin/products
|
||||
static $menu_title = 'My Product Admin';
|
||||
|
||||
}
|
||||
|
||||
|
||||
To add the ModelAdmin to your CMS menu, you simply need to define a couple of statics on your ModelAdmin subclass. See
|
||||
`[api:LeftAndMain]` on how to make your menu title translatable.
|
||||
|
||||
|
||||
### Step 2
|
||||
Add a `$searchable_fields` (See `[api:ModelAdmin::$searchable_fields]`) property to your data
|
||||
models, to define the fields and filters for the search interface:
|
||||
|
||||
Datamodel `Product`:
|
||||
Let's assume we want to manage a simple product listing as a sample data model:
|
||||
A product can have a name, price, and a category.
|
||||
|
||||
:::php
|
||||
class Product extends DataObject {
|
||||
|
||||
static $db = array(
|
||||
'Name' => 'Varchar',
|
||||
'ProductCode' => 'Varchar',
|
||||
'Description' => 'Text',
|
||||
'Price' => 'Currency'
|
||||
);
|
||||
|
||||
static $has_one = array(
|
||||
'Category' => 'Category'
|
||||
);
|
||||
|
||||
static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
);
|
||||
|
||||
static $db = array('Name' => 'Varchar', 'ProductCode' => 'Varchar', 'Price' => 'Currency');
|
||||
static $has_one = array('Category' => 'Category');
|
||||
}
|
||||
class Category extends DataObject {
|
||||
static $db = array('Title' => 'Text');
|
||||
static $has_many = array('Products' => 'Product');
|
||||
}
|
||||
|
||||
|
||||
Datamodel `Category`:
|
||||
To create your own `ModelAdmin`, simply extend the base class,
|
||||
and edit the `$managed_models` property with the list of
|
||||
data objects you want to scaffold an interface for.
|
||||
The class can manage multiple models in parallel, if required.
|
||||
We'll name it `MyAdmin`, but the class name can be anything you want.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
class Category extends DataObject {
|
||||
static $db = array(
|
||||
'Title' => 'Text'
|
||||
class MyAdmin extends ModelAdmin {
|
||||
public static $managed_models = array('Product','Category'); // Can manage multiple models
|
||||
static $url_segment = 'products'; // Linked as /admin/products/
|
||||
static $menu_title = 'My Product Admin';
|
||||
}
|
||||
|
||||
This will automatically add a new menu entry to the CMS, and you're ready to go!
|
||||
Try opening http://localhost/admin/products/?flush=all.
|
||||
|
||||
## Search Fields
|
||||
|
||||
ModelAdmin uses the `[SearchContext](/reference/searchcontext)` class to provide
|
||||
a search form, as well as get the searched results. Every DataObject can have its own context,
|
||||
based on the fields which should be searchable. The class makes a guess at how those fields
|
||||
should be searched, e.g. showing a checkbox for any boolean fields in your `$db` definition.
|
||||
|
||||
To remove, add or modify searchable fields, define a new `[$searchable_fields](api:DataObject::$searchable_fields)`
|
||||
static on your model class (see `[SearchContext](/reference/searchcontext)` docs for details).
|
||||
|
||||
:::php
|
||||
class Product extends DataObject {
|
||||
// ...
|
||||
static $searchable_fields = array(
|
||||
'Name',
|
||||
'ProductCode'
|
||||
// leaves out the 'Price' field, removing it from the search
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
For a more sophisticated customization, for example configuring the form fields
|
||||
for the search form, override `[api:DataObject->getCustomSearchContext()]` on your model class.
|
||||
|
||||
### Step 3
|
||||
You can now log in to the main CMS admin and manage your data objects, with no extra implementation required.
|
||||
## Result Columns
|
||||
|
||||
![](_images/modeladmin_edit.png)
|
||||
The results are shown in a tabular listing, powered by the `[GridField](/topics/grid-field)`,
|
||||
more specifically the `[api:GridFieldDataColumns]` component.
|
||||
It looks for a `[api:DataObject::$summary_fields]` static on your model class,
|
||||
where you can add or remove columns, or change their title.
|
||||
|
||||
![](_images/modeladmin_results.png)
|
||||
:::php
|
||||
class Product extends DataObject {
|
||||
// ...
|
||||
static $summary_fields = array(
|
||||
'Name' => 'Name',
|
||||
'Price' => 'Cost', // renames the column to "Cost"
|
||||
// leaves out the 'ProductCode' field, removing the column
|
||||
);
|
||||
}
|
||||
|
||||
### Note about has_one
|
||||
## Results Customization
|
||||
|
||||
Scaffolding **has_one** relationships in your ModelAdmin relies on a column in the related model to be named **Title**
|
||||
or **Name** of a string type (varchar, char, etc). These will be pulled in to the dropdown when creating a new object.
|
||||
The results are retrieved from `[api:SearchContext->getResults()]`,
|
||||
based on the parameters passed through the search form.
|
||||
If no search parameters are given, the results will show every record.
|
||||
Results are a `[api:DataList]` instance, so can be customized by additional
|
||||
SQL filters, joins, etc (see [datamodel](/topics/datamodel) for more info).
|
||||
|
||||
If you are seeing a list of ID#s when creating new objects, ensure you have one of those two in the related model.
|
||||
For example, we might want to exclude all products without prices in our sample `MyAdmin` implementation.
|
||||
|
||||
## Searchable Fields
|
||||
:::php
|
||||
class MyAdmin extends ModelAdmin {
|
||||
// ...
|
||||
function getList() {
|
||||
$list = parent::getList();
|
||||
// Always limit by model class, in case you're managing multiple
|
||||
if($this->modelClass == 'Product') {
|
||||
$list->exclude('Price', '0');
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
You can customize the fields which are searchable for each managed DataObject class, as well as the ways in which the
|
||||
fields are searched (e.g. "partial match", "fulltext", etc.) using `$searchable_fields`.
|
||||
You can also customize the search behaviour directly on your `ModelAdmin` instance.
|
||||
For example, we might want to have a checkbox which limits search results to expensive products (over $100).
|
||||
|
||||
* See `[api:DataObject]`
|
||||
:::php
|
||||
class MyAdmin extends ModelAdmin {
|
||||
// ...
|
||||
function getSearchContext() {
|
||||
$context = parent::getSearchContext();
|
||||
if($this->modelClass == 'Product') {
|
||||
$context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff'));
|
||||
}
|
||||
return $context;
|
||||
}
|
||||
function getList() {
|
||||
$list = parent::getList();
|
||||
$params = $this->request->requestVar('q'); // use this to access search parameters
|
||||
if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) {
|
||||
$list->exclude('Price:LessThan', '100');
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
![](_images/modeladmin_search.png)
|
||||
## Managing Relationships
|
||||
|
||||
## Summary Fields
|
||||
Has-one relationships are simply implemented as a `[api:DropdownField]` by default.
|
||||
Consider replacing it with a more powerful interface in case you have many records
|
||||
(through customizing `[api:DataObject->getCMSFields]`).
|
||||
|
||||
Summary Fields are the columns which are shown in the `[api:TableListField]` when viewing DataObjects. These can be
|
||||
customized for each `[api:DataObject]`'s search results using `$summary_fields`.
|
||||
Has-many and many-many relationships are usually handled via the `[GridField](/topics/grid-field)` class,
|
||||
more specifically the `[api:GridFieldAddExistingAutocompleter]` and `[api:GridFieldRelationDelete]` components.
|
||||
They provide a list/detail interface within a single record edited in your ModelAdmin.
|
||||
|
||||
* See `[api:DataObject]`
|
||||
## Permissions
|
||||
|
||||
`ModelAdmin` respects the permissions set on the model, through methods on your `DataObject` implementations:
|
||||
`canView()`, `canEdit()`, `canDelete()`, and `canCreate`.
|
||||
|
||||
In terms of access control to the interface itself, every `ModelAdmin` subclass
|
||||
creates its own "[permission code](/reference/permissions)", which can be assigned
|
||||
to groups through the `admin/security` management interface. To further limit
|
||||
permission, either override checks in `ModelAdmin->init()`, or define
|
||||
more permission codes through the `ModelAdmin::$required_permission_codes` static.
|
||||
|
||||
## Data Import
|
||||
|
||||
The `ModelAdmin` class provides import of CSV files through the `[api:CsvBulkLoader]` API.
|
||||
which has support for column mapping, updating existing records,
|
||||
and identifying relationships - so its a powerful tool to get your data into a SilverStripe database.
|
||||
|
||||
By default, each model management interface allows uploading a CSV file
|
||||
with all columns autodetected. To override with a more specific importer implementation,
|
||||
use the `[api:ModelAdmin::$model_importers] static.
|
||||
|
||||
## Data Export
|
||||
|
||||
Export is also available, although at the moment only to the CSV format,
|
||||
through a button at the end of a results list. You can also export search results.
|
||||
It is handled through the `[api:GridFieldExportButton]` component.
|
||||
|
||||
To customize the exported columns, override the edit form implementation in your `ModelAdmin`:
|
||||
|
||||
:::php
|
||||
class MyAdmin extends ModelAdmin {
|
||||
// ...
|
||||
function getEditForm($id = null) {
|
||||
$form = parent::getEditForm($id);
|
||||
if($this->modelClass == 'Product') {
|
||||
$grid = $form->Fields()->fieldByName('Product');
|
||||
$grid->getConfig()->getComponentByType('GridFieldExporter')
|
||||
->setExportColumns(array(
|
||||
// Excludes 'ProductCode' from the export
|
||||
'Name' => 'Name',
|
||||
'Price' => 'Price'
|
||||
));
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
## Extending existing ModelAdmins
|
||||
|
||||
Sometimes you'll work with ModelAdmins from other modules, e.g. the product management
|
||||
of an ecommerce module. To customize this, you can always subclass. But there's
|
||||
also another tool at your disposal: The `[api:Extension]` API.
|
||||
|
||||
:::php
|
||||
class MyAdminExtension extends Extension {
|
||||
function updateEditForm(&$form) {
|
||||
$form->Fields()->push(/* ... */)
|
||||
}
|
||||
}
|
||||
|
||||
// mysite/_config.php
|
||||
Object::add_extension('MyAdmin', 'MyAdminExtension');
|
||||
|
||||
The following extension points are available: `updateEditForm()`, `updateSearchContext()`,
|
||||
`updateSearchForm()`, `updateList()`, `updateImportForm`.
|
||||
|
||||
## Customizing the interface
|
||||
|
||||
Interfaces like `ModelAdmin` can be customized in many ways:
|
||||
|
||||
* JavaScript behaviour (e.g. overwritten jQuery.entwine rules)
|
||||
* CSS styles
|
||||
* HTML markup through templates
|
||||
|
||||
In general, use your `ModelAdmin->init()` method to add additional requirements
|
||||
through the `[Requirements](/reference/requirements)` API.
|
||||
For an introduction how to customize the CMS templates, see our [CMS Architecture Guide](/reference/cms-architecture).
|
||||
|
||||
## Related
|
||||
|
||||
|
@ -15,10 +15,6 @@ the $fields constructor parameter.
|
||||
`[api:SearchContext]` is mainly used by `[api:ModelAdmin]`, our generic data administration interface. Another
|
||||
implementation can be found in generic frontend search forms through the [genericviews](http://silverstripe.org/generic-views-module) module.
|
||||
|
||||
## Requirements
|
||||
|
||||
* *SilverStripe 2.3*
|
||||
|
||||
## Usage
|
||||
|
||||
Getting results
|
||||
|
@ -123,22 +123,17 @@ class FormScaffolder extends Object {
|
||||
$this->obj->fieldLabel($relationship)
|
||||
);
|
||||
}
|
||||
$relationshipFields = singleton($component)->summaryFields();
|
||||
|
||||
$foreignKey = $this->obj->getRemoteJoinField($relationship);
|
||||
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField';
|
||||
$ctf = new $fieldClass(
|
||||
$this,
|
||||
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'GridField';
|
||||
$grid = Object::create($fieldClass,
|
||||
$relationship,
|
||||
null,
|
||||
$relationshipFields,
|
||||
"getCMSFields"
|
||||
$this->obj->fieldLabel($relationship),
|
||||
$this->obj->$relationship(),
|
||||
GridFieldConfig_RelationEditor::create()
|
||||
);
|
||||
$ctf->setPermissions(TableListField::permissions_for_object($component));
|
||||
if($this->tabbed) {
|
||||
$fields->addFieldToTab("Root.$relationship", $ctf);
|
||||
$fields->addFieldToTab("Root.$relationship", $grid);
|
||||
} else {
|
||||
$fields->push($ctf);
|
||||
$fields->push($grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -152,22 +147,17 @@ class FormScaffolder extends Object {
|
||||
);
|
||||
}
|
||||
|
||||
$relationshipFields = singleton($component)->summaryFields();
|
||||
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField';
|
||||
$ctf = new $fieldClass(
|
||||
$this,
|
||||
$fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'GridField';
|
||||
$grid = Object::create($fieldClass,
|
||||
$relationship,
|
||||
$this->obj->fieldLabel($relationship),
|
||||
$this->obj->$relationship(),
|
||||
$relationshipFields,
|
||||
"getCMSFields"
|
||||
GridFieldConfig_RelationEditor::create()
|
||||
);
|
||||
|
||||
$ctf->setPermissions(TableListField::permissions_for_object($component));
|
||||
$ctf->popupClass = "ScaffoldingComplexTableField_Popup";
|
||||
if($this->tabbed) {
|
||||
$fields->addFieldToTab("Root.$relationship", $ctf);
|
||||
$fields->addFieldToTab("Root.$relationship", $grid);
|
||||
} else {
|
||||
$fields->push($ctf);
|
||||
$fields->push($grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,93 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Used by ModelAdmin scaffolding, to manage many-many relationships.
|
||||
*
|
||||
* @package forms
|
||||
* @subpackage fields-relational
|
||||
*/
|
||||
class ScaffoldingComplexTableField_Popup extends ComplexTableField_Popup {
|
||||
|
||||
public static $allowed_actions = array(
|
||||
'filter', 'record', 'httpSubmission', 'handleAction', 'handleField'
|
||||
);
|
||||
|
||||
function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) {
|
||||
$this->dataObject = $dataObject;
|
||||
|
||||
Requirements::clear();
|
||||
|
||||
$actions = new FieldList();
|
||||
if(!$readonly) {
|
||||
$actions->push(
|
||||
$saveAction = new FormAction("saveComplexTableField", "Save")
|
||||
);
|
||||
$saveAction->addExtraClass('save');
|
||||
}
|
||||
|
||||
$fields->push(new HiddenField("ComplexTableField_Path", Director::absoluteBaseURL()));
|
||||
|
||||
parent::__construct($controller, $name, $fields, $validator, $readonly, $dataObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a generic action passed in by the URL mapping.
|
||||
*
|
||||
* @param SS_HTTPRequest $request
|
||||
*/
|
||||
public function handleAction($request) {
|
||||
$action = str_replace("-","_",$request->param('Action'));
|
||||
if(!$this->action) $this->action = 'index';
|
||||
|
||||
if($this->checkAccessAction($action)) {
|
||||
if($this->hasMethod($action)) {
|
||||
$result = $this->$action($request);
|
||||
|
||||
// Method returns an array, that is used to customise the object before rendering with a template
|
||||
if(is_array($result)) {
|
||||
return $this->getViewer($action)->process($this->customise($result));
|
||||
|
||||
// Method returns a string / object, in which case we just return that
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// There is no method, in which case we just render this object using a (possibly alternate) template
|
||||
} else {
|
||||
return $this->getViewer($action)->process($this);
|
||||
}
|
||||
} else {
|
||||
return $this->httpError(403, "Action '$action' isn't allowed on class $this->class");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to render results for an autocomplete filter.
|
||||
*
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return void
|
||||
*/
|
||||
function filter($request) {
|
||||
//$model = singleton($this->modelClass);
|
||||
$context = $this->dataObject->getDefaultSearchContext();
|
||||
$value = $request->getVar('q');
|
||||
$results = $context->getResults(array("Name"=>$value));
|
||||
header("Content-Type: text/plain");
|
||||
foreach($results as $result) {
|
||||
echo $result->Name . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to populate edit box with a single data object via Ajax query
|
||||
*/
|
||||
function record($request) {
|
||||
$type = $request->getVar('type');
|
||||
$value = $request->getVar('value');
|
||||
if ($type && $value) {
|
||||
$record = DataObject::get_one($this->dataObject->class, "\"$type\" = '$value'");
|
||||
header("Content-Type: text/plain");
|
||||
echo json_encode(array("record"=>$record->toMap()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
window.onload = function() {
|
||||
|
||||
resourcePath = jQuery('form').attr('action');
|
||||
|
||||
jQuery("fieldset input:first").attr('autocomplete', 'off').autocomplete({list: ["mark rickerby", "maxwell sparks"]});
|
||||
|
||||
jQuery("fieldset input:first").bind('activate.autocomplete', function(e){
|
||||
|
||||
type = jQuery("fieldset input:first").attr('name');
|
||||
value = jQuery("fieldset input:first").val();
|
||||
|
||||
jQuery.getJSON(resourcePath + '/record', {'type':type, 'value':value}, function(data) {
|
||||
jQuery('form input').each(function(i, elm){
|
||||
if(elm.name in data.record) {
|
||||
val = data.record[elm.name];
|
||||
if (val != null) elm.setAttribute('value', val);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
};
|
@ -37,8 +37,15 @@ class ForeignKey extends Int {
|
||||
$field = new UploadField($relationName, $title);
|
||||
} else {
|
||||
$titleField = (singleton($hasOneClass)->hasField('Title')) ? "Title" : "Name";
|
||||
$map = DataList::create($hasOneClass)->map("ID", $titleField);
|
||||
$field = new DropdownField($this->name, $title, $map, null, null, ' ');
|
||||
$list = DataList::create($hasOneClass);
|
||||
// Don't scaffold a dropdown for large tables, as making the list concrete
|
||||
// might exceed the available PHP memory in creating too many DataObject instances
|
||||
if($list->count() < 100) {
|
||||
$field = new DropdownField($this->name, $title, $list->map("ID", $titleField), null, null, ' ');
|
||||
} else {
|
||||
$field = new NumericField($this->name, $title);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $field;
|
||||
|
Loading…
x
Reference in New Issue
Block a user