Merge remote-tracking branch 'origin/3.0'

Conflicts:
	.travis.yml
This commit is contained in:
Ingo Schommer 2012-09-07 17:21:41 +02:00
commit 1088d044c5
55 changed files with 1141 additions and 533 deletions

View File

@ -27,3 +27,7 @@ branches:
- translation-staging
- 2.4
notifications:
irc:
channels:
- "irc.freenode.org#silverstripe"

View File

@ -37,6 +37,11 @@ class LeftAndMain extends Controller implements PermissionProvider {
*/
static $menu_title;
/**
* @var string
*/
static $menu_icon;
/**
* @var int
*/
@ -93,6 +98,14 @@ class LeftAndMain extends Controller implements PermissionProvider {
*/
static $required_permission_codes;
/**
* @var String Namespace for session info, e.g. current record.
* Defaults to the current class name, but can be amended to share a namespace in case
* controllers are logically bundled together, and mainly separated
* to achieve more flexible templating.
*/
static $session_namespace;
/**
* Register additional requirements through the {@link Requirements} class.
* Used mainly to work around the missing "lazy loading" functionality
@ -289,6 +302,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js',
FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js',
),
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', true, true),
Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', true, true)
@ -432,6 +446,22 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $title;
}
/**
* Return styling for the menu icon, if a custom icon is set for this class
*
* Example: static $menu-icon = '/path/to/image/';
* @param type $class
* @return string
*/
static function menu_icon_for_class($class) {
$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
if (!empty($icon)) {
$class = strtolower($class);
return ".icon.icon-16.icon-{$class} { background: url('{$icon}'); } ";
}
return '';
}
public function show($request) {
// TODO Necessary for TableListField URLs to work properly
if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
@ -485,6 +515,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
// Encode into DO set
$menu = new ArrayList();
$menuItems = CMSMenu::get_viewable_menu_items();
// extra styling for custom menu-icons
$menuIconStyling = '';
if($menuItems) {
foreach($menuItems as $code => $menuItem) {
// alternate permission checks (in addition to LeftAndMain->canView())
@ -526,6 +560,14 @@ class LeftAndMain extends Controller implements PermissionProvider {
$title = $menuItem->title;
}
// Provide styling for custom $menu-icon. Done here instead of in
// CMSMenu::populate_menu(), because the icon is part of
// the CMS right pane for the specified class as well...
if($menuItem->controller) {
$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
}
$menu->push(new ArrayData(array(
"MenuItem" => $menuItem,
"Title" => Convert::raw2xml($title),
@ -535,6 +577,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
)));
}
}
if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
$this->_cache_MainMenu = $menu;
}
@ -1217,8 +1260,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $this->request->requestVar('ID');
} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
return $this->urlParams['ID'];
} elseif(Session::get("{$this->class}.currentPage")) {
return Session::get("{$this->class}.currentPage");
} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
return Session::get($this->sessionNamespace() . ".currentPage");
} else {
return null;
}
@ -1233,7 +1276,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
* @param int $id
*/
public function setCurrentPageID($id) {
Session::set("{$this->class}.currentPage", $id);
Session::set($this->sessionNamespace() . ".currentPage", $id);
}
/**
@ -1256,6 +1299,14 @@ class LeftAndMain extends Controller implements PermissionProvider {
return ($record->ID == $this->currentPageID());
}
/**
* @return String
*/
protected function sessionNamespace() {
$override = $this->stat('session_namespace');
return $override ? $override : $this->class;
}
/**
* URL to a previewable record which is shown through this controller.
* The controller might not have any previewable content, in which case

View File

@ -67,10 +67,7 @@
if(this.is('.is-collapsed')) return;
// var url = ui.xmlhttp.getResponseHeader('x-frontend-url');
var url = $('.cms-edit-form')
.find(':input[name=PreviewURL],:input[name=StageLink],:input[name=LiveLink]')
.filter(function() {return $(this).val() !== '';})
.val();
var url = $('.cms-edit-form').choosePreviewLink();
if(url) {
this.loadUrl(url);
this.unblock();
@ -298,10 +295,8 @@
e.preventDefault();
var preview = $('.cms-preview'),
url = $('.cms-edit-form')
.find(':input[name=PreviewURL],:input[name=StageLink],:input[name=LiveLink]')
.filter(function() {return $(this).val() !== '';})
.val();
url = $('.cms-edit-form').choosePreviewLink();
if(url) {
preview.loadUrl(url);
preview.unblock();
@ -309,5 +304,23 @@
}
}
});
$('.cms-edit-form').entwine({
/**
* Choose applicable preview link based on form data,
* in a fixed order of priority: The PreviewURL field is used as an override,
* which falls back to stage or live URLs.
*
* @return String Absolute URL
*/
choosePreviewLink: function() {
var self = this, urls = $.map(['PreviewURL', 'StageLink', 'LiveLink'], function(name) {
var val = self.find(':input[name=' + name + ']').val();
return val ? val : null;
});
return urls ? urls[0] : false;
}
});
});
}(jQuery));

View File

