diff --git a/Assets/File.php b/Assets/File.php index 6a5a6d2e5..e8325eb1a 100644 --- a/Assets/File.php +++ b/Assets/File.php @@ -15,7 +15,8 @@ use SilverStripe\Forms\DatetimeField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\HiddenField; -use SilverStripe\Forms\HTMLReadonlyField; +use SilverStripe\Forms\Tab; +use SilverStripe\Forms\TabSet; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\ReadonlyField; use SilverStripe\Forms\TextField; @@ -472,36 +473,36 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb public function getCMSFields() { $path = '/' . dirname($this->getFilename()); - $fields = FieldList::create([ - HeaderField::create('TitleHeader', $this->Title, 1), - LiteralField::create("ImageFull", $this->PreviewThumbnail()), - TextField::create("Name", $this->fieldLabel('Filename')), - ReadonlyField::create( - "Path", - _t('AssetTableField.PATH', 'Path'), - (($path !== '/.') ? $path : '') . '/' - ) - ]); + $width = (int)Image::config()->get('asset_preview_width'); + $previewLink = Convert::raw2att($this->ScaleMaxWidth($width)->getIcon()); + $image = ""; - if ($this->getIsImage()) { - $fields->push(ReadonlyField::create( - "DisplaySize", - _t('AssetTableField.SIZE', "File size"), - sprintf('%spx, %s', $this->getDimensions(), $this->getSize()) - )); - $fields->push(HTMLReadonlyField::create( - 'ClickableURL', - _t('AssetTableField.URL','URL'), - sprintf('%s', $this->Link(), $this->Link()) - )); - } - $fields->push(HiddenField::create('ID', $this->ID)); + $content = Tab::create('Main', + HeaderField::create('TitleHeader', $this->Title, 1) + ->addExtraClass('editor__heading'), + LiteralField::create("IconFull", $image) + ->addExtraClass('editor__file-preview'), + TabSet::create('Editor', + Tab::create('Details', + TextField::create("Title", $this->fieldLabel('Title')), + TextField::create("Name", $this->fieldLabel('Filename')), + ReadonlyField::create( + "Path", + _t('AssetTableField.PATH', 'Path'), + (($path !== '/.') ? $path : '') . '/' + ) + ), + Tab::create('Usage', + DatetimeField::create( + "LastEdited", + _t('AssetTableField.LASTEDIT', 'Last changed') + )->setReadonly(true) + ) + ), + HiddenField::create('ID', $this->ID) + ); - $fields->insertBefore('Name', TextField::create("Title", $this->fieldLabel('Title'))); - $fields->push(DatetimeField::create( - "LastEdited", - _t('AssetTableField.LASTEDIT', 'Last changed') - )->setReadonly(true)); + $fields = FieldList::create(TabSet::create('Root', $content)); $this->extend('updateCMSFields', $fields); diff --git a/Assets/Folder.php b/Assets/Folder.php index c5a61d6c1..bcbd460d8 100644 --- a/Assets/Folder.php +++ b/Assets/Folder.php @@ -11,6 +11,8 @@ use SilverStripe\Forms\TextField; use SilverStripe\ORM\DataList; use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\Versioning\Versioned; +use SilverStripe\Forms\Tab; +use SilverStripe\Forms\TabSet; /** * Represents a logical folder, which may be used to organise assets @@ -194,12 +196,24 @@ class Folder extends File { // Don't show readonly path until we can implement parent folder selection, // it's too confusing when readonly (makes sense for files only). - $fields = FieldList::create([ - HeaderField::create('TitleHeader', $this->Title, 1), - LiteralField::create("ImageFull", $this->PreviewThumbnail()), - TextField::create("Name", $this->fieldLabel('Filename')), + $width = (int)Image::config()->get('asset_preview_width'); + $previewLink = Convert::raw2att($this->ScaleMaxWidth($width)->getIcon()); + $image = ""; + + $content = Tab::create('Main', + HeaderField::create('TitleHeader', $this->Title, 1) + ->addExtraClass('editor__heading'), + LiteralField::create("IconFull", $image) + ->addExtraClass('editor__file-preview'), + TabSet::create('Editor', + Tab::create('Details', + TextField::create("Name", $this->fieldLabel('Filename')) + ) + ), HiddenField::create('ID', $this->ID) - ]); + ); + + $fields = FieldList::create(TabSet::create('Root', $content)); $this->extend('updateCMSFields', $fields); diff --git a/Assets/Image.php b/Assets/Image.php index 309ea8506..f6a64bfec 100644 --- a/Assets/Image.php +++ b/Assets/Image.php @@ -3,9 +3,18 @@ namespace SilverStripe\Assets; use SilverStripe\Core\Convert; -use SilverStripe\Forms\ReadonlyField; +use SilverStripe\Forms\HTMLReadonlyField; +use SilverStripe\Forms\LiteralField; use SilverStripe\View\Parsers\ShortcodeParser; use SilverStripe\View\Parsers\ShortcodeHandler; +use SilverStripe\Forms\Tab; +use SilverStripe\Forms\HeaderField; +use SilverStripe\Forms\TabSet; +use SilverStripe\Forms\TextField; +use SilverStripe\Forms\DatetimeField; +use SilverStripe\Forms\ReadonlyField; +use SilverStripe\Forms\HiddenField; +use SilverStripe\Forms\FieldList; /** * Represents an Image @@ -36,11 +45,64 @@ class Image extends File implements ShortcodeHandler { } public function getCMSFields() { - $fields = parent::getCMSFields(); - $fields->insertAfter( - 'LastEdited', - new ReadonlyField("Dimensions", _t('AssetTableField.DIM','Dimensions') . ':') + $path = '/' . dirname($this->getFilename()); + + $width = (int)Image::config()->get('asset_preview_width'); + $height = (int)Image::config()->get('asset_preview_height'); + $previewLink = Convert::raw2att($this + ->FitMax($width, $height) + ->PreviewLink() ); + $image = ""; + + $link = $this->Link(); + + $content = Tab::create('Main', + HeaderField::create('TitleHeader', $this->Title, 1) + ->addExtraClass('editor__heading'), + LiteralField::create("ImageFull", $image) + ->addExtraClass('editor__file-preview'), + TabSet::create('Editor', + Tab::create('Details', + TextField::create("Title", $this->fieldLabel('Title')), + TextField::create("Name", $this->fieldLabel('Filename')), + ReadonlyField::create( + "Path", + _t('AssetTableField.PATH', 'Path'), + (($path !== '/.') ? $path : '') . '/' + ), + HTMLReadonlyField::create( + 'ClickableURL', + _t('AssetTableField.URL','URL'), + sprintf('%s', + 'icon font-icon-link editor__url-icon', $link, $link) + ) + ), + Tab::create('Usage', + DatetimeField::create( + "LastEdited", + _t('AssetTableField.LASTEDIT', 'Last changed') + )->setReadonly(true) + ) + ), + HiddenField::create('ID', $this->ID) + ); + + if ($dimensions = $this->getDimensions()) { + $content->insertAfter( + 'TitleHeader', + LiteralField::create( + "DisplaySize", + sprintf('
%spx, %s
', + $dimensions, $this->getSize()) + ) + ); + } + + $fields = FieldList::create(TabSet::create('Root', $content)); + + $this->extend('updateCMSFields', $fields); + return $fields; } diff --git a/Assets/ImageManipulation.php b/Assets/ImageManipulation.php index 83a30ed63..050030079 100644 --- a/Assets/ImageManipulation.php +++ b/Assets/ImageManipulation.php @@ -156,13 +156,19 @@ trait ImageManipulation { /** * The width of an image preview in the Asset section * - * This thumbnail is only sized to width. - * * @config * @var int */ private static $asset_preview_width = 400; + /** + * The height of an image preview in the Asset section + * + * @config + * @var int + */ + private static $asset_preview_height = 336; + /** * Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad. * diff --git a/Assets/Storage/DBFile.php b/Assets/Storage/DBFile.php index aaccd8180..aaed8c935 100644 --- a/Assets/Storage/DBFile.php +++ b/Assets/Storage/DBFile.php @@ -12,6 +12,7 @@ use SilverStripe\ORM\ValidationResult; use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\FieldType\DBComposite; use SilverStripe\Security\Permission; +use SilverStripe\Core\Convert; /** * Represents a file reference stored in a database @@ -528,4 +529,27 @@ class DBFile extends DBComposite implements AssetContainer, Thumbnail { ->getStore() ->canView($this->Filename, $this->Hash); } + + /** + * Generates the URL for this DBFile preview, this is particularly important for images that + * have been manipulated e.g. by {@link ImageManipulation} + * Use the 'updatePreviewLink' extension point to customise the link. + * + * @param null $action + * @return bool|string + */ + public function PreviewLink($action = null) { + // Since AbsoluteURL can whitelist protected assets, + // do permission check first + if (!$this->failover->canView()) { + return false; + } + if ($this->getIsImage()) { + $link = $this->getAbsoluteURL(); + } else { + $link = Convert::raw2att($this->failover->getIcon()); + } + $this->extend('updatePreviewLink', $link, $action); + return $link; + } } diff --git a/Forms/CompositeField.php b/Forms/CompositeField.php index 99ea7b631..47170e0ef 100644 --- a/Forms/CompositeField.php +++ b/Forms/CompositeField.php @@ -76,6 +76,10 @@ class CompositeField extends FormField { } $defaults['children'] = $childSchema; } + + $defaults['data']['tag'] = $this->getTag(); + $defaults['data']['legend'] = $this->getLegend(); + return $defaults; } @@ -97,6 +101,35 @@ class CompositeField extends FormField { return $this->children; } + /** + * Returns the name (ID) for the element. + * If the CompositeField doesn't have a name, but we still want the ID/name to be set. + * This code generates the ID from the nested children. + * + * @todo this is temporary, and should be removed when FormTemplateHelper is updated to handle ID for CompositeFields with no name + * + * @return String $name + */ + public function getName(){ + if($this->name) { + return $this->name; + } + + $fieldList = $this->FieldList(); + $compositeTitle = ''; + $count = 0; + /** @var FormField $subfield */ + foreach($fieldList as $subfield){ + $compositeTitle .= $subfield->getName(); + if($subfield->getName()) $count++; + } + /** @skipUpgrade */ + if($count === 1) { + $compositeTitle .= 'Group'; + } + return preg_replace("/[^a-zA-Z0-9]+/", "", $compositeTitle); + } + /** * @param FieldList $children * @return $this diff --git a/Forms/FieldGroup.php b/Forms/FieldGroup.php index d2b98b48f..02ada59d1 100644 --- a/Forms/FieldGroup.php +++ b/Forms/FieldGroup.php @@ -103,6 +103,9 @@ class FieldGroup extends CompositeField { * Returns the name (ID) for the element. * In some cases the FieldGroup doesn't have a title, but we still want * the ID / name to be set. This code, generates the ID from the nested children + * + * TODO this is temporary, and should be removed when FormTemplateHelper is updated to handle ID + * for CompositeFields with no name */ public function getName(){ if($this->name) { @@ -110,17 +113,7 @@ class FieldGroup extends CompositeField { } if(!$this->title) { - $fs = $this->FieldList(); - $compositeTitle = ''; - $count = 0; - foreach($fs as $subfield){ - /** @var FormField $subfield */ - $compositeTitle .= $subfield->getName(); - if($subfield->getName()) $count++; - } - /** @skipUpgrade */ - if($count == 1) $compositeTitle .= 'Group'; - return preg_replace("/[^a-zA-Z0-9]+/", "", $compositeTitle); + return parent::getName(); } return preg_replace("/[^a-zA-Z0-9]+/", "", $this->title); diff --git a/Forms/GroupedDropdownField.php b/Forms/GroupedDropdownField.php index 3ecd710dd..7634fc850 100644 --- a/Forms/GroupedDropdownField.php +++ b/Forms/GroupedDropdownField.php @@ -53,6 +53,9 @@ use SilverStripe\View\ArrayData; */ class GroupedDropdownField extends DropdownField { + // TODO remove this when GroupedDropdownField is implemented + protected $schemaDataType = 'GroupedDropdownField'; + /** * Build a potentially nested fieldgroup * diff --git a/Forms/MoneyField.php b/Forms/MoneyField.php index 8c8c52521..13d4734df 100644 --- a/Forms/MoneyField.php +++ b/Forms/MoneyField.php @@ -16,7 +16,8 @@ use SilverStripe\ORM\DataObjectInterface; */ class MoneyField extends FormField { - protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT; + // TODO replace with `FormField::SCHEMA_DATA_TYPE_TEXT` when MoneyField is implemented + protected $schemaDataType = 'MoneyField'; /** * @var string $_locale diff --git a/Forms/Schema/FormSchema.php b/Forms/Schema/FormSchema.php index dd50b8716..17bd3b8c7 100644 --- a/Forms/Schema/FormSchema.php +++ b/Forms/Schema/FormSchema.php @@ -35,13 +35,13 @@ class FormSchema { 'actions' => [] ]; + /** @var FormField $action */ foreach ($form->Actions() as $action) { - /** @var FormField $action */ $schema['actions'][] = $action->getSchemaData(); } + /** @var FormField $field */ foreach ($form->Fields() as $field) { - /** @var FormField $field */ $schema['fields'][] = $field->getSchemaData(); } @@ -81,7 +81,7 @@ class FormSchema { if ($field instanceof CompositeField) { $subFields = $field->FieldList(); - array_merge($states, $this->getFieldStates($subFields)); + $states = array_merge($states, $this->getFieldStates($subFields)); } } return $states; diff --git a/Forms/Tab.php b/Forms/Tab.php index 3ae27b7a7..e39bcd4b1 100644 --- a/Forms/Tab.php +++ b/Forms/Tab.php @@ -21,6 +21,13 @@ use InvalidArgumentException; */ class Tab extends CompositeField { + /** + * Use custom react component + * + * @var string + */ + protected $schemaComponent = 'TabItem'; + /** * @var TabSet */ @@ -115,7 +122,9 @@ class Tab extends CompositeField { } public function extraClass() { - return implode(' ', (array)$this->extraClasses); + $classes = (array)$this->extraClasses; + + return implode(' ', $classes); } public function getAttributes() { diff --git a/Forms/TabSet.php b/Forms/TabSet.php index d340201ae..5e9da7190 100644 --- a/Forms/TabSet.php +++ b/Forms/TabSet.php @@ -32,6 +32,13 @@ use InvalidArgumentException; */ class TabSet extends CompositeField { + /** + * Use custom react component + * + * @var string + */ + protected $schemaComponent = 'Tabs'; + /** * @var TabSet */ diff --git a/ORM/Versioning/ChangeSet.php b/ORM/Versioning/ChangeSet.php index 922af2f02..d57bac358 100644 --- a/ORM/Versioning/ChangeSet.php +++ b/ORM/Versioning/ChangeSet.php @@ -369,11 +369,13 @@ class ChangeSet extends DataObject { } public function getCMSFields() { - $fields = new FieldList(); - $fields->push(TextField::create('Name', $this->fieldLabel('Name'))); - if($this->isInDB()) { - $fields->push(ReadonlyField::create('State', $this->fieldLabel('State'))); - } + $fields = parent::getCMSFields(); + + $fields->removeByName('OwnerID'); + $fields->removeByName('Changes'); + + $fields->dataFieldByName('State')->setReadonly(true); + $this->extend('updateCMSFields', $fields); return $fields; } diff --git a/admin/client/src/boot/index.js b/admin/client/src/boot/index.js index 980467641..309165a91 100644 --- a/admin/client/src/boot/index.js +++ b/admin/client/src/boot/index.js @@ -20,6 +20,9 @@ import PopoverField from 'components/PopoverField/PopoverField'; import HeaderField from 'components/HeaderField/HeaderField'; import LiteralField from 'components/LiteralField/LiteralField'; import HtmlReadonlyField from 'components/HtmlReadonlyField/HtmlReadonlyField'; +import CompositeField from 'components/CompositeField/CompositeField'; +import Tabs from 'components/Tabs/Tabs'; +import TabItem from 'components/Tabs/TabItem'; import { routerReducer } from 'react-router-redux'; // Sections @@ -43,6 +46,9 @@ function appBoot() { injector.register('HeaderField', HeaderField); injector.register('LiteralField', LiteralField); injector.register('HtmlReadonlyField', HtmlReadonlyField); + injector.register('CompositeField', CompositeField); + injector.register('Tabs', Tabs); + injector.register('TabItem', TabItem); injector.register('FormAction', FormAction); const initialState = {}; diff --git a/admin/client/src/components/CompositeField/CompositeField.js b/admin/client/src/components/CompositeField/CompositeField.js new file mode 100644 index 000000000..da1449d2c --- /dev/null +++ b/admin/client/src/components/CompositeField/CompositeField.js @@ -0,0 +1,36 @@ +import React from 'react'; +import SilverStripeComponent from 'lib/SilverStripeComponent'; + +class CompositeField extends SilverStripeComponent { + getLegend() { + return ( + this.props.data.tag === 'fieldset' && + this.props.data.legend && + {this.props.data.legend} + ); + } + + render() { + const legend = this.getLegend(); + const Tag = this.props.data.tag; + + return ( + + {legend} + {this.props.children} + + ); + } +} + +CompositeField.propTypes = { + tag: React.PropTypes.string, + legend: React.PropTypes.string, + extraClass: React.PropTypes.string, +}; + +CompositeField.defaultProps = { + tag: 'div', +}; + +export default CompositeField; diff --git a/admin/client/src/components/CompositeField/README.md b/admin/client/src/components/CompositeField/README.md new file mode 100644 index 000000000..7e96dbe8a --- /dev/null +++ b/admin/client/src/components/CompositeField/README.md @@ -0,0 +1,18 @@ +# CompositeField + +For containing groups of fields in a container element. + +## Example + +``` + + + + +``` + +## Properties + + * `tag` (string): The element type the composite field should use in HTML. + * `legend` (boolean): A label/legend for the group of fields contained. + * `extraClass` (string): Extra classes the CompositeField should have. diff --git a/admin/client/src/components/FieldHolder/FieldHolder.js b/admin/client/src/components/FieldHolder/FieldHolder.js index 68e0fb867..579370954 100644 --- a/admin/client/src/components/FieldHolder/FieldHolder.js +++ b/admin/client/src/components/FieldHolder/FieldHolder.js @@ -22,7 +22,7 @@ function fieldHolder(Field) { return (
{labelText && -