Improvements in the table widget, added new events to the CMS core, form styling, added support for the Table widget in in the CMS area.

This commit is contained in:
alekseybobkov 2015-01-07 22:21:52 -08:00
commit c26545913c
67 changed files with 478 additions and 204 deletions

View File

@ -1,4 +1,7 @@
* **Build 17x** (2014-12-xx)
* **Build 17x** (2015-01-xx)
- The variable `errors` will be included in a CMS page when redirecting via `Redirect::withErrors($validator)`.
* **Build 174** (2015-01-05)
- Improved asset caching (`cms.enableAssetCache`), when enabled the server will send a *304 Not Modified* header.
- Introduced new *Table* widget and *DataTable* form widget.
- There is now a simpler way for sending mail via `Mail::sendTo()`.

View File

@ -96,4 +96,16 @@ return array(
*/
'twigNoCache' => true,
/*
|--------------------------------------------------------------------------
| Convert Line Endings
|--------------------------------------------------------------------------
|
| Determines if October should convert line endings from the windows style
| \r\n to the unix style \n.
|
*/
'convertLineEndings' => true,
);

View File

@ -9013,6 +9013,7 @@ label {
}
.help-block {
font-size: 12px;
margin-bottom: 0;
}
.help-block.before-field {
margin-top: 0;

View File

@ -250,8 +250,7 @@
if ($('> li > a', this.$tabsContainer).length == 0)
this.$el.trigger('afterAllClosed.oc.tab')
this.$el.trigger('closed.oc.tab')
this.$el.trigger('closed.oc.tab', [$tab])
$(window).trigger('resize')
this.updateClasses()

View File

@ -187,6 +187,7 @@ label {
.help-block {
font-size: 12px;
margin-bottom: 0;
&.before-field {
margin-top: 0;
}

View File

@ -15,7 +15,6 @@ use Backend\Classes\ControllerBehavior;
*/
class ListController extends ControllerBehavior
{
/**
* @var array List definitions, keys for alias and value for configuration.
*/

View File

@ -13,7 +13,6 @@ use October\Rain\Router\Helper as RouterHelper;
*/
class BackendHelper
{
/**
* Returns a URL in context of the Backend
*/

View File

@ -395,4 +395,54 @@ class FormField
return Str::evalHtmlId($id);
}
/**
* Returns this fields value from a supplied data set, which can be
* an array or a model or another generic collection.
* @param mixed $data
* @return mixed
*/
public function getValueFromData($data, $default = null)
{
$fieldName = $this->fieldName;
/*
* Array field name, eg: field[key][key2][key3]
*/
$keyParts = Str::evalHtmlArray($fieldName);
$lastField = end($keyParts);
$result = $data;
/*
* Loop the field key parts and build a value.
* To support relations only the last field should return the
* relation value, all others will look up the relation object as normal.
*/
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
if ($key == $lastField) {
$result = $result->getRelationValue($key) ?: $default;
}
else {
$result = $result->{$key};
}
}
elseif (is_array($result)) {
if (!array_key_exists($key, $result)) {
return $default;
}
$result = $result[$key];
}
else {
if (!isset($result->{$key})) {
return $default;
}
$result = $result->{$key};
}
}
return $result;
}
}

View File

@ -16,7 +16,6 @@ use ArrayAccess;
*/
class FormTabs implements IteratorAggregate, ArrayAccess
{
const SECTION_OUTSIDE = 'outside';
const SECTION_PRIMARY = 'primary';
const SECTION_SECONDARY = 'secondary';
@ -176,5 +175,4 @@ class FormTabs implements IteratorAggregate, ArrayAccess
{
return isset($this->fields[$offset]) ? $this->fields[$offset] : null;
}
}
}

View File

@ -11,7 +11,6 @@ use Str;
*/
abstract class FormWidgetBase extends WidgetBase
{
/**
* @var FormField Object containing general form field information.
*/
@ -88,11 +87,11 @@ abstract class FormWidgetBase extends WidgetBase
}
/**
* Process the postback data for this widget.
* Process the postback value for this widget.
* @param $value The existing value for this widget.
* @return string The new value for this widget.
*/
public function getSaveData($value)
public function getSaveValue($value)
{
return $value;
}
@ -102,25 +101,19 @@ abstract class FormWidgetBase extends WidgetBase
* supports nesting via HTML array.
* @return string
*/
public function getLoadData()
public function getLoadValue()
{
list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom);
if (!is_null($model)) {
return $model->{$attribute};
}
return null;
return $this->formField->getValueFromData($this->model);
}
/**
* Returns the final model and attribute name of
* a nested HTML array attribute.
* Eg: list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom);
* Eg: list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
* @param string $attribute.
* @return array
*/
public function getModelArrayAttribute($attribute)
public function resolveModelAttribute($attribute)
{
$model = $this->model;
$parts = Str::evalHtmlArray($attribute);
@ -132,4 +125,5 @@ abstract class FormWidgetBase extends WidgetBase
return [$model, $last];
}
}