@ -86,7 +86,7 @@
.bind('move_node.jstree', function(e, data) {
if(self.getIsUpdatingTree()) return;
var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode);
var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode), newParentID = $(newParentNode).data('id') || 0, nodeID = $(movedNode).data('id');
var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
return $(el).data('id');
});
@ -94,10 +94,14 @@
$.ajax({
'url': self.data('urlSavetreenode'),
'data': {
ID: $(movedNode).data('id'),
ParentID: $(newParentNode).data('id') || 0,
ID: nodeID,
ParentID: newParentID,
SiblingIDs: siblingIDs
},
success: function() {
$('.cms-edit-form :input[name=ParentID]').val(newParentID);
self.updateNodesFromServer([nodeID]);
},
statusCode: {
403: function() {
$.jstree.rollback(data.rlbk);

View File

@ -0,0 +1,17 @@
(function($) {
$.entwine('ss', function($){
// Any TreeDowndownField needs to refresh it's contents after a form submission,
// because the tree on the backend might have changed
$('.TreeDropdownField').entwine({
'from .cms-container form': {
onaftersubmitform: function(e){
this.find('.tree-holder').empty();
this._super();
}
}
});
});
})(jQuery);

View File

@ -33,7 +33,11 @@ jQuery.noConflict();
el.siblings('.chzn-container').prop('title', title);
}
} else {
setTimeout(function() { applyChosen(el); }, 500);
setTimeout(function() {
// Make sure it's visible before applying the ui
el.show();
applyChosen(el); },
500);
}
};

15
cache/Cache.php vendored
View File

@ -76,6 +76,7 @@ class SS_Cache {
$cachedir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache';
if (!is_dir($cachedir)) mkdir($cachedir);
self::$backends['default'] = array('File', array('cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache'));
self::$cache_lifetime['default'] = array('lifetime' => 600, 'priority' => 1);
}
}
@ -110,6 +111,14 @@ class SS_Cache {
if ($priority >= $current) self::$backend_picks[$for] = array('name' => $name, 'priority' => $priority);
}
/**
* Return the cache lifetime for a particular named cache.
* @return array
*/
static function get_cache_lifetime($for) {
return (isset(self::$cache_lifetime[$for])) ? self::$cache_lifetime[$for] : false;
}
/**
* Set the cache lifetime for a particular named cache
*
@ -169,8 +178,10 @@ class SS_Cache {
static function factory($for, $frontend='Output', $frontendOptions=null) {
self::init();
$backend_name = 'default'; $backend_priority = -1;
$cache_lifetime = 600; $lifetime_priority = -1;
$backend_name = 'default';
$backend_priority = -1;
$cache_lifetime = self::$cache_lifetime['default']['lifetime'];
$lifetime_priority = -1;
foreach (array('any', $for) as $name) {
if (isset(self::$backend_picks[$name]) && self::$backend_picks[$name]['priority'] > $backend_priority) {

View File

@ -159,12 +159,13 @@ class Director implements TemplateGlobalProvider {
* @param string $body The HTTP body
* @param array $headers HTTP headers with key-value pairs
* @param array $cookies to populate $_COOKIE
* @param HTTP_Request $request The {@see HTTP_Request} object generated as a part of this request
* @return SS_HTTPResponse
*
* @uses getControllerForURL() The rule-lookup logic is handled by this.
* @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call.
*/
static function test($url, $postVars = null, $session = null, $httpMethod = null, $body = null, $headers = null, $cookies = null) {
static function test($url, $postVars = null, $session = null, $httpMethod = null, $body = null, $headers = null, $cookies = null, &$request = null) {
// These are needed so that calling Director::test() doesnt muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
$oldStage = Versioned::current_stage();
@ -209,10 +210,10 @@ class Director implements TemplateGlobalProvider {
$_COOKIE = (array) $cookies;
$_SERVER['REQUEST_URI'] = Director::baseURL() . $urlWithQuerystring;
$req = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v);
$request = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body);
if($headers) foreach($headers as $k => $v) $request->addHeader($k, $v);
// TODO: Pass in the DataModel
$result = Director::handleRequest($req, $session, DataModel::inst());
$result = Director::handleRequest($request, $session, DataModel::inst());
// Restore the superglobals
$_REQUEST = $existingRequestVars;
@ -249,6 +250,7 @@ class Director implements TemplateGlobalProvider {
}
if(($arguments = $request->match($pattern, true)) !== false) {
$request->setRouteParams($controllerOptions);
// controllerOptions provide some default arguments
$arguments = array_merge($controllerOptions, $arguments);
@ -286,20 +288,20 @@ class Director implements TemplateGlobalProvider {
/**
* Returns the urlParam with the given name
*
* @deprecated 3.0 Use SS_HTTPRequest->latestParam()
* @deprecated 3.0 Use SS_HTTPRequest->param()
*/
static function urlParam($name) {
Deprecation::notice('3.0', 'Use SS_HTTPRequest->latestParam() instead.');
Deprecation::notice('3.0', 'Use SS_HTTPRequest->param() instead.');
if(isset(Director::$urlParams[$name])) return Director::$urlParams[$name];
}
/**
* Returns an array of urlParams.
*
* @deprecated 3.0 Use SS_HTTPRequest->latestParams()
* @deprecated 3.0 Use SS_HTTPRequest->params()
*/
static function urlParams() {
Deprecation::notice('3.0', 'Use SS_HTTPRequest->latestParams() instead.');
Deprecation::notice('3.0', 'Use SS_HTTPRequest->params() instead.');
return Director::$urlParams;
}

View File

@ -82,6 +82,21 @@ class SS_HTTPRequest implements ArrayAccess {
*/
protected $latestParams = array();
/**
* @var array $routeParams Contains an associative array of all arguments
* explicitly set in the route table for the current request.
* Useful for passing generic arguments via custom routes.
*
* E.g. The "Locale" parameter would be assigned "en_NZ" below
*
* Director:
* rules:
* 'en_NZ/$URLSegment!//$Action/$ID/$OtherID':
* Controller: 'ModelAsController'
* Locale: 'en_NZ'
*/
protected $routeParams = array();
protected $unshiftedButParsedParts = 0;
/**
@ -364,7 +379,6 @@ class SS_HTTPRequest implements ArrayAccess {
$shiftCount = sizeof($patternParts);
}
$matched = true;
$arguments = array();
foreach($patternParts as $i => $part) {
$part = trim($part);
@ -447,22 +461,35 @@ class SS_HTTPRequest implements ArrayAccess {
function latestParams() {
return $this->latestParams;
}
function latestParam($name) {
if(isset($this->latestParams[$name]))
return $this->latestParams[$name];
else
return null;
if(isset($this->latestParams[$name])) return $this->latestParams[$name];
else return null;
}
function routeParams() {
return $this->routeParams;
}
function setRouteParams($params) {
$this->routeParams = $params;
}
function params()
{
return array_merge($this->allParams, $this->routeParams);
}
/**
* Finds a named URL parameter (denoted by "$"-prefix in $url_handlers)
* from the full URL.
* from the full URL, or a parameter specified in the route table
*
* @param string $name
* @return string Value of the URL parameter (if found)
*/
function param($name) {
if(isset($this->allParams[$name])) return $this->allParams[$name];
$params = $this->params();
if(isset($params[$name])) return $params[$name];
else return null;
}

View File

@ -58,8 +58,10 @@ class ClassInfo {
* Returns the manifest of all classes which are present in the database.
* @param string $class Class name to check enum values for ClassName field
*/
static function getValidSubClasses($class = 'SiteTree') {
return DB::getConn()->enumValuesForField($class, 'ClassName');
static function getValidSubClasses($class = 'SiteTree', $includeUnbacked = false) {
$classes = DB::getConn()->enumValuesForField($class, 'ClassName');
if (!$includeUnbacked) $classes = array_filter($classes, array('ClassInfo', 'exists'));
return $classes;
}
/**

View File

@ -52,6 +52,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setPaginationGetVar($var) {
$this->getVar = $var;
return $this;
}
/**
@ -70,6 +71,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setPageLength($length) {
$this->pageLength = $length;
return $this;
}
/**
@ -79,6 +81,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setCurrentPage($page) {
$this->pageStart = ($page - 1) * $this->pageLength;
return $this;
}
/**
@ -106,6 +109,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setPageStart($start) {
$this->pageStart = $start;
return $this;
}
/**
@ -129,6 +133,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setTotalItems($items) {
$this->totalItems = $items;
return $this;
}
/**
@ -143,6 +148,7 @@ class PaginatedList extends SS_ListDecorator {
$this->setPageStart($limit['start']);
$this->setTotalItems($query->unlimitedRowCount());
}
return $this;
}
/**
@ -165,6 +171,7 @@ class PaginatedList extends SS_ListDecorator {
*/
public function setLimitItems($limit) {
$this->limitItems = (bool) $limit;
return $this;
}
/**
@ -432,6 +439,7 @@ class PaginatedList extends SS_ListDecorator {
$this->setPageStart($pageStart);
$this->setPageLength($pageLength);
$this->setTotalSize($totalSize);
return $this;
}
}

View File

@ -10,7 +10,10 @@ body { background-color: #eee; margin: 0; overflow-x: hidden; padding: 0; font-f
.header { margin: 0; border-bottom: 6px solid #ccdef3; height: 23px; background-color: #666673; padding: 4px 0 2px 6px; }
.trace, .build, .options { padding: 6px 12px; }
.trace .test-case { font-size: 1.1em; }
.trace li, .build li, .options li { font-size: 14px; margin: 6px 0; }
.trace .failure { margin: 1em 0 2em; }
.trace .failure pre { color: #C80700; background: #FFE9E9; border-color: #FDC4C1; }
a { color: #666; }
a:hover { color: #222; }
@ -18,7 +21,7 @@ a:active { color: #111; }
p { margin-bottom: 6px; }
pre { margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #eee; border: 1px solid rgba(0, 0, 0, 0.08); color: #333; padding: 11px; overflow: auto; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); }
pre { margin: 1em 0; background-color: #f5f5f5; border: 1px solid #eee; border: 1px solid rgba(0, 0, 0, 0.1); color: #333; padding: 10px 15px; overflow: auto; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); }
pre span { color: #999; }
pre .error { color: #f00; }
@ -28,6 +31,12 @@ h3 { margin: 0 0 6px 0; color: #333; font-size: 18px; line-height: 24px; }
ul { margin: 0 0 18px 0; padding: 0 0 0 18px; }
.pass { margin-top: 18px; padding: 2px 20px 2px 40px; color: #006600; background: #E2F9E3; border: 1px solid #8DD38D; border-radius: 4px; }
.status { margin-top: 18px; padding: 0; color: #333; background: #EEEEEE; border: 1px solid #CCCCCC; border-radius: 4px; }
.status h2 { margin: 10px 15px; }
.pass { color: #006600; background: #E2F9E3; border-color: #92f192; }
.fail { color: #C80700; background: #FFE9E9; border-color: #FDC4C1; }
.fail { margin-top: 18px; padding: 2px 20px 2px 40px; color: #C80700; background: #FFE9E9; border: 1px solid #C80700; border-radius: 4px; }
.message { background-color: white; color: #333; margin: 0.5em; padding: 0.5em 0.8em 0.4em; border: 1px #CCC solid; border-radius: 4px; }
.message.warning { color: #fc7330; background: #fcf5ed; border-color: #fdd0a0; }
.total-time { padding: 0 15px; margin-bottom: 1em; }

View File

@ -159,7 +159,7 @@ class Debug {
if($showHeader) echo "Debug (line $caller[line] of $file):\n ";
echo $message . "\n";
} else {
echo "<p style=\"background-color: white; color: black; width: 95%; margin: 0.5em; padding: 0.3em; border: 1px #CCC solid\">\n";
echo "<p class=\"message warning\">\n";
if($showHeader) echo "<b>Debug (line $caller[line] of $file):</b>\n ";
echo Convert::raw2xml($message) . "</p>\n";
}

View File

@ -78,7 +78,7 @@ class DebugView extends Object {
$pathLinks[] = "<a href=\"$base$pathPart\">$part</a>";
}
}
return implode('&rarr;&nbsp;', $pathLinks);
return implode('&nbsp;&rarr;&nbsp;', $pathLinks);
}
/**

View File

@ -425,7 +425,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
$fixtureContent = $parser->load(Director::baseFolder().'/'.$fixtureFile);
$fixture = new YamlFixture($fixtureFile);
$fixture->saveIntoDatabase();
$fixture->saveIntoDatabase($this->model);
$this->fixtures[] = $fixture;
}

View File

@ -293,8 +293,8 @@ class SapphireTestReporter implements PHPUnit_Framework_TestListener {
}
if ($test['status'] != 1) {
echo "<div class=\"failure\"><span>&otimes; ". $this->testNameToPhrase($test['name']) ."</span><br>";
echo "<pre>".htmlentities($test['message'], ENT_COMPAT, 'UTF-8')."</pre><br>";
echo "<div class=\"failure\"><h2 class=\"test-case\">&otimes; ". $this->testNameToPhrase($test['name']) ."</h2>";
echo "<pre>".htmlentities($test['message'], ENT_COMPAT, 'UTF-8')."</pre>";
echo SS_Backtrace::get_rendered_backtrace($test['trace']);
echo "</div>";
}

View File

@ -324,7 +324,7 @@ class TestRunner extends Controller {
$endTime = microtime(true);
if(Director::is_cli()) echo "\n\nTotal time: " . round($endTime-$startTime,3) . " seconds\n";
else echo "<p>Total time: " . round($endTime-$startTime,3) . " seconds</p>\n";
else echo "<p class=\"total-time\">Total time: " . round($endTime-$startTime,3) . " seconds</p>\n";
if(!Director::is_cli()) echo '</div>';

View File

@ -0,0 +1,230 @@
# 3.0.2-rc1 #
## Overview ##
3.0.2 RC1 provides a number of bugfixes and minor enhancements, continuing to build on the 3.0.1 release, with a particularly focus on CMS UI consistency.
Upgrading from 3.0.x should be a straightforward matter of dropping in the new release, however, please note the API changes in case you relied on the old behaviour. The changes to the APIs wont' affect most users.
### API Changes
* 2012-08-27 [c2a8eec](https://github.com/silverstripe/sapphire/commit/c2a8eec) Changed behaviour of HTTP_Request::params to include route table params (as per 2.4 behaviour, see FIX: below). ADDED: HTTP_Request::params() to retrieve all (shifted) params used in the request FIXED: Issue where route-table level arguments would not be accessible without using non-deprecated API. ADDED: Test case to test the above items UPDATED: Extended Director::test to allow for the retrieval of the request object UPDATED: Deprecated notice on Director::urlParam and Director::urlParams REMOVED: Unused variable FIXED: Coding convention conformity (Damian Mooyman)
* 2012-08-23 [fa37c44](https://github.com/silverstripe/sapphire/commit/fa37c44) Reverse config extra statics control flow (Hamish Friedlander)
* 2012-08-19 [70b22fa](https://github.com/silverstripe/sapphire/commit/70b22fa) GridFieldConfig should extend object to make use of Object::create() this also fixes GridFieldConfig_RecordViewer::create() which was not working before (Zauberfisch)
* 2012-07-23 [c2414aa](https://github.com/silverstripe/sapphire/commit/c2414aa) Debug::showError() no longer calls exit() (fixes #2644) (jakr)
* 2012-07-16 [70eaa27](https://github.com/silverstripe/sapphire/commit/70eaa27) Allow to force URL reload, while replacing the history state (Mateusz Uzdowski)
### Features and Enhancements
* 2012-08-30 [19772f3](https://github.com/silverstripe/sapphire/commit/19772f3) Updates to the GridField documentation (fixes #7524) (Stig Lindqvist)
* 2012-08-29 [cc2e250](https://github.com/silverstripe/sapphire/commit/cc2e250) Allow querying if a field exists on a table (Hamish Friedlander)
* 2012-08-29 [949507c](https://github.com/silverstripe/silverstripe-cms/commit/949507c) Add warning if viewed SiteTree object class is obsolete (Hamish Friedlander)
* 2012-08-27 [2e21574](https://github.com/silverstripe/sapphire/commit/2e21574) FieldGroup_DefaultFieldHolder template (Ingo Schommer)
* 2012-08-27 [1d2288b](https://github.com/silverstripe/sapphire/commit/1d2288b) Open external links in preview mode in new window (fixes #7652) (Ingo Schommer)
* 2012-08-27 [cd8c3a0](https://github.com/silverstripe/silverstripe-cms/commit/cd8c3a0) Re-added SilverStripeNavigator styling (Ingo Schommer)
* 2012-08-27 [6009cfa](https://github.com/silverstripe/sapphire/commit/6009cfa) Allow debugging of config cyclic errors (Hamish Friedlander)
* 2012-08-21 [66dfa38](https://github.com/silverstripe/sapphire/commit/66dfa38) GreaterThanFilter should be consistent with LessThanFilter (unclecheese)
* 2012-08-19 [82500dd](https://github.com/silverstripe/sapphire/commit/82500dd) Custom menu icons for the CMS main menu (martimiz)
* 2012-08-17 [4fde42f](https://github.com/silverstripe/sapphire/commit/4fde42f) Add "jpeg" to list of allowed extensions (unclecheese)
* 2012-08-07 [1432a8e](https://github.com/silverstripe/sapphire/commit/1432a8e) create TestRunner setdb URL endpoint (Michał Ochman)
* 2012-07-26 [c97ed78](https://github.com/silverstripe/silverstripe-cms/commit/c97ed78) Maori Language javascript (Naomi Guyer)
* 2012-07-26 [55ec92d](https://github.com/silverstripe/sapphire/commit/55ec92d) Maori language javascript (Naomi Guyer)
* 2012-07-20 [0c0bcc9](https://github.com/silverstripe/sapphire/commit/0c0bcc9) Rewritten tutorial 5 to GridField API (Ingo Schommer)
* 2012-07-20 [11c71e1](https://github.com/silverstripe/sapphire/commit/11c71e1) Updated tutorial 4 (Naomi Guyer)
* 2012-07-06 [766b03f](https://github.com/silverstripe/sapphire/commit/766b03f) add selectsession URL endpoint (Michał Ochman)
* 2012-06-25 [5f94d23](https://github.com/silverstripe/sapphire/commit/5f94d23) Ntfcatn. image embedding(trac #7438) (mightycoco)
* 2012-06-11 [21bcc01](https://github.com/silverstripe/silverstripe-cms/commit/21bcc01) Made the tree search form more extensible. (Andrew Short)
### Bugfixes
* 2012-09-02 [fd8e852](https://github.com/silverstripe/silverstripe-cms/commit/fd8e852) Disallow "add page here" shortcut to avoid $allowed_children edge cases (fixes #7694) (Ingo Schommer)
* 2012-09-02 [1cd82e2](https://github.com/silverstripe/silverstripe-cms/commit/1cd82e2) Enforce $allowed_children in controllers on page creation (fixes #7694) (Ingo Schommer)
* 2012-09-02 [52263e6](https://github.com/silverstripe/sapphire/commit/52263e6) Gridfield fails when save changes filter criteria (fixes #7785) (Ingo Schommer)
* 2012-09-02 [fb6efb9](https://github.com/silverstripe/sapphire/commit/fb6efb9) Calling extraStatics() with args (regression from fa37c448) (Ingo Schommer)
* 2012-09-01 [d24ea5e](https://github.com/silverstripe/sapphire/commit/d24ea5e) jQueryUI configs broken because keys were all lowercase (Zauberfisch)
* 2012-08-30 [e540166](https://github.com/silverstripe/silverstripe-cms/commit/e540166) Filter pages by LastEdited always returns an empty list (Saophalkun Ponlu)
* 2012-08-30 [f3fcae3](https://github.com/silverstripe/sapphire/commit/f3fcae3) Fix wrong date conversion from PHP format 'y' to jquery date and back. (Saophalkun Ponlu)
* 2012-08-29 [651cb03](https://github.com/silverstripe/silverstripe-cms/commit/651cb03) Removed 'Sort' field from CMSMain edit form (Ingo Schommer)
* 2012-08-29 [f070f97](https://github.com/silverstripe/sapphire/commit/f070f97) Allow custom getters in summaryFields() (fixes #7788) (Ingo Schommer)
* 2012-08-29 [c3d622c](https://github.com/silverstripe/sapphire/commit/c3d622c) Fix an issue caused by moving a page from one location in the tree to another location doesn't update parent id in the edit form immediately (see #7740) The issue causes the moved page to revert to previous location when Save and Publish before any page refresh or page switching. This commit also adds 'Modified' badge to the moved page (Saophalkun Ponlu)
* 2012-08-28 [cec461b](https://github.com/silverstripe/silverstripe-cms/commit/cec461b) Use AbsoluteLiveLink() for CMS previews (Ingo Schommer)
* 2012-08-28 [8a514d8](https://github.com/silverstripe/silverstripe-cms/commit/8a514d8) Correct live state in SiteTree-&gt;getAbsoluteLiveLink() (Ingo Schommer)
* 2012-08-28 [f454f48](https://github.com/silverstripe/silverstripe-cms/commit/f454f48) Session namespace sharing for CMS controllers (Ingo Schommer)
* 2012-08-27 [dc08e87](https://github.com/silverstripe/sapphire/commit/dc08e87) Take first non-empty link field for preview (Ingo Schommer)
* 2012-08-27 [62783c7](https://github.com/silverstripe/silverstripe-cms/commit/62783c7) Prevent overwriting of draft/live preview form fields (Ingo Schommer)
* 2012-08-27 [f638935](https://github.com/silverstripe/sapphire/commit/f638935) Fix CMS layout after preview navigation (fixes #7463) (Ingo Schommer)
* 2012-08-27 [e59aec3](https://github.com/silverstripe/silverstripe-cms/commit/e59aec3) Redirect to edit view after page revert in CMS (fixes #7391) (Ingo Schommer)
* 2012-08-27 [3e351bc](https://github.com/silverstripe/sapphire/commit/3e351bc) open ticket 7812 correcting filter syntax on a DataObject used by function updatetreenodes (Kirk Mayo)
* 2012-08-23 [87685ee](https://github.com/silverstripe/sapphire/commit/87685ee) Fix Versioned's stage_unique mode on PostgreSQL. (Sam Minnee)
* 2012-08-23 [ed0341e](https://github.com/silverstripe/sapphire/commit/ed0341e) Ensure that subtracting a sorted DataList works. (Sam Minnee)
* 2012-08-22 [ae9c2e7](https://github.com/silverstripe/sapphire/commit/ae9c2e7) Restore tree children after updateNode() (fixes #7761) (Ingo Schommer)
* 2012-08-22 [4fdc76d](https://github.com/silverstripe/silverstripe-cms/commit/4fdc76d) Installer templates (Naomi Guyer)
* 2012-08-22 [69182c2](https://github.com/silverstripe/sapphire/commit/69182c2) Installer implies empty template used in tutorial (Naomi Guyer)
* 2012-08-21 [9a8313d](https://github.com/silverstripe/sapphire/commit/9a8313d) GridField delete icon now correctly deletes, rather than always just unlinking (Fixes 7801) (James Cocker)
* 2012-08-21 [296ee1f](https://github.com/silverstripe/sapphire/commit/296ee1f) Add double quotes to index columns for more reliable DB-schema management. (Sam Minnee)
* 2012-08-21 [dd302a6](https://github.com/silverstripe/sapphire/commit/dd302a6) Ensure that all_versions are sorted explicitly for better cross-db behaviour. (Sam Minnee)
* 2012-08-20 [06cddb7](https://github.com/silverstripe/sapphire/commit/06cddb7) Force refresh of GridFieldDetailEditForm after save (Ingo Schommer)
* 2012-08-17 [5f9362e](https://github.com/silverstripe/silverstripe-cms/commit/5f9362e) Visual cue that URLSegment is updating (Ryan Wachtl)
* 2012-08-16 [2923e55](https://github.com/silverstripe/silverstripe-cms/commit/2923e55) Restrict URLSegment preview to editable fields (Ingo Schommer)
* 2012-08-15 [f79d2df](https://github.com/silverstripe/sapphire/commit/f79d2df) More robust url comparison in CMS (Ingo Schommer)
* 2012-08-15 [3ca24a8](https://github.com/silverstripe/sapphire/commit/3ca24a8) Installer failed complaining about rewrite server-capability: XHR response was 3 chars long and therefore !== "OK" MINOR: Added charset &lt;meta&gt; declaration to prevent errors cluttering up browser-based debugger console output (Russell Michell)
* 2012-08-12 [395580b](https://github.com/silverstripe/sapphire/commit/395580b) Locale-isolated i18n/Zend cache (Ingo Schommer)
* 2012-08-12 [4bbd904](https://github.com/silverstripe/silverstripe-cms/commit/4bbd904) fix getting translated string for page type description (Fixes #7781). (Will Rossiter)
* 2012-08-10 [ce2d31b](https://github.com/silverstripe/sapphire/commit/ce2d31b) Consistently self-closing form field tags (#7557) (Ingo Schommer)
* 2012-08-10 [b649c09](https://github.com/silverstripe/sapphire/commit/b649c09) prevent notice when using selection group (Jak)
* 2012-08-10 [5c5a506](https://github.com/silverstripe/sapphire/commit/5c5a506) removed use of deprecated method (Nik Rolls)
* 2012-08-10 [ca1d38d](https://github.com/silverstripe/sapphire/commit/ca1d38d) Localize DataObject-&gt;summaryFields() (Ingo Schommer)
* 2012-08-09 [deb3780](https://github.com/silverstripe/sapphire/commit/deb3780) #7768 - add-button and breadcrumb translation in Security, ModelAdmin (martimiz)
* 2012-08-09 [ec17d36](https://github.com/silverstripe/sapphire/commit/ec17d36) Fix PHPUnit autoloading problems in text collector (Ingo Schommer)
* 2012-08-08 [03e4893](https://github.com/silverstripe/silverstripe-cms/commit/03e4893) Fixing a issue with a undefined variable in getLink (Kirk Mayo)
* 2012-08-06 [39a9093](https://github.com/silverstripe/silverstripe-cms/commit/39a9093) SiteTree-&gt;provideI18nEntities() limited to class (Ingo Schommer)
* 2012-08-06 [e925401](https://github.com/silverstripe/silverstripe-cms/commit/e925401) Re-added singular/plural name i18n entities (Ingo Schommer)
* 2012-08-06 [52e05f2](https://github.com/silverstripe/sapphire/commit/52e05f2) Re-added singular/plural name i18n entities (Ingo Schommer)
* 2012-08-06 [1db8307](https://github.com/silverstripe/sapphire/commit/1db8307) Class autoloading in i18nTextCollector (Ingo Schommer)
* 2012-08-06 [77ec21f](https://github.com/silverstripe/silverstripe-cms/commit/77ec21f) Fully qualified namespace for _t() in templates (Ingo Schommer)
* 2012-08-06 [d0a9811](https://github.com/silverstripe/sapphire/commit/d0a9811) Fully qualified namespace for _t() in templates (Ingo Schommer)
* 2012-08-06 [b135218](https://github.com/silverstripe/sapphire/commit/b135218) Detect JS lang by &lt;body&gt;, and force init (Ingo Schommer)
* 2012-08-03 [a855309](https://github.com/silverstripe/sapphire/commit/a855309) javascript tree node updating fails when Translatable is used (Niklas Forsdahl)
* 2012-08-02 [76c5b56](https://github.com/silverstripe/sapphire/commit/76c5b56) augmentSQL always extended on base data class on query finalization (Niklas Forsdahl)
* 2012-08-01 [fb9e997](https://github.com/silverstripe/sapphire/commit/fb9e997) Use tree/xxx instead of tree?ID=xxx when fetching subtrees for TreeDropdownField. Fix #7730 (jean)
* 2012-07-31 [7c0e387](https://github.com/silverstripe/silverstripe-cms/commit/7c0e387) Missing preview archive version button (fixes 7656) (Naomi Guyer)
* 2012-07-26 [18a40b4](https://github.com/silverstripe/silverstripe-cms/commit/18a40b4) Adding siteconfig translations (Ruud Arentsen)
* 2012-07-26 [7dfc7de](https://github.com/silverstripe/silverstripe-cms/commit/7dfc7de) Missing comma in Maori language translation (Naomi Guyer)
* 2012-07-26 [a605d06](https://github.com/silverstripe/sapphire/commit/a605d06) Logo padding in collapsed Menu (Naomi Guyer)
* 2012-07-24 [143eceb](https://github.com/silverstripe/sapphire/commit/143eceb) Correct wrong parameter order. (Mateusz Uzdowski)
* 2012-07-20 [ee2b1a9](https://github.com/silverstripe/silverstripe-cms/commit/ee2b1a9) Check for the parameter existence. (Mateusz Uzdowski)
* 2012-07-09 [63ad68a](https://github.com/silverstripe/silverstripe-cms/commit/63ad68a) fixing an edge-case bug where a 404-page would get statically published and overwrite the homepage of the site (this would sometimes happen when a RedirectorPage was set to an external URL and still referenced an internal page ID) (Julian Seidenberg)
* 2012-06-04 [97d678b](https://github.com/silverstripe/silverstripe-cms/commit/97d678b) Provide default constructor value to filesystem publisher so that singleton calls (which don't pass params) don't fail (Marcus Nyeholt)
* 2012-03-14 [2facc31](https://github.com/silverstripe/sapphire/commit/2facc31) Case insensitive search filters for PostgreSQL (fixes #6548) (Ingo Schommer)
### Other
* 2012-09-03 [540f238](https://github.com/silverstripe/sapphire/commit/540f238) Added IRC notifications to Travis (Sam Minnee)
* 2012-09-02 [b99c9e8](https://github.com/silverstripe/sapphire/commit/b99c9e8) Add reference to documentation directory structure (Will Rossiter)
* 2012-09-01 [e624742](https://github.com/silverstripe/sapphire/commit/e624742) Make the border colors of test report status texts more subtle thus less distracting (Saophalkun Ponlu)
* 2012-09-01 [60987ac](https://github.com/silverstripe/sapphire/commit/60987ac) Various minor visual enhancements for Sapphire test report (Saophalkun Ponlu)
* 2012-08-31 [85ab39b](https://github.com/silverstripe/sapphire/commit/85ab39b) FIX 7832 Lang files for ss macron plugin - correct path to "langs", not "lang" MINOR Use consistent ed.getLang method (jean)
* 2012-08-30 [10d0296](https://github.com/silverstripe/sapphire/commit/10d0296) FIX: ensure date input has a date picker to trigger open (#7504) (Will Rossiter)
* 2012-08-30 [678232f](https://github.com/silverstripe/sapphire/commit/678232f) Add reference for template documentation (Will Rossiter)
* 2012-08-30 [898f9ad](https://github.com/silverstripe/sapphire/commit/898f9ad) DOC Gave easier instructions for would-be authors (Sam Minnée)
* 2012-08-29 [d9243cd](https://github.com/silverstripe/silverstripe-cms/commit/d9243cd) FIX Pages with obsolete class shouldnt do first versionless write (Hamish Friedlander)
* 2012-08-29 [2f00884](https://github.com/silverstripe/sapphire/commit/2f00884) FIX If ClassName read from DB doesnt exist, dont break (Hamish Friedlander)
* 2012-08-29 [09e3fa4](https://github.com/silverstripe/sapphire/commit/09e3fa4) Removed pre-emptive dev/build from travis test run, to make it faster. (Sam Minnee)
* 2012-08-29 [362e979](https://github.com/silverstripe/silverstripe-cms/commit/362e979) Replace tutorial link (Naomi Guyer)
* 2012-08-29 [05fade3](https://github.com/silverstripe/sapphire/commit/05fade3) FIX 7763 TreeDropdownField needs to refresh after CMS edit form save (Hamish Friedlander)
* 2012-08-28 [6162ae5](https://github.com/silverstripe/sapphire/commit/6162ae5) Fixed preview link ordering in CMS (Ingo Schommer)
* 2012-08-28 [b53790e](https://github.com/silverstripe/sapphire/commit/b53790e) Fluent API for PaginatedList (Ingo Schommer)
* 2012-08-28 [4369727](https://github.com/silverstripe/silverstripe-cms/commit/4369727) Enable page sorting by Page name in list view (see #7601) (Saophalkun Ponlu)
* 2012-08-28 [e595b8f](https://github.com/silverstripe/sapphire/commit/e595b8f) GridFieldSortableHeader now allows composite fields to be sorted based db fields (see #7601) (Saophalkun Ponlu)
* 2012-08-28 [62cfd87](https://github.com/silverstripe/silverstripe-cms/commit/62cfd87) FIX 7819 Check if the current folder ID is in the url before assuming the list should not filter by folder ID (jean)
* 2012-08-28 [2637e6d](https://github.com/silverstripe/silverstripe-cms/commit/2637e6d) FIX Dont refer to framework module in config rules (Hamish Friedlander)
* 2012-08-28 [26cfd64](https://github.com/silverstripe/sapphire/commit/26cfd64) FIX issue with cyclic configs when framework called sapphire (Hamish Friedlander)
* 2012-08-28 [d45dd34](https://github.com/silverstripe/silverstripe-cms/commit/d45dd34) FIX VirtualPageTest failing on apps with no $db on Page (Hamish Friedlander)
* 2012-08-28 [aa0cd14](https://github.com/silverstripe/sapphire/commit/aa0cd14) FIX Make config DAG error message more dev friendly (Hamish Friedlander)
* 2012-08-28 [cbadd3e](https://github.com/silverstripe/silverstripe-cms/commit/cbadd3e) FIX Config frag legacycmsroutes doesnt need to come after _everything_ (Hamish Friedlander)
* 2012-08-28 [2f64381](https://github.com/silverstripe/sapphire/commit/2f64381) LeftAndMain::$session_namespace (Ingo Schommer)
* 2012-08-27 [5a44ea2](https://github.com/silverstripe/sapphire/commit/5a44ea2) Deselect tree nodes when reacting to form load event (fixes #7401) (Ingo Schommer)
* 2012-08-27 [11b85e9](https://github.com/silverstripe/silverstripe-cms/commit/11b85e9) Fixed "from"/"to" filter field widths (Ingo Schommer)
* 2012-08-27 [4a8236f](https://github.com/silverstripe/sapphire/commit/4a8236f) Removed special "from"/"to" filter field CSS rules (Ingo Schommer)
* 2012-08-27 [e4db3c6](https://github.com/silverstripe/sapphire/commit/e4db3c6) Removed DateField special width with .hasDatepicker class (Ingo Schommer)
* 2012-08-27 [76dd8cc](https://github.com/silverstripe/sapphire/commit/76dd8cc) Remove width limit on CMS panel dropdowns (Ingo Schommer)
* 2012-08-27 [88dfde8](https://github.com/silverstripe/sapphire/commit/88dfde8) Removed arbitrary width restrictions on field group children (Ingo Schommer)
* 2012-08-27 [dddc5bd](https://github.com/silverstripe/sapphire/commit/dddc5bd) Removed accidental *.orig files (Ingo Schommer)
* 2012-08-27 [8b6e4f5](https://github.com/silverstripe/sapphire/commit/8b6e4f5) Add some basic tests for ConfigManifest#relativeOrder (Hamish Friedlander)
* 2012-08-27 [e0b8f15](https://github.com/silverstripe/sapphire/commit/e0b8f15) FIX Config wasnt filtering wildcards properly (Hamish Friedlander)
* 2012-08-27 [c7ca47f](https://github.com/silverstripe/sapphire/commit/c7ca47f) FIX Config frag could only have one before or after rule (Hamish Friedlander)
* 2012-08-27 [9b6216d](https://github.com/silverstripe/sapphire/commit/9b6216d) FIXED: Error in test case deprecation (Damian Mooyman)
* 2012-08-27 [0a6a3fa](https://github.com/silverstripe/sapphire/commit/0a6a3fa) i18n for file type descriptors (see #7798) (Ingo Schommer)
* 2012-08-26 [8dccb7f](https://github.com/silverstripe/sapphire/commit/8dccb7f) i18n for GridField pagination footer (see #7798) (Ingo Schommer)
* 2012-08-26 [8442ed0](https://github.com/silverstripe/silverstripe-cms/commit/8442ed0) i18n for report table title (see #7798) (Ingo Schommer)
* 2012-08-26 [2fab657](https://github.com/silverstripe/sapphire/commit/2fab657) i18n for CMS section titles (see #7798) (Ingo Schommer)
* 2012-08-26 [3b59212](https://github.com/silverstripe/sapphire/commit/3b59212) i18n for "select an anchor" string (see #7798) (Ingo Schommer)
* 2012-08-26 [59546cc](https://github.com/silverstripe/silverstripe-cms/commit/59546cc) Localized page name in "add page" dialog and dropdowns (see #7798) (Ingo Schommer)
* 2012-08-26 [6b6dfae](https://github.com/silverstripe/silverstripe-cms/commit/6b6dfae) Fixed i18n namespace for "Sync Files" (Ingo Schommer)
* 2012-08-24 [14759b6](https://github.com/silverstripe/sapphire/commit/14759b6) FIX #7787 Handles ajax and normal requests differently when validation fails on gridfields (jean)
* 2012-08-23 [d20eae4](https://github.com/silverstripe/silverstripe-cms/commit/d20eae4) Updated translations (Ingo Schommer)
* 2012-08-23 [0aa2894](https://github.com/silverstripe/sapphire/commit/0aa2894) Updated translations (Ingo Schommer)
* 2012-08-22 [3e07822](https://github.com/silverstripe/sapphire/commit/3e07822) Allow scheme-relative URLs in requirements (Fred Condo)
* 2012-08-22 [9ebac90](https://github.com/silverstripe/sapphire/commit/9ebac90) Removed 'relation filters' from datamodel docs (Ingo Schommer)
* 2012-08-21 [e159a68](https://github.com/silverstripe/sapphire/commit/e159a68) FIX Removes version checking for LSB in Object::static_lookup() (Simon Welsh)
* 2012-08-21 [f6334dd](https://github.com/silverstripe/sapphire/commit/f6334dd) Added default sort to test data for better cross-db performance. (Sam Minnee)
* 2012-08-21 [d0bc9c6](https://github.com/silverstripe/sapphire/commit/d0bc9c6) FIX Hierarchy#liveChildren couldnt handle lots of pages (Hamish Friedlander)
* 2012-08-21 [7807842](https://github.com/silverstripe/silverstripe-cms/commit/7807842) FIXED: Additional issue where the add-page ajax parameters wouldu incorrectly concatenate additional query parameters into the add action url. Resolved by moving URL concatenation from view to controller where Controller::join_links is available (Damian Mooyman)
* 2012-08-21 [f7ffb79](https://github.com/silverstripe/sapphire/commit/f7ffb79) FIXED: Compatibility fixes for MS SQL Server. Replaced back ticks (which are mysql specific) with double quotes (Damian Mooyman)
* 2012-08-21 [abbce15](https://github.com/silverstripe/sapphire/commit/abbce15) Updated Travis-CI configuration to have a 4 build grid. (Sam Minnee)
* 2012-08-20 [2e791ab](https://github.com/silverstripe/silverstripe-cms/commit/2e791ab) Better i18n for "new page" label (fixes #7796) (Ingo Schommer)
* 2012-08-20 [e6e2ab4](https://github.com/silverstripe/silverstripe-cms/commit/e6e2ab4) Updated translations (Ingo Schommer)
* 2012-08-20 [f0340e6](https://github.com/silverstripe/sapphire/commit/f0340e6) Updated translations (Ingo Schommer)
* 2012-08-20 [c019f22](https://github.com/silverstripe/silverstripe-cms/commit/c019f22) Fix notice when ErrorPage tries to create static error pages and can't write (Sean Harvey)
* 2012-08-20 [89728ac](https://github.com/silverstripe/sapphire/commit/89728ac) UPDATED: Improved get_all_versions test case to test versions in the middle of version updates. (Damian Mooyman)
* 2012-08-20 [56fe7f8](https://github.com/silverstripe/sapphire/commit/56fe7f8) REMOVED: Unnecessary publish actions from test cases ADDED: Test case for get_all_versions (Damian Mooyman)
* 2012-08-20 [0f09305](https://github.com/silverstripe/sapphire/commit/0f09305) FIXED: Issue where temporary table would cause unpredictable behaviour. Temporary table functionality was substituted with subqueries in each use case. ADDED: Test case for version archive functionality. (Damian Mooyman)
* 2012-08-16 [4727523](https://github.com/silverstripe/sapphire/commit/4727523) Added correct CSS class to GroupedDropdownField (Ingo Schommer)
* 2012-08-16 [f5007a5](https://github.com/silverstripe/silverstripe-cms/commit/f5007a5) Allow extension of "add" link in CMS (Ingo Schommer)
* 2012-08-16 [b560d25](https://github.com/silverstripe/sapphire/commit/b560d25) Re-enable Entwine Inspector in CMS & document (Hamish Friedlander)
* 2012-08-16 [915ae1a](https://github.com/silverstripe/sapphire/commit/915ae1a) Upgrade entwine to latest (Hamish Friedlander)
* 2012-08-15 [701da8b](https://github.com/silverstripe/sapphire/commit/701da8b) Updated translations; i18n for fieldLabels (Roland Lehmann)
* 2012-08-14 [fe14346](https://github.com/silverstripe/sapphire/commit/fe14346) Revert "Make PHPUnit bootstrap add flush=1" (Sam Minnee)
* 2012-08-14 [2c62dda](https://github.com/silverstripe/sapphire/commit/2c62dda) Fixed Travis CI and make it use SQLite (Sam Minnee)
* 2012-08-14 [e003796](https://github.com/silverstripe/sapphire/commit/e003796) Make PHPUnit bootstrap add flush=1 (Sam Minnee)
* 2012-08-14 [b952211](https://github.com/silverstripe/sapphire/commit/b952211) Fixed bugs in Travis CI set-up (Sam Minnee)
* 2012-08-14 [04e3bed](https://github.com/silverstripe/sapphire/commit/04e3bed) Added support for Travis CI (Sam Minnee)
* 2012-08-13 [ec89832](https://github.com/silverstripe/sapphire/commit/ec89832) Registering Te Reo support in i18n (Ingo Schommer)
* 2012-08-13 [7170eb7](https://github.com/silverstripe/sapphire/commit/7170eb7) Localized parts of TinyMCE into Te Reo (Ingo Schommer)
* 2012-08-13 [857afc4](https://github.com/silverstripe/sapphire/commit/857afc4) Localization for custom TinyMCE ssmacron module (Ingo Schommer)
* 2012-08-12 [e486a16](https://github.com/silverstripe/silverstripe-cms/commit/e486a16) "Edit tree" button alignment (Ingo Schommer)
* 2012-08-12 [94b739e](https://github.com/silverstripe/sapphire/commit/94b739e) Updated translations (Ingo Schommer)
* 2012-08-12 [82699ba](https://github.com/silverstripe/silverstripe-cms/commit/82699ba) Updated translations (Ingo Schommer)
* 2012-08-10 [c55b018](https://github.com/silverstripe/sapphire/commit/c55b018) FIXED: Issue where versioned would join _versions tables on ID,Version instead of RecordID,Version (Damian Mooyman)
* 2012-08-10 [22c5f31](https://github.com/silverstripe/sapphire/commit/22c5f31) FIXED: Issue where viewing an archived version of a page caused invalid SQL to be generated. This would only occur with subclasses of Page. (Damian Mooyman)
* 2012-08-10 [023721a](https://github.com/silverstripe/sapphire/commit/023721a) GridFieldPaginator localization (Ingo Schommer)
* 2012-08-10 [c7fd9a6](https://github.com/silverstripe/sapphire/commit/c7fd9a6) CMS Localization (Ingo Schommer)
* 2012-08-09 [77d939f](https://github.com/silverstripe/sapphire/commit/77d939f) CMS Localization (Ingo Schommer)
* 2012-08-09 [68855a2](https://github.com/silverstripe/sapphire/commit/68855a2) Guard against double inclusion of phpunit (Ingo Schommer)
* 2012-08-09 [186d95c](https://github.com/silverstripe/sapphire/commit/186d95c) Argument optional in collectFromEntityProviders() (Ingo Schommer)
* 2012-08-09 [d172e16](https://github.com/silverstripe/sapphire/commit/d172e16) FIXED: Bug in GridFieldAddExistingAutocompleter.php where an uninitialised variable would occasionally crash searches REMOVED: Unused variable (Damian Mooyman)
* 2012-08-09 [a80daef](https://github.com/silverstripe/sapphire/commit/a80daef) FIXED: Issue where urls with querystring arguments would not be properly concatenated with additional query parameters during ajax requests. The behaviour would not normally be noted except when using a module (such as Translatable) that adds parameters to data-url fields in forms. (Damian Mooyman)
* 2012-08-08 [b1ee36e](https://github.com/silverstripe/sapphire/commit/b1ee36e) Fix: display the correct (menu) icon in the GridFieldDetailForm's breadcrumbs. (martimiz)
* 2012-08-08 [342f076](https://github.com/silverstripe/sapphire/commit/342f076) Revert "NEW add selectsession URL endpoint" (Ingo Schommer)
* 2012-08-08 [a6087f1](https://github.com/silverstripe/silverstripe-cms/commit/a6087f1) FIXED: Issue where links within the CMS page list view would not be correctly generated. E.g. when the translatable module is used, page links for the "show children" action would come up as admin/pages/?locale=en_NZ?ParentID=21&view=list when they should be shows as admin/pages/?locale=en_NZ&ParentID=21&view=list. Uses Controller::join_links to perform the necessary sanity check on urls. (Damian Mooyman)
* 2012-08-07 [8d9db7f](https://github.com/silverstripe/sapphire/commit/8d9db7f) FIX: Proper buttonset styling (dd1079)
* 2012-08-07 [ae52be5](https://github.com/silverstripe/sapphire/commit/ae52be5) FIX: Missing last login time (fixes 7666) (Naomi Guyer)
* 2012-08-07 [3481297](https://github.com/silverstripe/sapphire/commit/3481297) FIX 7742 Decode the URI encoded attribute before displaying it as the value for the tree dropdown (jean)
* 2012-08-06 [342ecd9](https://github.com/silverstripe/silverstripe-cms/commit/342ecd9) Removed custom entities from master file (Ingo Schommer)
* 2012-08-06 [9b15bac](https://github.com/silverstripe/sapphire/commit/9b15bac) Parameter omission in i18nTextCollector (Ingo Schommer)
* 2012-08-06 [2276336](https://github.com/silverstripe/silverstripe-cms/commit/2276336) Maori translation of URLSegment JS UI (Ingo Schommer)
* 2012-08-06 [635c05b](https://github.com/silverstripe/silverstripe-cms/commit/635c05b) URLSegment JS UI globalization (Ingo Schommer)
* 2012-08-06 [7e33fac](https://github.com/silverstripe/silverstripe-cms/commit/7e33fac) Updated translations (Ingo Schommer)
* 2012-08-06 [8320e4e](https://github.com/silverstripe/sapphire/commit/8320e4e) Updated translations (Ingo Schommer)
* 2012-08-06 [671c7da](https://github.com/silverstripe/silverstripe-cms/commit/671c7da) SiteConfig load/save with ID in CMS (Ingo Schommer)
* 2012-08-05 [9076286](https://github.com/silverstripe/silverstripe-cms/commit/9076286) SiteTree-&gt;CMSEditLink() (Ingo Schommer)
* 2012-08-05 [0abef42](https://github.com/silverstripe/sapphire/commit/0abef42) Pointer to CMS architecture docs (Ingo Schommer)
* 2012-08-05 [b4e3c13](https://github.com/silverstripe/sapphire/commit/b4e3c13) Improved tree docs (Ingo Schommer)
* 2012-08-05 [bbbec35](https://github.com/silverstripe/sapphire/commit/bbbec35) Update ideal commit message to reflect new guidelines (Will Rossiter)
* 2012-08-04 [00a2edd](https://github.com/silverstripe/sapphire/commit/00a2edd) Wrong deprecation notice in DBField::create() (Juerg Rast)
* 2012-08-03 [6adc39e](https://github.com/silverstripe/sapphire/commit/6adc39e) Fixed example code in docs/en/topics/datamodel.md. (jakr)
* 2012-08-03 [eb82094](https://github.com/silverstripe/sapphire/commit/eb82094) Datamodel documentation fixes (Will Rossiter)
* 2012-08-03 [d774cb5](https://github.com/silverstripe/sapphire/commit/d774cb5) Add nowrap to buttons to ensure single lines (https://skitch.com/willrossi/ekp44/silverstripe-pages). Thanks oetiker (Will Rossiter)
* 2012-08-01 [fa67106](https://github.com/silverstripe/sapphire/commit/fa67106) Update javascript/lang/de_DE.js (dd1079)
* 2012-08-01 [90b0fe8](https://github.com/silverstripe/sapphire/commit/90b0fe8) FIX Only reload data for a item edited through a GridField if the record exists. Fix 7721 (jean)
* 2012-08-01 [1900842](https://github.com/silverstripe/sapphire/commit/1900842) Make the list used for autocomplete search results settable. (Andrew Short)
* 2012-07-31 [7558d32](https://github.com/silverstripe/sapphire/commit/7558d32) FIX: use standard template rendering process for RSS feeds (Will Rossiter)
* 2012-07-31 [61862e3](https://github.com/silverstripe/silverstripe-cms/commit/61862e3) Added Swedish javascript translations (Niklas Forsdahl)
* 2012-07-31 [2503e48](https://github.com/silverstripe/sapphire/commit/2503e48) Only initialise chosen elements when visible. (Andrew Short)
* 2012-07-31 [b38735d](https://github.com/silverstripe/sapphire/commit/b38735d) Fix chosen dropdown width not being set. (Andrew Short)
* 2012-07-31 [c1f27c1](https://github.com/silverstripe/sapphire/commit/c1f27c1) Revert b9ed6f7f6d388fc451efbada2d1501d667322cb0. (Andrew Short)
* 2012-07-29 [4abe6be](https://github.com/silverstripe/sapphire/commit/4abe6be) The documentation about internationalization in templates in topics/i18n.md did not match how the parser works. Related to ticket #7706. (jakr)
* 2012-07-28 [4848bec](https://github.com/silverstripe/sapphire/commit/4848bec) Removed duplicated 'return ' (Juerg Rast)
* 2012-07-26 [ebc89ff](https://github.com/silverstripe/sapphire/commit/ebc89ff) Update docs/en/index.md (LiamW)
* 2012-07-27 [72efed1](https://github.com/silverstripe/sapphire/commit/72efed1) Dont need to wrap entwine blocks in onload blocks, theres no benefit (Hamish Friedlander)
* 2012-07-26 [37e8b09](https://github.com/silverstripe/sapphire/commit/37e8b09) Update the IIS7 folder permission configuration docs. (Mateusz Uzdowski)
* 2012-07-24 [3bc2798](https://github.com/silverstripe/sapphire/commit/3bc2798) Fix edge case in sessionStorage detection for FireFox. If it is disabled using about:config, typeof will be object, but the value will be null. (jakr)
* 2012-07-20 [0308cc2](https://github.com/silverstripe/sapphire/commit/0308cc2) Tutorial 2/3 and some howto tweaks (Ingo Schommer)
* 2012-07-19 [6d8976e](https://github.com/silverstripe/sapphire/commit/6d8976e) Forms, navigation howto plus adjustments to tutorial one (#6367 ) (Naomi Guyer)
* 2012-07-09 [e0c92f1](https://github.com/silverstripe/silverstripe-cms/commit/e0c92f1) Display of last edit date should be exact to the minute. (Devlin)
* 2012-07-05 [aeb279b](https://github.com/silverstripe/silverstripe-installer/commit/aeb279b) Ignore git subcheckouts (Sam Minnee)
* 2012-07-05 [4c00b45](https://github.com/silverstripe/silverstripe-installer/commit/4c00b45) Added demo site deploy script (Sam Minnee)
* 2012-07-05 [1dfc722](https://github.com/silverstripe/silverstripe-installer/commit/1dfc722) Added frameworktest to demo site (Sam Minnee)
* 2012-07-02 [2d80ea5](https://github.com/silverstripe/sapphire/commit/2d80ea5) Documentation, tutorial (part3, and tidy-up part 1&2 ) (Naomi Guyer)
* 2012-06-29 [a58cb37](https://github.com/silverstripe/sapphire/commit/a58cb37) Fix TreeDropdownField toggle alignment in FF (Francisco arenas)
* 2012-05-21 [b109824](https://github.com/silverstripe/silverstripe-installer/commit/b109824) Added code to run the demo site, and check out selected modules. (Sam Minnee)

View File

@ -0,0 +1,39 @@
# How to customize the CMS Menu #
## Defining a Custom Icon ##
Every time you add a new extension of the `api:LeftAndMain` class to the CMS, SilverStripe will automatically create a new menu-item for it, with a default title and icon.
We can easily change that behaviour by using the static `$menu_title` and `$menu_icon` statics to
provide a custom title and icon.
The most popular extension of LeftAndMain is the `api:ModelAdmin` class, so we'll use that for an example.
We'll take the `ProductAdmin` class used in the [ModelAdmin reference](../reference/modeladmin#setup).
First we'll need a custom icon. For this purpose SilverStripe uses 16x16 black-and-transparent PNG graphics.
In this case we'll place the icon in `mysite/images`, but you are free to use any location.
:::php
class ProductAdmin extends ModelAdmin {
// ...
static $menu_icon = 'mysite/images/product-icon.png';
}
## Defining a Custom Title ##
The title of menu entries is configured through the `$menu_title` static.
If its not defined, the CMS falls back to using the class name of the controller,
removing the "Admin" bit at the end.
:::php
class ProductAdmin extends ModelAdmin {
// ...
static $menu_title = 'My Custom Admin';
}
In order to localize the menu title in different languages, use the `<classname>.MENUTITLE`
entity name, which is automatically created when running the i18n text collection.
For more information on language and translations, please refer to the [i18n](../reference/ii8n) docs.
## Related
* [How to extend the CMS interface](extend-cms-interface)

View File

@ -12,6 +12,11 @@ the language and functions which are used in the guides.
* [PHPUnit Configuration](phpunit-configuration). How to setup your testing environment with PHPUnit
* [Extend the CMS Interface](extend-cms-interface).
* [How to customize CMS Tree](customize-cms-tree).
* [Cache control](cache-control). Override the default PHP cache-control settings.
* [Howto customize the CMS menu](customize-cms-menu).
* [How to create a navigation menu](navigation-menu). Create primary navigation for your website.
* [Paginating A List](pagination). Add pagination for an SS_List object.
* [How to make a simple contact form](simple-contact-form).
## Feedback

View File

@ -122,16 +122,35 @@ Report security issues to [security@silverstripe.com](mailto:security@silverstri
Documentation for a software project is a continued and collaborative effort,
we encourage everybody to contribute, from simply fixing spelling mistakes, to writing recipes/howtos,
reviewing existing documentation, and translating the whole thing.
Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and
[Markdown](http://daringfireball.net/projects/markdown/)/[SSMarkdown](ss-markdown) knowledge.
If you have downloaded SilverStripe or a module, chances
are that you already have the documentation files - they are kept alongside the source code (in the `docs/` subfolder).
In general, you have to "[fork](http://help.github.com/forking/)" the [github.com/silverstripe/sapphire](http://github.com/silverstripe/sapphire)
and [github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms) repositories
and send us "[pull requests](http://help.github.com/pull-requests/)".
Note: Smaller edits can be performed in the github.com web interface on your fork,
every page view should have an "edit this file" button.
Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and
[Markdown](http://daringfireball.net/projects/markdown/)/[SSMarkdown](ss-markdown) knowledge,
and a GitHub user account.
### Editing online
The easiest way of making a change the the documentation is to find the appropriate .md
file in the [github.com/silverstripe/sapphire](https://github.com/silverstripe/sapphire/edit/3.0/docs/) repository
and press the "edit" button. You will need a GitHub account to do this.
* After you have made your change, describe it in the "commit summary" and "extended description" fields below, and press "Commit Changes".
* After that you will see form to submit a Pull Request. You should just be able to submit the form, and your changes will be sent to the core team for approval.
**Coming soon:** each documentation page will have an "edit" link, to make it easier for you to find this feature.
### Editing on your computer
If you prefer to edit the content on your local machine, you can "[fork](http://help.github.com/forking/)"
the [github.com/silverstripe/sapphire](http://github.com/silverstripe/sapphire)
and [github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms)
repositories and send us "[pull requests](http://help.github.com/pull-requests/)". If you have
downloaded SilverStripe or a module, chances are that you already have these checkouts.
The documentation is kept alongside the source code in the `docs/` subfolder.
**Note:** If you submit a new feature or an API change, we strongly recommend that your patch
includes updates to the necessary documentation. This helps prevent our documentation from
getting out of date.
### Repositories

View File

@ -217,6 +217,8 @@ For an introduction how to customize the CMS templates, see our [CMS Architectur
## Related
* [/topics/grid-field](GridField): The UI component powering ModelAdmin
* [/tutorials/5-dataobject-relationship-management](Tutorial 5: Dataobject Relationship Management)
* `[api:SearchContext]`
* [genericviews Module](http://silverstripe.org/generic-views-module)
* [Presentation about ModelAdmin at SupperHappyDevHouse Wellington](http://www.slideshare.net/chillu/modeladmin-in-silverstripe-23)

View File

@ -124,7 +124,8 @@ See [CSS](/topics/css) and [Javascript](/topics/javascript) topics for individua
## Conditional Logic
You can conditionally include markup in the output. That is, test for something that is true or false, and based on that test, control what gets output.
You can conditionally include markup in the output. That is, test for something
that is true or false, and based on that test, control what gets output.
The simplest if block is to check for the presence of a value.
@ -133,16 +134,22 @@ The simplest if block is to check for the presence of a value.
<p>You are logged in as $CurrentMember.FirstName $CurrentMember.Surname.</p>
<% end_if %>
The following compares a page property called `MyDinner` with the value in quotes, `kipper`, which is a **literal**. If true, the text inside the if-block is output.
The following compares a page property called `MyDinner` with the value in
quotes, `kipper`, which is a **literal**. If true, the text inside the if-block
is output.
:::ss
<% if $MyDinner="kipper" %>
Yummy, kipper for tea.
<% end_if %>
Note that inside a tag like this, variables should have a '$' prefix, and literals should have quotes. SilverStripe 2.4 didn't include the quotes or $ prefix, and while this still works, we recommend the new syntax as it is less ambiguous.
Note that inside a tag like this, variables should have a '$' prefix, and
literals should have quotes. SilverStripe 2.4 didn't include the quotes or $
prefix, and while this still works, we recommend the new syntax as it is less
ambiguous.
This example shows the use of the `else` option. The markup after `else` is output if the tested condition is *not* true.
This example shows the use of the `else` option. The markup after `else` is
output if the tested condition is *not* true.
:::ss
<% if $MyDinner="kipper" %>
@ -151,7 +158,10 @@ This example shows the use of the `else` option. The markup after `else` is outp
I wish I could have kipper :-(
<% end_if %>
This example shows the user of `else\_if`. There can be any number of `else\_if` clauses. The conditions are tested from first to last, until one of them is true, and the markup for that condition is used. If none of the conditions are true, the markup in the `else` clause is used, if that clause is present.
This example shows the user of `else_if`. There can be any number of `else_if`
clauses. The conditions are tested from first to last, until one of them is true,
and the markup for that condition is used. If none of the conditions are true,
the markup in the `else` clause is used, if that clause is present.
:::ss
<% if $MyDinner="quiche" %>
@ -169,14 +179,16 @@ This example shows the use of `not` to negate the test.
I'm going out for dinner tonight.
<% end_if %>
You can combine two or more conditions with `||` ("or"). The markup is used if *either* of the conditions is true.
You can combine two or more conditions with `||` ("or"). The markup is used if
*either* of the conditions is true.
:::ss
<% if $MyDinner=="kipper" || $MyDinner=="salmon" %>
yummy, fish for tea
<% end_if %>
You can combine two or more conditions with `&&` ("and"). The markup is used if *both* of the conditions are true.
You can combine two or more conditions with `&&` ("and"). The markup is used if
*both* of the conditions are true.
:::ss
<% if $MyDinner=="quiche" && $YourDinner=="kipper" %>
@ -185,7 +197,8 @@ You can combine two or more conditions with `&&` ("and"). The markup is used if
## Looping Over Lists
The `<% loop %>...<% end_loop %>` tag is used to **iterate** or loop over a collection of items. For example:
The `<% loop %>...<% end_loop %>` tag is used to **iterate** or loop over a
collection of items. For example:
:::ss
<ul>
@ -194,12 +207,16 @@ The `<% loop %>...<% end_loop %>` tag is used to **iterate** or loop over a coll
<% end_loop %>
</ul>
This loops over the children of a page, and generates an unordered list showing the `Title` property from each one. Note that `$Title` *inside* the loop refers to the `Title` property on each object that is looped over, not the current page. To refer to the current page's `Title` property inside the loop, you can do `$Up.Title`. More about `Up` later.
This loops over the children of a page, and generates an unordered list showing
the `Title` property from each one. Note that `$Title` *inside* the loop refers
to the `Title` property on each object that is looped over, not the current page.
To refer to the current page's `Title` property inside the loop, you can do
`$Up.Title`. More about `Up` later.
### Position Indicators
Inside the loop scope, there are many variables at your disposal to determine the current position
in the list and iteration:
Inside the loop scope, there are many variables at your disposal to determine the
current position in the list and iteration:
* `$Even`, `$Odd`: Returns boolean, handy for zebra striping
* `$EvenOdd`: Returns a string, either 'even' or 'odd'. Useful for CSS classes.
@ -216,7 +233,9 @@ $Modulus and $MultipleOf can help to build column layouts.
$Modulus(value, offset) // returns an int
$MultipleOf(factor, offset) // returns a boolean.
The following example demonstrates how you can use $Modulus(4) to generate custom column names based on your loop statement. Note that this works for any control statement (not just children)
The following example demonstrates how you can use $Modulus(4) to generate
custom column names based on your loop statement. Note that this works for any
control statement (not just children).
:::ss
<% loop Children %>
@ -225,9 +244,11 @@ The following example demonstrates how you can use $Modulus(4) to generate custo
</div>
<% end_loop %>
Will return you column-3, column-2, column-1, column-0, column-3 etc. You can use these as styling hooks to float, position as you need.
Will return you column-3, column-2, column-1, column-0, column-3 etc. You can
use these as styling hooks to float, position as you need.
You can also use $MultipleOf(value, offset) to help build columned layouts. In this case we want to add a <br> after every 3th item
You can also use $MultipleOf(value, offset) to help build columned layouts. In
this case we want to add a <br> after every 3th item.
:::ss
<% loop Children %>
@ -238,31 +259,110 @@ You can also use $MultipleOf(value, offset) to help build columned layouts. In t
## Scope
In the `<% loop %>` section, we saw an example of two **scopes**. Outside the `<% loop %>...<% end_loop %>`, we were in the scope of the page. But inside the loop, we were in the scope of an item in the list. The scope determines where the value comes from when you refer to a variable. Typically the outer scope of a page type's layout template is the page that is currently being rendered. The outer scope of an included template is the scope that it was included into.
In the `<% loop %>` section, we saw an example of two **scopes**. Outside the
`<% loop %>...<% end_loop %>`, we were in the scope of the page. But inside the
loop, we were in the scope of an item in the list. The scope determines where
the value comes from when you refer to a variable. Typically the outer scope of
a page type's layout template is the page that is currently being rendered.
The outer scope of an included template is the scope that it was included into.
When we are in a scope, we sometimes want to refer to the scope outside the <% loop %> or <% with %>. We can do that easily by using `$Up`.
### Up
When we are in a scope, we sometimes want to refer to the scope outside the
<% loop %> or <% with %>. We can do that easily by using `$Up`. `$Up` takes
the scope back to the previous level. Take the following example:
:::ss
$Title
--
<% loop Children %>
$Title
$Up.Title
--
<% loop Children %>
$Title
$Up.Title
<% end_loop %>
<% end_loop %>
With a page structure (Blog -> Blog entry -> Child blog entry) the
above will produce:
:::sss
Blog
--
Blog entry
Blog
--
Child blog entry
Blog entry
### Top
While `$Up` provides us a way to go up 1 scope, `$Top` is a shortcut to jump to
the top most scope of the page. Using the previous example but expanded to
include `$Top`:
:::ss
$Title
--
<% loop Children %>
$Title
$Up.Title
$Top.Title
--
<% loop Children %>
$Title
$Up.Title
$Top.Title
<% end_loop %>
<% end_loop %>
Will produce
:::ss
Blog
--
Blog entry
Blog
Blog
--
Child blog entry
Blog entry
Blog
### With
The `<% with %>...<% end_with %>` tag lets you introduce a new scope. Consider the following example:
The `<% with %>...<% end_with %>` tag lets you introduce a new scope. Consider
the following example:
:::ss
<% with $CurrentMember %>
Hello $FirstName, welcome back. Your current balance is $Balance.
<% end_with %>
Outside the `<% with %>...<% end_with %>`, we are in the page scope. Inside it, we are in the scope of `$CurrentMember`. We can refer directly to properties and methods of that member. So $FirstName is equivalent to $CurrentMember.FirstName. This keeps the markup clean, and if the scope is a complicated expression we don't have to repeat it on each reference of a property.
`<% with %>` also lets us use a collection as a scope, so we can access properties of the collection itself, instead of iterating over it. For example:
Outside the `<% with %>...<% end_with %>`, we are in the page scope. Inside it,
we are in the scope of `$CurrentMember`. We can refer directly to properties and
methods of that member. So $FirstName is equivalent to $CurrentMember.FirstName.
This keeps the markup clean, and if the scope is a complicated expression we don't
have to repeat it on each reference of a property.
`<% with %>` also lets us use a collection as a scope, so we can access
properties of the collection itself, instead of iterating over it. For example:
:::ss
$Children.Length
returns the number of items in the $Children collection.
## Pagination
Lists can be paginated, and looped over page-by-page.
For this to work, the list needs to be wrapped in a `[api:PaginatedList]`.
The process is explained in detail on the ["pagination" howto](/howto/pagination).
Lists can be paginated, and looped over to generate pagination. For this to
work, the list needs to be wrapped in a `[api:PaginatedList]`. The process is
explained in detail on the ["pagination" howto](/howto/pagination).
The list is split up in multiple "pages", each . Note that "page" is this context
does not necessarily refer to a `Page` class (although it often happens to be one).

View File

@ -51,6 +51,30 @@ Example Forum:
![](_images/modules_folder.jpg)
### Module documentation
Module developers can bundle developer documentation with their code by producing
plain text files inside a 'docs' folder located in the module folder. These files
can be written with the Markdown syntax (See ["Writing Documentation"](/misc/contributing#writing-documentation))
and include media such as images or videos.
Inside the docs folder, developers should organize the markdown files into each
separate language they wish to write documentation for (usually just `en`). Inside
each languages' subfolder, developers then have freedom to create whatever structure
they wish for organizing the documentation they wish.
Example Forum Documentation:
| Directory | Description |
| --------- | ----------- |
| `forum/docs` | The docs folder will be picked up by the documentation viewer. |
| `forum/docs/_manifest_exclude` | Empty file to signify that SilverStripe does not need to load classes from this folder |
| `forum/docs/en/` | English documentation |
| `forum/docs/en/index.md` | Documentation homepage. Should provide an introduction and links to remaining docs |
| `forum/docs/en/installing.md` | |
| `forum/docs/en/_images/` | Folder to store any images or media |
| `forum/docs/en/sometopic/` | You can organize documentation into nested folders |
## PHP Include Paths

View File

@ -1,404 +1,263 @@
# Using and extending GridField
# Gridfield
The `GridField` is a flexible form field for creating tables of data. It's new in SilverStripe 3.0 and replaces `ComplexTableField`, `TableListField`, and `TableField`. It's built as a lean core with a number of components that you plug into it. By selecting from the components that we provide or writing your own, you can grid a wide variety of grid controls.
Gridfield is SilverStripe's implementation of data grids. Its main purpose is to display tabular data
in a format that is easy to view and modify. It's a can be thought of as a HTML table with some tricks.
## Using GridField
It's built in a way that provides developers with an extensible way to display tabular data in a
table and minimise the amount of code that needs to be written.
A GridField is created like any other field: you create an instance of the GridField object and add it to the fields of a form. At its simplest, GridField takes 3 arguments: field name, field title, and an `SS_List` of records to display.
In order to quickly get data-focused UIs up and running,
you might also be interested in the [/reference/modeladmin](ModelAdmin) class
which is driven largely by the `GridField` class explained here.
This example might come from a Controller designed to manage the members of a group:
## Overview
The `GridField` is a flexible form field for creating tables of data. It was introduced in
SilverStripe 3.0 and replaced the `ComplexTableField`, `TableListField`, and `TableField` from
previous versions of SilverStripe.
Each GridField is built from a number of components. Without any components, a GridField has almost no
functionality. The components are responsible for formatting data to be readable and also modifying it.
A gridfield with only the `GridFieldDataColumn` component will display a set of read-only columns
taken from your list, without any headers or pagination. Large datasets don't fit to one
page, so you could add a `GridFieldPaginator` to paginatate the data. Sorting is supported by adding
a `GridFieldSortableHeader` that enables sorting on fields that can be sorted.
This document aims to explain the usage of GridFields with code examples.
<div class="hint" markdown='1'>
GridField can only be used with datasets that are of the type `SS_List` such as `DataList`
or `ArrayList`
</div>
## Creating a base GridField
A gridfield is often setup from a `Controller` that will output a form to the user. Even if there
are no other HTML input fields for gathering data from users, the gridfield itself must have a
`Form` to support interactions with it.
Here is an example where we display a basic gridfield with the default settings:
:::php
/**
* Form to display all members in a group
*/
public function MemberForm() {
$field = new GridField("Members", "Members of this group", $this->group->Members());
return new Form("MemberForm", $this, new FieldList($field), new FieldList());
class GridController extends Page_Controller {
public function index(SS_HTTPRequest $request) {
$this->Content = $this->AllPages();
return $this->render();
}
Note that the only way to specify the data that is listed in a grid field is with `SS_List` argument. If you want to customise the data displayed, you can do so by customising this object.
public function AllPages() {
$gridField = new GridField('pages', 'All pages', SiteTree::get());
return new Form($this, "AllPages", new FieldList($gridField), new FieldList());
}
}
This will create a read-only grid field that will show the columns specified in the Member's `$summary_fields` setting, and will let you sort and/or filter by those columns, as well as show pagination controls with a handful of records per page.
__Note:__ This is example code and the gridfield might not be styled nicely depending on the rest of
the css included.
## GridFieldConfig: Portable configuration
This gridfield will only contain a single column with the `Title` of each page. Gridfield by default
uses the `DataObject::$display_fields` for guessing what fields to display.
The example above a useful default case, but when developing applications you may need to control the behaviour of your grid more precisely than this. To this end, the `GridField` constructor allows for fourth argument, `$config`, where you can pass a `GridFieldConfig` object.
Instead of modifying a core `DataObject` we can tell the gridfield which fields to display by
setting the display fields on the `GridFieldDataColumns` component.
This example creates exactly the same kind of grid as the previous example, but it creates the configuration manually:
:::php
public function AllPages() {
$gridField = new GridField('pages', 'All pages', SiteTree::get());
$dataColumns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
$dataColumns->setDisplayFields(array(
'Title' => 'Title',
'URLSegment'=> 'URL',
'LastEdited' => 'Changed'
));
return new Form($this, "AllPages", new FieldList($gridField), new FieldList());
}
We will now move onto what the `GridFieldConfig`s are and how to use them.
----
## GridFieldConfig
A gridfields's behaviour and look all depends on what config we're giving it. In the above example
we did not specify one, so it picked a default config called `GridFieldConfig_Base`.
A config object is a container for `GridFieldComponents` which contain the actual functionality and
view for the gridfield.
A config object can be either injected as the fourth argument of the GridField constructor,
`$config` or set at a later stage by using a setter:
:::php
// On initialisation:
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_Base::create());
// By a setter after initialisation:
$gridField = new GridField('pages', 'All pages', SiteTree::get());
$gridField->setConfig(GridFieldConfig_Base::create());
The framework comes shipped with some base GridFieldConfigs:
### GridFieldConfig_Base
A simple read-only and paginated view of records with sortable and searchable headers.
:::php
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_Base::create());
The fields displayed are from `DataObject::getSummaryFields()`
### GridFieldConfig_RecordViewer
Similar to `GridFieldConfig_Base` with the addition support of:
- View read-only details of individual records.
The fields displayed in the read-only view is from `DataObject::getCMSFields()`
:::php
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_RecordViewer::create());
### GridFieldConfig_RecordEditor
Similar to `GridFieldConfig_RecordViewer` with the addition support of:
- Viewing and changing an individual records data.
- Deleting a record
:::php
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_RecordEditor::create());
The fields displayed in the edit form are from `DataObject::getCMSFields()`
### GridFieldConfig_RelationEditor
Similar to `GridFieldConfig_RecordEditor`, but adds features to work on a record's has-many or
many-many relationships.
The relations can be:
- Searched for existing records and add a relationship
- Detach records from the relationship (rather than removing them from the database)
- Create new related records and automatically add the relationship.
:::php
$gridField = new GridField('pages', 'All pages', SiteTree::get(), GridFieldConfig_RecordEditor::create());
The fields displayed in the edit form are from `DataObject::getCMSFields()`
## GridFieldComponents
GridFieldComponents the actual workers in a gridfield. They can be responsible for:
- Output some HTML to be rendered
- Manipulate data
- Recieve actions
- Display links
Components are added and removed from a config by setters and getters.
:::php
$config = GridFieldConfig::create();
// Provide a header row with filter controls
$config->addComponent(new GridFieldFilterHeader());
// Provide a default set of columns based on $summary_fields
// Add the base data columns to the gridfield
$config->addComponent(new GridFieldDataColumns());
// Provide a header row with sort controls
$config->addComponent(new GridFieldSortableHeader());
// Paginate results to 25 items per page, and show a footer with pagination controls
$config->addComponent(new GridFieldPaginator(25));
$field = new GridField("Members", "Members of this group", $this->group->Members(), $config);
$gridField = new GridField('pages', 'All pages', SiteTree::get(), $config);
If we wanted to make a simpler grid without pagination or filtering, we could do so like this:
It's also possible to insert a component before another component.
:::php
$config = GridFieldConfig::create();
// Provide a default set of columns based on $summary_fields
$config->addComponent(new GridFieldDataColumns());
// Provide a header row with sort controls
$config->addComponent(new GridFieldPaginator(25));
$field = new GridField("Members", "Members of this group", $this->group->Members(), $config);
$config->addComponent(new GridFieldFilterHeader(), 'GridFieldDataColumns');
A `GridFieldConfig` is made up of a new of `GridFieldComponent` objects, which are described in the next chapter.
## GridFieldComponent: Modular features
`GridFieldComponent` is a family of interfaces.
SilverStripe Framework comes with the following components that you can use out of the box.
### GridFieldDataColumns
This is the one component that, in most cases, you must include. It provides the default columns, sourcing them from the underlying DataObject's `$summary_fields` if no specific configuration is provided.
Without GridFieldDataColumns added to a GridField, it would have no columns whatsoever. Although this isn't particularly useful most of the time, we have allowed for this for two reasons:
* You may have a grid whose fields are generated purely by another non-standard component.
* It keeps the core of the GridField lean, focused solely on providing APIs to the components.
There are a number of methods that can be called on GridField to configure its behaviour.
You can choose which fields you wish to display:
Adding multiple components in one call:
:::php
$gridField->setDisplayFields(array(
'ID' => 'ID',
'FirstName' => 'First name',
'Surname' => 'Surname',
'Email' => 'Email',
'LastVisited' => 'Last visited',
));
$config->addComponents(new GridFieldDataColumns(), new GridFieldToolbarHeader());
You can specify formatting operations, for example choosing the format in which a date is displayed:
Removing a component:
:::php
$gridField->setFieldCasting(array(
'LastVisited' => 'Date->Ago',
));
$config->removeComponentsByType('GridFieldToolbarHeader');
You can also specify formatting replacements, to replace column contents with HTML tags:
For more information, see the [API for GridFieldConfig](http://api.silverstripe.org/3.0/framework/GridFieldConfig.html).
:::php
$gridField->setFieldFormatting(array(
'Email' => '<strong>$Email</strong>',
));
Here is a list of components for generic use:
**EXPERIMENTAL API WARNING:** We will most likely refactor this so that this configuration methods are called on the component rather than the grid field.
- `[api:GridFieldToolbarHeader]`
- `[api:GridFieldSortableHeader]`
- `[api:GridFieldFilterHeader]`
- `[api:GridFieldDataColumns]`
- `[api:GridFieldDeleteAction]`
- `[api:GridFieldViewButton]`
- `[api:GridFieldEditButton]`
- `[api:GridFieldPaginator]`
- `[api:GridFieldDetailForm]`
### GridFieldSortableHeader
## Creating a custom GridFieldComponent
This component will add a header to the grid with sort buttons. It will detect which columns are sortable and only provide sort controls on those columns.
### GridFieldFilterHeader
This component will add a header row with a text field filter for each column, letting you filter the results with text searches. It will detect which columns are filterable and only provide filter controls on those columns.
### GridFieldPaginator
This component will limit output to a fixed number of items per page add a footer row with pagination controls. The constructor takes 1 argument: the number of items per page.
### GridFieldDeleteButton
TODO Describe component
### GridFieldEditButton
Adds a edit button to each row of the table. This needs another component to provide an edit interface - see GridFieldDetailForm for use within the CMS.
### GridFieldRelationAdd
This class is is responsible for adding objects to another object's has_many and many_many relation,
as defined by the `[api:RelationList]` passed to the GridField constructor.
Objects can be searched through an input field (partially matching one or more fields).
Selecting from the results will add the object to the relation.
Often used alongside `[api:GridFieldRemoveButton]` for detaching existing records from a relatinship.
For easier setup, have a look at a sample configuration in `[api:GridFieldConfig_RelationEditor]`.
### GridFieldRemoveButton
Allows to detach an item from an existing has_many or many_many relationship.
Similar to {@link GridFieldDeleteAction}, but allows to distinguish between
a "delete" and "detach" action in the UI - and to use both in parallel, if required.
Requires the GridField to be populated with a `[api:RelationList]` rather than a plain DataList.
Often used alongside `[api:GridFieldAddExistingAutocompleter]` to add existing records to the relationship.
### GridFieldDetailForm
Provides add and edit forms for use within the CMS. This allows editing of the linked records.
This only provides the actual add/edit forms, GridFieldEditButton is required to provide a button to link to the edit form,
and GridFieldToolbarHeader is required to provide an add button.
### GridFieldToolbarHeader
Adds a title bar to the top of the GridField, with optional "New" button. The New button doesn't provide any functionality with this component alone - see GridFieldDetailForm.
### GridFieldExportButton
Adds an "Download as CSV" button. This will save the current List shown in the GridField as CSV. Takes the
## Extending GridField with custom components
You can create a custom component by building a class that implements one or more of the following interfaces: `GridField_HTMLProvider`, `GridField_ColumnProvider`, `GridField_ActionProvider`, or `GridField_DataManipulator`.
All of the methods expected by these interfaces take `$gridField` as their first argument. The gridField related to the component isn't set as a property of the component instance. This means that you can re-use the same component object across multiple `GridField`s, if that is appropriate.
It's common for a component to implement several of these interfaces in order to provide the complete implementation of a feature. For example, `GridFieldSortableHeader` implements the following:
* `GridField_HTMLProvider`, to generate the header row including the GridField_Action buttons
* `GridField_ActionProvider`, to define the sortasc and sortdesc actions that add sort column and direction to the state.
* `GridField_DataManipulator`, to alter the sorting of the data list based on the sort column and direction values in the state.
### GridFieldAddExistingAutocompleter
A GridFieldAddExistingAutocompleter is responsible for adding objects to another object's `has_many` and `many_many` relation,
as defined by the `[api:RelationList]` passed to the GridField constructor.
Objects can be searched through an input field (partially matching one or more fields).
Selecting from the results will add the object to the relation.
:::php
$group = Group::get()->First();
$config = GridFieldConfig::create()->addComponent(new GridFieldAddExistingAutocompleter(array('FirstName', 'Surname', 'Email'));
$gridField = new GridField('Members', 'Members', $group->Members(), $config);
## Component interfaces
A single component often uses a number of interfaces.
### GridField_HTMLProvider
The core GridField provides the following basic HTML:
Provides HTML for the header/footer rows in the table or before/after the template.
* A `<table>`, with an empty `<thead>` and `<tfoot>`
* A collection of `<tr>`s, based on the grid's data list, each of which will contain a collection or `<td>`s based on the grid's columns.
Examples:
The `GridField_HTMLProvider` component can provide HTML that goes into the `<thead>` or `<tfoot>`, or that appears before or after the table itself.
It should define the getHTMLFragments() method, which should return a map. The map keys are can be 'header', 'footer', 'before', or 'after'. The map values should be strings containing the HTML content to put into each of these spots. Only the keys for which you wish to provide content need to be defined.
For example, this components will add a footer row to the grid field, thanking the user for their patronage. You can see that we make use of `$gridField->getColumnCount()` to ensure that the single-cell row takes up the full width of the grid.
:::php
class ThankYouForUsingSilverStripe implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$colSpan = $gridField->getColumnCount();
return array(
'footer' => '<tr><td colspan="' . $colSpan . '">Thank you for using SilverStripe!</td></tr>',
);
}
}
If you wish to add CSS or JavaScript for your component, you may also make `Requirements` calls in this method.
### Defining new fragments
Sometimes it is helpful to have one component write HTML into another component. For example, you might have an action header row at the top of your GridField that several different components may define actions for.
To do this, you can put the following code into one of the HTML fragments returned by an HTML provider.
$DefineFragment(fragment-name)
Other `GridField_HTMLProvider` components can now write to `fragment-name` just as they would write to footer, etc. Fragments can be nested.
For example, this component creates a `header-actions` fragment name that can be populated by other components:
:::php
class HeaderActionComponent implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$colSpan = $gridField->getColumnCount();
array(
"header" => "<tr><td colspan=\"$colspan\">\$DefineFragment(header-actions)</td></tr>"
);
}
}
This is a simple example of how you might populate that new fragment:
:::php
class AddNewActionComponent implements GridField_HTMLProvider {
public function getHTMLFragments($gridField) {
$colSpan = $gridField->getColumnCount();
array(
"header-actions" => "<button>Add new</button>"
);
}
}
If you write to a fragment that isn't defined anywhere, or you create a circular dependency within fragments, an exception will be thrown.
- A header html provider displays a header before the table
- A pagination html provider displays pagination controls under the table
- A filter html fields displays filter fields on top of the table
- A summary html field displays sums of a field at the bottom of the table
### GridField_ColumnProvider
By default, a grid contains no columns. All the columns displayed in a grid will need to be added by an appropriate component.
Add a new column to the table display body, or modify existing columns. Used once per record/row.
For example, you may create a grid field with several components providing columns:
Examples:
* `GridFieldDataColumns` could provide basic data columns.
* An editor component could provide a column containing action buttons on the right.
* A multiselect component clould provide a column showing a checkbox on the left.
In order to provide additional columns, your component must implement `GridField_ColumnProvider`.
First you need to define 2 methods that specify which columns need to be added:
* **`function augmentColumns($gridField, &$columns)`:** Update the `$columns` variable (passed by reference) to include the names of the additional columns that this component provides. You can insert the values at any point you wish, for example if you need to add a column to the left of the grid, rather than the right.
* **`function getColumnsHandled($gridField)`:** Return an array of the column names. This overlaps with the function of `augmentColumns()` but leaves out any information about the order in which the columns are added.
Then you define 3 methods that specify what should be shown in these columns:
* **`function getColumnContent($gridField, $record, $columnName)`:** Return the HTML content of this column for the given record. Like `GridField_HTMLProvider`, you may make `Requirements` calls in this method.
* **`function getColumnAttributes($gridField, $record, $columnName)`:** Return a map of the HTML attributes to add to this column's `<td>` for this record. Most commonly, this is used to specify a colspan.
* **`function getColumnMetadata($gridField, $columnName)`:** Return a map of the metadata about this column. Right now, only one piece of meta-data is specified, "title". Other components (such as those responsible for generating headers) may fetch the column meta-data for their own purposes.
- A data columns provider that displays data from the list in rows and columns.
- A delete button column provider that adds a delete button at the end of the row
### GridField_ActionProvider
Most grid fields worthy of the name are interactive in some way. Users might able to page between results, sort by different columns, filter the results or delete records. Where this interaction necessitates an action on the server side, the following generally happens:
Action providers runs actions, some examples are:
* The user triggers an action.
* That action updates the state, database, or something else.
* The GridField is re-rendered with that new state.
These actions can be provided by components that implement the `GridField_ActionProvider` interface.
An action is defined by two things: an action name, and zero or more named arguments. There is no built-in notion of a record-specific or column-specific action, but you may choose to define an argument such as ColumnName or RecordID in order to implement these.
To provide your actions, define the following two functions:
* **`function getActions($gridField)`:** Return a list of actions that this component provides. There is no namespacing on these actions, so you need to ensure that they don't conflict with other components.
* **`function handleAction(GridField $gridField, $actionName, $arguments, $data)`:** Handle the action defined by `$actionName` and `$arguments`. `$data` will contain the full data from the form, if you need to access that.
To call your actions, you need to create `GridField_FormAction` elsewhere in your component. Read more about them below.
**EXPERIMENTAL API WARNING:** handleAction implementations often contain a big switch statement and this interface might be amended on, such that each action is defined in a separate method. If we do this, it will be done before 3.0 stable so that we can lock down the API, but early adopters should be aware of this potential for change!
- A delete action provider that deletes a DataObject.
- An export action provider that will export the current list to a CSV file.
### GridField_DataManipulator
A `GridField_DataManipulator` component can modify the data list. For example, a paginating component can apply a limit, or a sorting component can apply a sort. Generally, the data manipulator will make use of to `GridState` variables to decide how to modify the data list (see GridState below).
Modifies the data list. In general, the data manipulator will make use of `GridState` variables
to decide how to modify the data list.
* **`getManipulatedData(GridField $gridField, SS_List $dataList)`:** Given this grid's data list, return an updated list to be used with this grid.
Examples:
- A paginating data manipulator can apply a limit to a list (show only 20 records)
- A sorting data manipulator can sort the Title in a descending order.
### GridField_URLHandler
Sometimes an action isn't enough: you need to provide additional support URLs for the grid. These URLs may return user-visible content, for example a pop-up form for editing a record's details, or they may be support URLs for front-end functionality, for example a URL that will return JSON-formatted data for a javascript grid control.
Sometimes an action isn't enough, we need to provide additional support URLs for the grid. It
has a list of URL's that it can handle and the GridField passes request on to URLHandlers on matches.
To build these components, you should implement the `GridField_URLHandler` interface. It only specifies one method: `getURLHandlers($gridField)`. This method should return an array similar to the `RequestHandler::$url_handlers` static. The action handlers should also be defined on the component; they will be passed `$gridField` and `$request`.
Examples:
Here is an example in full. The actual implementation of the view and edit forms isn't included.
- A pop-up form for editing a record's details.
- JSON formatted data used for javascript control of the gridfield.
:::php
/**
* Provides view and edit forms at GridField-specific URLs. These can be placed into pop-ups by an appropriate front-end.
*
* The URLs provided will be off the following form:
* - <FormURL>/field/<GridFieldName>/item/<RecordID>
* - <FormURL>/field/<GridFieldName>/item/<RecordID>/edit
*/
class GridFieldDetailForm implements GridField_URLHandler {
public function getURLHandlers($gridField) {
return array(
'item/$ID' => 'handleItem',
);
}
## GridField_FormAction
public function handleItem($gridField, $request) {
$record = $gridField->getList()->byId($request->param("ID"));
return new GridFieldDetailForm_ItemRequest($gridField, $this, $record);
}
}
class GridFieldDetailForm_ItemRequest extends RequestHandler {
protected $gridField;
protected $component;
protected $record;
public function __construct($gridField, $component, $record) {
$this->gridField = $gridField;
$this->component = $gridField;
$this->record = $record;
parent::__construct();
}
public function index() {
echo "view form for record #" . $record->ID;
}
public function edit() {
echo "edit form for record #" . $record->ID;
}
}
## Other tools
This object is used for creating actions buttons, for example a delete button. When a user clicks on
a FormAction, the gridfield finds a `GridField_ActionProvider` that listens on that action.
`GridFieldDeleteAction` have a pretty basic implementation of how to use a Form action.
### GridState
Each `GridField` object has a key-store available handled by the `GridState` class. You can call `$gridField->State` to get access to this key-store. You may reference any key name you like, and do so recursively to any depth you like:
Gridstate is a class that is used to contain the current state and actions on the gridfield. It's
transfered between page requests by being inserted as a hidden field in the form.
:::php
$gridField->State->Foo->Bar->Something = "hello";
A GridFieldComponent sets and gets data from the GridState.
Because there is no schema for the grid state, its good practice to keep your state within a namespace, by first accessing a state key that has the same name as your component class. For example, this is how the `GridFieldSortableHeader` component manages its sort state.
## Related
:::php
$state = $gridField->State->GridFieldSortableHeader;
$state->SortColumn = $arguments['SortColumn'];
$state->SortDirection = 'asc';
...
$state = $gridField->State->GridFieldSortableHeader;
if ($state->SortColumn == "") {
return $dataList;
} else {
return $dataList->sort($state->SortColumn, $state->SortDirection)
}
When checking for empty values in the state, you should compare the state value to the empty string. This is because state values always return a `GridState_Data` object, and comparing to an empty string will call its `__toString()` method.
:::php
// Good
if ($state->SortColumn == "") { ... }
// Bad
if (!$state->SortColumn) { ... }
**NOTE:** Under the hood, `GridState` is a subclass of hidden field that provides a `getData()` method that returns a `GridState_Data` object. `$gridField->getState()` returns that `GridState_Data` object.
### GridField_Action
The `GridField_Action` class is a subclass of `FormAction` that will provide a button designed to trigger a grid field action. This is how you can link user-interface controls to the actions defined in `GridField_ActionProvider` components.
To create the action button, instantiate the object with the following arguments to your constructor:
* grid field
* button name
* button label
* action name
* action arguments (an array of named arguments)
For example, this could be used to create a sort button:
:::php
$field = new GridField_Action(
$gridField, 'SetOrder'.$columnField, $title,
"sortasc", array('SortColumn' => $columnField));
Once you have created your button, you need to render it somewhere. You can include the `GridField_Action` object in a template that is being rendered, or you can call its `Field()` method to generate the HTML content.
:::php
$output .= $field->Field();
Most likely, you will do this in `GridField_HTMLProvider::getHTMLFragments()` or `GridField_ColumnProvider::getColumnContent()`.
### GridField Helper Methods
The GridField class provides a number of methods that are useful for components. See [the API documentation](api:GridField) for the full list, but here are a few:
* **`getList()`:** Returns the data list for this grid, without the state modifications applied.
* **`getState()`:** Also called as `$gridField->State`, returns the `GridState_Data` object storing the current state.
* **`getColumnMetadata($column)`:** Return the metadata of the given column.
* **`getColumnCount()`:** Returns the number of columns
* [/reference/modeladmin](ModelAdmin: A UI driven by GridField)
* [/tutorials/5-dataobject-relationship-management](Tutorial 5: Dataobject Relationship Management)

View File

@ -560,7 +560,7 @@ class Email extends ViewableData {
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
* http://creativecommons.org/licenses/by-sa/2.5/
*/
function is_valid_address($email){
public static function is_valid_address($email){
$qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]';
$dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]';
$atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c'.

View File

@ -138,6 +138,9 @@ class File extends DataObject {
),
'flash' => array(
'swf', 'fla'
),
'doc' => array(
'doc','docx','txt','rtf','xls','xlsx','pages', 'ppt','pptx','pps','csv', 'html','htm','xhtml', 'xml','pdf'
)
);

View File

@ -141,10 +141,12 @@ class DateField extends TextField {
// Add other jQuery UI specific, namespaced options (only serializable, no callbacks etc.)
// TODO Move to DateField_View_jQuery once we have a properly extensible HTML5 attribute system for FormField
$jqueryUIConfig = array();
foreach($this->getConfig() as $k => $v) {
if(preg_match('/^jQueryUI\.(.*)/', $k, $matches)) $config[$matches[1]] = $v;
if(preg_match('/^jQueryUI\.(.*)/', $k, $matches)) $jqueryUIConfig[$matches[1]] = $v;
}
if ($jqueryUIConfig)
$config['jqueryuiconfig'] = Convert::array2json(array_filter($jqueryUIConfig));
$config = array_filter($config);
foreach($config as $k => $v) $this->setAttribute('data-' . $k, $v);
@ -599,7 +601,8 @@ class DateField_View_JQuery extends Object {
'/l/' => '',
'/YYYY/' => 'yy',
'/yyyy/' => 'yy',
'/[^y]yy[^y]/' => 'y',
// See http://open.silverstripe.org/ticket/7669
'/y{1,3}/' => 'yy',
'/a/' => '',
'/B/' => '',
'/hh/' => '',

View File

@ -193,6 +193,21 @@ class GridField extends FormField {
return $this->list;
}
/**
* Get the datasource after applying the {@link GridField_DataManipulator}s to it.
*
* @return SS_List
*/
public function getManipulatedList() {
$list = $this->getList();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_DataManipulator) {
$list = $item->getManipulatedData($this, $list);
}
}
return $list;
}
/**
* Get the current GridState_Data or the GridState
*
@ -227,12 +242,7 @@ class GridField extends FormField {
$columns = $this->getColumns();
// Get data
$list = $this->getList();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_DataManipulator) {
$list = $item->getManipulatedData($this, $list);
}
}
$list = $this->getManipulatedList();
// Render headers, footers, etc
$content = array(

View File

@ -14,21 +14,23 @@
* - {@link GridFieldConfig_RelationEditor}
*/
class GridFieldConfig {
/**
*
* @return GridFieldConfig
*/
public static function create(){
return new GridFieldConfig();
}
/**
*
* @var ArrayList
*/
protected $components = null;
/**
* @param mixed $arguments,... arguments to pass to the constructor
* @return GridFieldConfig
*/
public static function create() {
return call_user_func_array('Object::create', array_merge(
array(get_called_class()),
func_get_args()
));
}
/**
*
*/
@ -131,16 +133,6 @@ class GridFieldConfig {
* with sortable and searchable headers.
*/
class GridFieldConfig_Base extends GridFieldConfig {
/**
*
* @param int $itemsPerPage - How many items per page should show up per page
* @return GridFieldConfig_Base
*/
public static function create($itemsPerPage=null){
return new GridFieldConfig_Base($itemsPerPage);
}
/**
*
* @param int $itemsPerPage - How many items per page should show up
@ -176,16 +168,6 @@ class GridFieldConfig_RecordViewer extends GridFieldConfig_Base {
*
*/
class GridFieldConfig_RecordEditor extends GridFieldConfig {
/**
*
* @param int $itemsPerPage - How many items per page should show up
* @return GridFieldConfig_RecordEditor
*/
public static function create($itemsPerPage=null){
return new GridFieldConfig_RecordEditor($itemsPerPage);
}
/**
*
* @param int $itemsPerPage - How many items per page should show up
@ -225,16 +207,6 @@ class GridFieldConfig_RecordEditor extends GridFieldConfig {
* </code>
*/
class GridFieldConfig_RelationEditor extends GridFieldConfig {
/**
*
* @param int $itemsPerPage - How many items per page should show up
* @return GridFieldConfig_RelationEditor
*/
public static function create($itemsPerPage=null){
return new GridFieldConfig_RelationEditor($itemsPerPage);
}
/**
*
* @param int $itemsPerPage - How many items per page should show up

View File

@ -327,9 +327,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
// regardless of overloaded CMS controller templates.
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
$form->setTemplate('LeftAndMain_EditForm');
$form->addExtraClass('cms-content cms-edit-form center ss-tabset');
$form->addExtraClass('cms-content cms-edit-form center');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
if($form->Fields()->hasTabset()) {
$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
$form->addExtraClass('ss-tabset');
}
if($toplevelController->hasMethod('Backlink')) {
$form->Backlink = $toplevelController->Backlink();
@ -364,6 +367,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
function doSave($data, $form) {
$new_record = $this->record->ID == 0;
$controller = Controller::curr();
try {
$form->saveInto($this->record);
@ -371,7 +375,18 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
$this->gridField->getList()->add($this->record);
} catch(ValidationException $e) {
$form->sessionMessage($e->getResult()->message(), 'bad');
return Controller::curr()->redirectBack();
$responseNegotiator = new PjaxResponseNegotiator(array(
'CurrentForm' => function() use(&$form) {
return $form->forTemplate();
},
'default' => function() use(&$controller) {
return $controller->redirectBack();
}
));
if($controller->getRequest()->isAjax()){
$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
}
return $responseNegotiator->respond($controller->getRequest());
}
// TODO Save this item into the given relationship
@ -386,10 +401,16 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
if($new_record) {
return Controller::curr()->redirect($this->Link());
} else {
} elseif($this->gridField->getList()->byId($this->record->ID)) {
// Return new view, as we can't do a "virtual redirect" via the CMS Ajax
// to the same URL (it assumes that its content is already current, and doesn't reload)
return $this->edit(Controller::curr()->getRequest());
} else {
// Changes to the record properties might've excluded the record from
// a filtered list, so return back to the main view if it can't be found
$noActionURL = $controller->removeAction($data['url']);
$controller->getRequest()->addHeader('X-Pjax', 'Content');
return $controller->redirect($noActionURL, 302);
}
}

View File

@ -108,7 +108,7 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
public function getManipulatedData(GridField $gridField, SS_List $dataList) {
if(!$this->checkDataType($dataList)) return $dataList;
$this->totalItems = $gridField->getList()->count();
$this->totalItems = $dataList->count();
$state = $gridField->State->GridFieldPaginator;
if(!is_int($state->currentPage)) {

View File

@ -14,6 +14,9 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
*/
protected $throwExceptionOnBadDataType = true;
/** @var array */
public $fieldSorting = array();
/**
* Determine what happens when this component is used with a list that isn't {@link SS_Filterable}.
*
@ -49,6 +52,24 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
}
}
/**
* Specify sortings with fieldname as the key, and actual fieldname to sort as value.
* Example: array("MyCustomTitle"=>"Title", "MyCustomBooleanField" => "ActualBooleanField")
*
* @param array $casting
*/
public function setFieldSorting($sorting) {
$this->fieldSorting = $sorting;
return $this;
}
/**
* @return array
*/
public function getFieldSorting() {
return $this->fieldSorting;
}
/**
* Returns the header row providing titles with sort buttons
*/
@ -65,6 +86,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
$currentColumn++;
$metadata = $gridField->getColumnMetadata($columnField);
$title = $metadata['title'];
if(isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) $columnField = $this->fieldSorting[$columnField];
if($title && $gridField->getList()->canSortBy($columnField)) {
$dir = 'asc';
if($state->SortColumn == $columnField && $state->SortDirection == 'asc') {

View File

@ -8,7 +8,7 @@
$(this).siblings("button").addClass("ui-icon ui-icon-calendar");
var holder = $(this).parents('.field.date:first'),
config = $.extend(opts || {}, $(this).data(), {});
config = $.extend(opts || {}, $(this).data(), $(this).data('jqueryuiconfig'), {});
if(!config.showcalendar) return;
if(config.locale && $.datepicker.regional[config.locale]) {
@ -28,6 +28,9 @@
$('.field.date input.text,.fieldholder-small input.text.date').live('click', function() {
$(this).ssDatepicker();
if($(this).data('datepicker')) {
$(this).datepicker('show');
}
});
}(jQuery));

View File

@ -84,7 +84,7 @@
.addClass('ui-icon-triangle-1-n');
if(tree.is(':empty')) this.loadTree();
this.trigger('panelshow');
},
closePanel: function() {
jQuery('body').unbind('click', _clickTestFn);
@ -100,6 +100,7 @@
this.getPanel().hide();
this.trigger('panelhide');
},
togglePanel: function() {
this[this.getPanel().is(':visible') ? 'closePanel' : 'openPanel']();

View File

@ -38,7 +38,7 @@ abstract class DataExtension extends Extension {
$extraStaticsMethod = 'extraStatics';
}
$statics = Injector::inst()->get($extension, true, $args)->$extraStaticsMethod();
$statics = Injector::inst()->get($extension, true, $args)->$extraStaticsMethod($class, $extension);
if ($statics) {
Deprecation::notice('3.1.0', "$extraStaticsMethod deprecated. Just define statics on your extension, or use get_extra_config", Deprecation::SCOPE_GLOBAL);

View File

@ -181,9 +181,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public static function database_fields($class) {
if(get_parent_class($class) == 'DataObject') {
$db = DB::getConn();
$existing = $db->hasField($class, 'ClassName') ? $db->query("SELECT DISTINCT \"ClassName\" FROM \"$class\"")->column() : array();
return array_merge (
array (
'ClassName' => "Enum('" . implode(', ', ClassInfo::subclassesFor($class)) . "')",
'ClassName' => "Enum('" . implode(', ', array_unique(array_merge($existing, ClassInfo::subclassesFor($class)))) . "')",
'Created' => 'SS_Datetime',
'LastEdited' => 'SS_Datetime'
),
@ -444,6 +447,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
}
}
function getObsoleteClassName() {
$className = $this->getField("ClassName");
if (!ClassInfo::exists($className)) return $className;
}
function getClassName() {
$className = $this->getField("ClassName");
if (!ClassInfo::exists($className)) return get_class($this);
return $className;
}
/**
* Set the ClassName attribute. {@link $class} is also updated.
* Warning: This will produce an inconsistent record, as the object
@ -986,15 +1000,32 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->brokenOnWrite = true;
$isNewRecord = false;
if(self::get_validation_enabled()) {
$writeException = null;
if ($this->ObsoleteClassName) {
$writeException = new ValidationException(
"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
"you need to change the ClassName before you can write it",
E_USER_WARNING
);
}
else if(self::get_validation_enabled()) {
$valid = $this->validate();
if (!$valid->valid()) {
$writeException = new ValidationException(
$valid,
"Validation error writing a $this->class object: " . $valid->message() . ". Object not written.",
E_USER_WARNING
);
}
}
if($writeException) {
// Used by DODs to clean up after themselves, eg, Versioned
$this->extend('onAfterSkippedWrite');
throw new ValidationException($valid, "Validation error writing a $this->class object: " . $valid->message() . ". Object not written.", E_USER_WARNING);
throw $writeException;
return false;
}
}
$this->onBeforeWrite();
if($this->brokenOnWrite) {
@ -3135,7 +3166,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @return array
*/
public function summaryFields(){
$fields = $this->stat('summary_fields');
// if fields were passed in numeric array,
@ -3158,8 +3188,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(!$fields) $fields['ID'] = 'ID';
// Localize fields (if possible)
$labels = $this->fieldLabels(false);
$fields = array_intersect_key($labels, $fields);
foreach($this->fieldLabels(false) as $name => $label) {
if(isset($fields[$name])) $fields[$name] = $label;
}
return $fields;
}

View File

@ -439,6 +439,18 @@ abstract class SS_Database {
}
}
/**
* Return true if the table exists and already has a the field specified
* @param string $tableName - The table to check
* @param string $fieldName - The field to check
* @return bool - True if the table exists and the field exists on the table
*/
function hasField($tableName, $fieldName) {
if (!$this->hasTable($tableName)) return false;
$fields = $this->fieldList($tableName);
return array_key_exists($fieldName, $fields);
}
/**
* Generate the given field on the table, modifying whatever already exists as necessary.
* @param string $table The table name.

View File

@ -110,7 +110,7 @@ class SQLQuery {
}
function __get($field) {
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use getSlect() instead');
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use getSelect() instead');
if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use getFrom() instead');
if(strtolower($field) == 'groupby') Deprecation::notice('3.0', 'Please use getGroupBy() instead');
if(strtolower($field) == 'orderby') Deprecation::notice('3.0', 'Please use getOrderBy() instead');

View File

@ -1103,7 +1103,7 @@ class Versioned_Version extends ViewableData {
$record['ID'] = $record['RecordID'];
$className = $record['ClassName'];
$this->object = new $className($record);
$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
$this->failover = $this->object;
parent::__construct();

View File

@ -25,7 +25,12 @@ class EndsWithFilter extends SearchFilter {
*/
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'");
return $query->where(sprintf(
"%s %s '%%%s'",
$this->getDbName(),
(DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE',
Convert::raw2sql($this->getValue())
));
}
public function isEmpty() {

View File

@ -15,13 +15,14 @@ class PartialMatchFilter extends SearchFilter {
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
$where = array();
$comparison = (DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE';
if(is_array($this->getValue())) {
foreach($this->getValue() as $value) {
$where[]= sprintf("%s LIKE '%%%s%%'", $this->getDbName(), Convert::raw2sql($value));
$where[]= sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($value));
}
} else {
$where[] = sprintf("%s LIKE '%%%s%%'", $this->getDbName(), Convert::raw2sql($this->getValue()));
$where[] = sprintf("%s %s '%%%s%%'", $this->getDbName(), $comparison, Convert::raw2sql($this->getValue()));
}
return $query->where(implode(' OR ', $where));

View File

@ -25,7 +25,12 @@ class StartsWithFilter extends SearchFilter {
*/
public function apply(DataQuery $query) {
$this->model = $query->applyRelation($this->relation);
return $query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'");
return $query->where(sprintf(
"%s %s '%s%%'",
$this->getDbName(),
(DB::getConn() instanceof PostgreSQLDatabase) ? 'ILIKE' : 'LIKE',
Convert::raw2sql($this->getValue())
));
}
public function isEmpty() {

View File

@ -1,5 +1,5 @@
<div id="$Name" class="field<% if extraClass %> $extraClass<% end_if %>">
$Field
<label class="right" for="$ID">$Title</label>
<% if Message %><span class="message $MessageType">$messageBlock</span><% end_if %>
<% if Message %><span class="message $MessageType">$Message</span><% end_if %>
</div>

View File

@ -22,6 +22,7 @@ class CacheTest extends SapphireTest {
SS_Cache::set_cache_lifetime('test', 0.5, 20);
$cache = SS_Cache::factory('test');
$this->assertEquals(0.5, $cache->getOption('lifetime'));
$cache->save('Good', 'cachekey');
$this->assertEquals('Good', $cache->load('cachekey'));
@ -44,5 +45,17 @@ class CacheTest extends SapphireTest {
$this->assertFalse($cache1->load('cachekey'));
$this->assertEquals('Bar', $cache2->load('cachekey'));
}
function testCacheDefault() {
SS_Cache::set_cache_lifetime('default', 1200);
$default = SS_Cache::get_cache_lifetime('default');
$this->assertEquals(1200, $default['lifetime']);
$cache = SS_Cache::factory('somethingnew');
$this->assertEquals(1200, $cache->getOption('lifetime'));
}
}

View File

@ -18,7 +18,11 @@ class DirectorTest extends SapphireTest {
}
Config::inst()->update('Director', 'rules', array(
'DirectorTestRule/$Action/$ID/$OtherID' => 'DirectorTestRequest_Controller'
'DirectorTestRule/$Action/$ID/$OtherID' => 'DirectorTestRequest_Controller',
'en-nz/$Action/$ID/$OtherID' => array(
'Controller' => 'DirectorTestRequest_Controller',
'Locale' => 'en_NZ'
)
));
}
@ -214,6 +218,25 @@ class DirectorTest extends SapphireTest {
Deprecation::restore_settings($originalDeprecation);
}
/**
* Tests that additional parameters specified in the routing table are
* saved in the request
*/
function testRouteParams() {
Director::test('en-nz/myaction/myid/myotherid', null, null, null, null, null, null, $request);
$this->assertEquals(
$request->params(),
array(
'Controller' => 'DirectorTestRequest_Controller',
'Action' => 'myaction',
'ID' => 'myid',
'OtherID' => 'myotherid',
'Locale' => 'en_NZ'
)
);
}
function testForceSSLProtectsEntireSite() {
$_SERVER['REQUEST_URI'] = Director::baseURL() . 'admin';
$output = Director::forceSSL();

View File

@ -392,6 +392,22 @@ class GridFieldTest extends SapphireTest {
$this->assertEquals((string)$members[1]->td[0], 'Otto Fischer', 'Second object Name should be Otto Fischer');
$this->assertEquals((string)$members[1]->td[1], 'otto.fischer@example.org', 'Second object Email should be otto.fischer@example.org');
}
public function testChainedDataManipulators() {
$config = new GridFieldConfig();
$data = new ArrayList(array(1, 2, 3, 4, 5, 6));
$gridField = new GridField('testfield', 'testfield', $data, $config);
$endList = $gridField->getManipulatedList();
$this->assertEquals($endList->Count(), 6);
$config->addComponent(new GridFieldTest_Component2);
$endList = $gridField->getManipulatedList();
$this->assertEquals($endList->Count(), 12);
$config->addComponent(new GridFieldPaginator(10));
$endList = $gridField->getManipulatedList();
$this->assertEquals($endList->Count(), 10);
}
}
class GridFieldTest_Component implements GridField_ColumnProvider, GridField_ActionProvider, TestOnly{
@ -430,6 +446,14 @@ class GridFieldTest_Component implements GridField_ColumnProvider, GridField_Act
}
class GridFieldTest_Component2 implements GridField_DataManipulator, TestOnly {
function getManipulatedData(GridField $gridField, SS_List $dataList) {
$dataList = clone $dataList;
$dataList->merge(new ArrayList(array(7, 8, 9, 10, 11, 12)));
return $dataList;
}
}
class GridFieldTest_Team extends DataObject implements TestOnly {
static $db = array(
'Name' => 'Varchar',

View File

@ -136,6 +136,32 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals("Filtered value", $results->First()->HiddenValue);
}
function testStartsWithFilterCaseInsensitive() {
$all = singleton("SearchContextTest_AllFilterTypes");
$context = $all->getDefaultSearchContext();
$params = array(
"StartsWith" => "12345-6789 camelcase", // spelled lowercase
);
$results = $context->getResults($params);
$this->assertEquals(1, $results->Count());
$this->assertEquals("Filtered value", $results->First()->HiddenValue);
}
function testEndsWithFilterCaseInsensitive() {
$all = singleton("SearchContextTest_AllFilterTypes");
$context = $all->getDefaultSearchContext();
$params = array(
"EndsWith" => "IJKL", // spelled uppercase
);
$results = $context->getResults($params);
$this->assertEquals(1, $results->Count());
$this->assertEquals("Filtered value", $results->First()->HiddenValue);
}
}
class SearchContextTest_Person extends DataObject implements TestOnly {

View File

@ -63,6 +63,6 @@ SearchContextTest_AllFilterTypes:
Negation: Shouldnt match me
HiddenValue: Filtered value
CollectionMatch: ExistingCollectionValue
StartsWith: 12345-6789
StartsWith: 12345-6789 CamelCase
EndsWith: abcd-efgh-ijkl
FulltextField: one two three

View File

@ -7,4 +7,3 @@ cp ./tests/travis/_config.php $BUILD_DIR/mysite
cp -r . $BUILD_DIR/framework
cd $BUILD_DIR
./framework/sake dev/build "flush=1"

View File

@ -40,7 +40,7 @@
// Register buttons
ed.addButton('ssmacron', {
title : t.editor.translate('insertmacron'),
title : ed.getLang('tinymce_ssmacron.insertmacron'),
cmd : 'mceInsertMacron',
image : url + '/img/macron.png'
});

View File

@ -1 +0,0 @@
tinyMCE.addI18n('en.ssmacron',{'insertmacron': 'Insert a Macron'});

View File

@ -1 +0,0 @@
tinyMCE.addI18n('mi_NZ.ssmacron',{'insertmacron': 'T\u0101urua he tohut\u014D'});

View File

@ -0,0 +1,3 @@
tinyMCE.addI18n('en.tinymce_ssmacron', {
insertmacron: 'Insert a macron'
});

View File

@ -0,0 +1,3 @@
tinyMCE.addI18n('mi_NZ.tinymce_ssmacron', {
insertmacron: 'T\u0101urua he tohut\u014D'
});