Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/open chart as map [DHIS2-5987] #213

Merged
merged 37 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9c09c3b
Api module for interacting with user data store
neeilya Feb 6, 2019
27019e0
Api methods for saving/retrieving analytical object in/from user data…
neeilya Feb 6, 2019
58cfe61
MapMenu component
neeilya Feb 6, 2019
4958517
Remove map dropdown menu, use chart type selection section instead
neeilya Feb 7, 2019
81d3b16
Globe icon asset
neeilya Feb 7, 2019
6b550f1
Define "Open as map" visualization type
neeilya Feb 7, 2019
fcf08bb
Move managing-AO-in-user-data-store requests to api/userDataStore.js
neeilya Feb 7, 2019
589d001
Move visualization type menu item into dedicated component
neeilya Feb 7, 2019
0aeec59
Enable support for "Open as: Geo Map" visualization type
neeilya Feb 7, 2019
c5f7847
Prevent url from changing on "update" button click if path = "/curren…
neeilya Feb 11, 2019
60af8b9
Remove id, name & displayName attributes from AO before putting it in…
neeilya Feb 11, 2019
ef91042
Enable support for retrieving and applying chart config from user dat…
neeilya Feb 11, 2019
aa86d6d
Correct name for create/update method in user data store api
neeilya Feb 11, 2019
5d4f8b5
prettier
neeilya Feb 11, 2019
0a33f5c
Move api related "prepareCurrentAnalyticalObject" function into api/u…
neeilya Feb 11, 2019
2f10200
Tests
neeilya Feb 12, 2019
8af839b
Tests
neeilya Feb 12, 2019
81c2a17
Tests
neeilya Feb 13, 2019
3491344
Merge branch 'master' into feature/open-chart-as-map
neeilya Feb 13, 2019
99c45b3
Append dimension items names to currentAnalyticalObject
neeilya Feb 18, 2019
c82bc6a
Set column as default chart type
neeilya Feb 19, 2019
d2c44e5
Get parent graph map from visualization function
neeilya Feb 19, 2019
ab71050
Open as map confirmation dialog component
neeilya Feb 19, 2019
c196e27
Show confirmation dialog if user opens chart as map and chart has mul…
neeilya Feb 19, 2019
6ca131d
Generate parent graph map from current analytical object
neeilya Feb 19, 2019
12c0a48
Remove open as map confirmation dialog
neeilya Feb 20, 2019
b84a907
Rename "Open as: Geo Map" chart type to "Open as: Map"
neeilya Feb 20, 2019
f7d3a49
Append names only for dimensions which have name property in metadata
neeilya Feb 20, 2019
93f5e05
Move current analytical object specifc functions to separate module
neeilya Feb 20, 2019
ba35ec8
Tests
neeilya Feb 20, 2019
35b6572
Prettier
neeilya Feb 20, 2019
38a28f2
Handle edge case when no org units are selected
neeilya Feb 22, 2019
e8b3f2a
Move styles to dedicated styles file
neeilya Feb 22, 2019
8ad9cd2
Prettier
neeilya Feb 22, 2019
7ccffce
Use procedural style in prepare current analytical object function
neeilya Feb 25, 2019
3f71e0d
Fix appendDimensionItemNamesToAnalyticalObject function, include item…
neeilya Feb 27, 2019
dd4c65b
Merge branch 'master' into feature/open-chart-as-map
neeilya Feb 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions packages/app/src/api/__tests__/userDataStore.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as d2lib from 'd2';
import * as userDataStore from '../userDataStore';
import {
apiSave,
apiFetch,
hasNamespace,
getNamespace,
apiSaveAOInUserDataStore,
apiFetchAOFromUserDataStore,
NAMESPACE,
CURRENT_AO_KEY,
} from '../userDataStore';

let mockD2;
let mockNamespace;

describe('api: user data store', () => {
beforeEach(() => {
mockNamespace = {
get: jest.fn(),
set: jest.fn(),
};
mockD2 = {
currentUser: {
dataStore: {
has: jest.fn().mockResolvedValue(false), // false default value for test purposes
get: jest.fn().mockResolvedValue(mockNamespace),
create: jest.fn().mockResolvedValue(mockNamespace),
},
},
};
d2lib.getInstance = () => Promise.resolve(mockD2);
});

describe('hasNamespace', () => {
it('uses result of "has" method of d2.currentUser.dataStore object', async () => {
const result = await hasNamespace(mockD2);

expect(mockD2.currentUser.dataStore.has).toBeCalledTimes(1);
expect(mockD2.currentUser.dataStore.has).toBeCalledWith(NAMESPACE);
expect(result).toEqual(false);
});
});

describe('getNamespace', () => {
it('retrieves and returns namespace if it exists', async () => {
const result = await getNamespace(mockD2, true);

expect(mockD2.currentUser.dataStore.get).toBeCalledTimes(1);
expect(mockD2.currentUser.dataStore.create).toBeCalledTimes(0);
expect(result).toMatchObject(mockNamespace);
});

it('creates and returns namespace if it doesnt exist', async () => {
const result = await getNamespace(mockD2, false);

expect(mockD2.currentUser.dataStore.get).toBeCalledTimes(0);
expect(mockD2.currentUser.dataStore.create).toBeCalledTimes(1);
expect(result).toMatchObject(mockNamespace);
});
});

describe('apiSave', () => {
it('uses d2 namespace.set for saving data under given key', async () => {
const data = {};
const key = 'someKey';

await apiSave(data, key, mockNamespace);

expect(mockNamespace.set).toBeCalledTimes(1);
expect(mockNamespace.set).toBeCalledWith(key, data);
});
});

describe('apiFetch', () => {
it('uses d2 namespace.get for retrieving data by given key', async () => {
const key = 'someKey';

await apiFetch(key, mockNamespace);

expect(mockNamespace.get).toBeCalledTimes(1);
expect(mockNamespace.get).toBeCalledWith(key);
});
});

describe('apiSaveAoInUserDataStore', () => {
beforeEach(() => {
userDataStore.getNamespace = () => Promise.resolve(mockNamespace);
});

it('uses default key unless specified', async () => {
const data = {};

await apiSaveAOInUserDataStore(data);

expect(mockNamespace.set).toBeCalledTimes(1);
expect(mockNamespace.set).toBeCalledWith(CURRENT_AO_KEY, data);
});
});

describe('apiFetchAOFromUserDataStore', () => {
beforeEach(() => {
userDataStore.getNamespace = () => Promise.resolve(mockNamespace);
});

it('uses default key unless specified', async () => {
await apiFetchAOFromUserDataStore();

expect(mockNamespace.get).toBeCalledTimes(1);
expect(mockNamespace.get).toBeCalledWith(CURRENT_AO_KEY);
});
});
});
43 changes: 43 additions & 0 deletions packages/app/src/api/userDataStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getInstance } from 'd2';
import { onError } from './index';

export const NAMESPACE = 'analytics';
export const CURRENT_AO_KEY = 'currentAnalyticalObject';

export const hasNamespace = async d2 =>
await d2.currentUser.dataStore.has(NAMESPACE);

export const getNamespace = async (d2, hasNamespace) =>
hasNamespace
? await d2.currentUser.dataStore.get(NAMESPACE)
: await d2.currentUser.dataStore.create(NAMESPACE);

export const apiSave = async (data, key, namespace) => {
try {
const d2 = await getInstance();
const ns =
namespace || (await getNamespace(d2, await hasNamespace(d2)));

return ns.set(key, data);
} catch (error) {
return onError(error);
}
};

export const apiFetch = async (key, namespace) => {
try {
const d2 = await getInstance();
const ns =
namespace || (await getNamespace(d2, await hasNamespace(d2)));

return ns.get(key);
} catch (error) {
return onError(error);
}
};

export const apiSaveAOInUserDataStore = (current, key = CURRENT_AO_KEY) =>
apiSave(current, key);

export const apiFetchAOFromUserDataStore = (key = CURRENT_AO_KEY) =>
apiFetch(key);
69 changes: 69 additions & 0 deletions packages/app/src/assets/GlobeIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';

const GlobeIcon = ({
style = { width: 24, height: 24, paddingRight: '8px' },
}) => (
<SvgIcon viewBox="0 0 48 48" style={style}>
<title>icon_chart_GIS</title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="0" width="48" height="48" />
</defs>
<g
id="Symbols"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g id="Icon/48x48/chart_GIS">
<g id="icon_chart_GIS">
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1" />
</mask>
<g id="Bounds" />
<circle
id="Oval-4"
stroke="#1976D2"
strokeWidth="2"
mask="url(#mask-2)"
cx="24"
cy="24"
r="23"
/>
<polyline
id="Path-6"
stroke="#1976D2"
strokeWidth="2"
mask="url(#mask-2)"
points="1 21 4 24 8 26 9 24 6 19 11 18 18 12 14 9 16 6 15 3"
/>
<polyline
id="Path-7"
stroke="#1976D2"
strokeWidth="2"
mask="url(#mask-2)"
points="47 25 45 21 43 19 40 18 37 18 34 17 32 18 30 23 33 27 37 27 38 30 38 38 38.5 42"
/>
<polyline
id="Path-5"
stroke="#1976D2"
strokeWidth="2"
mask="url(#mask-2)"
points="38 6 37 7 34 6 32 8 34 10 33 12 33 15 37 14 39 15 43 12"
/>
<polyline
id="Path-8"
stroke="#1976D2"
strokeWidth="2"
mask="url(#mask-2)"
points="18 46 16 41 15 36 13 34 10 31 11 28 14 26 18 27 20 29 23 30 25 32 25 36 23 40 21 47"
/>
</g>
</g>
</g>
</SvgIcon>
);

