Browse Source

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

Thanks to @justinvdm for the help

Merges: #2275
Josh Barr 9 years ago
parent
commit
14f02a0b50
47 changed files with 657 additions and 51 deletions
  1. 6 0
      .babelrc
  2. 4 3
      .drone.yml
  3. 34 0
      .eslintignore
  4. 25 0
      .eslintrc
  5. 1 0
      .gitignore
  6. 0 37
      .jscsrc
  7. 0 0
      client/.npmignore
  8. 42 0
      client/README.md
  9. 18 0
      client/package.json
  10. 3 0
      client/scss/_components.scss
  11. 1 0
      client/scss/_objects.scss
  12. 3 0
      client/scss/objects/_o.icon.scss
  13. 4 0
      client/scss/states/_animations.scss
  14. 0 0
      client/scss/states/_states.scss
  15. 10 0
      client/scss/style.scss
  16. 0 0
      client/scss/themes/_themes.scss
  17. 9 0
      client/scss/utilities/_utilities.scss
  18. 83 0
      client/src/cli/component.js
  19. 15 0
      client/src/cli/index.js
  20. 1 0
      client/src/components/explorer/README.md
  21. 28 0
      client/src/components/explorer/explorer-item.js
  22. 67 0
      client/src/components/explorer/index.js
  23. 13 0
      client/src/components/explorer/style.scss
  24. 7 0
      client/src/components/index.js
  25. 1 0
      client/src/components/loading-indicator/README.md
  26. 9 0
      client/src/components/loading-indicator/index.js
  27. 3 0
      client/src/components/loading-indicator/style.scss
  28. 9 0
      client/src/components/state-indicator/README.md
  29. 16 0
      client/src/components/state-indicator/index.js
  30. 5 0
      client/src/components/state-indicator/style.scss
  31. 1 0
      client/src/config/index.js
  32. 1 0
      client/src/index.js
  33. 9 0
      client/template/README.mst
  34. 15 0
      client/template/component.mst
  35. 5 0
      client/template/style.mst
  36. 10 0
      client/tests/components/explorer.test.js
  37. 1 0
      gulpfile.js/tasks/watch.js
  38. 34 9
      package.json
  39. 37 0
      wagtail/utils/setup.py
  40. 23 0
      wagtail/wagtailadmin/static_src/wagtailadmin/app/wagtailadmin.entry.js
  41. 7 1
      wagtail/wagtailadmin/static_src/wagtailadmin/scss/core.scss
  42. 1 0
      wagtail/wagtailadmin/templates/wagtailadmin/admin_base.html
  43. 1 1
      wagtail/wagtailadmin/templates/wagtailadmin/skeleton.html
  44. 2 0
      wagtail/wagtailcore/__init__.py
  45. 77 0
      webpack.base.config.js
  46. 8 0
      webpack.dev.config.js
  47. 8 0
      webpack.prd.config.js

+ 6 - 0
.babelrc

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

+ 4 - 3
.drone.yml

@@ -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 - 0
.eslintignore

@@ -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 - 0
.eslintrc

@@ -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 - 0
.gitignore

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

+ 0 - 37
.jscsrc

@@ -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"
-}

+ 0 - 0
client/.npmignore


+ 42 - 0
client/README.md

@@ -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`.
+
+

+ 18 - 0
client/package.json

@@ -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"
+  }
+}

+ 3 - 0
client/scss/_components.scss

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

+ 1 - 0
client/scss/_objects.scss

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

+ 3 - 0
client/scss/objects/_o.icon.scss

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

+ 4 - 0
client/scss/states/_animations.scss

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

+ 0 - 0
client/scss/states/_states.scss


+ 10 - 0
client/scss/style.scss

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

+ 0 - 0
client/scss/themes/_themes.scss


+ 9 - 0
client/scss/utilities/_utilities.scss

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

+ 83 - 0
client/src/cli/component.js

@@ -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;

+ 15 - 0
client/src/cli/index.js

@@ -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;

+ 1 - 0
client/src/components/explorer/README.md

@@ -0,0 +1 @@
+# Explorer

+ 28 - 0
client/src/components/explorer/explorer-item.js

@@ -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
+};

+ 67 - 0
client/src/components/explorer/index.js

@@ -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;

+ 13 - 0
client/src/components/explorer/style.scss

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

+ 7 - 0
client/src/components/index.js

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

+ 1 - 0
client/src/components/loading-indicator/README.md

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

+ 9 - 0
client/src/components/loading-indicator/index.js

@@ -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;

+ 3 - 0
client/src/components/loading-indicator/style.scss

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

+ 9 - 0
client/src/components/state-indicator/README.md

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

+ 16 - 0
client/src/components/state-indicator/index.js

@@ -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>
+    );
+  }
+}

+ 5 - 0
client/src/components/state-indicator/style.scss

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

+ 1 - 0
client/src/config/index.js

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

+ 1 - 0
client/src/index.js

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

+ 9 - 0
client/template/README.mst

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

+ 15 - 0
client/template/component.mst

@@ -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>
+    );
+  }
+}

+ 5 - 0
client/template/style.mst

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

+ 10 - 0
client/tests/components/explorer.test.js

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

+ 1 - 0
gulpfile.js/tasks/watch.js

@@ -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']);

+ 34 - 9
package.json

@@ -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/"
   }
 }

+ 37 - 0
wagtail/utils/setup.py

@@ -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):

+ 23 - 0
wagtail/wagtailadmin/static_src/wagtailadmin/app/wagtailadmin.entry.js

@@ -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);
+});

+ 7 - 1
wagtail/wagtailadmin/static_src/wagtailadmin/scss/core.scss

@@ -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;

+ 1 - 0
wagtail/wagtailadmin/templates/wagtailadmin/admin_base.html

@@ -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 %}

+ 1 - 1
wagtail/wagtailadmin/templates/wagtailadmin/skeleton.html

@@ -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]-->

+ 2 - 0
wagtail/wagtailcore/__init__.py

@@ -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'
 
 

+ 77 - 0
webpack.base.config.js

@@ -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')
+          ]
+        }
+      ]
+    }
+  };
+};

+ 8 - 0
webpack.dev.config.js

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

+ 8 - 0
webpack.prd.config.js

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