Refactor explorer code with tests using Jest

pull/3481/merge
Thibaud Colas 2016-06-17 15:48:33 +02:00
rodzic 743a8304a6
commit 8bf2c9bf2e
85 zmienionych plików z 17428 dodań i 12057 usunięć

Wyświetl plik

@ -2,5 +2,6 @@
"presets": [
"es2015",
"react"
]
],
"plugins": ["lodash"]
}

Wyświetl plik

@ -1,3 +1,7 @@
{
"extends": "wagtail"
"extends": "wagtail",
"env": {
"jest": true
}
}

Wyświetl plik

Wyświetl plik

@ -1,42 +1,31 @@
# Wagtail client-side components
This library aims to give developers the ability to subclass and configure Wagtail's UI components.
> This library aims to give developers the ability to subclass and configure Wagtail's UI components.
## Usage
```
```sh
npm install wagtail
```
```javascript
import { Explorer } from 'wagtail';
...
<Explorer onChoosePage={(page)=> { console.log(`You picked ${page}`); }} />
// [...]
<Explorer />
```
## Available components
## Development
TODO
- [ ] Explorer
- [ ] Modal
- [ ] DatePicker
- [ ] LinkChooser
- [ ] DropDown
## Building in development
Run `webpack` from the Wagtail project root.
```
webpack
```sh
# From the project root, start the webpack + styles compilation.
npm run start
```
## How to release
You will also need:
The front-end is bundled at the same time as the Wagtail project, via `setuptools`.
- [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) – React developer tools integrated into Chrome.
- [Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) – Redux developer tools integrated into Chrome.
## Releases
The front-end is bundled at the same time as the Wagtail project. This package also aims to be available separately on npm as [`wagtail`](https://www.npmjs.com/package/wagtail).

Wyświetl plik

@ -1,18 +1,21 @@
{
"name": "wagtail",
"name": "wagtail-client",
"version": "0.1.0",
"repository": "https://github.com/wagtail/wagtail",
"description": "Wagtail's client side code",
"license": "BSD-3-Clause",
"author": "Wagtail",
"version": "0.0.2",
"main": "src/index.js",
"bin": {
"wagtail": "./src/cli/index.js"
},
"scripts": {
"test": "npm test"
},
"main": "src/index.js",
"description": "Wagtail's client side code",
"files": [
"src/index.js"
],
"devDependencies": {},
"dependencies": {
"mustache": "^2.2.1",
"yargs": "^4.2.0"
}
},
"scripts": {}
}

Wyświetl plik

@ -1,3 +1 @@
@import '../src/components/explorer/style';
@import '../src/components/loading-indicator/style';
@import '../src/components/state-indicator/style';

Wyświetl plik

@ -0,0 +1,23 @@
import { get } from '../api/client';
import { ADMIN_API } from '../config/wagtail';
export const getChildPages = (id, options = {}) => {
let url = `${ADMIN_API.PAGES}?child_of=${id}`;
if (options.fields) {
url += `&fields=${global.encodeURIComponent(options.fields.join(','))}`;
}
if (options.filter) {
url += `&${options.filter}`;
}
return get(url).then(res => res.body);
};
export const getPage = (id) => {
const url = `${ADMIN_API.PAGES}${id}/`;
return get(url).then(res => res.body);
};

Wyświetl plik

@ -0,0 +1,38 @@
import _ from 'lodash';
const fetch = global.fetch;
// fetch wrapper for JSON APIs.
export const get = (url) => {
const headers = new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
});
const options = {
credentials: 'same-origin',
headers: headers,
method: 'GET'
};
return fetch(url, options)
.then((res) => {
const response = {
status: res.status,
statusText: res.statusText,
headers: res.headers
};
let ret;
if (response.status >= 200 && response.status < 300) {
ret = res.json().then(json => _.assign(response, { body: json }));
} else {
ret = res.text().then((text) => {
const err = _.assign(new Error(response.statusText), response, { body: text });
throw err;
});
}
return ret;
});
};

Wyświetl plik

@ -0,0 +1,23 @@
import React from 'react';
import moment from 'moment';
import { DATE_FORMAT, STRINGS } from '../../config/wagtail';
const AbsoluteDate = ({ time }) => {
const date = moment(time);
const text = time ? date.format(DATE_FORMAT) : STRINGS.NO_DATE;
return (
<span>{text}</span>
);
};
AbsoluteDate.propTypes = {
time: React.PropTypes.string,
};
AbsoluteDate.defaultProps = {
time: '',
};
export default AbsoluteDate;

Wyświetl plik

@ -0,0 +1,18 @@
import React from 'react';
import { shallow } from 'enzyme';
import AbsoluteDate from './AbsoluteDate';
describe('AbsoluteDate', () => {
it('exists', () => {
expect(AbsoluteDate).toBeDefined();
});
it('basic', () => {
expect(shallow(<AbsoluteDate />)).toMatchSnapshot();
});
it('#time', () => {
expect(shallow(<AbsoluteDate time="2016-09-19T20:22:33.356623Z" />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,11 @@
exports[`AbsoluteDate #time 1`] = `
<span>
19.09.2016
</span>
`;
exports[`AbsoluteDate basic 1`] = `
<span>
No date
</span>
`;

Wyświetl plik

@ -0,0 +1,88 @@
import React from 'react';
import _ from 'lodash';
/**
* A reusable button. Uses a <a> tag underneath.
*/
export default React.createClass({
propTypes: {
href: React.PropTypes.string,
className: React.PropTypes.string,
icon: React.PropTypes.string,
target: React.PropTypes.string,
children: React.PropTypes.node,
accessibleLabel: React.PropTypes.string,
onClick: React.PropTypes.func,
isLoading: React.PropTypes.bool,
preventDefault: React.PropTypes.bool,
},
getDefaultProps() {
return {
href: '#',
className: '',
icon: '',
target: null,
children: null,
accessibleLabel: null,
onClick: null,
isLoading: false,
preventDefault: true,
};
},
handleClick(e) {
const { href, onClick, preventDefault } = this.props;
if (preventDefault && href === '#') {
e.preventDefault();
e.stopPropagation();
}
if (onClick) {
onClick(e);
}
},
render() {
const {
className,
icon,
children,
accessibleLabel,
isLoading,
target,
} = this.props;
const props = _.omit(this.props, [
'className',
'icon',
'iconClassName',
'children',
'accessibleLabel',
'isLoading',
'onClick',
'preventDefault',
]);
const hasIcon = icon !== '';
const hasText = children !== null;
const iconName = isLoading ? 'spinner' : icon;
const accessibleElt = accessibleLabel ? (
<span className="visuallyhidden">
{accessibleLabel}
</span>
) : null;
return (
<a
className={`${className} ${hasIcon ? 'icon icon-' : ''}${iconName}`}
onClick={this.handleClick}
rel={target === '_blank' ? 'noopener' : null}
{...props}
>
{hasText ? children : accessibleElt}
</a>
);
},
});

Wyświetl plik

@ -0,0 +1,39 @@
import React from 'react';
import { shallow } from 'enzyme';
import Button from './Button';
describe('Button', () => {
it('exists', () => {
expect(Button).toBeDefined();
});
it('basic', () => {
expect(shallow(<Button />)).toMatchSnapshot();
});
it('#children', () => {
expect(shallow(<Button>To infinity and beyond!</Button>)).toMatchSnapshot();
});
it('#accessibleLabel', () => {
expect(shallow(<Button accessibleLabel="I am here in the shadows" />)).toMatchSnapshot();
});
it('#icon', () => {
expect(shallow(<Button icon="test-icon" />)).toMatchSnapshot();
});
it('#icon changes with #isLoading', () => {
expect(shallow(<Button icon="test-icon" isLoading={true} />)).toMatchSnapshot();
});
it('is clickable', () => {
const onClick = jest.fn();
shallow(<Button onClick={onClick} />).simulate('click', {
preventDefault() {},
stopPropagation() {},
});
expect(onClick).toHaveBeenCalledTimes(1);
});
});

Wyświetl plik

@ -0,0 +1,51 @@
exports[`Button #accessibleLabel 1`] = `
<a
className=" "
href="#"
onClick={[Function]}
rel={null}
target={null}>
<span
className="visuallyhidden">
I am here in the shadows
</span>
</a>
`;
exports[`Button #children 1`] = `
<a
className=" "
href="#"
onClick={[Function]}
rel={null}
target={null}>
To infinity and beyond!
</a>
`;
exports[`Button #icon 1`] = `
<a
className=" icon icon-test-icon"
href="#"
onClick={[Function]}
rel={null}
target={null} />
`;
exports[`Button #icon changes with #isLoading 1`] = `
<a
className=" icon icon-spinner"
href="#"
onClick={[Function]}
rel={null}
target={null} />
`;
exports[`Button basic 1`] = `
<a
className=" "
href="#"
onClick={[Function]}
rel={null}
target={null} />
`;

Wyświetl plik

@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import Explorer from './Explorer';
const mockProps = {
};
describe('Explorer', () => {
it('exists', () => {
expect(Explorer).toBeDefined();
});
});

Wyświetl plik

@ -0,0 +1,32 @@
import React from 'react';
import { shallow } from 'enzyme';
import ExplorerItem from './ExplorerItem';
const mockProps = {
data: {
meta: {
children: {
count: 0,
}
}
},
};
describe('ExplorerItem', () => {
it('exists', () => {
expect(ExplorerItem).toBeDefined();
});
it('basic', () => {
expect(shallow(<ExplorerItem />)).toMatchSnapshot();
});
it('#data', () => {
expect(shallow(<ExplorerItem {...mockProps} />)).toMatchSnapshot();
});
it('#typeName', () => {
expect(shallow(<ExplorerItem {...mockProps} typeName="Foo" />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,37 @@
import React from 'react';
import { createStore } from 'redux';
import { shallow } from 'enzyme';
import ExplorerToggle from './ExplorerToggle';
import rootReducer from './reducers';
const store = createStore(rootReducer);
describe('ExplorerToggle', () => {
it('exists', () => {
expect(ExplorerToggle).toBeDefined();
});
it('basic', () => {
expect(shallow(<ExplorerToggle store={store} />)).toMatchSnapshot();
});
it('loading state', (done) => {
store.subscribe(() => {
expect(shallow(<ExplorerToggle store={store} />)).toMatchSnapshot();
done();
});
store.dispatch({ type: 'FETCH_START' });
});
it('#children', () => {
expect(shallow((
<ExplorerToggle store={store}>
<span>
To infinity and beyond!
</span>
</ExplorerToggle>
))).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import LoadingSpinner from './LoadingSpinner';
describe('LoadingSpinner', () => {
it('exists', () => {
expect(LoadingSpinner).toBeDefined();
});
it('basic', () => {
expect(shallow(<LoadingSpinner />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,77 @@
exports[`ExplorerItem #data 1`] = `
<Button
accessibleLabel={null}
className="c-explorer__item"
href="/admin/pages/undefined"
icon=""
isLoading={false}
onClick={null}
preventDefault={true}
target={null}>
<h3
className="c-explorer__title" />
<p
className="c-explorer__meta">
<span
className="c-explorer__meta__type" />
|
<AbsoluteDate
time="" />
|
<PublicationStatus />
</p>
</Button>
`;
exports[`ExplorerItem #typeName 1`] = `
<Button
accessibleLabel={null}
className="c-explorer__item"
href="/admin/pages/undefined"
icon=""
isLoading={false}
onClick={null}
preventDefault={true}
target={null}>
<h3
className="c-explorer__title" />
<p
className="c-explorer__meta">
<span
className="c-explorer__meta__type">
Foo
</span>
|
<AbsoluteDate
time="" />
|
<PublicationStatus />
</p>
</Button>
`;
exports[`ExplorerItem basic 1`] = `
<Button
accessibleLabel={null}
className="c-explorer__item"
href="/admin/pages/undefined"
icon=""
isLoading={false}
onClick={null}
preventDefault={true}
target={null}>
<h3
className="c-explorer__title" />
<p
className="c-explorer__meta">
<span
className="c-explorer__meta__type" />
|
<AbsoluteDate
time={null} />
|
<PublicationStatus
status={null} />
</p>
</Button>
`;

Wyświetl plik

@ -0,0 +1,48 @@
exports[`ExplorerToggle #children 1`] = `
<ExplorerToggle
isFetching={true}
isVisible={false}
onToggle={[Function]}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}>
<span>
To infinity and beyond!
</span>
</ExplorerToggle>
`;
exports[`ExplorerToggle basic 1`] = `
<ExplorerToggle
isFetching={false}
isVisible={false}
onToggle={[Function]}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
} />
`;
exports[`ExplorerToggle loading state 1`] = `
<ExplorerToggle
isFetching={true}
isVisible={false}
onToggle={[Function]}
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
} />
`;

Wyświetl plik

@ -0,0 +1,11 @@
exports[`LoadingSpinner basic 1`] = `
<div
className="c-explorer__loading">
<Icon
className="c-explorer__spinner"
name="spinner"
title={null} />
Loading...
</div>
`;

Wyświetl plik

@ -0,0 +1,57 @@
import * as actions from '../actions';
import rootReducer from './index';
import explorer from './explorer';
import nodes from './nodes';
import transport from './transport';
describe('explorer reducers', () => {
describe('root', () => {
it('exists', () => {
expect(rootReducer).toBeDefined();
});
});
describe('explorer', () => {
it('exists', () => {
expect(explorer).toBeDefined();
});
});
describe('nodes', () => {
it('exists', () => {
expect(nodes).toBeDefined();
});
});
describe('transport', () => {
const initialState = {
error: null,
showMessage: false,
};
it('exists', () => {
expect(transport).toBeDefined();
});
it('returns the initial state', () => {
expect(transport(undefined, {})).toEqual(initialState);
});
it('returns error message and flag', () => {
const action = actions.fetchFailure(new Error('Test error'));
expect(transport(initialState, action)).toEqual({
error: 'Test error',
showMessage: true,
});
});
it('clears previous error message and flag', () => {
const action = actions.clearError();
const errorState = {
error: 'Test error',
showMessage: true,
};
expect(transport(errorState, action)).toEqual(initialState);
});
});
});

Wyświetl plik

@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from 'enzyme';
import Icon from './Icon';
describe('Icon', () => {
it('exists', () => {
expect(Icon).toBeDefined();
});
it('#name', () => {
expect(shallow(<Icon name="test" />)).toMatchSnapshot();
});
it('#className', () => {
expect(shallow(<Icon name="test" className="u-test" />)).toMatchSnapshot();
});
it('#title', () => {
expect(shallow(<Icon name="test" title="Test title" />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,22 @@
exports[`Icon #className 1`] = `
<span
aria-hidden={true}
className="icon icon-test u-test" />
`;
exports[`Icon #name 1`] = `
<span
aria-hidden={true}
className="icon icon-test " />
`;
exports[`Icon #title 1`] = `
<span
aria-hidden={false}
className="icon icon-test ">
<span
className="visuallyhidden">
Test title
</span>
</span>
`;

Wyświetl plik

@ -0,0 +1,10 @@
import React from 'react';
import { STRINGS } from '../../config/wagtail';
const LoadingIndicator = () => (
<div className="o-icon c-indicator is-spinning">
<span ariaRole="presentation">{STRINGS.LOADING}</span>
</div>
);
export default LoadingIndicator;

Wyświetl plik

@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import LoadingIndicator from './LoadingIndicator';
describe('LoadingIndicator', () => {
it('exists', () => {
expect(LoadingIndicator).toBeDefined();
});
it('basic', () => {
expect(shallow(<LoadingIndicator />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,9 @@
exports[`LoadingIndicator basic 1`] = `
<div
className="o-icon c-indicator is-spinning">
<span
ariaRole="presentation">
Loading...
</span>
</div>
`;

Wyświetl plik

@ -0,0 +1,13 @@
import React from 'react';
const PublicationStatus = ({ status }) => (status ? (
<span className={`o-pill c-status${status.live ? ' c-status--live' : ''}`}>
{status.status}
</span>
) : null);
PublicationStatus.propTypes = {
status: React.PropTypes.object,
};
export default PublicationStatus;

Wyświetl plik

@ -0,0 +1,39 @@
import React from 'react';
import { shallow } from 'enzyme';
import PublicationStatus from './PublicationStatus';
describe('PublicationStatus', () => {
it('exists', () => {
expect(PublicationStatus).toBeDefined();
});
// TODO Skipped because causing a test error. Apparently this is fixed when using React 15.
it.skip('basic', () => {
expect(shallow(<PublicationStatus />)).toMatchSnapshot();
});
it('#status live', () => {
expect(shallow((
<PublicationStatus
status={{
status: 'live + draft',
live: true,
has_unpublished_changes: true,
}}
/>
))).toMatchSnapshot();
});
it('#status not live', () => {
expect(shallow((
<PublicationStatus
status={{
status: 'live + draft',
live: false,
has_unpublished_changes: true,
}}
/>
))).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,19 @@
# PublicationStatus
Displays the publication status of a page in a pill.
## Usage
```javascript
import { PublicationStatus } from 'wagtail';
render(
<PublicationStatus
status={status}
/>
);
```
### Available props
- `status`: status object coming from the admin API. If no status is given, component renders to null.

Wyświetl plik

@ -0,0 +1,13 @@
exports[`PublicationStatus #status live 1`] = `
<span
className="o-pill c-status c-status--live">
live + draft
</span>
`;
exports[`PublicationStatus #status not live 1`] = `
<span
className="o-pill c-status">
live + draft
</span>
`;

Wyświetl plik

@ -1,16 +1,16 @@
import React, { Component, PropTypes } from 'react';
import React from 'react';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import { connect } from 'react-redux'
import { connect } from 'react-redux';
import * as actions from './actions';
import { EXPLORER_ANIM_DURATION } from 'config';
import { EXPLORER_ANIM_DURATION } from '../../config/config';
import ExplorerPanel from './ExplorerPanel';
class Explorer extends Component {
// TODO To refactor.
class Explorer extends React.Component {
constructor(props) {
super(props);
this._init = this._init.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
@ -19,7 +19,7 @@ class Explorer extends Component {
}
}
_init(id) {
init() {
if (this.props.page && this.props.page.isLoaded) {
return;
}
@ -27,15 +27,15 @@ class Explorer extends Component {
this.props.onShow(this.props.page ? this.props.page : this.props.defaultPage);
}
_getPage() {
let { nodes, depth, path } = this.props;
let id = path[path.length - 1];
getPage() {
const { nodes, path } = this.props;
const id = path[path.length - 1];
return nodes[id];
}
render() {
let { visible, depth, nodes, path, pageTypes, items, type, filter, fetching, resolved } = this.props;
let page = this._getPage();
const { isVisible, nodes, path, pageTypes, type, filter, fetching, resolved } = this.props;
const page = this.getPage();
const explorerProps = {
path,
@ -47,54 +47,56 @@ class Explorer extends Component {
nodes,
resolved,
ref: 'explorer',
left: this.props.left,
top: this.props.top,
onPop: this.props.onPop,
onItemClick: this.props.onItemClick,
onClose: this.props.onClose,
transport: this.props.transport,
onFilter: this.props.onFilter,
getChildren: this.props.getChildren,
loadItemWithChildren: this.props.loadItemWithChildren,
pushPage: this.props.pushPage,
init: this._init
}
init: this.init,
};
const transProps = {
component: 'div',
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
transitionName: 'explorer-toggle'
}
};
return (
<CSSTransitionGroup {...transProps}>
{ visible ? <ExplorerPanel {...explorerProps} /> : null }
{isVisible ? <ExplorerPanel {...explorerProps} /> : null}
</CSSTransitionGroup>
);
}
}
Explorer.propTypes = {
onPageSelect: PropTypes.func,
initialPath: PropTypes.string,
apiPath: PropTypes.string,
size: PropTypes.number,
position: PropTypes.object,
page: PropTypes.number,
defaultPage: PropTypes.number,
isVisible: React.PropTypes.bool.isRequired,
fetching: React.PropTypes.bool.isRequired,
resolved: React.PropTypes.bool.isRequired,
path: React.PropTypes.array,
type: React.PropTypes.string.isRequired,
filter: React.PropTypes.string.isRequired,
nodes: React.PropTypes.object.isRequired,
transport: React.PropTypes.object.isRequired,
page: React.PropTypes.any,
defaultPage: React.PropTypes.number,
onPop: React.PropTypes.func.isRequired,
setDefaultPage: React.PropTypes.func.isRequired,
onShow: React.PropTypes.func.isRequired,
onClose: React.PropTypes.func.isRequired,
onFilter: React.PropTypes.func.isRequired,
getChildren: React.PropTypes.func.isRequired,
loadItemWithChildren: React.PropTypes.func.isRequired,
pushPage: React.PropTypes.func.isRequired,
pageTypes: React.PropTypes.object.isRequired,
};
// =============================================================================
// Connector
// =============================================================================
const mapStateToProps = (state, ownProps) => ({
visible: state.explorer.isVisible,
const mapStateToProps = (state) => ({
isVisible: state.explorer.isVisible,
page: state.explorer.currentPage,
depth: state.explorer.depth,
loading: state.explorer.isLoading,
fetching: state.explorer.isFetching,
resolved: state.explorer.isResolved,
path: state.explorer.path,
@ -107,20 +109,15 @@ const mapStateToProps = (state, ownProps) => ({
transport: state.transport
});
const mapDispatchToProps = (dispatch) => {
return {
setDefaultPage: (id) => { dispatch(actions.setDefaultPage(id)) },
getChildren: (id) => { dispatch(actions.fetchChildren(id)) },
onShow: (id) => { dispatch(actions.fetchRoot()) },
onFilter: (filter) => { dispatch(actions.setFilter(filter)) },
loadItemWithChildren: (id) => { dispatch(actions.fetchPage(id)) },
pushPage: (id) => { dispatch(actions.pushPage(id)) },
onPop: () => { dispatch(actions.popPage()) },
onClose: () => { dispatch(actions.toggleExplorer()) }
}
}
const mapDispatchToProps = (dispatch) => ({
setDefaultPage: (id) => dispatch(actions.setDefaultPage(id)),
getChildren: (id) => dispatch(actions.fetchChildren(id)),
onShow: () => dispatch(actions.fetchRoot()),
onFilter: (filter) => dispatch(actions.setFilter(filter)),
loadItemWithChildren: (id) => dispatch(actions.fetchPage(id)),
pushPage: (id) => dispatch(actions.pushPage(id)),
onPop: () => dispatch(actions.popPage()),
onClose: () => dispatch(actions.toggleExplorer()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Explorer);
export default connect(mapStateToProps, mapDispatchToProps)(Explorer);

Wyświetl plik

@ -1,8 +0,0 @@
import React from 'react';
import { STRINGS } from 'config';
const ExplorerEmpty = () => (
<div className="c-explorer__placeholder">{STRINGS['NO_RESULTS']}</div>
);
export default ExplorerEmpty;

Wyświetl plik

@ -1,79 +1,60 @@
import React, { Component } from 'react';
import React from 'react';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import { EXPLORER_ANIM_DURATION, EXPLORER_FILTERS, STRINGS } from 'config';
import { EXPLORER_ANIM_DURATION, EXPLORER_FILTERS } from '../../config/config';
import { STRINGS } from '../../config/wagtail';
import Icon from 'components/icon/Icon';
import Filter from './Filter';
import Icon from '../../components/Icon/Icon';
import Filter from '../../components/Explorer/Filter';
class ExplorerHeader extends Component {
const ExplorerHeader = ({ page, depth, filter, onPop, onFilter, transName }) => {
const title = depth < 2 || !page ? STRINGS.EXPLORER : page.title;
constructor(p) {
super(p)
this.onFilter = this.onFilter.bind(this);
}
const transitionProps = {
component: 'span',
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
transitionName: `explorer-${transName}`,
className: 'c-explorer__rel',
};
_getBackBtn() {
return (
<span className='c-explorer__back'>
<Icon name="arrow-left" />
</span>
);
}
onFilter(e) {
this.props.onFilter(e.target.value);
}
_getClass() {
let cls = ['c-explorer__trigger'];
if (this.props.depth > 1) {
cls.push('c-explorer__trigger--enabled');
}
return cls.join(' ');
}
_getTitle() {
let { page, depth } = this.props;
if (depth < 2 || !page) {
return STRINGS['EXPLORER'];
}
return page.title;
}
render() {
let { page, depth, filter, onPop, onFilter, transName } = this.props;
const transitionProps = {
component: 'span',
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
transitionName: `explorer-${transName}`,
className: 'c-explorer__rel',
}
return (
<div className="c-explorer__header">
<span className={this._getClass()} onClick={onPop}>
<span className='u-overflow c-explorer__overflow'>
// TODO Do not use a span for a clickable element.
return (
<div className="c-explorer__header">
<span className={`c-explorer__trigger${depth > 1 ? ' c-explorer__trigger--enabled' : ''}`} onClick={onPop}>
<span className="u-overflow c-explorer__overflow">
<CSSTransitionGroup {...transitionProps}>
<span className='c-explorer__parent-name' key={depth}>
{ depth > 1 ? this._getBackBtn() : null }
{this._getTitle()}
<span className="c-explorer__parent-name" key={depth}>
{depth > 1 ? (
<span className="c-explorer__back">
<Icon name="arrow-left" />
</span>
) : null}
{title}
</span>
</CSSTransitionGroup>
</span>
</span>
<span className="c-explorer__filter">
{EXPLORER_FILTERS.map(props => {
return <Filter key={props.id} {...props} activeFilter={filter} onFilter={onFilter} />
})}
</span>
</div>
);
}
}
</span>
<span className="c-explorer__filter">
{EXPLORER_FILTERS.map((item) => (
<Filter
key={item.id}
{...item}
activeFilter={filter}
onFilter={onFilter}
/>
))}
</span>
</div>
);
};
ExplorerHeader.propTypes = {
page: React.PropTypes.object,
depth: React.PropTypes.number,
filter: React.PropTypes.string,
onPop: React.PropTypes.func,
onFilter: React.PropTypes.func,
transName: React.PropTypes.string,
};
export default ExplorerHeader;

Wyświetl plik

@ -1,63 +1,58 @@
import React, { Component, PropTypes } from 'react';
import React from 'react';
import { ADMIN_PAGES, STRINGS } from 'config';
import Icon from 'components/icon/Icon';
import PublishStatus from 'components/publish-status/PublishStatus';
import PublishedTime from 'components/published-time/PublishedTime';
import StateIndicator from 'components/state-indicator/StateIndicator';
import { ADMIN_URLS, STRINGS } from '../../config/wagtail';
import Icon from '../../components/Icon/Icon';
import Button from '../../components/Button/Button';
import PublicationStatus from '../../components/PublicationStatus/PublicationStatus';
import AbsoluteDate from '../../components/AbsoluteDate/AbsoluteDate';
export default class ExplorerItem extends Component {
const ExplorerItem = ({ title, typeName, data, filter, onItemClick }) => {
const { id, meta } = data;
const status = meta ? meta.status : null;
const time = meta ? meta.latest_revision_created_at : null;
constructor(props) {
super(props);
this._loadChildren = this._loadChildren.bind(this);
// If we only want pages with children, get this info by
// looking at the descendants count vs children count.
// // TODO refactor.
let count = 0;
if (meta) {
count = filter.match(/has_children/) ? meta.descendants.count - meta.children.count : meta.children.count;
}
const hasChildren = count > 0;
_onNavigate(id) {
window.location.href = `${ADMIN_PAGES}${id}`;
}
return (
<Button href={`${ADMIN_URLS.PAGES}${id}`} className="c-explorer__item">
{hasChildren ? (
<span
role="button"
className="c-explorer__children"
onClick={onItemClick.bind(null, id)}
>
<Icon name="folder-inverse" title={STRINGS.SEE_CHILDREN} />
</span>
) : null}
_loadChildren(e) {
e.stopPropagation();
let { onItemClick, data } = this.props;
onItemClick(data.id, data.title);
}
<h3 className="c-explorer__title">{title}</h3>
render() {
const { title, typeName, data, index } = this.props;
const { meta } = data;
let count = meta.children.count;
// TODO refactor.
// If we only want pages with children, get this info by
// looking at the descendants count vs children count.
if (this.props.filter && this.props.filter.match(/has_children/)) {
count = meta.descendants.count - meta.children.count;
}
return (
<div onClick={this._onNavigate.bind(this, data.id)} className="c-explorer__item">
{count > 0 ?
<span className="c-explorer__children" onClick={this._loadChildren}>
<Icon name="folder-inverse" />
<span aria-role='presentation'>
{STRINGS['SEE_CHILDREN']}
</span>
</span> : null }
<h3 className="c-explorer__title">
<StateIndicator state={data.state} />
{title}
</h3>
<p className='c-explorer__meta'>
<span className="c-explorer__meta__type">{typeName}</span> | <PublishedTime publishedAt={meta.latest_revision_created_at} /> | <PublishStatus status={meta.status} />
</p>
</div>
);
}
}
<p className="c-explorer__meta">
<span className="c-explorer__meta__type">{typeName}</span> | <AbsoluteDate time={time} /> | <PublicationStatus status={status} />
</p>
</Button>
);
};
ExplorerItem.propTypes = {
title: PropTypes.string,
data: PropTypes.object
title: React.PropTypes.string,
data: React.PropTypes.object,
filter: React.PropTypes.string,
typeName: React.PropTypes.string,
onItemClick: React.PropTypes.func,
};
ExplorerItem.defaultProps = {
filter: '',
data: {},
onItemClick: () => {},
};
export default ExplorerItem;

Wyświetl plik

@ -1,71 +1,67 @@
import React, { Component, PropTypes } from 'react';
import React from 'react';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import { EXPLORER_ANIM_DURATION, STRINGS } from 'config';
import ExplorerEmpty from './ExplorerEmpty';
import { EXPLORER_ANIM_DURATION } from '../../config/config';
import { STRINGS } from '../../config/wagtail';
import ExplorerHeader from './ExplorerHeader';
import ExplorerItem from './ExplorerItem';
import LoadingSpinner from './LoadingSpinner';
export default class ExplorerPanel extends Component {
export default class ExplorerPanel extends React.Component {
constructor(props) {
super(props);
this._clickOutside = this._clickOutside.bind(this);
this._onItemClick = this._onItemClick.bind(this);
this.closeModal = this.closeModal.bind(this);
this.clickOutside = this.clickOutside.bind(this);
this.onItemClick = this.onItemClick.bind(this);
this.state = {
modalIsOpen: false,
// TODO Refactor value to constant.
animation: 'push',
}
};
}
componentWillReceiveProps(newProps) {
let oldProps = this.props;
const { path } = this.props;
if (!oldProps.path) {
return;
}
if (path) {
const isPush = newProps.path.length > path.length;
const animation = isPush ? 'push' : 'pop';
if (newProps.path.length > oldProps.path.length) {
return this.setState({ animation: 'push' });
} else {
return this.setState({ animation: 'pop' });
this.setState({
animation: animation,
});
}
}
_loadChildren() {
let { page } = this.props;
loadChildren() {
const { page, getChildren } = this.props;
if (!page || page.children.isFetching) {
return false;
}
if (page.meta.children.count && !page.children.length && !page.children.isFetching && !page.children.isLoaded) {
this.props.getChildren(page.id);
if (page && !page.children.isFetching) {
if (page.meta.children.count && !page.children.length && !page.children.isFetching && !page.children.isLoaded) {
getChildren(page.id);
}
}
}
componentDidUpdate() {
this._loadChildren();
this.loadChildren();
}
componentDidMount() {
this.props.init();
document.body.style.overflow = 'hidden';
document.body.classList.add('u-explorer-open');
document.addEventListener('click', this._clickOutside);
document.body.classList.add('explorer-open');
document.addEventListener('click', this.clickOutside);
}
componentWillUnmount() {
document.body.style.overflow = '';
document.body.classList.remove('u-explorer-open');
document.removeEventListener('click', this._clickOutside);
document.body.classList.remove('explorer-open');
document.removeEventListener('click', this.clickOutside);
}
_clickOutside(e) {
let { explorer } = this.refs;
clickOutside(e) {
const { explorer } = this.refs;
if (!explorer) {
return;
@ -76,17 +72,9 @@ export default class ExplorerPanel extends Component {
}
}
_getStyle() {
const { top, left } = this.props;
return {
left: left + 'px',
top: top + 'px'
};
}
_getClass() {
let { type } = this.props;
let cls = ['c-explorer'];
getClass() {
const { type } = this.props;
const cls = ['c-explorer'];
if (type) {
cls.push(`c-explorer--${type}`);
@ -95,16 +83,11 @@ export default class ExplorerPanel extends Component {
return cls.join(' ');
}
closeModal() {
const { dispatch } = this.props;
dispatch(clearError());
this.setState({
modalIsOpen: false
});
}
onItemClick(id, e) {
const node = this.props.nodes[id];
_onItemClick(id) {
let node = this.props.nodes[id];
e.preventDefault();
e.stopPropagation();
if (node.isLoaded) {
this.props.pushPage(id);
@ -114,52 +97,54 @@ export default class ExplorerPanel extends Component {
}
renderChildren(page) {
let { nodes, pageTypes, filter } = this.props;
const { nodes, pageTypes, filter } = this.props;
if (!page || !page.children.items) {
return [];
}
return page.children.items.map(index => {
return nodes[index];
}).map(item => {
const typeName = pageTypes[item.meta.type] ? pageTypes[item.meta.type].verbose_name : item.meta.type;
const props = {
onItemClick: this._onItemClick,
parent: page,
key: item.id,
title: item.title,
typeName,
data: item,
filter,
};
return page.children.items
.map(index => nodes[index])
.map((item) => {
const typeName = pageTypes[item.meta.type] ? pageTypes[item.meta.type].verbose_name : item.meta.type;
const props = {
onItemClick: this.onItemClick,
parent: page,
key: item.id,
title: item.title,
typeName,
data: item,
filter,
};
return <ExplorerItem {...props} />
});
return (
<ExplorerItem {...props} />
);
});
}
_getContents() {
let { page } = this.props;
let contents = null;
getContents() {
const { page } = this.props;
let ret;
if (page) {
if (page.children.items.length) {
return this.renderChildren(page)
ret = this.renderChildren(page);
} else {
return <ExplorerEmpty />
ret = (
<div className="c-explorer__placeholder">{STRINGS.NO_RESULTS}</div>
);
}
}
return ret;
}
render() {
let {
const {
page,
onPop,
onClose,
loading,
type,
pageData,
transport,
onFilter,
filter,
path,
@ -167,8 +152,8 @@ export default class ExplorerPanel extends Component {
} = this.props;
// Don't show anything until the tree is resolved.
if (!this.props.resolved) {
return <div />
if (!resolved) {
return <div />;
}
const headerProps = {
@ -178,37 +163,37 @@ export default class ExplorerPanel extends Component {
onClose,
onFilter,
filter
}
};
const transitionTargetProps = {
key: path.length,
className: 'c-explorer__transition-group'
}
};
const transitionProps = {
component: 'div',
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
transitionName: `explorer-${this.state.animation}`
}
};
const innerTransitionProps = {
component: 'div',
transitionEnterTimeout: EXPLORER_ANIM_DURATION,
transitionLeaveTimeout: EXPLORER_ANIM_DURATION,
transitionName: `explorer-fade`
}
transitionName: 'explorer-fade'
};
return (
<div style={this._getStyle()} className={this._getClass()} ref='explorer'>
<div className={this.getClass()} ref="explorer">
<ExplorerHeader {...headerProps} transName={this.state.animation} />
<div className='c-explorer__drawer'>
<div className="c-explorer__drawer">
<CSSTransitionGroup {...transitionProps}>
<div {...transitionTargetProps}>
<CSSTransitionGroup {...innerTransitionProps}>
{page.isFetching ? <LoadingSpinner key={1} /> : (
<div key={0}>
{this._getContents()}
{this.getContents()}
</div>
)}
</CSSTransitionGroup>
@ -217,10 +202,23 @@ export default class ExplorerPanel extends Component {
</CSSTransitionGroup>
</div>
</div>
)
);
}
}
ExplorerPanel.propTypes = {
}
page: React.PropTypes.object,
onPop: React.PropTypes.func.isRequired,
onClose: React.PropTypes.func.isRequired,
type: React.PropTypes.string.isRequired,
onFilter: React.PropTypes.func.isRequired,
filter: React.PropTypes.string.isRequired,
path: React.PropTypes.array,
resolved: React.PropTypes.bool.isRequired,
init: React.PropTypes.func.isRequired,
getChildren: React.PropTypes.func.isRequired,
pushPage: React.PropTypes.func.isRequired,
loadItemWithChildren: React.PropTypes.func.isRequired,
nodes: React.PropTypes.object.isRequired,
pageTypes: React.PropTypes.object.isRequired,
};

Wyświetl plik

@ -0,0 +1,41 @@
import React from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
import Button from '../../components/Button/Button';
/**
* A Button which toggles the explorer, and doubles as a loading indicator.
*/
// TODO isVisible should not be used here, but at the moment there is a click
// binding problem between this and the ExplorerPanel clickOutside.
const ExplorerToggle = ({ isVisible, isFetching, children, onToggle }) => (
<Button
icon="folder-open-inverse"
isLoading={isFetching}
onClick={isVisible ? null : onToggle}
>
{children}
</Button>
);
ExplorerToggle.propTypes = {
isVisible: React.PropTypes.bool,
isFetching: React.PropTypes.bool,
onToggle: React.PropTypes.func,
children: React.PropTypes.node,
};
const mapStateToProps = (store) => ({
isFetching: store.explorer.isFetching,
isVisible: store.explorer.isVisible,
});
const mapDispatchToProps = (dispatch) => ({
onToggle() {
dispatch(actions.toggleExplorer());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ExplorerToggle);

Wyświetl plik

@ -0,0 +1,20 @@
import React from 'react';
// TODO Do not use a span for a clickable element.
const Filter = ({ label, filter = null, activeFilter, onFilter }) => (
<span
className={`c-filter${activeFilter === filter ? ' c-filter--active' : ''}`}
onClick={onFilter.bind(this, filter)}
>
{label}
</span>
);
Filter.propTypes = {
label: React.PropTypes.string.isRequired,
filter: React.PropTypes.string,
activeFilter: React.PropTypes.string,
onFilter: React.PropTypes.func.isRequired,
};
export default Filter;

Wyświetl plik

@ -1,9 +1,10 @@
import React from 'react';
import { STRINGS } from 'config';
import { STRINGS } from '../../config/wagtail';
import Icon from '../../components/Icon/Icon';
const LoadingSpinner = () => (
<div className="c-explorer__loading">
<span className="c-explorer__spinner icon icon-spinner" /> {STRINGS['LOADING']}...
<Icon name="spinner" className="c-explorer__spinner" /> {STRINGS.LOADING}
</div>
);

Wyświetl plik

@ -1,31 +0,0 @@
import React from 'react';
import { ADMIN_PAGES } from 'config';
const PageCount = ({ id, count }) => {
let prefix = '';
let suffix = 'pages';
if (count === 0) {
return <div />;
}
if (count > 1) {
prefix = 'all ';
}
if (count === 1) {
suffix = 'page';
}
return (
<div onClick={() => {
window.location.href = `${ADMIN_PAGES}${id}/`
}}
className="c-explorer__see-more">
See {prefix}{ count } {suffix}
</div>
);
}
export default PageCount;

Wyświetl plik

@ -1 +0,0 @@
# Explorer

Wyświetl plik

@ -1,27 +1,11 @@
import { createAction } from 'redux-actions';
import { API_PAGES, PAGES_ROOT_ID } from 'config';
function _getHeaders() {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
return {
credentials: 'same-origin',
headers: headers,
method: 'GET'
};
}
function _get(url) {
return fetch(url, _getHeaders()).then(response => response.json());
}
import { PAGES_ROOT_ID } from '../../../config/config';
import * as admin from '../../../api/admin';
export const fetchStart = createAction('FETCH_START');
export const fetchSuccess = createAction('FETCH_SUCCESS', (id, body) => {
return { id, body };
});
export const fetchSuccess = createAction('FETCH_SUCCESS', (id, body) => ({ id, body }));
export const fetchFailure = createAction('FETCH_FAILURE');
@ -29,9 +13,7 @@ export const pushPage = createAction('PUSH_PAGE');
export const popPage = createAction('POP_PAGE');
export const fetchBranchSuccess = createAction('FETCH_BRANCH_SUCCESS', (id, json) => {
return { id, json };
});
export const fetchBranchSuccess = createAction('FETCH_BRANCH_SUCCESS', (id, json) => ({ id, json }));
export const fetchBranchStart = createAction('FETCH_BRANCH_START');
@ -41,73 +23,66 @@ export const resetTree = createAction('RESET_TREE');
export const treeResolved = createAction('TREE_RESOLVED');
export const fetchChildrenSuccess = createAction('FETCH_CHILDREN_SUCCESS', (id, json) => ({ id, json }));
export const fetchChildrenStart = createAction('FETCH_CHILDREN_START');
/**
* Gets the children of a node from the API.
*/
export function fetchChildren(id = 'root') {
return (dispatch, getState) => {
const { explorer } = getState();
dispatch(fetchChildrenStart(id));
return admin.getChildPages(id, {
fields: explorer.fields,
filter: explorer.filter,
}).then(json => dispatch(fetchChildrenSuccess(id, json)));
};
}
// Make this a bit better... hmm....
export function fetchTree(id = 1) {
return (dispatch) => {
dispatch(fetchBranchStart(id));
return _get(`${API_PAGES}${id}/`)
.then(json => {
dispatch(fetchBranchSuccess(id, json));
return admin.getPage(id).then((json) => {
dispatch(fetchBranchSuccess(id, json));
// Recursively walk up the tree to the root, to figure out how deep
// in the tree we are.
if (json.meta.parent) {
dispatch(fetchTree(json.meta.parent.id));
} else {
dispatch(treeResolved());
}
});
// Recursively walk up the tree to the root, to figure out how deep
// in the tree we are.
if (json.meta.parent) {
dispatch(fetchTree(json.meta.parent.id));
} else {
dispatch(treeResolved());
}
});
};
}
export function fetchRoot() {
return (dispatch) => {
// TODO Should not need an id.
dispatch(resetTree(1));
dispatch(resetTree(PAGES_ROOT_ID));
dispatch(fetchBranchStart(PAGES_ROOT_ID));
return _get(`${API_PAGES}?child_of=${PAGES_ROOT_ID}`)
.then(json => {
// TODO right now, only works for a single homepage.
// TODO What do we do if there is no homepage?
const rootId = json.items[0].id;
dispatch(fetchBranchSuccess(PAGES_ROOT_ID, {
children: {},
meta: {
children: {},
},
}));
dispatch(fetchTree(rootId));
});
dispatch(fetchChildren(PAGES_ROOT_ID));
dispatch(treeResolved());
};
}
export const toggleExplorer = createAction('TOGGLE_EXPLORER');
export const fetchChildrenSuccess = createAction('FETCH_CHILDREN_SUCCESS', (id, json) => {
return { id, json };
});
export const fetchChildrenStart = createAction('FETCH_CHILDREN_START');
/**
* Gets the children of a node from the API
*/
export function fetchChildren(id = 'root') {
return (dispatch, getState) => {
const { explorer } = getState();
let api = `${API_PAGES}?child_of=${id}`;
if (explorer.fields) {
api += `&fields=${explorer.fields.map(global.encodeURIComponent).join(',')}`;
}
if (explorer.filter) {
api = `${api}&${explorer.filter}`;
}
dispatch(fetchChildrenStart(id));
return _get(api)
.then(json => dispatch(fetchChildrenSuccess(id, json)));
};
}
export function setFilter(filter) {
return (dispatch, getState) => {
@ -117,9 +92,9 @@ export function setFilter(filter) {
dispatch({
payload: {
filter,
id
id,
},
type: 'SET_FILTER'
type: 'SET_FILTER',
});
dispatch(fetchChildren(id));
@ -132,7 +107,7 @@ export function setFilter(filter) {
export function fetchPage(id = 1) {
return dispatch => {
dispatch(fetchStart(id));
return _get(`${API_PAGES}${id}/`)
return admin.getPage(id)
.then(json => dispatch(fetchSuccess(id, json)))
.then(json => dispatch(fetchChildren(id, json)))
.catch(json => dispatch(fetchFailure(new Error(JSON.stringify(json)))));

Wyświetl plik

@ -1,18 +0,0 @@
import React, { Component } from 'react';
const Filter = ({label, filter=null, activeFilter, onFilter}) => {
let click = onFilter.bind(this, filter);
let isActive = activeFilter === filter;
let cls = ['c-filter'];
if (isActive) {
cls.push('c-filter--active');
}
return (
<span className={cls.join(' ')} onClick={click}>{label}</span>
);
}
export default Filter;

Wyświetl plik

@ -1,95 +1,98 @@
const stateDefaults = {
import _ from 'lodash';
const defaultState = {
isVisible: false,
isFetching: false,
isResolved: false,
path: [],
currentPage: 1,
defaultPage: 1,
// TODO Change to include less fields (just 'descendants'?) in the next version of the admin API.
// Specificies which fields are to be fetched in the API calls.
fields: ['title', 'latest_revision_created_at', 'status', 'descendants', 'children'],
filter: 'has_children=1',
// Coming from the API in order to get translated / pluralised labels.
pageTypes: {},
}
export default function explorer(state = stateDefaults, action) {
};
export default function explorer(state = defaultState, action) {
let newNodes = state.path;
switch (action.type) {
case 'SET_DEFAULT_PAGE':
return Object.assign({}, state, {
defaultPage: action.payload
});
case 'SET_DEFAULT_PAGE':
return _.assign({}, state, {
defaultPage: action.payload
});
case 'RESET_TREE':
return Object.assign({}, state, {
isFetching: true,
isResolved: false,
currentPage: action.payload,
path: [],
});
case 'RESET_TREE':
return _.assign({}, state, {
isFetching: true,
isResolved: false,
currentPage: action.payload,
path: [],
});
case 'TREE_RESOLVED':
return Object.assign({}, state, {
isFetching: false,
isResolved: true
});
case 'TREE_RESOLVED':
return _.assign({}, state, {
isFetching: false,
isResolved: true
});
case 'TOGGLE_EXPLORER':
return Object.assign({}, state, {
isVisible: !state.isVisible,
currentPage: action.payload ? action.payload : state.defaultPage,
});
case 'TOGGLE_EXPLORER':
return _.assign({}, state, {
isVisible: !state.isVisible,
currentPage: action.payload ? action.payload : state.defaultPage,
});
case 'FETCH_START':
return Object.assign({}, state, {
isFetching: true
});
case 'FETCH_START':
return _.assign({}, state, {
isFetching: true
});
case 'FETCH_BRANCH_SUCCESS':
if (state.path.indexOf(action.payload.id) < 0) {
newNodes = [action.payload.id].concat(state.path);
}
case 'FETCH_BRANCH_SUCCESS':
if (state.path.indexOf(action.payload.id) < 0) {
newNodes = [action.payload.id].concat(state.path);
}
return Object.assign({}, state, {
path: newNodes,
currentPage: state.currentPage ? state.currentPage : action.payload.id
});
return _.assign({}, state, {
path: newNodes,
currentPage: state.currentPage ? state.currentPage : action.payload.id
});
// called on fetch page...
case 'FETCH_SUCCESS':
if (state.path.indexOf(action.payload.id) < 0) {
newNodes = state.path.concat([action.payload.id]);
}
case 'FETCH_SUCCESS':
if (state.path.indexOf(action.payload.id) < 0) {
newNodes = state.path.concat([action.payload.id]);
}
return Object.assign({}, state, {
isFetching: false,
path: newNodes,
});
return _.assign({}, state, {
isFetching: false,
path: newNodes,
});
case 'PUSH_PAGE':
return Object.assign({}, state, {
path: state.path.concat([action.payload])
});
return state;
case 'PUSH_PAGE':
return _.assign({}, state, {
path: state.path.concat([action.payload])
});
case 'POP_PAGE':
let poppedNodes = state.path.length > 1 ? state.path.slice(0, -1) : state.path;
return Object.assign({}, state, {
path: poppedNodes,
});
case 'POP_PAGE':
return _.assign({}, state, {
path: state.path.length > 1 ? state.path.slice(0, -1) : state.path,
});
case 'FETCH_CHILDREN_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
pageTypes: action.payload.json.__types,
});
case 'FETCH_CHILDREN_SUCCESS':
return _.assign({}, state, {
isFetching: false,
// eslint-disable-next-line no-underscore-dangle
pageTypes: _.assign({}, state.pageTypes, action.payload.json.__types),
});
case 'SET_FILTER':
return Object.assign({}, state, {
filter: action.filter
});
case 'SET_FILTER':
return _.assign({}, state, {
filter: action.filter
});
default:
return state;
}
return state;
}

Wyświetl plik

@ -1,99 +1,114 @@
function children(state={
import _ from 'lodash';
const childrenDefaultState = {
items: [],
count: 0,
isFetching: false
}, action) {
};
switch(action.type) {
case 'FETCH_CHILDREN_START':
return Object.assign({}, state, {
isFetching: true
});
const children = (state = childrenDefaultState, action) => {
switch (action.type) {
case 'FETCH_CHILDREN_START':
return _.assign({}, state, {
isFetching: true,
});
case 'FETCH_CHILDREN_SUCCESS':
return Object.assign({}, state, {
items: action.payload.json.items.map(item => { return item.id }),
count: action.payload.json.meta.total_count,
isFetching: false,
isLoaded: true
});
case 'FETCH_CHILDREN_SUCCESS':
return _.assign({}, state, {
items: action.payload.json.items.map(item => item.id),
count: action.payload.json.meta.total_count,
isFetching: false,
isLoaded: true,
});
default:
return state;
}
return state;
}
};
const defaultState = {
isError: false,
isFetching: false,
isLoaded: false,
children: children(undefined, {})
};
// TODO Why isn't the default state used on init?
export default function nodes(state = {}, action) {
let defaults = {
isError: false,
isFetching: false,
isLoaded: false,
children: children(undefined, {})
};
switch (action.type) {
case 'FETCH_CHILDREN_START':
// TODO Very hard to understand this code. To refactor.
return _.assign({}, state, {
[action.payload]: _.assign({}, state[action.payload], {
isFetching: true,
children: children(state[action.payload] ? state[action.payload].children : undefined, action)
})
});
switch(action.type) {
case 'FETCH_CHILDREN_START':
return Object.assign({}, state, {
[action.payload]: Object.assign({}, state[action.payload], {
isFetching: true,
children: children(state[action.payload] ? state[action.payload].children : undefined, action)
})
});
case 'FETCH_CHILDREN_SUCCESS':
let map = {};
action.payload.json.items.forEach(item => {
map = Object.assign({}, map, {
[item.id]: Object.assign({}, defaults, state[item.id], item, {
isLoaded: true
})
});
});
return Object.assign({}, state, map, {
[action.payload.id]: Object.assign({}, state[action.payload.id], {
isFetching: false,
children: children(state[action.payload.id].children, action)
})
});
case 'RESET_TREE':
return Object.assign({}, {});
case 'SET_FILTER':
// Unset all isLoaded states when the filter changes
let updatedState = {};
for (let _key in state) {
if (state.hasOwnProperty( _key )) {
let _obj = state[_key];
_obj.children.isLoaded = false;
updatedState[_obj.id] = Object.assign({}, _obj, { isLoaded: false })
}
}
return Object.assign({}, updatedState);
case 'FETCH_START':
return Object.assign({}, state, {
[action.payload]: Object.assign({}, defaults, state[action.payload], {
isFetching: true,
isError: false,
})
});
case 'FETCH_BRANCH_SUCCESS':
return Object.assign({}, state, {
[action.payload.id]: Object.assign({}, defaults, state[action.payload.id], action.payload.json, {
isFetching: false,
isError: false,
// eslint-disable-next-line no-case-declarations
case 'FETCH_CHILDREN_SUCCESS':
// TODO Very hard to understand this code. To refactor.
let map = {};
action.payload.json.items.forEach(item => {
map = _.assign({}, map, {
[item.id]: _.assign({}, defaultState, state[item.id], item, {
isLoaded: true
})
});
});
case 'FETCH_SUCCESS':
return state;
return _.assign({}, state, map, {
[action.payload.id]: _.assign({}, state[action.payload.id], {
isFetching: false,
children: children(state[action.payload.id].children, action)
})
});
case 'RESET_TREE':
return defaultState;
// eslint-disable-next-line no-case-declarations
case 'SET_FILTER':
// Unset all isLoaded states when the filter changes
const updatedState = {};
// TODO Do not use for in.
// TODO Very hard to understand this code. To refactor.
// eslint-disable-next-line
for (let key in state) {
if (state.hasOwnProperty(key)) {
// eslint-disable-next-line prefer-const
let obj = state[key];
obj.children.isLoaded = false;
updatedState[obj.id] = _.assign({}, obj, {
isLoaded: false,
});
}
}
return _.assign({}, updatedState);
case 'FETCH_START':
return _.assign({}, state, {
[action.payload]: _.assign({}, defaultState, state[action.payload], {
isFetching: true,
isError: false,
})
});
case 'FETCH_BRANCH_SUCCESS':
return _.assign({}, state, {
[action.payload.id]: _.assign({}, defaultState, state[action.payload.id], action.payload.json, {
isFetching: false,
isError: false,
isLoaded: true
})
});
case 'FETCH_SUCCESS':
return state;
default:
return state;
}
return state;
}

Wyświetl plik

@ -1,15 +1,23 @@
export default function transport(state={error: null, showMessage: false}, action) {
switch(action.type) {
case 'FETCH_FAILURE':
return Object.assign({}, state, {
error: action.payload.message,
showMessage: true
});
case 'CLEAR_TRANSPORT_ERROR':
return Object.assign({}, state, {
error: null,
showMessage: false
});
import _ from 'lodash';
const defaultState = {
error: null,
showMessage: false,
};
export default function transport(state = defaultState, action) {
switch (action.type) {
case 'FETCH_FAILURE':
return _.assign({}, state, {
error: action.payload.message,
showMessage: true
});
case 'CLEAR_TRANSPORT_ERROR':
return _.assign({}, state, {
error: null,
showMessage: false
});
default:
return state;
}
return state;
}

Wyświetl plik

@ -2,12 +2,12 @@ $c-explorer-bg: #4C4E4D;
$c-explorer-secondary: #aaa;
$c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
.c-explorer * {
.c-explorer, .c-explorer * {
box-sizing: border-box;
}
.c-explorer {
width: 320px;
width: 100%;
height: 500px;
background: $c-explorer-bg;
position: absolute;
@ -17,10 +17,7 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
.c-explorer--sidebar {
height: 100vh;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
left: 180px;
top: 0;
z-index: 150;
position: fixed;
}
.c-explorer__header {
@ -42,7 +39,7 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
cursor: pointer;
&:hover {
color: #fff;
color: $color-white;
background: rgba(0,0,0,0.2);
}
}
@ -65,12 +62,12 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
&:hover {
background: rgba(0,0,0,0.5);
border-color: rgba(0,0,0,0.5);
color: #fff;
color: $color-white;
}
}
.c-filter--active {
color: #fff;
color: $color-white;
border-color: rgba(255, 255, 255, .5);
}
@ -81,7 +78,7 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
margin-top: -1px;
&:hover {
color: #fff;
color: $color-white;
}
.icon {
@ -93,11 +90,11 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
.c-explorer__title {
margin: 0;
color: #fff;
color: $color-white;
}
.c-explorer__loading {
color: #fff;
color: $color-white;
padding: 1rem;
}
@ -113,7 +110,7 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
.c-explorer__placeholder {
padding: 1rem;
color: #fff;
color: $color-white;
}
.c-explorer__meta {
@ -127,28 +124,31 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
text-transform: capitalize;
}
.c-explorer__item:hover {
background: rgba(0, 0, 0, 0.25);
color: #fff;
.c-explorer__item {
display: block;
&:hover {
background: rgba(0, 0, 0, 0.25);
color: $color-white;
}
}
.c-explorer__see-more {
cursor: pointer;
padding: .5rem 1rem;
background: rgba(0,0,0,0.2);
color: #fff;
color: $color-white;
&:hover {
background: rgba(0,0,0,0.4);
}
}
.c-explorer__children {
display: inline-block;
border-radius: 50rem;
border: solid 1px #aaa;
color: #fff;
color: $color-white;
line-height: 1;
padding: .5em .3em .5em .5em;
float: right;
@ -156,29 +156,18 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
&:hover {
background: rgba(0,0,0,0.5);
}
> [aria-role='presentation'] {
display: none;
color: $color-white;
}
}
.c-status {
background: #333;
background: $color-grey-1;
color: #ddd;
text-transform: uppercase;
letter-spacing: .03rem;
font-size: 10px;
}
.c-status--live {
}
.c-explorer__drawer {
position: absolute;
bottom: 0;
@ -187,16 +176,17 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
overflow-y: auto;
}
.c-explorer__overflow {
max-width: 12rem;
display: block;
text-transform: uppercase;
float: left;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
// =============================================================================
// TODO: move to their own component..
// =============================================================================
@ -210,13 +200,6 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
line-height: 1.5;
}
.u-overflow {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.c-explorer__rel {
position: relative;
display: block;
@ -224,7 +207,6 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
width: 100%;
}
.c-explorer__parent-name {
position: absolute;
width: 100%;
@ -236,18 +218,13 @@ $c-explorer-easing: cubic-bezier(0.075, 0.820, 0.165, 1.000);
.c-explorer__spinner:after {
display: inline-block;
animation: spin 0.5s infinite linear;
line-height: 1
line-height: 1;
}
// =============================================================================
// Transitions
// =============================================================================
// $out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000);
// $in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335);
$out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
$in-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
$c-explorer-duration: 200ms;
@ -306,6 +283,9 @@ $c-explorer-duration: 200ms;
opacity: 0;
}
// =============================================================================
// Toggle transition
// =============================================================================
.explorer-toggle-enter {
opacity: 0;
@ -325,7 +305,6 @@ $c-explorer-duration: 200ms;
opacity: 0;
}
// =============================================================================
// Fade transition
// =============================================================================
@ -351,26 +330,3 @@ $c-explorer-duration: 200ms;
.explorer-fade-leave-active {
opacity: 0;
}
// =============================================================================
// Header transitions
// =============================================================================
.header-push-enter {
opacity: 0;
transition: opacity .1s linear .1s;
}
.header-push-enter-active {
opacity: 1;
}
.header-push-leave {
opacity: 1;
transition: opacity .1s;
}
.header-push-leave-active {
opacity: 0;
}

Wyświetl plik

@ -1,60 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
class Toggle extends Component {
constructor(props) {
super(props)
this._sandbox = this._sandbox.bind(this);
}
componentDidUpdate() {
if (this.props.visible) {
this.refs.btn.addEventListener('click', this._sandbox);
} else {
this.refs.btn.removeEventListener('click', this._sandbox);
}
}
_sandbox(e) {
e.stopPropagation();
e.preventDefault();
this.props.onToggle(this.props.page);
}
render() {
const cls = ['icon icon-folder-open-inverse dl-trigger'];
if (this.props.loading) {
cls.push('icon-spinner');
}
return (
<a href="#" ref="btn" onClick={this._sandbox} className={cls.join(' ')}>
{this.props.label}
</a>
);
}
}
Toggle.propTypes = {
};
const mapStateToProps = (store) => {
return {
loading: store.explorer.isFetching,
visible: store.explorer.isVisible,
}
}
const mapDispatchToProps = (dispatch) => {
return {
onToggle: (id) => {
dispatch(actions.toggleExplorer());
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Toggle);

Wyświetl plik

@ -1,17 +1,24 @@
import React, { PropTypes } from 'react';
import React from 'react';
// TODO Add support for accessible label.
const Icon = ({ name, className }) => (
<span className={`icon icon-${name} ${className}`} />
const Icon = ({ name, className, title }) => (
<span className={`icon icon-${name} ${className}`} aria-hidden={!title}>
{title ? (
<span className="visuallyhidden">
{title}
</span>
) : null}
</span>
);
Icon.propTypes = {
name: PropTypes.string.isRequired,
className: PropTypes.string,
name: React.PropTypes.string.isRequired,
className: React.PropTypes.string,
title: React.PropTypes.string,
};
Icon.defaultProps = {
className: '',
title: null,
};
export default Icon;

Wyświetl plik

@ -8,6 +8,16 @@ A simple component to render an icon. Abstracts away the actual icon implementat
import { Icon } from 'wagtail';
render(
<Icon name="arrow-left" className="icon--active icon--warning" />
<Icon
name="arrow-left"
className="icon--active icon--warning"
title="Move left"
/>
);
```
### Available props
- `name`: icon name
- `className`: additional CSS classes to add to the element
- `title`: accessible label intended for screen readers

Wyświetl plik

@ -1,5 +0,0 @@
// Icon
.c-icon {
display: block;
}

Wyświetl plik

@ -1,7 +0,0 @@
import Explorer from './explorer';
import LoadingIndicator from './LoadingIndicator';
import StateIndicator from './StateIndicator';
export { Explorer };
export { LoadingIndicator };
export { StateIndicator };

Wyświetl plik

@ -1,10 +0,0 @@
import React from 'react';
import { STRINGS } from 'config';
const LoadingIndicator = () => (
<div className="o-icon c-indicator is-spinning">
<span ariaRole="presentation">{STRINGS['LOADING']}...</span>
</div>
);
export default LoadingIndicator;

Wyświetl plik

@ -1 +0,0 @@
# Loading indicator

Wyświetl plik

@ -1,3 +0,0 @@
.c-indicator {
}

Wyświetl plik

@ -1,17 +0,0 @@
import React, { Component, PropTypes } from 'react';
const PublishStatus = ({ status }) => {
if (!status) {
return null;
}
let classes = ['o-pill', 'c-status', 'c-status--' + status.status];
return (
<span className={classes.join(' ')}>
{status.status}
</span>
);
}
export default PublishStatus;

Wyświetl plik

@ -1,9 +0,0 @@
# PublishStatus
About this component
## Usage
```javascript
import { PublishStatus } from 'wagtail';
```

Wyświetl plik

@ -1,5 +0,0 @@
// PublishStatus
.c-publish-status {
display: block;
}

Wyświetl plik

@ -1,14 +0,0 @@
import React, { Component, PropTypes } from 'react';
import moment from 'moment';
const PublishedTime = ({publishedAt}) => {
let date = moment(publishedAt);
let str = publishedAt ? date.format('DD.MM.YYYY') : 'No date';
return (
<span>{str}</span>
);
}
export default PublishedTime;

Wyświetl plik

@ -1,9 +0,0 @@
# PublishedTime
About this component
## Usage
```javascript
import { PublishedTime } from 'wagtail';
```

Wyświetl plik

@ -1,5 +0,0 @@
// PublishedTime
.c-published-time {
display: block;
}

Wyświetl plik

@ -1,9 +0,0 @@
# StateIndicator
About this component
## Usage
```javascript
import { StateIndicator } from 'wagtail';
```

Wyświetl plik

@ -1,16 +0,0 @@
import React, { Component, PropTypes } from 'react';
export default class StateIndicator extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="c-state-indicator">
</div>
);
}
}

Wyświetl plik

@ -1,5 +0,0 @@
// StateIndicator
.c-state-indicator {
display: block;
}

Wyświetl plik

@ -0,0 +1,10 @@
export const PAGES_ROOT_ID = 'root';
export const EXPLORER_ANIM_DURATION = 220;
// TODO Add back in when we want to support explorer that displays pages
// without children (API call without has_children=1).
export const EXPLORER_FILTERS = [
{ id: 1, label: 'A', filter: null },
{ id: 2, label: 'B', filter: 'has_children=1' }
];

Wyświetl plik

@ -0,0 +1,25 @@
import {
PAGES_ROOT_ID,
EXPLORER_ANIM_DURATION,
EXPLORER_FILTERS,
} from './config';
describe('config', () => {
describe('PAGES_ROOT_ID', () => {
it('exists', () => {
expect(PAGES_ROOT_ID).toBeDefined();
});
});
describe('EXPLORER_ANIM_DURATION', () => {
it('exists', () => {
expect(EXPLORER_ANIM_DURATION).toBeDefined();
});
});
describe('EXPLORER_FILTERS', () => {
it('exists', () => {
expect(EXPLORER_FILTERS).toBeDefined();
});
});
});

Wyświetl plik

@ -1,16 +0,0 @@
export const API = global.wagtailConfig.api;
export const API_PAGES = global.wagtailConfig.api.pages;
export const PAGES_ROOT_ID = 'root';
export const STRINGS = global.wagtailConfig.strings;
export const EXPLORER_ANIM_DURATION = 220;
export const ADMIN_PAGES = global.wagtailConfig.urls.pages;
export const EXPLORER_FILTERS = [
// TODO Add back in when we want to support explorer without has_children=1
// { id: 1, label: 'A', filter: null },
// { id: 2, label: 'B', filter: 'has_children=1' }
];

Wyświetl plik

@ -0,0 +1,5 @@
export const ADMIN_API = global.wagtailConfig.ADMIN_API;
export const STRINGS = global.wagtailConfig.STRINGS;
export const ADMIN_URLS = global.wagtailConfig.ADMIN_URLS;
export const DATE_FORMAT = 'DD.MM.YYYY';

Wyświetl plik

@ -0,0 +1,32 @@
import {
ADMIN_API,
STRINGS,
ADMIN_URLS,
DATE_FORMAT,
} from './wagtail';
describe('config', () => {
describe('ADMIN_API', () => {
it('exists', () => {
expect(ADMIN_API).toBeDefined();
});
});
describe('STRINGS', () => {
it('exists', () => {
expect(STRINGS).toBeDefined();
});
});
describe('ADMIN_URLS', () => {
it('exists', () => {
expect(ADMIN_URLS).toBeDefined();
});
});
describe('DATE_FORMAT', () => {
it('exists', () => {
expect(DATE_FORMAT).toBeDefined();
});
});
});

Wyświetl plik

@ -1 +1,20 @@
export * from './components';
/**
* Entry point for the wagtail package.
* Re-exports components and other modules via a cleaner API.
*/
import Button from './components/Button/Button';
import Explorer from './components/Explorer/Explorer';
import Icon from './components/Icon/Icon';
import LoadingIndicator from './components/LoadingIndicator/LoadingIndicator';
import AbsoluteDate from './components/AbsoluteDate/AbsoluteDate';
import PublicationStatus from './components/PublicationStatus/PublicationStatus';
export {
Button,
Explorer,
Icon,
LoadingIndicator,
AbsoluteDate,
PublicationStatus,
};

Wyświetl plik

@ -0,0 +1,41 @@
import {
Button,
Explorer,
Icon,
LoadingIndicator,
AbsoluteDate,
PublicationStatus,
} from './index';
describe('wagtail package API', () => {
describe('Button', () => {
it('exists', () => {
expect(Button).toBeDefined();
});
});
describe('Explorer', () => {
it('exists', () => {
expect(Explorer).toBeDefined();
});
});
describe('Icon', () => {
it('exists', () => {
expect(Icon).toBeDefined();
});
});
describe('LoadingIndicator', () => {
it('exists', () => {
expect(LoadingIndicator).toBeDefined();
});
});
describe('AbsoluteDate', () => {
it('exists', () => {
expect(AbsoluteDate).toBeDefined();
});
});
describe('PublicationStatus', () => {
it('exists', () => {
expect(PublicationStatus).toBeDefined();
});
});
});

Wyświetl plik

@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import React from 'react';
const {{ name }} = (props) => {
const {{ name }} = () => {
return (
<div className="c-{{ slug }}">
</div>

Wyświetl plik

@ -1,25 +1,15 @@
// TODO Move this file to the client/tests/components directory.
import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import '../stubs';
import { shallow } from 'enzyme';
import {{ name }} from '../../src/components/{{ slug }}/{{ name }}';
import {{ name }} from '../../src/components/{{ name }}/{{ name }}';
describe('{{ name }}', () => {
it('exists', () => {
expect({{ name }}).to.exist;
expect({{ name }}).toBeDefined();
});
it('contains spec with an expectation', () => {
expect(shallow(<{{ name }} />).contains(<div className="c-{{ slug }}" />)).to.equal(true);
});
it('contains spec with an expectation', () => {
expect(shallow(<{{ name }} />).is('.c-{{ slug }}')).to.equal(true);
});
it('contains spec with an expectation', () => {
expect(mount(<{{ name }} />).find('.c-{{ slug }}').length).to.equal(1);
it('basic', () => {
expect(shallow(<{{ name }} />)).toMatchSnapshot();
});
});

Wyświetl plik

@ -1,21 +0,0 @@
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import '../stubs';
import Icon from '../../src/components/icon/Icon';
describe('Icon', () => {
it('exists', () => {
// eslint-disable-next-line no-unused-expressions
expect(Icon).to.exist;
});
it('has just icon classes by default', () => {
expect(shallow(<Icon name="test" />).is('.icon.icon-test')).to.equal(true);
});
it('has additional classes if specified', () => {
expect(shallow(<Icon name="test" className="icon-red icon-big" />).prop('className')).to.contain('icon-red icon-big');
});
});

Wyświetl plik

@ -1,39 +0,0 @@
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import '../stubs';
import Explorer from '../../src/components/explorer/Explorer';
import ExplorerItem from '../../src/components/explorer/ExplorerItem';
describe('Explorer', () => {
it('exists', () => {
// eslint-disable-next-line no-unused-expressions
expect(Explorer).to.exist;
});
describe('ExplorerItem', () => {
const props = {
data: {
meta: {
children: {
count: 0,
}
}
},
};
it('exists', () => {
// eslint-disable-next-line no-unused-expressions
expect(ExplorerItem).to.exist;
});
it('has item metadata', () => {
expect(shallow(<ExplorerItem {...props} />).find('.c-explorer__meta')).to.have.lengthOf(1);
});
it('metadata contains item type', () => {
expect(shallow(<ExplorerItem {...props} typeName="Foo" />).find('.c-explorer__meta').text()).to.contain('Foo');
});
});
});

Wyświetl plik

@ -1,12 +1,25 @@
/**
* Test stubs to mirror available global variables.
* Those variables usually come from the back-end via templates.
* See /wagtailadmin/templates/wagtailadmin/admin_base.html.
*/
global.wagtailConfig = {
api: {
documents: '/admin/api/v1beta/documents/',
images: '/admin/api/v1beta/images/',
pages: '/admin/api/v1beta/pages/',
ADMIN_API: {
DOCUMENTS: '/admin/api/v2beta/documents/',
IMAGES: '/admin/api/v2beta/images/',
PAGES: '/admin/api/v2beta/pages/',
},
ADMIN_URLS: {
PAGES: '/admin/pages/',
},
STRINGS: {
EXPLORER: 'Explorer',
LOADING: 'Loading...',
NO_RESULTS: 'No results',
SEE_CHILDREN: 'See Children',
NO_DATE: 'No date',
},
urls: {
pages: '/admin/pages/',
}
};
global.wagtailVersion = '1.6a1';

Wyświetl plik

@ -17,7 +17,7 @@ function entryPoint(filename) {
var name = appName(filename);
var entryName = path.basename(filename, '.entry.js');
var outputPath = path.join('wagtail', name, 'static', name, 'js', entryName);
return [outputPath, filename];
return [outputPath, ['babel-polyfill', filename]];
}

Wyświetl plik

@ -5,6 +5,14 @@ var config = base('development');
// development overrides go here
config.watch = true;
// add poll-options for in vagrant development
// See http://andrewhfarmer.com/webpack-watch-in-vagrant-docker/
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
// See http://webpack.github.io/docs/configuration.html#devtool
config.devtool = 'inline-source-map';

26581
npm-shrinkwrap.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -13,19 +13,32 @@
]
},
"browserify-shim": {},
"jest": {
"rootDir": "client",
"setupFiles": [
"./tests/stubs.js"
],
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
},
"devDependencies": {
"babel-cli": "^6.5.1",
"babel-core": "^6.5.2",
"babel-jest": "^18.0.0",
"babel-loader": "^6.2.3",
"babel-plugin-lodash": "^3.2.9",
"babel-polyfill": "^6.5.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"chai": "^3.5.0",
"enzyme": "^2.3.0",
"enzyme-to-json": "^1.4.5",
"eslint": "^2.9.0",
"eslint-config-wagtail": "^0.1.0",
"eslint-config-wagtail": "^0.1.1",
"eslint-plugin-import": "^1.8.1",
"eslint-plugin-jsx-a11y": "^1.5.3",
"eslint-plugin-react": "^4.3.0",
"eslint-plugin-react": "^5.2.2",
"exports-loader": "^0.6.3",
"glob": "^7.0.0",
"gulp": "~3.8.11",
"gulp-autoprefixer": "~3.0.2",
@ -33,32 +46,24 @@
"gulp-sass": "~2.3.1",
"gulp-sourcemaps": "~1.5.2",
"gulp-util": "~2.2.14",
"isparta": "^4.0.0",
"lodash": "^4.5.1",
"mocha": "^2.4.5",
"imports-loader": "^0.6.5",
"jest": "^18.1.0",
"mustache": "^2.2.1",
"react-addons-test-utils": "^0.14.8",
"redux-devtools": "^3.1.1",
"react-addons-test-utils": "^15.4.2",
"require-dir": "^0.3.0",
"sinon": "^1.17.3"
"webpack": "^1.12.14"
},
"dependencies": {
"babel-polyfill": "^6.5.0",
"exports-loader": "^0.6.3",
"imports-loader": "^0.6.5",
"moment": "^2.11.2",
"react": "^0.14.7",
"react-accessible-modal": "0.0.5",
"react-addons-css-transition-group": "^0.14.7",
"react-dom": "^0.14.7",
"react-onclickoutside": "^4.5.0",
"react-redux": "^4.4.0",
"redux": "^3.3.1",
"redux-actions": "^0.10.0",
"redux-logger": "^2.6.0",
"redux-thunk": "^1.0.3",
"webpack": "^1.12.14",
"whatwg-fetch": "^0.11.0"
"lodash": "^4.17.4",
"moment": "^2.17.1",
"react": "^15.4.2",
"react-addons-css-transition-group": "^15.4.2",
"react-dom": "^15.4.2",
"react-redux": "^5.0.2",
"redux": "^3.6.0",
"redux-actions": "^1.2.1",
"redux-thunk": "^2.2.0",
"whatwg-fetch": "^2.0.2"
},
"scripts": {
"postinstall": "cd ./client; npm install; cd ..",
@ -68,9 +73,9 @@
"lint:js": "eslint --max-warnings 16 ./client",
"lint": "npm run lint:js",
"test": "npm run test:unit",
"test:unit": "env NODE_PATH=$NODE_PATH:$PWD/client/src mocha --compilers js:babel-core/register client/tests/**/*.test.js",
"test:unit:watch": "env NODE_PATH=$NODE_PATH:$PWD/client/src mocha --watch --compilers js:babel-core/register client/tests/**/*.test.js",
"test:unit:coverage": "env NODE_PATH=$NODE_PATH:$PWD/client/src babel-node $(npm bin)/isparta cover node_modules/mocha/bin/_mocha -- client/tests/**/*.test.js",
"test:unit": "jest",
"test:unit:watch": "jest --watch",
"test:unit:coverage": "jest --coverage",
"component": "node ./client/src/cli/index.js component --dir ./client/src/components/"
}
}

Wyświetl plik

@ -1,47 +1,48 @@
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger'
import thunkMiddleware from 'redux-thunk'
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import Explorer from 'components/explorer/Explorer';
import ExplorerToggle from 'components/explorer/toggle';
import ExplorerToggle from 'components/explorer/ExplorerToggle';
import rootReducer from 'components/explorer/reducers';
const initExplorer = () => {
const explorerNode = document.querySelector('#explorer');
const toggleNode = document.querySelector('[data-explorer-menu-url]');
document.addEventListener('DOMContentLoaded', e => {
const top = document.querySelector('.wrapper');
const div = document.createElement('div');
const trigger = document.querySelector('[data-explorer-menu-url]');
if (explorerNode && toggleNode) {
const middleware = [
thunkMiddleware,
];
let rect = trigger.getBoundingClientRect();
let triggerParent = trigger.parentNode;
let label = trigger.innerText;
const store = createStore(rootReducer, {}, compose(
applyMiddleware(...middleware),
// Expose store to Redux DevTools extension.
window.devToolsExtension ? window.devToolsExtension() : f => f
));
top.parentNode.appendChild(div);
const loggerMiddleware = createLogger();
const store = createStore(
rootReducer,
applyMiddleware(loggerMiddleware, thunkMiddleware)
);
ReactDOM.render((
const toggle = (
<Provider store={store}>
<ExplorerToggle label={label} />
<ExplorerToggle>{toggleNode.innerText}</ExplorerToggle>
</Provider>
),
triggerParent
);
);
ReactDOM.render(
<Provider store={store}>
<Explorer type={'sidebar'} top={0} left={rect.right} defaultPage={1} />
</Provider>,
div
);
const explorer = (
<Provider store={store}>
<Explorer type="sidebar" defaultPage={1} />
</Provider>
);
ReactDOM.render(toggle, toggleNode.parentNode);
ReactDOM.render(explorer, explorerNode);
}
};
/**
* Admin JS entry point. Add in here code to run once the page is loaded.
*/
document.addEventListener('DOMContentLoaded', () => {
initExplorer();
});

Wyświetl plik

@ -1,15 +1,14 @@
// min z-index: 500;
// max z-index: unknown;
// TODO Clean-up unused code in the new version of the explorer
$explorer-z-index: 500;
.explorer {
pointer-events: none;
width: 100%;
position: relative;
top: 0;
left: 0;
display: none;
ul {
background: $color-grey-1;
@ -30,33 +29,6 @@ $explorer-z-index: 500;
}
}
a {
text-decoration: none;
padding: 0.9em;
color: $color-white;
display: block;
position: relative;
outline: none;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:before {
opacity: 0.5;
margin-right: 0.5em;
font-size: 1.5em;
}
&:hover {
background: $color-teal-dark;
color: $color-white;
}
}
.has-children a {
padding-right: 5em;
}
.children {
position: absolute;
z-index: $explorer-z-index + 1;

Wyświetl plik

@ -244,6 +244,8 @@ body.nav-open {
// Explorer open condition, widens navigation area
body.explorer-open {
overflow: hidden;
.wrapper {
transform: translate3d($menu-width*2, 0, 0);
-webkit-transform: translate3d($menu-width*2, 0, 0);
@ -442,7 +444,6 @@ body.explorer-open {
position: absolute;
top: 0;
left: 99%;
margin-top: 175px; // same as .nav-main minus 1 pixel for border
}
.dl-menu {
@ -456,6 +457,19 @@ body.explorer-open {
}
body.explorer-open {
// TODO Do we want this layer appearing when the explorer is open?
&:after {
content: '';
position: fixed;
background: rgba(255, 255, 255, 0.5);
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 1;
animation: opacity .2s ease-out;
}
.wrapper {
-webkit-transform: none;
transform: none;

Wyświetl plik

@ -21,38 +21,7 @@
@import 'wagtailadmin/scss/fonts';
// scss-lint:disable all
#wagtail {
@import '../../../../../client/scss/style';
}
// scss-lint:enable all
@keyframes matteIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.u-explorer-open {
overflow: hidden;
&:after {
// content: '';
// position: fixed;
// background: rgba(255, 255, 255, 0.5);
// width: 100%;
// height: 100%;
// top: 0;
// left: 0;
// opacity: 1;
// animation: matteIn .2s ease-out;
}
}
@import '../../../../../client/scss/style';
html {
background: $color-grey-4;
@ -555,201 +524,3 @@ footer,
// a {
// @include transition(color 0.2s ease, background-color 0.2s ease);
// }
/**
// -----------------------------------------------------------------------------
// Modal lightboxes
// -----------------------------------------------------------------------------
//
// As of 2015, the vertical-align: middle table is still the best cross-browser
// way to vertically centre stuff. This modal component uses this pattern with
// the following structure:
//
// <div class="modal modal--active">
// <div class="modal__table">
// <div class="modal__center">
// <div class="modal__content">
// Hello!
// </div>
// </div>
// </div>
// </div>
//
// Requires '_animations.scss';
$z-index-modal: 1;
$z-index-modal-matte: 2;
$z-index-modal-content: 3;
$color-modal-close-bg: #333;
$color-modal-close-text: #fff;
$color-modal-content-bg: #fff;
$color-black-opacity-093: rgba(255, 255, 255, .93);
$color-dark-grey: #222;
*/
.u-body-modal-active {
overflow: hidden;
}
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
animation: modal-in .15s ease-out 0s backwards;
}
.modal--active {
display: block;
}
.modal--exit {
animation: modal-out .4s ease-out .4s forwards;
}
.modal--exit .modal__content {
animation: affordance-out .4s ease-in 0s forwards;
}
.modal--exit .modal__close {
animation: affordance-out-right .4s ease-in 0s forwards;
}
.modal__overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
background: rgba(0, 0, 0, .93);
}
.modal__table {
display: table;
position: relative;
width: 100%;
height: 100%;
vertical-align: middle;
}
.modal__center {
display: table-cell;
text-align: center;
vertical-align: middle;
animation: modal-in .15s ease-out .25s backwards;
}
.modal__content {
display: inline-block;
position: relative;
z-index: 3;
max-width: 32em;
min-width: 10.5em;
min-height: 6em;
padding: 1em 2em;
background: #fff;
animation: affordance-in .5s cubic-bezier(.075, .82, .165, 0) .3s backwards;
}
.modal__close {
position: absolute;
top: 0;
right: 0;
z-index: 3;
padding: .9rem 1.35rem 1.1rem;
font-size: 2em;
line-height: 1;
color: #fff;
cursor: pointer;
background: #333;
animation: affordance-in-right .5s cubic-bezier(.075, .82, .165, 0) .25s backwards;
}
.modal__close:hover,
.modal__close:active {
color: #fff;
background: #222;
}
/**
* Animation keyframes
*/
@keyframes modal-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes modal-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes affordance-in {
0% {
opacity: 0;
transform: translateY(5%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes affordance-out {
0% {
opacity: 1;
transform: translateY(0%);
}
100% {
opacity: 0;
transform: translateY(5%);
}
}
@keyframes affordance-in-right {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes affordance-out-right {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(100%);
}
}

Wyświetl plik

@ -18,20 +18,22 @@
<script>
(function(document, window) {
window.wagtailConfig = window.wagtailConfig || {};
wagtailConfig.api = {
pages: '{% url "wagtailadmin_api_v1:pages:listing" %}',
documents: '{% url "wagtailadmin_api_v1:documents:listing" %}',
images: '{% url "wagtailadmin_api_v1:images:listing" %}'
wagtailConfig.ADMIN_API = {
PAGES: '{% url "wagtailadmin_api_v1:pages:listing" %}',
DOCUMENTS: '{% url "wagtailadmin_api_v1:documents:listing" %}',
IMAGES: '{% url "wagtailadmin_api_v1:images:listing" %}'
};
wagtailConfig.strings = {
EXPLORER: "{% trans 'Explorer' %}",
LOADING: "{% trans 'Loading' %}",
NO_RESULTS: "{% trans 'No results' %}",
SEE_CHILDREN: "{% trans 'See Children' %}"
wagtailConfig.STRINGS = {
EXPLORER: "{% trans 'Explorer' %}",
LOADING: "{% trans 'Loading...' %}",
NO_RESULTS: "{% trans 'No results' %}",
SEE_CHILDREN: "{% trans 'See Children' %}",
NO_DATE: "{% trans 'No date' %}",
};
wagtailConfig.urls = {
pages: '{% url "wagtailadmin_explore_root" %}'
wagtailConfig.ADMIN_URLS = {
PAGES: '{% url "wagtailadmin_explore_root" %}'
};
})(document, window);
</script>