Tooling for modern front-end components: React JS, ES6, and BEM CSS

Thanks to @justinvdm for the help

Merges: #2275
pull/2275/merge
Josh Barr 2016-02-24 12:44:14 +02:00 zatwierdzone przez Karl Hobley
rodzic 985a987436
commit 14f02a0b50
47 zmienionych plików z 657 dodań i 51 usunięć

6
.babelrc 100644
Wyświetl plik

@ -0,0 +1,6 @@
{
"presets": [
"es2015",
"react"
]
}

Wyświetl plik

@ -6,11 +6,12 @@ build:
commands:
- XDG_CACHE_HOME=/drone/pip-cache pip install flake8
- flake8 wagtail
jscs:
js:
image: node:4.2.4
commands:
- npm install -g jscs@"^1.12.0" --quiet
- jscs ./wagtail
- npm install --quiet
- npm run lint
- npm run test:unit
scss-lint:
image: wagtail-scss-lint
commands:

34
.eslintignore 100644
Wyświetl plik

@ -0,0 +1,34 @@
node_modules
*.min.js
**/lib/
public/
coverage/
gulp/
**/vendor/
gulpfile.js
client/src/cli
wagtail/wagtailadmin/static
wagtail/wagtaildocs/static
wagtail/wagtailimages/static
wagtail/wagtailimages/static
wagtail/wagtailembeds/static
wagtail/wagtailsnippets/static
wagtail/wagtailusers/static
wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/inline_panel.js
wagtail/contrib/wagtailsearchpromotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js
wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js
wagtail/wagtailsnippets/templates/wagtailsnippets/chooser/chosen.js
wagtail/wagtailimages/templates/wagtailimages/chooser/image_chosen.js
wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.js
wagtail/wagtailsearch/templates/wagtailsearch/queries/chooser/chooser.js
wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.js
wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js
wagtail/wagtailembeds/templates/wagtailembeds/chooser/chooser.js
wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.js
wagtail/wagtaildocs/templates/wagtaildocs/chooser/document_chosen.js
wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.js
wagtail/wagtailadmin/templates/wagtailadmin/chooser/external_link_chosen.js
wagtail/wagtailadmin/templates/wagtailadmin/chooser/external_link.js
wagtail/wagtailadmin/templates/wagtailadmin/chooser/email_link.js
wagtail/wagtailadmin/templates/wagtailadmin/chooser/browse.js
wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy_done.js

25
.eslintrc 100644
Wyświetl plik

@ -0,0 +1,25 @@
{
"extends": "airbnb",
"rules": {
"indent": [2, 2],
"max-len": [1, 120, 4, {"ignoreUrls": true}],
"id-length": [1, {"min": 2, "exceptions": ["x", "y", "e", "i", "j", "k", "d", "n", "_", "$"]}],
"object-shorthand": [2, "methods"],
"no-new": [1],
"comma-dangle": [0],
"no-multi-spaces": [0],
"prefer-template": [0],
"no-var": [0],
"prefer-arrow-callback": [1],
"no-undef": [1],
"no-unused-vars": [1],
"no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],
"react/sort-comp": [0],
"react/jsx-boolean-value": [0],
"react/jsx-no-bind": [0],
"react/prefer-es6-class": [0, 'never'],
"react/jsx-indent-props": [2, 4],
"jsx-quotes": [1, "prefer-double"]
}
}

1
.gitignore vendored
Wyświetl plik

@ -18,3 +18,4 @@ npm-debug.log
*.iml
*.ipr
*.iws
coverage/

37
.jscsrc
Wyświetl plik

