From ff05955eadf3fc44207fc8d216602f1177aedbcc Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Mon, 14 Sep 2015 12:54:11 +0200 Subject: [PATCH] Reinit the webpack version --- .editorconfig | 15 ++ .eslintignore | 3 + .eslintrc | 3 + .gitignore | 15 ++ .scss-lint.yml | 8 + .travis.yml | 9 + config.rb | 14 ++ package.json | 69 ++++++++ scss/App/App.scss | 3 + scss/EditModel/EditModel.scss | 3 + scss/HeaderBar/HeaderBar.scss | 3 + scss/List/List.scss | 3 + scss/MainContent/MainContent.scss | 3 + scss/SideBar/SideBar.scss | 42 +++++ scss/maintenance.scss | 79 +++++++++ src/App/App.component.js | 31 ++++ src/EditModel/EditModel.component.js | 127 ++++++++++++++ src/EditModel/SingleModelStore.js | 42 +++++ .../DataElementGroupsFields.component.js | 129 ++++++++++++++ src/EditModel/modelToEditStrore.js | 3 + src/EditModel/objectActions.js | 27 +++ src/HeaderBar/HeaderBar.component.js | 16 ++ src/List/ContextActions.js | 13 ++ src/List/List.component.js | 163 ++++++++++++++++++ src/List/list.actions.js | 23 +++ src/List/list.store.js | 59 +++++++ src/MainContent/MainContent.component.js | 16 ++ src/SideBar/SideBar.component.js | 75 ++++++++ src/SideBar/SideBarContainer.component.js | 41 +++++ src/SideBar/sideBarItems.store.js | 14 ++ src/config/field-config/field-order.js | 36 ++++ src/config/field-config/header-fields.js | 22 +++ src/config/field-overrides/dataElement.js | 85 +++++++++ src/config/field-overrides/index.js | 28 +++ src/index.html | 43 +++++ src/maintenance.js | 22 +++ src/maintenance.scss | 2 + src/utils/ObservedEvents.mixin.js | 49 ++++++ src/utils/ObserverRegistry.mixin.js | 22 +++ src/utils/d2.js | 3 + test/App/App.component.test.js | 19 ++ test/EditModel/EditModel.component.test.js | 19 ++ test/HeaderBar/HeaderBar.component.test.js | 19 ++ test/List/List.component.test.js | 19 ++ test/List/list.store.test.js | 5 + .../MainContent/MainContent.component.test.js | 19 ++ test/SideBar/SideBar.component.test.js | 19 ++ test/SideBar/sideBarItems.store.test.js | 5 + test/config/karma.config.js | 70 ++++++++ test/index.js | 2 + webpack.config.js | 38 ++++ 51 files changed, 1597 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .scss-lint.yml create mode 100644 .travis.yml create mode 100644 config.rb create mode 100644 package.json create mode 100644 scss/App/App.scss create mode 100644 scss/EditModel/EditModel.scss create mode 100644 scss/HeaderBar/HeaderBar.scss create mode 100644 scss/List/List.scss create mode 100644 scss/MainContent/MainContent.scss create mode 100644 scss/SideBar/SideBar.scss create mode 100644 scss/maintenance.scss create mode 100644 src/App/App.component.js create mode 100644 src/EditModel/EditModel.component.js create mode 100644 src/EditModel/SingleModelStore.js create mode 100644 src/EditModel/model-specific-components/DataElementGroupsFields.component.js create mode 100644 src/EditModel/modelToEditStrore.js create mode 100644 src/EditModel/objectActions.js create mode 100644 src/HeaderBar/HeaderBar.component.js create mode 100644 src/List/ContextActions.js create mode 100644 src/List/List.component.js create mode 100644 src/List/list.actions.js create mode 100644 src/List/list.store.js create mode 100644 src/MainContent/MainContent.component.js create mode 100644 src/SideBar/SideBar.component.js create mode 100644 src/SideBar/SideBarContainer.component.js create mode 100644 src/SideBar/sideBarItems.store.js create mode 100644 src/config/field-config/field-order.js create mode 100644 src/config/field-config/header-fields.js create mode 100644 src/config/field-overrides/dataElement.js create mode 100644 src/config/field-overrides/index.js create mode 100644 src/index.html create mode 100644 src/maintenance.js create mode 100644 src/maintenance.scss create mode 100644 src/utils/ObservedEvents.mixin.js create mode 100644 src/utils/ObserverRegistry.mixin.js create mode 100644 src/utils/d2.js create mode 100644 test/App/App.component.test.js create mode 100644 test/EditModel/EditModel.component.test.js create mode 100644 test/HeaderBar/HeaderBar.component.test.js create mode 100644 test/List/List.component.test.js create mode 100644 test/List/list.store.test.js create mode 100644 test/MainContent/MainContent.component.test.js create mode 100644 test/SideBar/SideBar.component.test.js create mode 100644 test/SideBar/sideBarItems.store.test.js create mode 100644 test/config/karma.config.js create mode 100644 test/index.js create mode 100644 webpack.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..9789b2bb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.scss] +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..6b9a7ca35 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +webpack.config.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..30f1d11e5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "dhis2" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ad238d81f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.idea/* + +node_modules/* +.sass-cache +src/css/* +coverage/* + +dist/* + +#TODO: Jshint is a dependency of the git hook which installs these files on install... +.jshintignore +.jshintrc + +build/* diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 000000000..5da7d4db8 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,8 @@ +scss_files: ./src + +linters: + SelectorFormat: + convention: hyphenated_BEM + + HexNotation: + style: uppercase diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..490d94ce7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js + +node_js: + - "iojs" + +script: + - npm run lint + - npm test + - cat ./coverage/phantomjs/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/config.rb b/config.rb new file mode 100644 index 000000000..c4163950d --- /dev/null +++ b/config.rb @@ -0,0 +1,14 @@ +require "normalize-scss" +require "compass" +require "breakpoint" +require "susy" + +css_dir = "src/css" +sass_dir = "scss" + +class CSSImporter < Sass::Importers::Filesystem + def extensions + super.merge('css' => :scss) + end +end +sass_options = {:filesystem_importer => CSSImporter} diff --git a/package.json b/package.json new file mode 100644 index 000000000..f0da788e2 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "maintenance", + "version": "0.0.1", + "description": "maintenance", + "main": "index.js", + "scripts": { + "test": "./node_modules/karma/bin/karma start test/config/karma.config.js --single-run true", + "test-watch": "./node_modules/karma/bin/karma start test/config/karma.config.js", + "lint": "./node_modules/eslint/bin/eslint.js src && ./node_modules/eslint/bin/eslint.js --env node,mocha --global expect,sinon --rule 'no-unused-expressions: 0' test && scss-lint", + "prebuild": "npm test && npm run lint", + "build": "./node_modules/babel/bin/babel.js src --out-dir dist && compass compile && cp ./package.json ./dist/package.json && cp -r scss dist/scss", + "validate": "npm ls" + }, + "devDependencies": { + "babel": "^5.8.23", + "babel-core": "^5.8.23", + "babel-eslint": "4.0.10", + "babel-loader": "^5.3.2", + "chai": "3.0.0", + "coveralls": "2.11.4", + "d2-testutils": "0.1.7", + "eslint": "1.2.0", + "eslint-config-dhis2": "0.0.5", + "eslint-plugin-react": "3.2.3", + "istanbul": "0.3.18", + "karma": "0.13.9", + "karma-babel-preprocessor": "5.2.1", + "karma-chai": "0.1.0", + "karma-coverage": "0.5.0", + "karma-mocha": "0.2.0", + "karma-mocha-reporter": "1.1.1", + "karma-phantomjs-launcher": "0.2.1", + "karma-sinon": "1.0.4", + "karma-sinon-chai": "1.0.0", + "karma-sourcemap-loader": "^0.3.5", + "karma-webpack": "^1.7.0", + "mocha": "2.2.5", + "phantomjs": "1.9.18", + "phantomjs-polyfill": "0.0.1", + "precommit-hook": "3.0.0", + "react": "0.13.3", + "sinon": "1.15.4", + "sinon-chai": "2.8.0", + "webpack": "^1.12.1", + "webpack-dev-server": "^1.10.1" + }, + "dependencies": { + "babel-loader": "^5.3.2", + "classnames": "^2.1.3", + "d2": "0.0.11", + "d2-flux": "^0.4.0", + "d2-ui-basicfields": "^0.4.1", + "d2-ui-button": "0.0.2", + "d2-ui-datatable": "0.0.4", + "d2-ui-icon": "0.0.2", + "d2-ui-pagination": "0.0.4", + "d2-utils": "0.0.4", + "jquery": "^2.1.4", + "loglevel": "^1.4.0", + "react-router": "^0.13.3", + "react-select": "^0.5.5", + "rx": "^3.1.2" + }, + "pre-commit": [ + "lint", + "validate", + "test" + ] +} diff --git a/scss/App/App.scss b/scss/App/App.scss new file mode 100644 index 000000000..f2d838901 --- /dev/null +++ b/scss/App/App.scss @@ -0,0 +1,3 @@ +.app { + color: inherit; +} diff --git a/scss/EditModel/EditModel.scss b/scss/EditModel/EditModel.scss new file mode 100644 index 000000000..acb9d896d --- /dev/null +++ b/scss/EditModel/EditModel.scss @@ -0,0 +1,3 @@ +.edit-model { + color: inherit; +} diff --git a/scss/HeaderBar/HeaderBar.scss b/scss/HeaderBar/HeaderBar.scss new file mode 100644 index 000000000..bcefa104e --- /dev/null +++ b/scss/HeaderBar/HeaderBar.scss @@ -0,0 +1,3 @@ +.header-bar { + color: inherit; +} diff --git a/scss/List/List.scss b/scss/List/List.scss new file mode 100644 index 000000000..ed4a0830a --- /dev/null +++ b/scss/List/List.scss @@ -0,0 +1,3 @@ +.list { + color: inherit; +} diff --git a/scss/MainContent/MainContent.scss b/scss/MainContent/MainContent.scss new file mode 100644 index 000000000..19137c248 --- /dev/null +++ b/scss/MainContent/MainContent.scss @@ -0,0 +1,3 @@ +.main-content { + color: inherit; +} diff --git a/scss/SideBar/SideBar.scss b/scss/SideBar/SideBar.scss new file mode 100644 index 000000000..2c612d1cf --- /dev/null +++ b/scss/SideBar/SideBar.scss @@ -0,0 +1,42 @@ +$sidebar__background-color: #f7f7f7; +$sidebar__border-color: #e1e1e1; +$sidebar__border-style: 1px solid; +$sidebar--item__text-color: #303030; +$sidebar--item__hover-color: #EEEEEE; + +@mixin search-box($border-color) { + width: 100%; + padding: .5rem 1rem; + border-radius: .5rem; + border: 1px solid $border-color; + outline: none; + box-sizing: border-box; +} + +.sidebar { + padding: 1rem; + + .search-sidebar-items { + @include search-box($sidebar__border-color); + } + + ul { + padding-left: 0; + list-style: none; + } + + li { + border-bottom: $sidebar__border-style $sidebar__border-color; + + &:hover { + background: $sidebar--item__hover-color; + } + } + + a { + color: $sidebar--item__text-color; + display: block; + padding: 1rem; + text-decoration: none; + } +} diff --git a/scss/maintenance.scss b/scss/maintenance.scss new file mode 100644 index 000000000..cb10449e7 --- /dev/null +++ b/scss/maintenance.scss @@ -0,0 +1,79 @@ +@import "normalize"; + +// Component stylesheets from dependencies +@import '../node_modules/d2-ui-datatable/css/DataTable'; +@import '../node_modules/d2-ui-pagination/css/Pagination'; +@import '../node_modules/d2-ui-basicfields/css/BasicFields'; +@import '../node_modules/d2-ui-button/css/DefaultButton'; + +// Other imports +@import 'susy'; + +$sidebar-background-color: #f7f7f7; +$sidebar-border-color: #e1e1e1; +$sidebar-border-style: 1px solid; + +/** + * D2 Mixins + */ +@mixin search-box($border-color) { + width: 100%; + padding: .5rem 1rem; + border-radius: .5rem; + border: 1px solid $border-color; + outline: none; + box-sizing: border-box; +} +//End mixins + +//Basic for all +* { + box-sizing: border-box; +} + +html { + font-size: 12px; +} + +//Components +@import './SideBar/sidebar'; + +//App +#app { + @include container(); +} + +.sidebar-container { + @include span(3 of 12); + + background: $sidebar-background-color; + border-right: $sidebar-border-style $sidebar-border-color; + height: 100%; + position: fixed; + overflow: hidden; + top: 0; +} + +.sidebar-container--hide-scroll-bar { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + margin-right: -2rem; + padding-right: 2rem; +} + +.main-container { + @include span(9 of 12 last); + padding-right: 2rem; +} + +//TODO: Distribute + +//list +.search-list-items { + padding-bottom: 1rem; +} + +.search-list-items--input { + @include search-box(#e1e1e1); +} diff --git a/src/App/App.component.js b/src/App/App.component.js new file mode 100644 index 000000000..15b6a2c3e --- /dev/null +++ b/src/App/App.component.js @@ -0,0 +1,31 @@ +import React from 'react'; +import classes from 'classnames'; +import {RouteHandler} from 'react-router'; + +import HeaderBar from '../HeaderBar/HeaderBar.component'; +import MainContent from '../MainContent/MainContent.component'; +import SideBar from '../SideBar/SideBarContainer.component'; + +const App = React.createClass({ + render() { + const classList = classes('app'); + + return ( +
+ + +
+
+ +
+
+
+ +
+
+
+ ); + }, +}); + +export default App; diff --git a/src/EditModel/EditModel.component.js b/src/EditModel/EditModel.component.js new file mode 100644 index 000000000..491a88a52 --- /dev/null +++ b/src/EditModel/EditModel.component.js @@ -0,0 +1,127 @@ +import React from 'react/addons'; +import Router from 'react-router'; + +import FormForModel from 'd2-ui-basicfields/FormForModel.component'; + +import fieldOverrides from '../config/field-overrides/index'; +import fieldOrderNames from '../config/field-config/field-order'; +import headerFieldsNames from '../config/field-config/header-fields'; + +import FormFieldsForModel from 'd2-ui-basicfields/FormFieldsForModel'; +import FormFieldsManager from 'd2-ui-basicfields/FormFieldsManager'; +import AttributeFields from 'd2-ui-basicfields/AttributeFields.component'; +import DataElementGroupsFields from './model-specific-components/DataElementGroupsFields.component'; + +import Button from 'd2-ui-button/Button.component'; + +import d2 from '../utils/d2'; +import modelToEditStore from './modelToEditStrore'; +import objectActions from './objectActions'; + +//TODO: Gives a flash of the old content when switching models (Should probably display a loading bar) +export default React.createClass({ + statics: { + willTransitionTo: function (transition, params, query) { + objectActions.getObjectOfTypeById({objectType: params.modelType, objectId: params.modelId}); + } + }, + + getInitialState() { + return { + modelToEdit: undefined, + isLoading: true + }; + }, + + componentWillMount() { + let modelType = this.props.params.modelType; + + //TODO: Figure out a way to hide this d2.then stuff + d2.then(d2 => { + //TODO: When the schema exposes the correct field configs (ENUMS) the overrides can be removed and the FormFieldManager can be instantiated by the FormForModel Component + let formFieldsManager = new FormFieldsManager(new FormFieldsForModel(d2.models)); + formFieldsManager.setHeaderFields(headerFieldsNames.for(modelType)); + formFieldsManager.setFieldOrder(fieldOrderNames.for(modelType)); + + for (let [fieldName, overrideConfig] of fieldOverrides.for(modelType)) { + formFieldsManager.addFieldOverrideFor(fieldName, overrideConfig); + } + + this.disposable = modelToEditStore + .subscribe((modelToEdit) => { + this.setState({ + modelToEdit: modelToEdit, + isLoading: false + }); + }); + + this.setState({ + d2: d2, + formFieldsManager: formFieldsManager + }); + }); + + console.log('load the ', modelType, ' object for', this.props.params.modelId); + }, + + componentWillUnmount() { + this.disposable && this.disposable.dispose(); + }, + + saveAction(event) { + event.preventDefault(); + + objectActions.saveObject(this.props.params.modelId) + .subscribe( + (message) => alert(message), + (errorMessage) => alert(errorMessage) + ); + }, + + saveAndCloseAction() { + event.preventDefault(); + + objectActions.saveAndRedirectToList(this.props.params.modelId, this.props.params.modelType) + .subscribe(message => { + console.log(message); + Router.HashLocation.push(['/list', this.props.params.modelType].join('/')); + }, + (errorMessage) => alert(errorMessage)); + }, + + render() { + let renderForm = () => { + if (!this.state.d2) { + return undefined; + } + + return ( + + + + + {this.extraFieldsForModelType()} + + + + + ); + }; + + return ( +
+