View File

@ -9,7 +9,6 @@
*/
class ListColumn
{
/**
* @var string List column name.
*/

View File

@ -13,7 +13,6 @@ use Session;
*/
abstract class WidgetBase
{
use \System\Traits\AssetMaker;
use \System\Traits\ConfigMaker;
use \System\Traits\ViewMaker;

View File

@ -2,12 +2,10 @@
use Str;
use File;
use Lang;
use Closure;
use October\Rain\Support\Yaml;
use Illuminate\Container\Container;
use System\Classes\PluginManager;
use System\Classes\SystemException;
/**
* Widget manager
@ -57,34 +55,6 @@ class WidgetManager
$this->pluginManager = PluginManager::instance();
}
/**
* Makes a widget object with configuration set.
* @param string $className A widget class name.
* @param Controller $controller The Backend controller that spawned this widget.
* @param array $configuration Configuration values.
* @return WidgetBase The widget object.
*/
public function makeWidget($className, $controller = null, $configuration = null)
{
/*
* Build configuration
*/
if ($configuration === null) {
$configuration = [];
}
/*
* Create widget object
*/
if (!class_exists($className)) {
throw new SystemException(Lang::get('backend::lang.widget.not_registered', [
'name' => $className
]));
}
return new $className($controller, $configuration);
}
//
// Form Widgets
//

View File

@ -21,7 +21,6 @@ use Exception;
*/
class AccessLogs extends Controller
{
public $implement = [
'Backend.Behaviors.ListController'
];

View File

@ -14,7 +14,6 @@ use Backend\Models\EditorPreferences as EditorPreferencesModel;
*/
class EditorPreferences extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
];

View File

@ -106,7 +106,7 @@ class CodeEditor extends FormWidgetBase
$this->vars['stretch'] = $this->formField->stretch;
$this->vars['size'] = $this->formField->size;
$this->vars['name'] = $this->formField->getName();
$this->vars['value'] = $this->getLoadData();
$this->vars['value'] = $this->getLoadValue();
}
/**

View File

@ -55,7 +55,7 @@ class ColorPicker extends FormWidgetBase
public function prepareVars()
{
$this->vars['name'] = $this->formField->getName();
$this->vars['value'] = $value = $this->getLoadData();
$this->vars['value'] = $value = $this->getLoadValue();
$this->vars['availableColors'] = $this->availableColors;
$this->vars['isCustomColor'] = !in_array($value, $this->availableColors);
}
@ -74,7 +74,7 @@ class ColorPicker extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
return strlen($value) ? $value : null;
}

View File

@ -69,7 +69,7 @@ class DataGrid extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
return json_decode($value);
}

View File

@ -69,7 +69,7 @@ class DataTable extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
// TODO: provide a streaming implementation of saving
// data to the model. The current implementation returns
@ -96,7 +96,7 @@ class DataTable extends FormWidgetBase
// data from the model. The current implementation loads
// all records at once. -ab
$records = $this->getLoadData() ?: [];
$records = $this->getLoadValue() ?: [];
$dataSource->initRecords((array) $records);
}
@ -104,7 +104,13 @@ class DataTable extends FormWidgetBase
{
$config = $this->makeConfig((array) $this->config);
$config->dataSource = 'client';
$config->alias = $this->alias . 'Table';
// It's safe to use the field name as an alias
// as field names do not repeat in forms. This
// approach lets to access the table data by the
// field name in POST requests directly (required
// in some edge cases).
$config->alias = $this->fieldName;
$table = new Table($this->controller, $config);

View File

@ -62,7 +62,7 @@ class DatePicker extends FormWidgetBase
$this->vars['timeName'] = self::TIME_PREFIX.$this->formField->getName(false);
$this->vars['timeValue'] = null;
if ($value = $this->getLoadData()) {
if ($value = $this->getLoadValue()) {
/*
* Date / Time
@ -120,7 +120,7 @@ class DatePicker extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
if (!strlen($value)) {
return null;

View File

@ -109,7 +109,7 @@ class FileUpload extends FormWidgetBase
*/
protected function getRelationObject()
{
list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom);
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
return $model->{$attribute}();
}
@ -120,7 +120,7 @@ class FileUpload extends FormWidgetBase
*/
protected function getRelationType()
{
list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom);
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
return $model->getRelationType($attribute);
}
@ -195,7 +195,7 @@ class FileUpload extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
return FormField::NO_SAVE_DATA;
}

View File

