diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 9f6d143a0..4034a6a61 100644 --- a/admin/code/ModelAdmin.php +++ b/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 @@ abstract class ModelAdmin extends LeftAndMain { * * Available options: * - 'title': Set custom titles for the tabs or dropdown names - * - 'collection_controller': Set a custom class to use as a collection controller for this model - * - 'record_controller': Set a custom class to use as a record controller for this model * * @var array|string */ public static $managed_models = null; - /** - * More actions are dynamically added in {@link defineMethods()} below. - */ public static $allowed_actions = array( - 'add', - 'edit', - 'delete', - 'import', - 'renderimportform', - 'handleList', - 'handleItem', - 'ImportForm' + 'ImportForm', + 'SearchForm', ); - /** - * @param string $collection_controller_class Override for controller class - */ - public static $collection_controller_class = "ModelAdmin_CollectionController"; - - /** - * @param string $collection_controller_class Override for controller class - */ - public static $record_controller_class = "ModelAdmin_RecordController"; - - /** - * Forward control to the default action handler - */ public static $url_handlers = array( - '$Action' => 'handleAction' + '$ModelClass/$Action' => 'handleAction' ); - + /** - * Model object currently in manipulation queue. Used for updating Link to point - * to the correct generic data object in generated URLs. - * - * @var string + * @var String */ - private $currentModel = false; + protected $modelClass; /** * Change this variable if you don't want the Import from CSV form to appear. @@ -120,20 +93,7 @@ abstract class ModelAdmin extends LeftAndMain { * @var int */ public static $page_length = 30; - - /** - * Class name of the form field used for the results list. Overloading this in subclasses - * can let you customise the results table field. - */ - protected $resultsTableClassName = 'GridField'; - - /** - * Return {@link $this->resultsTableClassName} - */ - public function resultsTableClassName() { - return $this->resultsTableClassName; - } - + /** * Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins. * @@ -141,103 +101,101 @@ abstract class ModelAdmin extends LeftAndMain { */ public function init() { parent::init(); - + + $models = $this->getManagedModels(); + $this->modelClass = (isset($this->urlParams['ModelClass'])) ? $this->urlParams['ModelClass'] : $models[0]; + // security check for valid models - if(isset($this->urlParams['Action']) && !in_array($this->urlParams['Action'], $this->getManagedModels())) { - //user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR); + if(!in_array($this->modelClass, $models)) { + user_error('ModelAdmin::init(): Invalid Model class', E_USER_ERROR); } - Requirements::css(SAPPHIRE_ADMIN_DIR . '/css/silverstripe.tabs.css'); // follows the jQuery UI theme conventions - - Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js'); - Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-livequery/jquery.livequery.js'); - Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-ui/jquery-ui.js'); Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ModelAdmin.js'); - Requirements::javascript(SAPPHIRE_ADMIN_DIR . '/javascript/ModelAdmin.History.js'); } - - /** - * overwrite the static page_length of the admin panel, - * should be called in the project _config file. - */ - static function set_page_length($length){ - self::$page_length = $length; - } - - /** - * Return the static page_length of the admin, default as 30 - */ - static function get_page_length(){ - return self::$page_length; - } - - /** - * Return the class name of the collection controller - * - * @param string $model model name to get the controller for - * @return string the collection controller class - */ - function getCollectionControllerClass($model) { - $models = $this->getManagedModels(); - - if(isset($models[$model]['collection_controller'])) { - $class = $models[$model]['collection_controller']; - } else { - $class = $this->stat('collection_controller_class'); + + function getEditForm($id = null) { + $list = $this->getList(); + $listField = Object::create('GridField', + $this->modelClass, + false, + $list, + $fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length')) + ->addComponent(new GridFieldExportButton()) + ->removeComponentsByType('GridFieldFilter') + ); + + // Validation + if(singleton($this->modelClass)->hasMethod('getCMSValidator')) { + $detailValidator = singleton($this->modelClass)->getCMSValidator(); + $listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator); } + + $form = new Form( + $this, + 'EditForm', + new FieldList($listField), + new FieldList() + ); + $form->addExtraClass('cms-edit-form cms-panel-padded center'); + $form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); + $form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'EditForm')); + + $this->extend('updateEditForm', $form); - return $class; + return $form; + } + + /** + * @return SearchContext + */ + public function getSearchContext() { + $context = singleton($this->modelClass)->getDefaultSearchContext(); + + // Namespace fields, for easier detection if a search is present + foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName())); + foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName())); + + $this->extend('updateSearchContext', $context); + + return $context; + } + + /** + * @return Form + */ + public function SearchForm() { + $context = $this->getSearchContext(); + $form = new Form($this, "SearchForm", + $context->getSearchFields(), + new FieldList( + Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search')) + ->setUseButtonTag(true)->addExtraClass('ss-ui-action-minor'), + Object::create('FormAction', 'search', _t('MemberTableField.SEARCH', 'Search')) + ->setUseButtonTag(true) + ), + new RequiredFields() + ); + $form->setFormMethod('get'); + $form->setFormAction($this->Link($this->modelClass)); + $form->addExtraClass('cms-search-form'); + $form->disableSecurityToken(); + $form->loadDataFrom($this->request->getVars()); + + $this->extend('updateSearchForm', $form); + + return $form; } - /** - * Return the class name of the record controller - * - * @param string $model model name to get the controller for - * @return string the record controller class - */ - function getRecordControllerClass($model) { - $models = $this->getManagedModels(); - - if(isset($models[$model]['record_controller'])) { - $class = $models[$model]['record_controller']; - } else { - $class = $this->stat('record_controller_class'); - } - - return $class; - } - - /** - * Add mappings for generic form constructors to automatically delegate to a scaffolded form object. - */ - function defineMethods() { - parent::defineMethods(); - foreach($this->getManagedModels() as $class => $options) { - if(is_numeric($class)) $class = $options; - $this->addWrapperMethod($class, 'bindModelController'); - self::$allowed_actions[] = $class; - } - } - - /** - * Base scaffolding method for returning a generic model instance. - */ - public function bindModelController($model, $request = null) { - $class = $this->getCollectionControllerClass($model); - return new $class($this, $model); - } - - /** - * This method can be overloaded to specify the UI by which the search class is chosen. - * - * It can create a tab strip or a dropdown. The dropdown is useful when there are a large number of classes. - * By default, it will show a tabs for 1-3 classes, and a dropdown for 4 or more classes. - * - * @return String: 'tabs' or 'dropdown' - */ - public function SearchClassSelector() { - return sizeof($this->getManagedModels()) > 3 ? 'dropdown' : 'tabs'; + public function getList() { + $context = $this->getSearchContext(); + $params = $this->request->requestVar('q'); + $list = $context->getResults($params); + + $this->extend('updateList', $list); + + return $list; } + /** * Returns managed models' create, search, and import forms @@ -245,7 +203,7 @@ abstract class ModelAdmin extends LeftAndMain { * @uses SearchFilter * @return SS_List of forms */ - protected function getModelForms() { + protected function getManagedModelTabs() { $models = $this->getManagedModels(); $forms = new ArrayList(); @@ -254,7 +212,8 @@ abstract class ModelAdmin extends LeftAndMain { $forms->push(new ArrayData(array ( 'Title' => (is_array($options) && isset($options['title'])) ? $options['title'] : singleton($class)->i18n_singular_name(), 'ClassName' => $class, - 'Content' => $this->$class()->getModelSidebar() + 'Link' => $this->Link($class), + 'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link' ))); } @@ -287,177 +246,28 @@ abstract class ModelAdmin extends LeftAndMain { * with a default {@link CsvBulkLoader} class. In this case the column names of the first row * in the CSV file are assumed to have direct mappings to properties on the object. * - * @return array + * @return array Map of model class names to importer instances */ function getModelImporters() { - $importers = $this->stat('model_importers'); + $importerClasses = $this->stat('model_importers'); // fallback to all defined models if not explicitly defined - if(is_null($importers)) { + if(is_null($importerClasses)) { $models = $this->getManagedModels(); foreach($models as $modelName => $options) { if(is_numeric($modelName)) $modelName = $options; - $importers[$modelName] = 'CsvBulkLoader'; + $importerClasses[$modelName] = 'CsvBulkLoader'; } } + + $importers = array(); + foreach($importerClasses as $modelClass => $importerClass) { + $importers[$modelClass] = new $importerClass($modelClass); + } return $importers; } - -} -/** - * Handles a managed model class and provides default collection filtering behavior. - * - * @package cms - * @subpackage core - */ -class ModelAdmin_CollectionController extends Controller { - public $parentController; - protected $modelClass; - - public $showImportForm = null; - - static $url_handlers = array( - '$Action' => 'handleActionOrID' - ); - - function __construct($parent, $model) { - $this->parentController = $parent; - $this->modelClass = $model; - - parent::__construct(); - } - - /** - * Appends the model class to the URL. - * - * @param string $action - * @return string - */ - function Link($action = null) { - return $this->parentController->Link(Controller::join_links($this->modelClass, $action)); - } - - /** - * Return the class name of the model being managed. - * - * @return unknown - */ - function getModelClass() { - return $this->modelClass; - } - - /** - * Delegate to different control flow, depending on whether the - * URL parameter is a number (record id) or string (action). - * - * @param unknown_type $request - * @return unknown - */ - function handleActionOrID($request) { - if (is_numeric($request->param('Action'))) { - return $this->handleID($request); - } else { - return $this->handleAction($request); - } - } - - /** - * Delegate to the RecordController if a valid numeric ID appears in the URL - * segment. - * - * @param SS_HTTPRequest $request - * @return RecordController - */ - public function handleID($request) { - $class = $this->parentController->getRecordControllerClass($this->getModelClass()); - return new $class($this, $request); - } - - // ----------------------------------------------------------------------------------------------------------------- - - /** - * Get a combination of the Search, Import and Create forms that can be inserted into a {@link ModelAdmin} sidebar. - * - * @return string - */ - public function getModelSidebar() { - return $this->renderWith('ModelSidebar'); - } - - /** - * Get a search form for a single {@link DataObject} subclass. - * - * @return Form - */ - public function SearchForm() { - $SNG_model = singleton($this->modelClass); - $context = $SNG_model->getDefaultSearchContext(); - $fields = $context->getSearchFields(); - $columnSelectionField = $this->ColumnSelectionField(); - $fields->push($columnSelectionField); - - $validator = ($SNG_model->hasMethod('getCMSValidator')) ? $SNG_model->getCMSValidator() : new RequiredFields(); - $clearAction = new ResetFormAction('clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search')); - - $form = new Form($this, "SearchForm", - $fields, - new FieldList( - new FormAction('search', _t('MemberTableField.SEARCH', 'Search')), - $clearAction - ), - $validator - ); - //$form->setFormAction(Controller::join_links($this->Link(), "search")); - $form->setFormMethod('get'); - $form->setHTMLID("Form_SearchForm_" . $this->modelClass); - $form->disableSecurityToken(); - $clearAction->setUseButtonTag(true); - $clearAction->addExtraClass('ss-ui-action-minor'); - - return $form; - } - - /** - * Create a form that consists of one button - * that directs to a give model's Add form - */ - public function CreateForm() { - $modelName = $this->modelClass; - $SNG_model = singleton($modelName); - - if($this->hasMethod('alternatePermissionCheck')) { - if(!$this->alternatePermissionCheck()) return false; - } else { - if(!$SNG_model->canCreate(Member::currentUser())) return false; - } - - $buttonLabel = sprintf(_t('ModelAdmin.CREATEBUTTON', "Create '%s'", PR_MEDIUM, "Create a new instance from a model class"), $SNG_model->i18n_singular_name()); - - $validator = ($SNG_model->hasMethod('getCMSValidator')) ? $SNG_model->getCMSValidator() : new RequiredFields(); - $createButton = FormAction::create('add', $buttonLabel)->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept'); - - $form = new Form($this, "CreateForm", - new FieldList(), - new FieldList($createButton), - $validator - ); - - $createButton->dontEscape = true; - $form->setHTMLID("Form_CreateForm_" . $this->modelClass); - - return $form; - } - - /** - * Checks if a CSV import form should be generated by a className criteria or in general for ModelAdmin. - */ - function showImportForm() { - if($this->showImportForm === null) return $this->parentController->showImportForm; - else return $this->showImportForm; - } - /** * Generate a CSV import form for a single {@link DataObject} subclass. * @@ -466,8 +276,11 @@ class ModelAdmin_CollectionController extends Controller { public function ImportForm() { $modelName = $this->modelClass; // check if a import form should be generated - if(!$this->showImportForm() || (is_array($this->showImportForm()) && !in_array($modelName,$this->showImportForm()))) return false; - $importers = $this->parentController->getModelImporters(); + if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($modelName,$this->showImportForm))) { + return false; + } + + $importers = $this->getModelImporters(); if(!$importers || !isset($importers[$modelName])) return false; if(!singleton($modelName)->canCreate(Member::currentUser())) return false; @@ -508,7 +321,10 @@ class ModelAdmin_CollectionController extends Controller { $fields, $actions ); - $form->setHTMLID("Form_ImportForm_" . $this->modelClass); + $form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'ImportForm')); + + $this->extend('updateImportForm', $form); + return $form; } @@ -524,14 +340,12 @@ class ModelAdmin_CollectionController extends Controller { * @param SS_HTTPRequest $request */ function import($data, $form, $request) { + if(!$this->showImportForm || (is_array($this->showImportForm) && !in_array($this->modelClass,$this->showImportForm))) { + return false; + } - $modelName = $data['ClassName']; - - if(!$this->showImportForm() || (is_array($this->showImportForm()) && !in_array($modelName,$this->showImportForm()))) return false; - $importers = $this->parentController->getModelImporters(); - $importerClass = $importers[$modelName]; - - $loader = new $importerClass($data['ClassName']); + $importers = $this->getModelImporters(); + $loader = $importers[$this->modelClass]; // File wasn't properly uploaded, show a reminder to the user if( @@ -566,467 +380,38 @@ class ModelAdmin_CollectionController extends Controller { $form->sessionMessage($message, 'good'); $this->redirectBack(); } - - - /** - * Return the columns available in the column selection field. - * Overload this to make other columns available - */ - public function columnsAvailable() { - return singleton($this->modelClass)->summaryFields(); - } - /** - * Return the columns selected by default in the column selection field. - * Overload this to make other columns selected by default - */ - public function columnsSelectedByDefault() { - return array_keys(singleton($this->modelClass)->summaryFields()); - } - - /** - * Give the flexibilility to show variouse combination of columns in the search result table - */ - public function ColumnSelectionField() { - $model = singleton($this->modelClass); - $source = $this->columnsAvailable(); - - // select all fields by default - $value = $this->columnsSelectedByDefault(); - - // Reorder the source so that you read items down the column and then across - $columnisedSource = array(); - $keys = array_keys($source); - $midPoint = ceil(sizeof($source)/2); - for($i=0;$i<$midPoint;$i++) { - $key1 = $keys[$i]; - $columnisedSource[$key1] = $model->fieldLabel($source[$key1]); - // If there are an odd number of items, the last item will be unset - if(isset($keys[$i+$midPoint])) { - $key2 = $keys[$i+$midPoint]; - $columnisedSource[$key2] = $model->fieldLabel($source[$key2]); - } - } - - $checkboxes = new CheckboxSetField("ResultAssembly", false, $columnisedSource, $value); - - $field = new CompositeField( - new LiteralField( - "ToggleResultAssemblyLink", - sprintf("%s", - _t('ModelAdmin.CHOOSE_COLUMNS', 'Select result columns...') - ) - ), - $checkboxesBlock = new CompositeField( - $checkboxes, - new LiteralField("ClearDiv", "
"), - new LiteralField( - "TickAllAssemblyLink", - sprintf( - "%s", - _t('ModelAdmin.SELECTALL', 'select all') - ) - ), - new LiteralField( - "UntickAllAssemblyLink", - sprintf( - "%s", - _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: - * - * function getCMSFields() { - * $fields = parent::getCMSFields(); - * if($this->exists()) { - * $ctf = new HasManyComplexTableField($this, 'MyRelations', 'MyRelation'); - * $fields->addFieldToTab('Root.Main', $ctf); - * } - * return $fields; - * } - * - * - * @return Form - */ - public function AddForm() { - $newRecord = new $this->modelClass(); - - if($newRecord->canCreate()){ - if($newRecord->hasMethod('getCMSAddFormFields')) { - $fields = $newRecord->getCMSAddFormFields(); - } else { - $fields = $newRecord->getCMSFields(); - } - - $validator = ($newRecord->hasMethod('getCMSValidator')) ? $newRecord->getCMSValidator() : new RequiredFields(); - - $actions = new FieldList ( - FormAction::create("doCreate", _t('ModelAdmin.ADDBUTTON', "Add")) - ->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept') - ); - - $form = new Form($this, "AddForm", $fields, $actions, $validator); - $form->loadDataFrom($newRecord); - $form->addExtraClass('cms-edit-form'); - - return $form; - } - } - - function doCreate($data, $form, $request) { - $className = $this->getModelClass(); - $model = new $className(); - // We write before saveInto, since this will let us save has-many and many-many relationships :-) - $model->write(); - $form->saveInto($model); - $model->write(); - - if($this->isAjax()) { - $class = $this->parentController->getRecordControllerClass($this->getModelClass()); - $recordController = new $class($this, $request, $model->ID); - return new SS_HTTPResponse( - $recordController->EditForm()->forTemplate(), - 200, - sprintf( - _t('ModelAdmin.LOADEDFOREDITING', "Loaded '%s' for editing."), - $model->Title - ) - ); - } else { - Director::redirect(Controller::join_links($this->Link(), $model->ID , 'edit')); - } - } - /** * @return ArrayList */ - public function Breadcrumbs(){ - return new ArrayList(); - } -} + public function Breadcrumbs($unlinked = false) { + $items = parent::Breadcrumbs($unlinked); -/** - * Handles operations on a single record from a managed model. - * - * @package cms - * @subpackage core - * @todo change the parent controller varname to indicate the model scaffolding functionality in ModelAdmin - */ -class ModelAdmin_RecordController extends Controller { - protected $parentController; - protected $currentRecord; - - static $allowed_actions = array('edit', 'view', 'EditForm', 'ViewForm'); - - function __construct($parentController, $request, $recordID = null) { - $this->parentController = $parentController; - $modelName = $parentController->getModelClass(); - $recordID = ($recordID) ? $recordID : $request->param('Action'); - $this->currentRecord = DataObject::get_by_id($modelName, $recordID); - - parent::__construct(); - } - - /** - * Link fragment - appends the current record ID to the URL. - */ - public function Link($action = null) { - return $this->parentController->Link(Controller::join_links($this->currentRecord->ID, $action)); - } - - ///////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Edit action - shows a form for editing this record - */ - function edit($request) { - if ($this->currentRecord) { - if($this->isAjax()) { - $this->response->setBody($this->EditForm()->forTemplate()); - $this->response->setStatusCode( - 200, - sprintf( - _t('ModelAdmin.LOADEDFOREDITING', "Loaded '%s' for editing."), - $this->currentRecord->Title - ) - ); - return $this->response; - } else { - // This is really quite ugly; to fix will require a change in the way that customise() works. :-( - return $this->parentController->parentController->customise(array( - 'Content' => $this->parentController->parentController->customise(array( - 'EditForm' => $this->EditForm() - ))->renderWith(array("{$this->class}_Content",'ModelAdmin_Content', 'LeftAndMain_Content')) - ))->renderWith(array('ModelAdmin', 'LeftAndMain')); - } + // Show the class name rather than ModelAdmin title as root node + $models = $this->getManagedModels(); + $modelSpec = ArrayLib::is_associative($models) ? $models[$this->modelClass] : null; + if(is_array($modelSpec) && isset($modelSpec['title'])) { + $items[0]->Title = $modelSpec['title']; } else { - return _t('ModelAdmin.ITEMNOTFOUND', "I can't find that item"); + $items[0]->Title = singleton($this->modelClass)->i18n_singular_name(); } + + return $items; } /** - * Returns a form for editing the attached model + * overwrite the static page_length of the admin panel, + * should be called in the project _config file. */ - public function EditForm() { - $fields = $this->currentRecord->getCMSFields(); - $fields->push(new HiddenField("ID")); - - if($this->currentRecord->hasMethod('Link')) { - $fields->push(new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator())); - } - - $validator = ($this->currentRecord->hasMethod('getCMSValidator')) ? $this->currentRecord->getCMSValidator() : new RequiredFields(); - - $actions = $this->currentRecord->getCMSActions(); - if($this->currentRecord->canEdit(Member::currentUser())){ - if(!$actions->fieldByName('action_doSave') && !$actions->fieldByName('action_save')) { - $actions->push( - FormAction::create("doSave", _t('ModelAdmin.SAVE', "Save")) - ->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept') - ); - } - }else{ - $fields = $fields->makeReadonly(); - } - - if($this->currentRecord->canDelete(Member::currentUser())) { - if(!$actions->fieldByName('action_doDelete')) { - $actions->unshift( - FormAction::create('doDelete', _t('ModelAdmin.DELETE', 'Delete')) - ->addExtraClass('ss-ui-action-destructive')->setAttribute('data-icon', 'delete') - ); - } - } - - $form = new Form($this, "EditForm", $fields, $actions, $validator); - $form->loadDataFrom($this->currentRecord); - $form->addExtraClass('cms-edit-form'); - - return $form; + static function set_page_length($length){ + self::$page_length = $length; } - - /** - * Postback action to save a record - * - * @param array $data - * @param Form $form - * @param SS_HTTPRequest $request - * @return mixed - */ - function doSave($data, $form, $request) { - $form->saveInto($this->currentRecord); - - try { - $this->currentRecord->write(); - } catch(ValidationException $e) { - $form->sessionMessage($e->getResult()->message(), 'bad'); - } - - - // Behaviour switched on . - if($this->parentController->isAjax()) { - return $this->edit($request); - } else { - $this->parentController->redirectBack(); - } - } /** - * Delete the current record + * Return the static page_length of the admin, default as 30 */ - public function doDelete($data, $form, $request) { - if($this->currentRecord->canDelete(Member::currentUser())) { - $this->currentRecord->delete(); - Director::redirect($this->parentController->Link('SearchForm?action=search')); - } else { - $this->parentController->redirectBack(); - } - return; - } + static function get_page_length(){ + return self::$page_length; + } - ///////////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Renders the record view template. - * - * @param SS_HTTPRequest $request - * @return mixed - */ - function view($request) { - if($this->currentRecord) { - $form = $this->ViewForm(); - return $form->forTemplate(); - } else { - return _t('ModelAdmin.ITEMNOTFOUND'); - } - } - - /** - * Returns a form for viewing the attached model - * - * @return Form - */ - public function ViewForm() { - $fields = $this->currentRecord->getCMSFields(); - $form = new Form($this, "EditForm", $fields, new FieldList()); - $form->loadDataFrom($this->currentRecord); - $form->makeReadonly(); - return $form; - } - - ///////////////////////////////////////////////////////////////////////////////////////////////////////// - - function index() { - Director::redirect(Controller::join_links($this->Link(), 'edit')); - } - - function getCurrentRecord(){ - return $this->currentRecord; - } - -} - +} \ No newline at end of file diff --git a/admin/css/screen.css b/admin/css/screen.css index 14a3437aa..5c05c8eff 100644 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -265,11 +265,11 @@ body.cms { overflow: hidden; } .cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-state-default a, .cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-widget-content .ui-state-default a, .cms-edit-form .cms-content-header-tabs .ui-tabs-nav .ui-widget-header .ui-state-default a { color: #1f1f1f; } /** -------------------------------------------- Tabs -------------------------------------------- */ -.ui-tabs .cms-content-header .ui-tabs-nav li, .cms-dialog .ui-tabs-nav li { margin: 0; } -.ui-tabs .cms-content-header .ui-tabs-nav li a, .cms-dialog .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; } -.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-default, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-default, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-dialog .ui-tabs-nav .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-default { background-color: #b0bec7; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzhjYTFhZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #8ca1ae)); background-image: -webkit-linear-gradient(#b0bec7, #8ca1ae); background-image: -moz-linear-gradient(#b0bec7, #8ca1ae); background-image: -o-linear-gradient(#b0bec7, #8ca1ae); background-image: -ms-linear-gradient(#b0bec7, #8ca1ae); background-image: linear-gradient(#b0bec7, #8ca1ae); border-right-color: #a6a6a6; border-left-color: #d9d9d9; border-bottom: none; text-shadow: white 0 1px 0; } -.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-active, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-dialog .ui-tabs-nav .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active { background: #eceff1; border-right-color: #a6a6a6; border-left-color: #a6a6a6; margin-right: -1px; margin-left: -1px; z-index: 2; } -.ui-tabs .cms-content-header .ui-tabs-nav .ui-state-active a, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active a, .ui-tabs .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active a { border-bottom: none; } +.cms-content-header .ui-tabs-nav li, .cms-dialog .ui-tabs-nav li { margin: 0; } +.cms-content-header .ui-tabs-nav li a, .cms-dialog .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; } +.cms-content-header .ui-tabs-nav .ui-state-default, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-default, .cms-dialog .ui-tabs-nav .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-default, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-default { background-color: #b0bec7; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #8ca1ae)); background-image: -webkit-linear-gradient(#b0bec7, #8ca1ae); background-image: -moz-linear-gradient(#b0bec7, #8ca1ae); background-image: -o-linear-gradient(#b0bec7, #8ca1ae); background-image: -ms-linear-gradient(#b0bec7, #8ca1ae); background-image: linear-gradient(#b0bec7, #8ca1ae); border-right-color: #a6a6a6; border-left-color: #d9d9d9; border-bottom: none; text-shadow: white 0 1px 0; } +.cms-content-header .ui-tabs-nav .ui-state-active, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active, .cms-dialog .ui-tabs-nav .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active { background: #eceff1; border-right-color: #a6a6a6; border-left-color: #a6a6a6; margin-right: -1px; margin-left: -1px; z-index: 2; } +.cms-content-header .ui-tabs-nav .ui-state-active a, .cms-content-header .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-content-header .ui-tabs-nav .ui-widget-header .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-content .ui-state-active a, .cms-dialog .ui-tabs-nav .ui-widget-header .ui-state-active a { border-bottom: none; } .CMSPagesController .cms-content-header-tabs .ui-tabs-nav li a { font-weight: bold; line-height: 16px; padding: 12px 20px 11px; text-indent: -9999em; } .CMSPagesController .cms-content-header-tabs .ui-tabs-nav li a.content-treeview { background: url(../images/content-header-tabs-sprite.png) no-repeat 2px 0px; } @@ -338,7 +338,7 @@ body.cms { overflow: hidden; } /* -------------------------------------------------------- Content Tools is the sidebar on the left of the main content panel */ .cms-content-tools { background-color: #dde3e7; width: 192px; border-right: 1px solid #bfcad2; overflow-y: auto; overflow-x: hidden; z-index: 70; -moz-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; -webkit-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; -o-box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; box-shadow: rgba(107, 120, 123, 0.5) 0 0 4px; float: left; position: relative; } -.cms-content-tools .cms-panel-header { margin: 0 0 7px; line-height: 24px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); } +.cms-content-tools .cms-panel-header { clear: both; margin: 0 0 7px; line-height: 24px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); } .cms-content-tools .cms-panel-content { width: 176px; padding: 8px 8px; overflow: auto; height: 100%; } .cms-content-tools .cms-content-header { background-color: #748d9d; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2IwYmVjNyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzc0OGQ5ZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); background-size: 100%; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b0bec7), color-stop(100%, #748d9d)); background-image: -webkit-linear-gradient(#b0bec7, #748d9d); background-image: -moz-linear-gradient(#b0bec7, #748d9d); background-image: -o-linear-gradient(#b0bec7, #748d9d); background-image: -ms-linear-gradient(#b0bec7, #748d9d); background-image: linear-gradient(#b0bec7, #748d9d); } .cms-content-tools .cms-content-header h2 { text-shadow: #5c7382 -1px -1px 0; width: 176px; color: white; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; o-text-overflow: ellipsis; } @@ -475,6 +475,9 @@ body.cms-dialog { overflow: auto; background: url("../images/textures/bg_cms_mai .htmleditorfield-mediaform .ss-htmleditorfield-file .overview .action-delete { display: inline-block; } .htmleditorfield-mediaform .ss-htmleditorfield-file .details { padding: 16px; } +/** -------------------------------------------- Search forms (used in AssetAdmin, ModelAdmin, etc) -------------------------------------------- */ +.cms-search-form { overflow: auto; margin-bottom: 16px; } + /** -------------------------------------------- Step labels -------------------------------------------- */ .step-label > * { display: inline-block; vertical-align: top; } .step-label .flyout { height: 18px; font-size: 14px; font-weight: bold; -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; -o-border-top-left-radius: 3px; -ms-border-top-left-radius: 3px; -khtml-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; -o-border-bottom-left-radius: 3px; -ms-border-bottom-left-radius: 3px; -khtml-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; background-color: #667980; padding: 4px 3px 4px 6px; text-align: center; text-shadow: none; color: #fff; } @@ -663,20 +666,5 @@ li.class-ErrorPage > a .jstree-pageicon { background-position: 0 -112px; } .cms-menu-list.collapsed li .text, .cms-menu-list.collapsed li .toggle-children { display: none; } .cms-menu-list.collapsed li > li { display: none; } -/** -------------------------------------------- ModelAdmin -------------------------------------------- */ -.ModelAdmin .ResultAssemblyBlock { display: none; } -.ModelAdmin .cms-content-tools h3.cms-panel-header { display: none; } -.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav { overflow: hidden; } -.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav li { max-width: 99%; } -.ModelAdmin .cms-content-tools #SearchForm_holder ul.ui-tabs-nav li a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 85%; } -.ModelAdmin .cms-content-tools #SearchForm_holder #ModelClassSelector { margin-bottom: 2px; } -.ModelAdmin .cms-content-tools #SearchForm_holder #ModelClassSelector select { width: 96%; } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab { border: 1px solid #aaa; margin-top: -1px; background: #fff; padding: 8px; } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab h3 { margin-top: 16px; margin-bottom: 10px; border-bottom: 1px solid rgba(201, 205, 206, 0.8); -webkit-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -moz-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); -o-box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); box-shadow: 0 1px 0 rgba(228, 230, 230, 0.8); } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form input { margin: 0px; } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .field { border-bottom: 0px; margin-bottom: 6px; } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .Actions { max-width: 100%; overflow: hidden; } -.ModelAdmin .cms-content-tools #SearchForm_holder div.tab form .Actions button.ss-ui-action-minor { display: none; } - .SecurityAdmin .cms-edit-form .cms-content-header h2 { display: none; } .SecurityAdmin .permissioncheckboxset .optionset li, .SecurityAdmin .permissioncheckboxsetfield_readonly .optionset li { float: none; width: auto; } diff --git a/admin/javascript/ModelAdmin.History.js b/admin/javascript/ModelAdmin.History.js deleted file mode 100644 index 177231b72..000000000 --- a/admin/javascript/ModelAdmin.History.js +++ /dev/null @@ -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( - '
' - + '< ' + ss.i18n._t('ModelAdmin.HISTORYBACK', 'back') + '' - + '' + ss.i18n._t('ModelAdmin.HISTORYFORWARD', 'forward') + ' >' - + '
' - ).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); \ No newline at end of file diff --git a/admin/javascript/ssui.core.js b/admin/javascript/ssui.core.js index 3585ad237..281d439f2 100644 --- a/admin/javascript/ssui.core.js +++ b/admin/javascript/ssui.core.js @@ -15,7 +15,9 @@ this.find('ul').addClass('ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'); this.find('li').addClass('ui-state-default ui-corner-top'); // TODO Figure out selected tab - this.find('li:first').selectIt(); + var selected = this.find('li.current'); + if(!selected.length) selected = this.find('li:first'); + selected.selectIt(); } }); diff --git a/admin/scss/_ModelAdmin.scss b/admin/scss/_ModelAdmin.scss index 8615afd8b..e69de29bb 100644 --- a/admin/scss/_ModelAdmin.scss +++ b/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%; - } - } - } - } - } - } -} \ No newline at end of file diff --git a/admin/scss/_style.scss b/admin/scss/_style.scss index 72c6cd884..7e72a5f89 100644 --- a/admin/scss/_style.scss +++ b/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; @@ -1237,6 +1238,14 @@ body.cms-dialog { } +/** -------------------------------------------- + * Search forms (used in AssetAdmin, ModelAdmin, etc) + * -------------------------------------------- */ +.cms-search-form { + overflow: auto; + margin-bottom: $grid-y*2; +} + /** -------------------------------------------- * Step labels * -------------------------------------------- */ diff --git a/admin/templates/Includes/ModelAdmin_Content.ss b/admin/templates/Includes/ModelAdmin_Content.ss index caac9092b..fa8e516b8 100644 --- a/admin/templates/Includes/ModelAdmin_Content.ss +++ b/admin/templates/Includes/ModelAdmin_Content.ss @@ -1,18 +1,30 @@
-

- <% if SectionTitle %> - $SectionTitle - <% else %> - <% _t('ModelAdmin.Title', 'Data Models') %> - <% end_if %> -

+
+

+ <% if SectionTitle %> + $SectionTitle + <% else %> + <% _t('ModelAdmin.Title', 'Data Models') %> + <% end_if %> +

+ +
+
    + <% control ManagedModelTabs %> +
  • + $Title +
  • + <% end_control %> +
+
+ +
- $Tools - -
+
+ $Tools $EditForm
diff --git a/admin/templates/Includes/ModelAdmin_Results.ss b/admin/templates/Includes/ModelAdmin_Results.ss deleted file mode 100644 index a1882ae64..000000000 --- a/admin/templates/Includes/ModelAdmin_Results.ss +++ /dev/null @@ -1,5 +0,0 @@ -<% if Results %> - $Form -<% else %> -

<% sprintf(_t('ModelAdmin.NORESULTS', 'No results'), $ModelPluralName) %>

-<% end_if %> \ No newline at end of file diff --git a/admin/templates/Includes/ModelAdmin_Tools.ss b/admin/templates/Includes/ModelAdmin_Tools.ss index a374438c8..3ea0341a3 100644 --- a/admin/templates/Includes/ModelAdmin_Tools.ss +++ b/admin/templates/Includes/ModelAdmin_Tools.ss @@ -1,33 +1,12 @@ -
+ \ No newline at end of file diff --git a/docs/en/changelogs/3.0.0.md b/docs/en/changelogs/3.0.0.md index 7c5f15155..31aa52cd9 100644 --- a/docs/en/changelogs/3.0.0.md +++ b/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 diff --git a/docs/en/reference/modeladmin.md b/docs/en/reference/modeladmin.md index 4e81868fc..62ce13aab 100644 --- a/docs/en/reference/modeladmin.md +++ b/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* - -## Usage - -### Step 1 -Extend ModelAdmin with a custom class for your admin area, and edit the `$managed_models` property with the list of -data objects you want to scaffold an interface for: - - :::php - class MyCatalogAdmin extends ModelAdmin { - - public static $managed_models = array( //since 2.3.2 - 'Product', - 'Category' - ); - - static $url_segment = 'products'; // will be linked as /admin/products - static $menu_title = 'My Product Admin'; - - } - - -To add the ModelAdmin to your CMS menu, you simply need to define a couple of statics on your ModelAdmin subclass. See -`[api:LeftAndMain]` on how to make your menu title translatable. - - -### Step 2 -Add a `$searchable_fields` (See `[api:ModelAdmin::$searchable_fields]`) property to your data -models, to define the fields and filters for the search interface: - -Datamodel `Product`: +Let's assume we want to manage a simple product listing as a sample data model: +A product can have a name, price, and a category. :::php class Product extends DataObject { - - static $db = array( - 'Name' => 'Varchar', - 'ProductCode' => 'Varchar', - 'Description' => 'Text', - 'Price' => 'Currency' - ); - - static $has_one = array( - 'Category' => 'Category' - ); - - static $searchable_fields = array( - 'Name', - 'ProductCode' - ); - + static $db = array('Name' => 'Varchar', 'ProductCode' => 'Varchar', 'Price' => 'Currency'); + static $has_one = array('Category' => 'Category'); + } + class Category extends DataObject { + static $db = array('Title' => 'Text'); + static $has_many = array('Products' => 'Product'); } - -Datamodel `Category`: +To create your own `ModelAdmin`, simply extend the base class, +and edit the `$managed_models` property with the list of +data objects you want to scaffold an interface for. +The class can manage multiple models in parallel, if required. +We'll name it `MyAdmin`, but the class name can be anything you want. :::php - 'Text' + class MyAdmin extends ModelAdmin { + public static $managed_models = array('Product','Category'); // Can manage multiple models + static $url_segment = 'products'; // Linked as /admin/products/ + static $menu_title = 'My Product Admin'; + } + +This will automatically add a new menu entry to the CMS, and you're ready to go! +Try opening http://localhost/admin/products/?flush=all. + +## Search Fields + +ModelAdmin uses the `[SearchContext](/reference/searchcontext)` class to provide +a search form, as well as get the searched results. Every DataObject can have its own context, +based on the fields which should be searchable. The class makes a guess at how those fields +should be searched, e.g. showing a checkbox for any boolean fields in your `$db` definition. + +To remove, add or modify searchable fields, define a new `[$searchable_fields](api:DataObject::$searchable_fields)` +static on your model class (see `[SearchContext](/reference/searchcontext)` docs for details). + + :::php + class Product extends DataObject { + // ... + static $searchable_fields = array( + 'Name', + 'ProductCode' + // leaves out the 'Price' field, removing it from the search ); } - ?> +For a more sophisticated customization, for example configuring the form fields +for the search form, override `[api:DataObject->getCustomSearchContext()]` on your model class. -### Step 3 -You can now log in to the main CMS admin and manage your data objects, with no extra implementation required. +## Result Columns -![](_images/modeladmin_edit.png) +The results are shown in a tabular listing, powered by the `[GridField](/topics/grid-field)`, +more specifically the `[api:GridFieldDataColumns]` component. +It looks for a `[api:DataObject::$summary_fields]` static on your model class, +where you can add or remove columns, or change their title. -![](_images/modeladmin_results.png) + :::php + class Product extends DataObject { + // ... + static $summary_fields = array( + 'Name' => 'Name', + 'Price' => 'Cost', // renames the column to "Cost" + // leaves out the 'ProductCode' field, removing the column + ); + } -### Note about has_one +## Results Customization -Scaffolding **has_one** relationships in your ModelAdmin relies on a column in the related model to be named **Title** -or **Name** of a string type (varchar, char, etc). These will be pulled in to the dropdown when creating a new object. +The results are retrieved from `[api:SearchContext->getResults()]`, +based on the parameters passed through the search form. +If no search parameters are given, the results will show every record. +Results are a `[api:DataList]` instance, so can be customized by additional +SQL filters, joins, etc (see [datamodel](/topics/datamodel) for more info). -If you are seeing a list of ID#s when creating new objects, ensure you have one of those two in the related model. +For example, we might want to exclude all products without prices in our sample `MyAdmin` implementation. -## Searchable Fields + :::php + class MyAdmin extends ModelAdmin { + // ... + function getList() { + $list = parent::getList(); + // Always limit by model class, in case you're managing multiple + if($this->modelClass == 'Product') { + $list->exclude('Price', '0'); + } + return $list; + } + } -You can customize the fields which are searchable for each managed DataObject class, as well as the ways in which the -fields are searched (e.g. "partial match", "fulltext", etc.) using `$searchable_fields`. +You can also customize the search behaviour directly on your `ModelAdmin` instance. +For example, we might want to have a checkbox which limits search results to expensive products (over $100). - * See `[api:DataObject]` + :::php + class MyAdmin extends ModelAdmin { + // ... + function getSearchContext() { + $context = parent::getSearchContext(); + if($this->modelClass == 'Product') { + $context->getFields()->push(new CheckboxField('q[ExpensiveOnly]', 'Only expensive stuff')); + } + return $context; + } + function getList() { + $list = parent::getList(); + $params = $this->request->requestVar('q'); // use this to access search parameters + if($this->modelClass == 'Product' && isset($params['ExpensiveOnly']) && $params['ExpensiveOnly']) { + $list->exclude('Price:LessThan', '100'); + } + return $list; + } + } -![](_images/modeladmin_search.png) +## Managing Relationships -## Summary Fields +Has-one relationships are simply implemented as a `[api:DropdownField]` by default. +Consider replacing it with a more powerful interface in case you have many records +(through customizing `[api:DataObject->getCMSFields]`). -Summary Fields are the columns which are shown in the `[api:TableListField]` when viewing DataObjects. These can be -customized for each `[api:DataObject]`'s search results using `$summary_fields`. +Has-many and many-many relationships are usually handled via the `[GridField](/topics/grid-field)` class, +more specifically the `[api:GridFieldAddExistingAutocompleter]` and `[api:GridFieldRelationDelete]` components. +They provide a list/detail interface within a single record edited in your ModelAdmin. - * See `[api:DataObject]` +## Permissions + +`ModelAdmin` respects the permissions set on the model, through methods on your `DataObject` implementations: +`canView()`, `canEdit()`, `canDelete()`, and `canCreate`. + +In terms of access control to the interface itself, every `ModelAdmin` subclass +creates its own "[permission code](/reference/permissions)", which can be assigned +to groups through the `admin/security` management interface. To further limit +permission, either override checks in `ModelAdmin->init()`, or define +more permission codes through the `ModelAdmin::$required_permission_codes` static. + +## Data Import + +The `ModelAdmin` class provides import of CSV files through the `[api:CsvBulkLoader]` API. +which has support for column mapping, updating existing records, +and identifying relationships - so its a powerful tool to get your data into a SilverStripe database. + +By default, each model management interface allows uploading a CSV file +with all columns autodetected. To override with a more specific importer implementation, +use the `[api:ModelAdmin::$model_importers] static. + +## Data Export + +Export is also available, although at the moment only to the CSV format, +through a button at the end of a results list. You can also export search results. +It is handled through the `[api:GridFieldExportButton]` component. + +To customize the exported columns, override the edit form implementation in your `ModelAdmin`: + + :::php + class MyAdmin extends ModelAdmin { + // ... + function getEditForm($id = null) { + $form = parent::getEditForm($id); + if($this->modelClass == 'Product') { + $grid = $form->Fields()->fieldByName('Product'); + $grid->getConfig()->getComponentByType('GridFieldExporter') + ->setExportColumns(array( + // Excludes 'ProductCode' from the export + 'Name' => 'Name', + 'Price' => 'Price' + )); + } + return $form; + } + } + +## Extending existing ModelAdmins + +Sometimes you'll work with ModelAdmins from other modules, e.g. the product management +of an ecommerce module. To customize this, you can always subclass. But there's +also another tool at your disposal: The `[api:Extension]` API. + + :::php + class MyAdminExtension extends Extension { + function updateEditForm(&$form) { + $form->Fields()->push(/* ... */) + } + } + + // mysite/_config.php + Object::add_extension('MyAdmin', 'MyAdminExtension'); + +The following extension points are available: `updateEditForm()`, `updateSearchContext()`, +`updateSearchForm()`, `updateList()`, `updateImportForm`. + +## Customizing the interface + +Interfaces like `ModelAdmin` can be customized in many ways: + + * JavaScript behaviour (e.g. overwritten jQuery.entwine rules) + * CSS styles + * HTML markup through templates + +In general, use your `ModelAdmin->init()` method to add additional requirements +through the `[Requirements](/reference/requirements)` API. +For an introduction how to customize the CMS templates, see our [CMS Architecture Guide](/reference/cms-architecture). ## Related diff --git a/docs/en/reference/searchcontext.md b/docs/en/reference/searchcontext.md index 53abd480f..afd6212c1 100644 --- a/docs/en/reference/searchcontext.md +++ b/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 diff --git a/forms/FormScaffolder.php b/forms/FormScaffolder.php index 49b4d7560..7c8424b66 100644 --- a/forms/FormScaffolder.php +++ b/forms/FormScaffolder.php @@ -123,22 +123,17 @@ class FormScaffolder extends Object { $this->obj->fieldLabel($relationship) ); } - $relationshipFields = singleton($component)->summaryFields(); - - $foreignKey = $this->obj->getRemoteJoinField($relationship); - $fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField'; - $ctf = new $fieldClass( - $this, + $fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'GridField'; + $grid = Object::create($fieldClass, $relationship, - null, - $relationshipFields, - "getCMSFields" + $this->obj->fieldLabel($relationship), + $this->obj->$relationship(), + GridFieldConfig_RelationEditor::create() ); - $ctf->setPermissions(TableListField::permissions_for_object($component)); if($this->tabbed) { - $fields->addFieldToTab("Root.$relationship", $ctf); + $fields->addFieldToTab("Root.$relationship", $grid); } else { - $fields->push($ctf); + $fields->push($grid); } } } @@ -152,22 +147,17 @@ class FormScaffolder extends Object { ); } - $relationshipFields = singleton($component)->summaryFields(); - $fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'ComplexTableField'; - $ctf = new $fieldClass( - $this, + $fieldClass = (isset($this->fieldClasses[$relationship])) ? $this->fieldClasses[$relationship] : 'GridField'; + $grid = Object::create($fieldClass, $relationship, + $this->obj->fieldLabel($relationship), $this->obj->$relationship(), - $relationshipFields, - "getCMSFields" + GridFieldConfig_RelationEditor::create() ); - - $ctf->setPermissions(TableListField::permissions_for_object($component)); - $ctf->popupClass = "ScaffoldingComplexTableField_Popup"; if($this->tabbed) { - $fields->addFieldToTab("Root.$relationship", $ctf); + $fields->addFieldToTab("Root.$relationship", $grid); } else { - $fields->push($ctf); + $fields->push($grid); } } } diff --git a/forms/ScaffoldingComplexTableField.php b/forms/ScaffoldingComplexTableField.php deleted file mode 100644 index 67aa32318..000000000 --- a/forms/ScaffoldingComplexTableField.php +++ /dev/null @@ -1,93 +0,0 @@ -dataObject = $dataObject; - - Requirements::clear(); - - $actions = new FieldList(); - if(!$readonly) { - $actions->push( - $saveAction = new FormAction("saveComplexTableField", "Save") - ); - $saveAction->addExtraClass('save'); - } - - $fields->push(new HiddenField("ComplexTableField_Path", Director::absoluteBaseURL())); - - parent::__construct($controller, $name, $fields, $validator, $readonly, $dataObject); - } - - /** - * Handle a generic action passed in by the URL mapping. - * - * @param SS_HTTPRequest $request - */ - public function handleAction($request) { - $action = str_replace("-","_",$request->param('Action')); - if(!$this->action) $this->action = 'index'; - - if($this->checkAccessAction($action)) { - if($this->hasMethod($action)) { - $result = $this->$action($request); - - // Method returns an array, that is used to customise the object before rendering with a template - if(is_array($result)) { - return $this->getViewer($action)->process($this->customise($result)); - - // Method returns a string / object, in which case we just return that - } else { - return $result; - } - - // There is no method, in which case we just render this object using a (possibly alternate) template - } else { - return $this->getViewer($action)->process($this); - } - } else { - return $this->httpError(403, "Action '$action' isn't allowed on class $this->class"); - } - } - - /** - * Action to render results for an autocomplete filter. - * - * @param SS_HTTPRequest $request - * @return void - */ - function filter($request) { - //$model = singleton($this->modelClass); - $context = $this->dataObject->getDefaultSearchContext(); - $value = $request->getVar('q'); - $results = $context->getResults(array("Name"=>$value)); - header("Content-Type: text/plain"); - foreach($results as $result) { - echo $result->Name . "\n"; - } - } - - /** - * Action to populate edit box with a single data object via Ajax query - */ - function record($request) { - $type = $request->getVar('type'); - $value = $request->getVar('value'); - if ($type && $value) { - $record = DataObject::get_one($this->dataObject->class, "\"$type\" = '$value'"); - header("Content-Type: text/plain"); - echo json_encode(array("record"=>$record->toMap())); - } - } - -} diff --git a/javascript/ScaffoldComplexTableField.js b/javascript/ScaffoldComplexTableField.js deleted file mode 100644 index 81ba45bf7..000000000 --- a/javascript/ScaffoldComplexTableField.js +++ /dev/null @@ -1,24 +0,0 @@ -window.onload = function() { - - resourcePath = jQuery('form').attr('action'); - - jQuery("fieldset input:first").attr('autocomplete', 'off').autocomplete({list: ["mark rickerby", "maxwell sparks"]}); - - jQuery("fieldset input:first").bind('activate.autocomplete', function(e){ - - type = jQuery("fieldset input:first").attr('name'); - value = jQuery("fieldset input:first").val(); - - jQuery.getJSON(resourcePath + '/record', {'type':type, 'value':value}, function(data) { - jQuery('form input').each(function(i, elm){ - if(elm.name in data.record) { - val = data.record[elm.name]; - if (val != null) elm.setAttribute('value', val); - } - }); - }); - - }); - - -}; \ No newline at end of file diff --git a/model/fieldtypes/ForeignKey.php b/model/fieldtypes/ForeignKey.php index e298242e3..611464ca0 100644 --- a/model/fieldtypes/ForeignKey.php +++ b/model/fieldtypes/ForeignKey.php @@ -37,8 +37,15 @@ class ForeignKey extends Int { $field = new UploadField($relationName, $title); } else { $titleField = (singleton($hasOneClass)->hasField('Title')) ? "Title" : "Name"; - $map = DataList::create($hasOneClass)->map("ID", $titleField); - $field = new DropdownField($this->name, $title, $map, null, null, ' '); + $list = DataList::create($hasOneClass); + // Don't scaffold a dropdown for large tables, as making the list concrete + // might exceed the available PHP memory in creating too many DataObject instances + if($list->count() < 100) { + $field = new DropdownField($this->name, $title, $list->map("ID", $titleField), null, null, ' '); + } else { + $field = new NumericField($this->name, $title); + } + } return $field;