kopia lustrzana https://github.com/wagtail/wagtail
Refactor explorer code with tests using Jest
rodzic
743a8304a6
commit
8bf2c9bf2e
3
.babelrc
3
.babelrc
|
@ -2,5 +2,6 @@
|
|||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
]
|
||||
],
|
||||
"plugins": ["lodash"]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"extends": "wagtail"
|
||||
"extends": "wagtail",
|
||||
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
@import '../src/components/explorer/style';
|
||||
@import '../src/components/loading-indicator/style';
|
||||
@import '../src/components/state-indicator/style';
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
exports[`AbsoluteDate #time 1`] = `
|
||||
<span>
|
||||
19.09.2016
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`AbsoluteDate basic 1`] = `
|
||||
<span>
|
||||
No date
|
||||
</span>
|
||||
`;
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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} />
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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],
|
||||
}
|
||||
} />
|
||||
`;
|
|
@ -0,0 +1,11 @@
|
|||
exports[`LoadingSpinner basic 1`] = `
|
||||
<div
|
||||
className="c-explorer__loading">
|
||||
<Icon
|
||||
className="c-explorer__spinner"
|
||||
name="spinner"
|
||||
title={null} />
|
||||
|
||||
Loading...
|
||||
</div>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
exports[`LoadingIndicator basic 1`] = `
|
||||
<div
|
||||
className="o-icon c-indicator is-spinning">
|
||||
<span
|
||||
ariaRole="presentation">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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.
|
|
@ -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>
|
||||
`;
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
# Explorer
|
|
@ -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)))));
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// Icon
|
||||
|
||||
.c-icon {
|
||||
display: block;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import Explorer from './explorer';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import StateIndicator from './StateIndicator';
|
||||
|
||||
export { Explorer };
|
||||
export { LoadingIndicator };
|
||||
export { StateIndicator };
|
|
@ -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;
|
|
@ -1 +0,0 @@
|
|||
# Loading indicator
|
|
@ -1,3 +0,0 @@
|
|||
.c-indicator {
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -1,9 +0,0 @@
|
|||
# PublishStatus
|
||||
|
||||
About this component
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { PublishStatus } from 'wagtail';
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
// PublishStatus
|
||||
|
||||
.c-publish-status {
|
||||
display: block;
|
||||
}
|
|
@ -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;
|
|
@ -1,9 +0,0 @@
|
|||
# PublishedTime
|
||||
|
||||
About this component
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { PublishedTime } from 'wagtail';
|
||||
```
|
|
@ -1,5 +0,0 @@
|
|||
// PublishedTime
|
||||
|
||||
.c-published-time {
|
||||
display: block;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
# StateIndicator
|
||||
|
||||
About this component
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { StateIndicator } from 'wagtail';
|
||||
```
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
// StateIndicator
|
||||
|
||||
.c-state-indicator {
|
||||
display: block;
|
||||
}
|
|
@ -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' }
|
||||
];
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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' }
|
||||
];
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const {{ name }} = (props) => {
|
||||
const {{ name }} = () => {
|
||||
return (
|
||||
<div className="c-{{ slug }}">
|
||||
</div>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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]];
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Plik diff jest za duży
Load Diff
61
package.json
61
package.json
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue