├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .tx └── config ├── README.md ├── assets ├── css │ ├── CustomDatePicker.css │ ├── LoginPage.css │ ├── accordion.css │ ├── background.css │ ├── basicLayout.css │ ├── cardList.css │ ├── header.css │ ├── headerAlt.css │ ├── loader.css │ ├── loginForm.css │ ├── omrsGrid.css │ ├── patientHeader.css │ ├── tabs.css │ ├── taskList.css │ ├── toolTip.css │ └── widgets.css └── images │ └── arr-right.svg ├── build-with-docker.sh ├── package-lock.json ├── package.json ├── postcss.config.js ├── samples ├── App.js ├── SamplePage.jsx └── store.js ├── src ├── __tests__ │ └── store.test.jsx ├── components │ ├── README.md │ ├── accordion │ │ ├── Accordion.jsx │ │ └── __tests__ │ │ │ ├── Accordion.test.jsx │ │ │ └── __snapshots__ │ │ │ └── Accordion.test.jsx.snap │ ├── encounter │ │ ├── EncounterHistory.jsx │ │ └── __tests__ │ │ │ └── EncounterHistory.test.jsx │ ├── errors │ │ └── Errors.jsx │ ├── form │ │ ├── Cancel.jsx │ │ ├── EncounterDate.jsx │ │ ├── EncounterForm.jsx │ │ ├── EncounterFormPanel.jsx │ │ ├── FormContext.jsx │ │ ├── Obs.jsx │ │ ├── ObsGroup.jsx │ │ ├── ObsGroupContext.jsx │ │ ├── Section.jsx │ │ ├── Submit.jsx │ │ ├── __tests__ │ │ │ ├── EncounterDate.test.jsx │ │ │ ├── EncounterForm.test.jsx │ │ │ └── ObsGroup.test.jsx │ │ ├── withFormContext.jsx │ │ └── withObsGroupContext.jsx │ ├── grid │ │ ├── DataGrid.jsx │ │ └── __tests__ │ │ │ ├── DataGrid.test.jsx │ │ │ └── __snapshots__ │ │ │ └── DataGrid.test.jsx.snap │ ├── header │ │ ├── Head.jsx │ │ ├── Header.jsx │ │ ├── HeaderAlt.jsx │ │ ├── LocationMenu.jsx │ │ ├── NavBarMenu.jsx │ │ ├── PatientHeader.jsx │ │ ├── SlidingNavMenu.jsx │ │ └── __tests__ │ │ │ ├── Header.test.jsx │ │ │ ├── PatientHeader.test.jsx │ │ │ └── __snapshots__ │ │ │ └── Header.test.jsx.snap │ ├── home │ │ └── HomePage.jsx │ ├── layout │ │ └── BasicLayout.jsx │ ├── list │ │ ├── List.jsx │ │ └── __test__ │ │ │ ├── List.test.jsx │ │ │ └── __snapshots__ │ │ │ └── List.test.jsx.snap │ ├── loading │ │ └── LoadingView.jsx │ ├── localization │ │ ├── LocalizationContext.jsx │ │ ├── LocalizedMessage.jsx │ │ ├── __tests__ │ │ │ ├── LocalizedMessage.test.jsx │ │ │ └── withLocalization.test.jsx │ │ ├── test │ │ │ └── helpers │ │ │ │ └── intl-test.js │ │ └── withLocalization.jsx │ ├── login │ │ ├── Login.jsx │ │ ├── LoginForm.jsx │ │ ├── LoginPage.jsx │ │ ├── Logout.jsx │ │ └── __tests__ │ │ │ ├── Login.test.jsx │ │ │ ├── LoginForm.test.jsx │ │ │ ├── LoginPage.test.jsx │ │ │ └── __snapshots__ │ │ │ ├── Login.test.jsx.snap │ │ │ ├── LoginForm.test.jsx.snap │ │ │ └── LoginPage.test.jsx.snap │ ├── obs │ │ ├── ObsHistory.jsx │ │ └── ObsValue.jsx │ ├── patient │ │ └── PatientCard.jsx │ ├── program │ │ └── ProgramEnrollment.jsx │ ├── routes │ │ ├── AuthenticatedRoute.jsx │ │ └── __tests__ │ │ │ ├── AuthenticatedRoute.test.jsx │ │ │ └── __snapshots__ │ │ │ └── AuthenticatedRoute.test.jsx.snap │ ├── search │ │ ├── CardList.jsx │ │ └── PatientSearch.jsx │ ├── system │ │ └── SystemAlert.jsx │ ├── table │ │ └── SortableTable.jsx │ ├── tabs │ │ ├── Tab.jsx │ │ └── Tabs.jsx │ ├── task │ │ ├── TaskList.jsx │ │ ├── TaskListGroup.jsx │ │ ├── TaskListItem.jsx │ │ └── __tests__ │ │ │ ├── TaskList.test.jsx │ │ │ ├── TaskListGroup.test.jsx │ │ │ └── TaskListItem.test.jsx │ ├── tooltip │ │ ├── ToolTip.jsx │ │ └── __tests__ │ │ │ ├── ToolTip.test.jsx │ │ │ └── __snapshots__ │ │ │ └── ToolTip.test.jsx.snap │ └── widgets │ │ ├── ButtonGroup.jsx │ │ ├── CheckBox.jsx │ │ ├── CustomDatePicker.jsx │ │ ├── Dropdown.jsx │ │ ├── FieldInput.jsx │ │ ├── LineChart.js │ │ ├── Loader.jsx │ │ ├── TextArea.jsx │ │ └── __tests__ │ │ ├── ButtonGroup.test.jsx │ │ ├── CheckBox.test.jsx │ │ ├── CustomDatePicker.test.jsx │ │ ├── DropDown.test.jsx │ │ ├── FieldInput.test.js │ │ ├── Loader.test.jsx │ │ ├── TextArea.test.jsx │ │ └── __snapshots__ │ │ ├── ButtonGroup.test.jsx.snap │ │ ├── CheckBox.test.jsx.snap │ │ ├── CustomDatePicker.test.jsx.snap │ │ ├── DropDown.test.jsx.snap │ │ ├── FieldInput.test.js.snap │ │ ├── Loader.test.jsx.snap │ │ └── TextArea.test.jsx.snap ├── config.js ├── constants.js ├── domain │ ├── concept │ │ └── constants.js │ ├── encounter │ │ ├── constants.js │ │ └── filters │ │ │ └── encountersByEncounterTypeFilter.js │ ├── obs │ │ └── constants.js │ ├── patient │ │ ├── __tests__ │ │ │ └── patientUtils.test.js │ │ ├── constants.js │ │ ├── converters │ │ │ └── visitRestRepToPatientObjConverter.js │ │ ├── filters │ │ │ ├── patientObjByEncounterTypeAndObsFilter.js │ │ │ ├── patientObjByEncounterTypeFilter.js │ │ │ └── patientObjByVisitLocationFilter.js │ │ └── patientUtil.js │ ├── program │ │ └── constants.js │ └── visit │ │ └── constants.js ├── features │ ├── concept │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ ├── types.js │ │ └── util.js │ ├── encounter │ │ ├── actions.js │ │ ├── index.js │ │ ├── sagas.js │ │ └── types.js │ ├── errors │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ └── reducers.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ └── types.js │ ├── form │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── sagas.test.js │ │ │ ├── util.test.js │ │ │ └── validations.test.jsx │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ ├── types.js │ │ ├── util.js │ │ └── validations.jsx │ ├── globalproperty │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── grid │ │ ├── __tests__ │ │ │ └── action.test.js │ │ ├── actions.js │ │ ├── index.js │ │ └── types.js │ ├── header │ │ ├── __tests__ │ │ │ └── actions.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── list │ │ ├── __test__ │ │ │ └── createListReducer.test.js │ │ └── createListReducer.js │ ├── location │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── login │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── obs │ │ ├── __tests__ │ │ │ └── util.test.js │ │ └── util.js │ ├── patient │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ └── reducers.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── types.js │ │ └── utils.js │ ├── patientIdentifierTypes │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── search │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── session │ │ ├── __tests__ │ │ │ ├── actions.test.js │ │ │ ├── reducers.test.js │ │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ ├── system │ │ ├── actions.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── sagas.js │ │ └── types.js │ └── visit │ │ ├── __tests__ │ │ ├── actions.test.js │ │ └── sagas.test.js │ │ ├── actions.js │ │ ├── index.js │ │ ├── sagas.js │ │ └── types.js ├── index.js ├── localization │ ├── locale-data │ │ └── ht.js │ └── translations │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ └── ht.json ├── mocks │ └── fileMock.js ├── rest │ ├── __mocks__ │ │ ├── conceptRest.js │ │ ├── encounterRest.js │ │ ├── globalPropertyRest.js │ │ ├── loginRest.js │ │ ├── obsRest.js │ │ ├── patientRest.js │ │ ├── sessionRest.js │ │ └── visitRest.js │ ├── appframeworkRest.js │ ├── conceptRest.js │ ├── encounterRest.js │ ├── globalPropertyRest.js │ ├── locationRest.js │ ├── loginRest.js │ ├── obsRest.js │ ├── orderRest.js │ ├── patientRest.js │ ├── programRest.js │ ├── providerRest.js │ ├── reportingRest.js │ ├── sessionRest.js │ ├── systemRest.js │ └── visitRest.js ├── setupTests.js ├── store.js ├── types.js └── util │ ├── __tests__ │ └── dateUtil.test.js │ ├── dateUtil.js │ ├── filterUtil.js │ └── generalUtil.js └── styleguide.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["react", "env"], 3 | "plugins": ["transform-object-rest-spread", 4 | "transform-es2015-destructuring", 5 | "transform-class-properties", 6 | "transform-regenerator"] 7 | } 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: "14" 22 | - run: npm ci 23 | - run: npm run test:update-snapshots 24 | - run: npm run lint 25 | - run: npm run build 26 | - name: Upload Artifacts 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: dist 30 | path: | 31 | dist 32 | 33 | pre_release: 34 | runs-on: ubuntu-latest 35 | 36 | needs: build 37 | 38 | if: ${{ github.event_name == 'push' }} 39 | 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Download Artifacts 43 | uses: actions/download-artifact@v2 44 | - name: Use Node.js 45 | uses: actions/setup-node@v1 46 | with: 47 | node-version: "14" 48 | registry-url: "https://registry.npmjs.org" 49 | - run: npm install 50 | - run: sed -i -e "s/\(\"version\":\\s\+\"\([0-9]\+\.\?\)\+\)/\1-pre.${{ github.run_number }}/" 'package.json' 51 | - run: npm publish --access public --tag next 52 | env: 53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 54 | 55 | release: 56 | runs-on: ubuntu-latest 57 | 58 | needs: build 59 | 60 | if: ${{ github.event_name == 'release' }} 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | - name: Download Artifacts 65 | uses: actions/download-artifact@v2 66 | - name: Use Node.js 67 | uses: actions/setup-node@v1 68 | with: 69 | node-version: "14" 70 | registry-url: 'https://registry.npmjs.org' 71 | - run: npm install 72 | - run: npm publish --access public 73 | env: 74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen 10 | 11 | lib/* 12 | 13 | .DS_Store 14 | build 15 | node_modules 16 | *.log 17 | 18 | openmrs-react-components*.tgz 19 | 20 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com/ 3 | 4 | [o:openmrs:p:OpenMRS:r:react-components] 5 | file_filter = src/localization/translations/.json 6 | source_file = src/localization/translations/en.json 7 | source_lang = en 8 | type = KEYVALUEJSON 9 | 10 | -------------------------------------------------------------------------------- /assets/css/CustomDatePicker.css: -------------------------------------------------------------------------------- 1 | .react-datepicker { 2 | font-size: 1em; 3 | } 4 | .react-datepicker__header { 5 | padding-top: 0.8em; 6 | } 7 | .react-datepicker__month { 8 | margin: 0.4em 1em; 9 | } 10 | .react-datepicker__day-name, .react-datepicker__day { 11 | width: 1.9em; 12 | line-height: 1.9em; 13 | margin: 0.166em; 14 | } 15 | .react-datepicker__current-month { 16 | font-size: 1em; 17 | } 18 | .react-datepicker__navigation { 19 | height: 10px; 20 | width: 10px; 21 | line-height: 1.7em; 22 | border: 0.9em solid transparent; 23 | } 24 | 25 | .react-datepicker__portal .react-datepicker__navigation--next { 26 | border-left-color: #ccc !important; 27 | } 28 | 29 | .react-datepicker__portal .react-datepicker__navigation--previous { 30 | border-right-color: #ccc !important; 31 | } 32 | .react-datepicker__navigation--previous { 33 | border-right-color: #ccc; 34 | left: -0.2em; 35 | } 36 | .react-datepicker__navigation--next { 37 | border-left-color: #ccc; 38 | right: -0.2em; 39 | } 40 | 41 | .react-datepicker__portal { 42 | position: absolute; 43 | } 44 | .react-datepicker__portal .react-datepicker{ 45 | top: -20%; 46 | font-size: 2em; 47 | } 48 | 49 | .react-datepicker__portal .react-datepicker__month { 50 | margin: 0.6em 1.2em; 51 | } 52 | 53 | .react-datepicker__portal .react-datepicker__day { 54 | width: 1.9em; 55 | line-height: 1.9em; 56 | margin: 0.5em 57 | } 58 | 59 | .react-datepicker__portal .react-datepicker__day-name { 60 | width: 1.9em; 61 | line-height: 1.9em; 62 | margin: 0.5em 63 | } -------------------------------------------------------------------------------- /assets/css/LoginPage.css: -------------------------------------------------------------------------------- 1 | .foreground { 2 | postion: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | z-index: 1; 8 | } 9 | 10 | .loginLogo { 11 | max-height: 100px; 12 | padding: 3%; 13 | margin-bottom: 30px; 14 | border-radius: 10px; 15 | } 16 | 17 | .darken-pseudo { 18 | position: relative; 19 | } 20 | 21 | .darken-pseudo:after { 22 | content: ''; 23 | position: absolute; 24 | top: 0; 25 | bottom: 0; 26 | left: 0; 27 | right: 0; 28 | display: block; 29 | background-color: rgba(0, 0, 0, 0.4); 30 | } 31 | -------------------------------------------------------------------------------- /assets/css/accordion.css: -------------------------------------------------------------------------------- 1 | .accordion.border { 2 | border: 2px solid rgba(0,0,0,.125); 3 | border-radius: .25rem; 4 | } 5 | 6 | .accordion .header { 7 | cursor: pointer; 8 | background-color: #F9F9F9; 9 | border-bottom: 1px solid rgba(0,0,0,.125); 10 | padding: 5px; 11 | font-size: 1.2em; 12 | } 13 | 14 | .accordion .content { 15 | padding: 0px 5px; 16 | overflow-y: scroll; 17 | max-height: 0px; 18 | transition: all .5s ease; 19 | } 20 | 21 | .accordion .content.close { 22 | max-height: 0; 23 | } 24 | 25 | .accordion .content.open { 26 | max-height: 500px; 27 | } 28 | 29 | .rotate90 { 30 | transform: rotate(90deg); 31 | } 32 | 33 | .rotate180 { 34 | transform: rotate(180deg); 35 | } 36 | 37 | .icon-caret-up::before { 38 | color: #AE8C79; 39 | display: inline-block; 40 | vertical-align: middle; 41 | content: '\F0D8'; 42 | font-size: 12px; 43 | } 44 | 45 | a:hover { 46 | text-decoration: none; 47 | } 48 | -------------------------------------------------------------------------------- /assets/css/background.css: -------------------------------------------------------------------------------- 1 | .background { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | min-height: 100vh; 7 | flex-direction: column; 8 | background-position:50% 50%; /* Sets reference point to scale from */ 9 | background-size:cover; /* Sets background image to cover entire element */ 10 | } 11 | -------------------------------------------------------------------------------- /assets/css/basicLayout.css: -------------------------------------------------------------------------------- 1 | /* * 2 | * Css for Basic Layout Component 3 | */ 4 | html, body { 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | .header { 10 | background: #F8971D; 11 | z-index: 999; 12 | } 13 | 14 | #dropdown { 15 | color: white; 16 | } 17 | 18 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { 19 | background: #f78b08; 20 | } 21 | 22 | #root, #outer-container, .ag-theme-material, .container-fluid, 23 | .basic-layout.left-rail, .basic-layout.left-rail > div, .list-group, 24 | .div-container.summary-and-form, .div-container.container, .basic-layout.auth-route > div { 25 | height: 100%; 26 | } 27 | 28 | .div-container.container { 29 | padding: 0px; 30 | margin: 0 15px; 31 | } 32 | 33 | .basic-layout.row { 34 | display: flex; 35 | width: 100%; 36 | margin: 0 auto; 37 | height: inherit; 38 | } 39 | 40 | .row-container.row { 41 | padding-left: 10px; 42 | } 43 | 44 | .row-container { 45 | background: #ffa500b3; 46 | } 47 | 48 | .basic-layout.auth-route { 49 | padding-right: 0px; 50 | padding-left: 0px; 51 | } 52 | 53 | .summary-form { 54 | margin-right: 25%; 55 | } 56 | 57 | .header-panel.scroll-disabled { 58 | background: #fff; 59 | padding-left: 15px; 60 | padding-right: 15px; 61 | z-index: 2000; 62 | } 63 | 64 | .authenticated-route-container { 65 | overflow: scroll; 66 | height: calc(100vh - 300px); 67 | } 68 | 69 | .authenticated-route-container.full{ 70 | height: 90vh !important; 71 | } 72 | 73 | .authenticated-route-container.no-scroll{ 74 | overflow: unset; 75 | } 76 | -------------------------------------------------------------------------------- /assets/css/header.css: -------------------------------------------------------------------------------- 1 | /* * 2 | * Css for Header Component 3 | */ 4 | 5 | .location-container { 6 | display: block; 7 | background: white; 8 | position: absolute; 9 | z-index: 2; 10 | display: flex; 11 | flex-wrap: wrap; 12 | min-width: 320px; 13 | border: 2px solid #006056; 14 | right: 0px; 15 | width: 97.5%; 16 | } 17 | 18 | .location-container li { 19 | color: #007fff; 20 | padding: 5px; 21 | cursor: pointer; 22 | width: 32%; 23 | text-align: left; 24 | border-bottom: 1px #EBEBED ridge; 25 | margin: 5px 10px !important; 26 | } 27 | 28 | .location-container li:hover { 29 | background: #007fff; 30 | color: white; 31 | } 32 | 33 | .location-container li.selected { 34 | background: #007fff; 35 | color: white; 36 | } 37 | 38 | .change-location a { 39 | outline: none; 40 | } 41 | -------------------------------------------------------------------------------- /assets/css/loader.css: -------------------------------------------------------------------------------- 1 | .custom-loader { 2 | display: inline-block; 3 | position: relative; 4 | width: 64px; 5 | height: 64px; 6 | } 7 | .custom-loader div { 8 | position: absolute; 9 | width: 5px; 10 | height: 5px; 11 | background: #222; 12 | border-radius: 50%; 13 | animation: custom-loader 1.2s linear infinite; 14 | } 15 | .custom-loader div:nth-child(1) { 16 | animation-delay: 0s; 17 | top: 29px; 18 | left: 53px; 19 | } 20 | .custom-loader div:nth-child(2) { 21 | animation-delay: -0.1s; 22 | top: 18px; 23 | left: 50px; 24 | } 25 | .custom-loader div:nth-child(3) { 26 | animation-delay: -0.2s; 27 | top: 9px; 28 | left: 41px; 29 | } 30 | .custom-loader div:nth-child(4) { 31 | animation-delay: -0.3s; 32 | top: 6px; 33 | left: 29px; 34 | } 35 | .custom-loader div:nth-child(5) { 36 | animation-delay: -0.4s; 37 | top: 9px; 38 | left: 18px; 39 | } 40 | .custom-loader div:nth-child(6) { 41 | animation-delay: -0.5s; 42 | top: 18px; 43 | left: 9px; 44 | } 45 | .custom-loader div:nth-child(7) { 46 | animation-delay: -0.6s; 47 | top: 29px; 48 | left: 6px; 49 | } 50 | .custom-loader div:nth-child(8) { 51 | animation-delay: -0.7s; 52 | top: 41px; 53 | left: 9px; 54 | } 55 | .custom-loader div:nth-child(9) { 56 | animation-delay: -0.8s; 57 | top: 50px; 58 | left: 18px; 59 | } 60 | .custom-loader div:nth-child(10) { 61 | animation-delay: -0.9s; 62 | top: 53px; 63 | left: 29px; 64 | } 65 | .custom-loader div:nth-child(11) { 66 | animation-delay: -1s; 67 | top: 50px; 68 | left: 41px; 69 | } 70 | .custom-loader div:nth-child(12) { 71 | animation-delay: -1.1s; 72 | top: 41px; 73 | left: 50px; 74 | } 75 | @keyframes custom-loader { 76 | 0%, 20%, 80%, 100% { 77 | transform: scale(1); 78 | } 79 | 50% { 80 | transform: scale(1.5); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /assets/css/loginForm.css: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | padding: 20px; 7 | background: rgba(255,255,255,0.4); 8 | border-radius: 0px; 9 | } 10 | 11 | .midPanelItemContainer { 12 | margin-top: 20px; 13 | width: 220px; 14 | } 15 | 16 | .bottomPanelItemContainer { 17 | margin-top: 20px; 18 | width: 220px; 19 | align-content: stretch; 20 | } 21 | 22 | .panelItem { 23 | width: 220px; 24 | } 25 | 26 | .loginButton { 27 | width: 100%; 28 | border-radius: 0px; 29 | } 30 | 31 | .form-control { 32 | background-color: #ffffff; 33 | border-radius: 0px; 34 | border: 0px; 35 | color: #777; 36 | } 37 | 38 | .locationSelector{ 39 | background-color: #ffffff; 40 | height: 34px; 41 | padding: 6px 12px; 42 | font-size: 14px; 43 | line-height: 1.42857143; 44 | color: #555; 45 | } 46 | 47 | .alert-info { 48 | color: #666; 49 | background-image: none; 50 | background-color: #ffffff80; 51 | border-color: #871414; 52 | border-radius: 0px; 53 | border-width: 3px; 54 | } 55 | -------------------------------------------------------------------------------- /assets/css/omrsGrid.css: -------------------------------------------------------------------------------- 1 | .grid-wrapper { 2 | height: -webkit-calc(100vh - 72px); 3 | height: -moz-calc(100vh - 72px); 4 | height: calc(100vh - 72px); 5 | } 6 | -------------------------------------------------------------------------------- /assets/css/tabs.css: -------------------------------------------------------------------------------- 1 | .tab-list { 2 | border-bottom: 1px solid #ccc; 3 | padding-left: 0; 4 | } 5 | 6 | .tab-list-item { 7 | display: inline-block; 8 | list-style: none; 9 | margin: 2px; 10 | margin-bottom: -2px; 11 | padding: 0.5rem 0.75rem; 12 | cursor: pointer; 13 | 14 | border: solid #ccc; 15 | border-width: 1px 1px 0px 1px; 16 | 17 | background: #F1F1F1; 18 | color: #A9A9A9; 19 | 20 | font-size: 1em; 21 | 22 | } 23 | 24 | .tab-list-active { 25 | background-color: white; 26 | border: solid #ccc; 27 | border-width: 4px 1px 0px 1px; 28 | 29 | border-top-color: #363463; 30 | color: #363463; 31 | 32 | font-size: 1.2em; 33 | font-weight: bold; 34 | } 35 | -------------------------------------------------------------------------------- /assets/css/taskList.css: -------------------------------------------------------------------------------- 1 | a.list-group-item { 2 | border-top: 1px solid gray !important; 3 | border-radius: 0px !important; 4 | font-weight: 600; 5 | padding: 10px !important; 6 | } 7 | 8 | a.list-group-item::before { 9 | content: "\00a0\00a0"; 10 | } 11 | 12 | .list-group-item-heading { 13 | border-top: 1px solid black !important; 14 | background: gainsboro; 15 | padding: 13px; 16 | margin-bottom: 0px; 17 | font-size: 20px; 18 | } 19 | 20 | .list-group-item { 21 | padding: 0px !important; 22 | } 23 | 24 | .list-group button:last-child .list-group-item:last-child { 25 | border-bottom: 1px solid gray !important; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /assets/css/toolTip.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: relative; 3 | } 4 | .tooltip .tooltip-text { 5 | visibility: hidden; 6 | width: 300px; 7 | background-color: white; 8 | border: 1px solid grey; 9 | border-radius: 6px; 10 | text-align: left; 11 | position: absolute; 12 | bottom: 130%; 13 | left: 30%; 14 | z-index: 1; 15 | margin-left: -60px; 16 | opacity: 0; 17 | transition: opacity 0.3s; 18 | } 19 | .tooltip .tooltip-text p { 20 | padding: 5px 0 0 8px; 21 | font-size: 13px; 22 | font-weight: bold; 23 | } 24 | .tooltip .tooltip-text div { 25 | display: flex; 26 | flex-flow: row wrap; 27 | padding: 0px 10px 5px 10px; 28 | } 29 | .tooltip .tooltip-text div span { 30 | margin: 0 5px 7px 0; 31 | text-align: left; 32 | width: 31.5%; 33 | } 34 | .tooltip .tooltip-text::after { 35 | content: ""; 36 | position: absolute; 37 | top: 100%; 38 | left: 10%; 39 | margin-left: -5px; 40 | border-width: 10px; 41 | border-style: solid; 42 | border-color: #555 transparent transparent transparent; 43 | } 44 | .tooltip:hover .tooltip-text { 45 | visibility: visible; 46 | opacity: 1; 47 | } 48 | -------------------------------------------------------------------------------- /assets/images/arr-right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build-with-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage () { 4 | echo -e "Expects one of the following arguments: " 5 | echo -e "ci - runs npm ci" 6 | echo -e "test - runs npm run test" 7 | echo -e "cypress-test - runs npm run cypress-test. For this option, expects username, password, and url to be passed as subsequent arguments. eg. admin Admin123 localhost:8080/" 8 | echo -e "build - runs npm run build. For this option, the build number should be passed in as the second argument" 9 | echo -e "If no argument is passed, executes ci, followed by test, followed by build" 10 | } 11 | 12 | npmci() { 13 | docker run --rm \ 14 | -v $(pwd):/data \ 15 | -w="/data" \ 16 | node:14 \ 17 | npm ci 18 | } 19 | 20 | npmtest() { 21 | docker run --rm \ 22 | -v $(pwd):/data \ 23 | -w="/data" \ 24 | node:14 \ 25 | npm run test:update-snapshots 26 | } 27 | 28 | npmlint() { 29 | docker run --rm \ 30 | -v $(pwd):/data \ 31 | -w="/data" \ 32 | node:14 \ 33 | npm run lint 34 | } 35 | 36 | npmpack() { 37 | docker run --rm \ 38 | -v $(pwd):/data \ 39 | -w="/data" \ 40 | node:14 \ 41 | npm run pack 42 | } 43 | 44 | npmci 45 | npmtest 46 | npmlint 47 | npmpack 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': { 4 | root: __dirname, 5 | }, 6 | 'postcss-mixins': {}, 7 | 'postcss-each': {}, 8 | 'postcss-cssnext': {} 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /samples/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 4 | import './App.css'; 5 | import createStore from './store'; 6 | import { AuthenticatedRoute, LoginPage } from '@openmrs/react-components'; 7 | import SamplePage from './SamplePage'; 8 | 9 | const store = createStore(); 10 | 11 | const App = props => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /samples/SamplePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SamplePage = props => { 4 | return ( 5 |
6 | Hello World 7 |
8 | ); 9 | }; 10 | 11 | export default SamplePage; 12 | -------------------------------------------------------------------------------- /samples/store.js: -------------------------------------------------------------------------------- 1 | /* * This Source Code Form is subject to the terms of the Mozilla Public License, 2 | * v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under 4 | * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. 5 | * 6 | * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS 7 | * graphic logo is a trademark of OpenMRS Inc. 8 | */ 9 | 10 | import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; 11 | import logger from 'redux-logger'; 12 | import createSagaMiddleware from 'redux-saga'; 13 | import { sagas as openmrsSagas, reducers as openmrsReducers } from '@openmrs/react-components'; 14 | import { reducer as reduxFormReducer } from 'redux-form' 15 | // import reducers from './reducers'; 16 | 17 | const sagaMiddleware = createSagaMiddleware(); 18 | 19 | const middlewares = [sagaMiddleware]; 20 | 21 | const rootReducer = combineReducers({ 22 | // owaNamespace: reducers, // add your own reducers here under the namespace you chose 23 | openmrs: openmrsReducers, 24 | form: reduxFormReducer 25 | }) 26 | 27 | 28 | if (process.env.NODE_ENV !== 'production') { 29 | middlewares.push(logger); 30 | } 31 | 32 | export default () => { 33 | const store = createStore(rootReducer, compose( 34 | applyMiddleware(...middlewares), 35 | window.devToolsExtension && process.env.NODE_ENV !== 'production' 36 | ? window.devToolsExtension() : f => f, 37 | )); 38 | sagaMiddleware.run(openmrsSagas); 39 | return store; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | - [PatientHeader](https://github.com/openmrs/openmrs-react-components/tree/OEUI_183/src/components#patientheader) 3 | - Tabs 4 | 5 | 6 | ## PatientHeader 7 | 8 | ### Props 9 | 10 | 11 | | Name |Type | Default | Description | 12 | | ------------- |:-------------:| --- | ---- | 13 | | patient|`object`| | Patient object having `person`, `patientId` and `attributes` properties | 14 | | note | `array` | `[]` | The first index is displayed, index will be an object having `value` and `auditInfo` properties| 15 | 16 | 17 | ``` 18 | import { PatientHeader } from '@openmrs/react-component'; 19 | 20 | function header = (props) => { 21 | return ( 22 | 26 | ); 27 | } 28 | ``` 29 | 30 | ## Tabs 31 | 32 | ### Basic Usage 33 | 34 | For Every Tabbed Content create a div inside the Tabs component with a label and children. 35 | 36 | ``` 37 | import { Tabs } from '@openmrs/react-component'; 38 | 39 | function TabbedComponent = (props) => { 40 | return ( 41 |
42 |

