From 500628d70d1ceceede4209334ad71ad9e92d31d9 Mon Sep 17 00:00:00 2001 From: Hendrik de Graaf Date: Tue, 6 Jul 2021 13:59:12 +0200 Subject: [PATCH] feat: add status tag and workflow-context (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add status tag and workflow-context * chore: fix broken import * feat: add title bar * refactor: pass data more granularly to title bar Co-authored-by: Médi-Rémi Hashim Co-authored-by: Médi-Rémi Hashim <4295266+mediremi@users.noreply.github.com> --- i18n/en.pot | 25 +++++++- src/app-data/app-data-context.js | 5 ++ .../app-data-provider.js} | 19 +++--- .../app-data-provider.test.js} | 12 ++-- src/app-data/index.js | 3 + src/app-data/use-app-data.js | 4 ++ .../use-app-data.test.js} | 12 ++-- src/app/app.js | 22 ++++--- src/auth/auth-wall.js | 15 ++--- src/auth/auth-wall.test.js | 3 +- src/auth/use-is-authorized.js | 4 +- src/auth/use-is-authorized.test.js | 14 ++--- src/bottom-bar/bottom-bar.js | 12 +++- src/current-user/current-user-context.js | 5 -- src/current-user/index.js | 3 - src/current-user/use-current-user.js | 4 -- src/data-workspace/data-workspace.js | 12 ++-- src/data-workspace/title-bar.js | 27 ++++++++ src/data-workspace/title-bar.module.css | 20 ++++++ src/navigation/index.js | 4 +- src/navigation/use-query-params.js | 18 ------ src/shared/error-message.js | 19 ++++++ .../error-message.module.css} | 1 + src/shared/index.js | 3 + src/shared/loader.js | 10 +++ src/shared/status-tag/icons.js | 42 +++++++++++++ src/shared/status-tag/index.js | 1 + src/shared/status-tag/status-tag.js | 29 +++++++++ src/shared/status-tag/use-approval-state.js | 55 ++++++++++++++++ .../clear-all-button/clear-all-button.js | 2 +- .../clear-all-button/clear-all-button.test.js | 4 +- .../org-unit-select/org-unit-select.js | 6 +- .../org-unit-select/org-unit-select.test.js | 12 ++-- src/top-bar/period-select/period-menu.js | 2 +- src/top-bar/period-select/period-menu.test.js | 4 +- src/top-bar/period-select/period-select.js | 2 +- .../period-select/period-select.test.js | 12 ++-- .../{selection => selection-context}/index.js | 0 .../initial-values.js | 0 .../initial-values.test.js | 0 .../selection-context.js | 0 .../selection-provider.js | 4 +- .../use-selection-context.js | 0 .../use-selection-context.test.js | 8 +-- src/top-bar/top-bar.js | 2 +- .../workflow-select/workflow-select.js | 6 +- .../workflow-select/workflow-select.test.js | 26 ++++---- src/workflow-context/index.js | 3 + src/workflow-context/use-selected-workflow.js | 13 ++++ src/workflow-context/use-selection-params.js | 33 ++++++++++ src/workflow-context/use-workflow-context.js | 4 ++ src/workflow-context/workflow-context.js | 9 +++ src/workflow-context/workflow-provider.js | 63 +++++++++++++++++++ 53 files changed, 482 insertions(+), 136 deletions(-) create mode 100644 src/app-data/app-data-context.js rename src/{current-user/current-user-provider.js => app-data/app-data-provider.js} (74%) rename src/{current-user/current-user-provider.test.js => app-data/app-data-provider.test.js} (77%) create mode 100644 src/app-data/index.js create mode 100644 src/app-data/use-app-data.js rename src/{current-user/use-current-user.test.js => app-data/use-app-data.test.js} (51%) delete mode 100644 src/current-user/current-user-context.js delete mode 100644 src/current-user/index.js delete mode 100644 src/current-user/use-current-user.js create mode 100644 src/data-workspace/title-bar.js create mode 100644 src/data-workspace/title-bar.module.css delete mode 100644 src/navigation/use-query-params.js create mode 100644 src/shared/error-message.js rename src/{auth/auth-wall.module.css => shared/error-message.module.css} (77%) create mode 100644 src/shared/index.js create mode 100644 src/shared/loader.js create mode 100644 src/shared/status-tag/icons.js create mode 100644 src/shared/status-tag/index.js create mode 100644 src/shared/status-tag/status-tag.js create mode 100644 src/shared/status-tag/use-approval-state.js rename src/top-bar/{selection => selection-context}/index.js (100%) rename src/top-bar/{selection => selection-context}/initial-values.js (100%) rename src/top-bar/{selection => selection-context}/initial-values.test.js (100%) rename src/top-bar/{selection => selection-context}/selection-context.js (100%) rename src/top-bar/{selection => selection-context}/selection-provider.js (97%) rename src/top-bar/{selection => selection-context}/use-selection-context.js (100%) rename src/top-bar/{selection => selection-context}/use-selection-context.test.js (97%) create mode 100644 src/workflow-context/index.js create mode 100644 src/workflow-context/use-selected-workflow.js create mode 100644 src/workflow-context/use-selection-params.js create mode 100644 src/workflow-context/use-workflow-context.js create mode 100644 src/workflow-context/workflow-context.js create mode 100644 src/workflow-context/workflow-provider.js diff --git a/i18n/en.pot b/i18n/en.pot index 855eea07..c562d5bf 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2021-06-27T19:54:18.211Z\n" -"PO-Revision-Date: 2021-06-27T19:54:18.211Z\n" +"POT-Creation-Date: 2021-07-06T11:47:50.144Z\n" +"PO-Revision-Date: 2021-07-06T11:47:50.144Z\n" msgid "Not authorized" msgstr "Not authorized" @@ -18,6 +18,24 @@ msgstr "" "You don't have access to the Data Approval App. Contact a system " "administrator to request access." +msgid "{{dataSetsCount}} data sets" +msgstr "{{dataSetsCount}} data sets" + +msgid "Approved" +msgstr "Approved" + +msgid "Ready for approval and accepted" +msgstr "Ready for approval and accepted" + +msgid "Ready for approval" +msgstr "Ready for approval" + +msgid "Waiting" +msgstr "Waiting" + +msgid "Cannot approve" +msgstr "Cannot approve" + msgid "Clear selections" msgstr "Clear selections" @@ -143,3 +161,6 @@ msgstr "Workflow" msgid "No workflows found. None may exist, or you may not have access to any." msgstr "No workflows found. None may exist, or you may not have access to any." + +msgid "Could not load approval data" +msgstr "Could not load approval data" diff --git a/src/app-data/app-data-context.js b/src/app-data/app-data-context.js new file mode 100644 index 00000000..689110ff --- /dev/null +++ b/src/app-data/app-data-context.js @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +const AppDataContext = createContext({}) + +export { AppDataContext } diff --git a/src/current-user/current-user-provider.js b/src/app-data/app-data-provider.js similarity index 74% rename from src/current-user/current-user-provider.js rename to src/app-data/app-data-provider.js index ed40c8ca..5af7a95d 100644 --- a/src/current-user/current-user-provider.js +++ b/src/app-data/app-data-provider.js @@ -1,8 +1,9 @@ import { useDataQuery } from '@dhis2/app-runtime' import { PropTypes } from '@dhis2/prop-types' -import { CircularLoader, Layer, CenteredContent } from '@dhis2/ui' +import { Layer } from '@dhis2/ui' import React from 'react' -import { CurrentUserContext } from './current-user-context.js' +import { Loader } from '../shared/index.js' +import { AppDataContext } from './app-data-context.js' const query = { me: { @@ -28,15 +29,13 @@ const query = { }, } -const CurrentUserProvider = ({ children }) => { +const AppDataProvider = ({ children }) => { const { data, loading, error } = useDataQuery(query) if (loading) { return ( - - - + ) } @@ -56,14 +55,14 @@ const CurrentUserProvider = ({ children }) => { } return ( - + {children} - + ) } -CurrentUserProvider.propTypes = { +AppDataProvider.propTypes = { children: PropTypes.node.isRequired, } -export { CurrentUserProvider } +export { AppDataProvider } diff --git a/src/current-user/current-user-provider.test.js b/src/app-data/app-data-provider.test.js similarity index 77% rename from src/current-user/current-user-provider.test.js rename to src/app-data/app-data-provider.test.js index d3ef006f..96a19855 100644 --- a/src/current-user/current-user-provider.test.js +++ b/src/app-data/app-data-provider.test.js @@ -2,7 +2,7 @@ import { useDataQuery } from '@dhis2/app-runtime' import { shallow, mount } from 'enzyme' import React from 'react' import { expectRenderError } from '../test-utils/expect-render-error.js' -import { CurrentUserProvider } from './current-user-provider.js' +import { AppDataProvider } from './app-data-provider.js' jest.mock('@dhis2/app-runtime', () => ({ useDataQuery: jest.fn(), @@ -12,11 +12,11 @@ afterEach(() => { jest.resetAllMocks() }) -describe('', () => { +describe('', () => { it('shows a spinner when loading', () => { useDataQuery.mockImplementation(() => ({ loading: true })) - const wrapper = mount(Child) + const wrapper = mount(Child) const loadingIndicator = wrapper.find({ 'data-test': 'dhis2-uicore-circularloader', }) @@ -34,7 +34,7 @@ describe('', () => { error, })) - expectRenderError(, message) + expectRenderError(, message) }) it('renders the children once data has been received', () => { @@ -46,9 +46,7 @@ describe('', () => { }, })) - const wrapper = shallow( - Child - ) + const wrapper = shallow(Child) expect(wrapper.text()).toEqual(expect.stringContaining('Child')) }) diff --git a/src/app-data/index.js b/src/app-data/index.js new file mode 100644 index 00000000..5b4b3f05 --- /dev/null +++ b/src/app-data/index.js @@ -0,0 +1,3 @@ +export { AppDataContext } from './app-data-context.js' +export { AppDataProvider } from './app-data-provider.js' +export { useAppData } from './use-app-data.js' diff --git a/src/app-data/use-app-data.js b/src/app-data/use-app-data.js new file mode 100644 index 00000000..f5e7314b --- /dev/null +++ b/src/app-data/use-app-data.js @@ -0,0 +1,4 @@ +import { useContext } from 'react' +import { AppDataContext } from './app-data-context.js' + +export const useAppData = () => useContext(AppDataContext) diff --git a/src/current-user/use-current-user.test.js b/src/app-data/use-app-data.test.js similarity index 51% rename from src/current-user/use-current-user.test.js rename to src/app-data/use-app-data.test.js index 5b63b608..8016618b 100644 --- a/src/current-user/use-current-user.test.js +++ b/src/app-data/use-app-data.test.js @@ -1,21 +1,21 @@ import { renderHook } from '@testing-library/react-hooks' import React from 'react' -import { CurrentUserContext } from './current-user-context.js' -import { useCurrentUser } from './use-current-user.js' +import { AppDataContext } from './app-data-context.js' +import { useAppData } from './use-app-data.js' -describe('useCurrentUser', () => { +describe('useAppData', () => { const value = { authorities: ['dummy'], } const wrapper = ({ children }) => ( - + {children} - + ) it('returns an object with current user properties', () => { - const { result } = renderHook(() => useCurrentUser(), { wrapper }) + const { result } = renderHook(() => useAppData(), { wrapper }) expect(result.current).toEqual(value) }) diff --git a/src/app/app.js b/src/app/app.js index 6bdccd30..68eee1cc 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -1,30 +1,32 @@ import { CssVariables } from '@dhis2/ui' import React from 'react' +import { AppDataProvider } from '../app-data/index.js' import { AuthWall } from '../auth/index.js' import { BottomBar } from '../bottom-bar/index.js' -import { CurrentUserProvider } from '../current-user/index.js' import { DataWorkspace } from '../data-workspace/index.js' import { TopBar } from '../top-bar/index.js' +import { WorkflowProvider } from '../workflow-context/index.js' import { Layout } from './layout.js' - const App = () => ( <> - + - - - - - - + + + + + + + + - + ) diff --git a/src/auth/auth-wall.js b/src/auth/auth-wall.js index 1e30ac58..5c33d6da 100644 --- a/src/auth/auth-wall.js +++ b/src/auth/auth-wall.js @@ -1,8 +1,7 @@ import i18n from '@dhis2/d2-i18n' -import { NoticeBox } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' -import classes from './auth-wall.module.css' +import { ErrorMessage } from '../shared/index.js' import { useIsAuthorized } from './use-is-authorized.js' const AuthWall = ({ children }) => { @@ -10,13 +9,11 @@ const AuthWall = ({ children }) => { if (!isAuthorized) { return ( -
- - {i18n.t( - "You don't have access to the Data Approval App. Contact a system administrator to request access." - )} - -
+ + {i18n.t( + "You don't have access to the Data Approval App. Contact a system administrator to request access." + )} + ) } diff --git a/src/auth/auth-wall.test.js b/src/auth/auth-wall.test.js index 53f41c28..d0c90c11 100644 --- a/src/auth/auth-wall.test.js +++ b/src/auth/auth-wall.test.js @@ -1,5 +1,6 @@ import { shallow } from 'enzyme' import React from 'react' +import { ErrorMessage } from '../shared/index.js' import { AuthWall } from './auth-wall.js' import { useIsAuthorized } from './use-is-authorized.js' @@ -17,7 +18,7 @@ describe('', () => { const wrapper = shallow(Child) - expect(wrapper.find('NoticeBox')).toHaveLength(1) + expect(wrapper.find(ErrorMessage)).toHaveLength(1) }) it('renders the children for authorised users', () => { diff --git a/src/auth/use-is-authorized.js b/src/auth/use-is-authorized.js index f1199396..a8bf4523 100644 --- a/src/auth/use-is-authorized.js +++ b/src/auth/use-is-authorized.js @@ -1,7 +1,7 @@ -import { useCurrentUser } from '../current-user/index.js' +import { useAppData } from '../app-data/index.js' export const useIsAuthorized = () => { - const { authorities } = useCurrentUser() + const { authorities } = useAppData() return authorities.some( authority => authority === 'ALL' || authority === 'M_dhis-web-approval' ) diff --git a/src/auth/use-is-authorized.test.js b/src/auth/use-is-authorized.test.js index 29f95a08..f589c6ff 100644 --- a/src/auth/use-is-authorized.test.js +++ b/src/auth/use-is-authorized.test.js @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks' import React from 'react' -import { CurrentUserContext } from '../current-user/index.js' +import { AppDataContext } from '../app-data/index.js' import { useIsAuthorized } from './use-is-authorized.js' describe('useIsAuthorized', () => { @@ -10,9 +10,9 @@ describe('useIsAuthorized', () => { } const wrapper = ({ children }) => ( - + {children} - + ) const { result } = renderHook(() => useIsAuthorized(), { wrapper }) @@ -26,9 +26,9 @@ describe('useIsAuthorized', () => { } const wrapper = ({ children }) => ( - + {children} - + ) const { result } = renderHook(() => useIsAuthorized(), { wrapper }) @@ -42,9 +42,9 @@ describe('useIsAuthorized', () => { } const wrapper = ({ children }) => ( - + {children} - + ) const { result } = renderHook(() => useIsAuthorized(), { wrapper }) diff --git a/src/bottom-bar/bottom-bar.js b/src/bottom-bar/bottom-bar.js index 1dc08a30..20119076 100644 --- a/src/bottom-bar/bottom-bar.js +++ b/src/bottom-bar/bottom-bar.js @@ -1,5 +1,15 @@ import React from 'react' +import { StatusTag } from '../shared/index.js' +import { useWorkflowContext } from '../workflow-context/index.js' -const BottomBar = () =>
BottomBar placeholder
+const BottomBar = () => { + const { approvalStatus } = useWorkflowContext() + + return ( + <> + + + ) +} export { BottomBar } diff --git a/src/current-user/current-user-context.js b/src/current-user/current-user-context.js deleted file mode 100644 index 36518bd5..00000000 --- a/src/current-user/current-user-context.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from 'react' - -const CurrentUserContext = createContext({}) - -export { CurrentUserContext } diff --git a/src/current-user/index.js b/src/current-user/index.js deleted file mode 100644 index f95b2bcb..00000000 --- a/src/current-user/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { CurrentUserContext } from './current-user-context.js' -export { CurrentUserProvider } from './current-user-provider.js' -export { useCurrentUser } from './use-current-user.js' diff --git a/src/current-user/use-current-user.js b/src/current-user/use-current-user.js deleted file mode 100644 index 233124c0..00000000 --- a/src/current-user/use-current-user.js +++ /dev/null @@ -1,4 +0,0 @@ -import { useContext } from 'react' -import { CurrentUserContext } from './current-user-context.js' - -export const useCurrentUser = () => useContext(CurrentUserContext) diff --git a/src/data-workspace/data-workspace.js b/src/data-workspace/data-workspace.js index 1d5939f2..6a8f4452 100644 --- a/src/data-workspace/data-workspace.js +++ b/src/data-workspace/data-workspace.js @@ -1,13 +1,17 @@ import React from 'react' -import { useQueryParams } from '../navigation/index.js' +import { useWorkflowContext } from '../workflow-context/index.js' +import { TitleBar } from './title-bar.js' const DataWorkspace = () => { - const query = useQueryParams() + const workflow = useWorkflowContext() return ( <> -

Data workspace placeholder

-
{JSON.stringify(query, null, 4)}
+ ) } diff --git a/src/data-workspace/title-bar.js b/src/data-workspace/title-bar.js new file mode 100644 index 00000000..05970ee7 --- /dev/null +++ b/src/data-workspace/title-bar.js @@ -0,0 +1,27 @@ +import i18n from '@dhis2/d2-i18n' +import { IconDimensionDataSet16 } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import { StatusTag } from '../shared/status-tag/index.js' +import styles from './title-bar.module.css' + +const TitleBar = ({ name, dataSetsCount, approvalState }) => ( +
+ {name} + + + {i18n.t('{{dataSetsCount}} data sets', { + dataSetsCount, + })} + + +
+) + +TitleBar.propTypes = { + approvalState: PropTypes.string.isRequired, + dataSetsCount: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, +} + +export { TitleBar } diff --git a/src/data-workspace/title-bar.module.css b/src/data-workspace/title-bar.module.css new file mode 100644 index 00000000..efccea81 --- /dev/null +++ b/src/data-workspace/title-bar.module.css @@ -0,0 +1,20 @@ +.titleBar { + display: grid; + grid-template-columns: max-content max-content max-content; + align-items: center; + grid-gap: var(--spacers-dp12); +} + +.workflowName { + font-weight: 500; + font-size: 20px; + line-height: 24px; +} + +.workflowDataSetsCount { + display: grid; + grid-template-columns: auto auto; + align-items: center; + grid-gap: var(--spacers-dp4); + color: var(--colors-grey600); +} diff --git a/src/navigation/index.js b/src/navigation/index.js index d4750a47..bc455916 100644 --- a/src/navigation/index.js +++ b/src/navigation/index.js @@ -1,4 +1,4 @@ +export { createHref } from './create-href.js' +export { history } from './history.js' export { pushStateToHistory } from './push-state-to-history.js' export { readQueryParams } from './read-query-params.js' -export { createHref } from './create-href.js' -export { useQueryParams } from './use-query-params.js' diff --git a/src/navigation/use-query-params.js b/src/navigation/use-query-params.js deleted file mode 100644 index a7325893..00000000 --- a/src/navigation/use-query-params.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useState, useEffect } from 'react' -import { history } from './history.js' -import { readQueryParams } from './read-query-params.js' - -export const useQueryParams = () => { - const [params, setParams] = useState(readQueryParams) - - useEffect( - () => - // The call to listen returns an `unlisten` function to clean up the effect - history.listen(() => { - setParams(readQueryParams()) - }), - [] - ) - - return params -} diff --git a/src/shared/error-message.js b/src/shared/error-message.js new file mode 100644 index 00000000..64fc279a --- /dev/null +++ b/src/shared/error-message.js @@ -0,0 +1,19 @@ +import { NoticeBox } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import classes from './error-message.module.css' + +const ErrorMessage = ({ children, title }) => ( +
+ + {children} + +
+) + +ErrorMessage.propTypes = { + children: PropTypes.node.isRequired, + title: PropTypes.string.isRequired, +} + +export { ErrorMessage } diff --git a/src/auth/auth-wall.module.css b/src/shared/error-message.module.css similarity index 77% rename from src/auth/auth-wall.module.css rename to src/shared/error-message.module.css index 5ed0a48d..4a92cc0e 100644 --- a/src/auth/auth-wall.module.css +++ b/src/shared/error-message.module.css @@ -1,4 +1,5 @@ .wrapper { + min-width: 400px; max-width: 800px; margin: var(--spacers-dp24) auto 0; } diff --git a/src/shared/index.js b/src/shared/index.js new file mode 100644 index 00000000..bb6abf71 --- /dev/null +++ b/src/shared/index.js @@ -0,0 +1,3 @@ +export { ErrorMessage } from './error-message.js' +export { Loader } from './loader.js' +export { StatusTag } from './status-tag/index.js' diff --git a/src/shared/loader.js b/src/shared/loader.js new file mode 100644 index 00000000..b4603927 --- /dev/null +++ b/src/shared/loader.js @@ -0,0 +1,10 @@ +import { CenteredContent, CircularLoader } from '@dhis2/ui' +import React from 'react' + +const Loader = () => ( + + + +) + +export { Loader } diff --git a/src/shared/status-tag/icons.js b/src/shared/status-tag/icons.js new file mode 100644 index 00000000..d02c945e --- /dev/null +++ b/src/shared/status-tag/icons.js @@ -0,0 +1,42 @@ +import React from 'react' + +const Approved = () => ( + + + +) + +const Ready = () => ( + + + +) + +const Waiting = () => ( + + + +) + +export { Approved, Ready, Waiting } diff --git a/src/shared/status-tag/index.js b/src/shared/status-tag/index.js new file mode 100644 index 00000000..eac87ebf --- /dev/null +++ b/src/shared/status-tag/index.js @@ -0,0 +1 @@ +export { StatusTag } from './status-tag.js' diff --git a/src/shared/status-tag/status-tag.js b/src/shared/status-tag/status-tag.js new file mode 100644 index 00000000..63d63f7b --- /dev/null +++ b/src/shared/status-tag/status-tag.js @@ -0,0 +1,29 @@ +import { Tag } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React from 'react' +import { useApprovalState } from './use-approval-state.js' + +const StatusTag = ({ approvalState }) => { + const { icon: Icon, displayName, type } = useApprovalState(approvalState) + const props = { + [type]: true, + icon: , + } + + return {displayName} +} + +StatusTag.propTypes = { + approvalState: PropTypes.oneOf([ + 'APPROVED_HERE', + 'APPROVED_ELSEWHERE', + 'ACCEPTED_HERE', + 'ACCEPTED_ELSEWHERE', + 'UNAPPROVED_READY', + 'UNAPPROVED_WAITING', + 'UNAPPROVED_ELSEWHERE', + 'UNAPPROVABLE', + ]), +} + +export { StatusTag } diff --git a/src/shared/status-tag/use-approval-state.js b/src/shared/status-tag/use-approval-state.js new file mode 100644 index 00000000..a13ea816 --- /dev/null +++ b/src/shared/status-tag/use-approval-state.js @@ -0,0 +1,55 @@ +import i18n from '@dhis2/d2-i18n' +import { Approved, Ready, Waiting } from './icons.js' + +/* + * TODO: The current classification logic was discussed with + * Joe Cooper, but needs to be confirmed by either Lars or Jim. + * Specifically these cases are not clear: + * A. Do ACCEPTED_HERE and ACCEPTED_ELSEWHERE fall into "Ready for + * approval and accepted"? This doesn't seem to match the webapi docs. + * B. Should we show a red tag for UNAPPROVABLE and show a negative tag? + * This was not included in the design specs. + */ +const useApprovalState = approvalState => { + switch (approvalState) { + case 'APPROVED_HERE': + case 'APPROVED_ELSEWHERE': + return { + icon: Approved, + displayName: i18n.t('Approved'), + type: 'positive', + } + + case 'ACCEPTED_HERE': + case 'ACCEPTED_ELSEWHERE': + return { + icon: Ready, + displayName: i18n.t('Ready for approval and accepted'), + type: 'neutral', + } + + case 'UNAPPROVED_READY': + return { + icon: Ready, + displayName: i18n.t('Ready for approval'), + type: 'neutral', + } + + case 'UNAPPROVED_WAITING': + case 'UNAPPROVED_ELSEWHERE': + return { + icon: Waiting, + displayName: i18n.t('Waiting'), + type: 'default', + } + + case 'UNAPPROVABLE': + return { + icon: Waiting, + displayName: i18n.t('Cannot approve'), + type: 'negative', + } + } +} + +export { useApprovalState } diff --git a/src/top-bar/clear-all-button/clear-all-button.js b/src/top-bar/clear-all-button/clear-all-button.js index 3bc7b089..177f1741 100644 --- a/src/top-bar/clear-all-button/clear-all-button.js +++ b/src/top-bar/clear-all-button/clear-all-button.js @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import { Button } from '@dhis2/ui' import React from 'react' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import classes from './clear-all-button.module.css' const ClearAllButton = () => { diff --git a/src/top-bar/clear-all-button/clear-all-button.test.js b/src/top-bar/clear-all-button/clear-all-button.test.js index df55c72e..6039162a 100644 --- a/src/top-bar/clear-all-button/clear-all-button.test.js +++ b/src/top-bar/clear-all-button/clear-all-button.test.js @@ -1,9 +1,9 @@ import { shallow } from 'enzyme' import React from 'react' -import { useSelectionContext } from '../selection/use-selection-context.js' +import { useSelectionContext } from '../selection-context/use-selection-context.js' import { ClearAllButton } from './clear-all-button.js' -jest.mock('../selection/use-selection-context.js', () => ({ +jest.mock('../selection-context/use-selection-context.js', () => ({ useSelectionContext: jest.fn(), })) diff --git a/src/top-bar/org-unit-select/org-unit-select.js b/src/top-bar/org-unit-select/org-unit-select.js index 6e1f3fc0..ad814de5 100644 --- a/src/top-bar/org-unit-select/org-unit-select.js +++ b/src/top-bar/org-unit-select/org-unit-select.js @@ -1,15 +1,15 @@ import i18n from '@dhis2/d2-i18n' import { OrganisationUnitTree } from '@dhis2/ui' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { ContextSelect } from '../context-select/index.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import classes from './org-unit-select.module.css' export const ORG_UNIT = 'ORG_UNIT' const OrgUnitSelect = () => { - const { organisationUnits } = useCurrentUser() + const { organisationUnits } = useAppData() const { orgUnit, selectOrgUnit, diff --git a/src/top-bar/org-unit-select/org-unit-select.test.js b/src/top-bar/org-unit-select/org-unit-select.test.js index 038b97b8..60351a0c 100644 --- a/src/top-bar/org-unit-select/org-unit-select.test.js +++ b/src/top-bar/org-unit-select/org-unit-select.test.js @@ -2,10 +2,10 @@ import { useDataQuery } from '@dhis2/app-runtime' import { Popover, Layer, OrganisationUnitTree, Tooltip } from '@dhis2/ui' import { shallow } from 'enzyme' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { readQueryParams } from '../../navigation/read-query-params.js' import { ContextSelect } from '../context-select/context-select.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { ORG_UNIT, OrgUnitSelect } from './org-unit-select.js' jest.mock('@dhis2/app-runtime', () => ({ @@ -15,11 +15,11 @@ jest.mock('../../navigation/read-query-params.js', () => ({ readQueryParams: jest.fn(), })) -jest.mock('../../current-user/index.js', () => ({ - useCurrentUser: jest.fn(), +jest.mock('../../app-data/index.js', () => ({ + useAppData: jest.fn(), })) -jest.mock('../selection/index.js', () => ({ +jest.mock('../selection-context/index.js', () => ({ useSelectionContext: jest.fn(), })) @@ -47,7 +47,7 @@ afterEach(() => { }) beforeEach(() => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, organisationUnits: mockOrgUnitRoots, })) diff --git a/src/top-bar/period-select/period-menu.js b/src/top-bar/period-select/period-menu.js index 8f8a82d6..e27789e7 100644 --- a/src/top-bar/period-select/period-menu.js +++ b/src/top-bar/period-select/period-menu.js @@ -2,7 +2,7 @@ import { Menu, MenuItem } from '@dhis2/ui' import PropTypes from 'prop-types' import React from 'react' import { createHref } from '../../navigation/index.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { getFixedPeriodsByTypeAndYear } from './fixed-periods.js' import classes from './period-menu.module.css' diff --git a/src/top-bar/period-select/period-menu.test.js b/src/top-bar/period-select/period-menu.test.js index 2d406aa4..24ef3389 100644 --- a/src/top-bar/period-select/period-menu.test.js +++ b/src/top-bar/period-select/period-menu.test.js @@ -1,10 +1,10 @@ import { MenuItem, Menu } from '@dhis2/ui' import { shallow } from 'enzyme' import React from 'react' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { PeriodMenu } from './period-menu.js' -jest.mock('../selection/index.js', () => ({ +jest.mock('../selection-context/index.js', () => ({ useSelectionContext: jest.fn(), })) diff --git a/src/top-bar/period-select/period-select.js b/src/top-bar/period-select/period-select.js index 0750d840..9321c03e 100644 --- a/src/top-bar/period-select/period-select.js +++ b/src/top-bar/period-select/period-select.js @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import React, { useEffect, useState } from 'react' import { ContextSelect } from '../context-select/index.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { PeriodMenu } from './period-menu.js' import { YearNavigator, currentYear } from './year-navigator.js' diff --git a/src/top-bar/period-select/period-select.test.js b/src/top-bar/period-select/period-select.test.js index 8cfb7c34..b7e0b69e 100644 --- a/src/top-bar/period-select/period-select.test.js +++ b/src/top-bar/period-select/period-select.test.js @@ -1,10 +1,10 @@ import { Popover, Layer, MenuItem, Tooltip } from '@dhis2/ui' import { shallow } from 'enzyme' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { readQueryParams } from '../../navigation/read-query-params.js' import { ContextSelect } from '../context-select/context-select.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { PeriodMenu } from './period-menu.js' import { PERIOD, PeriodSelect } from './period-select.js' import { YearNavigator } from './year-navigator.js' @@ -13,11 +13,11 @@ jest.mock('../../navigation/read-query-params.js', () => ({ readQueryParams: jest.fn(), })) -jest.mock('../../current-user/index.js', () => ({ - useCurrentUser: jest.fn(), +jest.mock('../../app-data/index.js', () => ({ + useAppData: jest.fn(), })) -jest.mock('../selection/index.js', () => ({ +jest.mock('../selection-context/index.js', () => ({ useSelectionContext: jest.fn(), })) @@ -35,7 +35,7 @@ const mockWorkflows = [ ] beforeEach(() => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) diff --git a/src/top-bar/selection/index.js b/src/top-bar/selection-context/index.js similarity index 100% rename from src/top-bar/selection/index.js rename to src/top-bar/selection-context/index.js diff --git a/src/top-bar/selection/initial-values.js b/src/top-bar/selection-context/initial-values.js similarity index 100% rename from src/top-bar/selection/initial-values.js rename to src/top-bar/selection-context/initial-values.js diff --git a/src/top-bar/selection/initial-values.test.js b/src/top-bar/selection-context/initial-values.test.js similarity index 100% rename from src/top-bar/selection/initial-values.test.js rename to src/top-bar/selection-context/initial-values.test.js diff --git a/src/top-bar/selection/selection-context.js b/src/top-bar/selection-context/selection-context.js similarity index 100% rename from src/top-bar/selection/selection-context.js rename to src/top-bar/selection-context/selection-context.js diff --git a/src/top-bar/selection/selection-provider.js b/src/top-bar/selection-context/selection-provider.js similarity index 97% rename from src/top-bar/selection/selection-provider.js rename to src/top-bar/selection-context/selection-provider.js index 87a91686..12981c0b 100644 --- a/src/top-bar/selection/selection-provider.js +++ b/src/top-bar/selection-context/selection-provider.js @@ -1,6 +1,6 @@ import { PropTypes } from '@dhis2/prop-types' import React, { useEffect, useReducer } from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { pushStateToHistory } from '../../navigation/index.js' import { initialValues, initialWorkflowValue } from './initial-values.js' import { SelectionContext } from './selection-context.js' @@ -63,7 +63,7 @@ const reducer = (state, { type, payload }) => { } const SelectionProvider = ({ children }) => { - const { dataApprovalWorkflows } = useCurrentUser() + const { dataApprovalWorkflows } = useAppData() const [{ openedSelect, workflow, period, orgUnit }, dispatch] = useReducer( reducer, { diff --git a/src/top-bar/selection/use-selection-context.js b/src/top-bar/selection-context/use-selection-context.js similarity index 100% rename from src/top-bar/selection/use-selection-context.js rename to src/top-bar/selection-context/use-selection-context.js diff --git a/src/top-bar/selection/use-selection-context.test.js b/src/top-bar/selection-context/use-selection-context.test.js similarity index 97% rename from src/top-bar/selection/use-selection-context.test.js rename to src/top-bar/selection-context/use-selection-context.test.js index 11f0b4dc..ec7897f6 100644 --- a/src/top-bar/selection/use-selection-context.test.js +++ b/src/top-bar/selection-context/use-selection-context.test.js @@ -1,6 +1,6 @@ import { act, renderHook } from '@testing-library/react-hooks' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { pushStateToHistory } from '../../navigation/push-state-to-history.js' import { readQueryParams } from '../../navigation/read-query-params.js' import { SelectionProvider } from './selection-provider.js' @@ -14,8 +14,8 @@ jest.mock('../../navigation/read-query-params.js', () => ({ readQueryParams: jest.fn(), })) -jest.mock('../../current-user/index.js', () => ({ - useCurrentUser: jest.fn(), +jest.mock('../../app-data/index.js', () => ({ + useAppData: jest.fn(), })) afterEach(() => { @@ -36,7 +36,7 @@ const mockWorkflows = [ ] beforeEach(() => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) }) diff --git a/src/top-bar/top-bar.js b/src/top-bar/top-bar.js index 7c16f784..9bf4d843 100644 --- a/src/top-bar/top-bar.js +++ b/src/top-bar/top-bar.js @@ -2,7 +2,7 @@ import React from 'react' import { ClearAllButton } from './clear-all-button/index.js' import { OrgUnitSelect } from './org-unit-select/index.js' import { PeriodSelect } from './period-select/index.js' -import { SelectionProvider } from './selection/index.js' +import { SelectionProvider } from './selection-context/index.js' import { WorkflowSelect } from './workflow-select/index.js' const TopBar = () => ( diff --git a/src/top-bar/workflow-select/workflow-select.js b/src/top-bar/workflow-select/workflow-select.js index 3aaa4a34..c7beaba7 100644 --- a/src/top-bar/workflow-select/workflow-select.js +++ b/src/top-bar/workflow-select/workflow-select.js @@ -1,16 +1,16 @@ import i18n from '@dhis2/d2-i18n' import { Menu } from '@dhis2/ui' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { ContextSelect } from '../context-select/index.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { WorkflowSelectOption } from './workflow-select-option.js' import classes from './workflow-select.module.css' const WORKFLOW = 'WORKFLOW' const WorkflowSelect = () => { - const { dataApprovalWorkflows } = useCurrentUser() + const { dataApprovalWorkflows } = useAppData() const { workflow: selectedWorkflow, selectWorkflow, diff --git a/src/top-bar/workflow-select/workflow-select.test.js b/src/top-bar/workflow-select/workflow-select.test.js index 026b38bb..c53ce370 100644 --- a/src/top-bar/workflow-select/workflow-select.test.js +++ b/src/top-bar/workflow-select/workflow-select.test.js @@ -1,10 +1,10 @@ import { Popover, Layer } from '@dhis2/ui' import { shallow } from 'enzyme' import React from 'react' -import { useCurrentUser } from '../../current-user/index.js' +import { useAppData } from '../../app-data/index.js' import { readQueryParams } from '../../navigation/read-query-params.js' import { ContextSelect } from '../context-select/context-select.js' -import { useSelectionContext } from '../selection/index.js' +import { useSelectionContext } from '../selection-context/index.js' import { WorkflowSelectOption } from './workflow-select-option.js' import { WORKFLOW, WorkflowSelect } from './workflow-select.js' @@ -12,11 +12,11 @@ jest.mock('../../navigation/read-query-params.js', () => ({ readQueryParams: jest.fn(), })) -jest.mock('../../current-user/index.js', () => ({ - useCurrentUser: jest.fn(), +jest.mock('../../app-data/index.js', () => ({ + useAppData: jest.fn(), })) -jest.mock('../selection/index.js', () => ({ +jest.mock('../selection-context/index.js', () => ({ useSelectionContext: jest.fn(), })) @@ -37,7 +37,7 @@ describe('', () => { ] it('renders a ContextSelect with WorkflowSelectOptions', () => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -54,7 +54,7 @@ describe('', () => { }) it('renders a placeholder text when no workflow is selected', () => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -72,7 +72,7 @@ describe('', () => { }) it('renders a the value when a workflow is selected', () => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -91,7 +91,7 @@ describe('', () => { }) it('opens the ContextSelect when the opened select matches "WORKFLOW"', () => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -107,7 +107,7 @@ describe('', () => { }) it('shows an info message when no workflows have been found', () => { - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: [], })) readQueryParams.mockImplementation(() => ({})) @@ -130,7 +130,7 @@ describe('', () => { it('calls the setOpenedSelect to open when clicking the ContextSelect button', () => { const setOpenedSelect = jest.fn() - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -152,7 +152,7 @@ describe('', () => { it('calls the selectWorkflow when clicking a WorkflowSelectOptions', () => { const selectWorkflow = jest.fn() - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) @@ -174,7 +174,7 @@ describe('', () => { it('calls the setOpenedSelect to close when clicking the backdrop', () => { const setOpenedSelect = jest.fn() - useCurrentUser.mockImplementation(() => ({ + useAppData.mockImplementation(() => ({ dataApprovalWorkflows: mockWorkflows, })) readQueryParams.mockImplementation(() => ({})) diff --git a/src/workflow-context/index.js b/src/workflow-context/index.js new file mode 100644 index 00000000..7c41c62a --- /dev/null +++ b/src/workflow-context/index.js @@ -0,0 +1,3 @@ +export { WorkflowContext } from './workflow-context.js' +export { WorkflowProvider } from './workflow-provider.js' +export { useWorkflowContext } from './use-workflow-context.js' diff --git a/src/workflow-context/use-selected-workflow.js b/src/workflow-context/use-selected-workflow.js new file mode 100644 index 00000000..8e1f6f29 --- /dev/null +++ b/src/workflow-context/use-selected-workflow.js @@ -0,0 +1,13 @@ +import { useAppData } from '../app-data/index.js' + +const useSelectedWorkflow = params => { + const { dataApprovalWorkflows } = useAppData() + + if (!(params && params.wf && dataApprovalWorkflows)) { + return {} + } + + return dataApprovalWorkflows.find(({ id }) => id === params.wf) || {} +} + +export { useSelectedWorkflow } diff --git a/src/workflow-context/use-selection-params.js b/src/workflow-context/use-selection-params.js new file mode 100644 index 00000000..fae432e2 --- /dev/null +++ b/src/workflow-context/use-selection-params.js @@ -0,0 +1,33 @@ +import { useState, useEffect } from 'react' +import { history, readQueryParams } from '../navigation/index.js' + +const getParamsIfAllAvailable = () => { + const { wf, pe, ou: orgUnitPath } = readQueryParams() + if (wf && pe && orgUnitPath) { + const orgUnitPathSegments = orgUnitPath.split('/') + const orgUnitId = orgUnitPathSegments[orgUnitPathSegments.length - 1] + + return { + wf, + pe, + ou: orgUnitId, + } + } + + return null +} + +export const useSelectionParams = () => { + const [params, setParams] = useState(getParamsIfAllAvailable) + + useEffect( + () => + // The call to listen returns an `unlisten` function to clean up the effect + history.listen(() => { + setParams(getParamsIfAllAvailable()) + }), + [] + ) + + return params +} diff --git a/src/workflow-context/use-workflow-context.js b/src/workflow-context/use-workflow-context.js new file mode 100644 index 00000000..bc9537aa --- /dev/null +++ b/src/workflow-context/use-workflow-context.js @@ -0,0 +1,4 @@ +import { useContext } from 'react' +import { WorkflowContext } from './workflow-context.js' + +export const useWorkflowContext = () => useContext(WorkflowContext) diff --git a/src/workflow-context/workflow-context.js b/src/workflow-context/workflow-context.js new file mode 100644 index 00000000..9b4d09e4 --- /dev/null +++ b/src/workflow-context/workflow-context.js @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +const WorkflowContext = createContext({ + displayName: '', + dataSets: [], + status: {}, +}) + +export { WorkflowContext } diff --git a/src/workflow-context/workflow-provider.js b/src/workflow-context/workflow-provider.js new file mode 100644 index 00000000..e0334d08 --- /dev/null +++ b/src/workflow-context/workflow-provider.js @@ -0,0 +1,63 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { PropTypes } from '@dhis2/prop-types' +import React, { useEffect } from 'react' +import { ErrorMessage, Loader } from '../shared/index.js' +import { useSelectedWorkflow } from './use-selected-workflow.js' +import { useSelectionParams } from './use-selection-params.js' +import { WorkflowContext } from './workflow-context.js' + +const query = { + approvalStatus: { + resource: 'dataApprovals', + params: ({ wf, pe, ou }) => ({ wf, pe, ou }), + }, +} + +const WorkflowProvider = ({ children }) => { + const params = useSelectionParams() + const { displayName, dataSets } = useSelectedWorkflow(params) + const { loading, error, data, called, refetch } = useDataQuery(query, { + lazy: true, + }) + + useEffect(() => { + if (params) { + refetch(params) + } + }, [params]) + + if (!params || !called) { + return null + } + + if (loading) { + return + } + + if (error) { + return ( + + {error.message} + + ) + } + + return ( + + {children} + + ) +} + +WorkflowProvider.propTypes = { + children: PropTypes.node.isRequired, +} + +export { WorkflowProvider }