Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
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...
commit e12a3a4ab7bc4e344d2e3aa8ca1408bcffe542a3 1 parent 64db811
@chillu chillu authored
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,20 +93,7 @@
* @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 @@ public function resultsTableClassName() {
*/
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
@@ -245,7 +203,7 @@ public function SearchClassSelector() {
* @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
Please sign in to comment.
Something went wrong with that request. Please try again.