Permalink
Browse files

API CHANGE ModelAdmin uses GridField, DataList and new layout. Remove…

…d ModelAdmin_CollectionController, ModelAdmin_RecordController and related functionality.

API CHANGE Removed ModelAdmin->ResultColumns()/ColumnSelectionField, selection of own fields no longer possible through the UI, to be replaced by a more generic GridField component
  • Loading branch information...
1 parent 64db811 commit e12a3a4ab7bc4e344d2e3aa8ca1408bcffe542a3 @chillu chillu committed Mar 9, 2012
View
865 admin/code/ModelAdmin.php
@@ -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 @@
*
* 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,132 +93,117 @@
* @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.
*
* @todo remove reliance on urlParams
*/
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');
- }
-
- return $class;
}
-
- /**
- * 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');
+
+ 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;
}
-
+
/**
- * Add mappings for generic form constructors to automatically delegate to a scaffolded form object.
+ * @return SearchContext
*/
- function defineMethods() {
- parent::defineMethods();
- foreach($this->getManagedModels() as $class => $options) {
- if(is_numeric($class)) $class = $options;
- $this->addWrapperMethod($class, 'bindModelController');
- self::$allowed_actions[] = $class;
- }
+ 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;
}
-
+
/**
- * Base scaffolding method for returning a generic model instance.
+ * @return Form
*/
- public function bindModelController($model, $request = null) {
- $class = $this->getCollectionControllerClass($model);
- return new $class($this, $model);
+ 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;
}
- /**
- * 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
* @uses SearchContext
* @uses SearchFilter
* @return SS_List of forms
*/
- protected function getModelForms() {
+ protected function getManagedModelTabs() {
$models = $this->getManagedModels();
$forms = new ArrayList();
@@ -254,7 +212,8 @@ protected function getModelForms() {
$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'
)));
}
@@ -303,161 +262,7 @@ function getModelImporters() {
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 +271,11 @@ function showImportForm() {
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 +316,10 @@ public function ImportForm() {
$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 +335,13 @@ public function ImportForm() {
* @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();
+ $importerClass = $importers[$this->modelClass];
+ $loader = new $importerClass($this->modelClass);
// File wasn't properly uploaded, show a reminder to the user
if(
@@ -566,467 +376,38 @@ function import($data, $form, $request) {
$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();
- }
-}
-
-/**
- * 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));
- }
+ public function Breadcrumbs($unlinked = false) {
+ $items = parent::Breadcrumbs($unlinked);
- /////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- /**
- * 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();
}
- }
-
- /**
- * Returns a form for editing the attached model
- */
- 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;
+ return $items;
}
/**
- * 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
+ * overwrite the static page_length of the admin panel,
+ * should be called in the project _config file.
*/
- 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 set_page_length($length){
+ self::$page_length = $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
+ * Return the static page_length of the admin, default as 30
*/
- 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;
- }
+ static function get_page_length(){
+ return self::$page_length;
+ }
-}
-
+}
View
30 admin/css/screen.css
@@ -264,11 +264,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; }
@@ -337,7 +337,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; }
@@ -474,6 +474,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; }
@@ -662,20 +665,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; }
View
180 admin/javascript/ModelAdmin.History.js
@@ -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">&lt; ' + ss.i18n._t('ModelAdmin.HISTORYBACK', 'back') + '</a>'
- + '<a href="#" class="forward">' + ss.i18n._t('ModelAdmin.HISTORYFORWARD', 'forward') + ' &gt;</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);
View
78 admin/scss/_ModelAdmin.scss
@@ -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%;
- }
- }
- }
- }
- }
- }
-}
View
11 admin/scss/_style.scss
@@ -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;
@@ -1241,6 +1242,14 @@ body.cms-dialog {
}
/** --------------------------------------------
+ * Search forms (used in AssetAdmin, ModelAdmin, etc)
+ * -------------------------------------------- */
+.cms-search-form {
+ overflow: auto;
+ margin-bottom: $grid-y*2;
+}
+
+/** --------------------------------------------
* Step labels
* -------------------------------------------- */
.step-label {
View
32 admin/templates/Includes/ModelAdmin_Content.ss
@@ -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>
+ <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>
- $Tools
+ </div>
+ </div>
- <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>
View
5 admin/templates/Includes/ModelAdmin_Results.ss
@@ -1,5 +0,0 @@
-<% if Results %>
- $Form
-<% else %>
- <p><% sprintf(_t('ModelAdmin.NORESULTS', 'No results'), $ModelPluralName) %></p>
-<% end_if %>
View
37 admin/templates/Includes/ModelAdmin_Tools.ss
@@ -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>
View
14 docs/en/changelogs/3.0.0.md
@@ -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
View
238 docs/en/reference/modeladmin.md
@@ -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*
+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.
-## Usage
+ :::php
+ class Product extends DataObject {
+ 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');
+ }
-### 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:
+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
- 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
+ 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.
-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.
-
+## Search Fields
-### 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:
+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.
-Datamodel `Product`:
+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 $db = array(
- 'Name' => 'Varchar',
- 'ProductCode' => 'Varchar',
- 'Description' => 'Text',
- 'Price' => 'Currency'
- );
-
- static $has_one = array(
- 'Category' => 'Category'
- );
-
+ // ...
static $searchable_fields = array(
'Name',
- 'ProductCode'
+ '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.
-Datamodel `Category`:
+## Result Columns
+
+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.
:::php
- <?php
- class Category extends DataObject {
- static $db = array(
- 'Title' => 'Text'
+ 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
);
}
- ?>
+## Results Customization
+
+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).
+
+For example, we might want to exclude all products without prices in our sample `MyAdmin` implementation.
+
+ :::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 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).
+
+ :::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;
+ }
+ }
+
+## Managing Relationships
+
+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]`).
+
+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.
+
+## Permissions
+
+`ModelAdmin` respects the permissions set on the model, through methods on your `DataObject` implementations:
+`canView()`, `canEdit()`, `canDelete()`, and `canCreate`.
-### Step 3
-You can now log in to the main CMS admin and manage your data objects, with no extra implementation required.
+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.
-![](_images/modeladmin_edit.png)
+## Data Import
-![](_images/modeladmin_results.png)
+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.
-### Note about has_one
+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.
-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.
+## Data Export
-If you are seeing a list of ID#s when creating new objects, ensure you have one of those two in the related model.
+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.
-## Searchable Fields
+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(/* ... */)
+ }
+ }
-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`.
+ // mysite/_config.php
+ Object::add_extension('MyAdmin', 'MyAdminExtension');
- * See `[api:DataObject]`
+The following extension points are available: `updateEditForm()`, `updateSearchContext()`,
+`updateSearchForm()`, `updateList()`, `updateImportForm`.
-![](_images/modeladmin_search.png)
+## Customizing the interface
-## Summary Fields
+Interfaces like `ModelAdmin` can be customized in many ways:
-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`.
+ * JavaScript behaviour (e.g. overwritten jQuery.entwine rules)
+ * CSS styles
+ * HTML markup through templates
- * See `[api:DataObject]`
+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
View
4 docs/en/reference/searchcontext.md
@@ -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

0 comments on commit e12a3a4

Please sign in to comment.