@ -1,37 +0,0 @@
{
"validateIndentation": 4,
"safeContextKeyword": ["_this", "widget", "jcropapi"],
"requireSpaceBeforeKeywords": [
"else",
"while",
"catch"
],
"disallowMultipleVarDecl": "exceptUndefined",
"excludeFiles": [
"node_modules/**",
"**/*.min.js",
"**/vendor/**/*.js",
"./wagtail/wagtailadmin/static/**",
"./wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/inline_panel.js",
"./wagtail/contrib/wagtailsearchpromotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js",
"./wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js",
"./wagtail/wagtailsnippets/templates/wagtailsnippets/chooser/chosen.js",
"./wagtail/wagtailimages/templates/wagtailimages/chooser/image_chosen.js",
"./wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.js",
"./wagtail/wagtailsearch/templates/wagtailsearch/queries/chooser/chooser.js",
"./wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.js",
"./wagtail/wagtailembeds/templates/wagtailembeds/chooser/embed_chosen.js",
"./wagtail/wagtailembeds/templates/wagtailembeds/chooser/chooser.js",
"./wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.js",
"./wagtail/wagtaildocs/templates/wagtaildocs/chooser/document_chosen.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/chooser/external_link_chosen.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/chooser/external_link.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/chooser/email_link.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/chooser/browse.js",
"./wagtail/wagtailadmin/templates/wagtailadmin/page_privacy/set_privacy_done.js"
],
"fileExtensions": [".js"],
"preset":"airbnb",
"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties"
}

Wyświetl plik

42
client/README.md 100644
Wyświetl plik

@ -0,0 +1,42 @@
# Wagtail client-side components
This library aims to give developers the ability to subclass and configure Wagtail's UI components.
## Usage
```
npm install wagtail
```
```javascript
import { Explorer } from 'wagtail';
...
<Explorer onChoosePage={(page)=> { console.log(`You picked ${page}`); }} />
```
## Available components
TODO
- [ ] Explorer
- [ ] Modal
- [ ] DatePicker
- [ ] LinkChooser
- [ ] DropDown
## Building in development
Run `webpack` from the Wagtail project root.
```
webpack
```
## How to release
The front-end is bundled at the same time as the Wagtail project, via `setuptools`. You'll need to set the `__semver__` property to a npm-compliant version number in `wagtail.wagtailcore`.

Wyświetl plik

@ -0,0 +1,18 @@
{
"name": "wagtail",
"license": "BSD-3-Clause",
"author": "Wagtail",
"version": "0.0.2",
"bin": {
"wagtail": "./src/cli/index.js"
},
"scripts": {
"test": "npm test"
},
"main": "src/index.js",
"description": "Wagtail's client side code",
"dependencies": {
"mustache": "^2.2.1",
"yargs": "^4.2.0"
}
}

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1 @@
@import 'objects/o.icon';

Wyświetl plik

@ -0,0 +1,3 @@
.o-icon {
display: inline-block;
}

Wyświetl plik

@ -0,0 +1,4 @@
.is-spinning {
}

Wyświetl plik

@ -0,0 +1,10 @@
// =============================================================================
// Wagtail CMS main stylesheet
// =============================================================================
@import 'objects';
@import 'components';
@import 'states/states';
@import 'states/animations';
@import 'utilities/utilities';
@import 'themes/themes';

Wyświetl plik

@ -0,0 +1,9 @@
.u-text-center {
text-align: center;
}
@media screen and (min-width: 15em) {
.u-text-center\@sm {
text-align: center;
}
}

Wyświetl plik

@ -0,0 +1,83 @@
var path = require('path');
var fs = require('fs');
var Mustache = require('mustache');
var TEMPLATES = path.join(__dirname, '..', '..', 'template');
var files = [
{
name: 'index.js',
template: 'component.mst'
},
{
name: 'style.scss',
template: 'style.mst'
},
{
name: 'README.md',
template: 'README.mst'
}
];
// =============================================================================
// Helper methods
// =============================================================================
function slugify(text) {
return text.toString().split(/(?=[A-Z])/).join('-').toLowerCase().trim()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/&/g, '-and-') // Replace & with 'and'
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-'); // Replace multiple - with single -
}
function write(name, data) {
fs.writeFile(name, data, function(err) {
if (err) {
return console.log('[ error ] ' + err);
}
console.log('[ created ] ' + name);
});
}
// =============================================================================
// Write files!
// =============================================================================
function run(argv) {
var name = argv.name;
var slug = slugify(name);
var directory = path.join(argv.dir, slug);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory);
} else {
console.warn('[ error ] ' + directory + ' already exists');
return;
}
files.forEach(function(file) {
var template = fs.readFileSync(path.join(TEMPLATES, file.template), 'utf8');
var newPath = path.join(directory, file.name);
var context = {
name: name,
slug: slug
};
write(newPath, Mustache.render(template, context));
});
}
function build(cli) {
return cli
.option('dir', {
default: process.env.PWD
});
}
exports.handler = run;
exports.builder = build;

