Skip to content

Commit

Permalink
NEW Show CMS field (top-level) tabs in more actions dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
ScopeyNZ committed Sep 6, 2018
1 parent 9462cb3 commit 55411b2
Show file tree
Hide file tree
Showing 14 changed files with 258 additions and 23 deletions.
6 changes: 6 additions & 0 deletions _config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ Symbiote\GridFieldExtensions\GridFieldAddNewMultiClassHandler:
SilverStripe\Core\Injector\Injector:
SilverStripe\CMS\Controllers\CMSSiteTreeFilter_Search:
class: DNADesign\Elemental\Controllers\ElementSiteTreeFilterSearch
Psr\SimpleCache\CacheInterface.ElementTabCache:
factory: SilverStripe\Core\Cache\CacheFactory
constructor:
namespace: "ElementTabCache"
DNADesign\Elemental\Services\ElementTabProvider:
cache: Psr\SimpleCache\CacheInterface.ElementTabCache
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

14 changes: 3 additions & 11 deletions client/src/components/ElementEditor/AddElementPopoverContent.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* global window */

import React, { Component, PropTypes } from 'react';
import { Button } from 'reactstrap';
import classNames from 'classnames';
import { elementTypeType } from 'types/elementTypeType';

/**
* The AddElementPopoverContent component used in the context of an ElementEditor shows the
Expand All @@ -28,7 +27,7 @@ class AddElementPopoverContent extends Component {
'element-editor-add-element-content__button'
)
}
key={elementType.ID}
key={elementType.name}
>
{elementType.title}
</Button>
Expand All @@ -47,14 +46,7 @@ class AddElementPopoverContent extends Component {
}
}
AddElementPopoverContent.propTypes = {
elementTypes: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
icon: PropTypes.string,
})),
};

AddElementPopoverContent.defaultProps = {

elementTypes: PropTypes.arrayOf(elementTypeType),
};

export default AddElementPopoverContent;
9 changes: 5 additions & 4 deletions client/src/components/ElementEditor/AddNewButton.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { InputGroup, Input, InputGroupAddon, Button } from 'reactstrap';
import i18n from 'i18n';
import { elementTypeType } from 'types/elementTypeType';

class AddNewButton extends Component {
constructor(props) {
Expand All @@ -20,7 +21,7 @@ class AddNewButton extends Component {
handleTypeChange(event) {
const type = event.target
? this.props.elementTypes.find(
candidateType => candidateType.value === event.target.value
candidateType => candidateType.name === event.target.value
) || null
: null;

Expand All @@ -35,7 +36,7 @@ class AddNewButton extends Component {
*/
renderAddButton() {
const { selectedType } = this.state;
const buttonHref = selectedType ? `${this.props.baseAddHref}/${selectedType.value}` : '#';
const buttonHref = selectedType ? `${this.props.baseAddHref}/${selectedType.name}` : '#';
const title = selectedType
? i18n.inject(i18n._t('ElementalAddNewButton.TITLE', 'Add a "{type}" block'), { type: selectedType.title })
: '';
Expand Down Expand Up @@ -67,7 +68,7 @@ class AddNewButton extends Component {
}

return elementTypes.map(type => (
<option key={type.value} value={type.value}>{type.title}</option>
<option key={type.name} value={type.name}>{type.title}</option>
));
}