Edit for {this.props.params.modelType} with id {this.props.params.modelId}

+ {this.state.isLoading ? 'Loading data...' : renderForm()} +
+ ); + }, + + extraFieldsForModelType() { + if (this.props.params.modelType === 'dataElement') { + return ( + + ); + return undefined; + } + } +}); diff --git a/src/EditModel/SingleModelStore.js b/src/EditModel/SingleModelStore.js new file mode 100644 index 000000000..aafe96205 --- /dev/null +++ b/src/EditModel/SingleModelStore.js @@ -0,0 +1,42 @@ +import Store from 'd2-flux/store/Store'; +import d2 from '../utils/d2'; + +const singleModelStoreConfig = { + getObjectOfTypeById({objectType, objectId}) { + d2.then(d2 => { + if (d2.models[objectType]) { + d2.models[objectType] + .get(objectId, objectType === 'dataElement' ? {fields: ':all,dataElementGroups[id,name,dataElementGroupSet[id]]'} : undefined) + .then(model => { + this.setState(model) + }); + } + }); + }, + + save({data, complete, error}) { + let objectId = data; + if (this.state.id === objectId) { + this.state.save() + .then(() => { + console.log('trigger saved action'); + complete('Saved'); + }) + .catch((e) => { + console.warn(e); + console.log('trigger save failed action'); + error(e); + }); + } + } +}; + +export default { + create(storeConfig) { + if (storeConfig) { + storeConfig = Object.assign(singleModelStoreConfig, storeConfig); + } + + return Store.create(singleModelStoreConfig); + } +} diff --git a/src/EditModel/model-specific-components/DataElementGroupsFields.component.js b/src/EditModel/model-specific-components/DataElementGroupsFields.component.js new file mode 100644 index 000000000..e9d6602b8 --- /dev/null +++ b/src/EditModel/model-specific-components/DataElementGroupsFields.component.js @@ -0,0 +1,129 @@ +import d2 from '../../utils/d2'; +import React from 'react'; +import modelToEditStore from '../modelToEditStrore'; +import objectActions from '../objectActions'; + +import FormFields from 'd2-ui-basicfields/FormFields.component'; +import ReactSelect from 'react-select'; + +const rejectWhenGroupSetIs = (dataElementGroupSetId) => (dataElementGroup) => dataElementGroup.dataElementGroupSet.id !== dataElementGroupSetId; +const getObjectWithId = (objectsWithIds, id) => objectsWithIds.reduce((result, objectsWithId) => (objectsWithId.id === id ? objectsWithId : result), undefined); + +const DataElementGroupsFields = React.createClass({ + propTypes: { + model: React.PropTypes.shape({ + dataElementGroups: React.PropTypes.array.isRequired, + }).isRequired, + }, + + getInitialState() { + return {}; + }, + + componentWillMount() { + d2.then(d2 => { + const api = d2.Api.getApi(); + + d2.models.dataElementGroupSet + .list({paging: false, fields: 'id,name,displayName,dataElementGroups[id,name,displayName]'}) + .then(dataElementGroupSetsCollection => dataElementGroupSetsCollection.toArray()) + .then(dataElementGroupSets => this.setState({dataElementGroupSets})) + .then(() => { + let oldDataElementGroups = modelToEditStore.getState().dataElementGroups + .filter(dataElementGroup => dataElementGroup.id) + .map(dataElementGroup => dataElementGroup.id); + + // FIXME: This saves on every click on the save button / even when the model hasn't changed + this.saveSubscription = objectActions.saveObject.subscribe(({data: dataElementId}) => { + // Do not attempt to save when the model is not dirty + if (!modelToEditStore.getState().dirty) { + return; + } + + const newDataElementGroups = modelToEditStore + .getState() + .dataElementGroups + .filter(dataElementGroup => dataElementGroup.id) + .map(dataElementGroup => dataElementGroup.id); + + Promise.all(oldDataElementGroups + .filter(dataElementGroup => newDataElementGroups.indexOf(dataElementGroup) === -1) + .map(dataElementGroup => { + return api.request('delete', `${api.baseUrl}/dataElementGroups/${dataElementGroup}/dataElements/${dataElementId}`) + }) + ) + .then(() => { + // Removed dataElementFromOld groups now save to new groups + return Promise.all( + newDataElementGroups + .filter(dataElementGroup => oldDataElementGroups.indexOf(dataElementGroup) === -1) + .map(dataElementGroup => { + return api.request('post', `${api.baseUrl}/dataElementGroups/${dataElementGroup}/dataElements/${dataElementId}`); + }) + ); + }) + .then(() => { + // Save successful, set the new values to be the "old ones" + oldDataElementGroups = newDataElementGroups; + }); + + }); + }); + }); + }, + + componentWillUnmount() { + if (this.saveSubscription.dispose) { + this.saveSubscription.dispose(); + } + }, + + render() { + if (!Array.isArray(this.state.dataElementGroupSets)) { + return (
Loading data element group sets..
); + } + + return ( + + {this.state.dataElementGroupSets.map(dataElementGroupSet => { + const reactSelectProps = { + name: dataElementGroupSet.name, + options: dataElementGroupSet.dataElementGroups.map(dataElementGroup => { + return {label: dataElementGroup.displayName, value: dataElementGroup.id}; + }), + value: this.props.model.dataElementGroups + .filter(dataElementGroup => dataElementGroup.dataElementGroupSet.id === dataElementGroupSet.id) + .reduce((selectBoxValue, dataElementGroup) => { + if (dataElementGroup && dataElementGroup.id) { + return dataElementGroup.id; + } + } , undefined), + onChange: function (value) { + const newDataElementGroups = this.props.model.dataElementGroups + .filter(rejectWhenGroupSetIs(dataElementGroupSet.id)); + const dataElementGroupToAdd = getObjectWithId(dataElementGroupSet.dataElementGroups, value); + + dataElementGroupToAdd.dataElementGroupSet = {id: dataElementGroupSet.id}; + newDataElementGroups.push(dataElementGroupToAdd); + + // TODO: Changing props is bad practice.. (Should ideally create a new model from data and set that, think `Model.clone();`) + this.props.model.dataElementGroups = newDataElementGroups; + modelToEditStore.setState(this.props.model); + }.bind(this), + }; + + return ( +
+
+ +
+ +
+ ); + })} +
+ ); + } +}); + +export default DataElementGroupsFields; diff --git a/src/EditModel/modelToEditStrore.js b/src/EditModel/modelToEditStrore.js new file mode 100644 index 000000000..7c3cbc916 --- /dev/null +++ b/src/EditModel/modelToEditStrore.js @@ -0,0 +1,3 @@ +import SingleModelStore from './SingleModelStore'; + +export default SingleModelStore.create(); diff --git a/src/EditModel/objectActions.js b/src/EditModel/objectActions.js new file mode 100644 index 000000000..054d72dea --- /dev/null +++ b/src/EditModel/objectActions.js @@ -0,0 +1,27 @@ +import Action from 'd2-flux/action/Action'; +import modelToEditStore from './modelToEditStrore'; + +const objectActions = Action.createActionsFromNames(['getObjectOfTypeById', 'saveObject', 'saveAndRedirectToList']); + +//TODO: Extract this a convenience method onto the action object? +objectActions.mapActionsToStore = function mapActionsToStore(actionsConfig, store) { + actionsConfig.forEach(mapping => { + mapping[0].forEach(actionKey => { + let actionTransformer = mapping[2]; + + this[actionKey].subscribe(function (actionConfig) { + if (actionTransformer) { + actionConfig = actionTransformer(actionConfig); + } + store[mapping[1]](actionConfig); + }); + }); + }); +}; + +objectActions.mapActionsToStore([ + [['saveObject', 'saveAndRedirectToList'], 'save'], + [['getObjectOfTypeById'], 'getObjectOfTypeById', (config) => { console.log('action transformer'); return config.data; }] +], modelToEditStore); + +export default objectActions; diff --git a/src/HeaderBar/HeaderBar.component.js b/src/HeaderBar/HeaderBar.component.js new file mode 100644 index 000000000..05a005c62 --- /dev/null +++ b/src/HeaderBar/HeaderBar.component.js @@ -0,0 +1,16 @@ +import React from 'react'; +import classes from 'classnames'; + +const HeaderBar = React.createClass({ + render() { + const classList = classes('header-bar'); + + return ( +
+ +
+ ); + }, +}); + +export default HeaderBar; diff --git a/src/List/ContextActions.js b/src/List/ContextActions.js new file mode 100644 index 000000000..a43e4703c --- /dev/null +++ b/src/List/ContextActions.js @@ -0,0 +1,13 @@ +'use strict'; + +import Router from 'react-router'; +import Action from 'd2-flux/action/Action'; + +let contextActions = Action.createActionsFromNames(['edit', 'translate']); + +contextActions.edit + .subscribe(function (action) { + Router.HashLocation.push(['/edit', action.data.modelDefinition.name, action.data.id].join('/')) + }); + +export default contextActions; diff --git a/src/List/List.component.js b/src/List/List.component.js new file mode 100644 index 000000000..1dc8b60e7 --- /dev/null +++ b/src/List/List.component.js @@ -0,0 +1,163 @@ +import React from 'react'; +import {Navigation} from 'react-router'; + +import log from 'loglevel'; + +import {isIterable} from 'd2-utils'; +import DataTable from 'd2-ui-datatable/DataTable.component'; +import Pagination from 'd2-ui-pagination/Pagination.component'; + +import contextActions from './ContextActions'; + +import listStore from './list.store'; +import listActions from './list.actions'; + +import ObservedEvents from '../utils/ObservedEvents.mixin'; +import ObserverRegistry from '../utils/ObserverRegistry.mixin'; + +var LoadingStatus = React.createClass({ + getDefaultProps() { + return { + isLoading: false, + loadingText: 'Loading...' + } + }, + + render() { + if (!this.props.isLoading) { return null; } + + return ( +
{this.props.loadingText}
+ ); + } +}); + +var SearchBox = React.createClass({ + mixins: [ObservedEvents], + + componentDidMount() { + //TODO: Observer gets registered multiple times. Which results in many calls. + let searchObserver = this.events.searchList + .throttle(400) + .map(event => event && event.target && event.target.value ? event.target.value : '') + .distinctUntilChanged(); + + this.props.searchObserverHandler(searchObserver); + }, + + render() { + return ( +
+ +
+ ); + } +}); + +var List = React.createClass({ + mixins: [Navigation, ObserverRegistry], + + getInitialState() { + return { + dataRows: [], + pager: { + total: 0 + }, + isLoading: true + }; + }, + + componentWillMount() { + this.executeLoadListAction(this.props.params.modelType); + + const sourceStoreDisposable = listStore.subscribe(listStoreValue => { + if (!isIterable(listStoreValue.list)) { + return; //Received value is not iterable, keep waiting + } + + this.setState({ + dataRows: listStoreValue.list, + pager: listStoreValue.pager, + isLoading: false, + }); + }); + + this.registerDisposable(sourceStoreDisposable); + }, + + executeLoadListAction(modelType) { + const searchListDisposable = listActions.loadList(modelType) + .subscribe( + (message) => { console.info(message); }, + (message) => { + if (/^.+s$/.test(modelType)) { + let nonPluralAttempt = modelType.substring(0, modelType.length - 1); + log.warn(`Could not find requested model type '${modelType}' attempting to redirect to '${nonPluralAttempt}'`); + this.transitionTo('list', {modelType: nonPluralAttempt}); + } else { + log.error('No clue where', modelType, 'comes from... Redirecting to app root'); + log.error(message); + this.transitionTo('/'); + } + } + ); + + this.registerDisposable(searchListDisposable); + }, + + componentWillReceiveProps(newProps) { + if (this.props.params.modelType !== newProps.params.modelType) { + this.setState({ + isLoading: true + }); + this.executeLoadListAction(newProps.params.modelType); + } + }, + + searchListByName(searchObserver) { + const searchListByNameDisposable = searchObserver + .subscribe((value) => { + console.log('Starting search'); + this.setState({ + isLoading: true + }); + + listActions.searchByName({modelType: this.props.params.modelType, searchString: value}) + .subscribe(() => {}, (error) => log.error(error)); + }); + + this.registerDisposable(searchListByNameDisposable); + }, + + render() { + let currentlyShown = `${(this.state.dataRows.length * (this.state.pager.page - 1))} - ${this.state.dataRows.length * this.state.pager.page}`; + + let paginationProps = { + hasNextPage: () => Boolean(this.state.pager.hasNextPage) && this.state.pager.hasNextPage(), + hasPreviousPage: () => Boolean(this.state.pager.hasPreviousPage) && this.state.pager.hasPreviousPage(), + onNextPageClick: () => { + this.setState({isLoading: true}); + listActions.getNextPage(); + }, + onPreviousPageClick: () => { + this.setState({isLoading: true}); + listActions.getPreviousPage(); + }, + total: this.state.pager.total, + currentlyShown + }; + + return ( +
+

List of {this.props.params.modelType}

+ + + + + {this.state.dataRows.length ? null :
No results found
} +
+ ); + }, +}); + +export default List; diff --git a/src/List/list.actions.js b/src/List/list.actions.js new file mode 100644 index 000000000..2e23b04d0 --- /dev/null +++ b/src/List/list.actions.js @@ -0,0 +1,23 @@ +import Action from 'd2-flux/action/Action'; +import listStore from './list.store'; + +const listActions = Action.createActionsFromNames(['loadList', 'searchByName', 'getNextPage', 'getPreviousPage']); + +listActions.loadList.subscribe(action => { + listStore.getListFor(action.data, action.complete, action.error); +}); + +listActions.searchByName.subscribe(action => { + listStore.searchByName(action.data.modelType, action.data.searchString, action.complete, action.error); +}); + +//TODO: For simple action mapping like this we should be able to do something less boiler plate like +listActions.getNextPage.subscribe(() => { + listStore.getNextPage(); +}); + +listActions.getPreviousPage.subscribe(() => { + listStore.getPreviousPage(); +}); + +export default listActions; diff --git a/src/List/list.store.js b/src/List/list.store.js new file mode 100644 index 000000000..fc6553775 --- /dev/null +++ b/src/List/list.store.js @@ -0,0 +1,59 @@ +import d2 from '../utils/d2'; +import {Subject, Observable} from 'rx'; + +import Store from 'd2-flux/store/Store'; + +export default Store.create({ + listSourceSubject: new Subject(), + + initialise() { + this.listSourceSubject + .flatMapLatest(list => list) + .subscribe(modelCollection => { + this.setState({ + pager: modelCollection.pager, + list: modelCollection.toArray() + }); + }); + return this; + }, + + getListFor(modelName, complete, error) { + d2.then(d2 => { + if (d2.models[modelName]) { + let listPromise = d2.models[modelName] + .list(); + + this.listSourceSubject.onNext(Observable.fromPromise(listPromise)); + + complete(modelName + ' list loading'); + } else { + error(modelName + ' is not a valid schema name'); + } + }); + }, + + getNextPage() { + this.listSourceSubject.onNext(Observable.fromPromise(this.state.pager.getNextPage())); + }, + + getPreviousPage() { + this.listSourceSubject.onNext(Observable.fromPromise(this.state.pager.getPreviousPage())); + }, + + searchByName(modelType, searchString, complete, error) { + d2.then(d2 => { + if (!d2.models[modelType]) { + error(modelType + ' is not a valid schema name'); + } + + let listSearchPromise = d2.models[modelType] + .filter().on('name').like(searchString) + .list({fields: 'name,id,lastUpdated'}); + + this.listSourceSubject.onNext(Observable.fromPromise(listSearchPromise)); + + complete(modelType + ` list with search on 'name' for '${searchString}' is loading`); + }); + } +}).initialise(); diff --git a/src/MainContent/MainContent.component.js b/src/MainContent/MainContent.component.js new file mode 100644 index 000000000..99a9ac400 --- /dev/null +++ b/src/MainContent/MainContent.component.js @@ -0,0 +1,16 @@ +import React from 'react'; +import classes from 'classnames'; + +const MainContent = React.createClass({ + render() { + const classList = classes('main-content'); + + return ( +
+ {this.props.children} +
+ ); + }, +}); + +export default MainContent; diff --git a/src/SideBar/SideBar.component.js b/src/SideBar/SideBar.component.js new file mode 100644 index 000000000..0982a10f0 --- /dev/null +++ b/src/SideBar/SideBar.component.js @@ -0,0 +1,75 @@ +import React from 'react'; +import log from 'loglevel'; +import ObservedEvents from '../utils/ObservedEvents.mixin'; + +const SideBar = React.createClass({ + mixins: [ObservedEvents], + + propTypes: { + filterChildren: React.PropTypes.func + }, + + getDefaultProps() { + return { + items: [] + }; + }, + + getInitialState() { + return { + searchString: '' + }; + }, + + componentDidMount() { + this.events.search + .throttle(200) + .do(event => { + if (event.which === 13) { + this.selectTheFirst(); + } + }) + .distinctUntilChanged() + .map(event => event && event.target && event.target.value ? event.target.value : '') + .subscribe( + searchString => { + console.log(searchString); + this.setState({ + searchString: searchString + }); + }, + error => { + log.error('Could not set the search string'); + } + ); + }, + + selectTheFirst() { + //TODO: This ties into the DOM, which is not really best practice. Think about a way to solve this without having to do other hacky magic. + React.findDOMNode(this).querySelector('a').click(); + }, + + render() { + this.filteredChildren = React.Children.map(this.props.children, child => { + if (this.state.searchString && this.props.filterChildren) { + //Do not render children that do not comply with the filter + if (!this.props.filterChildren(this.state.searchString, child)) { + return null; + } + } + return (
  • {React.cloneElement(child, {key: child.props.params.modelType})}
  • ); + }); + + return ( +
    +

    {this.props.title}

    + +
      + {this.filteredChildren} +
    +
    + ); + } +}); + +export default SideBar; diff --git a/src/SideBar/SideBarContainer.component.js b/src/SideBar/SideBarContainer.component.js new file mode 100644 index 000000000..3990b7a28 --- /dev/null +++ b/src/SideBar/SideBarContainer.component.js @@ -0,0 +1,41 @@ +import React from 'react'; +import {State, Link} from 'react-router'; + +import sideBarItemsStore from './sideBarItems.store'; + +import SideBar from './SideBar.component'; + +let SideBarContainer = React.createClass({ + mixins: [State], + + getInitialState() { + return { + sideBarItems: [] + }; + }, + + componentWillMount() { + sideBarItemsStore.subscribe(sideBarItems => { + this.setState({ + sideBarItems: sideBarItems, + }); + }); + }, + + filterChildren(searchString, child) { + //Both values are transformed to lowercase so we can do case insensitive search + return child.props.params.modelType.toLowerCase().indexOf(searchString.toLowerCase()) >= 0; + }, + + render() { + return ( + + {this.state.sideBarItems.map(modelType => { + return ({modelType}); + })} + + ); + } +}); + +export default SideBarContainer; diff --git a/src/SideBar/sideBarItems.store.js b/src/SideBar/sideBarItems.store.js new file mode 100644 index 000000000..dfa91d7b0 --- /dev/null +++ b/src/SideBar/sideBarItems.store.js @@ -0,0 +1,14 @@ +import Store from 'd2-flux/store/Store'; +import d2 from '../utils/d2'; + +const sideBarItemsStore = Store.create(); + +d2.then((d2) => { + const sideBarItems = d2.models.mapThroughDefinitions(definition => { + return definition.name; + }).sort(); + + sideBarItemsStore.setState(sideBarItems); +}); + +export default sideBarItemsStore; diff --git a/src/config/field-config/field-order.js b/src/config/field-config/field-order.js new file mode 100644 index 000000000..308bed6a2 --- /dev/null +++ b/src/config/field-config/field-order.js @@ -0,0 +1,36 @@ +let fieldOrderByName = new Map([ + ['dataElement', [ + 'name', 'code', 'shortName', 'description', 'formName', 'domainType', 'type', 'numberType', 'textType', 'aggregationOperator', + 'zeroIsSignificant', 'url', 'categoryCombo', 'optionSet', 'commentOptionSet', 'legendSet', 'aggregationLevels', 'data']], + ['dataElementGroupSet', ['name', 'code', 'description', 'compulsory', 'dataDimension']], + ['category', ['name', 'code', 'dataDimension', 'dataDimensionType', 'categoryOptions']], + ['categoryCombo', ['name', 'code', 'dimensionType', 'skipTotal', 'categories']], + ['categoryOptionGroupSet', ['name', 'description', 'dataDimension', 'categoryOptionGroups']], + ['indicator', ['name', 'shortName', 'code', 'description', 'annualized', 'decimals', 'indicatorType', 'legendSet', 'url']], + ['indicatorGroup', ['name', 'indicators']], + ['indicatorType', ['name', 'factor', 'number']], + ['indicatorGroupSet', ['name', 'description', 'compulsory', 'indicatorGroups']] +]); + +export default { + /** + * @method + * + * @params {String} schemaName The name of the schema for which to get the field order + * @returns {Array} An arraylist of field names + * This can be used to set field order on the `FormFieldsManager` + * + * @example + * ``` + * import fieldOverrides from 'field-overrides'; + * + * let dataElementOverrides = fieldOverrides.for('dataElement'); + * ``` + */ + for(schemaName) { + if (schemaName && fieldOrderByName.has(schemaName)) { + return fieldOrderByName.get(schemaName); + } + return ['name', 'shortName', 'code']; + } +}; diff --git a/src/config/field-config/header-fields.js b/src/config/field-config/header-fields.js new file mode 100644 index 000000000..65667ec10 --- /dev/null +++ b/src/config/field-config/header-fields.js @@ -0,0 +1,22 @@ +let headerFieldsMap = new Map([ + ['dataElementGroupSet', ['name', 'code']], + ['categoryOptionCombo', ['name', 'code']], + ['category', ['name', 'code']], + ['categoryCombo', ['name', 'code']] +]); + +export default { + /** + * @method + * + * @params {String} schemaName The name of the schema for which to get the field order + * @returns {Array} An arraylist of field names + * This can be used to set the header fields on the `FormFieldsManager` + */ + for(schemaName) { + if (schemaName && headerFieldsMap.has(schemaName)) { + return headerFieldsMap.get(schemaName); + } + return ['name', 'shortName']; + } +}; diff --git a/src/config/field-overrides/dataElement.js b/src/config/field-overrides/dataElement.js new file mode 100644 index 000000000..5419586fa --- /dev/null +++ b/src/config/field-overrides/dataElement.js @@ -0,0 +1,85 @@ +import {SELECT, MULTISELECT} from 'd2-ui-basicfields/fields'; +import d2 from '../../utils/d2'; + +export default new Map([ + ['type', { + type: SELECT, + templateOptions: { + options: [ + 'int', + 'string', + 'bool', + 'trueOnly', + 'date', + 'username' + ] + } + }], + ['aggregationOperator', { + type: SELECT, + templateOptions: { + options: [ + 'sum', + 'average', + 'avg', + 'count', + 'stddev', + 'variance', + 'min', + 'max' + ] + }, + hide: model => (['bool', 'trueOnly', 'int'].indexOf(model.type)) === -1 + }], + ['numberType', { + type: SELECT, + templateOptions: { + options: [ + 'number', + 'int', + 'posInt', + 'negInt', + 'zeroPositiveInt', + 'unitInterval', + 'percentage' + ] + }, + hide: (model) => model.type !== 'int' + }], + ['textType', { + type: SELECT, + templateOptions: { + options: [ + 'text', + 'longText' + ] + }, + hide: (model) => model.type !== 'string' + }], + ['aggregationLevels', { + type: MULTISELECT, + source() { + return d2.then(d2 => { + return d2.models.organisationUnitLevel.list() + .then(collection => { + return collection.toArray() + .map(item => { + return { + name: item.name, + id: item.level + }; + }); + }); + }); + }, + fromModelTransformer(modelValue) { + console.log(modelValue); + return { + id: modelValue + }; + }, + toModelTransformer(object) { + return parseInt(object.id, 10); + }, + }], +]); diff --git a/src/config/field-overrides/index.js b/src/config/field-overrides/index.js new file mode 100644 index 000000000..e574d0370 --- /dev/null +++ b/src/config/field-overrides/index.js @@ -0,0 +1,28 @@ +import dataElement from './dataElement'; + +let overridesByType = { + dataElement +}; + +export default { + /** + * @method + * + * @params {String} schemaName The name of the schema for which to get the overrides + * @returns {Map} A map with the name and configs of the field overrides. + * This can be used to easily add overrides for a type to your `FormFieldsManager` + * + * @example + * ``` + * import fieldOverrides from 'field-overrides'; + * + * let dataElementOverrides = fieldOverrides.for('dataElement'); + * ``` + */ + for(schemaName) { + if (schemaName && overridesByType[schemaName]) { + return overridesByType[schemaName] + } + return new Map(); + } +}; diff --git a/src/index.html b/src/index.html new file mode 100644 index 000000000..92e81ceab --- /dev/null +++ b/src/index.html @@ -0,0 +1,43 @@ + + + + + maintenance + + + + + + + + + + +
    + + + + + + + diff --git a/src/maintenance.js b/src/maintenance.js new file mode 100644 index 000000000..ec18ef125 --- /dev/null +++ b/src/maintenance.js @@ -0,0 +1,22 @@ +import React from 'react'; +import Router from 'react-router'; +import {Route, RouteHandler, DefaultRoute, Navigation} from 'react-router'; +import App from './App/App.component'; +import List from './List/List.component'; +import EditModelForm from './EditModel/EditModel.component'; +import Action from 'd2-flux/action/Action'; + +const routeActions = Action.createActionsFromNames(['transition']); + +// declare our routes and their hierarchy +const routes = ( + + + + +); + +Router.run(routes, Router.HashLocation, (Root) => { + React.render(, document.getElementById('app')); + routeActions.transition(Router.HashLocation.getCurrentPath()); +}); diff --git a/src/maintenance.scss b/src/maintenance.scss new file mode 100644 index 000000000..70fbe83f9 --- /dev/null +++ b/src/maintenance.scss @@ -0,0 +1,2 @@ +@import 'normalize'; +@import 'susy'; diff --git a/src/utils/ObservedEvents.mixin.js b/src/utils/ObservedEvents.mixin.js new file mode 100644 index 000000000..84e0e54ea --- /dev/null +++ b/src/utils/ObservedEvents.mixin.js @@ -0,0 +1,49 @@ +'use strict'; + +import React from 'react'; +import {Subject, Observable} from 'rx/dist/rx.all'; + +import log from 'loglevel'; + +let ObservedEvents = { + getInitialState() { + this.events = {}; + this.eventSubjects = {}; + + return {}; + }, + + createEventObserver: function () { + let subjectMap = new Map(); + + return function (referenceName) { + let subject; + + if (!subjectMap.has(referenceName)) { + subject = new Subject(); + subjectMap.set(referenceName, subject); + } else { + subject = subjectMap.get(referenceName); + } + + if (!this.events[referenceName]) { + //Run a map that keeps a copy of the event + this.events[referenceName] = subject.map(event => { return Object.assign({}, event); }); + } + + return (event) => { + subject.onNext(event); + } + }; + }(), + + componentWillUnmount() { + //Complete any eventsSubjects + Object.keys(this.eventSubjects).forEach(eventSubjectKey => { + log.info('Completing: ' + this.constructor.name + '.' + eventSubjectKey); + this.eventSubjects[eventSubjectKey].onCompleted(); + }); + } +}; + +export default ObservedEvents; diff --git a/src/utils/ObserverRegistry.mixin.js b/src/utils/ObserverRegistry.mixin.js new file mode 100644 index 000000000..e75f860b2 --- /dev/null +++ b/src/utils/ObserverRegistry.mixin.js @@ -0,0 +1,22 @@ +'use strict'; + +import React from 'react'; +import {Subject, Observable} from 'rx/dist/rx.all'; + +import log from 'loglevel'; + +const ObserverRegistry = { + observerDisposables: [], + + componentWillUnmount() { + log.info('Disposing: ', this.observerDisposables); + this.observerDisposables.forEach(disposable => disposable.dispose()); + }, + + registerDisposable(disposable) { + log.info('Registered: ', disposable); + this.observerDisposables.push(disposable); + } +}; + +export default ObserverRegistry; diff --git a/src/utils/d2.js b/src/utils/d2.js new file mode 100644 index 000000000..3d87dd61e --- /dev/null +++ b/src/utils/d2.js @@ -0,0 +1,3 @@ +import d2Init from 'd2'; + +export default d2Init({baseUrl: 'http://localhost:8080/dhis/api'}); diff --git a/test/App/App.component.test.js b/test/App/App.component.test.js new file mode 100644 index 000000000..d7bbe43f7 --- /dev/null +++ b/test/App/App.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import App from '../../src/App/App.component'; + +const TestUtils = React.addons.TestUtils; + +describe('App component', () => { + let appComponent; + + beforeEach(() => { + appComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(appComponent.getDOMNode()).hasClass('app')).to.be.true; + }); +}); diff --git a/test/EditModel/EditModel.component.test.js b/test/EditModel/EditModel.component.test.js new file mode 100644 index 000000000..0b5d41463 --- /dev/null +++ b/test/EditModel/EditModel.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import EditModel from '../../src/EditModel/EditModel.component'; + +const TestUtils = React.addons.TestUtils; + +describe('EditModel component', () => { + let editModelComponent; + + beforeEach(() => { + editModelComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(editModelComponent.getDOMNode()).hasClass('edit-model')).to.be.true; + }); +}); diff --git a/test/HeaderBar/HeaderBar.component.test.js b/test/HeaderBar/HeaderBar.component.test.js new file mode 100644 index 000000000..1c7c4aeef --- /dev/null +++ b/test/HeaderBar/HeaderBar.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import HeaderBar from '../../src/HeaderBar/HeaderBar.component'; + +const TestUtils = React.addons.TestUtils; + +describe('HeaderBar component', () => { + let headerBarComponent; + + beforeEach(() => { + headerBarComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(headerBarComponent.getDOMNode()).hasClass('header-bar')).to.be.true; + }); +}); diff --git a/test/List/List.component.test.js b/test/List/List.component.test.js new file mode 100644 index 000000000..3b810dd94 --- /dev/null +++ b/test/List/List.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import List from '../../src/List/List.component'; + +const TestUtils = React.addons.TestUtils; + +describe('List component', () => { + let listComponent; + + beforeEach(() => { + listComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(listComponent.getDOMNode()).hasClass('list')).to.be.true; + }); +}); diff --git a/test/List/list.store.test.js b/test/List/list.store.test.js new file mode 100644 index 000000000..6f28b9d15 --- /dev/null +++ b/test/List/list.store.test.js @@ -0,0 +1,5 @@ +import listStore from '../../src/List/listStore'; + +describe('Store: listStore', () => { + +}); diff --git a/test/MainContent/MainContent.component.test.js b/test/MainContent/MainContent.component.test.js new file mode 100644 index 000000000..1255e5330 --- /dev/null +++ b/test/MainContent/MainContent.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import MainContent from '../../src/MainContent/MainContent.component'; + +const TestUtils = React.addons.TestUtils; + +describe('MainContent component', () => { + let mainContentComponent; + + beforeEach(() => { + mainContentComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(mainContentComponent.getDOMNode()).hasClass('main-content')).to.be.true; + }); +}); diff --git a/test/SideBar/SideBar.component.test.js b/test/SideBar/SideBar.component.test.js new file mode 100644 index 000000000..02c7ee5ab --- /dev/null +++ b/test/SideBar/SideBar.component.test.js @@ -0,0 +1,19 @@ +import React from 'react/addons'; +import {element} from 'd2-testutils'; +import SideBar from '../../src/SideBar/SideBar.component'; + +const TestUtils = React.addons.TestUtils; + +describe('SideBar component', () => { + let sideBarComponent; + + beforeEach(() => { + sideBarComponent = TestUtils.renderIntoDocument( + + ); + }); + + it('should have the component name as a class', () => { + expect(element(sideBarComponent.getDOMNode()).hasClass('side-bar')).to.be.true; + }); +}); diff --git a/test/SideBar/sideBarItems.store.test.js b/test/SideBar/sideBarItems.store.test.js new file mode 100644 index 000000000..38a991889 --- /dev/null +++ b/test/SideBar/sideBarItems.store.test.js @@ -0,0 +1,5 @@ +import sideBarItemsStore from '../../src/SideBar/sideBarItems.store'; + +describe('Store: sideBarItemsStore', () => { + +}); diff --git a/test/config/karma.config.js b/test/config/karma.config.js new file mode 100644 index 000000000..bdfac7956 --- /dev/null +++ b/test/config/karma.config.js @@ -0,0 +1,70 @@ +module.exports = function karmaConfig( config ) { + config.set({ + basePath: '../../', + + // Frameworks to use with karma + frameworks: ['mocha', 'chai', 'sinon-chai', 'sinon'], + + // How will the results of the tests be reported (coverage reporter generates the coverage) + reporters: ['mocha', 'coverage'], + + preprocessors: { + // source files, that you wanna generate coverage for + // do not include tests or libraries + // (these files will be instrumented by Istanbul) + 'src/**/*.js': ['babel'], + 'test/*': ['webpack'], + }, + + // optionally, configure the reporter + coverageReporter: { + type: 'lcov', + dir: './coverage/', + subdir: function flattenBrowserName(browser) { + // normalization process to keep a consistent browser name accross different OS + return browser.toLowerCase().split(/[ /-]/)[0]; + }, + }, + + babelPreprocessor: { + options: { + sourceMap: 'inline', + }, + }, + + webpack: { + context: __dirname, + module: { + loaders: [ + { + test: /.+?/, + exclude: /(node_modules)/, + loader: 'babel', + query: { + stage: 2, + }, + }, + ], + }, + }, + + // Files that should be included by karma + files: [ + './node_modules/phantomjs-polyfill/bind-polyfill.js', + './node_modules/babel-core/browser-polyfill.js', + 'test/index.js', + 'src/**/*.js', + ], + + + // list of preprocessors + preprocessors: { + 'test/*': ['webpack'] + }, + + logLevel: config.LOG_INFO, + + browsers: ['PhantomJS'], + singleRun: false, + }); +}; diff --git a/test/index.js b/test/index.js new file mode 100644 index 000000000..0928acc83 --- /dev/null +++ b/test/index.js @@ -0,0 +1,2 @@ +var testsContext = require.context(".", true, /\.test$/); +testsContext.keys().forEach(testsContext); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..de1f24212 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,38 @@ +var webpack = require('webpack'); +var path = require('path'); + +module.exports = { + context: __dirname, + entry: './src/maintenance.js', + output: { + path: path.join(__dirname, '/build'), + filename: 'maintenance.js', + }, + module: { + loaders: [ + { + test: /\.jsx?$/, + exclude: [/(node_modules)/, /d2\-ui/], + loader: 'babel', + query: { + stage: 2, + }, + }, + { + test: /\.css$/, + loader: 'style-loader!css-loader', + }, + ], + }, + plugins: [ + new webpack.optimize.DedupePlugin(), + ], + devServer: { + contentBase: './src/', + progress: true, + colors: true, + port: 8081, + inline: true, + }, + devtool: ['sourcemap'], +};