Wyświetl plik

@ -0,0 +1,15 @@
#!/usr/bin/env node
var cli = require('yargs');
cli
.usage('Usage: $0 <command> [options]')
.help('help');
cli
.command(
'component <name>',
'scaffold out a wagtail component',
require('./component'));
cli
.argv;

Wyświetl plik

@ -0,0 +1 @@
# Explorer

Wyświetl plik

@ -0,0 +1,28 @@
import React, { Component, PropTypes } from 'react';
import StateIndicator from 'components/state-indicator';
export default class ExplorerItem extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { title, data } = this.props;
return (
<div className="c-explorer__item">
<h3 className="c-explorer__title">
<StateIndicator state={data.state} />
{title}
</h3>
</div>
);
}
}
ExplorerItem.propTypes = {
title: PropTypes.string,
data: PropTypes.object
};

Wyświetl plik

@ -0,0 +1,67 @@
import React, { Component, PropTypes } from 'react';
import LoadingIndicator from 'components/loading-indicator';
import ExplorerItem from './explorer-item';
import { API } from 'config';
class Explorer extends Component {
constructor(props) {
super(props);
this.state = { cursor: null };
}
componentDidMount() {
fetch(`${API}/pages/?child_of=root`)
.then(res => res.json())
.then(body => {
this.setState({
cursor: body
});
});
}
componentWillUnmount(cursor) {
}
_getPages(cursor) {
if (!cursor) {
return [];
}
return cursor.pages.map(item =>
<ExplorerItem key={item.id} title={item.title} data={item} />
);
}
getPosition() {
const { position } = this.props;
return {
left: position.right + 'px',
top: position.top + 'px'
};
}
render() {
const { cursor } = this.state;
const pages = this._getPages(cursor);
return (
<div style={this.getPosition()} className="c-explorer">
{cursor ? pages : <LoadingIndicator />}
</div>
);
}
}
Explorer.propTypes = {
onPageSelect: PropTypes.func,
initialPath: PropTypes.string,
apiPath: PropTypes.string,
size: PropTypes.number,
position: PropTypes.object
};
export default Explorer;

Wyświetl plik