Expand Down Expand Up @@ -97,7 +98,7 @@ class AddNewButton extends Component {
AddNewButton.defaultProps = {};
AddNewButton.propTypes = {
baseAddHref: PropTypes.string.isRequired,
elementTypes: PropTypes.array.isRequired,
elementTypes: PropTypes.arrayOf(elementTypeType).isRequired,
};

export default AddNewButton;
3 changes: 3 additions & 0 deletions client/src/components/ElementEditor/Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class Element extends Component {
HeaderComponent,
ContentComponent,
link,
editTabs,
} = this.props;

const { previewExpanded } = this.state;
Expand Down Expand Up @@ -118,6 +119,7 @@ class Element extends Component {
elementType={element.BlockSchema.type}
fontIcon={element.BlockSchema.iconClass}
link={link}
editTabs={editTabs}
caretClickCallback={this.handleExpand}
previewExpanded={previewExpanded}
expandable={element.InlineEditable}
Expand All @@ -136,6 +138,7 @@ class Element extends Component {
Element.propTypes = {
element: elementType,
link: PropTypes.string.isRequired,
editTabs: PropTypes.arrayOf(PropTypes.string),
};

Element.defaultProps = {
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/ElementEditor/ElementEditor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { PureComponent, PropTypes } from 'react';
import { inject } from 'lib/Injector';
import { elementTypeType } from 'types/elementTypeType';

/**
* The ElementEditor is used in the CMS to manage a list or nested lists of
Expand All @@ -19,7 +20,7 @@ class ElementEditor extends PureComponent {
}

ElementEditor.propTypes = {
elementTypes: PropTypes.array.isRequired,
elementTypes: PropTypes.arrayOf(elementTypeType).isRequired,
pageId: PropTypes.number.isRequired,
baseAddHref: PropTypes.string.isRequired,
};
Expand Down
23 changes: 22 additions & 1 deletion client/src/components/ElementEditor/ElementList.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@ import React, { Component, PropTypes } from 'react';
import { elementType } from 'types/elementType';
import { inject } from 'lib/Injector';
import AddElementPopoverContent from 'components/ElementEditor/AddElementPopoverContent';
import { elementTypeType } from 'types/elementTypeType';

class ElementList extends Component {
/**
* Given an elementType, return a list of tabs that should be available in the edit form for an
* element.
*
* @param {elementTypeType} element
* @returns {string[]}
*/
getEditTabs(element) {
const { elementTypes } = this.props;
const matchingType = elementTypes.find(type => element.BlockSchema.type === type.title);

if (!matchingType || !matchingType.tabs) {
return [];
}

return matchingType.tabs;
}

/**
* Renders a list of Element components, each with an elementType object
* of data mapped into it. The data is provided by a GraphQL HOC registered
Expand All @@ -16,10 +35,12 @@ class ElementList extends Component {
return null;
}


return blocks.map((element) => (
<ElementComponent
key={element.ID}
element={element}
editTabs={this.getEditTabs(element)}
link={element.BlockSchema.actions.edit}
/>
));
Expand Down Expand Up @@ -54,7 +75,7 @@ class ElementList extends Component {
ElementList.propTypes = {
// @todo support either ElementList or Element children in an array (or both)
blocks: PropTypes.arrayOf(elementType),
elementTypes: PropTypes.array.isRequired,
elementTypes: PropTypes.arrayOf(elementTypeType).isRequired,
loading: PropTypes.bool,
};

Expand Down
22 changes: 20 additions & 2 deletions client/src/components/ElementEditor/Header.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global confirm */

import React, { Component, PropTypes } from 'react';
import { Tooltip } from 'reactstrap';
import { Tooltip, DropdownItem } from 'reactstrap';
import { compose } from 'redux';
import { inject } from 'lib/Injector';
import archiveBlockMutation from 'state/editor/archiveBlockMutation';
Expand Down Expand Up @@ -155,7 +155,7 @@ class Header extends Component {
* @returns {ActionMenuComponent|null}
*/
renderActionsMenu() {
const { id, expandable, ActionMenuComponent } = this.props;
const { id, expandable, editTabs, ActionMenuComponent } = this.props;

// Don't show the menu when inline editing is not enabled
if (!expandable) {
Expand All @@ -171,6 +171,8 @@ class Header extends Component {
dropdownMenuProps={{ right: true }}
toggleCallback={(event) => event.stopPropagation()}
>
{ this.renderEditTabs() }
{ !editTabs || !editTabs.length || <DropdownItem divider /> }
<button
onClick={this.handleArchive}
title={archiveTitle}
Expand All @@ -185,6 +187,21 @@ class Header extends Component {
);
}

/**
* Render buttons for the edit form tabs that will be a part of the edit form (if they exist)
*
* @returns {DOMElement[]|null}
*/
renderEditTabs() {
const { editTabs } = this.props;

if (!editTabs || !editTabs.length) {
return null;
}

return editTabs.map((tab) => <button key={tab} className="dropdown-item">{tab}</button>);
}

/**
* Renders a message indicating the current versioned state of the element
*
Expand Down Expand Up @@ -281,6 +298,7 @@ Header.propTypes = {
isPublished: PropTypes.bool,
elementType: PropTypes.string,
fontIcon: PropTypes.string,
editTabs: PropTypes.arrayOf(PropTypes.string),
actions: PropTypes.shape({
handleArchiveBlock: PropTypes.func.isRequired,
handlePublishBlock: PropTypes.func,
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/ElementEditor/Toolbar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { PureComponent, PropTypes } from 'react';
import { inject } from 'lib/Injector';
import { elementTypeType } from 'types/elementTypeType';

// eslint-disable-next-line react/prefer-stateless-function
class Toolbar extends PureComponent {
Expand All @@ -15,7 +16,7 @@ class Toolbar extends PureComponent {

Toolbar.defaultProps = {};
Toolbar.propTypes = {
elementTypes: PropTypes.array.isRequired,
elementTypes: PropTypes.arrayOf(elementTypeType).isRequired,
baseAddHref: PropTypes.string.isRequired,
AddNewButtonComponent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ Enzyme.configure({ adapter: new Adapter() });
describe('ElementEditor', () => {
const ToolbarComponent = () => <div />;
const ListComponent = () => <div className="elemental-editor__list" />;
const testElementType = {
name: 'TestElement',
title: 'Test Block',
icon: 'nothing',
tabs: ['Content', 'History'],
};

describe('render()', () => {
it('should render ElementList and Toolbar', () => {
Expand All @@ -20,7 +26,7 @@ describe('ElementEditor', () => {
ListComponent={ListComponent}
pageId={8}
baseAddHref="#"
elementTypes={['TestElement']}
elementTypes={[testElementType]}
/>
);

Expand Down
20 changes: 20 additions & 0 deletions client/src/components/ElementEditor/tests/Header-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Enzyme.configure({ adapter: new Adapter() });

describe('Header', () => {
const ActionMenuComponent = () => <div />;
const testTabs = ['Content', 'Settings', 'History'];

describe('render()', () => {
it('should render the icon', () => {
Expand All @@ -35,6 +36,7 @@ describe('Header', () => {
title="Sample File Block"
elementType="File"
fontIcon="font-icon-block-file"
editTabs={testTabs}
ActionMenuComponent={ActionMenuComponent}
/>
);
Expand All @@ -50,6 +52,7 @@ describe('Header', () => {
title="Sample File Block"
elementType="File"
fontIcon="font-icon-block-file"
editTabs={testTabs}
ActionMenuComponent={ActionMenuComponent}
/>
);
Expand Down Expand Up @@ -123,6 +126,23 @@ describe('Header', () => {

expect(wrapper.text()).not.toContain('ActionMenuComponent');
});

it('should render the given "edit tabs" in the action menu', () => {
const wrapper = shallow(
<Header
expandable
editTabs={testTabs}
ActionMenuComponent={ActionMenuComponent}
/>
);

// See the dropdown separator
expect(wrapper.find(ActionMenuComponent).children().find('DropdownItem').length).toBe(1);
// See all the relevant action menu options
expect(wrapper.find(ActionMenuComponent).children().map(node => node.text())).toEqual(
expect.arrayContaining(testTabs)
);
});
});

describe('renderVersionedStateMessage()', () => {
Expand Down
11 changes: 11 additions & 0 deletions client/src/types/elementTypeType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PropTypes } from 'react';

// Describes the structure of an element coming in via GraphQL
const elementTypeType = PropTypes.shape({
name: PropTypes.string,
title: PropTypes.string,
icon: PropTypes.string,
tabs: PropTypes.arrayOf(PropTypes.string),
});

export { elementTypeType };
8 changes: 7 additions & 1 deletion src/Forms/ElementalAreaField.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use DNADesign\Elemental\Services\ElementTabProvider;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
Expand Down Expand Up @@ -115,11 +116,16 @@ public function getSchemaDataDefaults()

$blockTypes = [];

// Use the internal (temporary) provider to get cached tab names.
/** @var ElementTabProvider $tabProvider */
$tabProvider = Injector::inst()->get(ElementTabProvider::class);

foreach ($this->getTypes() as $className => $blockTitle) {
$blockTypes[] = [
'value' => str_replace('\\', '-', $className),
'name' => str_replace('\\', '-', $className),
'title' => $blockTitle,
'icon' => Config::inst()->get($className, 'icon'),
'tabs' => array_values($tabProvider->getTabsForElement($className)),
];
}

Expand Down
Loading

0 comments on commit 55411b2

Please sign in to comment.