export default GlobeIcon;
35 changes: 32 additions & 3 deletions packages/app/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import * as fromActions from '../actions';
import history from '../modules/history';
import defaultMetadata from '../modules/metadata';
import { sGetUi } from '../reducers/ui';
import {
apiFetchAOFromUserDataStore,
CURRENT_AO_KEY,
} from '../api/userDataStore';

import '@dhis2/ui/defaults/reset.css';

import './App.css';
import './scrollbar.css';
import { getParentGraphMapFromVisualization } from '../modules/ui';

export class App extends Component {
unlisten = null;
Expand Down Expand Up @@ -60,13 +65,27 @@ export class App extends Component {
const { store } = this.context;

if (location.pathname.length > 1) {
// /currentAnalyticalObject
// /${id}/
// /${id}/interpretation/${interpretationId}
const pathParts = location.pathname.slice(1).split('/');
const id = pathParts[0];
const interpretationId = pathParts[2];
const urlContainsCurrentAOKey = id === CURRENT_AO_KEY;

if (this.refetch(location)) {
if (urlContainsCurrentAOKey) {
const AO = await apiFetchAOFromUserDataStore();

this.props.addParentGraphMap(
getParentGraphMapFromVisualization(AO)
);

this.props.setVisualization(AO);
this.props.setUiFromVisualization(AO);
this.props.setCurrentFromUi(this.props.ui);
}

if (!urlContainsCurrentAOKey && this.refetch(location)) {
await store.dispatch(
fromActions.tDoLoadVisualization(
this.props.apiObjectName,
Expand Down Expand Up @@ -106,6 +125,7 @@ export class App extends Component {
);

this.loadVisualization(this.props.location);

this.unlisten = history.listen(location => {
this.loadVisualization(location);
});
Expand All @@ -115,7 +135,7 @@ export class App extends Component {
e =>
e.key === 'Enter' &&
e.ctrlKey === true &&
this.props.onKeyUp(this.props.ui)
this.props.setCurrentFromUi(this.props.ui)
);
};

Expand Down Expand Up @@ -197,7 +217,16 @@ const mapStateToProps = state => {
};

const mapDispatchToProps = dispatch => ({
onKeyUp: ui => dispatch(fromActions.fromCurrent.acSetCurrentFromUi(ui)),
setCurrentFromUi: ui =>
dispatch(fromActions.fromCurrent.acSetCurrentFromUi(ui)),
setVisualization: visualization =>
dispatch(
fromActions.fromVisualization.acSetVisualization(visualization)
),
setUiFromVisualization: visualization =>
dispatch(fromActions.fromUi.acSetUiFromVisualization(visualization)),
addParentGraphMap: parentGraphMap =>
dispatch(fromActions.fromUi.acAddParentGraphMap(parentGraphMap)),
});

App.contextTypes = {
Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/components/UpdateButton/UpdateButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { sGetCurrent } from '../../reducers/current';
import * as fromActions from '../../actions';
import history from '../../modules/history';
import styles from './styles/UpdateButton.style';
import { CURRENT_AO_KEY } from '../../api/userDataStore';

const UpdateButton = ({
classes,
Expand All @@ -25,10 +26,16 @@ const UpdateButton = ({
clearLoadError();
onUpdate(ui);

const urlContainsCurrentAOKey =
history.location.pathname === '/' + CURRENT_AO_KEY;

const pathWithoutInterpretation =
current && current.id ? `/${current.id}` : '/';

if (history.location.pathname !== pathWithoutInterpretation) {
if (
!urlContainsCurrentAOKey &&
history.location.pathname !== pathWithoutInterpretation
) {
history.push(pathWithoutInterpretation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import AreaIcon from '../../assets/AreaIcon';
import RadarIcon from '../../assets/RadarIcon';
import YearOverYearLineIcon from '../../assets/YearOverYearLineIcon';
import YearOverYearColumnIcon from '../../assets/YearOverYearColumnIcon';
import GlobeIcon from '../../assets/GlobeIcon';

import {
COLUMN,
STACKED_COLUMN,
Expand All @@ -24,6 +26,7 @@ import {
GAUGE,
YEAR_OVER_YEAR_LINE,
YEAR_OVER_YEAR_COLUMN,
OPEN_AS_MAP,
chartTypeDisplayNames,
} from '../../modules/chartTypes';

Expand All @@ -49,6 +52,8 @@ const VisualizationTypeIcon = ({ type = COLUMN, style }) => {
return <YearOverYearLineIcon style={style} />;
case YEAR_OVER_YEAR_COLUMN:
return <YearOverYearColumnIcon style={style} />;
case OPEN_AS_MAP:
return <GlobeIcon style={style} />;
case COLUMN:
default:
return <ColumnIcon style={style} />;
Expand Down
Loading