@ -0,0 +1,13 @@
.c-explorer {
width: 320px;
height: 500px;
background: #333;
position: absolute;
z-index: 25;
top: 0;
left: 180px;
}
.c-explorer__item {
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,9 @@
import React from 'react';
const LoadingIndicator = () =>
<div className="o-icon c-indicator is-spinning">
<span ariaRole="presentation">Loading...</span>
</div>;
export default LoadingIndicator;

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1 @@
export const API = '/admin/api/v2beta/';

Wyświetl plik

@ -0,0 +1 @@
export * from './components';

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,10 @@
/*eslint-disable */
import { expect } from 'chai';
import Explorer from '../../src/components/explorer';
describe('Explorer', () => {
it('exists', () => {
expect(Explorer).to.exist;
});
});

Wyświetl plik

@ -7,6 +7,7 @@ var config = require('../config');
*/
gulp.task('watch', ['build'], function () {
config.apps.forEach(function(app) {
gulp.watch(path.join('./client/src/**/*.scss'), ['styles:sass']);
gulp.watch(path.join(app.sourceFiles, '*/scss/**'), ['styles:sass']);
gulp.watch(path.join(app.sourceFiles, '*/css/**'), ['styles:css']);
gulp.watch(path.join(app.sourceFiles, '*/js/**'), ['scripts']);

Wyświetl plik

@ -11,22 +11,47 @@
},
"browserify-shim": {},
"devDependencies": {
"browserify": "~3.46.1",
"browserify-shim": "~3.4.1",
"babel-cli": "^6.5.1",
"babel-core": "^6.5.2",
"babel-loader": "^6.2.3",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"chai": "^3.5.0",
"eslint": "^2.2.0",
"eslint-config-airbnb": "^6.0.2",
"eslint-plugin-react": "^4.1.0",
"glob": "^7.0.0",
"gulp": "~3.8.11",
"gulp-autoprefixer": "~3.0.2",
"gulp-rename": "^1.2.2",
"gulp-sass": "~2.0.4",
"gulp-sourcemaps": "~1.5.2",
"gulp-util": "~2.2.14",
"jscs": "^1.12.0",
"require-dir": "^0.3.0"
"isparta": "^4.0.0",
"lodash": "^4.5.1",
"mocha": "^2.4.5",
"mustache": "^2.2.1",
"redux-devtools": "^3.1.1",
"require-dir": "^0.3.0",
"sinon": "^1.17.3"
},
"dependencies": {
"exports-loader": "^0.6.3",
"imports-loader": "^0.6.5",
"react-redux": "^4.4.0",
"redux": "^3.3.1",
"whatwg-fetch": "^0.11.0"
},
"dependencies": {},
"scripts": {
"build": "gulp build",
"start": "gulp watch",
"lint:js": "./node_modules/.bin/jscs ./wagtail || true",
"format:js": "./node_modules/.bin/jscs ./wagtail -x"
"install": "pushd ./client; npm install; popd",
"build": "gulp build; webpack --progress --colors --config webpack.prd.config.js",
"watch": "webpack --progress --colors --config webpack.dev.config.js & gulp watch",
"start": "npm run watch",
"lint:js": "eslint --max-warnings 16 webpack.*.config.js ./client/src",
"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:coverage": "env NODE_PATH=$NODE_PATH:$PWD/client/src babel-node $(npm bin)/isparta cover node_modules/mocha/bin/_mocha -- client/tests/**/*.test.js",
"component": "node ./client/src/cli/index.js component --dir ./client/src/components/"
}
}

Wyświetl plik

@ -1,11 +1,15 @@
from __future__ import absolute_import, print_function, unicode_literals
import os
import io
import subprocess
import json
from setuptools import Command
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.sdist import sdist as base_sdist
from wagtail.wagtailcore import __semver__
class assets_mixin(object):
@ -17,6 +21,37 @@ class assets_mixin(object):
print('Error compiling assets: ' + str(e))
raise SystemExit(1)
def publish_assets(self):
try:
subprocess.check_call(['npm', 'publish', 'client'])
except (OSError, subprocess.CalledProcessError) as e:
print('Error publishing front-end assets: ' + str(e))
raise SystemExit(1)
def bump_client_version(self):
"""
Writes the current Wagtail version number into package.json
"""
path = os.path.join('.', 'client', 'package.json')
input_file = io.open(path, "r")
try:
package = json.loads(input_file.read().decode("utf-8"))
except (ValueError) as e:
print('Unable to read ' + path + ' ' + e)
raise SystemExit(1)
package['version'] = __semver__
try:
with io.open(path, 'w', encoding='utf-8') as f:
from django.utils import six
f.write(six.text_type(json.dumps(package, indent=2, ensure_ascii=False)))
except (IOError) as e:
print('Error setting the version for front-end assets: ' + str(e))
raise SystemExit(1)
class assets(Command, assets_mixin):
user_options = []
@ -28,7 +63,9 @@ class assets(Command, assets_mixin):
pass
def run(self):
self.bump_client_version()
self.compile_assets()
self.publish_assets()
class sdist(base_sdist, assets_mixin):

Wyświetl plik

@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Explorer from 'components/explorer';
document.addEventListener('DOMContentLoaded', e => {
const top = document.querySelector('.wrapper');
const div = document.createElement('div');
const trigger = document.querySelector('[data-explorer-menu-url]');
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (!div.childNodes.length) {
ReactDOM.render(<Explorer position={trigger.getBoundingClientRect()} />, div);
} else {
ReactDOM.unmountComponentAtNode(div);
}
});
top.parentNode.appendChild(div);
});

Wyświetl plik

@ -19,6 +19,12 @@
@import 'wagtailadmin/scss/fonts';
// scss-lint:disable all
#wagtail {
@import '../../../../../client/scss/style';
}
// scss-lint:enable all
html {
background: $color-grey-4;
height: 100%;
@ -107,7 +113,7 @@ footer {
@include transition(bottom 0.5s ease 1s);
@include row();
border-radius: 3px 3px 0 0;
box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
box-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
background: $color-grey-1;
position: fixed;
bottom: 0;

Wyświetl plik

@ -25,6 +25,7 @@
<script src="{% static 'wagtailadmin/js/vendor/jquery.dlmenu.js' %}"></script>
<script src="{% static 'wagtailadmin/js/vendor/tag-it.js' %}"></script>
<script src="{% static 'wagtailadmin/js/core.js' %}"></script>
{% main_nav_js %}
{% block extra_js %}{% endblock %}

Wyświetl plik

@ -16,7 +16,7 @@
{% block branding_favicon %}{% endblock %}
</head>
<body class="{% block bodyclass %}{% endblock %} {% if messages %}has-messages{% endif %}">
<body id="wagtail" class="{% block bodyclass %}{% endblock %} {% if messages %}has-messages{% endif %}">
<!--[if lt IE 9]>
<p class="capabilitymessage">{% blocktrans %}You are using an <strong>outdated</strong> browser not supported by this software. Please <a href="http://browsehappy.com/">upgrade your browser</a>.{% endblocktrans %}</p>
<![endif]-->

Wyświetl plik

@ -1,4 +1,6 @@
__version__ = '1.4a0'
# Required for npm package for frontend
__semver__ = '1.4.0-alpha'
default_app_config = 'wagtail.wagtailcore.apps.WagtailCoreAppConfig'

Wyświetl plik

@ -0,0 +1,77 @@
var _ = require('lodash');
var path = require('path');
var glob = require('glob').sync;
var webpack = require('webpack');
var COMMON_PATH = './wagtail/wagtailadmin/static/wagtailadmin/js/common.js';
function appName(filename) {
return _(filename)
.split(path.sep)
.get(2);
}
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];
}
function entryPoints(paths) {
return _(glob(paths))
.map(entryPoint)
.fromPairs()
.value();
}
module.exports = function exports() {
var CLIENT_DIR = path.resolve(__dirname, 'client', 'src');
return {
entry: entryPoints('./wagtail/**/static_src/**/app/*.entry.js'),
resolve: {
alias: {
components: path.resolve(CLIENT_DIR, 'components')
}
},
output: {
path: './',
filename: '[name].js',
publicPath: '/static/js/'
},
plugins: [
new webpack.ProvidePlugin({
fetch: 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}),
new webpack.optimize.CommonsChunkPlugin('common', COMMON_PATH, Infinity)
],
devtool: '#inline-source-map',
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
include: [
CLIENT_DIR,
path.resolve(__dirname, 'wagtail')
]
},
{
test: /\.jsx$/,
loader: 'babel',
exclude: /node_modules/,
include: [
CLIENT_DIR,
path.resolve(__dirname, 'wagtail')
]
}
]
}
};
};

Wyświetl plik

@ -0,0 +1,8 @@
var base = require('./webpack.base.config');
var config = base('development');
// development overrides go here
config.watch = true;
module.exports = config;

Wyświetl plik

@ -0,0 +1,8 @@
var base = require('./webpack.base.config');
var config = base('production');
// production overrides go here
module.exports = config;