Tabs Demo

43 | 44 |
45 | Welcome, Dear Patient! 46 |
47 |
48 | Sad to see you Go! 49 |
50 |
51 |
52 | ); 53 | } 54 | ``` 55 | 56 | ![Tabs Demo Screenshot](https://s2.gifyu.com/images/tabbedcomponent.gif) 57 | 58 | -------------------------------------------------------------------------------- /src/components/accordion/Accordion.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../../../assets/css/accordion.css'; 4 | import Arrow from '../../../assets/images/arr-right.svg'; 5 | 6 | class Accordion extends React.Component { 7 | state = { 8 | isVisible: this.props.open === undefined ? false : this.props.open, 9 | }; 10 | 11 | render() { 12 | return ( 13 |
14 |
{ 17 | this.setState(() => ({ isVisible: !this.state.isVisible })); 18 | }} 19 | role="button" 20 | tabIndex={0} 21 | > 22 | 23 | 24 | 30 |    31 | {this.props.title} 32 | 33 |
34 |
35 | {this.props.children} 36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | 43 | Accordion.propTypes = { 44 | border: PropTypes.bool, 45 | children: PropTypes.node.isRequired, 46 | open: PropTypes.bool, 47 | title: PropTypes.string.isRequired, 48 | }; 49 | 50 | Accordion.defaultProps = { 51 | open: false, 52 | border: false, 53 | }; 54 | 55 | export default Accordion; 56 | -------------------------------------------------------------------------------- /src/components/accordion/__tests__/Accordion.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Accordion from '../Accordion'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = renderer.create(); 11 | } 12 | return mountedComponent; 13 | }; 14 | 15 | describe('Component: Accordion', () => { 16 | beforeEach(() => { 17 | props = { 18 | open: true, 19 | title: 'Header', 20 | children:

I love this

21 | }; 22 | mountedComponent = undefined; 23 | }); 24 | 25 | it('renders properly', () => { 26 | const component = getComponent(); 27 | expect(component.toJSON()).toMatchSnapshot(); 28 | }); 29 | 30 | it('calls the onclick', () => { 31 | const renderedComponent = getComponent().root; 32 | renderedComponent.findByProps({ className: "header" }).props.onClick(); 33 | expect(renderedComponent.instance.state.isVisible).toEqual(false); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/accordion/__tests__/__snapshots__/Accordion.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Component: Accordion renders properly 1`] = ` 4 |
7 |
13 | 14 | 15 | 21 | 22 |    23 | Header 24 | 25 |
26 |
29 |

30 | I love this 31 |

32 |
33 |
34 | `; 35 | -------------------------------------------------------------------------------- /src/components/encounter/__tests__/EncounterHistory.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import configureMockStore from 'redux-mock-store'; 3 | import {mount} from 'enzyme'; 4 | import {Provider} from 'react-redux'; 5 | import EncounterHistory from '../EncounterHistory'; 6 | import EncounterForm from "../../form/EncounterForm"; 7 | 8 | let props, store; 9 | let mountedComponent 10 | 11 | const mockStore = configureMockStore(); 12 | 13 | const encounterForm = () => { 14 | if (!mountedComponent) { 15 | mountedComponent = mount( 16 | 17 | 18 | 19 | ); 20 | } 21 | return mountedComponent; 22 | }; 23 | 24 | describe("EncounterHistory", () => { 25 | 26 | it("should render correctly", () => { 27 | 28 | store = mockStore( 29 | { 30 | openmrs: { 31 | patients: { 32 | set: { 33 | 'abcd-1234': { "uuid": "abcd-1234" }, 34 | 'efgh-5678': { "uuid": "efgh-5678" }, 35 | }, 36 | isUpdating: false, 37 | selected: 'abcd-1234' 38 | } 39 | } 40 | }); 41 | 42 | props = { 43 | encounterType: { 44 | uuid: 'some-encounter-uuid' 45 | } 46 | }; 47 | 48 | expect(encounterForm().find('div').length).toBe(1); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/errors/Errors.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from "react-redux"; 3 | import { Alert } from 'react-bootstrap'; 4 | 5 | class Errors extends React.Component { 6 | 7 | render() { 8 | let errorMessages = null; 9 | 10 | if (this.props.errors) { 11 | const uniqueErrorMessage = [...new Set(this.props.errors.map(error => error.message))]; 12 | errorMessages = uniqueErrorMessage.map((e, i) => { 13 | return ({ e }); 14 | }); 15 | } 16 | 17 | if (errorMessages) { 18 | return( 19 |
20 | { errorMessages } 21 |
22 | ); 23 | } 24 | else { 25 | return null; 26 | } 27 | } 28 | 29 | } 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | errors: state.openmrs ? state.openmrs.errors : null 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps)(Errors); 38 | -------------------------------------------------------------------------------- /src/components/form/Cancel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import LocalizedMessage from '../localization/LocalizedMessage'; 4 | import withFormContext from './withFormContext'; 5 | 6 | const Cancel = (props) => { 7 | 8 | return ( 9 | 20 | ); 21 | }; 22 | 23 | export default withFormContext(Cancel); 24 | -------------------------------------------------------------------------------- /src/components/form/EncounterDate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field } from 'redux-form'; 4 | import { endOfDay } from 'date-fns'; 5 | import CustomDatePicker from '../widgets/CustomDatePicker'; 6 | import withFormContext from './withFormContext'; 7 | import validators from '../../features/form/validations'; 8 | 9 | const maxDateRange = validators.maxDateValue(endOfDay(new Date()), "today's"); 10 | 11 | const EncounterDate = (props) => { 12 | const { validations } = props; 13 | return ( 14 | 23 | ); 24 | }; 25 | 26 | const mapStateToProps = (state, props) => { 27 | return { 28 | value: props.formContext ? props.formContext.selector(state, `encounter-datetime`) : null 29 | }; 30 | }; 31 | 32 | export default withFormContext(connect(mapStateToProps)(EncounterDate)); 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/form/FormContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormContext = React.createContext(null); 4 | 5 | export default FormContext; 6 | -------------------------------------------------------------------------------- /src/components/form/ObsGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import ObsGroupContext from './ObsGroupContext'; 4 | import withObsGroupContext from './withObsGroupContext'; 5 | 6 | const ObsGroup = (props) => { 7 | 8 | const existingGroupingConcepts = props.obsGroupContext ? props.obsGroupContext.groupingConcepts : []; 9 | 10 | const context = { 11 | path: (props.obsGroupContext ? props.obsGroupContext.path + '^' : '') + props.path, 12 | groupingConcepts:[...existingGroupingConcepts, props.groupingConcept] 13 | }; 14 | 15 | return ( 16 | 17 | {props.children} 18 | 19 | ); 20 | 21 | }; 22 | 23 | ObsGroup.propTypes = { 24 | groupingConcept: PropTypes.oneOfType([ 25 | PropTypes.object, 26 | PropTypes.string]).isRequired, 27 | path: PropTypes.string.isRequired, 28 | }; 29 | 30 | export default withObsGroupContext(ObsGroup); 31 | -------------------------------------------------------------------------------- /src/components/form/ObsGroupContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ObsGroupContext = React.createContext(null); 4 | 5 | export default ObsGroupContext; 6 | -------------------------------------------------------------------------------- /src/components/form/Section.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'react-bootstrap'; 3 | 4 | const sectionStyle = { 5 | backgroundColor: 'rgba(255, 177, 0, 0.26)' 6 | }; 7 | const leftPadding = { 8 | paddingLeft: '60px', 9 | height: '40px' 10 | }; 11 | 12 | const Section = (props) => { 13 | 14 | return ( 15 | 16 | 17 |

{props.title}

18 | 19 |
20 | ); 21 | 22 | }; 23 | 24 | export default Section; 25 | -------------------------------------------------------------------------------- /src/components/form/Submit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import LocalizedMessage from '../localization/LocalizedMessage'; 4 | import withFormContext from './withFormContext'; 5 | import PropTypes from "prop-types"; 6 | 7 | const Submit = (props) => { 8 | 9 | return ( 10 | 21 | ); 22 | }; 23 | 24 | Submit.propTypes = { 25 | submitLabelCode: PropTypes.string, 26 | }; 27 | 28 | Submit.defaultProps = { 29 | labelCode: "reactcomponents.submit" 30 | }; 31 | 32 | export default withFormContext(Submit); 33 | -------------------------------------------------------------------------------- /src/components/form/__tests__/EncounterDate.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { Provider } from 'react-redux'; 4 | import configureMockStore from 'redux-mock-store'; 5 | import { reduxForm } from 'redux-form'; 6 | import EncounterDate from '../EncounterDate'; 7 | 8 | let props, store; 9 | let mountedComponent; 10 | 11 | const mockStore = configureMockStore(); 12 | 13 | const DecoratedEncounterDate = reduxForm({ 14 | form: 'testForm' 15 | })(EncounterDate); 16 | 17 | const encounterDate = () => { 18 | if (!mountedComponent) { 19 | mountedComponent = mount( 20 | 21 | 22 | 23 | ); 24 | } 25 | return mountedComponent; 26 | }; 27 | 28 | describe("EncounterDate", () => { 29 | 30 | beforeEach(() => { 31 | store = mockStore({}); 32 | mountedComponent = undefined; 33 | }); 34 | 35 | it("should render correctly", () => { 36 | 37 | expect(true).toBe(true); 38 | 39 | // TODO get this test working again 40 | 41 | /*props = { 42 | context: { 43 | mode: "edit" 44 | } 45 | }; 46 | 47 | //console.log(store); 48 | expect(encounterDate().find('Field').length).toBe(1);*/ 49 | }); 50 | 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/form/__tests__/ObsGroup.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import ObsGroup from '../ObsGroup'; 4 | 5 | let mountedComponent; 6 | 7 | 8 | const obsGroup = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = shallow( 11 | 12 | ); 13 | } 14 | return mountedComponent; 15 | }; 16 | 17 | describe("ObsGroup", () => { 18 | 19 | beforeEach(() => { 20 | mountedComponent = undefined; 21 | }); 22 | 23 | it("should render correctly", () => { 24 | obsGroup(); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/form/withFormContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormContext from './FormContext'; 3 | 4 | function withFormContext(WrappedComponent) { 5 | 6 | class WithFormContext extends React.PureComponent { 7 | render() { 8 | return ( 9 | 10 | {formContext => } 11 | 12 | ); 13 | } 14 | } 15 | 16 | WithFormContext.displayName = `WithFormContext(${getDisplayName(WrappedComponent)})`; 17 | 18 | return WithFormContext; 19 | } 20 | 21 | function getDisplayName(WrappedComponent) { 22 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 23 | } 24 | 25 | export default withFormContext; 26 | -------------------------------------------------------------------------------- /src/components/form/withObsGroupContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ObsGroupContext from './ObsGroupContext'; 3 | 4 | function withObsGroupContext(WrappedComponent) { 5 | 6 | class WithObsGroupContext extends React.PureComponent { 7 | render() { 8 | return ( 9 | 10 | {obsGroupContext => } 11 | 12 | ); 13 | } 14 | } 15 | 16 | WithObsGroupContext.displayName = `WithObsGroupContext(${getDisplayName(WrappedComponent)})`; 17 | 18 | return WithObsGroupContext; 19 | } 20 | 21 | function getDisplayName(WrappedComponent) { 22 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 23 | } 24 | 25 | export default withObsGroupContext; 26 | -------------------------------------------------------------------------------- /src/components/grid/__tests__/DataGrid.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import toJson from 'enzyme-to-json'; 4 | import { spy } from 'sinon'; 5 | import configureMockStore from 'redux-mock-store'; 6 | import { Provider } from 'react-redux'; 7 | import DataGrid from '../DataGrid'; 8 | 9 | 10 | let props, store; 11 | let mountedComponent; 12 | 13 | const mockStore = configureMockStore(); 14 | 15 | spy(DataGrid.prototype, 'componentDidMount'); 16 | 17 | const dataGrid = () => { 18 | if (!mountedComponent) { 19 | mountedComponent = mount( 20 | 21 | 22 | 23 | ); 24 | } 25 | return mountedComponent; 26 | }; 27 | 28 | describe('Component: DataGrid', () => { 29 | beforeEach(() => { 30 | // in an actual implementation of a queue, these would be mapped in 31 | props = { 32 | rowData: [], 33 | dispatch: () => {} 34 | }; 35 | store = mockStore({}); 36 | mountedComponent = undefined; 37 | }); 38 | 39 | it('renders properly', () => { 40 | expect(toJson(dataGrid())).toMatchSnapshot(); 41 | expect(DataGrid.prototype.componentDidMount.calledOnce).toBe(true); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/header/Head.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import PropTypes from 'prop-types'; 4 | import LocalizedMessage from '../localization/LocalizedMessage'; 5 | 6 | const Head = ({ id, defaultTitle }) => { 7 | return ( 8 | 9 | 10 | 11 | 15 | { text => {text}} 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | Head.propTypes = { 23 | defaultTitle: PropTypes.string.isRequired, 24 | id: PropTypes.string.isRequired, 25 | }; 26 | 27 | 28 | export default Head; 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/header/LocationMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavDropdown, MenuItem } from 'react-bootstrap'; 3 | import { sessionActions } from '../../features/session'; 4 | 5 | export class LocationMenu extends React.Component { 6 | 7 | render() { 8 | var locations_array = this.props.locations.filter(location => location.uuid !== this.props.sessionLocation.uuid); 9 | return ( 10 | 16 | {locations_array.map(location => ( 17 | { 19 | this.props.dispatch(sessionActions.setSessionLocation(location.uuid)); 20 | }} 21 | > 22 | {location.display} 23 | )) 24 | } 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | export default LocationMenu; 32 | -------------------------------------------------------------------------------- /src/components/header/NavBarMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import '../../../assets/css/headerAlt.css'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { NavDropdown, MenuItem } from 'react-bootstrap'; 5 | 6 | 7 | class NavBarMenu extends React.Component { 8 | 9 | render() { 10 | var page_path_array = Object.keys(this.props.pageOptions).filter(path => path !== this.props.pathname); 11 | return( 12 |
13 | 14 | 20 | {page_path_array.map( path => ( 21 | 22 | {this.props.pageOptions[path].icon && 23 | 24 | } 25 | {this.props.pageOptions[path].display} 26 | 27 | ))} 28 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default NavBarMenu; 36 | -------------------------------------------------------------------------------- /src/components/header/SlidingNavMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from 'prop-types'; 3 | import '../../../assets/css/headerAlt.css'; 4 | import { Row, Nav, NavItem } from 'react-bootstrap'; 5 | 6 | 7 | class SlidingNavMenu extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | const { pageOptions, pathname } = props; 11 | const pagePathArray = Object.keys(pageOptions); 12 | 13 | this.state = { 14 | activeKey: pathname, 15 | pagePathArray, 16 | }; 17 | this.handleSelect = this.handleSelect.bind(this); 18 | } 19 | 20 | handleSelect(path) { 21 | this.setState({ 22 | activeKey: path, 23 | }); 24 | } 25 | 26 | componentDidUpdate(prevProps) { 27 | if (prevProps.pathname !== this.props.pathname) { 28 | this.setState({ activeKey: this.props.pathname}) 29 | } 30 | } 31 | 32 | render() { 33 | 34 | const { pagePathArray, activeKey } = this.state; 35 | const { pageOptions } = this.props; 36 | 37 | return ( 38 |
39 |
40 | 41 | 57 | 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | SlidingNavMenu.propTypes = { 65 | pageOptions: PropTypes.object.isRequired, 66 | pathname: PropTypes.string.isRequired, 67 | } 68 | 69 | export default SlidingNavMenu; 70 | -------------------------------------------------------------------------------- /src/components/header/__tests__/__snapshots__/Header.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`header should render correctly 1`] = ` 4 |
5 |
6 |
9 | 12 | 16 | 17 |
18 | 82 |
83 |
84 | `; 85 | -------------------------------------------------------------------------------- /src/components/home/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from "react-redux"; 4 | import { Row } from 'react-bootstrap'; 5 | import { patientActions } from '../../features/patient'; 6 | import { LOCATION_TYPES } from '../../constants'; 7 | import '../../../assets/css/background.css'; 8 | 9 | class HomePage extends React.Component { 10 | 11 | componentDidMount() { 12 | this.props.dispatch(patientActions.clearSelectedPatient()); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 |
22 | 23 | ); 24 | } 25 | } 26 | 27 | HomePage.propTypes = { 28 | homeImage: PropTypes.string, 29 | }; 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | location: state.openmrs.session.sessionLocation ? state.openmrs.session.sessionLocation.uuid : LOCATION_TYPES.UnknownLocation 34 | }; 35 | }; 36 | 37 | 38 | export default connect(mapStateToProps)(HomePage); 39 | -------------------------------------------------------------------------------- /src/components/list/__test__/List.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import toJson from 'enzyme-to-json'; 4 | import { spy } from 'sinon'; 5 | import { Provider } from 'react-redux'; 6 | import configureMockStore from 'redux-mock-store'; 7 | import DataGrid from '../../grid/DataGrid'; 8 | import List from '../List'; 9 | 10 | let props, store; 11 | let mountedComponent; 12 | 13 | const mockStore = configureMockStore(); 14 | 15 | spy(List.prototype, 'componentDidMount'); 16 | 17 | const list = () => { 18 | if (!mountedComponent) { 19 | mountedComponent = mount( 20 | 21 | 22 | 23 | ); 24 | } 25 | return mountedComponent; 26 | }; 27 | 28 | describe('Component: List', () => { 29 | beforeEach(() => { 30 | // in an actual implementation of a list, these would be mapped in 31 | props = { 32 | rowData: [], 33 | dispatch: () => {}, 34 | fetchListActionCreator: () => {} 35 | }; 36 | store = mockStore({}); 37 | mountedComponent = undefined; 38 | }); 39 | 40 | it('renders properly', () => { 41 | expect(toJson(list())).toMatchSnapshot(); 42 | expect(list().find(DataGrid).length).toBe(1); 43 | expect(List.prototype.componentDidMount.calledOnce).toBe(true); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/loading/LoadingView.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class LoadingView extends Component { 4 | render() { 5 | return ( 6 |
Loading
7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/localization/LocalizationContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LocalizationContext = React.createContext({ 4 | intlProviderAvailable: false 5 | }); 6 | 7 | export default LocalizationContext; 8 | -------------------------------------------------------------------------------- /src/components/localization/LocalizedMessage.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { FormattedMessage } from "react-intl"; 5 | import LocalizationContext from './LocalizationContext'; 6 | 7 | const LocalizedMessage = (props) => { 8 | return ( 9 | 10 | 11 | {context => 12 | context.intlProviderAvailable ? 13 | ( 14 | 18 | {props.children} 19 | 20 | ) : ( 21 | 22 | {props.defaultMessage} 23 | 24 | ) 25 | } 26 | 27 | ); 28 | }; 29 | 30 | 31 | LocalizedMessage.propTypes = { 32 | defaultMessage: PropTypes.string.isRequired, 33 | id: PropTypes.string.isRequired 34 | }; 35 | 36 | export default LocalizedMessage; 37 | -------------------------------------------------------------------------------- /src/components/localization/__tests__/LocalizedMessage.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import configureMockStore from 'redux-mock-store'; 4 | import { Provider } from 'react-redux'; 5 | import { FormattedMessage } from "react-intl"; 6 | 7 | import withLocalization, { initializeLocalization } from '../withLocalization'; 8 | import LocalizedMessage from '../LocalizedMessage'; 9 | 10 | 11 | let store; 12 | let mountedComponent; 13 | 14 | let defaultMessage = ""; 15 | let id = ""; 16 | 17 | const mockStore = configureMockStore(); 18 | 19 | 20 | const localizedMessage = () => { 21 | 22 | const LocalizedComponent = withLocalization(LocalizedMessage); 23 | 24 | if (!mountedComponent) { 25 | mountedComponent = mount( 26 | 27 | 31 | 32 | ); 33 | } 34 | return mountedComponent; 35 | }; 36 | 37 | describe("LocalizedMessage", () => { 38 | 39 | beforeEach(() => { 40 | mountedComponent = undefined; 41 | }); 42 | 43 | it("should render FormattedMessage with id and default message", () => { 44 | 45 | store = mockStore({ 46 | openmrs: { 47 | session: { 48 | 49 | } 50 | } 51 | }); 52 | 53 | defaultMessage = "Some default message"; 54 | id = "some_id"; 55 | 56 | initializeLocalization(); 57 | 58 | expect(localizedMessage().find(FormattedMessage).props().defaultMessage).toBe("Some default message"); 59 | expect(localizedMessage().find(FormattedMessage).props().id).toBe("some_id"); 60 | }); 61 | 62 | it("should just render default message if no IntlProvider", () => { 63 | expect(mount( 64 | ) 68 | .find('span').text()).toBe("Some default message"); 69 | }); 70 | 71 | 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/localization/test/helpers/intl-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Components using the react-intl module require access to the intl context. 3 | * This is not available when mounting single components in Enzyme. 4 | * These helper functions aim to address that and wrap a valid, 5 | * English-locale intl context around them. 6 | */ 7 | 8 | import React from 'react'; 9 | import { IntlProvider, intlShape } from 'react-intl'; 10 | import { mount, shallow, configure } from 'enzyme'; 11 | import Adapter from 'enzyme-adapter-react-16'; 12 | 13 | configure({ adapter: new Adapter() }); 14 | 15 | const intlProvider = new IntlProvider({ locale: 'en' }, {}); 16 | const { intl } = intlProvider.getChildContext(); 17 | 18 | /** 19 | * When using React-Intl `injectIntl` on components, props.intl is required. 20 | */ 21 | function nodeWithIntlProp(node) { 22 | return React.cloneElement(node, { intl }); 23 | } 24 | 25 | export const shallowWithIntl = node => shallow(nodeWithIntlProp(node), { context: { intl } }); 26 | 27 | export const mountWithIntl = node => mount(nodeWithIntlProp(node), { 28 | context: { intl }, 29 | childContextTypes: { intl: intlShape }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import LoginForm from './LoginForm'; 4 | import { loginActions } from '../../features/login'; 5 | import { errorsActions } from '../../features/errors'; 6 | 7 | class Login extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.handleLogin = this.handleLogin.bind(this); 12 | } 13 | 14 | handleLogin(values) { 15 | this.props.dispatch(loginActions.login(values.username, values.password, values.location)); 16 | } 17 | 18 | componentDidMount() { 19 | this.props.dispatch(errorsActions.clearErrors()); 20 | this.props.dispatch(loginActions.getLoginLocations()); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | } 31 | 32 | export default connect()(Login); 33 | 34 | -------------------------------------------------------------------------------- /src/components/login/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import Login from './Login'; 6 | import '../../../assets/css/LoginPage.css'; 7 | import '../../../assets/css/background.css'; 8 | 9 | const LoginPage = props => { 10 | const { from } = props.location.state || { from: { pathname: "/" } }; 11 | 12 | if (props.session.authenticated === true) { 13 | return ; 14 | } 15 | else { 16 | return( 17 |
18 |
19 | {props.logo && ( 20 | ) 21 | } 22 | 23 |
24 |
25 | ); 26 | } 27 | }; 28 | 29 | LoginPage.propTypes = { 30 | homeImage: PropTypes.string, 31 | location: PropTypes.object.isRequired, 32 | logo: PropTypes.string, 33 | session: PropTypes.object.isRequired 34 | }; 35 | 36 | const mapStateToProps = (state) => { 37 | return { session: state.openmrs.session }; 38 | }; 39 | 40 | export default connect(mapStateToProps)(LoginPage); 41 | -------------------------------------------------------------------------------- /src/components/login/Logout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loginActions } from '../../features/login'; 4 | import { errorsActions } from '../../features/errors'; 5 | import { Redirect } from 'react-router-dom'; 6 | 7 | class Logout extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.handleLogout = this.handleLogout.bind(this); 12 | this.handleLogout(); 13 | } 14 | 15 | handleLogout() { 16 | this.props.dispatch(errorsActions.clearErrors()); 17 | this.props.dispatch(loginActions.logout()); 18 | } 19 | 20 | render() { 21 | return ( 22 | 27 | ); 28 | } 29 | } 30 | 31 | export default connect()(Logout); 32 | -------------------------------------------------------------------------------- /src/components/login/__tests__/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import toJson from 'enzyme-to-json'; 4 | import configureMockStore from 'redux-mock-store'; 5 | import { Provider } from 'react-redux'; 6 | import Login from '../Login'; 7 | import LoginForm from '../LoginForm'; 8 | import { errorsActions } from '../../../features/errors'; 9 | 10 | let props, store; 11 | let mountedComponent; 12 | 13 | const mockStore = configureMockStore(); 14 | 15 | const login = () => { 16 | if (!mountedComponent) { 17 | mountedComponent = mount( 18 | 19 | 20 | ); 21 | } 22 | return mountedComponent; 23 | }; 24 | 25 | describe('Component: Login', () => { 26 | beforeEach(() => { 27 | store = mockStore( 28 | { 29 | dispatch: {}, 30 | openmrs: { 31 | loginLocations: { 32 | list: [] 33 | } 34 | } 35 | }); 36 | mountedComponent = undefined; 37 | }); 38 | 39 | it('renders properly', () => { 40 | expect(toJson(login())).toMatchSnapshot(); 41 | expect(login().find(LoginForm).length).toBe(1); 42 | expect(login().find(LoginForm).props().onSubmit.length).toBe(1); 43 | expect(login().find(LoginForm).props().onSubmit.name).toBe("bound handleLogin"); 44 | }); 45 | 46 | it('onSubmit triggers login action', () => { 47 | const onSubmit = login().find(LoginForm).props().onSubmit; 48 | onSubmit({ username: "somename", password: "somepassword" }); 49 | expect(store.getActions()).toContainEqual(errorsActions.clearErrors()); 50 | expect(store.getActions()).toContainEqual({"password": "somepassword", "type": "login/REQUESTED", "username": "somename"}); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/login/__tests__/LoginForm.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import configureMockStore from 'redux-mock-store'; 4 | import { Provider } from 'react-redux'; 5 | import LoginForm from '../LoginForm'; 6 | 7 | 8 | describe("Component: LoginForm", () => { 9 | 10 | const mockStore = configureMockStore(); 11 | 12 | it("should render correctly", () => { 13 | 14 | const store = mockStore( 15 | { 16 | dispatch: {}, 17 | openmrs: { 18 | loginLocations: { 19 | list: [] 20 | } 21 | } 22 | }); 23 | const locations = []; 24 | 25 | const rendered = renderer.create( 26 | 27 | 28 | 29 | ); 30 | 31 | expect(rendered.toJSON()).toMatchSnapshot(); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/patient/PatientCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format } from 'date-fns'; 3 | import { uniqBy, prop } from "ramda"; 4 | import {DATE_FORMAT} from "../../constants"; 5 | import { formatAge } from '../../features/patient/utils' 6 | 7 | const PatientCard = (patient, index, onRowSelected, getPatientIdentifiers) => ( 8 |
onRowSelected(patient)}> 12 |
13 | 14 | {patient.name && patient.name.givenName && patient.name.givenName} 15 | {patient.name && patient.name.familyName && patient.name.familyName} 16 | 17 | 18 | {patient.gender && patient.gender === 'M' ? "Male" : "Female"} 19 | {formatAge(patient.birthdate).age} 20 | ({patient.birthdate && format(patient.birthdate, DATE_FORMAT)}) 21 | 22 |
23 |
24 | {patient.alert && 25 | 26 | {/* This removes duplicate patient alerts */} 27 | {uniqBy(prop('alert'))(patient.alert).map((alert) => ( 28 | {alert.alert} 29 | ))} 30 | 31 | } 32 |
33 |
34 | {getPatientIdentifiers(patient) && getPatientIdentifiers(patient).split('
').map((identifier, index) => ( 35 | {identifier} 36 | ))} 37 |
38 |
39 | ); 40 | 41 | export default PatientCard; 42 | -------------------------------------------------------------------------------- /src/components/routes/AuthenticatedRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Redirect, Route } from 'react-router-dom'; 4 | 5 | // adapted from: https://reacttraining.com/react-router/web/example/auth-workflow 6 | // will redirect to a Login Page if user is not authenicated 7 | // see sample/App.js for example of usage 8 | 9 | // TODO: make the "/login" pathname configurable via a prop? 10 | 11 | const AuthenicatedRoute = props => { 12 | 13 | if (props.session.authenticated === true) { 14 | return ( 15 | 19 | ); 20 | } 21 | else { 22 | return ( 23 | 29 | ); 30 | } 31 | 32 | }; 33 | 34 | const mapStateToProps = (state) => { 35 | return { session: state.openmrs.session }; 36 | }; 37 | 38 | export default connect(mapStateToProps)(AuthenicatedRoute); 39 | -------------------------------------------------------------------------------- /src/components/tabs/Tab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Tab extends React.Component { 5 | 6 | onClick = () => { 7 | const { label, onClick } = this.props; 8 | onClick(label); 9 | } 10 | 11 | render() { 12 | const { 13 | onClick, 14 | props: { 15 | activeTab, 16 | label, 17 | }, 18 | } = this; 19 | 20 | let className = 'tab-list-item'; 21 | 22 | if (activeTab === label) { 23 | className += ' tab-list-active tab-list-border'; 24 | } 25 | 26 | return ( 27 |
  • 31 | {label} 32 |
  • 33 | ); 34 | } 35 | } 36 | 37 | 38 | Tab.propTypes = { 39 | activeTab: PropTypes.string.isRequired, 40 | label: PropTypes.string.isRequired, 41 | onClick: PropTypes.func.isRequired, 42 | }; 43 | 44 | export default Tab; 45 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Tab from './Tab'; 4 | import '../../../assets/css/tabs.css'; 5 | 6 | class Tabs extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | activeTab: this.props.children[0].props.label, 12 | }; 13 | } 14 | 15 | onClickTabItem = (tab) => { 16 | this.setState({ activeTab: tab }); 17 | } 18 | 19 | render() { 20 | const { 21 | onClickTabItem, 22 | props: { 23 | children, 24 | }, 25 | state: { 26 | activeTab, 27 | } 28 | } = this; 29 | 30 | return ( 31 |
    32 |
      33 | {children.map((child) => { 34 | const { label } = child.props; 35 | 36 | return ( 37 | 43 | ); 44 | })} 45 |
    46 |
    47 | {children.map((child) => { 48 | if (child.props.label !== activeTab) {return undefined;} 49 | return child.props.children; 50 | })} 51 |
    52 |
    53 | ); 54 | } 55 | } 56 | 57 | 58 | Tabs.propTypes = { 59 | children: PropTypes.instanceOf(Array).isRequired, 60 | }; 61 | 62 | export default Tabs; 63 | -------------------------------------------------------------------------------- /src/components/task/TaskList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ListGroup } from 'react-bootstrap'; 4 | import TaskListGroup from './TaskListGroup'; 5 | 6 | import '../../../assets/css/taskList.css'; 7 | 8 | let TaskList = props => { 9 | 10 | return ( 11 |
    12 | 13 | {props.taskGroups.map(taskGroup => ( 14 | 19 | ))} 20 | 21 |
    22 | ); 23 | 24 | }; 25 | 26 | TaskList.propTypes = { 27 | patient: PropTypes.object.isRequired, 28 | taskGroups: PropTypes.array.isRequired 29 | }; 30 | 31 | export default TaskList; 32 | -------------------------------------------------------------------------------- /src/components/task/TaskListGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ListGroupItem } from 'react-bootstrap'; 4 | import TaskListItem from './TaskListItem'; 5 | 6 | class TaskListGroup extends React.PureComponent { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | // TODO have a way for this to be overridden 12 | this.taskListGroupStyle = { 13 | 'border': 0, 14 | 'padding': "5px", 15 | }; 16 | 17 | this.state = { 18 | expanded: typeof(props.taskGroup.expanded) !== 'undefined' ? props.taskGroup.expanded : true 19 | }; 20 | 21 | this.toggleExpanded = this.toggleExpanded.bind(this); 22 | }; 23 | 24 | toggleExpanded(e) { 25 | // bit of a hack, we only want to expand/contract when clicking on the heading, not the nested menu items 26 | if (e.target.className === 'list-group-item-heading') { 27 | this.setState({ 28 | expanded: !this.state.expanded 29 | }); 30 | } 31 | } 32 | 33 | render() { 34 | 35 | const requiredTasks = this.props.taskGroup.tasks.filter((task) => task.required ? task.required(this.props.patient) : true); 36 | 37 | if (requiredTasks.length > 0) { 38 | return ( 39 | 45 | {this.state.expanded && requiredTasks.map(task => ( 46 | 53 | ))} 54 | ); 55 | } 56 | else { 57 | return (null); 58 | } 59 | } 60 | } 61 | 62 | TaskListGroup.propTypes = { 63 | patient: PropTypes.object.isRequired, 64 | taskGroup: PropTypes.object.isRequired 65 | }; 66 | 67 | export default TaskListGroup; 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/components/task/TaskListItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { ListGroupItem } from 'react-bootstrap'; 5 | 6 | let TaskListItem = props => { 7 | 8 | const completed = props.completed ? props.completed(props.patient) : false; 9 | 10 | // TODO have a way for this to be overridden 11 | let taskListItemStyle = { 12 | 'border': 0, 13 | 'padding': "5px 2px", 14 | 'color': completed ? props.colorComplete : props.colorPending 15 | }; 16 | 17 | return ( 18 | 22 | 26 |    {props.title} 27 | { props.timeFn } 28 | 29 | ); 30 | 31 | }; 32 | 33 | TaskListItem.propTypes = { 34 | colorComplete: PropTypes.string, 35 | colorPending: PropTypes.string, 36 | completed: PropTypes.func, 37 | link: PropTypes.string, 38 | patient: PropTypes.object.isRequired, 39 | title: PropTypes.string.isRequired 40 | }; 41 | 42 | TaskListItem.defaultProps = { 43 | colorComplete: "green", 44 | colorPending: "gray1" 45 | }; 46 | 47 | export default TaskListItem; 48 | -------------------------------------------------------------------------------- /src/components/task/__tests__/TaskList.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { ListGroup } from 'react-bootstrap'; 4 | import { Provider } from 'react-redux'; 5 | import configureMockStore from 'redux-mock-store'; 6 | import TaskList from '../TaskList'; 7 | 8 | let props, store; 9 | let mountedComponent; 10 | 11 | const mockStore = configureMockStore(); 12 | 13 | const taskList = () => { 14 | if (!mountedComponent) { 15 | mountedComponent = mount( 16 | 17 | 18 | 19 | ); 20 | } 21 | return mountedComponent; 22 | }; 23 | 24 | describe('Component: TaskList', () => { 25 | 26 | props = { 27 | patient: {}, 28 | taskGroups: [ 29 | { 30 | key: 1, 31 | title: "Group 1", 32 | tasks: [ 33 | { 34 | title: "Check-In", 35 | } 36 | ] 37 | }, 38 | { 39 | key: 2, 40 | title: "Group 2", 41 | tasks: [ 42 | { 43 | title: "Viral Load", 44 | }, 45 | { 46 | title: "Adherence Counseling", 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | 53 | store = mockStore(); 54 | 55 | it('renders properly', () => { 56 | expect(taskList().find(ListGroup).length).toBe(1); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/task/__tests__/TaskListGroup.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {mount} from "enzyme/build"; 3 | import TaskListGroup from "../TaskListGroup"; 4 | import {ListGroupItem} from "react-bootstrap"; 5 | 6 | 7 | let props; 8 | let mountedComponent; 9 | 10 | const taskListGroup = () => { 11 | if (!mountedComponent) { 12 | mountedComponent = mount( 13 | 14 | ); 15 | } 16 | return mountedComponent; 17 | }; 18 | 19 | 20 | describe('Component: TaskListGroup', () => { 21 | 22 | beforeEach(() => { 23 | mountedComponent = undefined; 24 | }); 25 | 26 | it('renders properly', () => { 27 | 28 | props = { 29 | patient: {}, 30 | taskGroup: { 31 | key: 1, 32 | title: "Group 1", 33 | tasks: [ 34 | { 35 | title: "Check-In" 36 | }, 37 | { 38 | title: "Blood Pressure" 39 | } 40 | ] 41 | } 42 | }; 43 | 44 | expect(taskListGroup().find(ListGroupItem).length).toBe(3); 45 | expect(taskListGroup().find(ListGroupItem).find({ header: 'Group 1' }).length).toBe(1); 46 | expect(taskListGroup().find(ListGroupItem).find({ title: 'Check-In' }).length).toBe(1); 47 | expect(taskListGroup().find(ListGroupItem).find({ title: 'Blood Pressure' }).length).toBe(1); 48 | 49 | }); 50 | 51 | it('should hide task that is not required', () => { 52 | 53 | props = { 54 | patient: {}, 55 | taskGroup: { 56 | key: 1, 57 | title: "Group 1", 58 | tasks: [ 59 | { 60 | title: "Check-In" 61 | }, 62 | { 63 | title: "Blood Pressure", 64 | required: () => false 65 | } 66 | ] 67 | } 68 | }; 69 | 70 | expect(taskListGroup().find(ListGroupItem).length).toBe(2); 71 | expect(taskListGroup().find(ListGroupItem).find({ header: 'Group 1' }).length).toBe(1); 72 | expect(taskListGroup().find(ListGroupItem).find({ title: 'Check-In' }).length).toBe(1); 73 | 74 | }); 75 | 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/task/__tests__/TaskListItem.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { ListGroupItem } from 'react-bootstrap'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import TaskListItem from '../TaskListItem'; 6 | 7 | let props; 8 | let mountedComponent; 9 | 10 | const taskList = () => { 11 | if (!mountedComponent) { 12 | mountedComponent = mount( 13 | 14 | ); 15 | } 16 | return mountedComponent; 17 | }; 18 | 19 | describe('Component: TaskListItem', () => { 20 | 21 | beforeEach(() => { 22 | mountedComponent = undefined; 23 | }); 24 | 25 | it('renders properly', () => { 26 | 27 | props = { 28 | title: "Blood Pressure", 29 | patient: { 30 | uuid: 'abcd' 31 | }, 32 | }; 33 | 34 | expect(taskList().find(ListGroupItem).length).toBe(1); 35 | expect(taskList().find(ListGroupItem).text()).toContain("Blood Pressure"); 36 | }); 37 | 38 | it('renders checkmark when task completed', () => { 39 | 40 | props = { 41 | title: "Blood Pressure", 42 | patient: { 43 | uuid: 'abcd' 44 | }, 45 | completed: () => true, 46 | }; 47 | 48 | expect(taskList().find(ListGroupItem).length).toBe(1); 49 | expect(taskList().find(ListGroupItem).text()).toContain("Blood Pressure"); 50 | expect(taskList().find(FontAwesomeIcon).length).toBe(1); 51 | expect(taskList().find(FontAwesomeIcon).props().icon).toBe("check"); 52 | 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/tooltip/ToolTip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../../../assets/css/toolTip.css'; 4 | 5 | const ToolTip = ({ toolTipHeader, toolTipBody }) => ( 6 | 7 | {toolTipHeader &&

    { toolTipHeader }

    } 8 |
    9 | {toolTipBody && toolTipBody.map((message, index) => ( 10 | {message} 11 | ))} 12 |
    13 |
    14 | ); 15 | 16 | ToolTip.propTypes = { 17 | toolTipBody: PropTypes.arrayOf(PropTypes.string), 18 | toolTipHeader: PropTypes.string 19 | }; 20 | 21 | export default ToolTip; 22 | -------------------------------------------------------------------------------- /src/components/tooltip/__tests__/ToolTip.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import ToolTip from '../ToolTip'; 4 | 5 | 6 | const props = { 7 | tests: [ 'sampleA', 'sampleB', 'sampleC' ], 8 | header: "mockToolTipHeader" 9 | }; 10 | describe('Component: TestsToolTip', () => { 11 | it('should render correctly', () => { 12 | const wrapper = renderer.create( 13 | ).toJSON(); 17 | expect(wrapper).toMatchSnapshot(); 18 | }); 19 | it('should render without toolTipHeader', () => { 20 | const wrapper = renderer.create( 21 | ).toJSON(); 24 | expect(wrapper).toMatchSnapshot(); 25 | }); 26 | it('should render without toolTipBody', () => { 27 | const wrapper = renderer.create( 28 | ).toJSON(); 31 | expect(wrapper).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/tooltip/__tests__/__snapshots__/ToolTip.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Component: TestsToolTip should render correctly 1`] = ` 4 | 7 |

    8 | mockToolTipHeader 9 |

    10 |
    11 | 12 | sampleA 13 | 14 | 15 | sampleB 16 | 17 | 18 | sampleC 19 | 20 |
    21 |
    22 | `; 23 | 24 | exports[`Component: TestsToolTip should render without toolTipBody 1`] = ` 25 | 28 |

    29 | mockToolTipHeader 30 |

    31 |
    32 | 33 | `; 34 | 35 | exports[`Component: TestsToolTip should render without toolTipHeader 1`] = ` 36 | 39 |
    40 | 41 | sampleA 42 | 43 | 44 | sampleB 45 | 46 | 47 | sampleC 48 | 49 |
    50 |
    51 | `; 52 | -------------------------------------------------------------------------------- /src/components/widgets/ButtonGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToggleButtonGroup, ToggleButton } from 'react-bootstrap'; 3 | import '../../../assets/css/widgets.css'; 4 | import formUtil from '../../features/form/util'; 5 | 6 | const formatId = (display) => display && display.toLowerCase().replace(/\s/g,'_'); 7 | 8 | const buttonStyle = { 9 | whiteSpace: "normal" 10 | }; 11 | 12 | const ButtonGroup = ({ 13 | displayValue, 14 | input, 15 | mode, 16 | options, 17 | justified, 18 | disabled 19 | }) => { 20 | 21 | if (typeof justified === 'undefined' || justified === null) { 22 | justified = true; 23 | } 24 | 25 | const edit = ( 26 | {}}> 27 | {options.map((option) => { 28 | const displayId = formatId(option.display); 29 | return ( 30 | { 35 | if (input.value === option.uuid) { 36 | input.onChange(''); 37 | } else { 38 | input.onChange(option.uuid); 39 | } 40 | }} 41 | style={buttonStyle} 42 | value={option.uuid} 43 | > 44 | {option.name || option.display} 45 | 46 | ); 47 | } 48 | )} 49 | 50 | ); 51 | 52 | const display = formUtil.conceptAnswerDisplay(displayValue, options); 53 | const view = ( 54 | 58 | {display} 59 | 60 | ); 61 | 62 | return ( 63 |
    64 | {!mode || mode === 'edit' ? edit : view} 65 |
    66 | ); 67 | 68 | }; 69 | 70 | export default ButtonGroup; 71 | -------------------------------------------------------------------------------- /src/components/widgets/FieldInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FormControl } from 'react-bootstrap'; 4 | import '../../../assets/css/widgets.css'; 5 | 6 | const FieldInput = ({ 7 | input, 8 | mode, 9 | placeholder, 10 | displayValue, 11 | type, 12 | id, 13 | meta: { error, warning } 14 | }) => { 15 | 16 | const edit = ( 17 | 23 | ); 24 | 25 | const view = ( 26 | 29 | {displayValue} 30 | 31 | ); 32 | 33 | const validations = ( 34 |
    35 | 39 | {error ? error : (warning ? warning : '_')} 40 | 41 |
    42 | ); 43 | 44 | return ( 45 |
    46 | {!mode || mode === 'edit' ? edit : view} 47 | {validations} 48 |
    49 | ); 50 | 51 | }; 52 | 53 | FieldInput.defaultProps = { 54 | meta: { 55 | error: false, 56 | warning: false, 57 | } 58 | }; 59 | 60 | FieldInput.propTypes = { 61 | input: PropTypes.object, 62 | meta: PropTypes.object, 63 | placeholder: PropTypes.string, 64 | type: PropTypes.string, 65 | id: PropTypes.string, 66 | }; 67 | 68 | export default FieldInput; 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/widgets/LineChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {format} from 'date-fns'; 4 | import { 5 | LineChart, 6 | Line, 7 | CartesianGrid, 8 | XAxis, 9 | YAxis, 10 | Tooltip, 11 | } from 'recharts'; 12 | 13 | const CustomLineChart = (props) => { 14 | const { ...otherProps } = props; 15 | return ( 16 | 22 | 26 | format(unixTime, 'DD MMM YYYY')} 30 | type='number' 31 | /> 32 | format(unixTime,'DD MMM YYYY')} 34 | position={{ y: 200 }} 35 | /> 36 | 40 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | CustomLineChart.defaultProps = { 50 | type: "monotone", 51 | }; 52 | 53 | CustomLineChart.propTypes = { 54 | data: PropTypes.array, 55 | height: PropTypes.number.isRequired, 56 | type: PropTypes.string, 57 | width: PropTypes.number.isRequired, 58 | xAxisKey: PropTypes.string.isRequired, 59 | yAxisKey: PropTypes.string.isRequired, 60 | }; 61 | 62 | export default CustomLineChart; 63 | -------------------------------------------------------------------------------- /src/components/widgets/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../../assets/css/loader.css'; 3 | 4 | const Loader = () => ( 5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 | ); 20 | 21 | export default Loader; 22 | -------------------------------------------------------------------------------- /src/components/widgets/TextArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FormControl } from 'react-bootstrap'; 4 | import '../../../assets/css/widgets.css'; 5 | 6 | const TextArea = ({ 7 | input, 8 | mode, 9 | placeholder, 10 | displayValue, 11 | }) => { 12 | 13 | const edit = ( 14 | 19 | ); 20 | 21 | const view = ( 22 | 25 | {displayValue} 26 | 27 | ); 28 | 29 | return ( 30 |
    31 | {!mode || mode === 'edit' ? edit : view} 32 |
    33 | ); 34 | 35 | }; 36 | 37 | TextArea.propTypes = { 38 | input: PropTypes.object, 39 | meta: PropTypes.object, 40 | placeholder: PropTypes.string, 41 | type: PropTypes.string, 42 | }; 43 | 44 | export default TextArea; 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/ButtonGroup.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import ButtonGroup from '../ButtonGroup'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = renderer.create(); 11 | } 12 | return mountedComponent; 13 | }; 14 | 15 | describe('Component: ButtonGroup', () => { 16 | beforeEach(() => { 17 | props = { 18 | input: { 19 | name: 'mockname' 20 | }, 21 | options: [{ 22 | name: 'mockName', 23 | uuid: '1234', 24 | display: 'Mock Display', 25 | }] 26 | }; 27 | mountedComponent = undefined; 28 | }); 29 | 30 | it('renders properly', () => { 31 | const component = getComponent(); 32 | expect(component.toJSON()).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/CheckBox.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import CheckBox from '../CheckBox'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = renderer.create(); 11 | } 12 | return mountedComponent; 13 | }; 14 | 15 | describe('Component: CheckBox', () => { 16 | beforeEach(() => { 17 | props = { 18 | }; 19 | mountedComponent = undefined; 20 | }); 21 | 22 | it('renders properly', () => { 23 | const component = getComponent(); 24 | expect(component.toJSON()).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/CustomDatePicker.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { parse } from 'date-fns'; 3 | import renderer from 'react-test-renderer'; 4 | import CustomDatePicker from '../CustomDatePicker'; 5 | 6 | let props; 7 | let mountedComponent; 8 | 9 | const getComponent = () => { 10 | if (!mountedComponent) { 11 | mountedComponent = renderer.create(); 12 | } 13 | return mountedComponent; 14 | }; 15 | 16 | describe('Component: CustomDatePicker', () => { 17 | beforeEach(() => { 18 | props = { 19 | defaultDate: parse('2018-10-23T00:00:00.000'), 20 | excludeDates: [] 21 | }; 22 | mountedComponent = undefined; 23 | }); 24 | 25 | it('renders properly', () => { 26 | const component = getComponent(); 27 | expect(component.toJSON()).toMatchSnapshot(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/DropDown.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Dropdown from '../Dropdown'; 4 | import { IntlProvider } from 'react-intl'; 5 | 6 | let props; 7 | let mountedComponent; 8 | 9 | const getComponent = () => { 10 | if (!mountedComponent) { 11 | mountedComponent = renderer.create(); 12 | } 13 | return mountedComponent; 14 | }; 15 | 16 | describe('Component: Dropdown', () => { 17 | beforeEach(() => { 18 | props = { 19 | }; 20 | mountedComponent = undefined; 21 | }); 22 | 23 | it('renders properly', () => { 24 | const component = getComponent(); 25 | expect(component.toJSON()).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/FieldInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import FieldInput from '../FieldInput'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | props= { 11 | input: {}, 12 | placeholder: 'mockplaceholder', 13 | type: 'mockType', 14 | meta: { touched: true, error: false, warning: false } 15 | }; 16 | mountedComponent = renderer.create(); 17 | } 18 | return mountedComponent; 19 | }; 20 | 21 | describe('Component: FieldInput', () => { 22 | beforeEach(() => { 23 | props = { 24 | }; 25 | mountedComponent = undefined; 26 | }); 27 | 28 | it('renders properly', () => { 29 | const component = getComponent(); 30 | expect(component.toJSON()).toMatchSnapshot(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/Loader.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import Loader from '../Loader'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = renderer.create(); 11 | } 12 | return mountedComponent; 13 | }; 14 | 15 | describe('Component: Loader', () => { 16 | beforeEach(() => { 17 | props = { 18 | }; 19 | mountedComponent = undefined; 20 | }); 21 | 22 | it('renders properly', () => { 23 | const component = getComponent(); 24 | expect(component.toJSON()).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/widgets/__tests__/TextArea.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import TextArea from '../TextArea'; 4 | 5 | let props; 6 | let mountedComponent; 7 | 8 | const getComponent = () => { 9 | if (!mountedComponent) { 10 | mountedComponent = renderer.create(