From ebbb286d2c60b8c9312725d7fa06efe6ddf98a1f Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 30 Sep 2015 11:51:50 +0200 Subject: [PATCH] Maintenance wip --- .gitignore | 2 + .scss-lint.yml | 2 +- package.json | 10 +- scss/List/List.scss | 21 ++ scss/SideBar/SideBar.scss | 31 +-- scss/maintenance.scss | 42 ++-- src/App/App.component.js | 49 +++- src/App/app.theme.js | 20 ++ .../CloneModelContainer.component.js | 8 + src/EditModel/EditModel.component.js | 121 +++++---- src/EditModel/EditModelContainer.component.js | 44 ++++ src/EditModel/SaveButton.component.js | 21 ++ src/EditModel/SingleModelStore.js | 76 +++--- .../DataElementEditModel.component.js | 68 ++++++ .../DataElementGroupsFields.component.js | 97 +++----- .../IndicatorEditModel.component.js | 138 +++++++++++ ...torExpressionManagerContainer.component.js | 54 ++++ ...delToEditStrore.js => modelToEditStore.js} | 0 src/EditModel/objectActions.js | 44 +++- src/List/ContextActions.js | 43 +++- src/List/List.component.js | 171 ++++++------- src/List/ListActionBar.component.js | 33 +++ src/List/LoadingStatus.component.js | 22 ++ src/List/SearchBox.component.js | 38 +++ src/List/details.store.js | 3 + src/List/list.actions.js | 9 +- src/List/list.store.js | 16 +- src/MainContent/MainContent.component.js | 4 + src/SideBar/SideBar.component.js | 60 +++-- src/SideBar/SideBarContainer.component.js | 53 ++-- src/SideBar/sideBarItems.store.js | 28 ++- src/Snackbar/SnackbarContainer.component.js | 50 ++++ src/Snackbar/snack.actions.js | 23 ++ src/Snackbar/snack.store.js | 3 + src/config/field-config/field-order.js | 6 +- src/config/field-config/header-fields.js | 6 +- src/config/field-overrides/dataElement.js | 92 +++---- src/config/field-overrides/index.js | 10 +- src/config/field-overrides/indicator.js | 14 ++ src/i18n/i18n_global.properties | 231 ++++++++++++++++++ src/index.html | 5 +- src/maintenance.js | 15 +- src/maintenance.scss | 2 - src/router.js | 23 ++ src/utils/ObservedEvents.mixin.js | 24 +- src/utils/ObserverRegistry.mixin.js | 11 +- src/utils/d2.js | 3 - test/List/list.store.test.js | 2 - test/SideBar/sideBarItems.store.test.js | 4 +- test/config/karma.config.js | 6 - test/index.js | 2 +- webpack.config.js | 2 +- 52 files changed, 1362 insertions(+), 500 deletions(-) create mode 100644 src/App/app.theme.js create mode 100644 src/EditModel/CloneModelContainer.component.js create mode 100644 src/EditModel/EditModelContainer.component.js create mode 100644 src/EditModel/SaveButton.component.js create mode 100644 src/EditModel/model-specific-components/DataElementEditModel.component.js create mode 100644 src/EditModel/model-specific-components/IndicatorEditModel.component.js create mode 100644 src/EditModel/model-specific-components/IndicatorExpressionManagerContainer.component.js rename src/EditModel/{modelToEditStrore.js => modelToEditStore.js} (100%) create mode 100644 src/List/ListActionBar.component.js create mode 100644 src/List/LoadingStatus.component.js create mode 100644 src/List/SearchBox.component.js create mode 100644 src/List/details.store.js create mode 100644 src/Snackbar/SnackbarContainer.component.js create mode 100644 src/Snackbar/snack.actions.js create mode 100644 src/Snackbar/snack.store.js create mode 100644 src/config/field-overrides/indicator.js create mode 100644 src/i18n/i18n_global.properties delete mode 100644 src/maintenance.scss create mode 100644 src/router.js delete mode 100644 src/utils/d2.js diff --git a/.gitignore b/.gitignore index ad238d81f..58a665239 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ dist/* .jshintrc build/* + +npm-debug.log diff --git a/.scss-lint.yml b/.scss-lint.yml index 5da7d4db8..452d4e5a0 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -1,4 +1,4 @@ -scss_files: ./src +scss_files: ./scss linters: SelectorFormat: diff --git a/package.json b/package.json index f0da788e2..e5e254bb5 100644 --- a/package.json +++ b/package.json @@ -47,23 +47,27 @@ "dependencies": { "babel-loader": "^5.3.2", "classnames": "^2.1.3", - "d2": "0.0.11", + "d2": "0.0.16", "d2-flux": "^0.4.0", + "d2-ui": "0.0.1", "d2-ui-basicfields": "^0.4.1", "d2-ui-button": "0.0.2", "d2-ui-datatable": "0.0.4", + "d2-ui-detailsbox": "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", + "material-ui": "^0.12.1", "react-router": "^0.13.3", "react-select": "^0.5.5", + "react-sticky": "^2.5.2", + "react-tap-event-plugin": "^0.1.7", "rx": "^3.1.2" }, "pre-commit": [ "lint", - "validate", - "test" + "validate" ] } diff --git a/scss/List/List.scss b/scss/List/List.scss index ed4a0830a..74a993b9b 100644 --- a/scss/List/List.scss +++ b/scss/List/List.scss @@ -1,3 +1,24 @@ +@import 'susy'; + .list { color: inherit; } + +.data-table-wrap { + @include span(9 of 9 last); + + opacity: 0; + transition: width .3s linear; + transition-delay: .3s; + width: 100%; + + &.smaller { + @include span(6 of 9); + } +} + +.details-box-wrap.show-as-column { + @include span(3 of 9 last); + + opacity: 1; +} diff --git a/scss/SideBar/SideBar.scss b/scss/SideBar/SideBar.scss index 2c612d1cf..4640b3954 100644 --- a/scss/SideBar/SideBar.scss +++ b/scss/SideBar/SideBar.scss @@ -1,40 +1,27 @@ -$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--background-color: #F7F7F7; +$sidebar--border-color: #E1E1E1; +$sidebar--border-style: 1px solid; +$sidebar--item--text-color: #303030; +$sidebar--item--hover-color: #EEE; .sidebar { padding: 1rem; - .search-sidebar-items { - @include search-box($sidebar__border-color); - } - ul { - padding-left: 0; list-style: none; + padding-left: 0; } li { - border-bottom: $sidebar__border-style $sidebar__border-color; + border-bottom: $sidebar--border-style $sidebar--border-color; &:hover { - background: $sidebar--item__hover-color; + background: $sidebar--item--hover-color; } } a { - color: $sidebar--item__text-color; + color: $sidebar--item--text-color; display: block; padding: 1rem; text-decoration: none; diff --git a/scss/maintenance.scss b/scss/maintenance.scss index cb10449e7..fb7865379 100644 --- a/scss/maintenance.scss +++ b/scss/maintenance.scss @@ -1,30 +1,21 @@ -@import "normalize"; +@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'; +@import '../node_modules/d2-ui-detailsbox/css/DetailsBox'; +@import '../node_modules/d2-ui/css/IndicatorExpressionManager'; // Other imports @import 'susy'; -$sidebar-background-color: #f7f7f7; -$sidebar-border-color: #e1e1e1; -$sidebar-border-style: 1px solid; +@import '../scss/List/List'; -/** - * 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 +$sidebar-background-color: #F7F7F7; +$sidebar-border-color: #E1E1E1; +$sidebar-border-style: 1px solid; //Basic for all * { @@ -32,14 +23,14 @@ $sidebar-border-style: 1px solid; } html { - font-size: 12px; + font-size: 14px; } //Components @import './SideBar/sidebar'; //App -#app { +.app { @include container(); } @@ -49,16 +40,16 @@ html { background: $sidebar-background-color; border-right: $sidebar-border-style $sidebar-border-color; height: 100%; - position: fixed; overflow: hidden; + position: fixed; top: 0; } .sidebar-container--hide-scroll-bar { height: 100%; + margin-right: -2rem; overflow-x: hidden; overflow-y: auto; - margin-right: -2rem; padding-right: 2rem; } @@ -71,9 +62,14 @@ html { //list .search-list-items { - padding-bottom: 1rem; + //padding-bottom: 1rem; } -.search-list-items--input { - @include search-box(#e1e1e1); +// Needed to override the inline style set by the component +// scss-lint:disable ImportantRule +.details-box-wrap .sticky { + left: initial !important; + margin-right: 2rem; + margin-top: 2rem; + width: 22%; } diff --git a/src/App/App.component.js b/src/App/App.component.js index 15b6a2c3e..401c5f096 100644 --- a/src/App/App.component.js +++ b/src/App/App.component.js @@ -5,11 +5,44 @@ import {RouteHandler} from 'react-router'; import HeaderBar from '../HeaderBar/HeaderBar.component'; import MainContent from '../MainContent/MainContent.component'; import SideBar from '../SideBar/SideBarContainer.component'; +import SnackbarContainer from '../Snackbar/SnackbarContainer.component'; +import {init, config} from 'd2'; +import AppWithD2 from 'd2-ui/app/AppWithD2.component'; +import log from 'loglevel'; +import appTheme from './app.theme'; + +log.setLevel(log.levels.INFO); + +const ThemeManager = require('material-ui/lib/styles/theme-manager'); + +// Needed for onTouchTap +// Can go away when react 1.0 release +// Check this repo: +// https://github.com/zilverline/react-tap-event-plugin +import injectTapEventPlugin from 'react-tap-event-plugin'; +injectTapEventPlugin(); + +// D2 pre-init config +// config.i18n.sources.add('/i18n/i18n_global.properties'); +config.baseUrl = 'http://localhost:8080/dhis/api'; + +const withMuiContext = Object.assign(AppWithD2.childContextTypes, {muiTheme: React.PropTypes.object}); +class App extends AppWithD2 { + childContextTypes: withMuiContext + + getChildContext() { + return Object.assign({}, super.getChildContext(), { + muiTheme: ThemeManager.getMuiTheme(appTheme), + }); + } -const App = React.createClass({ render() { const classList = classes('app'); + if (!this.state.d2) { + return (
App loading...
); + } + return (
@@ -23,9 +56,19 @@ const App = React.createClass({
+ ); - }, -}); + } +} +App.defaultProps = { + d2: (() => { + return new Promise(resolve => { + setTimeout(() => { + resolve(init()); + }); + }); + })(), +}; export default App; diff --git a/src/App/app.theme.js b/src/App/app.theme.js new file mode 100644 index 000000000..a8df5bad4 --- /dev/null +++ b/src/App/app.theme.js @@ -0,0 +1,20 @@ +const Colors = require('material-ui/lib/styles/colors'); +const ColorManipulator = require('material-ui/lib/utils/color-manipulator'); +const Spacing = require('material-ui/lib/styles/spacing'); + +export default { + spacing: Spacing, + fontFamily: 'Roboto, sans-serif', + palette: { + primary1Color: Colors.cyan500, + primary2Color: Colors.cyan700, + primary3Color: Colors.lightBlack, + accent1Color: Colors.pinkA200, + accent2Color: Colors.grey100, + accent3Color: Colors.grey500, + textColor: Colors.darkBlack, + alternateTextColor: Colors.white, + borderColor: Colors.grey300, + disabledColor: ColorManipulator.fade(Colors.darkBlack, 0.3), + }, +}; diff --git a/src/EditModel/CloneModelContainer.component.js b/src/EditModel/CloneModelContainer.component.js new file mode 100644 index 000000000..498cb8e34 --- /dev/null +++ b/src/EditModel/CloneModelContainer.component.js @@ -0,0 +1,8 @@ +import objectActions from './objectActions'; +import {EditModelBase} from './EditModelContainer.component'; + +export default class extends EditModelBase { + static willTransitionTo(transition, params) { + objectActions.getObjectOfTypeByIdAndClone({objectType: params.modelType, objectId: params.modelId}); + } +} diff --git a/src/EditModel/EditModel.component.js b/src/EditModel/EditModel.component.js index 491a88a52..2449a4704 100644 --- a/src/EditModel/EditModel.component.js +++ b/src/EditModel/EditModel.component.js @@ -1,49 +1,39 @@ 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 {getInstance as getD2} from 'd2'; +import modelToEditStore from './modelToEditStore'; 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 { +import snackActions from '../Snackbar/snack.actions'; +import RaisedButton from 'material-ui/lib/raised-button'; +import SaveButton from './SaveButton.component'; + +// TODO: Gives a flash of the old content when switching models (Should probably display a loading bar) +export default class EditModel extends React.Component { + constructor(props) { + super(props); + this.state = { modelToEdit: undefined, - isLoading: true + isLoading: true, }; - }, + } componentWillMount() { - let modelType = this.props.params.modelType; + const modelType = this.props.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)); + getD2().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 + const 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)) { + for (const [fieldName, overrideConfig] of fieldOverrides.for(modelType)) { formFieldsManager.addFieldOverrideFor(fieldName, overrideConfig); } @@ -51,46 +41,25 @@ export default React.createClass({ .subscribe((modelToEdit) => { this.setState({ modelToEdit: modelToEdit, - isLoading: false + isLoading: false, }); }); this.setState({ d2: d2, - formFieldsManager: formFieldsManager + formFieldsManager: formFieldsManager, }); }); - console.log('load the ', modelType, ' object for', this.props.params.modelId); - }, + console.log('load the ', modelType, ' object for', this.props.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 = () => { + const renderForm = () => { if (!this.state.d2) { return undefined; } @@ -102,26 +71,46 @@ export default React.createClass({ {this.extraFieldsForModelType()} - - + Save + ); }; return (
-

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

+

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

{this.state.isLoading ? 'Loading data...' : renderForm()}
); - }, + } + + saveAction(event) { + event.preventDefault(); + + objectActions.saveObject({id: this.props.modelId}) + .subscribe( + (message) => snackActions.show({message, action: 'Ok!'}), + (errorMessage) => { + if (errorMessage.messages && errorMessage.messages.length > 0) { + console.log(errorMessage.messages); + snackActions.show({message: `${errorMessage.messages[0].property}: ${errorMessage.messages[0].message} `}); + } + } + ); + } + + closeAction() { + event.preventDefault(); + + Router.HashLocation.push(['/list', this.props.modelType].join('/')); + } extraFieldsForModelType() { - if (this.props.params.modelType === 'dataElement') { - return ( - - ); - return undefined; - } + return undefined; } -}); +} +EditModel.propTypes = { + modelId: React.PropTypes.string.isRequired, + modelType: React.PropTypes.string.isRequired, +}; diff --git a/src/EditModel/EditModelContainer.component.js b/src/EditModel/EditModelContainer.component.js new file mode 100644 index 000000000..438167817 --- /dev/null +++ b/src/EditModel/EditModelContainer.component.js @@ -0,0 +1,44 @@ +import React from 'react/addons'; +import EditModel from './EditModel.component'; +import DataElementEditModel from './model-specific-components/DataElementEditModel.component'; +import IndicatorEditModel from './model-specific-components/IndicatorEditModel.component'; +import objectActions from './objectActions'; +import {getInstance as getD2} from 'd2'; +import modelToEditStore from './modelToEditStore'; + +export class EditModelBase extends React.Component { + constructor(props) { + super(props); + this.state = { + modelType: props.params.modelType, + modelId: props.params.modelId, + }; + } + + render() { + // Special case for dataElement forms + if (this.state.modelType === 'dataElement') { + return ( + + ); + } + + if (this.state.modelType === 'indicator') { + return ( + + ); + } + + return ; + } +} + +export default class extends EditModelBase { + static willTransitionTo(transition, params) { + if (params.modelId === 'add') { + getD2().then((d2) => modelToEditStore.setState(d2.models[params.modelType].create())); + } else { + objectActions.getObjectOfTypeById({objectType: params.modelType, objectId: params.modelId}); + } + } +} diff --git a/src/EditModel/SaveButton.component.js b/src/EditModel/SaveButton.component.js new file mode 100644 index 000000000..c9e887a86 --- /dev/null +++ b/src/EditModel/SaveButton.component.js @@ -0,0 +1,21 @@ +import React from 'react'; +import RaisedButton from 'material-ui/lib/raised-button'; + +const SaveButton = React.createClass({ + propTypes: { + isFormValid: React.PropTypes.func.isRequired, + onClick: React.PropTypes.func.isRequired, + }, + + render() { + const saveButtonStyle = { + marginRight: '1rem', + }; + + return ( + + ); + }, +}); + +export default SaveButton; diff --git a/src/EditModel/SingleModelStore.js b/src/EditModel/SingleModelStore.js index aafe96205..22f1fc8fe 100644 --- a/src/EditModel/SingleModelStore.js +++ b/src/EditModel/SingleModelStore.js @@ -1,42 +1,54 @@ import Store from 'd2-flux/store/Store'; -import d2 from '../utils/d2'; +import {getInstance as getD2} from 'd2'; +import Rx from 'rx'; + +function loadModelFromD2(objectType, objectId) { + return getD2().then(d2 => { + if (d2.models[objectType]) { + return d2.models[objectType] + .get(objectId, objectType === 'dataElement' ? {fields: ':all,dataElementGroups[id,name,dataElementGroupSet[id]]'} : undefined); + } + return Promise.reject('Invalid model'); + }); +} 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) - }); - } - }); + loadModelFromD2(objectType, objectId) + .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); - }); - } - } + getObjectOfTypeByIdAndClone({objectType, objectId}) { + loadModelFromD2(objectType, objectId) + .then(model => { + model.id = undefined; + this.setState(model); + }); + }, + + save() { + const importResultPromise = this.state.save() + .then(response => { + if (response.response.importCount.imported === 1) { + return response; + } + + if (response.response.importConflicts && response.response.importConflicts.length > 0) { + return Promise.reject(response.response.importConflicts[0].value); + } + return Promise.reject('Failed to save'); + }); + + return Rx.Observable.fromPromise(importResultPromise); + }, }; export default { - create(storeConfig) { - if (storeConfig) { - storeConfig = Object.assign(singleModelStoreConfig, storeConfig); - } + create(config) { + const storeConfig = Object.assign({}, singleModelStoreConfig, config || {}); - return Store.create(singleModelStoreConfig); - } -} + return Store.create(storeConfig); + }, +}; diff --git a/src/EditModel/model-specific-components/DataElementEditModel.component.js b/src/EditModel/model-specific-components/DataElementEditModel.component.js new file mode 100644 index 000000000..0ab51f47a --- /dev/null +++ b/src/EditModel/model-specific-components/DataElementEditModel.component.js @@ -0,0 +1,68 @@ +import React from 'react'; +import EditModel from '../EditModel.component'; +import {getInstance as getD2} from 'd2'; +import modelToEditStore from '../modelToEditStore'; +import objectActions from '../objectActions'; +import DataElementGroupsFields from './DataElementGroupsFields.component'; + +export default class extends EditModel { + componentWillMount() { + super.componentWillMount(); + + modelToEditStore.subscribe(() => { + if (!this.oldDataElementGroups) { + this.oldDataElementGroups = (modelToEditStore.getState().dataElementGroups || []) + .filter(dataElementGroup => dataElementGroup.id) + .map(dataElementGroup => dataElementGroup.id); + } + }); + } + + afterSave({data: {id: dataElementId}}) { + const newDataElementGroups = (modelToEditStore.getState().dataElementGroups || []) + .filter(dataElementGroup => dataElementGroup.id) + .map(dataElementGroup => dataElementGroup.id); + + return Rx.Observable.fromPromise(d2.then(() => { + const api = getD2.Api.getApi(); + + return Promise.all(this.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 + console.log('new1: ', newDataElementGroups); + return Promise.all( + newDataElementGroups + .filter(dataElementGroup => this.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" + this.oldDataElementGroups = newDataElementGroups; + }); + })); + } + + saveAction(event) { + event.preventDefault(); + + objectActions.saveObject({id: this.props.modelId, afterSave: this.afterSave.bind(this)}) + .subscribe( + (message) => alert(message), + (errorMessage) => alert(errorMessage) + ); + } + + extraFieldsForModelType() { + return ( + + ); + } +} diff --git a/src/EditModel/model-specific-components/DataElementGroupsFields.component.js b/src/EditModel/model-specific-components/DataElementGroupsFields.component.js index e9d6602b8..5dd1a6985 100644 --- a/src/EditModel/model-specific-components/DataElementGroupsFields.component.js +++ b/src/EditModel/model-specific-components/DataElementGroupsFields.component.js @@ -1,12 +1,10 @@ -import d2 from '../../utils/d2'; +import {getInstance as getD2} from 'd2'; import React from 'react'; -import modelToEditStore from '../modelToEditStrore'; -import objectActions from '../objectActions'; - +import modelToEditStore from '../modelToEditStore'; import FormFields from 'd2-ui-basicfields/FormFields.component'; import ReactSelect from 'react-select'; -const rejectWhenGroupSetIs = (dataElementGroupSetId) => (dataElementGroup) => dataElementGroup.dataElementGroupSet.id !== dataElementGroupSetId; +const rejectWhenGroupSetIs = (dataElementGroupSetId) => (dataElementGroup) => dataElementGroup.dataElementGroupSet && dataElementGroup.dataElementGroupSet.id !== dataElementGroupSetId; const getObjectWithId = (objectsWithIds, id) => objectsWithIds.reduce((result, objectsWithId) => (objectsWithId.id === id ? objectsWithId : result), undefined); const DataElementGroupsFields = React.createClass({ @@ -21,64 +19,29 @@ const DataElementGroupsFields = React.createClass({ }, componentWillMount() { - d2.then(d2 => { - const api = d2.Api.getApi(); - + getD2().then(d2 => { 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; - }); - - }); - }); + .then(dataElementGroupSets => this.setState({dataElementGroupSets})); }); }, - componentWillUnmount() { - if (this.saveSubscription.dispose) { - this.saveSubscription.dispose(); + render() { + function getSelectedValue(dataElementGroups, dataElementGroupSet) { + if (!Array.isArray(dataElementGroups)) { + return undefined; + } + + return dataElementGroups + .filter(dataElementGroup => dataElementGroup.dataElementGroupSet && dataElementGroup.dataElementGroupSet.id === dataElementGroupSet.id) + .reduce((selectBoxValue, dataElementGroup) => { + if (dataElementGroup && dataElementGroup.id) { + return dataElementGroup.id; + } + }, undefined); } - }, - render() { if (!Array.isArray(this.state.dataElementGroupSets)) { return (
Loading data element group sets..
); } @@ -91,20 +54,16 @@ const DataElementGroupsFields = React.createClass({ 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)); + value: getSelectedValue(this.props.model.dataElementGroups, dataElementGroupSet), + onChange: function onChange(value) { const dataElementGroupToAdd = getObjectWithId(dataElementGroupSet.dataElementGroups, value); + const newDataElementGroups = (this.props.model.dataElementGroups || []) + .filter(rejectWhenGroupSetIs(dataElementGroupSet.id)); - dataElementGroupToAdd.dataElementGroupSet = {id: dataElementGroupSet.id}; - newDataElementGroups.push(dataElementGroupToAdd); + if (dataElementGroupToAdd) { + 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; @@ -115,7 +74,7 @@ const DataElementGroupsFields = React.createClass({ return (
- +
@@ -123,7 +82,7 @@ const DataElementGroupsFields = React.createClass({ })} ); - } + }, }); export default DataElementGroupsFields; diff --git a/src/EditModel/model-specific-components/IndicatorEditModel.component.js b/src/EditModel/model-specific-components/IndicatorEditModel.component.js new file mode 100644 index 000000000..b917ed83d --- /dev/null +++ b/src/EditModel/model-specific-components/IndicatorEditModel.component.js @@ -0,0 +1,138 @@ +import React from 'react'; +import EditModel from '../EditModel.component'; +import {getInstance as getD2} from 'd2'; +import Pager from 'd2/pager/Pager'; +import Dialog from 'material-ui/lib/dialog'; +import FormUpdateContext from 'd2-ui-basicfields/FormUpdateContext.mixin'; +import RaisedButton from 'material-ui/lib/raised-button'; + +// Indicator expression manager +import IndicatorExpressionManagerContainer from './IndicatorExpressionManagerContainer.component'; +import dataElementOperandStore from 'd2-ui/indicator-expression-manager/dataElementOperand.store'; +import dataElementOperandSelectorActions from 'd2-ui/indicator-expression-manager/dataElementOperandSelector.actions'; +import {Observable} from 'rx'; + +const createFakePager = response => { + // Fake the modelCollection since dataElementOperands do not have a valid uid + return { + pager: new Pager(response.pager), + toArray() { + return response.dataElementOperands; + }, + }; +}; + +dataElementOperandSelectorActions.loadList.subscribe(() => { + getD2() + .then(d2 => d2.Api.getApi().get('dataElementOperands', {fields: 'id,displayName'})) + .then(createFakePager) + .then(collection => { + dataElementOperandStore.setState(collection); + }); +}); + +dataElementOperandSelectorActions.search + .throttle(500) + .distinctUntilChanged(action => action.data) + .map(action => { + const searchPromise = getD2() + .then(d2 => d2.Api.getApi().get('dataElementOperands', {fields: 'id,displayName', filter: [`name:like:${encodeURIComponent(action.data)}`]})) + .then(createFakePager) + .then(collection => { + return { + complete: action.complete, + error: action.error, + collection: collection, + }; + }); + + return Observable.fromPromise(searchPromise); + }) + .concatAll() + .subscribe(actionResult => { + dataElementOperandStore.setState(actionResult.collection); + actionResult.complete(); + }); + +dataElementOperandSelectorActions.getNextPage + .forEach((pager) => { + console.log('Next:', pager); + }); + +dataElementOperandSelectorActions.getPreviousPage + .forEach((pager) => { + console.log('Previous:', pager); + }); + +const ExtraFields = React.createClass({ + propTypes: { + modelToEdit: React.PropTypes.object.isRequired, + description: React.PropTypes.string.isRequired, + formula: React.PropTypes.string.isRequired, + }, + + mixins: [FormUpdateContext], + + getInitialState() { + return { + dialogValid: true, + }; + }, + + render() { + const dialogActions = [ + , + ]; + + return ( +
+ + + + {this.state ? : null} + +
+ ); + }, + + setNumerator() { + this.setState({ + type: 'numerator', + }); + this.refs.dialog.show(); + }, + + setDenominator() { + this.setState({ + type: 'denominator', + }); + this.refs.dialog.show(); + }, + + closeDialog() { + this.refs.dialog.dismiss(); + }, + + indicatorExpressionChanged(data) { + this.setState({ + dialogValid: data.expressionStatus.isValid && Boolean(data.description.trim()), + }); + + if (data.expressionStatus.isValid) { + this.context.updateForm('numerator', data.formula, this.props.formula); + this.context.updateForm('numeratorDescription', data.description, this.props.description); + } + }, +}); + +export default class extends EditModel { + componentWillMount() { + super.componentWillMount(); + } + + extraFieldsForModelType() { + return ( + + ); + } +} diff --git a/src/EditModel/model-specific-components/IndicatorExpressionManagerContainer.component.js b/src/EditModel/model-specific-components/IndicatorExpressionManagerContainer.component.js new file mode 100644 index 000000000..db731d952 --- /dev/null +++ b/src/EditModel/model-specific-components/IndicatorExpressionManagerContainer.component.js @@ -0,0 +1,54 @@ +import React from 'react'; +import Action from 'd2-flux/action/Action'; +import FormUpdateContext from 'd2-ui-basicfields/FormUpdateContext.mixin'; +import IndicatorExpressionManager from 'd2-ui/indicator-expression-manager/IndicatorExpressionManager.component'; +import indicatorExpressionStatusStore from 'd2-ui/indicator-expression-manager/indicatorExpressionStatus.store'; +import dataElementOperandSelectorActions from 'd2-ui/indicator-expression-manager/dataElementOperandSelector.actions'; +import {getInstance as getD2} from 'd2'; +import {Observable} from 'rx'; + +const indicatorExpressionStatusActions = Action.createActionsFromNames(['requestExpressionStatus']); +indicatorExpressionStatusActions.requestExpressionStatus + .throttle(500) + .map(action => { + const encodedFormula = encodeURIComponent(action.data); + const url = `expressions/description?expression=${encodedFormula}`; + const request = getD2() + .then(d2 => { + return d2.Api.getApi().get(url); + }); + + return Observable.fromPromise(request); + }) + .concatAll() + .subscribe(response => { + indicatorExpressionStatusStore.setState(response); + }); + +const IndicatorExpressionManagerContainer = React.createClass({ + propTypes: { + indicatorExpressionChanged: React.PropTypes.func.isRequired, + description: React.PropTypes.string.isRequired, + formula: React.PropTypes.string.isRequired, + }, + + mixins: [FormUpdateContext], + + render() { + return ( + + ); + }, +}); + +export default IndicatorExpressionManagerContainer; diff --git a/src/EditModel/modelToEditStrore.js b/src/EditModel/modelToEditStore.js similarity index 100% rename from src/EditModel/modelToEditStrore.js rename to src/EditModel/modelToEditStore.js diff --git a/src/EditModel/objectActions.js b/src/EditModel/objectActions.js index 054d72dea..8950bef53 100644 --- a/src/EditModel/objectActions.js +++ b/src/EditModel/objectActions.js @@ -1,27 +1,53 @@ import Action from 'd2-flux/action/Action'; -import modelToEditStore from './modelToEditStrore'; +import modelToEditStore from './modelToEditStore'; +import {isFunction} from 'd2-utils'; +import log from 'loglevel'; -const objectActions = Action.createActionsFromNames(['getObjectOfTypeById', 'saveObject', 'saveAndRedirectToList']); +const objectActions = Action.createActionsFromNames(['getObjectOfTypeById', 'getObjectOfTypeByIdAndClone', 'saveObject', 'afterSave', 'saveAndRedirectToList']); -//TODO: Extract this a convenience method onto the action object? +// 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]; + const actionTransformer = mapping[2]; - this[actionKey].subscribe(function (actionConfig) { + this[actionKey].subscribe(actionConfig => { + let action = actionConfig; if (actionTransformer) { - actionConfig = actionTransformer(actionConfig); + action = actionTransformer(action); } - store[mapping[1]](actionConfig); + store[mapping[1]](action); }); }); }); }; objectActions.mapActionsToStore([ - [['saveObject', 'saveAndRedirectToList'], 'save'], - [['getObjectOfTypeById'], 'getObjectOfTypeById', (config) => { console.log('action transformer'); return config.data; }] + [['getObjectOfTypeById'], 'getObjectOfTypeById', (action) => { return action.data; }], + [['getObjectOfTypeByIdAndClone'], 'getObjectOfTypeByIdAndClone', (action) => { return action.data; }], ], modelToEditStore); +objectActions.saveObject.subscribe(action => { + const errorHandler = (message) => { + action.error(message); + }; + + const successHandler = () => { + if (isFunction(action.data.afterSave)) { + log.info('Handling after save'); + action.data.afterSave(action) + .subscribe( + () => action.complete('Success with aftersave'), + errorHandler + ); + } else { + action.complete('Success'); + } + }; + + return modelToEditStore + .save(action.data.id) + .subscribe(successHandler, errorHandler); +}); + export default objectActions; diff --git a/src/List/ContextActions.js b/src/List/ContextActions.js index a43e4703c..4518d9a5b 100644 --- a/src/List/ContextActions.js +++ b/src/List/ContextActions.js @@ -1,13 +1,46 @@ -'use strict'; - import Router from 'react-router'; import Action from 'd2-flux/action/Action'; +import detailsStore from './details.store'; + +const contextActions = Action.createActionsFromNames(['edit', 'clone', 'delete', 'details', 'translate']); -let contextActions = Action.createActionsFromNames(['edit', 'translate']); +const confirm = (message) => { + return new Promise((resolve, reject) => { + if (window.confirm(message)) { + resolve(); + } + reject(); + }); +}; contextActions.edit - .subscribe(function (action) { - Router.HashLocation.push(['/edit', action.data.modelDefinition.name, action.data.id].join('/')) + .subscribe(action => { + Router.HashLocation.push(['/edit', action.data.modelDefinition.name, action.data.id].join('/')); + }); + +contextActions.clone + .subscribe(action => { + Router.HashLocation.push(['/clone', action.data.modelDefinition.name, action.data.id].join('/')); + }); + +contextActions.delete + .subscribe(({data: model}) => { + return confirm(`Do you really want to delete ${model.name}?`) + .then(() => { + model.delete() + .then(() => { + console.info('Deleted!'); + }) + .catch(response => { + console.warn(response.responseJSON.message); + }); + }); + }); + +contextActions.details + .subscribe(({data: model}) => { + console.log(model); + detailsStore.setState(model); }); export default contextActions; diff --git a/src/List/List.component.js b/src/List/List.component.js index 1dc8b60e7..78d821d50 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -1,69 +1,43 @@ import React from 'react'; import {Navigation} from 'react-router'; - +import classes from 'classnames'; 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 DetailsBox from 'd2-ui-detailsbox/DetailsBox.component'; +import Sticky from 'react-sticky'; import contextActions from './ContextActions'; - +import detailsStore from './details.store'; 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...' - } +import Paper from 'material-ui/lib/paper'; +import {config} from 'd2'; +import Translate from 'd2-ui/i18n/Translate.mixin'; +import ListActionBar from './ListActionBar.component'; +import SearchBox from './SearchBox.component'; +import LoadingStatus from './LoadingStatus.component'; + +config.i18n.strings.add('list_for'); + +const List = React.createClass({ + propTypes: { + params: React.PropTypes.shape({ + modelType: React.PropTypes.string.isRequired, + }), }, - 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], + mixins: [Navigation, ObserverRegistry, Translate], getInitialState() { return { dataRows: [], pager: { - total: 0 + total: 0, }, - isLoading: true + isLoading: true, + detailsObject: null, }; }, @@ -72,7 +46,7 @@ var List = React.createClass({ const sourceStoreDisposable = listStore.subscribe(listStoreValue => { if (!isIterable(listStoreValue.list)) { - return; //Received value is not iterable, keep waiting + return; // Received value is not iterable, keep waiting } this.setState({ @@ -81,58 +55,28 @@ var List = React.createClass({ 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('/'); - } - } - ); + const detailsStoreDisposable = detailsStore.subscribe(detailsObject => { + this.setState({detailsObject}); + }); - this.registerDisposable(searchListDisposable); + this.registerDisposable(detailsStoreDisposable); }, componentWillReceiveProps(newProps) { if (this.props.params.modelType !== newProps.params.modelType) { this.setState({ - isLoading: true + 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}`; + const currentlyShown = `${(this.state.dataRows.length * (this.state.pager.page - 1))} - ${this.state.dataRows.length * this.state.pager.page}`; - let paginationProps = { + const paginationProps = { hasNextPage: () => Boolean(this.state.pager.hasNextPage) && this.state.pager.hasNextPage(), hasPreviousPage: () => Boolean(this.state.pager.hasPreviousPage) && this.state.pager.hasPreviousPage(), onNextPageClick: () => { @@ -144,20 +88,67 @@ var List = React.createClass({ listActions.getPreviousPage(); }, total: this.state.pager.total, - currentlyShown + currentlyShown, }; return (
-

List of {this.props.params.modelType}

+

{this.getTranslation('list_for')} {this.props.params.modelType}

+ - - {this.state.dataRows.length ? null :
No results found
} +
+ + {this.state.dataRows.length ? null :
No results found
} +
+
+ + + + + +
); }, + + executeLoadListAction(modelType) { + this.setState({isLoading: true}); + + const searchListDisposable = listActions.loadList(modelType) + .subscribe( + (message) => { console.info(message); }, + (message) => { + if (/^.+s$/.test(modelType)) { + const 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); + }, + + 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); + }, }); export default List; diff --git a/src/List/ListActionBar.component.js b/src/List/ListActionBar.component.js new file mode 100644 index 000000000..38ec384cc --- /dev/null +++ b/src/List/ListActionBar.component.js @@ -0,0 +1,33 @@ +import React from 'react'; +import {Navigation} from 'react-router'; +import FloatingActionButton from 'material-ui/lib/floating-action-button'; +import FontIcon from 'material-ui/lib/font-icon'; + +const ListActionBar = React.createClass({ + propTypes: { + modelType: React.PropTypes.string.isRequired, + }, + + mixins: [Navigation], + + render() { + const cssStyles = { + textAlign: 'right', + marginTop: '1rem', + }; + + return ( +
+ + add + +
+ ); + }, + + _addClick() { + this.transitionTo('genericEdit', {modelType: this.props.modelType, modelId: 'add'}); + }, +}); + +export default ListActionBar; diff --git a/src/List/LoadingStatus.component.js b/src/List/LoadingStatus.component.js new file mode 100644 index 000000000..7c57ce5f0 --- /dev/null +++ b/src/List/LoadingStatus.component.js @@ -0,0 +1,22 @@ +import React from 'react'; +import LinearProgress from 'material-ui/lib/linear-progress'; + +export default React.createClass({ + propTypes: { + isLoading: React.PropTypes.bool.isRequired, + }, + + getDefaultProps() { + return { + isLoading: false, + }; + }, + + render() { + if (!this.props.isLoading) { return null; } + + return ( + + ); + }, +}); diff --git a/src/List/SearchBox.component.js b/src/List/SearchBox.component.js new file mode 100644 index 000000000..7d55d1a0f --- /dev/null +++ b/src/List/SearchBox.component.js @@ -0,0 +1,38 @@ +import React from 'react'; +import ObservedEvents from '../utils/ObservedEvents.mixin'; +import Translate from 'd2-ui/i18n/Translate.mixin'; +import TextField from 'material-ui/lib/text-field'; +import {config} from 'd2'; + +config.i18n.strings.add('search_by_name'); +config.i18n.strings.add('press_enter_to_search'); + +const SearchBox = React.createClass({ + propTypes: { + searchObserverHandler: React.PropTypes.func.isRequired, + }, + + mixins: [ObservedEvents, Translate], + + componentDidMount() { + const searchObserver = this.events.searchBox + .throttle(400) + .map(event => event && event.target && event.target.value ? event.target.value : '') + .distinctUntilChanged(); + + this.props.searchObserverHandler(searchObserver); + }, + + render() { + return ( +
+ +
+ ); + }, +}); + +export default SearchBox; diff --git a/src/List/details.store.js b/src/List/details.store.js new file mode 100644 index 000000000..9923850d2 --- /dev/null +++ b/src/List/details.store.js @@ -0,0 +1,3 @@ +import Store from 'd2-flux/store/Store'; + +export default Store.create(); diff --git a/src/List/list.actions.js b/src/List/list.actions.js index 2e23b04d0..d1eb53ec6 100644 --- a/src/List/list.actions.js +++ b/src/List/list.actions.js @@ -1,7 +1,8 @@ import Action from 'd2-flux/action/Action'; import listStore from './list.store'; +import detailsStore from './details.store'; -const listActions = Action.createActionsFromNames(['loadList', 'searchByName', 'getNextPage', 'getPreviousPage']); +const listActions = Action.createActionsFromNames(['loadList', 'searchByName', 'getNextPage', 'getPreviousPage', 'hideDetailsBox']); listActions.loadList.subscribe(action => { listStore.getListFor(action.data, action.complete, action.error); @@ -11,7 +12,7 @@ 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 +// TODO: For simple action mapping like this we should be able to do something less boiler plate like listActions.getNextPage.subscribe(() => { listStore.getNextPage(); }); @@ -20,4 +21,8 @@ listActions.getPreviousPage.subscribe(() => { listStore.getPreviousPage(); }); +listActions.hideDetailsBox.subscribe(() => { + detailsStore.setState(null); +}); + export default listActions; diff --git a/src/List/list.store.js b/src/List/list.store.js index fc6553775..93d9a4731 100644 --- a/src/List/list.store.js +++ b/src/List/list.store.js @@ -1,4 +1,4 @@ -import d2 from '../utils/d2'; +import {getInstance as getD2} from 'd2'; import {Subject, Observable} from 'rx'; import Store from 'd2-flux/store/Store'; @@ -8,20 +8,20 @@ export default Store.create({ initialise() { this.listSourceSubject - .flatMapLatest(list => list) + .concatAll() .subscribe(modelCollection => { this.setState({ pager: modelCollection.pager, - list: modelCollection.toArray() + list: modelCollection.toArray(), }); }); return this; }, getListFor(modelName, complete, error) { - d2.then(d2 => { + getD2().then(d2 => { if (d2.models[modelName]) { - let listPromise = d2.models[modelName] + const listPromise = d2.models[modelName] .list(); this.listSourceSubject.onNext(Observable.fromPromise(listPromise)); @@ -42,12 +42,12 @@ export default Store.create({ }, searchByName(modelType, searchString, complete, error) { - d2.then(d2 => { + getD2().then(d2 => { if (!d2.models[modelType]) { error(modelType + ' is not a valid schema name'); } - let listSearchPromise = d2.models[modelType] + const listSearchPromise = d2.models[modelType] .filter().on('name').like(searchString) .list({fields: 'name,id,lastUpdated'}); @@ -55,5 +55,5 @@ export default Store.create({ 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 index 99a9ac400..1b4b7e630 100644 --- a/src/MainContent/MainContent.component.js +++ b/src/MainContent/MainContent.component.js @@ -2,6 +2,10 @@ import React from 'react'; import classes from 'classnames'; const MainContent = React.createClass({ + propTypes: { + children: React.PropTypes.array.isRequired, + }, + render() { const classList = classes('main-content'); diff --git a/src/SideBar/SideBar.component.js b/src/SideBar/SideBar.component.js index 0982a10f0..a710ef783 100644 --- a/src/SideBar/SideBar.component.js +++ b/src/SideBar/SideBar.component.js @@ -1,23 +1,31 @@ import React from 'react'; import log from 'loglevel'; import ObservedEvents from '../utils/ObservedEvents.mixin'; +import List from 'material-ui/lib/lists/list'; +import ListItem from 'material-ui/lib/lists/list-item'; +import TextField from 'material-ui/lib/text-field'; const SideBar = React.createClass({ - mixins: [ObservedEvents], - propTypes: { - filterChildren: React.PropTypes.func + filterChildren: React.PropTypes.func, + items: React.PropTypes.shape({ + map: React.PropTypes.func.isRequired, + }).isRequired, + title: React.PropTypes.string.isRequired, + searchHint: React.PropTypes.string.isRequired, }, + mixins: [ObservedEvents], + getDefaultProps() { return { - items: [] + items: [], }; }, getInitialState() { return { - searchString: '' + searchString: '', }; }, @@ -35,41 +43,45 @@ const SideBar = React.createClass({ searchString => { console.log(searchString); this.setState({ - searchString: searchString + searchString: searchString, }); }, error => { - log.error('Could not set the search string'); + log.error('Could not set the search string', error); } ); }, - 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; + this.filteredChildren = this.props.items + .map(item => { + 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, item.primaryText)) { + return null; + } } - } - return (
  • {React.cloneElement(child, {key: child.props.params.modelType})}
  • ); - }); + return (); + }); return (

    {this.props.title}

    - -
      + + {this.filteredChildren} -
    +
    ); - } + }, + + 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(); + }, }); export default SideBar; diff --git a/src/SideBar/SideBarContainer.component.js b/src/SideBar/SideBarContainer.component.js index 3990b7a28..66971a376 100644 --- a/src/SideBar/SideBarContainer.component.js +++ b/src/SideBar/SideBarContainer.component.js @@ -1,16 +1,21 @@ import React from 'react'; -import {State, Link} from 'react-router'; - +import {State, Navigation} from 'react-router'; import sideBarItemsStore from './sideBarItems.store'; - import SideBar from './SideBar.component'; +import {config} from 'd2'; +import Translate from 'd2-ui/i18n/Translate.mixin'; +import {camelCaseToUnderscores} from 'd2-utils'; + +config.i18n.strings.add('maintenance'); +config.i18n.strings.add('filter_menu_items_by_name'); +config.i18n.strings.add('enter_to_go_to_first'); -let SideBarContainer = React.createClass({ - mixins: [State], +const SideBarContainer = React.createClass({ + mixins: [State, Navigation, Translate], getInitialState() { return { - sideBarItems: [] + sideBarItems: [], }; }, @@ -22,20 +27,34 @@ let SideBarContainer = React.createClass({ }); }, - 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() { + const items = this.state.sideBarItems + .map(listItem => { + return { + primaryText: this.getTranslation(camelCaseToUnderscores(listItem)), + secondaryText: this.getTranslation(`intro_${camelCaseToUnderscores(listItem)}`), + secondaryTextLines: 2, + modelType: listItem, + isActive: this.isActive('list', {modelType: listItem}), + onClick: function onClick() { + this.transitionTo('list', {modelType: listItem}); + }.bind(this), + }; + }); + return ( - - {this.state.sideBarItems.map(modelType => { - return ({modelType}); - })} - + ); - } + }, + + filterChildren(searchString, child) { + // Both values are transformed to lowercase so we can do case insensitive search + return child.toLowerCase().indexOf(searchString.toLowerCase()) >= 0; + }, }); export default SideBarContainer; diff --git a/src/SideBar/sideBarItems.store.js b/src/SideBar/sideBarItems.store.js index dfa91d7b0..60ac0fa3e 100644 --- a/src/SideBar/sideBarItems.store.js +++ b/src/SideBar/sideBarItems.store.js @@ -1,12 +1,30 @@ import Store from 'd2-flux/store/Store'; -import d2 from '../utils/d2'; +import {getInstance as getD2} from 'd2'; const sideBarItemsStore = Store.create(); +const isInPredefinedList = (name) => { + return [ + 'dataElement', + 'dataElementGroup', + 'dataElementGroupSet', + 'categoryOptionCombo', + 'categoryOption', + 'category', + 'categoryCombo', + 'categoryOptionGroup', + 'categoryOptionGroupSet', + 'indicator', + 'indicatorType', + 'indicatorGroup', + 'indicatorGroupSet', + ].indexOf(name) >= 0; +}; -d2.then((d2) => { - const sideBarItems = d2.models.mapThroughDefinitions(definition => { - return definition.name; - }).sort(); +getD2().then((d2) => { + const sideBarItems = d2.models + .mapThroughDefinitions(definition => definition.name) + .sort() + .filter(isInPredefinedList); sideBarItemsStore.setState(sideBarItems); }); diff --git a/src/Snackbar/SnackbarContainer.component.js b/src/Snackbar/SnackbarContainer.component.js new file mode 100644 index 000000000..31fa56fac --- /dev/null +++ b/src/Snackbar/SnackbarContainer.component.js @@ -0,0 +1,50 @@ +import React from 'react'; +import Snackbar from 'material-ui/lib/snackbar'; +import snackStore from './snack.store'; +import ObserverRegistry from '../utils/ObserverRegistry.mixin'; + +const SnackBarContainer = React.createClass({ + mixins: [ObserverRegistry], + + getInitialState() { + return {}; + }, + + componentWillMount() { + const snackStoreDisposable = snackStore.subscribe(snack => { + console.log('Show!'); + + if (snack) { + this.setState({ + snack: snack, + }, () => { + this.refs.snackbar.show(); + }); + } else { + this.refs.snackbar.dismiss(); + } + }, console.log.bind(console)); + + this.registerDisposable(snackStoreDisposable); + }, + + render() { + console.log(this.state.snack); + if (!this.state.snack) { + return null; + } + + return ( + + ); + }, +}); + +export default SnackBarContainer; diff --git a/src/Snackbar/snack.actions.js b/src/Snackbar/snack.actions.js new file mode 100644 index 000000000..dc58ea36a --- /dev/null +++ b/src/Snackbar/snack.actions.js @@ -0,0 +1,23 @@ +import Action from 'd2-flux/action/Action'; +import snackStore from './snack.store'; + +const snackActions = Action.createActionsFromNames(['show', 'hide']); + +snackActions.show.subscribe(actionConfig => { + const {message, action, autoHideDuration, onActionTouchTap} = actionConfig.data; + + snackStore.setState({ + message, + action: action || 'dismiss', + autoHideDuration, + onActionTouchTap: onActionTouchTap || (() => { + snackActions.hide(); + }), + }); +}); + +snackActions.hide.subscribe(() => { + snackStore.setState(null); +}); + +export default snackActions; diff --git a/src/Snackbar/snack.store.js b/src/Snackbar/snack.store.js new file mode 100644 index 000000000..9923850d2 --- /dev/null +++ b/src/Snackbar/snack.store.js @@ -0,0 +1,3 @@ +import Store from 'd2-flux/store/Store'; + +export default Store.create(); diff --git a/src/config/field-config/field-order.js b/src/config/field-config/field-order.js index 308bed6a2..d6da365f1 100644 --- a/src/config/field-config/field-order.js +++ b/src/config/field-config/field-order.js @@ -1,4 +1,4 @@ -let fieldOrderByName = new Map([ +const fieldOrderByName = new Map([ ['dataElement', [ 'name', 'code', 'shortName', 'description', 'formName', 'domainType', 'type', 'numberType', 'textType', 'aggregationOperator', 'zeroIsSignificant', 'url', 'categoryCombo', 'optionSet', 'commentOptionSet', 'legendSet', 'aggregationLevels', 'data']], @@ -9,7 +9,7 @@ let fieldOrderByName = new Map([ ['indicator', ['name', 'shortName', 'code', 'description', 'annualized', 'decimals', 'indicatorType', 'legendSet', 'url']], ['indicatorGroup', ['name', 'indicators']], ['indicatorType', ['name', 'factor', 'number']], - ['indicatorGroupSet', ['name', 'description', 'compulsory', 'indicatorGroups']] + ['indicatorGroupSet', ['name', 'description', 'compulsory', 'indicatorGroups']], ]); export default { @@ -32,5 +32,5 @@ export default { 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 index 65667ec10..dea6f7afa 100644 --- a/src/config/field-config/header-fields.js +++ b/src/config/field-config/header-fields.js @@ -1,8 +1,8 @@ -let headerFieldsMap = new Map([ +const headerFieldsMap = new Map([ ['dataElementGroupSet', ['name', 'code']], ['categoryOptionCombo', ['name', 'code']], ['category', ['name', 'code']], - ['categoryCombo', ['name', 'code']] + ['categoryCombo', ['name', 'code']], ]); export default { @@ -18,5 +18,5 @@ export default { return headerFieldsMap.get(schemaName); } return ['name', 'shortName']; - } + }, }; diff --git a/src/config/field-overrides/dataElement.js b/src/config/field-overrides/dataElement.js index 5419586fa..27e5efc69 100644 --- a/src/config/field-overrides/dataElement.js +++ b/src/config/field-overrides/dataElement.js @@ -1,20 +1,19 @@ import {SELECT, MULTISELECT} from 'd2-ui-basicfields/fields'; -import d2 from '../../utils/d2'; +import {getInstance as getD2} from 'd2'; + +const organisationUnitLevelsPromise = getD2() + .then(d2 => d2.models.organisationUnitLevel.list()); + +const organisationUnitLevelsMapPromise = organisationUnitLevelsPromise + .then(collection => { + return new Map(collection + .toArray() + .map(value => { + return [value.level, value.name]; + })); + }); export default new Map([ - ['type', { - type: SELECT, - templateOptions: { - options: [ - 'int', - 'string', - 'bool', - 'trueOnly', - 'date', - 'username' - ] - } - }], ['aggregationOperator', { type: SELECT, templateOptions: { @@ -26,57 +25,34 @@ export default new Map([ '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' - ] + 'max', + ], }, - 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 - }; - }); - }); - }); + return organisationUnitLevelsPromise + .then(collection => { + return collection.toArray() + .map(item => { + return { + name: item.name, + id: item.level, + }; + }); + }); }, fromModelTransformer(modelValue) { - console.log(modelValue); - return { - id: modelValue - }; + return organisationUnitLevelsMapPromise + .then(organisationUnitLevelsMap => { + if (organisationUnitLevelsMap.has(modelValue)) { + return { + name: organisationUnitLevelsMap.get(modelValue), + 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 index e574d0370..ab9af80cc 100644 --- a/src/config/field-overrides/index.js +++ b/src/config/field-overrides/index.js @@ -1,7 +1,9 @@ import dataElement from './dataElement'; +import indicator from './indicator'; -let overridesByType = { - dataElement +const overridesByType = { + dataElement, + indicator, }; export default { @@ -21,8 +23,8 @@ export default { */ for(schemaName) { if (schemaName && overridesByType[schemaName]) { - return overridesByType[schemaName] + return overridesByType[schemaName]; } return new Map(); - } + }, }; diff --git a/src/config/field-overrides/indicator.js b/src/config/field-overrides/indicator.js new file mode 100644 index 000000000..622921ef0 --- /dev/null +++ b/src/config/field-overrides/indicator.js @@ -0,0 +1,14 @@ +export default new Map([ + ['numerator', { + hide: () => true, + }], + ['numeratorDescription', { + hide: () => true, + }], + ['denominator', { + hide: () => true, + }], + ['denominatorDescription', { + hide: () => true, + }], +]); diff --git a/src/i18n/i18n_global.properties b/src/i18n/i18n_global.properties new file mode 100644 index 000000000..bbd0e6cff --- /dev/null +++ b/src/i18n/i18n_global.properties @@ -0,0 +1,231 @@ +indicator_type=Indicator Type +aggregate=Aggregate +tracker=Tracker +no_option_assigned=Use of category with an empty option is not allowed! +number_of_category_options=Number of category options +number_of_categories=Number of categories +sort=Sort +create_new_data_element_category=Create new data element category +selected_categories=Selected categories +available_categories=Available categories +category=Category +category_option=Category Option +data_element_category_management=Data element category management +data_element_category_combo_management=Data element category combination management +create_new_data_element_category_combo=Create new data element category combination +confirm_delete_data_element_category_option=Are you sure you want to delete this data element category option? +confirm_delete_data_element_category=Are you sure you want to delete this data element category? +confirm_delete_data_element_category_combo=Are you sure you want to delete this data element category combo? +edit_data_element_category=Edit data element category +edit_data_element_category_combo=Edit data element category combination +category_combination=Category Combination +data_element_category_combo_details=Data element category combo details +category_combo=Combination of categories +data_element_category=Data Element Category +data_element_category_combo=Data Element Category Combination +select=Select +data_element=Data Element +data_element_group=Data Element Group +data_element_groups=Data Element Groups +data_elements=Data elements +indicator=Indicator +indicator_group=Indicator Group +indicator_groups=Indicator Groups +create_new_data_element=Create new data element +domain_type=Domain Type +create_new_data_element_group=Create new data element group +group_members=Group members +available_data_elements=Available data elements +create_new_indicator=Create new indicator +create_new_indicator_group=Create new indicator group +available_indicators=Available indicators +create_new_indicator_type=Create new indicator type +factor=Factor +data_element_management=Data element management +data_element_group_management=Data element group management +number_of_members=Number of members +export=Export +import=Import +indicator_management=Indicator management +indicator_group_management=Indicator group management +indicator_type_management=Indicator type management +edit_data_element=Edit data element +edit_data_element_group=Edit data element group +edit_indicator=Edit indicator +edit_indicator_group=Edit indicator group +edit_indicator_type=Edit indicator type +edit_numerator=Edit numerator +edit_denominator=Edit denominator +confirm_delete_data_element=Are you sure you want to delete this data element? +confirm_delete_data_element_group=Are you sure you want to delete this data element group? +confirm_delete_indicator=Are you sure you want to delete this indicator? +confirm_delete_indicator_group=Are you sure you want to delete this indicator group? +confirm_delete_indicator_type=Are you sure you want to delete this indicator type? +confirm_delete_data_dictionary=Are you sure you want to delete this data dictionary? +everything_is_ok=Everything is OK +adding_data_element_group_failed=Adding the data element group failed with the following message +saving_data_element_group_failed=Saving the data element group failed with the following message +adding_indicator_failed=Adding the indicator failed with the following message +saving_indicator_failed=Saving the indicator failed with the following message +adding_indicator_group_failed=Adding the indicator group failed with the following message +saving_indicator_group_failed=Saving the indicator group failed with the following message +adding_indicator_type_failed=Adding the indicator type failed with the following message +saving_indicator_type_failed=Saving the indicator type failed with the following message +calculated=Calculated +available_dataelements=Available Data Elements +selected_dataelements=Selected Data Elements +add_selected=Add selected +remove_all=Remove all +create_new_data_dictionary=Create new data dictionary +region=Region +select_domain_type=Select domain type +current=Current +date=Date +selected_data_elements=Selected data elements +selected_indicators=Selected indicators +available_indicators=Available indicators +item_deleted_successfully=Item deleted successfully +value=Value +confirm_delete_indicator=Do you want to delete indicator? +adding_indicator_failed=Adding indicator failed +saving_indicator_failed=Saving indicator failed +translation_translate=Translate +select_indicator=Please select indicator +object_not_deleted_associated_by_objects=Object not deleted because it is associated by objects of type +hide_warning=Hide warning +update_dataelement_group_members=Update Data Element Group Member +update_success=Update was successful +member_of=Member of +data_element_group_editor=Data Element Group Editor +move_up=Move up +move_down=Move down +move_to_top=Move to top +move_to_bottom=Move to bottom +indicator_sort_order=Indicator sort order +data_element_sort_order=Data element sort order +indicator_group_editor=Indicator Group Editor +update_indicator_group_member=Update Indicator Member +factor_cannot_be_zero=Factor cannot be zero +url=URL +aggregation_levels=Aggregation levels +available_aggregation_levels=Available aggregation levels +selected_aggregation_levels=Selected aggregation levels +remove_selected=Remove selected +category_options=Category options +add_category_option=Add category option +must_include_category_option=Please include one or more category options +specify_category_option_name=Please specify a category option name +category_option_name_already_exists=The category option name already exists +data_element_group_set=Data Element Group Set +indicator_group_set=Indicator Group Set +name_aldready_exists=Name already exists ! +available_dataelementgroup=Available Data Element Groups +selected_dataelementgroup=Selected Data Element Groups +add_dataelementgroupset=Add Data Element Group Set +update_dataelementgroupset=Update Data Element Group Set +add_indicatorgroupset=Add Indicator Group Set +update_indicatorgroupset=Update Indicator Group Set +available_indicatorgroup=Available Indicator Groups +selected_indicatorgroup=Selected Indicator Groups +last_updated=Last updated +dataelement_id_not_numeric=Data element identifier must be a number +category_option_combo_id_not_numeric=Category option combo identifier must be a number +data_element_does_not_exist=Identifier does not reference a data element +category_option_combo_does_not_exist=Identifier does not reference a category option combo +expression_not_well_formed=Expression is not well formed +intro_data_element=Create, modify, view and delete data elements. Data elements are phenomena for which will be captured and analyzed. +intro_data_element_group=Create, modify, view and delete data element groups. Groups are used for improved analysis. +intro_data_element_group_editor=Easily add or remove data elements to and from data element groups, as well as deleted data elements. +intro_data_element_group_set=Create, modify, view and delete data element group sets. Group sets are used for improved analysis. +intro_category_option=Create, modify, view and delete data element category options. Category options are options with in category. +intro_category=Create, modify, view and delete data element categories. Categories are used for disaggregation of data elements. +intro_category_combo=Create, modify, view and delete data element category combinations. +intro_category_option_group=Create, modify, view and delete category option groups, which can be used to classify category options. +intro_category_option_group_set=Create, modify, view and delete category option group sets, which can be used for improved data analysis. +intro_indicator=Create, modify, view and delete indicators. An indicator is a formula consisting of data elements and numbers. +intro_indicator_type=Create, modify, view and delete indicator types. An indicator type is a factor for an indicator, like percentage. +intro_indicator_group=Create, modify, view and delete indicator groups. Groups are used for improved analysis. +intro_indicator_group_editor=Easily add or remove indicators to and from indicator groups, as well as delete indicators. +intro_indicator_group_set=Create, modify, view and delete indicator group sets. Group sets are used for improved analysis. +intro_data_dictionary=Create, modify, view and delete data dictionaries. A data dictionary is a set of meta-data. +intro_concept=Create, modify, view and delete concepts. A concept can be used by a category. +available_data_element_group_sets=Available data element group sets +selected_data_element_group_sets=Selected data element group sets +concept_name=Concept name +available_groups=Available Groups +selected_groups=Selected Groups +assign_groups_for_dataelement=Assign Groups for Data Element +select_dataelement=Please select data element +option_rename_successfully=Category option renamed successfully +create_new_concept=Create new concept +edit_concept=Edit concept +concept_name=Concept name +confirm_delete_concept=Are you sure you want to delete this concept? +adding_concept_failed=Adding concept failed +saving_concept_failed=Saving concept failed +regex_add_edit_concept=Enter alphabet (a-z A-Z), number (0-9) and underscore (_) characters only. +concept_management=Concept management +concept=Concept +selected_name=Selected name +update_category_option=Update category option +move_selected=Move selected +number_value_type=Number type +number=Number +int=Integer +view_1=View 1 +view_2=View 2 +store_zero_data_values=Store zero data values +form_name=Form name +compulsory=Compulsory +data_dimension=Data Dimension +select_group=Select group +option_set=Option set +please_select=Please select +formula=Formula +legend_set=Legend set +skip_total_in_reports=Skip category total in reports +data_element_category_option = Data Element Category Option +data_element_category_option_management = Data element category option management +data_element_category_option_combination_management = Data element category option combination management +create_new_data_element_category_option=Create new data element category option +edit_data_element_category_option=Edit data element category option +available_category_options=Available category options +selected_category_options=Selected category options +use_as_data_dimension=Use as data dimension +tip=Tip +use=use +dimension_type=Dimension type +disaggregation=Disaggregation +attribute=Attribute +option_set_for_data_values=Option set for data values +option_set_for_comments=Option set for comments +organisation_unit_counts=Organisation unit counts +category_option=Category Option +category=Category +category_combo=Category Combination +category_option_group = Category Option Group +category_option_group_management = Category option group management +create_new_category_option_group = Create new category option group +edit_category_option_group = Edit category option group +confirm_delete_category_option_group = Are you sure you want to delete this category option group? +category_option_group_set = Category Option Group Set +category_option_group_set_management = Category option group set management +create_new_category_option_group_set = Create new category option group set +edit_category_option_group_set = Edit category option group set +available_category_option_groups = Available category option groups +selected_category_option_groups = Selected category option groups +confirm_delete_category_option_group_set = Are you sure you want to delete this category option group set? +show_more_options=Show more options +show_fewer_options=Show fewer options +id=Id +min=Min +max=Max +category_option_combo = Category Option Combination +intro_category_option_combo = View and edit data element category option combinations. Category option combinations are break-downs of category. +edit_data_element_category_option_combo = Edit data element category option combo +average_sum_in_org_unit_hierarchy=Average (sum in org unit hierarchy) +approve_data=Approve data +decimals_in_data_output=Decimals in data output +categories = Categories +ignore_data_approval=Ignore data approval +data_dimension_type=Data dimension type diff --git a/src/index.html b/src/index.html index 92e81ceab..96a5bbd12 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,6 @@ - @@ -27,14 +26,14 @@ -
    +
    diff --git a/src/maintenance.js b/src/maintenance.js index ec18ef125..1982e4014 100644 --- a/src/maintenance.js +++ b/src/maintenance.js @@ -1,22 +1,11 @@ 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'; +import router from './router'; const routeActions = Action.createActionsFromNames(['transition']); -// declare our routes and their hierarchy -const routes = ( - - - - -); - -Router.run(routes, Router.HashLocation, (Root) => { +router.run((Root) => { React.render(, document.getElementById('app')); routeActions.transition(Router.HashLocation.getCurrentPath()); }); diff --git a/src/maintenance.scss b/src/maintenance.scss deleted file mode 100644 index 70fbe83f9..000000000 --- a/src/maintenance.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'normalize'; -@import 'susy'; diff --git a/src/router.js b/src/router.js new file mode 100644 index 000000000..2f59f15c9 --- /dev/null +++ b/src/router.js @@ -0,0 +1,23 @@ +import React from 'react'; +import Router from 'react-router'; +import {Route} from 'react-router'; +import App from './App/App.component'; +import List from './List/List.component'; +import EditModelContainer from './EditModel/EditModelContainer.component'; +import CloneModelContainer from './EditModel/CloneModelContainer.component'; + +const routes = ( + + + + + +); + +// we can create a router instance before "running" it +const router = Router.create({ + routes: routes, + location: Router.HashLocation, +}); + +module.exports = router; diff --git a/src/utils/ObservedEvents.mixin.js b/src/utils/ObservedEvents.mixin.js index 84e0e54ea..1d3f8078f 100644 --- a/src/utils/ObservedEvents.mixin.js +++ b/src/utils/ObservedEvents.mixin.js @@ -1,11 +1,7 @@ -'use strict'; - -import React from 'react'; -import {Subject, Observable} from 'rx/dist/rx.all'; - +import {Subject} from 'rx/dist/rx.all'; import log from 'loglevel'; -let ObservedEvents = { +const ObservedEvents = { getInitialState() { this.events = {}; this.eventSubjects = {}; @@ -13,10 +9,10 @@ let ObservedEvents = { return {}; }, - createEventObserver: function () { - let subjectMap = new Map(); + createEventObserver: (function createEventObserver() { + const subjectMap = new Map(); - return function (referenceName) { + return function ObservedEventHandler(referenceName) { let subject; if (!subjectMap.has(referenceName)) { @@ -27,23 +23,23 @@ let ObservedEvents = { } if (!this.events[referenceName]) { - //Run a map that keeps a copy of the event + // 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 + // 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 index e75f860b2..025dca87e 100644 --- a/src/utils/ObserverRegistry.mixin.js +++ b/src/utils/ObserverRegistry.mixin.js @@ -1,12 +1,9 @@ -'use strict'; - -import React from 'react'; -import {Subject, Observable} from 'rx/dist/rx.all'; - import log from 'loglevel'; const ObserverRegistry = { - observerDisposables: [], + componentWillMount() { + this.observerDisposables = []; + }, componentWillUnmount() { log.info('Disposing: ', this.observerDisposables); @@ -16,7 +13,7 @@ const ObserverRegistry = { 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 deleted file mode 100644 index 3d87dd61e..000000000 --- a/src/utils/d2.js +++ /dev/null @@ -1,3 +0,0 @@ -import d2Init from 'd2'; - -export default d2Init({baseUrl: 'http://localhost:8080/dhis/api'}); diff --git a/test/List/list.store.test.js b/test/List/list.store.test.js index 6f28b9d15..d8c266d04 100644 --- a/test/List/list.store.test.js +++ b/test/List/list.store.test.js @@ -1,5 +1,3 @@ -import listStore from '../../src/List/listStore'; - describe('Store: listStore', () => { }); diff --git a/test/SideBar/sideBarItems.store.test.js b/test/SideBar/sideBarItems.store.test.js index 38a991889..a1c462b4a 100644 --- a/test/SideBar/sideBarItems.store.test.js +++ b/test/SideBar/sideBarItems.store.test.js @@ -1,5 +1,7 @@ import sideBarItemsStore from '../../src/SideBar/sideBarItems.store'; describe('Store: sideBarItemsStore', () => { - + it('should be defined', () => { + expect(sideBarItemsStore).to.not.be.undefined; + }); }); diff --git a/test/config/karma.config.js b/test/config/karma.config.js index bdfac7956..f88d646f3 100644 --- a/test/config/karma.config.js +++ b/test/config/karma.config.js @@ -56,12 +56,6 @@ module.exports = function karmaConfig( config ) { 'src/**/*.js', ], - - // list of preprocessors - preprocessors: { - 'test/*': ['webpack'] - }, - logLevel: config.LOG_INFO, browsers: ['PhantomJS'], diff --git a/test/index.js b/test/index.js index 0928acc83..ac90c7c14 100644 --- a/test/index.js +++ b/test/index.js @@ -1,2 +1,2 @@ -var testsContext = require.context(".", true, /\.test$/); +const testsContext = require.context('.', true, /\.test$/); testsContext.keys().forEach(testsContext); diff --git a/webpack.config.js b/webpack.config.js index de1f24212..b90d18953 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,7 +15,7 @@ module.exports = { exclude: [/(node_modules)/, /d2\-ui/], loader: 'babel', query: { - stage: 2, + stage: 0, }, }, {