@ -130,7 +130,7 @@ class RecordFinder extends FormWidgetBase
public function onRefresh()
{
list($model, $attribute) = $this->getModelArrayAttribute($this->valueFrom);
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
$model->{$attribute} = post($this->formField->getName());
$this->prepareVars();
@ -142,8 +142,7 @@ class RecordFinder extends FormWidgetBase
*/
public function prepareVars()
{
// This should be a relation and return a Model
$this->relationModel = $this->getLoadData();
$this->relationModel = $this->getLoadValue();
$this->vars['value'] = $this->getKeyValue();
$this->vars['field'] = $this->formField;
@ -165,11 +164,25 @@ class RecordFinder extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
return strlen($value) ? $value : null;
}
/**
* {@inheritDoc}
*/
public function getLoadValue()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
if (!is_null($model)) {
return $model->{$attribute};
}
return null;
}
public function getKeyValue()
{
if (!$this->relationModel) {

View File

@ -104,7 +104,7 @@ class Relation extends FormWidgetBase
$field = clone $this->formField;
list($model, $attribute) = $this->getModelArrayAttribute($this->relationName);
list($model, $attribute) = $this->resolveModelAttribute($this->relationName);
$relatedObj = $model->makeRelation($attribute);
$query = $model->{$attribute}()->newQuery();
@ -141,7 +141,7 @@ class Relation extends FormWidgetBase
/**
* {@inheritDoc}
*/
public function getSaveData($value)
public function getSaveValue($value)
{
if (is_string($value) && !strlen($value)) {
return null;

View File

@ -41,7 +41,7 @@ class RichEditor extends FormWidgetBase
$this->vars['stretch'] = $this->formField->stretch;
$this->vars['size'] = $this->formField->size;
$this->vars['name'] = $this->formField->getName();
$this->vars['value'] = $this->getLoadData();
$this->vars['value'] = $this->getLoadValue();
}
/**

View File

@ -1,10 +1,9 @@
<?php
/*
/**
* Register Backend routes before all user routes.
*/
App::before(function ($request) {
/*
* Other pages
*/
@ -16,5 +15,4 @@ App::before(function ($request) {
* Entry point
*/
Route::any(Config::get('cms.backendUri', 'backend'), 'Backend\Classes\BackendController@run');
});

View File

@ -1,5 +1,6 @@
<?php namespace Backend\Traits;
use Lang;
use Backend\Classes\WidgetManager;
use System\Classes\SystemException;
@ -19,14 +20,20 @@ trait WidgetMaker
* Makes a widget object with the supplied configuration file.
* @param string $class Widget class name
* @param array $configuration An array of config.
* @return WidgetBase The widget or null
* @return WidgetBase The widget object
*/
public function makeWidget($class, $configuration = null)
public function makeWidget($class, $configuration = [])
{
$controller = ($this->controller) ?: $this;
$controller = property_exists($this, 'controller') && $this->controller
? $this->controller
: $this;
$manager = WidgetManager::instance();
$widget = $manager->makeWidget($class, $controller, $configuration);
return $widget;
if (!class_exists($class)) {
throw new SystemException(Lang::get('backend::lang.widget.not_registered', [
'name' => $class
]));
}
return new $class($controller, $configuration);
}
}

View File

@ -709,47 +709,11 @@ class Form extends WidgetBase
$field = $this->fields[$field];
}
$fieldName = $field->fieldName;
$defaultValue = (!$this->model->exists && $field->defaults !== '') ? $field->defaults : null;
$defaultValue = (!$this->model->exists && $field->defaults !== '')
? $field->defaults
: null;
/*
* Array field name, eg: field[key][key2][key3]
*/
$keyParts = Str::evalHtmlArray($fieldName);
$lastField = end($keyParts);
$result = $this->data;
/*
* Loop the field key parts and build a value.
* To support relations only the last field should return the
* relation value, all others will look up the relation object as normal.
*/
foreach ($keyParts as $key) {
if ($result instanceof Model && $result->hasRelation($key)) {
if ($key == $lastField) {
$result = $result->getRelationValue($key) ?: $defaultValue;
}
else {
$result = $result->{$key};
}
}
elseif (is_array($result)) {
if (!array_key_exists($key, $result)) {
return $defaultValue;
}
$result = $result[$key];
}
else {
if (!isset($result->{$key})) {
return $defaultValue;
}
$result = $result->{$key};
}
}
return $result;
return $field->getValueFromData($this->data, $defaultValue);
}
/**
@ -806,7 +770,16 @@ class Form extends WidgetBase
$parts = Str::evalHtmlArray($field);
$dotted = implode('.', $parts);
$widgetValue = $widget->getSaveData(array_get($data, $dotted));
$widgetValue = $widget->getSaveValue(array_get($data, $dotted));
/*
* @deprecated Remove if year >= 2016
*/
if (method_exists($widget, 'getSaveData')) {
traceLog('Method getSaveData() is deprecated, use getSaveValue() instead. Found in: ' . get_class($widget), 'warning');
$widgetValue = $widget->getSaveData(array_get($data, $dotted));
}
array_set($data, $dotted, $widgetValue);
}

View File

@ -63,7 +63,10 @@ class Table extends WidgetBase
$this->dataSource = new $dataSourceClass($this->recordsKeyFrom);
if (Request::method() == 'POST' && $this->isClientDataSource()) {
$requestDataField = $this->alias.'TableData';
if ( strpos($this->alias, '[') === false )
$requestDataField = $this->alias.'TableData';
else
$requestDataField = $this->alias.'[TableData]';
if (Request::exists($requestDataField)) {
// Load data into the client memory data source on POST
@ -135,7 +138,7 @@ class Table extends WidgetBase
}
/**
* Converts the columns associative array to a regular array.
* Converts the columns associative array to a regular array and translates column headers and drop-down options.
* Working with regular arrays is much faster in JavaScript.
* References:
* - http://www.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/
@ -147,6 +150,15 @@ class Table extends WidgetBase
foreach ($this->columns as $key=>$data) {
$data['key'] = $key;
if (isset($data['title']))
$data['title'] = trans($data['title']);
if (isset($data['options'])) {
foreach ($data['options'] as &$option)
$option = trans($option);
}
$result[] = $data;
}

View File

@ -2,13 +2,19 @@
* General control styling
*/
.control-table .table-container {
border: 1px solid #808c8d;
border: 1px solid #e0e0e0;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
overflow: hidden;
margin-bottom: 15px;
}
.control-table .table-container:last-child {
margin-bottom: 0;
}
.control-table.active .table-container {
border-color: #808c8d;
}
.control-table table {
width: 100%;
border-collapse: collapse;
@ -35,7 +41,7 @@
left: 1px;
right: 1px;
margin-top: -1px;
border-bottom: 1px solid #bdc3c7;
border-bottom: 1px solid #e0e0e0;
}
.control-table table.headers th {
padding: 7px 10px;
@ -55,9 +61,11 @@
.control-table table.headers th:last-child {
border-right: none;
}
.control-table.active table.headers:after {
border-bottom-color: #808c8d;
}
.control-table table.data td {
border: 1px solid #ecf0f1;
/* TODO: this should be applied only when the control is active */
}
.control-table table.data td .content-container {
position: relative;
@ -114,7 +122,7 @@
}
.control-table .toolbar {
background: white;
border-bottom: 1px solid #bdc3c7;
border-bottom: 1px solid #e0e0e0;
}
.control-table .toolbar a {
color: #323e50;
@ -144,6 +152,9 @@
.control-table .toolbar a.delete-table-row:before {
background-position: 0 -113px;
}
.control-table.active .toolbar {
border-bottom-color: #808c8d;
}
.control-table .pagination ul {
padding: 0;
margin-bottom: 15px;
@ -304,6 +315,7 @@ html.cssanimations .control-table td[data-column-type=dropdown] [data-view-conta
border-top: none;
padding-top: 1px;
overflow: hidden;
z-index: 1000;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;

View File

@ -65,6 +65,7 @@
// Event handlers
this.clickHandler = this.onClick.bind(this)
this.keydownHandler = this.onKeydown.bind(this)
this.documentClickHandler = this.onDocumentClick.bind(this)
this.toolbarClickHandler = this.onToolbarClick.bind(this)
if (this.options.postback && this.options.clientDataSourceClass == 'client')
@ -135,6 +136,7 @@
Table.prototype.registerHandlers = function() {
this.el.addEventListener('click', this.clickHandler)
this.el.addEventListener('keydown', this.keydownHandler)
document.addEventListener('click', this.documentClickHandler)
if (this.options.postback && this.options.clientDataSourceClass == 'client')
this.$el.closest('form').bind('oc.beforeRequest', this.formSubmitHandler)
@ -146,6 +148,8 @@
Table.prototype.unregisterHandlers = function() {
this.el.removeEventListener('click', this.clickHandler);
document.removeEventListener('click', this.documentClickHandler)
this.clickHandler = null
this.el.removeEventListener('keydown', this.keydownHandler);
@ -449,6 +453,8 @@
* Removes editor from the currently edited cell and commits the row if needed.
*/
Table.prototype.unfocusTable = function() {
this.elementRemoveClass(this.el, 'active')
if (this.activeCellProcessor)
this.activeCellProcessor.onUnfocus()
@ -461,6 +467,13 @@
this.activeCell = null
}
/*
* Makes the table focused in the UI
*/
Table.prototype.focusTable = function() {
this.elementAddClass(this.el, 'active')
}
/*
* Calls the onFocus() method for the cell processor responsible for the
* newly focused cell. Commit the previous edited row to the data source
@ -471,6 +484,8 @@
if (columnName === null)
return
this.focusTable()
var processor = this.getCellProcessor(columnName)
if (!processor)
throw new Error("Cell processor not found for the column "+columnName)
@ -615,6 +630,8 @@
// ============================
Table.prototype.onClick = function(ev) {
this.focusTable()
if (this.navigation.onClick(ev) === false)
return
@ -670,7 +687,12 @@
Table.prototype.onFormSubmit = function(ev, data) {
if (data.handler == this.options.postbackHandlerName) {
this.unfocusTable()
data.options.data[this.options.alias + 'TableData'] = this.dataSource.getAllData()
var fieldName = this.options.alias.indexOf('[') > -1 ?
this.options.alias + '[TableData]' :
this.options.alias + 'TableData';
data.options.data[fieldName] = this.dataSource.getAllData()
}
}
@ -693,6 +715,24 @@
this.stopEvent(ev)
}
Table.prototype.onDocumentClick = function(ev) {
var target = this.getEventTarget(ev)
// Determine if the click was inside the table element
// and just exit if so
if (this.parentContainsElement(this.el, target))
return
// Request the active cell processor if the clicked
// element belongs to any extra-table element created
// by the processor
if (this.activeCellProcessor && this.activeCellProcessor.elementBelongsToProcessor(target))
return
this.unfocusTable()
}
// PUBLIC METHODS
// ============================
@ -790,6 +830,44 @@
ev.returnValue = false
}
Table.prototype.elementHasClass = function(el, className) {
// TODO: refactor to a core library
if (el.classList)
return el.classList.contains(className);
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
}
Table.prototype.elementAddClass = function(el, className) {
// TODO: refactor to a core library
if (this.elementHasClass(el, className))
return
if (el.classList)
el.classList.add(className);
else
el.className += ' ' + className;
}
Table.prototype.elementRemoveClass = function(el, className) {
// TODO: refactor to a core library
if (el.classList)
el.classList.remove(className);
else
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
Table.prototype.parentContainsElement = function(parent, element) {
while (element && element != parent) {
element = element.parentNode
}
return element ? true : false
}
Table.prototype.getCellValue = function(cellElement) {
return cellElement.querySelector('[data-container]').value
}

View File

@ -167,5 +167,13 @@
return this.getViewContainer(cellElement).textContent = value
}
/*
* Determines whether the specified element is some element created by the
* processor.
*/
Base.prototype.elementBelongsToProcessor = function(element) {
return false
}
$.oc.table.processor.base = Base
}(window.jQuery);

View File

@ -246,7 +246,7 @@
DropdownProcessor.prototype.getAbsolutePosition = function(element) {
// TODO: refactor to a core library
var top = 0,
var top = document.body.scrollTop,
left = 0
do {
@ -344,7 +344,7 @@
/*
* This method is called when a cell value in the row changes.
*/
Base.prototype.onRowValueChanged = function(columnName, cellElement) {
DropdownProcessor.prototype.onRowValueChanged = function(columnName, cellElement) {
// Determine if this drop-down depends on the changed column
// and update the option list if necessary
@ -382,5 +382,16 @@
})
}
/*
* Determines whether the specified element is some element created by the
* processor.
*/
DropdownProcessor.prototype.elementBelongsToProcessor = function(element) {
if (!this.itemListElement)
return false
return this.tableObj.parentContainsElement(this.itemListElement, element)
}
$.oc.table.processor.dropdown = DropdownProcessor;
}(window.jQuery);

View File

@ -4,12 +4,23 @@
* General control styling
*/
@table-active-border: #808c8d;
@table-inactive-border: #e0e0e0;
.control-table {
.table-container {
border: 1px solid #808c8d;
border: 1px solid @table-inactive-border;
.border-radius(4px);
overflow: hidden;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
&.active .table-container {
border-color: @table-active-border;
}
table {
@ -41,7 +52,7 @@
left: 1px;
right: 1px;
margin-top: -1px;
border-bottom: 1px solid #bdc3c7;
border-bottom: 1px solid @table-inactive-border;
}
th {
@ -66,6 +77,10 @@
}
}
&.active table.headers:after {
border-bottom-color: @table-active-border;
}
table.data {
td {
border: 1px solid #ecf0f1;
@ -75,7 +90,6 @@
padding: 1px;
}
/* TODO: this should be applied only when the control is active */
&.active {
border-color: @color-focus!important;
@ -146,7 +160,7 @@
.toolbar {
background: white;
border-bottom: 1px solid #bdc3c7;
border-bottom: 1px solid @table-inactive-border;
a {
color: #323e50;
@ -180,6 +194,10 @@
}
}
&.active .toolbar {
border-bottom-color: @table-active-border;
}
.pagination {
ul {
padding: 0;
@ -354,10 +372,11 @@ html.cssanimations {
.user-select(none);
position: absolute;
background: white;
border: 1px solid #808c8d;
border: 1px solid @table-active-border;
border-top: none;
padding-top: 1px;
overflow: hidden;
z-index: 1000;
.box-sizing(border-box);
.border-bottom-radius(4px);

View File

@ -80,7 +80,7 @@
})
/*
* Listen for the closed event
* Listen for the closing events
*/
$('#cms-master-tabs').on('closed.oc.tab', function(event){
updateModifiedCounter()
@ -89,6 +89,11 @@
setPageTitle('')
})
$('#cms-master-tabs').on('beforeClose.oc.tab', function(event){
// Dispose data table widgets
$('[data-control=table]', event.relatedTarget).table('dispose')
})
/*
* Listen for the onBeforeRequest event
*/

View File

@ -16,7 +16,6 @@ use Exception;
*/
class CmsException extends ApplicationException
{
/**
* @var Cms\Classes\CmsCompoundObject A reference to a CMS object used for masking errors.
*/

View File

@ -10,7 +10,6 @@ use System\Classes\ApplicationException;
*/
class CmsObjectQuery
{
protected $useCache = false;
protected $cmsObject;

View File

@ -8,6 +8,7 @@ use View;
use Lang;
use Event;
use Config;
use Session;
use Request;
use Response;
use Exception;
@ -235,6 +236,13 @@ class Controller extends BaseController
'environment' => App::environment(),
];
/*
* Check for the presence of validation errors in the session.
*/
$this->vars['errors'] = (Config::get('session.driver') && Session::has('errors'))
? Session::get('errors')
: new \Illuminate\Support\ViewErrorBag;
/*
* Handle AJAX requests and execute the life cycle functions
*/

View File

@ -1,5 +1,7 @@
<?php namespace Cms\Classes;
use Str;
/**
* This class parses CMS object files (pages, partials and layouts).
* Returns the structured file information.
@ -72,6 +74,7 @@ class SectionParser
*/
public static function parseOffset($content)
{
$content = Str::normalizeEol($content);
$sections = preg_split('/^={2,}\s*/m', $content, -1);
$count = count($sections);

View File

@ -349,6 +349,8 @@ class Index extends Controller
throw new ApplicationException(trans('cms::lang.template.not_found'));
}
Event::fire('cms.template.processSettingsAfterLoad', [$this, $template]);
return $template;
}
@ -443,7 +445,13 @@ class Index extends Controller
if (array_key_exists('viewBag', $_POST))
$settings['viewBag'] = $_POST['viewBag'];
return $settings;
$dataHolder = (object)[
'settings' => $settings
];
Event::fire('cms.template.processSettingsBeforeSave', [$this, $dataHolder]);
return $dataHolder->settings;
}
/**

View File

@ -1,14 +1,12 @@
<?php
/*
/**
* Register CMS routes before all user routes.
*/
App::before(function ($request) {
/*
* The CMS module intercepts all URLs that were not
* The CMS module intercepts all URLs that were not
* handled by the back-end modules.
*/
Route::any('{slug}', 'Cms\Classes\Controller@run')->where('slug', '(.*)?');
});

View File

@ -1,6 +1,7 @@
<?php namespace System\Behaviors;
use Cache;
use DbDongle;
use System\Classes\ModelBehavior;
use System\Classes\ApplicationException;
@ -100,7 +101,7 @@ class SettingsModel extends ModelBehavior
*/
public function isConfigured()
{
return $this->getSettingsRecord() !== null;
return DbDongle::hasDatabase() && $this->getSettingsRecord() !== null;
}
/**

View File

@ -13,7 +13,6 @@ use Exception;
*/
class Controller extends BaseController
{
/**
* Combines JavaScript and StyleSheet assets.
* @param string $name Combined file code

View File

@ -19,7 +19,6 @@ use System\Classes\ApplicationException;
*/
class ErrorHandler
{
/**
* @var System\Classes\ExceptionBase A prepared mask exception used to mask any exception fired.
*/

View File

@ -16,7 +16,6 @@ use System\Classes\ApplicationException;
*/
class ExceptionBase extends Exception
{
/**
* @var Exception If this exception is acting as a mask, this property stores the face exception.
*/

View File

@ -12,7 +12,6 @@ use October\Rain\Database\ModelBehavior as ModelBehaviorBase;
*/
class ModelBehavior extends ModelBehaviorBase
{
/**
* @var array Properties that must exist in the model using this behavior.
*/
@ -29,7 +28,8 @@ class ModelBehavior extends ModelBehaviorBase
/*
* Validate model properties
*/
foreach ($this->requiredProperties as $property) {
foreach ($this->requiredProperties as $property)
{
if (!isset($model->{$property})) {
throw new ApplicationException(Lang::get('system::lang.behavior.missing_property', [
'class' => get_class($model),

View File

@ -211,7 +211,6 @@ class PluginManager
/**
* Returns the directory path to a plugin
*
*/
public function getPluginPath($id)
{
@ -220,7 +219,7 @@ class PluginManager
return null;
}
return $this->pathMap[$classId];
return File::normalizePath($this->pathMap[$classId]);
}
/**

View File

@ -1,5 +1,6 @@
<?php namespace System\Classes;
use App;
use Log;
/**
@ -14,6 +15,12 @@ class SystemException extends ExceptionBase
public function __construct($message = "", $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
Log::error($this);
/*
* Log the exception
*/
if (!App::runningUnitTests()) {
Log::error($this);
}
}
}

View File

@ -18,11 +18,9 @@ use Exception;
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*
*/
class EventLogs extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
'Backend.Behaviors.ListController'

View File

@ -17,11 +17,9 @@ use Exception;
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*
*/
class MailLayouts extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
];

View File

@ -20,11 +20,9 @@ use Exception;
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*
*/
class MailTemplates extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
'Backend.Behaviors.ListController'

View File

@ -18,11 +18,9 @@ use Exception;
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*
*/
class RequestLogs extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
'Backend.Behaviors.ListController'

View File

@ -11,7 +11,6 @@ use Model;
*/
class EventLog extends Model
{
/**
* @var string The database table used by the model.
*/

View File

@ -11,7 +11,6 @@ use Request;
*/
class RequestLog extends Model
{
/**
* @var string The database table used by the model.
*/

View File

@ -1,13 +1,11 @@
<?php
/*
/**
* Register System routes before all user routes.
*/
App::before(function ($request) {
/*
* Combine JavaScript and StyleSheet assets
*/
Route::any('combine/{file}', 'System\Classes\Controller@combine');
});

View File

@ -12,7 +12,7 @@
>
<testsuites>
<testsuite name="October CMS Test Suite">
<directory>./tests</directory>
<directory>./tests/unit</directory>
</testsuite>
<testsuite name="October Rain Test Suite">
<directory>./vendor/october/rain/tests</directory>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../../autoload.php"
bootstrap="../../bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"

View File

@ -5,23 +5,6 @@ use Backend\Classes\WidgetManager;
class WidgetManagerTest extends TestCase
{
public function testMakeWidget()
{
$manager = WidgetManager::instance();
$widget = $manager->makeWidget('Backend\Widgets\Search');
$this->assertTrue($widget instanceof \Backend\Widgets\Search);
$controller = new Controller;
$widget = $manager->makeWidget('Backend\Widgets\Search', $controller);
$this->assertInstanceOf('Backend\Widgets\Search', $widget);
$this->assertInstanceOf('Backend\Classes\Controller', $widget->getController());
$config = ['test' => 'config'];
$widget = $manager->makeWidget('Backend\Widgets\Search', null, $config);
$this->assertInstanceOf('Backend\Widgets\Search', $widget);
$this->assertEquals('config', $widget->getConfig('test'));
}
public function testListFormWidgets()
{
$manager = WidgetManager::instance();

View File

@ -0,0 +1,60 @@
<?php
use Backend\Classes\Controller;
class ExampleTraitClass
{
use \Backend\Traits\WidgetMaker;
public function __construct()
{
$this->controller = new Controller;
}
}
class WidgetMakerTest extends TestCase
{
/**
* The object under test.
*
* @var object
*/
private $traitObject;
/**
* Sets up the fixture.
*
* This method is called before a test is executed.
*
* @return void
*/
public function setUp()
{
$traitName = 'Backend\Traits\WidgetMaker';
$this->traitObject = $this->getObjectForTrait($traitName);
}
public function testTraitObject()
{
$maker = $this->traitObject;
$widget = $maker->makeWidget('Backend\Widgets\Search');
$this->assertTrue($widget instanceof \Backend\Widgets\Search);
}
public function testMakeWidget()
{
$manager = new ExampleTraitClass;
$controller = new Controller;
$widget = $manager->makeWidget('Backend\Widgets\Search');
$this->assertInstanceOf('Backend\Widgets\Search', $widget);
$this->assertInstanceOf('Backend\Classes\Controller', $widget->getController());
$config = ['test' => 'config'];
$widget = $manager->makeWidget('Backend\Widgets\Search', $config);
$this->assertInstanceOf('Backend\Widgets\Search', $widget);
$this->assertEquals('config', $widget->getConfig('test'));
}
}

View File

@ -162,7 +162,7 @@ class CmsCompoundObjectTest extends TestCase
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEquals($referenceFilePath, $destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
public function testSaveMarkupAndSettings()
@ -187,7 +187,7 @@ class CmsCompoundObjectTest extends TestCase
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEquals($referenceFilePath, $destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
public function testSaveFull()
@ -195,8 +195,9 @@ class CmsCompoundObjectTest extends TestCase
$theme = Theme::load('apitest');
$destFilePath = $theme->getPath().'/testobjects/compound.htm';
if (file_exists($destFilePath))
if (file_exists($destFilePath)) {
unlink($destFilePath);
}
$this->assertFileNotExists($destFilePath);
@ -213,6 +214,22 @@ class CmsCompoundObjectTest extends TestCase
$this->assertFileExists($referenceFilePath);
$this->assertFileExists($destFilePath);
$this->assertFileEquals($referenceFilePath, $destFilePath);
$this->assertFileEqualsNormalized($referenceFilePath, $destFilePath);
}
//
// Helpers
//
protected function assertFileEqualsNormalized($expected, $actual)
{
$expected = file_get_contents($expected);
$expected = preg_replace('~\R~u', PHP_EOL, $expected); // Normalize EOL
$actual = file_get_contents($actual);
$actual = preg_replace('~\R~u', PHP_EOL, $actual); // Normalize EOL
$this->assertEquals($expected, $actual);
}
}

View File

@ -141,7 +141,7 @@ class CmsObjectTest extends TestCase
/**
* @expectedException \System\Classes\ApplicationException
* @expectedExceptionMessage The property "something" cannot be set
* @expectedExceptionMessage The property 'something' cannot be set
*/
public function testFillNotFillable()
{

View File

@ -253,10 +253,22 @@ class CodeParserTest extends TestCase
$referenceFilePath = base_path().'/tests/fixtures/cms/reference/namespaces.php';
$this->assertFileExists($referenceFilePath);
$referenceContents = file_get_contents($referenceFilePath);
$referenceContents = $this->getContents($referenceFilePath);
$referenceContents = str_replace('{className}', $info['className'], $referenceContents);
$this->assertEquals($referenceContents, file_get_contents($info['filePath']));
$this->assertEquals($referenceContents, $this->getContents($info['filePath']));
}
//
// Helpers
//
protected function getContents($path)
{
$content = file_get_contents($path);
$content = preg_replace('~\R~u', PHP_EOL, $content); // Normalize EOL
return $content;
}
}

View File

@ -28,7 +28,7 @@ class FileHelperTest extends TestCase
$str = FileHelper::formatIniString($data);
$this->assertNotEmpty($str);
$this->assertEquals(file_get_contents($path), $str);
$this->assertEquals($this->getContents($path), $str);
$data = [
'section' => [
@ -47,7 +47,7 @@ class FileHelperTest extends TestCase
$this->assertFileExists($path);
$str = FileHelper::formatIniString($data);
$this->assertEquals(file_get_contents($path), $str);
$this->assertEquals($this->getContents($path), $str);
$data = [
'section' => [
@ -75,6 +75,18 @@ class FileHelperTest extends TestCase
$this->assertFileExists($path);
$str = FileHelper::formatIniString($data);
$this->assertEquals(file_get_contents($path), $str);
$this->assertEquals($this->getContents($path), $str);
}
//
// Helpers
//
protected function getContents($path)
{
$content = file_get_contents($path);
$content = preg_replace('~\R~u', PHP_EOL, $content); // Normalize EOL
return $content;
}
}

View File

@ -6,11 +6,10 @@ use Cms\Classes\Theme;
class RouterTest extends TestCase
{
protected static $theme = null;
public static function setUpBeforeClass()
{
self::$theme = new Theme();
self::$theme->load('test');
self::$theme = Theme::load('test');
}
protected static function getMethod($name)
@ -28,7 +27,7 @@ class RouterTest extends TestCase
$property->setAccessible(true);
return $property;
}
public function testLoadUrlMap()
{
$method = self::getMethod('loadUrlMap');

View File

@ -2,7 +2,7 @@
use Cms\Classes\Theme;
class ThemeTest extends TestCase
class ThemeTest extends TestCase
{
public function setUp()
{

18
tests/unit/phpunit.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="October Test Suite">
<directory>./</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -72,7 +72,8 @@ class PluginManagerTest extends TestCase
{
$manager = PluginManager::instance();
$result = $manager->getPluginPath('October\Tester');
$this->assertEquals(base_path() . '/tests/fixtures/system/plugins/october/tester', $result);
$basePath = str_replace('\\', '/', base_path());
$this->assertEquals($basePath . '/tests/fixtures/system/plugins/october/tester', $result);
}
public function testGetPlugins()

View File

@ -3,6 +3,7 @@ description = "Default layout"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>October CMS - {{ this.page.title }}</title>
<meta name="author" content="October CMS">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -60,4 +61,4 @@ description = "Default layout"
{% scripts %}
</body>
</html>
</html>