diff --git a/.eslintrc.js b/.eslintrc.js index a91ef4dcd7..07674dd3f8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,11 +28,11 @@ module.exports = { 'import/resolver': { node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], - moduleDirectory: ["node_modules"], + moduleDirectory: ['node_modules'], }, - "typescript": { + typescript: { alwaysTryTypes: true, - } + }, }, 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'], @@ -43,6 +43,7 @@ module.exports = { 'prettier/prettier': 'error', '@typescript-eslint/member-delimiter-style': 'off', '@typescript-eslint/explicit-function-return-type': 'off', + "@typescript-eslint/explicit-module-boundary-types": 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], '@typescript-eslint/unified-signatures': 'error', @@ -61,19 +62,19 @@ module.exports = { 'no-nested-ternary': 'off', 'import/no-unresolved': 'off', 'import/extensions': ['error', 'never'], - 'import/order': ["error", { - "groups": [ - "external", - ["sibling","parent","internal"], - "builtin", - "unknown", - ], - "newlines-between": "always", - "alphabetize": { - "order": 'asc', - "caseInsensitive": true, + 'import/order': [ + 'error', + { + groups: ['external', ['sibling', 'parent', 'internal'], 'builtin', 'unknown'], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, }, - }], + ], curly: ['error', 'all'], + 'react/require-default-props': ['warn'], + 'react/default-props-match-prop-types': ['warn'] }, } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f2db2b2464..1074e42bff 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,12 +15,49 @@ To contribute, ## Setting up the Local Development Environment +### Get the source code + 1. [Fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repository 2. Open your favorite command line tool and navigate to the directory you wish to clone this repository to: `cd /path/to/clone` 3. [Clone](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) your fork: `git clone git@github.com:{your username}/hospitalrun-frontend.git` 4. Navigate to the hosptialrun-frontend directory: `cd hospitalrun-frontend` -5. Install dependencies: `yarn` -6. Run the application `yarn start` + +### Configure CouchDB + +CouchDB is the server side database which data from the frontend will sync to. In order to login +to HospitalRun, CouchDB is required. For convienence, we have added a docker compose file in the +root of this project to help launch CouchDB. However, you could install and run CouchDB in any way you wish. + +The following directions will be for running CouchDB via Docker Compose. + +1. Install [Docker](https://docs.docker.com/get-docker/) +2. Install [Docker Compose](https://docs.docker.com/compose/install/) +3. Run `docker-compose up --build -d` in the root directory. + +This should launch a new CouchDB instance on `http://localhost:5984`, create system database, configure CouchDB as Single Node, enable CORS, create `hospitalrun` database, create a default admin with a username of `admin` and password of 'password' + +4. Make the `hospitalrun` database a public database by removing its member permissions: + + ``` + curl -X PUT http://admin:password@localhost:5984/hospitalrun/_security -d '{"members": {}, "admins": {"roles": ["_admin"] }}' + ``` + +5. Create a sample user with a username of `username` and password of 'password' to use new login page [#2137](https://github.com/HospitalRun/hospitalrun-frontend/pull/2137) + + ``` + curl -X PUT http://admin:password@localhost:5984/_users/org.couchdb.user:username -H "Accept: application/json" -H "Content-Type: application/json" -d '{"name": "username", "password": "password", "metadata": { "givenName": "John", "familyName": "Doe"}, "roles": [], "type": "user"}' + ``` + +6. Launch `http://localhost:5984/_utils` to view Fauxton and perform administrative tasks. + +**_Cleanup_** +To delete the development database, go to the root of the project and run `docker-compose down -v --rmi all --remove-orphans` + +### Install dependencies & start the application + +1. Install dependencies: `npm install` +2. Configure `REACT_APP_HOSPITALRUN_API=http://localhost:5984` environment variable in `.env` +3. Run the application `npm start` ## Online one-click setup for contributing @@ -96,13 +133,14 @@ Before submitting a request, please search for similar ones in the ## Pull Requests -1. Ensure any install or build dependencies are removed before the end of the layer when doing a +1. **Don't open the PR until it's ready for review. We need to optimize build time.** +2. Ensure any install or build dependencies are removed before the end of the layer when doing a build. -2. Update the README.md with details of changes to the interface, this includes new environment +3. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. -3. Increase the version numbers in any examples files and the README.md to the new version that this +4. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you +5. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. ## Contributor License Agreement diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1daeac015..d1b1152a51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - # node-version: [12.x, 14.x] - node-version: [12.x] + node-version: [12.x, 14.x] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - run: git config --global core.autocrlf false # this is needed to prevent git changing EOL after cloning on Windows OS @@ -22,7 +21,7 @@ jobs: npm install - name: Lint code run: | - npm run lint + npm run lint:fix - name: Build if: always() run: | @@ -36,29 +35,3 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./coverage/lcov.info - - npm-node14: - runs-on: ${{ matrix.os }} - strategy: - matrix: - node-version: [14.x] - os: [ubuntu-latest] - steps: - - run: git config --global core.autocrlf false # this is needed to prevent git changing EOL after cloning on Windows OS - - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install with npm - run: | - npm install - - name: Lint code - run: | - npm run lint - - name: Build - run: | - npm run build - - name: Run tests - run: | - npm run test:ci diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml deleted file mode 100644 index b3197c5cc4..0000000000 --- a/.github/workflows/compressed-size.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Compressed Size - -on: [pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - uses: preactjs/compressed-size-action@v2 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/pr-updater.yml b/.github/workflows/pr-updater.yml deleted file mode 100644 index 26ebb922f6..0000000000 --- a/.github/workflows/pr-updater.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: PR update - -on: - push: - branches: - - master - -jobs: - autoupdate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: PR updater - uses: maxkomarychev/pr-updater-action@v1.0.0 - with: - token: ${{ secrets.GH_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 98c81376c6..9780d62c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,67 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.0.0-alpha.4](https://github.com/HospitalRun/hospitalrun-frontend/compare/v2.0.0-alpha.3...v2.0.0-alpha.4) (2020-06-26) + + +### Features + +* **navbar:** add hamberger icon for mobile, and dividers ([1cf6575](https://github.com/HospitalRun/hospitalrun-frontend/commit/1cf657516e1249373c3e44247700d435473c7d59)) +* apply select component changes ([c38004b](https://github.com/HospitalRun/hospitalrun-frontend/commit/c38004bf9285a6cc53c36150754bd659130e5998)) +* **add new script for checking missing translations:** translations ([9ca5232](https://github.com/HospitalRun/hospitalrun-frontend/commit/9ca5232632503fbefb5ffae4e4d23a10d44fc62c)), closes [#1919](https://github.com/HospitalRun/hospitalrun-frontend/issues/1919) +* **care plan:** fix internationalization ([72a5bed](https://github.com/HospitalRun/hospitalrun-frontend/commit/72a5bede7862c72eecb068c0c20141ef619a8f27)) +* **care-plan:** add care plan form and tests ([e96eb83](https://github.com/HospitalRun/hospitalrun-frontend/commit/e96eb835c37c0d4174ee8a8878286fbabda309c8)) +* **careplan:** adds ability to add a new care plan ([0aa0cf9](https://github.com/HospitalRun/hospitalrun-frontend/commit/0aa0cf93b3f9e3153592684ce05babee2e0c5379)) +* **checkmissingtranslations.ts:** add language in logs ([d20b9c7](https://github.com/HospitalRun/hospitalrun-frontend/commit/d20b9c718dcb3f72142cb3280d779d7ef74f973e)) +* **checkmissingtranslations.ts:** change type of searchingPath ([7ab8195](https://github.com/HospitalRun/hospitalrun-frontend/commit/7ab81957277eec5368d6daea5e38f003a7f03255)) +* **checkmissingtranslations.ts:** refactor ([75f9981](https://github.com/HospitalRun/hospitalrun-frontend/commit/75f998103cde809054d01b5d999dba557952bb09)) +* **checkmissintranlations.ts:** add colors to log ([ede7b2e](https://github.com/HospitalRun/hospitalrun-frontend/commit/ede7b2eca4046bc93dc47c43a73ef9007f2f1594)) +* **checkmissintranslations.ts:** run script before default ones ([5ef3f7e](https://github.com/HospitalRun/hospitalrun-frontend/commit/5ef3f7ec702331bb1bea14cd51a46e48a2a31ca2)) +* **datepicker:** add year selector dropdown ([#2060](https://github.com/HospitalRun/hospitalrun-frontend/issues/2060)) ([b159d7a](https://github.com/HospitalRun/hospitalrun-frontend/commit/b159d7ada730976583d416b0f44d38a186abe4cd)) +* **i18n:** add italian translation ([#2035](https://github.com/HospitalRun/hospitalrun-frontend/issues/2035)) ([44f15f3](https://github.com/HospitalRun/hospitalrun-frontend/commit/44f15f3a12b3a4252f70c745b37a1140c732d976)) +* **incident:** Added Report Incident button ([43cafa6](https://github.com/HospitalRun/hospitalrun-frontend/commit/43cafa6da8334bf41440588ce71a27b42b73ef9d)) +* **incidents:** add ability to view an incident ([5887859](https://github.com/HospitalRun/hospitalrun-frontend/commit/5887859542247573843fc5af980cd081f6cc6f25)) +* **incidents:** add incident related routing ([2e9e985](https://github.com/HospitalRun/hospitalrun-frontend/commit/2e9e985c877db2f095ba11fb6900fc177283d5cc)) +* **incidents:** adds ability to report incident ([4a4a682](https://github.com/HospitalRun/hospitalrun-frontend/commit/4a4a6821838982f51b94ff050ff5e614a95d8839)) +* **incidents:** adds ability to view all incidents ([f11d8e9](https://github.com/HospitalRun/hospitalrun-frontend/commit/f11d8e90fe2752eba35fa2108e188053b89e6e8f)) +* **incidents:** filter incidents ([#2087](https://github.com/HospitalRun/hospitalrun-frontend/issues/2087)) ([5309a85](https://github.com/HospitalRun/hospitalrun-frontend/commit/5309a859a4b19617f44183b1d70d0f36af6206d8)) +* **labs:** ability to filter by status on labs screen ([#2033](https://github.com/HospitalRun/hospitalrun-frontend/issues/2033)) ([2b5c789](https://github.com/HospitalRun/hospitalrun-frontend/commit/2b5c789bb6247c3fe62fad8818d7fa1a16034a5b)) +* **labs:** add lab code ([#2040](https://github.com/HospitalRun/hospitalrun-frontend/issues/2040)) ([b695ac8](https://github.com/HospitalRun/hospitalrun-frontend/commit/b695ac899ea73e7059a50ad633bea90932431ff8)) +* **navbar:** add shortcut icon to the create pages ([b1a8cdf](https://github.com/HospitalRun/hospitalrun-frontend/commit/b1a8cdf3e3288f6084592f583407ea89cebd5134)) +* **network-status:** Notify users when they're working offline ([#2109](https://github.com/HospitalRun/hospitalrun-frontend/issues/2109)) ([fa5bcb6](https://github.com/HospitalRun/hospitalrun-frontend/commit/fa5bcb6a86ae082de789aca84a938bc7f99a8ca7)) +* **newlabrequest:** add requestBy to model ([c791de3](https://github.com/HospitalRun/hospitalrun-frontend/commit/c791de39fa8c67d875f8fb1aa99baf164691b5eb)), closes [#2082](https://github.com/HospitalRun/hospitalrun-frontend/issues/2082) +* **pagecomponent:** user can change page size ([7411ad0](https://github.com/HospitalRun/hospitalrun-frontend/commit/7411ad09ee680e5a691fdd34f1ca33c452398f1b)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* **patient:** add input validation ([#2032](https://github.com/HospitalRun/hospitalrun-frontend/issues/2032)) ([bb02fa2](https://github.com/HospitalRun/hospitalrun-frontend/commit/bb02fa20a23eab285c88cfe6ef16d28783d24ec6)) +* **patient:** multiple contact info ([#2113](https://github.com/HospitalRun/hospitalrun-frontend/issues/2113)) ([ceb96a4](https://github.com/HospitalRun/hospitalrun-frontend/commit/ceb96a4af48d4108babb7ff4f5d3e75bf74ec1fb)) +* **settings:** add navbar dropdown and settings page ([#2096](https://github.com/HospitalRun/hospitalrun-frontend/issues/2096)) ([e5677fe](https://github.com/HospitalRun/hospitalrun-frontend/commit/e5677fe459fe07637b0c9b3a2445296a89d16ee1)) +* **viewpatient:** added labs tab to ViewPatient ([#1987](https://github.com/HospitalRun/hospitalrun-frontend/issues/1987)) ([4a1c7ed](https://github.com/HospitalRun/hospitalrun-frontend/commit/4a1c7ed4a80265e55020f8b86fbec1aedf366330)) +* **viewpatients:** add a new field 'index', paging in next direction ([d1c55e7](https://github.com/HospitalRun/hospitalrun-frontend/commit/d1c55e7ab0bdcbe9a851751a747a6fe71714dc6a)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) [#1967](https://github.com/HospitalRun/hospitalrun-frontend/issues/1967) +* **viewpatients:** add paging feature in ViewPatients component ([dff2b3e](https://github.com/HospitalRun/hospitalrun-frontend/commit/dff2b3e44ee076f8154290fe9183106d6fe3f231)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* **viewpatients:** add paging for search patients ([b96680f](https://github.com/HospitalRun/hospitalrun-frontend/commit/b96680fbe1329384ed6eefa9e414803c781897dd)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* **viewpatients:** add Tests, Fix bug ([671ad02](https://github.com/HospitalRun/hospitalrun-frontend/commit/671ad02d6992727f73c777f673a6c305fd57b1b2)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* **viewpatients:** enables to navigation to previous page ([52a59d3](https://github.com/HospitalRun/hospitalrun-frontend/commit/52a59d3865444d37a2c605d2799346172fec2904)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* **viewpatients:** refactor code as recommended ([5308f5f](https://github.com/HospitalRun/hospitalrun-frontend/commit/5308f5fc7fa358d13797dee5e6a78b724795f31b)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) +* add Sort request in ViewPatients ([c4109a4](https://github.com/HospitalRun/hospitalrun-frontend/commit/c4109a470290843b39023e656db68845930174d0)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) + + +### Bug Fixes + +* **datetime:** datetime pickers are bigger now ([#2056](https://github.com/HospitalRun/hospitalrun-frontend/issues/2056)) ([52d30a3](https://github.com/HospitalRun/hospitalrun-frontend/commit/52d30a3ec1cc27c2ee2ec8827dce331180203461)) +* **eslint:** fix pouchdb test error ([f19d276](https://github.com/HospitalRun/hospitalrun-frontend/commit/f19d276bff9fa8aaf906cf0b8f1f97dcfa362e4a)) +* **i18n:** fix build ([30bb158](https://github.com/HospitalRun/hospitalrun-frontend/commit/30bb1587125e1d208ffb68e6f8ec9d4aab09a291)) +* **incidents:** add loading during fetch phase ([#2085](https://github.com/HospitalRun/hospitalrun-frontend/issues/2085)) ([d1fb940](https://github.com/HospitalRun/hospitalrun-frontend/commit/d1fb94078494a048f9d8aae5ac1b803f429dc07e)) +* **navbar:** Make Navbar mobile-friendly ([#2118](https://github.com/HospitalRun/hospitalrun-frontend/issues/2118)) ([391271a](https://github.com/HospitalRun/hospitalrun-frontend/commit/391271a23a3cdd66f27911aafb729c4add10c171)) +* **patient:** make note not required in care plan ([#2158](https://github.com/HospitalRun/hospitalrun-frontend/issues/2158)) ([34e6041](https://github.com/HospitalRun/hospitalrun-frontend/commit/34e6041cd051aa8ce98bdf359cc6874da1244096)) +* **sidebar:** sidebar should only show links user has access to see ([66feda1](https://github.com/HospitalRun/hospitalrun-frontend/commit/66feda1eefba9f663598f4f1d8f3f0484dfe38eb)), closes [#2110](https://github.com/HospitalRun/hospitalrun-frontend/issues/2110) +* **sidebar:** sidebar should only show links user has access to see ([2f22ebd](https://github.com/HospitalRun/hospitalrun-frontend/commit/2f22ebdab707028d17a1417a995aa57b40422ad3)), closes [#2110](https://github.com/HospitalRun/hospitalrun-frontend/issues/2110) +* **toolchain:** extends scripts tsconfig.json from base one ([6532028](https://github.com/HospitalRun/hospitalrun-frontend/commit/653202870b4eafad6f960136b63ef76639ca005c)), closes [#2113](https://github.com/HospitalRun/hospitalrun-frontend/issues/2113) +* **toolchain:** fix broken deps and updates translate-check script ([d0c35db](https://github.com/HospitalRun/hospitalrun-frontend/commit/d0c35db60aef95d0892fe7650960635eeda25df8)) +* standardize mock store setup in tests ([#2075](https://github.com/HospitalRun/hospitalrun-frontend/issues/2075)) ([50f9e49](https://github.com/HospitalRun/hospitalrun-frontend/commit/50f9e49ec1edc3bfdb8c2bbaed98bb8ceeb27f92)) +* standardized react router imports in tests and source code ([#2067](https://github.com/HospitalRun/hospitalrun-frontend/issues/2067)) ([cb3cea5](https://github.com/HospitalRun/hospitalrun-frontend/commit/cb3cea5d6e1c3c568ecd6536c55d8aa5e6d6e62d)) +* styling the Navbar to make it Sticky ([#2057](https://github.com/HospitalRun/hospitalrun-frontend/issues/2057)) ([11e4de8](https://github.com/HospitalRun/hospitalrun-frontend/commit/11e4de80fc2d36d5fdeacdf24466db14de10eb30)) +* **viewpatients:** call PatientRepository.findAll() only once ([#2044](https://github.com/HospitalRun/hospitalrun-frontend/issues/2044)) ([9084411](https://github.com/HospitalRun/hospitalrun-frontend/commit/9084411bc459abfd5e7003460ff2f1574cbbc243)) +* **viewpatients.tsx:** add userPageRequest in dependency array ([11b6c8b](https://github.com/HospitalRun/hospitalrun-frontend/commit/11b6c8be0ee643bea6c33250c8adb36b8dafbf8f)), closes [#1969](https://github.com/HospitalRun/hospitalrun-frontend/issues/1969) + ## [2.0.0-alpha.3](https://github.com/HospitalRun/hospitalrun-frontend/compare/v2.0.0-alpha.2...v2.0.0-alpha.3) (2020-05-02) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index cf8345253b..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM node:12-alpine as build - -ENV HOME=/home/app -COPY . $HOME/node/ - -WORKDIR $HOME/node -RUN npm install -q - -RUN npm run build -RUN npm prune --production - -FROM nginx:stable-alpine - -COPY --from=build /home/app/node/build/ /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 2c71128ab0..8d48abf70d 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,17 @@ React frontend for [HospitalRun](http://hospitalrun.io/): free software for deve +# Staging area + +You can follow developments by visiting the dedicated [staging environment](https://staging.hospitalrun.io). Use `username` / `password` as credentials to access. + # Contributing Contributions are always welcome. Before contributing please read our [contributor guide](https://github.com/HospitalRun/hospitalrun-frontend/blob/master/.github/CONTRIBUTING.md). -1. Fork this repository to your own GitHub account and then clone it to your local device -2. Navigate to the cloned folder: `cd hospitalrun-frontend` -3. Install the dependencies: `npm install` -4. Run `npm run start` to build and watch for code changes - ## Translation -Use the stadards in [this readme](https://github.com/HospitalRun/hospitalrun-frontend/tree/master/src/locales/README.md). +Use the standards in [this readme](https://github.com/HospitalRun/hospitalrun-frontend/tree/master/src/locales/README.md). ## Online one-click setup for contributing @@ -44,41 +43,6 @@ Contribute to HospitalRun using a fully featured online development environment [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/HospitalRun/hospitalrun-frontend) -## Connecting to HospitalRun Server - -**Note: The following instructions are for connecting to HospitalRun Server during development and are not intended to be for production use. For production deployments, see the deployment instructions.** - -1. Configure [HospitalRun Server](https://github.com/HospitalRun/hospitalrun-server) -2. Start the HospitalRun Development Server -3. Copy the `.env.example` file to `.env` -4. Change the `REACT_APP_HOSPITALRUN_API` variable to point to the HospitalRun Development Server. - -### Potential Setup Issues - -Some developers have reported the following errors and the corresponding fixes - -### Problem with Project Dependency Tree - -``` -There might be a problem with the project dependency tree. -It is likely not a bug in Create React App, but something you need to fix locally. -The react-scripts package provided by Create React App requires a dependency: - "babel-loader": "8.1.0" -Don't try to install it manually: your package manager does it automatically. -However, a different version of babel-loader was detected higher up in the tree: - /path/to/hospitalrun/node_modules/babel-loader (version: 8.0.6) -Manually installing incompatible versions is known to cause hard-to-debug issues. -If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project. -That will permanently disable this message but you might encounter other issues. -To fix the dependency tree, try following the steps below in the exact order: - 1. Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder. - 2. Delete node_modules in your project folder. - 3. Remove "babel-loader" from dependencies and/or devDependencies in the package.json file in your project folder. - 4. Run npm install or yarn, depending on the package manager you use. -``` - -To fix this issue, add `SKIP_PREFLIGHT_CHECK=true` to the `.env` file. - ## Running Tests and Linter `npm run test:ci` will run the entire test suite diff --git a/couchdb/Dockerfile b/couchdb/Dockerfile new file mode 100644 index 0000000000..8077e3b5ab --- /dev/null +++ b/couchdb/Dockerfile @@ -0,0 +1,3 @@ +FROM couchdb:3.1.0 + +COPY local.ini /opt/couchdb/etc/local.d/ diff --git a/couchdb/local.ini b/couchdb/local.ini new file mode 100644 index 0000000000..70c17e9c0f --- /dev/null +++ b/couchdb/local.ini @@ -0,0 +1,14 @@ +[couchdb] +single_node=true +users_db_security_editable = true + +[httpd] +enable_cors = true + +[cors] +origins = * +credentials = true + + +[chttpd] +bind_address = 0.0.0.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..0a24d922c9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +# Dockerc compose only for developing purpose + +version: "3.8" + +services: + couchdb: + build: + context: ./couchdb + dockerfile: Dockerfile + container_name: hr_couchdb + ports: + - "5984:5984" + environment: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=password + + dbinit: + image: curlimages/curl + command: > + sh -c + "sleep 5s && + curl -X PUT http://admin:password@couchdb:5984/_global_changes && + curl -X PUT http://admin:password@couchdb:5984/_users/_security -d '{}' && + curl -X PUT http://admin:password@couchdb:5984/hospitalrun?partitioned=false" diff --git a/package.json b/package.json index da12935cb6..2762c90f73 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,41 @@ { "name": "@hospitalrun/frontend", - "version": "2.0.0-alpha.3", + "version": "2.0.0-alpha.4", "description": "React frontend for HospitalRun", "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~1.6.0", - "@reduxjs/toolkit": "~1.3.0", + "@hospitalrun/components": "~1.14.2", + "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", "date-fns": "~2.14.0", "escape-string-regexp": "~4.0.0", - "i18next": "~19.4.0", - "i18next-browser-languagedetector": "~4.3.0", + "i18next": "~19.5.0", + "i18next-browser-languagedetector": "~5.0.0", "i18next-xhr-backend": "~3.2.2", "lodash": "^4.17.15", "node-sass": "~4.14.0", "pouchdb": "~7.2.1", "pouchdb-adapter-memory": "~7.2.1", + "pouchdb-authentication": "~1.1.3", "pouchdb-find": "~7.2.1", "pouchdb-quick-search": "~1.3.0", "react": "~16.13.0", "react-bootstrap": "~1.0.0-beta.16", - "react-bootstrap-typeahead": "~4.2.0", + "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", - "react-i18next": "~11.5.0", + "react-i18next": "~11.7.0", "react-redux": "~7.2.0", - "react-router": "~5.1.2", - "react-router-dom": "~5.1.2", + "react-router": "~5.2.0", + "react-router-dom": "~5.2.0", "react-scripts": "~3.4.0", "redux": "~4.0.5", "redux-thunk": "~2.3.0", + "relational-pouch": "~4.0.0", "shortid": "^2.2.15", - "typescript": "~3.8.2", + "typescript": "~3.8.3", "uuid": "^8.0.0", "validator": "^13.0.0" }, @@ -54,10 +56,10 @@ ], "devDependencies": { "@commitlint/cli": "~8.3.5", - "@commitlint/config-conventional": "~8.3.4", - "@commitlint/core": "~8.3.5", - "@commitlint/prompt": "~8.3.5", - "@testing-library/react": "~10.1.0", + "@commitlint/config-conventional": "~9.0.1", + "@commitlint/core": "~9.0.1", + "@commitlint/prompt": "~9.0.1", + "@testing-library/react": "~10.4.0", "@testing-library/react-hooks": "~3.3.0", "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", @@ -72,9 +74,9 @@ "@types/redux-mock-store": "~1.0.1", "@types/shortid": "^0.0.29", "@types/uuid": "^8.0.0", - "@types/validator": "~13.0.0", - "@typescript-eslint/eslint-plugin": "~2.34.0", - "@typescript-eslint/parser": "~2.34.0", + "@types/validator": "~13.1.0", + "@typescript-eslint/eslint-plugin": "~3.5.0", + "@typescript-eslint/parser": "~3.5.0", "chalk": "^4.0.0", "commitizen": "~4.1.2", "commitlint-config-cz": "~0.13.0", @@ -84,40 +86,45 @@ "enzyme": "~3.11.0", "enzyme-adapter-react-16": "~1.15.2", "eslint": "~6.8.0", - "eslint-config-airbnb": "~18.1.0", + "eslint-config-airbnb": "~18.2.0", "eslint-config-prettier": "~6.11.0", "eslint-import-resolver-typescript": "~2.0.0", - "eslint-plugin-import": "~2.21.1", - "eslint-plugin-jest": "~23.13.0", - "eslint-plugin-jsx-a11y": "~6.2.3", + "eslint-plugin-import": "~2.22.0", + "eslint-plugin-jest": "~23.17.1", + "eslint-plugin-jsx-a11y": "~6.3.0", "eslint-plugin-prettier": "~3.1.2", "eslint-plugin-react": "~7.20.0", - "eslint-plugin-react-hooks": "~2.5.0", - "history": "~4.10.1", + "eslint-plugin-react-hooks": "~4.0.4", + "history": "4.10.1", "husky": "~4.2.1", "jest": "~24.9.0", "lint-staged": "~10.2.0", "memdown": "~5.1.0", "prettier": "~2.0.4", "redux-mock-store": "~1.5.4", + "rimraf": "~3.0.2", "source-map-explorer": "^2.2.2", "standard-version": "~8.0.0", - "ts-jest": "~24.3.0" + "ts-jest": "~26.1.0" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "commit": "npx git-cz", - "start": "yarn translation:check && react-scripts start", + "start": "npm run translation:check && react-scripts start", "build": "react-scripts build", + "update": "npx npm-check -u", "prepublishOnly": "npm run build", - "test": "yarn translation:check && react-scripts test --detectOpenHandles", + "test": "npm run translation:check && react-scripts test --detectOpenHandles", "test:ci": "cross-env CI=true react-scripts test --passWithNoTests", - "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"scripts/**/*.{js,ts}\"", - "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"scripts/**/*.{js,ts}\" --fix", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"scripts/check-translations/**/*.{js,ts}\"", + "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \"scripts/check-translations/**/*.{js,ts}\" --fix", "lint-staged": "lint-staged", "commitlint": "commitlint", "coveralls": "npm run test:ci -- --coverage --watchAll=false && cat ./coverage/lcov.info", - "translation:check": "cd scripts && tsc && node ../bin/scripts/checkMissingTranslations.js" + "remove-compiled-translations": "rimraf ./bin/scripts/check-translations", + "pretranslation:check": "npm run remove-compiled-translations", + "translation:check": "tsc -p scripts/tsconfig.json && node ./bin/scripts/check-translations/index.js", + "posttranslation:check": "npm run remove-compiled-translations" }, "browserslist": { "production": [ diff --git a/public/logo.png b/public/logo.png index 0dc826d022..de56866544 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js index c6ef8183ad..56bb1d0620 100644 --- a/scripts/.eslintrc.js +++ b/scripts/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { extends: '../.eslintrc.js', rules: { - "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] - } + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + }, } diff --git a/scripts/checkMissingTranslations.ts b/scripts/check-translations/index.ts similarity index 97% rename from scripts/checkMissingTranslations.ts rename to scripts/check-translations/index.ts index 222131cb65..dbca8fb590 100644 --- a/scripts/checkMissingTranslations.ts +++ b/scripts/check-translations/index.ts @@ -1,7 +1,7 @@ import chalk from 'chalk' import { ResourceKey } from 'i18next' -import resources from '../src/locales' +import resources from '../../src/shared/locales' const error = chalk.bold.red const warning = chalk.keyword('orange') diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index f1141d0082..55e2b73017 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,8 +1,9 @@ { - "include": ["./checkMissingTranslations.ts"], + "include": ["./**/*.ts"], "compilerOptions": { "module": "commonjs", "outDir": "../bin", - "target": "es5" + "target": "es5", + "esModuleInterop": true } } diff --git a/src/App.tsx b/src/App.tsx index 5d69f48774..61ffb3a15a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,23 @@ import { Spinner } from '@hospitalrun/components' import React, { Suspense } from 'react' import { Provider } from 'react-redux' -import { BrowserRouter } from 'react-router-dom' +import { BrowserRouter, Route, Switch } from 'react-router-dom' import HospitalRun from './HospitalRun' -import store from './store' +import Login from './login/Login' +import store from './shared/store' const App: React.FC = () => (
- }> - - - - + + }> + + + + + +
) diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index b3f9cadcdf..6e9a9b406c 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -1,26 +1,30 @@ import { Toaster } from '@hospitalrun/components' import React from 'react' import { useSelector } from 'react-redux' -import { Switch, Route } from 'react-router-dom' +import { Redirect, Route, Switch } from 'react-router-dom' -import Breadcrumbs from './breadcrumbs/Breadcrumbs' -import Navbar from './components/Navbar' -import { NetworkStatusMessage } from './components/network-status' -import PrivateRoute from './components/PrivateRoute' -import Sidebar from './components/Sidebar' import Dashboard from './dashboard/Dashboard' import Incidents from './incidents/Incidents' import Labs from './labs/Labs' -import { ButtonBarProvider } from './page-header/ButtonBarProvider' -import ButtonToolBar from './page-header/ButtonToolBar' +import Breadcrumbs from './page-header/breadcrumbs/Breadcrumbs' +import { ButtonBarProvider } from './page-header/button-toolbar/ButtonBarProvider' +import ButtonToolBar from './page-header/button-toolbar/ButtonToolBar' import Patients from './patients/Patients' import Appointments from './scheduling/appointments/Appointments' import Settings from './settings/Settings' -import { RootState } from './store' +import Navbar from './shared/components/navbar/Navbar' +import { NetworkStatusMessage } from './shared/components/network-status' +import Sidebar from './shared/components/Sidebar' +import { RootState } from './shared/store' const HospitalRun = () => { const { title } = useSelector((state: RootState) => state.title) const { sidebarCollapsed } = useSelector((state: RootState) => state.components) + const { user } = useSelector((root: RootState) => root.user) + + if (user === undefined) { + return + } return (
@@ -44,11 +48,11 @@ const HospitalRun = () => {
- - - - - + + + + +
diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index f3379a0293..e726884bbd 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -1,12 +1,9 @@ -import '../__mocks__/matchMediaMock' - -import { mount } from 'enzyme' +import { shallow } from 'enzyme' import React from 'react' import App from '../App' -import HospitalRun from '../HospitalRun' it('renders without crashing', () => { - const wrapper = mount() - expect(wrapper.find(HospitalRun)).toHaveLength(1) + const wrapper = shallow() + expect(wrapper).toBeDefined() }) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 31a05117b3..ef18ef3bb2 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -1,5 +1,3 @@ -import '../__mocks__/matchMediaMock' - import { Toaster } from '@hospitalrun/components' import { mount } from 'enzyme' import React from 'react' @@ -9,16 +7,16 @@ import { MemoryRouter } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import { addBreadcrumbs } from '../breadcrumbs/breadcrumbs-slice' -import LabRepository from '../clients/db/LabRepository' import Dashboard from '../dashboard/Dashboard' import HospitalRun from '../HospitalRun' import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' -import Permissions from '../model/Permissions' +import { addBreadcrumbs } from '../page-header/breadcrumbs/breadcrumbs-slice' import Appointments from '../scheduling/appointments/Appointments' import Settings from '../settings/Settings' -import { RootState } from '../store' +import LabRepository from '../shared/db/LabRepository' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' const mockStore = createMockStore([thunk]) @@ -28,7 +26,7 @@ describe('HospitalRun', () => { it('should render the appointments screen when /appointments is accessed', async () => { const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ReadAppointments] }, + user: { user: { id: '123' }, permissions: [Permissions.ReadAppointments] }, appointments: { appointments: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -62,7 +60,7 @@ describe('HospitalRun', () => { { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ViewLabs] }, + user: { user: { id: '123' }, permissions: [Permissions.ViewLabs] }, labs: { labs: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -107,7 +105,7 @@ describe('HospitalRun', () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) const store = mockStore({ title: 'test', - user: { permissions: [] }, + user: { user: { id: '123' }, permissions: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) @@ -129,7 +127,7 @@ describe('HospitalRun', () => { it('should render the Incidents component when /incidents is accessed', async () => { const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ViewIncidents] }, + user: { user: { id: '123' }, permissions: [Permissions.ViewIncidents] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, incidents: { incidents: [] }, @@ -154,7 +152,7 @@ describe('HospitalRun', () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) const store = mockStore({ title: 'test', - user: { permissions: [] }, + user: { user: { id: '123' }, permissions: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) @@ -176,7 +174,7 @@ describe('HospitalRun', () => { it('should render the Settings component when /settings is accessed', async () => { const store = mockStore({ title: 'test', - user: { permissions: [] }, + user: { user: { id: '123' }, permissions: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) @@ -200,7 +198,7 @@ describe('HospitalRun', () => { { - if (row.doc) { - await patients.remove(row.doc) - } - }), - ) -} - -describe('patient repository', () => { - describe('find', () => { - afterEach(async () => { - await removeAllDocs() - }) - it('should return a patient with the correct data', async () => { - await patients.put({ _id: 'id1111' }) // store another patient just to make sure we pull back the right one - const expectedPatient = await patients.put({ _id: 'id2222' }) - - const actualPatient = await PatientRepository.find('id2222') - - expect(actualPatient).toBeDefined() - expect(actualPatient.id).toEqual(expectedPatient.id) - }) - }) - - describe('search', () => { - afterEach(async () => { - await removeAllDocs() - }) - - it('should escape all special chars from search text', async () => { - await patients.put({ _id: 'id9999', code: 'P00001', fullName: 'test -]?}(){*[\\$+.^test' }) - - const result = await PatientRepository.search('test -]?}(){*[\\$+.^test') - - expect(result).toHaveLength(1) - expect(result[0].id).toEqual('id9999') - }) - - it('should return all records that patient code matches search text', async () => { - // same full name to prove that it is finding by patient code - const expectedPatientCode = 'P00001' - await patients.put({ _id: 'someId1', code: expectedPatientCode, fullName: 'test test' }) - await patients.put({ _id: 'someId2', code: 'P00002', fullName: 'test test' }) - - const result = await PatientRepository.search(expectedPatientCode) - - expect(result).toHaveLength(1) - expect(result[0].code).toEqual(expectedPatientCode) - }) - - it('should return all records that fullName contains search text', async () => { - await patients.put({ _id: 'id3333', code: 'P00002', fullName: 'blh test test blah' }) - await patients.put({ _id: 'id4444', code: 'P00001', fullName: 'test test' }) - await patients.put({ _id: 'id5555', code: 'P00003', fullName: 'not found' }) - - const result = await PatientRepository.search('test test') - - expect(result).toHaveLength(2) - expect(result[0].id).toEqual('id3333') - expect(result[1].id).toEqual('id4444') - }) - - it('should match search criteria with case insensitive match', async () => { - await patients.put({ _id: 'id6666', code: 'P00001', fullName: 'test test' }) - await patients.put({ _id: 'id7777', code: 'P00002', fullName: 'not found' }) - - const result = await PatientRepository.search('TEST TEST') - - expect(result).toHaveLength(1) - expect(result[0].id).toEqual('id6666') - }) - }) - - describe('findAll', () => { - afterEach(async () => { - await removeAllDocs() - }) - it('should find all patients in the database sorted by their ids', async () => { - const expectedPatient1 = await patients.put({ _id: 'id9999' }) - const expectedPatient2 = await patients.put({ _id: 'id8888' }) - - const result = await PatientRepository.findAll() - - expect(result).toHaveLength(2) - expect(result[0].id).toEqual(expectedPatient2.id) - expect(result[1].id).toEqual(expectedPatient1.id) - }) - }) - - describe('save', () => { - afterEach(async () => { - await removeAllDocs() - }) - - it('should generate an id that is a uuid for the patient', async () => { - const newPatient = await PatientRepository.save({ - fullName: 'test test', - } as Patient) - - expect(uuidV4Regex.test(newPatient.id)).toBeTruthy() - }) - - it('should generate a patient code', async () => { - const newPatient = await PatientRepository.save({ - fullName: 'test1 test1', - } as Patient) - - expect(shortid.isValid(newPatient.code)).toBeTruthy() - }) - - it('should generate a timestamp for created date and last updated date', async () => { - const newPatient = await PatientRepository.save({ - fullName: 'test1 test1', - } as Patient) - - expect(newPatient.createdAt).toBeDefined() - expect(newPatient.updatedAt).toBeDefined() - }) - - it('should override the created date and last updated date even if one was passed in', async () => { - const unexpectedTime = new Date(2020, 2, 1).toISOString() - const newPatient = await PatientRepository.save({ - fullName: 'test1 test1', - createdAt: unexpectedTime, - updatedAt: unexpectedTime, - } as Patient) - - expect(newPatient.createdAt).not.toEqual(unexpectedTime) - expect(newPatient.updatedAt).not.toEqual(unexpectedTime) - }) - }) - - describe('saveOrUpdate', () => { - afterEach(async () => { - await removeAllDocs() - }) - - it('should save the patient if an id was not on the entity', async () => { - const newPatient = await PatientRepository.saveOrUpdate({ - fullName: 'test4 test4', - } as Patient) - - expect(newPatient.id).toBeDefined() - }) - - it('should update the patient if one was already existing', async () => { - const existingPatient = await PatientRepository.save({ - fullName: 'test5 test5', - } as Patient) - - const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) - - expect(updatedPatient.id).toEqual(existingPatient.id) - }) - - it('should update the existing fields', async () => { - const existingPatient = await PatientRepository.save({ - fullName: 'test6 test6', - } as Patient) - existingPatient.fullName = 'changed' - - const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) - - expect(updatedPatient.fullName).toEqual('changed') - }) - - it('should add new fields without changing existing fields', async () => { - const existingPatient = await PatientRepository.save({ - fullName: 'test7 test7', - } as Patient) - existingPatient.givenName = 'givenName' - - const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) - - expect(updatedPatient.fullName).toEqual(existingPatient.fullName) - expect(updatedPatient.givenName).toEqual('givenName') - }) - - it('should update the last updated date', async () => { - const time = new Date(2020, 1, 1).toISOString() - await patients.put({ _id: 'id2222222', createdAt: time, updatedAt: time }) - const existingPatient = await PatientRepository.find('id2222222') - - const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) - - expect( - isAfter(new Date(updatedPatient.updatedAt), new Date(updatedPatient.createdAt)), - ).toBeTruthy() - expect(updatedPatient.updatedAt).not.toEqual(existingPatient.updatedAt) - }) - - it('should not update the created date', async () => { - const time = getTime(new Date(2020, 1, 1)) - await patients.put({ _id: 'id111111', createdAt: time, updatedAt: time }) - const existingPatient = await PatientRepository.find('id111111') - const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) - - expect(updatedPatient.createdAt).toEqual(existingPatient.createdAt) - }) - }) - - describe('delete', () => { - it('should delete the patient', async () => { - const patientToDelete = await PatientRepository.save({ - fullName: 'test8 test8', - } as Patient) - - await PatientRepository.delete(patientToDelete) - - const allDocs = await patients.allDocs() - expect(allDocs.total_rows).toEqual(0) - }) - }) - - describe('findAllPaged', () => { - const patientsData = [ - { _id: 'a', fullName: 'a', code: 'P-a', index: 'aP-a' }, - { _id: 'b', fullName: 'b', code: 'P-b', index: 'bP-b' }, - { _id: 'c', fullName: 'c', code: 'P-c', index: 'cP-c' }, - ] - - afterEach(async () => { - await removeAllDocs() - }) - - beforeEach(async () => { - await PatientRepository.createIndex() - patientsData.forEach((patientData) => patients.put(patientData)) - }) - - it('should find all patients in the database', async () => { - const result = await PatientRepository.findAllPaged() - - expect(result.hasNext).toEqual(false) - expect(result.hasPrevious).toEqual(false) - expect(result.content).toHaveLength(patientsData.length) - }) - - it('should find all patients in the database with sort request and page request', async () => { - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - const pageRequest: PageRequest = { - number: 1, - size: 1, - direction: 'next', - nextPageInfo: undefined, - previousPageInfo: undefined, - } - - const result = await PatientRepository.findAllPaged(sortRequest, pageRequest) - - expect(result.content).toHaveLength(1) - expect(result.hasNext).toEqual(true) - expect(result.hasPrevious).toEqual(false) - expect(result.pageRequest?.nextPageInfo).toEqual({ index: 'bP-b' }) - }) - - it('page request less than number of records', async () => { - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - - const pageRequest: PageRequest = { - number: 1, - size: 4, - direction: 'next', - nextPageInfo: undefined, - previousPageInfo: undefined, - } - - const result = await PatientRepository.findAllPaged(sortRequest, pageRequest) - - expect(result.content).toHaveLength(patientsData.length) - expect(result.hasNext).toEqual(false) - expect(result.hasPrevious).toEqual(false) - expect(result.pageRequest?.nextPageInfo).toBe(undefined) - expect(result.pageRequest?.previousPageInfo).toBe(undefined) - }) - - it('go till last page', async () => { - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - const pageRequest1: PageRequest = { - number: 1, - size: 1, - direction: 'next', - nextPageInfo: undefined, - previousPageInfo: undefined, - } - - const result1 = await PatientRepository.findAllPaged(sortRequest, pageRequest1) - - const pageRequest2: PageRequest = { - number: 2, - size: 1, - direction: 'next', - nextPageInfo: result1.pageRequest?.nextPageInfo, - previousPageInfo: undefined, - } - const result2 = await PatientRepository.findAllPaged(sortRequest, pageRequest2) - - expect(result2.hasPrevious).toBe(true) - expect(result2.hasNext).toBe(true) - - const pageRequest3: PageRequest = { - number: 2, - size: 1, - direction: 'next', - nextPageInfo: result2.pageRequest?.nextPageInfo, - previousPageInfo: undefined, - } - const result3 = await PatientRepository.findAllPaged(sortRequest, pageRequest3) - - expect(result3.content).toHaveLength(1) - expect(result3.hasNext).toEqual(false) - expect(result3.hasPrevious).toEqual(true) - expect(result3.content.length).toEqual(1) - expect(result3.pageRequest?.previousPageInfo).toEqual({ index: 'cP-c' }) - }) - - it('go to previous page', async () => { - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - const pageRequest1: PageRequest = { - number: 1, - size: 1, - direction: 'next', - nextPageInfo: undefined, - previousPageInfo: undefined, - } - - const result1 = await PatientRepository.findAllPaged(sortRequest, pageRequest1) - - const pageRequest2: PageRequest = { - number: 2, - size: 1, - direction: 'next', - nextPageInfo: result1.pageRequest?.nextPageInfo, - previousPageInfo: undefined, - } - const result2 = await PatientRepository.findAllPaged(sortRequest, pageRequest2) - - expect(result2.hasPrevious).toBe(true) - expect(result2.hasNext).toBe(true) - - const pageRequest3: PageRequest = { - number: 1, - size: 1, - direction: 'previous', - nextPageInfo: undefined, - previousPageInfo: result2.pageRequest?.previousPageInfo, - } - const result3 = await PatientRepository.findAllPaged(sortRequest, pageRequest3) - - expect(result3.content).toHaveLength(1) - expect(result3.hasNext).toEqual(true) - expect(result3.hasPrevious).toEqual(false) - expect(result3.content.length).toEqual(1) - expect(result3.content[0].index).toEqual('aP-a') - expect(result3.pageRequest?.nextPageInfo).toEqual({ index: 'bP-b' }) - }) - }) - - describe('searchPaged', () => { - const patientsData = [ - { _id: 'a', fullName: 'a', code: 'P-a', index: 'aP-a' }, - { _id: 'b', fullName: 'b', code: 'P-b', index: 'bP-b' }, - { _id: 'c', fullName: 'c', code: 'P-c', index: 'cP-c' }, - ] - - afterEach(async () => { - await removeAllDocs() - }) - - beforeEach(async () => { - await PatientRepository.createIndex() - patientsData.forEach((patientData) => patients.put(patientData)) - }) - - it('should search patient in the database', async () => { - const result = await PatientRepository.searchPaged('a') - - expect(result.content).toHaveLength(1) - expect(result.hasNext).toEqual(false) - expect(result.hasPrevious).toEqual(false) - - expect(result.content.length).toEqual(1) - }) - - it('should search patient in the database with sort request', async () => { - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - - const result = await PatientRepository.searchPaged('a', UnpagedRequest, sortRequest) - - expect(result.content).toHaveLength(1) - expect(result.hasNext).toEqual(false) - expect(result.hasPrevious).toEqual(false) - - expect(result.content.length).toEqual(1) - }) - }) -}) diff --git a/src/__tests__/components/PageComponent.test.tsx b/src/__tests__/components/PageComponent.test.tsx deleted file mode 100644 index fa1bf5a4ef..0000000000 --- a/src/__tests__/components/PageComponent.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import '../../__mocks__/matchMediaMock' -import { Button, Select } from '@hospitalrun/components' -import { mount } from 'enzyme' -import React from 'react' - -import PageComponent, { defaultPageSize } from '../../components/PageComponent' - -describe('PageComponenet test', () => { - it('should render PageComponent Component', () => { - const wrapper = mount( - , - ) - const buttons = wrapper.find(Button) - expect(buttons).toHaveLength(2) - expect(buttons.at(0).prop('disabled')).toBeTruthy() - expect(buttons.at(1).prop('disabled')).toBeTruthy() - - const select = wrapper.find(Select) - expect(select.prop('defaultValue')).toEqual(defaultPageSize.value?.toString()) - - const options = select.find('option') - expect(options).toHaveLength(5) - }) -}) diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx index ecc4626461..7474948481 100644 --- a/src/__tests__/incidents/Incidents.test.tsx +++ b/src/__tests__/incidents/Incidents.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { act } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' @@ -8,13 +6,13 @@ import { MemoryRouter } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import IncidentRepository from '../../clients/db/IncidentRepository' import Incidents from '../../incidents/Incidents' import ReportIncident from '../../incidents/report/ReportIncident' import ViewIncident from '../../incidents/view/ViewIncident' -import Incident from '../../model/Incident' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import IncidentRepository from '../../shared/db/IncidentRepository' +import Incident from '../../shared/model/Incident' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/incidents/incident-slice.test.ts b/src/__tests__/incidents/incident-slice.test.ts index 103462c9e3..416208e30e 100644 --- a/src/__tests__/incidents/incident-slice.test.ts +++ b/src/__tests__/incidents/incident-slice.test.ts @@ -4,7 +4,6 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import shortid from 'shortid' -import IncidentRepository from '../../clients/db/IncidentRepository' import incident, { reportIncidentStart, reportIncidentSuccess, @@ -14,10 +13,11 @@ import incident, { fetchIncidentSuccess, fetchIncident, } from '../../incidents/incident-slice' -import Incident from '../../model/Incident' -import Permissions from '../../model/Permissions' -import User from '../../model/User' -import { RootState } from '../../store' +import IncidentRepository from '../../shared/db/IncidentRepository' +import Incident from '../../shared/model/Incident' +import Permissions from '../../shared/model/Permissions' +import User from '../../shared/model/User' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/incidents/incidents-slice.test.ts b/src/__tests__/incidents/incidents-slice.test.ts index 47dd1b4656..031aa32f49 100644 --- a/src/__tests__/incidents/incidents-slice.test.ts +++ b/src/__tests__/incidents/incidents-slice.test.ts @@ -2,15 +2,15 @@ import { AnyAction } from 'redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import IncidentRepository from '../../clients/db/IncidentRepository' import IncidentFilter from '../../incidents/IncidentFilter' import incidents, { fetchIncidentsStart, fetchIncidentsSuccess, searchIncidents, } from '../../incidents/incidents-slice' -import Incident from '../../model/Incident' -import { RootState } from '../../store' +import IncidentRepository from '../../shared/db/IncidentRepository' +import Incident from '../../shared/model/Incident' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx index ab3dff4db2..99d2808265 100644 --- a/src/__tests__/incidents/list/ViewIncidents.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -1,7 +1,6 @@ -import '../../../__mocks__/matchMediaMock' - +import { Table } from '@hospitalrun/components' import { act } from '@testing-library/react' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { Provider } from 'react-redux' @@ -9,15 +8,15 @@ import { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' -import IncidentRepository from '../../../clients/db/IncidentRepository' import IncidentFilter from '../../../incidents/IncidentFilter' import ViewIncidents from '../../../incidents/list/ViewIncidents' -import Incident from '../../../model/Incident' -import Permissions from '../../../model/Permissions' -import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' -import * as titleUtil from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import IncidentRepository from '../../../shared/db/IncidentRepository' +import Incident from '../../../shared/model/Incident' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -72,29 +71,15 @@ describe('View Incidents', () => { ) }) wrapper.update() - return wrapper + return wrapper as ReactWrapper } it('should filter incidents by status=reported on first load ', async () => { - const wrapper = await setup([Permissions.ViewIncidents]) - const filterSelect = wrapper.find('select') - expect(filterSelect.props().value).toBe(IncidentFilter.reported) + await setup([Permissions.ViewIncidents]) expect(IncidentRepository.search).toHaveBeenCalled() expect(IncidentRepository.search).toHaveBeenCalledWith({ status: IncidentFilter.reported }) }) - it('should call IncidentRepository after changing filter', async () => { - const wrapper = await setup([Permissions.ViewIncidents]) - const filterSelect = wrapper.find('select') - - expect(IncidentRepository.findAll).not.toHaveBeenCalled() - - filterSelect.simulate('change', { target: { value: IncidentFilter.all } }) - expect(IncidentRepository.findAll).toHaveBeenCalled() - filterSelect.simulate('change', { target: { value: IncidentFilter.reported } }) - expect(IncidentRepository.search).toHaveBeenCalledTimes(2) - expect(IncidentRepository.search).toHaveBeenLastCalledWith({ status: IncidentFilter.reported }) - }) describe('layout', () => { it('should set the title', async () => { await setup([Permissions.ViewIncidents]) @@ -104,36 +89,40 @@ describe('View Incidents', () => { it('should render a table with the incidents', async () => { const wrapper = await setup([Permissions.ViewIncidents]) + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'incidents.reports.code', key: 'code' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'incidents.reports.dateOfIncident', key: 'date' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'incidents.reports.reportedBy', key: 'reportedBy' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'incidents.reports.reportedOn', key: 'reportedOn' }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'incidents.reports.status', key: 'status' }), + ) - const table = wrapper.find('table') - const tableHeader = table.find('thead') - const tableBody = table.find('tbody') - const tableHeaders = tableHeader.find('th') - const tableColumns = tableBody.find('td') - - expect(tableHeaders.at(0).text()).toEqual('incidents.reports.code') - expect(tableHeaders.at(1).text()).toEqual('incidents.reports.dateOfIncident') - expect(tableHeaders.at(2).text()).toEqual('incidents.reports.reportedBy') - expect(tableHeaders.at(3).text()).toEqual('incidents.reports.reportedOn') - expect(tableHeaders.at(4).text()).toEqual('incidents.reports.status') - - expect(tableColumns.at(0).text()).toEqual(expectedIncidents[0].code) - expect(tableColumns.at(1).text()).toEqual('2020-06-03 07:48 PM') - expect(tableColumns.at(2).text()).toEqual(expectedIncidents[0].reportedBy) - expect(tableColumns.at(3).text()).toEqual('2020-06-03 07:48 PM') - expect(tableColumns.at(4).text()).toEqual(expectedIncidents[0].status) + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(expectedIncidents) }) }) - describe('on table row click', () => { + describe('on view button click', () => { it('should navigate to the incident when the table row is clicked', async () => { const wrapper = await setup([Permissions.ViewIncidents]) const tr = wrapper.find('tr').at(1) act(() => { - const onClick = tr.prop('onClick') - onClick() + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) }) expect(history.location.pathname).toEqual(`/incidents/${expectedIncidents[0].id}`) diff --git a/src/__tests__/incidents/report/ReportIncident.test.tsx b/src/__tests__/incidents/report/ReportIncident.test.tsx index 51a301ae30..12b8c6f4ee 100644 --- a/src/__tests__/incidents/report/ReportIncident.test.tsx +++ b/src/__tests__/incidents/report/ReportIncident.test.tsx @@ -1,8 +1,6 @@ -import '../../../__mocks__/matchMediaMock' - import { Button } from '@hospitalrun/components' import { act } from '@testing-library/react' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { Provider } from 'react-redux' @@ -10,13 +8,14 @@ import { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' -import IncidentRepository from '../../../clients/db/IncidentRepository' import ReportIncident from '../../../incidents/report/ReportIncident' -import Permissions from '../../../model/Permissions' -import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' -import * as titleUtil from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import IncidentRepository from '../../../shared/db/IncidentRepository' +import Incident from '../../../shared/model/Incident' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -61,7 +60,7 @@ describe('Report Incident', () => { ) }) wrapper.update() - return wrapper + return wrapper as ReactWrapper } describe('layout', () => { @@ -179,7 +178,7 @@ describe('Report Incident', () => { category: 'some category', categoryItem: 'some category item', description: 'some description', - } + } as Incident jest .spyOn(IncidentRepository, 'save') .mockResolvedValue({ id: 'someId', ...expectedIncident }) @@ -217,7 +216,7 @@ describe('Report Incident', () => { const saveButton = wrapper.find(Button).at(0) await act(async () => { - const onClick = saveButton.prop('onClick') + const onClick = saveButton.prop('onClick') as any onClick() }) diff --git a/src/__tests__/incidents/view/ViewIncident.test.tsx b/src/__tests__/incidents/view/ViewIncident.test.tsx index d8bdeae1aa..9b2d77798d 100644 --- a/src/__tests__/incidents/view/ViewIncident.test.tsx +++ b/src/__tests__/incidents/view/ViewIncident.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { act } from '@testing-library/react' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -9,14 +7,14 @@ import { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' -import IncidentRepository from '../../../clients/db/IncidentRepository' import ViewIncident from '../../../incidents/view/ViewIncident' -import Incident from '../../../model/Incident' -import Permissions from '../../../model/Permissions' -import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' -import * as titleUtil from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import IncidentRepository from '../../../shared/db/IncidentRepository' +import Incident from '../../../shared/model/Incident' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/labs/Labs.test.tsx b/src/__tests__/labs/Labs.test.tsx index 9489b30f8c..2dde1714c5 100644 --- a/src/__tests__/labs/Labs.test.tsx +++ b/src/__tests__/labs/Labs.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { act } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' @@ -8,15 +6,15 @@ import { MemoryRouter } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../clients/db/LabRepository' -import PatientRepository from '../../clients/db/PatientRepository' import Labs from '../../labs/Labs' import NewLabRequest from '../../labs/requests/NewLabRequest' import ViewLab from '../../labs/ViewLab' -import Lab from '../../model/Lab' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import LabRepository from '../../shared/db/LabRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/labs/ViewLab.test.tsx b/src/__tests__/labs/ViewLab.test.tsx index f7c0963aea..a8968a897c 100644 --- a/src/__tests__/labs/ViewLab.test.tsx +++ b/src/__tests__/labs/ViewLab.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { Badge, Button, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import format from 'date-fns/format' @@ -11,27 +9,27 @@ import { Router, Route } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../clients/db/LabRepository' -import PatientRepository from '../../clients/db/PatientRepository' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' import ViewLab from '../../labs/ViewLab' -import Lab from '../../model/Lab' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import * as ButtonBarProvider from '../../page-header/ButtonBarProvider' -import * as titleUtil from '../../page-header/useTitle' -import { RootState } from '../../store' +import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../page-header/title/useTitle' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import LabRepository from '../../shared/db/LabRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) -describe('View Labs', () => { +describe('View Lab', () => { let history: any const mockPatient = { fullName: 'test' } const mockLab = { code: 'L-1234', id: '12456', status: 'requested', - patientId: '1234', + patient: '1234', type: 'lab type', notes: 'lab notes', requestedOn: '2020-03-30T04:43:20.102Z', diff --git a/src/__tests__/labs/ViewLabs.test.tsx b/src/__tests__/labs/ViewLabs.test.tsx index 2c3eae5a1b..fefdea666f 100644 --- a/src/__tests__/labs/ViewLabs.test.tsx +++ b/src/__tests__/labs/ViewLabs.test.tsx @@ -1,8 +1,5 @@ -import '../../__mocks__/matchMediaMock' - -import { TextInput, Select } from '@hospitalrun/components' +import { TextInput, Select, Table } from '@hospitalrun/components' import { act } from '@testing-library/react' -import format from 'date-fns/format' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -11,14 +8,14 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../clients/db/LabRepository' import * as labsSlice from '../../labs/labs-slice' import ViewLabs from '../../labs/ViewLabs' -import Lab from '../../model/Lab' -import Permissions from '../../model/Permissions' -import * as ButtonBarProvider from '../../page-header/ButtonBarProvider' -import * as titleUtil from '../../page-header/useTitle' -import { RootState } from '../../store' +import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../page-header/title/useTitle' +import LabRepository from '../../shared/db/LabRepository' +import Lab from '../../shared/model/Lab' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) @@ -104,7 +101,7 @@ describe('View Labs', () => { code: 'L-1234', id: '1234', type: 'lab type', - patientId: 'patientId', + patient: 'patientId', status: 'requested', requestedOn: '2020-03-30T04:43:20.102Z', } as Lab @@ -132,45 +129,30 @@ describe('View Labs', () => { }) it('should render a table with data', () => { - const table = wrapper.find('table') - const tableHeader = table.find('thead') - const tableBody = table.find('tbody') - - const tableColumnHeaders = tableHeader.find('th') - const tableDataColumns = tableBody.find('td') - - expect(table).toBeDefined() - expect(tableHeader).toBeDefined() - expect(tableBody).toBeDefined() - expect(tableColumnHeaders.at(0).text().trim()).toEqual('labs.lab.code') - - expect(tableColumnHeaders.at(1).text().trim()).toEqual('labs.lab.type') - - expect(tableColumnHeaders.at(2).text().trim()).toEqual('labs.lab.requestedOn') - - expect(tableColumnHeaders.at(3).text().trim()).toEqual('labs.lab.status') - - expect(tableDataColumns.at(0).text().trim()).toEqual(expectedLab.code) - - expect(tableDataColumns.at(1).text().trim()).toEqual(expectedLab.type) - - expect(tableDataColumns.at(2).text().trim()).toEqual( - format(new Date(expectedLab.requestedOn), 'yyyy-MM-dd hh:mm a'), + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.code', key: 'code' })) + expect(columns[1]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'labs.lab.status', key: 'status' }), ) - expect(tableDataColumns.at(3).text().trim()).toEqual(expectedLab.status) + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual([expectedLab]) }) - it('should navigate to the lab when the row is clicked', () => { - const table = wrapper.find('table') - const tableBody = table.find('tbody') - const tableRow = tableBody.find('tr').at(0) + it('should navigate to the lab when the view button is clicked', () => { + const tr = wrapper.find('tr').at(1) act(() => { - const onClick = tableRow.prop('onClick') as any - onClick() + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) }) - expect(history.location.pathname).toEqual(`/labs/${expectedLab.id}`) }) }) @@ -183,7 +165,7 @@ describe('View Labs', () => { const expectedLab = { id: '1234', type: 'lab type', - patientId: 'patientId', + patient: 'patientId', status: 'requested', requestedOn: '2020-03-30T04:43:20.102Z', } as Lab @@ -236,7 +218,7 @@ describe('View Labs', () => { const expectedLab = { id: '1234', type: 'lab type', - patientId: 'patientId', + patient: 'patientId', status: 'requested', requestedOn: '2020-03-30T04:43:20.102Z', } as Lab diff --git a/src/__tests__/labs/lab-slice.test.ts b/src/__tests__/labs/lab-slice.test.ts index a6dbe8de95..45449b3aea 100644 --- a/src/__tests__/labs/lab-slice.test.ts +++ b/src/__tests__/labs/lab-slice.test.ts @@ -1,8 +1,6 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../clients/db/LabRepository' -import PatientRepository from '../../clients/db/PatientRepository' import labSlice, { requestLab, fetchLabStart, @@ -22,9 +20,11 @@ import labSlice, { requestLabError, updateLab, } from '../../labs/lab-slice' -import Lab from '../../model/Lab' -import Patient from '../../model/Patient' -import { RootState } from '../../store' +import LabRepository from '../../shared/db/LabRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) @@ -41,7 +41,7 @@ describe('lab slice', () => { describe('fetchLabSuccess', () => { it('should set the lab, patient, and status to success', () => { const expectedLab = { id: 'labId' } as Lab - const expectedPatient = { id: 'patientId' } as Patient + const expectedPatient = { id: 'patient' } as Patient const labStore = labSlice( undefined, @@ -155,11 +155,11 @@ describe('lab slice', () => { const mockLab = { id: 'labId', - patientId: 'patientId', + patient: 'patient', } as Lab const mockPatient = { - id: 'patientId', + id: 'patient', } as Patient beforeEach(() => { @@ -175,7 +175,7 @@ describe('lab slice', () => { expect(actions[0]).toEqual(fetchLabStart()) expect(labRepositoryFindSpy).toHaveBeenCalledWith(mockLab.id) - expect(patientRepositorySpy).toHaveBeenCalledWith(mockLab.patientId) + expect(patientRepositorySpy).toHaveBeenCalledWith(mockLab.patient) expect(actions[1]).toEqual(fetchLabSuccess({ lab: mockLab, patient: mockPatient })) }) }) @@ -183,7 +183,7 @@ describe('lab slice', () => { describe('cancel lab', () => { const mockLab = { id: 'labId', - patientId: 'patientId', + patient: 'patient', } as Lab let labRepositorySaveOrUpdateSpy: any @@ -229,7 +229,7 @@ describe('lab slice', () => { describe('complete lab', () => { const mockLab = { id: 'labId', - patientId: 'patientId', + patient: 'patient', result: 'lab result', } as Lab let labRepositorySaveOrUpdateSpy: any @@ -293,7 +293,7 @@ describe('lab slice', () => { const mockLab = { id: 'labId', type: 'labType', - patientId: 'patientId', + patient: 'patient', } as Lab let labRepositorySaveSpy: any @@ -366,7 +366,7 @@ describe('lab slice', () => { describe('update lab', () => { const mockLab = { id: 'labId', - patientId: 'patientId', + patient: 'patient', result: 'lab result', } as Lab let labRepositorySaveOrUpdateSpy: any diff --git a/src/__tests__/labs/labs.slice.test.ts b/src/__tests__/labs/labs.slice.test.ts index d02dbca38b..5e05eb15fb 100644 --- a/src/__tests__/labs/labs.slice.test.ts +++ b/src/__tests__/labs/labs.slice.test.ts @@ -1,10 +1,10 @@ import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' -import LabRepository from '../../clients/db/LabRepository' -import SortRequest from '../../clients/db/SortRequest' import labs, { fetchLabsStart, fetchLabsSuccess, searchLabs } from '../../labs/labs-slice' -import Lab from '../../model/Lab' +import LabRepository from '../../shared/db/LabRepository' +import SortRequest from '../../shared/db/SortRequest' +import Lab from '../../shared/model/Lab' interface SearchContainer { text: string diff --git a/src/__tests__/labs/requests/NewLabRequest.test.tsx b/src/__tests__/labs/requests/NewLabRequest.test.tsx index 3270cf682c..01d1392ffd 100644 --- a/src/__tests__/labs/requests/NewLabRequest.test.tsx +++ b/src/__tests__/labs/requests/NewLabRequest.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { Button, Typeahead, Label, Alert } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' @@ -10,15 +8,15 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../../clients/db/LabRepository' -import PatientRepository from '../../../clients/db/PatientRepository' -import TextFieldWithLabelFormGroup from '../../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' import NewLabRequest from '../../../labs/requests/NewLabRequest' -import Lab from '../../../model/Lab' -import Patient from '../../../model/Patient' -import * as titleUtil from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import * as titleUtil from '../../../page-header/title/useTitle' +import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import LabRepository from '../../../shared/db/LabRepository' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -180,7 +178,7 @@ describe('New Lab Request', () => { let labRepositorySaveSpy: any const expectedDate = new Date() const expectedLab = { - patientId: '12345', + patient: '12345', type: 'expected type', status: 'requested', notes: 'expected notes', @@ -195,7 +193,7 @@ describe('New Lab Request', () => { jest .spyOn(PatientRepository, 'search') - .mockResolvedValue([{ id: expectedLab.patientId, fullName: 'some full name' }] as Patient[]) + .mockResolvedValue([{ id: expectedLab.patient, fullName: 'some full name' }] as Patient[]) history.push('/labs/new') const store = mockStore({ @@ -216,7 +214,7 @@ describe('New Lab Request', () => { const patientTypeahead = wrapper.find(Typeahead) await act(async () => { const onChange = patientTypeahead.prop('onChange') - await onChange([{ id: expectedLab.patientId }] as Patient[]) + await onChange([{ id: expectedLab.patient }] as Patient[]) }) const typeInput = wrapper.find(TextInputWithLabelFormGroup) @@ -241,7 +239,7 @@ describe('New Lab Request', () => { expect(labRepositorySaveSpy).toHaveBeenCalledTimes(1) expect(labRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ - patientId: expectedLab.patientId, + patient: expectedLab.patient, type: expectedLab.type, notes: expectedLab.notes, status: 'requested', diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/page-header/breadcrumbs/Breadcrumbs.test.tsx similarity index 89% rename from src/__tests__/breadcrumbs/Breadcrumbs.test.tsx rename to src/__tests__/page-header/breadcrumbs/Breadcrumbs.test.tsx index 664af2a3bd..397630ffc9 100644 --- a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx +++ b/src/__tests__/page-header/breadcrumbs/Breadcrumbs.test.tsx @@ -1,4 +1,3 @@ -import '../../__mocks__/matchMediaMock' import { Breadcrumb as HRBreadcrumb, BreadcrumbItem as HRBreadcrumbItem, @@ -11,9 +10,9 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Breadcrumbs from '../../breadcrumbs/Breadcrumbs' -import Breadcrumb from '../../model/Breadcrumb' -import { RootState } from '../../store' +import Breadcrumbs from '../../../page-header/breadcrumbs/Breadcrumbs' +import Breadcrumb from '../../../shared/model/Breadcrumb' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts b/src/__tests__/page-header/breadcrumbs/breadcrumbs-slice.test.ts similarity index 94% rename from src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts rename to src/__tests__/page-header/breadcrumbs/breadcrumbs-slice.test.ts index a3b46992b1..f65dc97a8e 100644 --- a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts +++ b/src/__tests__/page-header/breadcrumbs/breadcrumbs-slice.test.ts @@ -1,8 +1,9 @@ -import '../../__mocks__/matchMediaMock' - import { AnyAction } from 'redux' -import breadcrumbs, { addBreadcrumbs, removeBreadcrumbs } from '../../breadcrumbs/breadcrumbs-slice' +import breadcrumbs, { + addBreadcrumbs, + removeBreadcrumbs, +} from '../../../page-header/breadcrumbs/breadcrumbs-slice' describe('breadcrumbs slice', () => { describe('breadcrumbs reducer', () => { diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx b/src/__tests__/page-header/breadcrumbs/useAddBreadcrumbs.test.tsx similarity index 90% rename from src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx rename to src/__tests__/page-header/breadcrumbs/useAddBreadcrumbs.test.tsx index f67b9fd22f..50732bc28d 100644 --- a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx +++ b/src/__tests__/page-header/breadcrumbs/useAddBreadcrumbs.test.tsx @@ -4,9 +4,9 @@ import { Provider } from 'react-redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as breadcrumbsSlice from '../../breadcrumbs/breadcrumbs-slice' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import { RootState } from '../../store' +import * as breadcrumbsSlice from '../../../page-header/breadcrumbs/breadcrumbs-slice' +import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/page-header/ButtonBarProvider.test.tsx b/src/__tests__/page-header/button-toolbar/ButtonBarProvider.test.tsx similarity index 90% rename from src/__tests__/page-header/ButtonBarProvider.test.tsx rename to src/__tests__/page-header/button-toolbar/ButtonBarProvider.test.tsx index 70e8f95713..b8310667c3 100644 --- a/src/__tests__/page-header/ButtonBarProvider.test.tsx +++ b/src/__tests__/page-header/button-toolbar/ButtonBarProvider.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { Button } from '@hospitalrun/components' import { renderHook } from '@testing-library/react-hooks' import React, { useEffect } from 'react' @@ -8,7 +6,7 @@ import { ButtonBarProvider, useButtons, useButtonToolbarSetter, -} from '../../page-header/ButtonBarProvider' +} from '../../../page-header/button-toolbar/ButtonBarProvider' describe('Button Bar Provider', () => { it('should update and fetch data from the button bar provider', () => { diff --git a/src/__tests__/page-header/ButtonToolBar.test.tsx b/src/__tests__/page-header/button-toolbar/ButtonToolBar.test.tsx similarity index 85% rename from src/__tests__/page-header/ButtonToolBar.test.tsx rename to src/__tests__/page-header/button-toolbar/ButtonToolBar.test.tsx index f5ac62fe16..e451f75c58 100644 --- a/src/__tests__/page-header/ButtonToolBar.test.tsx +++ b/src/__tests__/page-header/button-toolbar/ButtonToolBar.test.tsx @@ -1,12 +1,10 @@ -import '../../__mocks__/matchMediaMock' - import { Button } from '@hospitalrun/components' import { mount } from 'enzyme' import React from 'react' import { mocked } from 'ts-jest/utils' -import * as ButtonBarProvider from '../../page-header/ButtonBarProvider' -import ButtonToolBar from '../../page-header/ButtonToolBar' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import ButtonToolBar from '../../../page-header/button-toolbar/ButtonToolBar' describe('Button Tool Bar', () => { beforeEach(() => { diff --git a/src/__tests__/page-header/title-slice.test.ts b/src/__tests__/page-header/title/title-slice.test.ts similarity index 91% rename from src/__tests__/page-header/title-slice.test.ts rename to src/__tests__/page-header/title/title-slice.test.ts index 0c56a92fe3..8684ed6513 100644 --- a/src/__tests__/page-header/title-slice.test.ts +++ b/src/__tests__/page-header/title/title-slice.test.ts @@ -1,6 +1,6 @@ import { AnyAction } from 'redux' -import title, { updateTitle, changeTitle } from '../../page-header/title-slice' +import title, { updateTitle, changeTitle } from '../../../page-header/title/title-slice' describe('title slice', () => { describe('title reducer', () => { diff --git a/src/__tests__/page-header/useTitle.test.tsx b/src/__tests__/page-header/title/useTitle.test.tsx similarity index 80% rename from src/__tests__/page-header/useTitle.test.tsx rename to src/__tests__/page-header/title/useTitle.test.tsx index 1534ca2b58..8155df39c3 100644 --- a/src/__tests__/page-header/useTitle.test.tsx +++ b/src/__tests__/page-header/title/useTitle.test.tsx @@ -4,9 +4,9 @@ import { Provider } from 'react-redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleSlice from '../../page-header/title-slice' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import * as titleSlice from '../../../page-header/title/title-slice' +import useTitle from '../../../page-header/title/useTitle' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/patients/ContactInfo.test.tsx b/src/__tests__/patients/ContactInfo.test.tsx new file mode 100644 index 0000000000..debfb82cdd --- /dev/null +++ b/src/__tests__/patients/ContactInfo.test.tsx @@ -0,0 +1,213 @@ +import { Column, Spinner } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import ContactInfo from '../../patients/ContactInfo' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import { ContactInfoPiece } from '../../shared/model/ContactInformation' +import * as uuid from '../../shared/util/uuid' + +describe('Contact Info in its Editable mode', () => { + const data = [ + { id: '123', value: '123456', type: 'home' }, + { id: '456', value: '789012', type: undefined }, + ] + const dataForNoAdd = [ + { id: '123', value: '123456', type: 'home' }, + { id: '456', value: ' ', type: undefined }, + ] + const errors = ['this is an error', ''] + const label = 'this is a label' + const name = 'this is a name' + let onChange: jest.Mock + + const setup = (_data?: ContactInfoPiece[], _errors?: string[]) => { + const history = createMemoryHistory() + history.push('/patients/new') + onChange = jest.fn() + + const wrapper = mount( + + + , + ) + return wrapper + } + + it('should show a spinner if no data is present', () => { + const wrapper = setup() + const spinnerWrapper = wrapper.find(Spinner) + + expect(spinnerWrapper).toHaveLength(1) + }) + + it('should call onChange if no data is provided', () => { + const newId = 'newId' + jest.spyOn(uuid, 'uuid').mockReturnValue(newId) + setup() + + const expectedNewData = [{ id: newId, value: '' }] + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(expectedNewData) + }) + + it('should render the labels if data is provided', () => { + const wrapper = setup(data) + const headerWrapper = wrapper.find('.header') + const columnWrappers = headerWrapper.find(Column) + const expectedTypeLabel = 'patient.contactInfoType.label' + + expect(columnWrappers.at(0).text()).toEqual(`${expectedTypeLabel} & ${label}`) + expect(columnWrappers.at(1).text()).toEqual(label) + }) + + it('should display the entries if data is provided', () => { + const wrapper = setup(data) + for (let i = 0; i < wrapper.length; i += 1) { + const inputWrapper = wrapper.findWhere((w: any) => w.prop('name') === `${name}${i}`) + + expect(inputWrapper.prop('value')).toEqual(data[i].value) + } + }) + + it('should display the error if error is provided', () => { + const wrapper = setup(data, errors) + const feedbackWrappers = wrapper.find('.invalid-feedback') + + expect(feedbackWrappers).toHaveLength(errors.length) + + feedbackWrappers.forEach((_, i) => { + expect(feedbackWrappers.at(i).text()).toEqual(errors[i]) + }) + }) + + it('should display the add button', () => { + const wrapper = setup(data) + const buttonWrapper = wrapper.find('button') + + expect(buttonWrapper.text().trim()).toEqual('actions.add') + }) + + it('should call the onChange callback if input is changed', () => { + const wrapper = setup(data) + const input = wrapper.findWhere((w: any) => w.prop('name') === `${name}0`).find('input') + input.getDOMNode().value = '777777' + input.simulate('change') + + const expectedNewData = [ + { id: '123', value: '777777', type: 'home' }, + { id: '456', value: '789012', type: undefined }, + ] + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(expectedNewData) + }) + + it('should call the onChange callback if an add button is clicked with valid entries', () => { + const wrapper = setup(data) + const buttonWrapper = wrapper.find('button') + const onClick = buttonWrapper.prop('onClick') as any + const newId = 'newId' + jest.spyOn(uuid, 'uuid').mockReturnValue(newId) + + act(() => { + onClick() + }) + + const expectedNewData = [...data, { id: newId, value: '' }] + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(expectedNewData) + }) + + it('should call the onChange callback if an add button is clicked with an empty entry', () => { + const wrapper = setup(dataForNoAdd) + const buttonWrapper = wrapper.find('button') + const onClick = buttonWrapper.prop('onClick') as any + const newId = 'newId' + jest.spyOn(uuid, 'uuid').mockReturnValue(newId) + + act(() => { + onClick() + }) + + const expectedNewData = [ + { id: '123', value: '123456', type: 'home' }, + { id: newId, value: '' }, + ] + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(expectedNewData) + }) +}) + +describe('Contact Info in its non-Editable mode', () => { + const data = [ + { id: '123', value: '123456', type: 'home' }, + { id: '456', value: '789012', type: undefined }, + ] + const label = 'this is a label' + const name = 'this is a name' + + const setup = (_data?: ContactInfoPiece[]) => { + const history = createMemoryHistory() + history.push('/patients/new') + + const wrapper = mount( + + + , + ) + return wrapper + } + + it('should render an empty element if no data is present', () => { + const wrapper = setup() + const contactInfoWrapper = wrapper.find(ContactInfo) + + expect(contactInfoWrapper.find('div')).toHaveLength(1) + expect(contactInfoWrapper.containsMatchingElement(
)).toEqual(true) + }) + + it('should render the labels if data is provided', () => { + const wrapper = setup(data) + const headerWrapper = wrapper.find('.header') + const columnWrappers = headerWrapper.find(Column) + const expectedTypeLabel = 'patient.contactInfoType.label' + + expect(columnWrappers.at(0).text()).toEqual(`${expectedTypeLabel} & ${label}`) + expect(columnWrappers.at(1).text()).toEqual(label) + }) + + it('should display the entries if data is provided', () => { + const wrapper = setup(data) + for (let i = 0; i < wrapper.length; i += 1) { + const inputWrapper = wrapper.findWhere((w: any) => w.prop('name') === `${name}${i}`) + + expect(inputWrapper.prop('value')).toEqual(data[i].value) + } + }) + + it('should show inputs that are not editable', () => { + const wrapper = setup(data) + const inputWrappers = wrapper.find(TextInputWithLabelFormGroup) + for (let i = 0; i < inputWrappers.length; i += 1) { + expect(inputWrappers.at(i).prop('isEditable')).toBeFalsy() + } + }) +}) diff --git a/src/__tests__/patients/GeneralInformation.test.tsx b/src/__tests__/patients/GeneralInformation.test.tsx index 353334e293..ecd1d1cc0d 100644 --- a/src/__tests__/patients/GeneralInformation.test.tsx +++ b/src/__tests__/patients/GeneralInformation.test.tsx @@ -1,15 +1,13 @@ -import '../../__mocks__/matchMediaMock' - import { Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import { startOfDay, subYears } from 'date-fns' import { mount, ReactWrapper } from 'enzyme' -import { createMemoryHistory } from 'history' +import { createMemoryHistory, MemoryHistory } from 'history' import React from 'react' import { Router } from 'react-router-dom' -import Patient from '../../model/Patient' import GeneralInformation from '../../patients/GeneralInformation' +import Patient from '../../shared/model/Patient' describe('Error handling', () => { it('should display errors', () => { @@ -17,32 +15,50 @@ describe('Error handling', () => { message: 'some message', givenName: 'given name message', dateOfBirth: 'date of birth message', - phoneNumber: 'phone number message', - email: 'email message', + phoneNumbers: ['phone number message'], + emails: ['email message'], } - const wrapper = mount() + const wrapper = mount( + , + ) wrapper.update() const errorMessage = wrapper.find(Alert) const givenNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') const dateOfBirthInput = wrapper.findWhere((w: any) => w.prop('name') === 'dateOfBirth') - const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email') - const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber') + const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber0') + const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email0') + expect(errorMessage).toBeTruthy() expect(errorMessage.prop('message')).toMatch(error.message) + expect(givenNameInput.prop('isInvalid')).toBeTruthy() expect(givenNameInput.prop('feedback')).toEqual(error.givenName) + expect(dateOfBirthInput.prop('isInvalid')).toBeTruthy() expect(dateOfBirthInput.prop('feedback')).toEqual(error.dateOfBirth) - expect(emailInput.prop('feedback')).toEqual(error.email) - expect(emailInput.prop('isInvalid')).toBeTruthy() - expect(phoneNumberInput.prop('feedback')).toEqual(error.phoneNumber) + expect(phoneNumberInput.prop('isInvalid')).toBeTruthy() + expect(phoneNumberInput.prop('feedback')).toEqual(error.phoneNumbers[0]) + + expect(emailInput.prop('isInvalid')).toBeTruthy() + expect(emailInput.prop('feedback')).toEqual(error.emails[0]) }) }) describe('General Information, without isEditable', () => { + let wrapper: ReactWrapper + let history = createMemoryHistory() const patient = { id: '123', prefix: 'prefix', @@ -51,19 +67,25 @@ describe('General Information, without isEditable', () => { suffix: 'suffix', sex: 'male', type: 'charity', + dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), + isApproximateDateOfBirth: false, occupation: 'occupation', preferredLanguage: 'preferredLanguage', - phoneNumber: 'phoneNumber', - email: 'email@email.com', - address: 'address', + phoneNumbers: [ + { value: '123456', type: undefined, id: '123' }, + { value: '789012', type: undefined, id: '456' }, + ], + emails: [ + { value: 'abc@email.com', type: undefined, id: '789' }, + { value: 'xyz@email.com', type: undefined, id: '987' }, + ], + addresses: [ + { value: 'address A', type: undefined, id: '654' }, + { value: 'address B', type: undefined, id: '321' }, + ], code: 'P00001', - dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), - isApproximateDateOfBirth: false, } as Patient - let wrapper: ReactWrapper - let history = createMemoryHistory() - beforeEach(() => { Date.now = jest.fn().mockReturnValue(new Date().valueOf()) jest.restoreAllMocks() @@ -105,7 +127,7 @@ describe('General Information, without isEditable', () => { it('should render the sex select', () => { const sexSelect = wrapper.findWhere((w: any) => w.prop('name') === 'sex') - expect(sexSelect.prop('value')).toEqual(patient.sex) + expect(sexSelect.prop('defaultSelected')[0].value).toEqual(patient.sex) expect(sexSelect.prop('label')).toEqual('patient.sex') expect(sexSelect.prop('isEditable')).toBeFalsy() expect(sexSelect.prop('options')).toHaveLength(4) @@ -121,7 +143,7 @@ describe('General Information, without isEditable', () => { it('should render the patient type select', () => { const typeSelect = wrapper.findWhere((w: any) => w.prop('name') === 'type') - expect(typeSelect.prop('value')).toEqual(patient.type) + expect(typeSelect.prop('defaultSelected')[0].value).toEqual(patient.type) expect(typeSelect.prop('label')).toEqual('patient.type') expect(typeSelect.prop('isEditable')).toBeFalsy() expect(typeSelect.prop('options')).toHaveLength(2) @@ -139,6 +161,23 @@ describe('General Information, without isEditable', () => { expect(dateOfBirthInput.prop('isEditable')).toBeFalsy() }) + it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { + patient.isApproximateDateOfBirth = true + await act(async () => { + wrapper = await mount( + + ) + , + ) + }) + + const approximateAgeInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') + + expect(approximateAgeInput.prop('value')).toEqual('30') + expect(approximateAgeInput.prop('label')).toEqual('patient.approximateAge') + expect(approximateAgeInput.prop('isEditable')).toBeFalsy() + }) + it('should render the occupation of the patient', () => { const occupationInput = wrapper.findWhere((w: any) => w.prop('name') === 'occupation') expect(occupationInput.prop('value')).toEqual(patient.occupation) @@ -155,48 +194,35 @@ describe('General Information, without isEditable', () => { expect(preferredLanguageInput.prop('isEditable')).toBeFalsy() }) - it('should render the phone number of the patient', () => { - const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber') - expect(phoneNumberInput.prop('value')).toEqual(patient.phoneNumber) - expect(phoneNumberInput.prop('label')).toEqual('patient.phoneNumber') - expect(phoneNumberInput.prop('isEditable')).toBeFalsy() - }) - - it('should render the email of the patient', () => { - const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email') - expect(emailInput.prop('value')).toEqual(patient.email) - expect(emailInput.prop('label')).toEqual('patient.email') - expect(emailInput.prop('isEditable')).toBeFalsy() + it('should render the phone numbers of the patient', () => { + patient.phoneNumbers.forEach((phoneNumber, i) => { + const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === `phoneNumber${i}`) + expect(phoneNumberInput.prop('value')).toEqual(phoneNumber.value) + expect(phoneNumberInput.prop('isEditable')).toBeFalsy() + }) }) - it('should render the address of the patient', () => { - const addressInput = wrapper.findWhere((w: any) => w.prop('name') === 'address') - expect(addressInput.prop('value')).toEqual(patient.address) - expect(addressInput.prop('label')).toEqual('patient.address') - expect(addressInput.prop('isEditable')).toBeFalsy() + it('should render the emails of the patient', () => { + patient.emails.forEach((email, i) => { + const emailInput = wrapper.findWhere((w: any) => w.prop('name') === `email${i}`) + expect(emailInput.prop('value')).toEqual(email.value) + expect(emailInput.prop('isEditable')).toBeFalsy() + }) }) - it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { - patient.isApproximateDateOfBirth = true - await act(async () => { - wrapper = await mount( - - ) - , - ) + it('should render the addresses of the patient', () => { + patient.addresses.forEach((address, i) => { + const addressInput = wrapper.findWhere((w: any) => w.prop('name') === `address${i}`) + expect(addressInput.prop('value')).toEqual(address.value) + expect(addressInput.prop('isEditable')).toBeFalsy() }) - - wrapper.update() - - const ageInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') - - expect(ageInput.prop('value')).toEqual('30') - expect(ageInput.prop('label')).toEqual('patient.approximateAge') - expect(ageInput.prop('isEditable')).toBeFalsy() }) }) describe('General Information, isEditable', () => { + let wrapper: ReactWrapper + let history: MemoryHistory + let onFieldChange: jest.Mock const patient = { id: '123', prefix: 'prefix', @@ -205,28 +231,33 @@ describe('General Information, isEditable', () => { suffix: 'suffix', sex: 'male', type: 'charity', + dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), + isApproximateDateOfBirth: false, occupation: 'occupation', preferredLanguage: 'preferredLanguage', - phoneNumber: 'phoneNumber', - email: 'email@email.com', - address: 'address', + phoneNumbers: [ + { value: '123456', type: undefined, id: '123' }, + { value: '789012', type: undefined, id: '456' }, + ], + emails: [ + { value: 'abc@email.com', type: undefined, id: '789' }, + { value: 'xyz@email.com', type: undefined, id: '987' }, + ], + addresses: [ + { value: 'address A', type: undefined, id: '654' }, + { value: 'address B', type: undefined, id: '321' }, + ], code: 'P00001', - dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), - isApproximateDateOfBirth: false, } as Patient - let wrapper: ReactWrapper - let history = createMemoryHistory() - - const onFieldChange = jest.fn() - beforeEach(() => { jest.restoreAllMocks() Date.now = jest.fn().mockReturnValue(new Date().valueOf()) history = createMemoryHistory() + onFieldChange = jest.fn() wrapper = mount( - ) + ) , ) }) @@ -235,87 +266,90 @@ describe('General Information, isEditable', () => { const expectedGivenName = 'expectedGivenName' const expectedFamilyName = 'expectedFamilyName' const expectedSuffix = 'expectedSuffix' - const expectedSex = 'expectedSex' - const expectedType = 'expectedType' + const expectedDateOfBirth = '1937-06-14T05:00:00.000Z' const expectedOccupation = 'expectedOccupation' const expectedPreferredLanguage = 'expectedPreferredLanguage' - const expectedPhoneNumber = 'expectedPhoneNumber' - const expectedEmail = 'expectedEmail' - const expectedAddress = 'expectedAddress' - const expectedDateOfBirth = '1937-06-14T05:00:00.000Z' + const expectedPhoneNumbers = [ + { value: '111111', type: undefined, id: '123' }, + { value: '222222', type: undefined, id: '456' }, + ] + const expectedEmails = [ + { value: 'def@email.com', type: undefined, id: '789' }, + { value: 'uvw@email.com', type: undefined, id: '987' }, + ] + const expectedAddresses = [ + { value: 'address C', type: undefined, id: '654' }, + { value: 'address D', type: undefined, id: '321' }, + ] it('should render the prefix', () => { const prefixInput = wrapper.findWhere((w: any) => w.prop('name') === 'prefix') - const generalInformation = wrapper.find(GeneralInformation) + expect(prefixInput.prop('value')).toEqual(patient.prefix) expect(prefixInput.prop('label')).toEqual('patient.prefix') expect(prefixInput.prop('isEditable')).toBeTruthy() - act(() => { - prefixInput.prop('onChange')({ target: { value: expectedPrefix } }) - }) + const input = prefixInput.find('input') + input.getDOMNode().value = expectedPrefix + input.simulate('change') - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('prefix', expectedPrefix) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, prefix: expectedPrefix }) }) it('should render the given name', () => { const givenNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') - const generalInformation = wrapper.find(GeneralInformation) + expect(givenNameInput.prop('value')).toEqual(patient.givenName) expect(givenNameInput.prop('label')).toEqual('patient.givenName') expect(givenNameInput.prop('isEditable')).toBeTruthy() - act(() => { - givenNameInput.prop('onChange')({ target: { value: expectedGivenName } }) - }) + const input = givenNameInput.find('input') + input.getDOMNode().value = expectedGivenName + input.simulate('change') - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'givenName', - expectedGivenName, - ) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, givenName: expectedGivenName }) }) it('should render the family name', () => { const familyNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'familyName') - const generalInformation = wrapper.find(GeneralInformation) expect(familyNameInput.prop('value')).toEqual(patient.familyName) expect(familyNameInput.prop('label')).toEqual('patient.familyName') expect(familyNameInput.prop('isEditable')).toBeTruthy() - act(() => { - familyNameInput.prop('onChange')({ target: { value: expectedFamilyName } }) - }) + const input = familyNameInput.find('input') + input.getDOMNode().value = expectedFamilyName + input.simulate('change') - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'familyName', - expectedFamilyName, - ) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, familyName: expectedFamilyName }) }) it('should render the suffix', () => { const suffixInput = wrapper.findWhere((w: any) => w.prop('name') === 'suffix') - const generalInformation = wrapper.find(GeneralInformation) expect(suffixInput.prop('value')).toEqual(patient.suffix) expect(suffixInput.prop('label')).toEqual('patient.suffix') expect(suffixInput.prop('isEditable')).toBeTruthy() - act(() => { - suffixInput.prop('onChange')({ target: { value: expectedSuffix } }) - }) + const input = suffixInput.find('input') + input.getDOMNode().value = expectedSuffix + input.simulate('change') - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('suffix', expectedSuffix) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, suffix: expectedSuffix }) }) it('should render the sex select', () => { const sexSelect = wrapper.findWhere((w: any) => w.prop('name') === 'sex') - const generalInformation = wrapper.find(GeneralInformation) - expect(sexSelect.prop('value')).toEqual(patient.sex) + expect(sexSelect.prop('defaultSelected')[0].value).toEqual(patient.sex) expect(sexSelect.prop('label')).toEqual('patient.sex') expect(sexSelect.prop('isEditable')).toBeTruthy() expect(sexSelect.prop('options')).toHaveLength(4) + expect(sexSelect.prop('options')[0].label).toEqual('sex.male') expect(sexSelect.prop('options')[0].value).toEqual('male') expect(sexSelect.prop('options')[1].label).toEqual('sex.female') @@ -324,37 +358,24 @@ describe('General Information, isEditable', () => { expect(sexSelect.prop('options')[2].value).toEqual('other') expect(sexSelect.prop('options')[3].label).toEqual('sex.unknown') expect(sexSelect.prop('options')[3].value).toEqual('unknown') - - act(() => { - sexSelect.prop('onChange')({ target: { value: expectedSex } }) - }) - - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('sex', expectedSex) }) it('should render the patient type select', () => { const typeSelect = wrapper.findWhere((w: any) => w.prop('name') === 'type') - const generalInformation = wrapper.find(GeneralInformation) - expect(typeSelect.prop('value')).toEqual(patient.type) + expect(typeSelect.prop('defaultSelected')[0].value).toEqual(patient.type) expect(typeSelect.prop('label')).toEqual('patient.type') expect(typeSelect.prop('isEditable')).toBeTruthy() + expect(typeSelect.prop('options')).toHaveLength(2) expect(typeSelect.prop('options')[0].label).toEqual('patient.types.charity') expect(typeSelect.prop('options')[0].value).toEqual('charity') expect(typeSelect.prop('options')[1].label).toEqual('patient.types.private') expect(typeSelect.prop('options')[1].value).toEqual('private') - - act(() => { - typeSelect.prop('onChange')({ target: { value: expectedType } }) - }) - - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('type', expectedType) }) it('should render the date of the birth of the patient', () => { const dateOfBirthInput = wrapper.findWhere((w: any) => w.prop('name') === 'dateOfBirth') - const generalInformation = wrapper.find(GeneralInformation) expect(dateOfBirthInput.prop('value')).toEqual(new Date(patient.dateOfBirth)) expect(dateOfBirthInput.prop('label')).toEqual('patient.dateOfBirth') @@ -365,126 +386,156 @@ describe('General Information, isEditable', () => { dateOfBirthInput.prop('onChange')(new Date(expectedDateOfBirth)) }) - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'dateOfBirth', - expectedDateOfBirth, - ) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, dateOfBirth: expectedDateOfBirth }) + }) + + it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { + patient.isApproximateDateOfBirth = true + await act(async () => { + wrapper = await mount( + + ) + , + ) + }) + + const approximateAgeInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') + + expect(approximateAgeInput.prop('value')).toEqual('30') + expect(approximateAgeInput.prop('label')).toEqual('patient.approximateAge') + expect(approximateAgeInput.prop('isEditable')).toBeTruthy() + + const input = approximateAgeInput.find('input') + input.getDOMNode().value = '20' + input.simulate('change') + + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ + ...patient, + dateOfBirth: startOfDay(subYears(new Date(Date.now()), 20)).toISOString(), + }) }) it('should render the occupation of the patient', () => { const occupationInput = wrapper.findWhere((w: any) => w.prop('name') === 'occupation') - const generalInformation = wrapper.find(GeneralInformation) expect(occupationInput.prop('value')).toEqual(patient.occupation) expect(occupationInput.prop('label')).toEqual('patient.occupation') expect(occupationInput.prop('isEditable')).toBeTruthy() - act(() => { - occupationInput.prop('onChange')({ target: { value: expectedOccupation } }) - }) + const input = occupationInput.find('input') + input.getDOMNode().value = expectedOccupation + input.simulate('change') - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'occupation', - expectedOccupation, - ) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ ...patient, occupation: expectedOccupation }) }) it('should render the preferred language of the patient', () => { const preferredLanguageInput = wrapper.findWhere( (w: any) => w.prop('name') === 'preferredLanguage', ) - const generalInformation = wrapper.find(GeneralInformation) expect(preferredLanguageInput.prop('value')).toEqual(patient.preferredLanguage) expect(preferredLanguageInput.prop('label')).toEqual('patient.preferredLanguage') expect(preferredLanguageInput.prop('isEditable')).toBeTruthy() - act(() => { - preferredLanguageInput.prop('onChange')({ target: { value: expectedPreferredLanguage } }) - }) - - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'preferredLanguage', - expectedPreferredLanguage, - ) - }) - - it('should render the phone number of the patient', () => { - const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber') - const generalInformation = wrapper.find(GeneralInformation) - - expect(phoneNumberInput.prop('value')).toEqual(patient.phoneNumber) - expect(phoneNumberInput.prop('label')).toEqual('patient.phoneNumber') - expect(phoneNumberInput.prop('isEditable')).toBeTruthy() + const input = preferredLanguageInput.find('input') + input.getDOMNode().value = expectedPreferredLanguage + input.simulate('change') - act(() => { - phoneNumberInput.prop('onChange')({ target: { value: expectedPhoneNumber } }) + expect(onFieldChange).toHaveBeenCalledTimes(1) + expect(onFieldChange).toHaveBeenCalledWith({ + ...patient, + preferredLanguage: expectedPreferredLanguage, }) - - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'phoneNumber', - expectedPhoneNumber, - ) }) - it('should render the email of the patient', () => { - const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email') - const generalInformation = wrapper.find(GeneralInformation) + it('should render the phone numbers of the patient', () => { + patient.phoneNumbers.forEach((phoneNumber, i) => { + const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === `phoneNumber${i}`) + expect(phoneNumberInput.prop('value')).toEqual(phoneNumber.value) + expect(phoneNumberInput.prop('isEditable')).toBeTruthy() - expect(emailInput.prop('value')).toEqual(patient.email) - expect(emailInput.prop('label')).toEqual('patient.email') - expect(emailInput.prop('isEditable')).toBeTruthy() + const input = phoneNumberInput.find('input') + input.getDOMNode().value = expectedPhoneNumbers[i].value + input.simulate('change') + }) - act(() => { - emailInput.prop('onChange')({ target: { value: expectedEmail } }) + const calledWith = [] as any + patient.phoneNumbers.forEach((_, i) => { + const newPhoneNumbers = [] as any + patient.phoneNumbers.forEach((__, j) => { + if (j <= i) { + newPhoneNumbers.push(expectedPhoneNumbers[j]) + } else { + newPhoneNumbers.push(patient.phoneNumbers[j]) + } + }) + calledWith.push({ ...patient, phoneNumbers: newPhoneNumbers }) }) - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('email', expectedEmail) + expect(onFieldChange).toHaveBeenCalledTimes(calledWith.length) + expect(onFieldChange).toHaveBeenNthCalledWith(1, calledWith[0]) + // expect(onFieldChange).toHaveBeenNthCalledWith(2, calledWith[1]) }) - it('should render the address of the patient', () => { - const addressInput = wrapper.findWhere((w: any) => w.prop('name') === 'address') - const generalInformation = wrapper.find(GeneralInformation) + it('should render the emails of the patient', () => { + patient.emails.forEach((email, i) => { + const emailInput = wrapper.findWhere((w: any) => w.prop('name') === `email${i}`) + expect(emailInput.prop('value')).toEqual(email.value) + expect(emailInput.prop('isEditable')).toBeTruthy() - expect(addressInput.prop('value')).toEqual(patient.address) - expect(addressInput.prop('label')).toEqual('patient.address') - expect(addressInput.prop('isEditable')).toBeTruthy() + const input = emailInput.find('input') + input.getDOMNode().value = expectedEmails[i].value + input.simulate('change') + }) - act(() => { - addressInput.prop('onChange')({ currentTarget: { value: expectedAddress } }) + const calledWith = [] as any + patient.emails.forEach((_, i) => { + const newEmails = [] as any + patient.emails.forEach((__, j) => { + if (j <= i) { + newEmails.push(expectedEmails[j]) + } else { + newEmails.push(patient.emails[j]) + } + }) + calledWith.push({ ...patient, emails: newEmails }) }) - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'address', - expectedAddress, - ) + expect(onFieldChange).toHaveBeenCalledTimes(calledWith.length) + expect(onFieldChange).toHaveBeenNthCalledWith(1, calledWith[0]) + // expect(onFieldChange).toHaveBeenNthCalledWith(2, calledWith[1]) }) - it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { - patient.isApproximateDateOfBirth = true - await act(async () => { - wrapper = await mount( - - ) - , - ) - }) + it('should render the addresses of the patient', () => { + patient.addresses.forEach((address, i) => { + const addressTextArea = wrapper.findWhere((w: any) => w.prop('name') === `address${i}`) + expect(addressTextArea.prop('value')).toEqual(address.value) + expect(addressTextArea.prop('isEditable')).toBeTruthy() - wrapper.update() - - const approximateAgeInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') - const generalInformation = wrapper.find(GeneralInformation) - expect(approximateAgeInput.prop('value')).toEqual('30') - expect(approximateAgeInput.prop('label')).toEqual('patient.approximateAge') - expect(approximateAgeInput.prop('isEditable')).toBeTruthy() + const textarea = addressTextArea.find('textarea') + textarea.getDOMNode().value = expectedAddresses[i].value + textarea.simulate('change') + }) - act(() => { - approximateAgeInput.prop('onChange')({ target: { value: '20' } }) + const calledWith = [] as any + patient.addresses.forEach((_, i) => { + const newAddresses = [] as any + patient.addresses.forEach((__, j) => { + if (j <= i) { + newAddresses.push(expectedAddresses[j]) + } else { + newAddresses.push(patient.addresses[j]) + } + }) + calledWith.push({ ...patient, addresses: newAddresses }) }) - expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( - 'dateOfBirth', - startOfDay(subYears(new Date(Date.now()), 20)).toISOString(), - ) + expect(onFieldChange).toHaveBeenCalledTimes(calledWith.length) + expect(onFieldChange).toHaveBeenNthCalledWith(1, calledWith[0]) + // expect(onFieldChange).toHaveBeenNthCalledWith(2, calledWith[1]) }) }) diff --git a/src/__tests__/patients/Patients.test.tsx b/src/__tests__/patients/Patients.test.tsx index 08e99e36fb..8214eb15bf 100644 --- a/src/__tests__/patients/Patients.test.tsx +++ b/src/__tests__/patients/Patients.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { mount } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' @@ -8,16 +6,16 @@ import { MemoryRouter } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import { addBreadcrumbs } from '../../breadcrumbs/breadcrumbs-slice' -import PatientRepository from '../../clients/db/PatientRepository' import Dashboard from '../../dashboard/Dashboard' import HospitalRun from '../../HospitalRun' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' +import { addBreadcrumbs } from '../../page-header/breadcrumbs/breadcrumbs-slice' import EditPatient from '../../patients/edit/EditPatient' import NewPatient from '../../patients/new/NewPatient' import ViewPatient from '../../patients/view/ViewPatient' -import { RootState } from '../../store' +import PatientRepository from '../../shared/db/PatientRepository' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) @@ -25,7 +23,7 @@ describe('/patients/new', () => { it('should render the new patient screen when /patients/new is accessed', async () => { const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WritePatients] }, + user: { user: { id: '123' }, permissions: [Permissions.WritePatients] }, patient: {}, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -59,7 +57,7 @@ describe('/patients/new', () => { { const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WritePatients, Permissions.ReadPatients] }, + user: { + user: { id: '123' }, + permissions: [Permissions.WritePatients, Permissions.ReadPatients], + }, patient: { patient }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -120,7 +121,7 @@ describe('/patients/edit/:id', () => { { { const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ReadPatients] }, + user: { user: { id: '123' }, permissions: [Permissions.ReadPatients] }, patient: { patient }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -199,7 +200,7 @@ describe('/patients/:id', () => { ([thunk]) const history = createMemoryHistory() diff --git a/src/__tests__/patients/allergies/NewAllergyModal.test.tsx b/src/__tests__/patients/allergies/NewAllergyModal.test.tsx index 6492bc0b8c..15cba448c2 100644 --- a/src/__tests__/patients/allergies/NewAllergyModal.test.tsx +++ b/src/__tests__/patients/allergies/NewAllergyModal.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { Modal, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' @@ -8,12 +6,12 @@ import { Provider } from 'react-redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' -import Patient from '../../../model/Patient' import NewAllergyModal from '../../../patients/allergies/NewAllergyModal' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/patients/appointments/AppointmentsList.test.tsx b/src/__tests__/patients/appointments/AppointmentsList.test.tsx index 0aca51515a..938b92361e 100644 --- a/src/__tests__/patients/appointments/AppointmentsList.test.tsx +++ b/src/__tests__/patients/appointments/AppointmentsList.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' @@ -10,10 +8,11 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Patient from '../../../model/Patient' import AppointmentsList from '../../../patients/appointments/AppointmentsList' -import * as appointmentsSlice from '../../../scheduling/appointments/appointments-slice' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../../shared/model/Appointment' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const expectedPatient = { id: '123', @@ -23,7 +22,7 @@ const expectedAppointments = [ { id: '456', rev: '1', - patientId: '1234', + patient: '1234', startDateTime: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), endDateTime: new Date(2020, 1, 1, 9, 30, 0, 0).toISOString(), location: 'location', @@ -32,13 +31,13 @@ const expectedAppointments = [ { id: '123', rev: '1', - patientId: '1234', + patient: '1234', startDateTime: new Date(2020, 1, 1, 8, 0, 0, 0).toISOString(), endDateTime: new Date(2020, 1, 1, 8, 30, 0, 0).toISOString(), location: 'location', reason: 'Checkup', }, -] +] as Appointment[] const mockStore = createMockStore([thunk]) const history = createMemoryHistory() @@ -46,6 +45,8 @@ const history = createMemoryHistory() let store: any const setup = (patient = expectedPatient, appointments = expectedAppointments) => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'getAppointments').mockResolvedValue(appointments) store = mockStore({ patient, appointments: { appointments } } as any) const wrapper = mount( @@ -71,22 +72,6 @@ describe('AppointmentsList', () => { ) }) - it('should search for "ch" in the list', () => { - jest.spyOn(appointmentsSlice, 'fetchPatientAppointments') - const searchText = 'ch' - const wrapper = setup() - - const searchInput: ReactWrapper = wrapper.find('input').at(0) - searchInput.simulate('change', { target: { value: searchText } }) - - wrapper.find('button').at(1).simulate('click') - - expect(appointmentsSlice.fetchPatientAppointments).toHaveBeenCalledWith( - expectedPatient.id, - searchText, - ) - }) - describe('New appointment button', () => { it('should render a new appointment button', () => { const wrapper = setup() @@ -96,11 +81,11 @@ describe('AppointmentsList', () => { expect(addNewAppointmentButton.text().trim()).toEqual('scheduling.appointments.new') }) - it('should navigate to new appointment page', () => { + it('should navigate to new appointment page', async () => { const wrapper = setup() - act(() => { - wrapper.find(components.Button).at(0).simulate('click') + await act(async () => { + await wrapper.find(components.Button).at(0).simulate('click') }) wrapper.update() diff --git a/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx index e62b486275..ba9998606e 100644 --- a/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx +++ b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx @@ -1,4 +1,3 @@ -import '../../../__mocks__/matchMediaMock' import { Modal } from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -9,12 +8,13 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' -import Patient from '../../../model/Patient' import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' import CarePlanForm from '../../../patients/care-plans/CarePlanForm' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import { CarePlanIntent, CarePlanStatus } from '../../../shared/model/CarePlan' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -42,6 +42,8 @@ describe('Add Care Plan Modal', () => { const onCloseSpy = jest.fn() const setup = () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'saveOrUpdate') const store = mockStore({ patient: { patient, carePlanError } } as any) const history = createMemoryHistory() const wrapper = mount( diff --git a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx index 20f83dd4c8..8a92683677 100644 --- a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx +++ b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx @@ -1,14 +1,13 @@ -import '../../../__mocks__/matchMediaMock' import { Alert } from '@hospitalrun/components' import { addDays } from 'date-fns' import { mount } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' -import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' -import Diagnosis from '../../../model/Diagnosis' -import Patient from '../../../model/Patient' import CarePlanForm from '../../../patients/care-plans/CarePlanForm' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../shared/model/CarePlan' +import Diagnosis from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' describe('Care Plan Form', () => { let onCarePlanChangeSpy: any @@ -98,7 +97,7 @@ describe('Care Plan Form', () => { expect(conditionSelector).toHaveLength(1) expect(conditionSelector.prop('patient.carePlan.condition')) expect(conditionSelector.prop('isRequired')).toBeTruthy() - expect(conditionSelector.prop('value')).toEqual(carePlan.diagnosisId) + expect(conditionSelector.prop('defaultSelected')[0].value).toEqual(carePlan.diagnosisId) expect(conditionSelector.prop('options')).toEqual([ { value: diagnosis.id, label: diagnosis.name }, ]) @@ -110,7 +109,7 @@ describe('Care Plan Form', () => { act(() => { const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') const onChange = conditionSelector.prop('onChange') as any - onChange({ currentTarget: { value: expectedNewCondition } }) + onChange([expectedNewCondition]) }) expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ diagnosisId: expectedNewCondition }) @@ -124,7 +123,7 @@ describe('Care Plan Form', () => { expect(statusSelector).toHaveLength(1) expect(statusSelector.prop('patient.carePlan.status')) expect(statusSelector.prop('isRequired')).toBeTruthy() - expect(statusSelector.prop('value')).toEqual(carePlan.status) + expect(statusSelector.prop('defaultSelected')[0].value).toEqual(carePlan.status) expect(statusSelector.prop('options')).toEqual( Object.values(CarePlanStatus).map((v) => ({ label: v, value: v })), ) @@ -136,7 +135,7 @@ describe('Care Plan Form', () => { act(() => { const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') const onChange = statusSelector.prop('onChange') as any - onChange({ currentTarget: { value: expectedNewStatus } }) + onChange([expectedNewStatus]) }) expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ status: expectedNewStatus }) @@ -150,7 +149,7 @@ describe('Care Plan Form', () => { expect(intentSelector).toHaveLength(1) expect(intentSelector.prop('patient.carePlan.intent')) expect(intentSelector.prop('isRequired')).toBeTruthy() - expect(intentSelector.prop('value')).toEqual(carePlan.intent) + expect(intentSelector.prop('defaultSelected')[0].value).toEqual(carePlan.intent) expect(intentSelector.prop('options')).toEqual( Object.values(CarePlanIntent).map((v) => ({ label: v, value: v })), ) @@ -162,7 +161,7 @@ describe('Care Plan Form', () => { act(() => { const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') const onChange = intentSelector.prop('onChange') as any - onChange({ currentTarget: { value: newIntent } }) + onChange([newIntent]) }) expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ intent: newIntent }) @@ -226,7 +225,6 @@ describe('Care Plan Form', () => { const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') expect(noteInput).toHaveLength(1) expect(noteInput.prop('patient.carePlan.note')) - expect(noteInput.prop('isRequired')).toBeTruthy() expect(noteInput.prop('value')).toEqual(carePlan.note) }) @@ -299,13 +297,13 @@ describe('Care Plan Form', () => { expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) expect(conditionSelector.prop('isInvalid')).toBeTruthy() - expect(conditionSelector.prop('feedback')).toEqual(expectedError.condition) + // expect(conditionSelector.prop('feedback')).toEqual(expectedError.condition) expect(statusSelector.prop('isInvalid')).toBeTruthy() - expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + // expect(statusSelector.prop('feedback')).toEqual(expectedError.status) expect(intentSelector.prop('isInvalid')).toBeTruthy() - expect(intentSelector.prop('feedback')).toEqual(expectedError.intent) + // expect(intentSelector.prop('feedback')).toEqual(expectedError.intent) expect(startDatePicker.prop('isInvalid')).toBeTruthy() expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) diff --git a/src/__tests__/patients/care-plans/CarePlanTab.test.tsx b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx index 1a202bc841..ba024db7ac 100644 --- a/src/__tests__/patients/care-plans/CarePlanTab.test.tsx +++ b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx @@ -1,4 +1,3 @@ -import '../../../__mocks__/matchMediaMock' import { Button } from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -9,12 +8,12 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Permissions from '../../../model/Permissions' import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' import CarePlanTab from '../../../patients/care-plans/CarePlanTab' import CarePlanTable from '../../../patients/care-plans/CarePlanTable' import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' -import { RootState } from '../../../store' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -88,13 +87,13 @@ describe('Care Plan Tab', () => { }) it('should render the care plans table when on /patient/:id/care-plans', () => { - const { wrapper } = setup('/patients/123/care-plans', []) + const { wrapper } = setup('/patients/123/care-plans', [Permissions.ReadCarePlan]) expect(wrapper.find(CarePlanTable)).toHaveLength(1) }) it('should render the care plan view when on /patient/:id/care-plans/:carePlanId', () => { - const { wrapper } = setup('/patients/123/care-plans/456', []) + const { wrapper } = setup('/patients/123/care-plans/456', [Permissions.ReadCarePlan]) expect(wrapper.find(ViewCarePlan)).toHaveLength(1) }) diff --git a/src/__tests__/patients/care-plans/CarePlanTable.test.tsx b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx index ce705f2a59..713197fa80 100644 --- a/src/__tests__/patients/care-plans/CarePlanTable.test.tsx +++ b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx @@ -1,6 +1,5 @@ -import '../../../__mocks__/matchMediaMock' -import { Button } from '@hospitalrun/components' -import { mount } from 'enzyme' +import { Table } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -9,10 +8,10 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' -import Patient from '../../../model/Patient' import CarePlanTable from '../../../patients/care-plans/CarePlanTable' -import { RootState } from '../../../store' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../shared/model/CarePlan' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -47,41 +46,41 @@ describe('Care Plan Table', () => { , ) - return { wrapper, history } + return { wrapper: wrapper as ReactWrapper, history } } it('should render a table', () => { const { wrapper } = setup() - const table = wrapper.find('table') - const tableHeader = table.find('thead') - const headers = tableHeader.find('th') - const body = table.find('tbody') - const columns = body.find('tr').find('td') - - expect(headers.at(0).text()).toEqual('patient.carePlan.title') - expect(headers.at(1).text()).toEqual('patient.carePlan.startDate') - expect(headers.at(2).text()).toEqual('patient.carePlan.endDate') - expect(headers.at(3).text()).toEqual('patient.carePlan.status') - expect(headers.at(4).text()).toEqual('actions.label') + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'patient.carePlan.title', key: 'title' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.carePlan.startDate', key: 'startDate' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'patient.carePlan.endDate', key: 'endDate' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'patient.carePlan.status', key: 'status' }), + ) - expect(columns.at(0).text()).toEqual(carePlan.title) - expect(columns.at(1).text()).toEqual('2020-07-03') - expect(columns.at(2).text()).toEqual('2020-07-05') - expect(columns.at(3).text()).toEqual(carePlan.status) - expect(columns.at(4).find('button')).toHaveLength(1) + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(patient.carePlans) }) it('should navigate to the care plan view when the view details button is clicked', () => { const { wrapper, history } = setup() - const table = wrapper.find('table') - const body = table.find('tbody') - const columns = body.find('tr').find('td') + const tr = wrapper.find('tr').at(1) act(() => { - const onClick = columns.at(4).find(Button).prop('onClick') as any - onClick() + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) }) expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-plans/${carePlan.id}`) diff --git a/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx index 558b2773cf..67481533ec 100644 --- a/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx +++ b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx @@ -1,4 +1,3 @@ -import '../../../__mocks__/matchMediaMock' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -7,10 +6,10 @@ import { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Patient from '../../../model/Patient' import CarePlanForm from '../../../patients/care-plans/CarePlanForm' import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' -import { RootState } from '../../../store' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -22,7 +21,7 @@ describe('View Care Plan', () => { } as Patient const setup = () => { - const store = mockStore({ patient: { patient } } as any) + const store = mockStore({ patient: { patient }, user: { user: { id: '123' } } } as any) const history = createMemoryHistory() history.push(`/patients/${patient.id}/care-plans/${patient.carePlans[0].id}`) const wrapper = mount( diff --git a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx index c31be04acd..19707ce111 100644 --- a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx +++ b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { Modal, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' @@ -8,14 +6,14 @@ import { Provider } from 'react-redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import DatePickerWithLabelFormGroup from '../../../components/input/DatePickerWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' -import Diagnosis from '../../../model/Diagnosis' -import Patient from '../../../model/Patient' import AddDiagnosisModal from '../../../patients/diagnoses/AddDiagnosisModal' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import DatePickerWithLabelFormGroup from '../../../shared/components/input/DatePickerWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx index f53e29c021..453bb50cb2 100644 --- a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx +++ b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -10,12 +8,12 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import Diagnosis from '../../../model/Diagnosis' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' import Diagnoses from '../../../patients/diagnoses/Diagnoses' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const expectedPatient = { id: '123', diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index 1eab177acb..da394a5c40 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -1,6 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - -import { Button } from '@hospitalrun/components' import { subDays } from 'date-fns' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -11,13 +8,13 @@ import { Router, Route } from 'react-router-dom' import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import Patient from '../../../model/Patient' -import * as titleUtil from '../../../page-header/useTitle' +import * as titleUtil from '../../../page-header/title/useTitle' import EditPatient from '../../../patients/edit/EditPatient' import GeneralInformation from '../../../patients/GeneralInformation' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -33,9 +30,9 @@ describe('Edit Patient', () => { type: 'charity', occupation: 'occupation', preferredLanguage: 'preferredLanguage', - phoneNumber: '123456789', - email: 'email@email.com', - address: 'address', + phoneNumbers: [{ value: '123456789', id: '789' }], + emails: [{ value: 'email@email.com', id: '456' }], + addresses: [{ value: 'address', id: '123' }], code: 'P00001', dateOfBirth: subDays(new Date(), 2).toISOString(), index: 'givenName familyName suffixP00001', @@ -107,7 +104,7 @@ describe('Edit Patient', () => { wrapper.update() - const saveButton = wrapper.find(Button).at(0) + const saveButton = wrapper.find('.btn-save').at(0) const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') @@ -128,7 +125,7 @@ describe('Edit Patient', () => { wrapper.update() - const cancelButton = wrapper.find(Button).at(1) + const cancelButton = wrapper.find('.btn-cancel').at(1) const onClick = cancelButton.prop('onClick') as any expect(cancelButton.text().trim()).toEqual('actions.cancel') diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx index 5a36aca1c9..bd7d7fa884 100644 --- a/src/__tests__/patients/labs/LabsTab.test.tsx +++ b/src/__tests__/patients/labs/LabsTab.test.tsx @@ -1,7 +1,6 @@ -import '../../../__mocks__/matchMediaMock' import * as components from '@hospitalrun/components' -import format from 'date-fns/format' -import { mount } from 'enzyme' +import { Table } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -10,21 +9,21 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import LabRepository from '../../../clients/db/LabRepository' -import Lab from '../../../model/Lab' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' import LabsTab from '../../../patients/labs/LabsTab' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const expectedPatient = { id: '123', } as Patient -const labs = [ +const expectedLabs = [ { id: 'labId', - patientId: '123', + patient: '123', type: 'type', status: 'requested', requestedOn: new Date().toISOString(), @@ -37,55 +36,52 @@ const history = createMemoryHistory() let user: any let store: any -const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => { - user = { permissions } - store = mockStore({ patient, user } as any) - jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue(labs) - const wrapper = mount( - - - - - , - ) +const setup = async (labs = expectedLabs) => { + jest.resetAllMocks() + user = { permissions: [Permissions.ReadPatients] } + store = mockStore({ patient: expectedPatient, user } as any) + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue(labs) - return wrapper + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + return { wrapper: wrapper as ReactWrapper } } describe('Labs Tab', () => { it('should list the patients labs', async () => { - const expectedLabs = labs - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - wrapper.update() - - const table = wrapper.find('table') - const tableHeader = wrapper.find('thead') - const tableHeaders = wrapper.find('th') - const tableBody = wrapper.find('tbody') - const tableData = wrapper.find('td') + const { wrapper } = await setup() - expect(table).toHaveLength(1) - expect(tableHeader).toHaveLength(1) - expect(tableBody).toHaveLength(1) - expect(tableHeaders.at(0).text()).toEqual('labs.lab.type') - expect(tableHeaders.at(1).text()).toEqual('labs.lab.requestedOn') - expect(tableHeaders.at(2).text()).toEqual('labs.lab.status') - expect(tableData.at(0).text()).toEqual(expectedLabs[0].type) - expect(tableData.at(1).text()).toEqual( - format(new Date(expectedLabs[0].requestedOn), 'yyyy-MM-dd hh:mm a'), + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), ) - expect(tableData.at(2).text()).toEqual(expectedLabs[0].status) + expect(columns[2]).toEqual( + expect.objectContaining({ + label: 'labs.lab.status', + key: 'status', + }), + ) + + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(expectedLabs) }) it('should render a warning message if the patient does not have any labs', async () => { - let wrapper: any - - await act(async () => { - wrapper = await setup({ ...expectedPatient }) - }) + const { wrapper } = await setup([]) const alert = wrapper.find(components.Alert) diff --git a/src/__tests__/patients/list/ViewPatients.test.tsx b/src/__tests__/patients/list/ViewPatients.test.tsx index 0e730dadb7..35a8afdcae 100644 --- a/src/__tests__/patients/list/ViewPatients.test.tsx +++ b/src/__tests__/patients/list/ViewPatients.test.tsx @@ -1,7 +1,4 @@ -import '../../../__mocks__/matchMediaMock' - -import { TextInput, Spinner, Select } from '@hospitalrun/components' -import format from 'date-fns/format' +import { TextInput, Spinner, Table } from '@hospitalrun/components' import { mount } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' @@ -11,43 +8,32 @@ import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import { UnpagedRequest } from '../../../clients/db/PageRequest' -import PatientRepository from '../../../clients/db/PatientRepository' -import SortRequest from '../../../clients/db/SortRequest' -import Page from '../../../clients/Page' -import { defaultPageSize } from '../../../components/PageComponent' -import Patient from '../../../model/Patient' -import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' import ViewPatients from '../../../patients/list/ViewPatients' import * as patientSlice from '../../../patients/patients-slice' +import { UnpagedRequest } from '../../../shared/db/PageRequest' +import PatientRepository from '../../../shared/db/PatientRepository' const middlewares = [thunk] const mockStore = configureStore(middlewares) describe('Patients', () => { - const patients: Page = { - content: [ - { - id: '123', - fullName: 'test test', - isApproximateDateOfBirth: false, - givenName: 'test', - familyName: 'test', - code: 'P12345', - sex: 'male', - dateOfBirth: new Date().toISOString(), - phoneNumber: '99999999', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - rev: '', - index: 'test test P12345', - }, - ], - hasNext: false, - hasPrevious: false, - pageRequest: UnpagedRequest, - } - const mockedPatientRepository = mocked(PatientRepository, true) + const patients = [ + { + id: '123', + fullName: 'test test', + isApproximateDateOfBirth: false, + givenName: 'test', + familyName: 'test', + code: 'P12345', + sex: 'male', + dateOfBirth: new Date().toISOString(), + phoneNumber: '99999999', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + rev: '', + }, + ] const setup = (isLoading?: boolean) => { const store = mockStore({ @@ -68,43 +54,18 @@ describe('Patients', () => { beforeEach(() => { jest.resetAllMocks() - jest.spyOn(PatientRepository, 'findAll') - jest.spyOn(PatientRepository, 'searchPaged') - jest.spyOn(PatientRepository, 'findAllPaged') - - mockedPatientRepository.findAll.mockResolvedValue([]) - mockedPatientRepository.findAllPaged.mockResolvedValue( - new Promise>((resolve) => { - const pagedResult: Page = { - content: [], - hasPrevious: false, - hasNext: false, - } - resolve(pagedResult) - }), - ) - - mockedPatientRepository.searchPaged.mockResolvedValue( - new Promise>((resolve) => { - const pagedResult: Page = { - content: [], - hasPrevious: false, - hasNext: false, - } - resolve(pagedResult) - }), - ) + jest.spyOn(PatientRepository, 'findAll').mockResolvedValue([]) + jest.spyOn(PatientRepository, 'search').mockResolvedValue([]) }) - describe('initalLoad', () => { + describe('initial load', () => { afterEach(() => { jest.restoreAllMocks() }) it('should call fetchPatients only once', () => { setup() - const findAllPagedSpy = jest.spyOn(PatientRepository, 'findAllPaged') - expect(findAllPagedSpy).toHaveBeenCalledTimes(1) + expect(PatientRepository.findAll).toHaveBeenCalledTimes(1) }) }) @@ -122,26 +83,27 @@ describe('Patients', () => { it('should render a table of patients', () => { const wrapper = setup() - const table = wrapper.find('table') - const tableHeaders = table.find('th') - const tableColumns = table.find('td') + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any expect(table).toHaveLength(1) - expect(tableHeaders).toHaveLength(5) - expect(tableColumns).toHaveLength(5) - expect(tableHeaders.at(0).text()).toEqual('patient.code') - expect(tableHeaders.at(1).text()).toEqual('patient.givenName') - expect(tableHeaders.at(2).text()).toEqual('patient.familyName') - expect(tableHeaders.at(3).text()).toEqual('patient.sex') - expect(tableHeaders.at(4).text()).toEqual('patient.dateOfBirth') - - expect(tableColumns.at(0).text()).toEqual(patients.content[0].code) - expect(tableColumns.at(1).text()).toEqual(patients.content[0].givenName) - expect(tableColumns.at(2).text()).toEqual(patients.content[0].familyName) - expect(tableColumns.at(3).text()).toEqual(patients.content[0].sex) - expect(tableColumns.at(4).text()).toEqual( - format(new Date(patients.content[0].dateOfBirth), 'yyyy-MM-dd'), + + expect(columns[0]).toEqual(expect.objectContaining({ label: 'patient.code', key: 'code' })) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.givenName', key: 'givenName' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'patient.familyName', key: 'familyName' }), ) + expect(columns[3]).toEqual(expect.objectContaining({ label: 'patient.sex', key: 'sex' })) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'patient.dateOfBirth', key: 'dateOfBirth' }), + ) + + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('data')).toEqual(patients) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') }) it('should add a "New Patient" button to the button tool bar', () => { @@ -156,47 +118,6 @@ describe('Patients', () => { }) }) - describe('change page size', () => { - afterEach(() => { - jest.restoreAllMocks() - }) - it('should call the change handler on change', () => { - const searchPagedSpy = jest.spyOn(patientSlice, 'searchPatients') - const wrapper = setup() - const sortRequest: SortRequest = { - sorts: [{ field: 'index', direction: 'asc' }], - } - - expect(searchPagedSpy).toBeCalledWith('', sortRequest, { - direction: 'next', - nextPageInfo: { index: null }, - number: 1, - previousPageInfo: { index: null }, - size: defaultPageSize.value, - }) - - act(() => { - ;(wrapper.find(Select).prop('onChange') as any)({ - target: { - value: '50', - }, - } as React.ChangeEvent) - }) - - wrapper.update() - - expect(searchPagedSpy).toHaveBeenCalledTimes(2) - - expect(searchPagedSpy).toBeCalledWith('', sortRequest, { - direction: 'next', - nextPageInfo: { index: null }, - number: 1, - previousPageInfo: { index: null }, - size: 50, - }) - }) - }) - describe('search functionality', () => { beforeEach(() => jest.useFakeTimers()) @@ -209,14 +130,8 @@ describe('Patients', () => { const expectedSearchText = 'search text' act(() => { - ;(wrapper.find(TextInput).prop('onChange') as any)({ - target: { - value: expectedSearchText, - }, - preventDefault(): void { - // noop - }, - } as React.ChangeEvent) + const onChange = wrapper.find(TextInput).prop('onChange') as any + onChange({ target: { value: expectedSearchText } }) }) act(() => { @@ -226,19 +141,9 @@ describe('Patients', () => { wrapper.update() expect(searchPatientsSpy).toHaveBeenCalledTimes(1) - expect(searchPatientsSpy).toHaveBeenLastCalledWith( - expectedSearchText, - { - sorts: [{ field: 'index', direction: 'asc' }], - }, - { - number: 1, - size: defaultPageSize.value, - nextPageInfo: { index: null }, - direction: 'next', - previousPageInfo: { index: null }, - }, - ) + expect(searchPatientsSpy).toHaveBeenLastCalledWith(expectedSearchText, { + sorts: [{ field: 'index', direction: 'asc' }], + }) }) }) }) diff --git a/src/__tests__/patients/new/NewPatient.test.tsx b/src/__tests__/patients/new/NewPatient.test.tsx index f5237c1091..61ea79d945 100644 --- a/src/__tests__/patients/new/NewPatient.test.tsx +++ b/src/__tests__/patients/new/NewPatient.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -11,13 +9,13 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import PatientRepository from '../../../clients/db/PatientRepository' -import Patient from '../../../model/Patient' -import * as titleUtil from '../../../page-header/useTitle' +import * as titleUtil from '../../../page-header/title/useTitle' import GeneralInformation from '../../../patients/GeneralInformation' import NewPatient from '../../../patients/new/NewPatient' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -96,12 +94,12 @@ describe('New Patient', () => { const generalInformationForm = wrapper.find(GeneralInformation) act(() => { - generalInformationForm.prop('onFieldChange')('givenName', 'first') + generalInformationForm.prop('onChange')(patient) }) wrapper.update() - const saveButton = wrapper.find(components.Button).at(0) + const saveButton = wrapper.find('.btn-save').at(0) const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') @@ -125,12 +123,12 @@ describe('New Patient', () => { const generalInformationForm = wrapper.find(GeneralInformation) act(() => { - generalInformationForm.prop('onFieldChange')('givenName', 'first') + generalInformationForm.prop('onChange')(patient) }) wrapper.update() - const saveButton = wrapper.find(components.Button).at(0) + const saveButton = wrapper.find('.btn-save').at(0) const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') @@ -152,7 +150,7 @@ describe('New Patient', () => { wrapper = await setup() }) - const cancelButton = wrapper.find(components.Button).at(1) + const cancelButton = wrapper.find('.btn-cancel').at(0) const onClick = cancelButton.prop('onClick') as any expect(cancelButton.text().trim()).toEqual('actions.cancel') diff --git a/src/__tests__/patients/notes/NewNoteModal.test.tsx b/src/__tests__/patients/notes/NewNoteModal.test.tsx index ba9a4abd7d..e71cf5f5ca 100644 --- a/src/__tests__/patients/notes/NewNoteModal.test.tsx +++ b/src/__tests__/patients/notes/NewNoteModal.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { Alert, Modal } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' @@ -8,12 +6,12 @@ import { Provider } from 'react-redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import TextFieldWithLabelFormGroup from '../../../components/input/TextFieldWithLabelFormGroup' -import Patient from '../../../model/Patient' import NewNoteModal from '../../../patients/notes/NewNoteModal' import * as patientSlice from '../../../patients/patient-slice' -import { RootState } from '../../../store' +import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/patients/notes/NotesTab.test.tsx b/src/__tests__/patients/notes/NotesTab.test.tsx index b4191e03ae..1f4cda1b77 100644 --- a/src/__tests__/patients/notes/NotesTab.test.tsx +++ b/src/__tests__/patients/notes/NotesTab.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -10,12 +8,12 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import Note from '../../../model/Note' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' import NoteTab from '../../../patients/notes/NoteTab' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Note from '../../../shared/model/Note' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const expectedPatient = { id: '123', diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index 9ce0c2ddf6..4ce21fd54a 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -1,16 +1,8 @@ -import '../../__mocks__/matchMediaMock' - import { addDays, subDays } from 'date-fns' import { AnyAction } from 'redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../clients/db/PatientRepository' -import Allergy from '../../model/Allergy' -import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' -import Diagnosis from '../../model/Diagnosis' -import Patient from '../../model/Patient' -import RelatedPerson from '../../model/RelatedPerson' import patient, { addAllergy, addAllergyError, @@ -33,8 +25,14 @@ import patient, { updatePatientSuccess, addCarePlanError, } from '../../patients/patient-slice' -import { RootState } from '../../store' -import * as uuid from '../../util/uuid' +import PatientRepository from '../../shared/db/PatientRepository' +import Allergy from '../../shared/model/Allergy' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../shared/model/CarePlan' +import Diagnosis from '../../shared/model/Diagnosis' +import Patient from '../../shared/model/Patient' +import RelatedPerson from '../../shared/model/RelatedPerson' +import { RootState } from '../../shared/store' +import * as uuid from '../../shared/util/uuid' const mockStore = createMockStore([thunk]) @@ -167,6 +165,7 @@ describe('patients slice', () => { const expectedPatient = { id: 'sliceId1', givenName: 'some name', + fullName: 'some name', } as Patient await store.dispatch(createPatient(expectedPatient)) @@ -251,13 +250,13 @@ describe('patients slice', () => { ) }) - it('should validate that the patient email is a valid email', async () => { + it('should validate that the patient phone number is a valid phone number', async () => { const store = mockStore() const expectedPatientId = 'sliceId10' const expectedPatient = { id: expectedPatientId, givenName: 'some given name', - phoneNumber: 'not a phone number', + phoneNumbers: [{ value: 'not a phone number' }], } as Patient const saveOrUpdateSpy = jest .spyOn(PatientRepository, 'saveOrUpdate') @@ -271,18 +270,18 @@ describe('patients slice', () => { expect(store.getActions()[1]).toEqual( createPatientError({ message: 'patient.errors.createPatientError', - phoneNumber: 'patient.errors.invalidPhoneNumber', + phoneNumbers: ['patient.errors.invalidPhoneNumber'], }), ) }) - it('should validate that the patient phone number is a valid phone number', async () => { + it('should validate that the patient email is a valid email', async () => { const store = mockStore() const expectedPatientId = 'sliceId10' const expectedPatient = { id: expectedPatientId, givenName: 'some given name', - phoneNumber: 'not a phone number', + emails: [{ value: 'not an email' }], } as Patient const saveOrUpdateSpy = jest .spyOn(PatientRepository, 'saveOrUpdate') @@ -296,7 +295,7 @@ describe('patients slice', () => { expect(store.getActions()[1]).toEqual( createPatientError({ message: 'patient.errors.createPatientError', - phoneNumber: 'patient.errors.invalidPhoneNumber', + emails: ['patient.errors.invalidEmail'], }), ) }) @@ -383,7 +382,11 @@ describe('patients slice', () => { it('should call the PatientRepository saveOrUpdate function with the correct data', async () => { const store = mockStore() const expectedPatientId = 'sliceId9' - const expectedPatient = { id: expectedPatientId, givenName: 'some name' } as Patient + const expectedPatient = { + id: expectedPatientId, + givenName: 'some name', + fullName: 'some name', + } as Patient jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) await store.dispatch(updatePatient(expectedPatient)) @@ -694,7 +697,6 @@ describe('patients slice', () => { startDate: 'patient.carePlan.error.startDateRequired', endDate: 'patient.carePlan.error.endDateRequired', condition: 'patient.carePlan.error.conditionRequired', - note: 'patient.carePlan.error.noteRequired', } const store = mockStore() const expectedCarePlan = {} as CarePlan diff --git a/src/__tests__/patients/patients-slice.test.ts b/src/__tests__/patients/patients-slice.test.ts index 1ece46e5c4..84565aedd2 100644 --- a/src/__tests__/patients/patients-slice.test.ts +++ b/src/__tests__/patients/patients-slice.test.ts @@ -1,19 +1,31 @@ -import '../../__mocks__/matchMediaMock' - import { AnyAction } from 'redux' -import { mocked } from 'ts-jest/utils' -import { UnpagedRequest } from '../../clients/db/PageRequest' -import PatientRepository from '../../clients/db/PatientRepository' -import Page from '../../clients/Page' -import Patient from '../../model/Patient' import patients, { fetchPatientsStart, fetchPatientsSuccess, searchPatients, } from '../../patients/patients-slice' +import PatientRepository from '../../shared/db/PatientRepository' +import Patient from '../../shared/model/Patient' describe('patients slice', () => { + const expectedPatients = [ + { + id: '123', + fullName: 'test test', + isApproximateDateOfBirth: false, + givenName: 'test', + familyName: 'test', + code: 'P12345', + sex: 'male', + dateOfBirth: new Date().toISOString(), + phoneNumber: '99999999', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + rev: '', + } as Patient, + ] + beforeEach(() => { jest.resetAllMocks() }) @@ -22,31 +34,10 @@ describe('patients slice', () => { it('should create the proper initial state with empty patients array', () => { const patientsStore = patients(undefined, {} as AnyAction) expect(patientsStore.isLoading).toBeFalsy() - expect(patientsStore.patients.content).toHaveLength(0) + expect(patientsStore.patients).toHaveLength(0) }) it('should handle the FETCH_PATIENTS_SUCCESS action', () => { - const expectedPatients = { - content: [ - { - id: '123', - fullName: 'test test', - isApproximateDateOfBirth: false, - givenName: 'test', - familyName: 'test', - code: 'P12345', - sex: 'male', - dateOfBirth: new Date().toISOString(), - phoneNumber: '99999999', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - rev: '', - }, - ], - hasNext: false, - hasPrevious: false, - pageRequest: UnpagedRequest, - } const patientsStore = patients(undefined, { type: fetchPatientsSuccess.type, payload: expectedPatients, @@ -59,31 +50,8 @@ describe('patients slice', () => { describe('searchPatients', () => { beforeEach(() => { - const mockedPatientRepository = mocked(PatientRepository, true) - jest.spyOn(PatientRepository, 'findAllPaged') - jest.spyOn(PatientRepository, 'searchPaged') - - mockedPatientRepository.findAllPaged.mockResolvedValue( - new Promise>((resolve) => { - const pagedResult: Page = { - content: [], - hasPrevious: false, - hasNext: false, - } - resolve(pagedResult) - }), - ) - - mockedPatientRepository.searchPaged.mockResolvedValue( - new Promise>((resolve) => { - const pagedResult: Page = { - content: [], - hasPrevious: false, - hasNext: false, - } - resolve(pagedResult) - }), - ) + jest.spyOn(PatientRepository, 'findAll').mockResolvedValue(expectedPatients) + jest.spyOn(PatientRepository, 'search').mockResolvedValue(expectedPatients) }) it('should dispatch the FETCH_PATIENTS_START action', async () => { @@ -95,28 +63,25 @@ describe('patients slice', () => { expect(dispatch).toHaveBeenCalledWith({ type: fetchPatientsStart.type }) }) - it('should call the PatientRepository searchPaged method with the correct search criteria', async () => { + it('should call the PatientRepository search method with the correct search criteria', async () => { const dispatch = jest.fn() const getState = jest.fn() - jest.spyOn(PatientRepository, 'searchPaged') + jest.spyOn(PatientRepository, 'search') const expectedSearchString = 'search string' await searchPatients(expectedSearchString)(dispatch, getState, null) - expect(PatientRepository.searchPaged).toHaveBeenCalledWith( - expectedSearchString, - UnpagedRequest, - ) + expect(PatientRepository.search).toHaveBeenCalledWith(expectedSearchString) }) - it('should call the PatientRepository findAllPaged method if there is no string text', async () => { + it('should call the PatientRepository findAll method if there is no string text', async () => { const dispatch = jest.fn() const getState = jest.fn() - jest.spyOn(PatientRepository, 'findAllPaged') + jest.spyOn(PatientRepository, 'findAll') await searchPatients('')(dispatch, getState, null) - expect(PatientRepository.findAllPaged).toHaveBeenCalledTimes(1) + expect(PatientRepository.findAll).toHaveBeenCalledTimes(1) }) it('should dispatch the FETCH_PATIENTS_SUCCESS action', async () => { @@ -127,11 +92,7 @@ describe('patients slice', () => { expect(dispatch).toHaveBeenLastCalledWith({ type: fetchPatientsSuccess.type, - payload: { - content: [], - hasPrevious: false, - hasNext: false, - }, + payload: expectedPatients, }) }) }) diff --git a/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx b/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx index 37f65a319f..bbf919c155 100644 --- a/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx +++ b/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { Modal, Alert, Typeahead } from '@hospitalrun/components' import { act } from '@testing-library/react' import { ReactWrapper, mount } from 'enzyme' @@ -8,12 +6,12 @@ import { Provider } from 'react-redux' import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' -import Patient from '../../../model/Patient' import * as patientSlice from '../../../patients/patient-slice' import AddRelatedPersonModal from '../../../patients/related-persons/AddRelatedPersonModal' -import { RootState } from '../../../store' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx b/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx similarity index 72% rename from src/__tests__/patients/related-persons/RelatedPersons.test.tsx rename to src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx index 2beacd71b9..2fce3faaa8 100644 --- a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx +++ b/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx @@ -1,6 +1,5 @@ -import '../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' +import { Table } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -10,14 +9,13 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import PatientRepository from '../../../clients/db/PatientRepository' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' -import RelatedPerson from '../../../model/RelatedPerson' import * as patientSlice from '../../../patients/patient-slice' import AddRelatedPersonModal from '../../../patients/related-persons/AddRelatedPersonModal' import RelatedPersonTab from '../../../patients/related-persons/RelatedPersonTab' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -41,6 +39,7 @@ describe('Related Persons Tab', () => { jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue([]) user = { permissions: [Permissions.WritePatients, Permissions.ReadPatients], @@ -111,6 +110,7 @@ describe('Related Persons Tab', () => { familyName: 'test', fullName: 'test test', id: '123001', + type: 'type', } as Patient const user = { @@ -134,51 +134,45 @@ describe('Related Persons Tab', () => { }) it('should render a list of related persons with their full name being displayed', () => { - const table = wrapper.find('table') - const tableHeader = wrapper.find('thead') - const tableHeaders = wrapper.find('th') - const tableBody = wrapper.find('tbody') - const tableData = wrapper.find('td') - const deleteButton = tableData.at(3).find(components.Button) - expect(table).toHaveLength(1) - expect(tableHeader).toHaveLength(1) - expect(tableBody).toHaveLength(1) - expect(tableHeaders.at(0).text()).toEqual('patient.givenName') - expect(tableHeaders.at(1).text()).toEqual('patient.familyName') - expect(tableHeaders.at(2).text()).toEqual('patient.relatedPersons.relationshipType') - expect(tableHeaders.at(3).text()).toEqual('actions.label') - expect(tableData.at(0).text()).toEqual(expectedRelatedPerson.givenName) - expect(tableData.at(1).text()).toEqual(expectedRelatedPerson.familyName) - expect(tableData.at(2).text()).toEqual((patient.relatedPersons as RelatedPerson[])[0].type) - expect(deleteButton).toHaveLength(1) - expect(deleteButton.text().trim()).toEqual('actions.delete') - expect(deleteButton.prop('color')).toEqual('danger') + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'patient.givenName', key: 'givenName' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.familyName', key: 'familyName' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ + label: 'patient.relatedPersons.relationshipType', + key: 'type', + }), + ) + + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(actions[1]).toEqual(expect.objectContaining({ label: 'actions.delete' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual([expectedRelatedPerson]) }) it('should remove the related person when the delete button is clicked', async () => { const removeRelatedPersonSpy = jest.spyOn(patientSlice, 'removeRelatedPerson') - const eventPropagationSpy = jest.fn() - - const table = wrapper.find('table') - const tableBody = table.find('tbody') - const tableData = tableBody.find('td') - const deleteButton = tableData.at(3).find(components.Button) + const tr = wrapper.find('tr').at(1) - await act(async () => { - const onClick = deleteButton.prop('onClick') - await onClick({ stopPropagation: eventPropagationSpy }) + act(() => { + const onClick = tr.find('button').at(1).prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) }) - expect(removeRelatedPersonSpy).toHaveBeenCalledWith(patient.id, expectedRelatedPerson.id) }) it('should navigate to related person patient profile on related person click', async () => { - const table = wrapper.find('table') - const tableBody = table.find('tbody') - const row = tableBody.find('tr') - await act(async () => { - const onClick = row.prop('onClick') - await onClick() + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').at(0).prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) }) expect(history.location.pathname).toEqual('/patients/123001') diff --git a/src/__tests__/patients/util/patient-name-util.test.ts b/src/__tests__/patients/util/patient-name-util.test.ts index ef668c36fd..f0fd700634 100644 --- a/src/__tests__/patients/util/patient-name-util.test.ts +++ b/src/__tests__/patients/util/patient-name-util.test.ts @@ -1,5 +1,5 @@ -import Patient from '../../../model/Patient' import { getPatientFullName, getPatientName } from '../../../patients/util/patient-name-util' +import Patient from '../../../shared/model/Patient' describe('patient name util', () => { describe('getPatientName', () => { diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 3cc651b1e3..1bd7d808a2 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -1,7 +1,5 @@ -import '../../../__mocks__/matchMediaMock' - import { TabsHeader, Tab } from '@hospitalrun/components' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -11,12 +9,8 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import LabRepository from '../../../clients/db/LabRepository' -import PatientRepository from '../../../clients/db/PatientRepository' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' -import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' -import * as titleUtil from '../../../page-header/useTitle' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' import Allergies from '../../../patients/allergies/Allergies' import AppointmentsList from '../../../patients/appointments/AppointmentsList' import CarePlanTab from '../../../patients/care-plans/CarePlanTab' @@ -27,7 +21,10 @@ import NotesTab from '../../../patients/notes/NoteTab' import * as patientSlice from '../../../patients/patient-slice' import RelatedPersonTab from '../../../patients/related-persons/RelatedPersonTab' import ViewPatient from '../../../patients/view/ViewPatient' -import { RootState } from '../../../store' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -52,9 +49,9 @@ describe('ViewPatient', () => { let history: any let store: MockStore - const setup = (permissions = [Permissions.ReadPatients]) => { + const setup = async (permissions = [Permissions.ReadPatients]) => { jest.spyOn(PatientRepository, 'find') - jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue([]) + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue([]) const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) history = createMemoryHistory() @@ -65,18 +62,21 @@ describe('ViewPatient', () => { } as any) history.push('/patients/123') - const wrapper = mount( - - - - - - - , - ) - + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + , + ) + }) wrapper.update() - return wrapper + + return { wrapper: wrapper as ReactWrapper } } beforeEach(() => { @@ -84,9 +84,7 @@ describe('ViewPatient', () => { }) it('should dispatch fetchPatient when component loads', async () => { - await act(async () => { - await setup() - }) + await setup() expect(PatientRepository.find).toHaveBeenCalledWith(patient.id) expect(store.getActions()).toContainEqual(patientSlice.fetchPatientStart()) @@ -95,41 +93,38 @@ describe('ViewPatient', () => { it('should render a header with the patients given, family, and suffix', async () => { jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) + + await setup() + expect(titleUtil.default).toHaveBeenCalledWith( `${patient.givenName} ${patient.familyName} ${patient.suffix} (${patient.code})`, ) }) - it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', () => { + it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', async () => { jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup([Permissions.WritePatients]) + await setup([Permissions.WritePatients]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('actions.edit') }) - it('button toolbar empty if only has ReadPatients permission', () => { + it('button toolbar empty if only has ReadPatients permission', async () => { jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup() + await setup() const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons.length).toEqual(0) }) it('should render a tabs header with the correct tabs', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) @@ -147,10 +142,7 @@ describe('ViewPatient', () => { }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) @@ -161,10 +153,7 @@ describe('ViewPatient', () => { }) it('should navigate /patients/:id when the general information tab is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) @@ -178,15 +167,13 @@ describe('ViewPatient', () => { }) it('should mark the related persons tab as active when it is clicked and render the Related Person Tab component when route is /patients/:id/relatedpersons', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(1).prop('onClick')() + const onClick = tabs.at(1).prop('onClick') as any + onClick() }) wrapper.update() @@ -202,15 +189,13 @@ describe('ViewPatient', () => { }) it('should mark the appointments tab as active when it is clicked and render the appointments tab component when route is /patients/:id/appointments', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(2).prop('onClick')() + const onClick = tabs.at(2).prop('onClick') as any + onClick() }) wrapper.update() @@ -226,15 +211,13 @@ describe('ViewPatient', () => { }) it('should mark the allergies tab as active when it is clicked and render the allergies component when route is /patients/:id/allergies', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(3).prop('onClick')() + const onClick = tabs.at(3).prop('onClick') as any + onClick() }) wrapper.update() @@ -250,15 +233,13 @@ describe('ViewPatient', () => { }) it('should mark the diagnoses tab as active when it is clicked and render the diagnoses component when route is /patients/:id/diagnoses', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(4).prop('onClick')() + const onClick = tabs.at(4).prop('onClick') as any + onClick() }) wrapper.update() @@ -274,15 +255,13 @@ describe('ViewPatient', () => { }) it('should mark the notes tab as active when it is clicked and render the note component when route is /patients/:id/notes', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(5).prop('onClick')() + const onClick = tabs.at(5).prop('onClick') as any + onClick() }) wrapper.update() @@ -298,15 +277,13 @@ describe('ViewPatient', () => { }) it('should mark the labs tab as active when it is clicked and render the lab component when route is /patients/:id/labs', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(6).prop('onClick')() + const onClick = tabs.at(6).prop('onClick') as any + onClick() }) wrapper.update() @@ -322,15 +299,13 @@ describe('ViewPatient', () => { }) it('should mark the care plans tab as active when it is clicked and render the care plan tab component when route is /patients/:id/care-plans', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) + const { wrapper } = await setup() await act(async () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - tabs.at(7).prop('onClick')() + const onClick = tabs.at(7).prop('onClick') as any + onClick() }) wrapper.update() diff --git a/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx b/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx index 3b57080793..7d810ac8d9 100644 --- a/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx +++ b/src/__tests__/scheduling/appointments/AppointmentDetailForm.test.tsx @@ -1,15 +1,13 @@ -import '../../../__mocks__/matchMediaMock' - import { Typeahead, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import { roundToNearestMinutes, addMinutes } from 'date-fns' import { mount, ReactWrapper } from 'enzyme' import React from 'react' -import PatientRepository from '../../../clients/db/PatientRepository' -import Appointment from '../../../model/Appointment' -import Patient from '../../../model/Patient' import AppointmentDetailForm from '../../../scheduling/appointments/AppointmentDetailForm' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../../shared/model/Appointment' +import Patient from '../../../shared/model/Patient' describe('AppointmentDetailForm', () => { describe('Error handling', () => { @@ -66,7 +64,7 @@ describe('AppointmentDetailForm', () => { expect(patientTypeahead).toHaveLength(1) expect(patientTypeahead.prop('placeholder')).toEqual('scheduling.appointment.patient') - expect(patientTypeahead.prop('value')).toEqual(expectedAppointment.patientId) + expect(patientTypeahead.prop('value')).toEqual(expectedAppointment.patient) }) it('should render as start date date time picker', () => { @@ -112,7 +110,7 @@ describe('AppointmentDetailForm', () => { expect(typeSelect.prop('options')[3].value).toEqual('routine') expect(typeSelect.prop('options')[4].label).toEqual('scheduling.appointment.types.walkIn') expect(typeSelect.prop('options')[4].value).toEqual('walk in') - expect(typeSelect.prop('value')).toEqual(expectedAppointment.type) + expect(typeSelect.prop('defaultSelected')[0].value).toEqual(expectedAppointment.type) }) it('should render a reason text field input', () => { @@ -149,7 +147,7 @@ describe('AppointmentDetailForm', () => { describe('layout - not editable', () => { let wrapper: ReactWrapper const expectedAppointment = { - patientId: 'patientId', + patient: 'patientId', startDateTime: roundToNearestMinutes(new Date(), { nearestTo: 15 }).toISOString(), endDateTime: addMinutes( roundToNearestMinutes(new Date(), { nearestTo: 15 }), @@ -217,7 +215,7 @@ describe('AppointmentDetailForm', () => { }) wrapper.update() - expect(onFieldChange).toHaveBeenLastCalledWith('patientId', expectedPatientId) + expect(onFieldChange).toHaveBeenLastCalledWith('patient', expectedPatientId) }) it('should call onFieldChange when start date time changes', () => { @@ -263,18 +261,6 @@ describe('AppointmentDetailForm', () => { expect(onFieldChange).toHaveBeenLastCalledWith('location', expectedLocation) }) - it('should call onFieldChange when type changes', () => { - const expectedType = 'follow up' - - act(() => { - const typeSelect = wrapper.findWhere((w) => w.prop('name') === 'type') - typeSelect.prop('onChange')({ target: { value: expectedType } }) - }) - wrapper.update() - - expect(onFieldChange).toHaveBeenLastCalledWith('type', expectedType) - }) - it('should call onFieldChange when reason changes', () => { const expectedReason = 'reason' diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 02d42fd37f..2de4c800d8 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -1,5 +1,3 @@ -import '../../../__mocks__/matchMediaMock' - import { mount } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' @@ -8,18 +6,19 @@ import { MemoryRouter } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import { addBreadcrumbs } from '../../../breadcrumbs/breadcrumbs-slice' -import AppointmentRepository from '../../../clients/db/AppointmentRepository' -import PatientRepository from '../../../clients/db/PatientRepository' import Dashboard from '../../../dashboard/Dashboard' import HospitalRun from '../../../HospitalRun' -import Appointment from '../../../model/Appointment' -import Patient from '../../../model/Patient' -import Permissions from '../../../model/Permissions' +import { addBreadcrumbs } from '../../../page-header/breadcrumbs/breadcrumbs-slice' +import Appointments from '../../../scheduling/appointments/Appointments' import EditAppointment from '../../../scheduling/appointments/edit/EditAppointment' import NewAppointment from '../../../scheduling/appointments/new/NewAppointment' import ViewAppointments from '../../../scheduling/appointments/ViewAppointments' -import { RootState } from '../../../store' +import AppointmentRepository from '../../../shared/db/AppointmentRepository' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../../shared/model/Appointment' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -36,7 +35,7 @@ describe('/appointments', () => { const wrapper = mount( - + , ) @@ -60,7 +59,7 @@ describe('/appointments', () => { { const wrapper = mount( - + , ) @@ -110,7 +109,7 @@ describe('/appointments/new', () => { { it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { const appointment = { id: '123', - patientId: '456', + patient: '456', } as Appointment const patient = { @@ -150,7 +149,7 @@ describe('/appointments/edit/:id', () => { const wrapper = mount( - + , ) @@ -175,7 +174,7 @@ describe('/appointments/edit/:id', () => { { { const expectedAppointments = [ { id: '123', rev: '1', - patientId: '1234', + patient: '1234', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts index 428c95be1e..ef37235a47 100644 --- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts @@ -1,13 +1,7 @@ -import '../../../__mocks__/matchMediaMock' - import { subDays } from 'date-fns' import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' -import AppointmentRepository from '../../../clients/db/AppointmentRepository' -import PatientRepository from '../../../clients/db/PatientRepository' -import Appointment from '../../../model/Appointment' -import Patient from '../../../model/Patient' import appointment, { fetchAppointmentStart, fetchAppointmentSuccess, @@ -24,6 +18,10 @@ import appointment, { deleteAppointmentStart, deleteAppointmentSuccess, } from '../../../scheduling/appointments/appointment-slice' +import AppointmentRepository from '../../../shared/db/AppointmentRepository' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../../shared/model/Appointment' +import Patient from '../../../shared/model/Patient' describe('appointment slice', () => { describe('appointment reducer', () => { @@ -59,7 +57,7 @@ describe('appointment slice', () => { it('should handle the UPDATE_APPOINTMENT_SUCCESS action', () => { const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -130,7 +128,7 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -150,7 +148,7 @@ describe('appointment slice', () => { mocked(AppointmentRepository, true).save.mockResolvedValue({ id: '123' } as Appointment) const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -175,7 +173,7 @@ describe('appointment slice', () => { const getState = jest.fn() const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -218,12 +216,14 @@ describe('appointment slice', () => { const expectedAppointment: Appointment = { id: '1', rev: '1', - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', type: 'type', reason: 'reason', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), } const expectedPatient: Patient = { @@ -333,7 +333,7 @@ describe('appointment slice', () => { jest.spyOn(AppointmentRepository, 'saveOrUpdate') const expectedAppointment = { - patientId: 'sliceId9', + patient: 'sliceId9', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -355,7 +355,7 @@ describe('appointment slice', () => { jest.spyOn(AppointmentRepository, 'saveOrUpdate') const expectedAppointment = { - patientId: 'sliceId10', + patient: 'sliceId10', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -377,7 +377,7 @@ describe('appointment slice', () => { jest.spyOn(AppointmentRepository, 'saveOrUpdate') const expectedAppointment = { - patientId: 'sliceId11', + patient: 'sliceId11', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', @@ -403,7 +403,7 @@ describe('appointment slice', () => { jest.spyOn(AppointmentRepository, 'saveOrUpdate') const expectedAppointment = { - patientId: 'sliceId12', + patient: 'sliceId12', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', diff --git a/src/__tests__/scheduling/appointments/appointments-slice.test.ts b/src/__tests__/scheduling/appointments/appointments-slice.test.ts index 46272dd887..656fbd79a3 100644 --- a/src/__tests__/scheduling/appointments/appointments-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointments-slice.test.ts @@ -1,13 +1,13 @@ import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' -import AppointmentRepository from '../../../clients/db/AppointmentRepository' -import Appointment from '../../../model/Appointment' import appointments, { fetchAppointmentsStart, fetchAppointmentsSuccess, fetchAppointments, } from '../../../scheduling/appointments/appointments-slice' +import AppointmentRepository from '../../../shared/db/AppointmentRepository' +import Appointment from '../../../shared/model/Appointment' describe('appointments slice', () => { describe('appointments reducer', () => { @@ -49,7 +49,7 @@ describe('appointments slice', () => { { id: '1', rev: '1', - patientId: '123', + patient: '123', startDateTime: new Date().toISOString(), endDateTime: new Date().toISOString(), location: 'location', diff --git a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx index c8a006b06f..5745e2dc14 100644 --- a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx @@ -1,5 +1,3 @@ -import '../../../../__mocks__/matchMediaMock' - import { Button } from '@hospitalrun/components' import { roundToNearestMinutes, addMinutes } from 'date-fns' import { mount } from 'enzyme' @@ -12,22 +10,22 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import AppointmentRepository from '../../../../clients/db/AppointmentRepository' -import PatientRepository from '../../../../clients/db/PatientRepository' -import Appointment from '../../../../model/Appointment' -import Patient from '../../../../model/Patient' -import * as titleUtil from '../../../../page-header/useTitle' +import * as titleUtil from '../../../../page-header/title/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import EditAppointment from '../../../../scheduling/appointments/edit/EditAppointment' -import { RootState } from '../../../../store' +import AppointmentRepository from '../../../../shared/db/AppointmentRepository' +import PatientRepository from '../../../../shared/db/PatientRepository' +import Appointment from '../../../../shared/model/Appointment' +import Patient from '../../../../shared/model/Patient' +import { RootState } from '../../../../shared/store' const mockStore = createMockStore([thunk]) describe('Edit Appointment', () => { const appointment = { id: '123', - patientId: '456', + patient: '456', startDateTime: roundToNearestMinutes(new Date(), { nearestTo: 15 }).toISOString(), endDateTime: addMinutes(roundToNearestMinutes(new Date(), { nearestTo: 15 }), 60).toISOString(), location: 'location', @@ -107,7 +105,7 @@ describe('Edit Appointment', () => { }) expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) - expect(PatientRepository.find).toHaveBeenCalledWith(appointment.patientId) + expect(PatientRepository.find).toHaveBeenCalledWith(appointment.patient) expect(store.getActions()).toContainEqual(appointmentSlice.fetchAppointmentStart()) expect(store.getActions()).toContainEqual( appointmentSlice.fetchAppointmentSuccess({ appointment, patient }), diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 6da7e26ec1..2f81a3b7dd 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -1,5 +1,3 @@ -import '../../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' import { act } from '@testing-library/react' import { roundToNearestMinutes, addMinutes } from 'date-fns' @@ -12,16 +10,16 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import AppointmentRepository from '../../../../clients/db/AppointmentRepository' -import LabRepository from '../../../../clients/db/LabRepository' -import Appointment from '../../../../model/Appointment' -import Lab from '../../../../model/Lab' -import Patient from '../../../../model/Patient' -import * as titleUtil from '../../../../page-header/useTitle' +import * as titleUtil from '../../../../page-header/title/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import NewAppointment from '../../../../scheduling/appointments/new/NewAppointment' -import { RootState } from '../../../../store' +import AppointmentRepository from '../../../../shared/db/AppointmentRepository' +import LabRepository from '../../../../shared/db/LabRepository' +import Appointment from '../../../../shared/model/Appointment' +import Lab from '../../../../shared/model/Lab' +import Patient from '../../../../shared/model/Patient' +import { RootState } from '../../../../shared/store' const mockStore = createMockStore([thunk]) const mockedComponents = mocked(components, true) @@ -91,7 +89,7 @@ describe('New Appointment', () => { }) const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: roundToNearestMinutes(new Date(), { nearestTo: 15 }).toISOString(), endDateTime: addMinutes( roundToNearestMinutes(new Date(), { nearestTo: 15 }), @@ -105,7 +103,7 @@ describe('New Appointment', () => { act(() => { const appointmentDetailForm = wrapper.find(AppointmentDetailForm) const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('patientId', expectedAppointment.patientId) + onFieldChange('patient', expectedAppointment.patient) }) wrapper.update() @@ -171,7 +169,7 @@ describe('New Appointment', () => { }) const expectedAppointment = { - patientId: '123', + patient: '123', startDateTime: roundToNearestMinutes(new Date(), { nearestTo: 15 }).toISOString(), endDateTime: addMinutes( roundToNearestMinutes(new Date(), { nearestTo: 15 }), @@ -185,7 +183,7 @@ describe('New Appointment', () => { act(() => { const appointmentDetailForm = wrapper.find(AppointmentDetailForm) const onFieldChange = appointmentDetailForm.prop('onFieldChange') - onFieldChange('patientId', expectedAppointment.patientId) + onFieldChange('patient', expectedAppointment.patient) }) wrapper.update() const saveButton = wrapper.find(mockedComponents.Button).at(0) diff --git a/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts index 9f248451f1..0a3b963b54 100644 --- a/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts +++ b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts @@ -1,5 +1,5 @@ -import Appointment from '../../../../model/Appointment' import { getAppointmentLabel } from '../../../../scheduling/appointments/util/scheduling-appointment.util' +import Appointment from '../../../../shared/model/Appointment' describe('scheduling appointment util', () => { describe('getAppointmentLabel', () => { diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 15de39ed57..3490f28e18 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -1,7 +1,5 @@ -import '../../../../__mocks__/matchMediaMock' - import * as components from '@hospitalrun/components' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -11,17 +9,17 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import AppointmentRepository from '../../../../clients/db/AppointmentRepository' -import PatientRepository from '../../../../clients/db/PatientRepository' -import Appointment from '../../../../model/Appointment' -import Patient from '../../../../model/Patient' -import Permissions from '../../../../model/Permissions' -import * as ButtonBarProvider from '../../../../page-header/ButtonBarProvider' -import * as titleUtil from '../../../../page-header/useTitle' +import * as ButtonBarProvider from '../../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../../page-header/title/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import ViewAppointment from '../../../../scheduling/appointments/view/ViewAppointment' -import { RootState } from '../../../../store' +import AppointmentRepository from '../../../../shared/db/AppointmentRepository' +import PatientRepository from '../../../../shared/db/PatientRepository' +import Appointment from '../../../../shared/model/Appointment' +import Patient from '../../../../shared/model/Patient' +import Permissions from '../../../../shared/model/Permissions' +import { RootState } from '../../../../shared/store' const mockStore = createMockStore([thunk]) @@ -42,16 +40,10 @@ describe('View Appointment', () => { let history: any let store: MockStore - const setup = (status: string, permissions = [Permissions.ReadAppointments]) => { - jest.spyOn(AppointmentRepository, 'find') - jest.spyOn(AppointmentRepository, 'delete') - const mockedAppointmentRepository = mocked(AppointmentRepository, true) - mockedAppointmentRepository.find.mockResolvedValue(appointment) - mockedAppointmentRepository.delete.mockResolvedValue(appointment) - - jest.spyOn(PatientRepository, 'find') - const mockedPatientRepository = mocked(PatientRepository, true) - mockedPatientRepository.find.mockResolvedValue(patient) + const setup = async (status = 'completed', permissions = [Permissions.ReadAppointments]) => { + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(AppointmentRepository, 'delete').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) history = createMemoryHistory() history.push('/appointments/123') @@ -67,18 +59,21 @@ describe('View Appointment', () => { }, } as any) - const wrapper = mount( - - - - - - - , - ) + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + , + ) + }) wrapper.update() - return wrapper + return { wrapper: wrapper as ReactWrapper } } beforeEach(() => { @@ -87,30 +82,27 @@ describe('View Appointment', () => { it('should use the correct title', async () => { jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup('loading') - }) + await setup() expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment') }) - it('should add a "Edit Appointment" button to the button tool bar if has WriteAppointment permissions', () => { - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + it('should add a "Edit Appointment" button to the button tool bar if has WriteAppointment permissions', async () => { const setButtonToolBarSpy = jest.fn() - mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - setup('loading', [Permissions.WriteAppointments, Permissions.ReadAppointments]) + await setup('loading', [Permissions.WriteAppointments, Permissions.ReadAppointments]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('actions.edit') }) - it('should add a "Delete Appointment" button to the button tool bar if has DeleteAppointment permissions', () => { + it('should add a "Delete Appointment" button to the button tool bar if has DeleteAppointment permissions', async () => { jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') const setButtonToolBarSpy = jest.fn() mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) - setup('loading', [Permissions.DeleteAppointment, Permissions.ReadAppointments]) + await setup('loading', [Permissions.DeleteAppointment, Permissions.ReadAppointments]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual( @@ -118,21 +110,18 @@ describe('View Appointment', () => { ) }) - it('button toolbar empty if has only ReadAppointments permission', () => { - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + it('button toolbar empty if has only ReadAppointments permission', async () => { const setButtonToolBarSpy = jest.fn() - mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - setup('loading') + await setup('loading') const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons.length).toEqual(0) }) it('should dispatch getAppointment if id is present', async () => { - await act(async () => { - await setup('loading') - }) + await setup() expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) expect(store.getActions()).toContainEqual(appointmentSlice.fetchAppointmentStart()) @@ -142,19 +131,13 @@ describe('View Appointment', () => { }) it('should render a loading spinner', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('loading') - }) + const { wrapper } = await setup('loading') expect(wrapper.find(components.Spinner)).toHaveLength(1) }) it('should render a AppointmentDetailForm with the correct data', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('completed') - }) + const { wrapper } = await setup() const appointmentDetailForm = wrapper.find(AppointmentDetailForm) expect(appointmentDetailForm.prop('appointment')).toEqual(appointment) @@ -162,14 +145,11 @@ describe('View Appointment', () => { }) it('should render a modal for delete confirmation', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('completed') - }) + const { wrapper } = await setup() const deleteAppointmentConfirmationModal = wrapper.find(components.Modal) expect(deleteAppointmentConfirmationModal).toHaveLength(1) - expect(deleteAppointmentConfirmationModal.prop('closeButton').children).toEqual( + expect(deleteAppointmentConfirmationModal.prop('closeButton')?.children).toEqual( 'actions.delete', ) expect(deleteAppointmentConfirmationModal.prop('body')).toEqual( @@ -190,9 +170,7 @@ describe('View Appointment', () => { }) it('should render a delete appointment button in the button toolbar', async () => { - await act(async () => { - await setup('completed', [Permissions.ReadAppointments, Permissions.DeleteAppointment]) - }) + await setup('completed', [Permissions.ReadAppointments, Permissions.DeleteAppointment]) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] @@ -202,13 +180,10 @@ describe('View Appointment', () => { }) it('should pop up the modal when on delete appointment click', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('completed', [ - Permissions.ReadAppointments, - Permissions.DeleteAppointment, - ]) - }) + const { wrapper } = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] @@ -224,13 +199,10 @@ describe('View Appointment', () => { }) it('should close the modal when the toggle button is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('completed', [ - Permissions.ReadAppointments, - Permissions.DeleteAppointment, - ]) - }) + const { wrapper } = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] @@ -252,18 +224,16 @@ describe('View Appointment', () => { }) it('should dispatch DELETE_APPOINTMENT action when modal confirmation button is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup('completed', [ - Permissions.ReadAppointments, - Permissions.DeleteAppointment, - ]) - }) + const { wrapper } = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) const deleteConfirmationModal = wrapper.find(components.Modal) await act(async () => { - await deleteConfirmationModal.prop('closeButton').onClick() + const closeButton = (await deleteConfirmationModal.prop('closeButton')) as any + closeButton.onClick() }) wrapper.update() @@ -278,18 +248,16 @@ describe('View Appointment', () => { jest.spyOn(components, 'Toast') const mockedComponents = mocked(components, true) - let wrapper: any - await act(async () => { - wrapper = await setup('completed', [ - Permissions.ReadAppointments, - Permissions.DeleteAppointment, - ]) - }) + const { wrapper } = await setup('completed', [ + Permissions.ReadAppointments, + Permissions.DeleteAppointment, + ]) const deleteConfirmationModal = wrapper.find(components.Modal) await act(async () => { - await deleteConfirmationModal.prop('closeButton').onClick() + const closeButton = (await deleteConfirmationModal.prop('closeButton')) as any + closeButton.onClick() }) wrapper.update() diff --git a/src/__tests__/settings/Settings.test.tsx b/src/__tests__/settings/Settings.test.tsx index d4820d513b..18ad6a78f4 100644 --- a/src/__tests__/settings/Settings.test.tsx +++ b/src/__tests__/settings/Settings.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { mount } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -8,9 +6,9 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleUtil from '../../page-header/useTitle' +import * as titleUtil from '../../page-header/title/useTitle' import Settings from '../../settings/Settings' -import { RootState } from '../../store' +import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx similarity index 98% rename from src/__tests__/components/Sidebar.test.tsx rename to src/__tests__/shared/components/Sidebar.test.tsx index a9f3548202..34e78818fd 100644 --- a/src/__tests__/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { ListItem } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' @@ -10,9 +8,9 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Sidebar from '../../components/Sidebar' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import Sidebar from '../../../shared/components/Sidebar' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) diff --git a/src/__tests__/components/input/DatePickerWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/DatePickerWithLabelFormGroup.test.tsx similarity index 95% rename from src/__tests__/components/input/DatePickerWithLabelFormGroup.test.tsx rename to src/__tests__/shared/components/input/DatePickerWithLabelFormGroup.test.tsx index ee3c169a82..0f50874ba5 100644 --- a/src/__tests__/components/input/DatePickerWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/DatePickerWithLabelFormGroup.test.tsx @@ -1,10 +1,8 @@ -import '../../../__mocks__/matchMediaMock' - import { DateTimePicker, Label } from '@hospitalrun/components' import { shallow } from 'enzyme' import React, { ChangeEvent } from 'react' -import DatePickerWithLabelFormGroup from '../../../components/input/DatePickerWithLabelFormGroup' +import DatePickerWithLabelFormGroup from '../../../../shared/components/input/DatePickerWithLabelFormGroup' describe('date picker with label form group', () => { describe('layout', () => { diff --git a/src/__tests__/components/input/DateTimePickerWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/DateTimePickerWithLabelFormGroup.test.tsx similarity index 94% rename from src/__tests__/components/input/DateTimePickerWithLabelFormGroup.test.tsx rename to src/__tests__/shared/components/input/DateTimePickerWithLabelFormGroup.test.tsx index 9052cd0fe9..80b4e8ad42 100644 --- a/src/__tests__/components/input/DateTimePickerWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/DateTimePickerWithLabelFormGroup.test.tsx @@ -1,10 +1,8 @@ -import '../../../__mocks__/matchMediaMock' - import { DateTimePicker, Label } from '@hospitalrun/components' import { shallow } from 'enzyme' import React, { ChangeEvent } from 'react' -import DateTimePickerWithLabelFormGroup from '../../../components/input/DateTimePickerWithLabelFormGroup' +import DateTimePickerWithLabelFormGroup from '../../../../shared/components/input/DateTimePickerWithLabelFormGroup' describe('date picker with label form group', () => { describe('layout', () => { diff --git a/src/__tests__/components/input/SelectWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx similarity index 62% rename from src/__tests__/components/input/SelectWithLabelFormGroup.test.tsx rename to src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx index 96b04df022..d4cf9cc75a 100644 --- a/src/__tests__/components/input/SelectWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx @@ -1,10 +1,8 @@ -import '../../../__mocks__/matchMediaMock' - import { Label, Select } from '@hospitalrun/components' import { shallow } from 'enzyme' import React from 'react' -import SelectWithLabelFormGroup from '../../../components/input/SelectWithLableFormGroup' +import SelectWithLabelFormGroup from '../../../../shared/components/input/SelectWithLableFormGroup' describe('select with label form group', () => { describe('layout', () => { @@ -15,7 +13,6 @@ describe('select with label form group', () => { options={[{ value: 'value1', label: 'label1' }]} name={expectedName} label="test" - value="" isEditable onChange={jest.fn()} />, @@ -27,30 +24,6 @@ describe('select with label form group', () => { expect(label.prop('text')).toEqual(expectedName) }) - it('should render a select with the proper options', () => { - const expectedName = 'test' - const wrapper = shallow( - , - ) - - const select = wrapper.find(Select) - expect(select).toHaveLength(1) - - const options = select.find('option') - expect(options).toHaveLength(2) - expect(options.at(0).prop('value')).toEqual('') - expect(options.at(0).text()).toEqual('-- Choose --') - expect(options.at(1).prop('value')).toEqual('value1') - expect(options.at(1).text()).toEqual('label1') - }) - it('should render disabled is isDisable disabled is true', () => { const expectedName = 'test' const wrapper = shallow( @@ -58,7 +31,6 @@ describe('select with label form group', () => { options={[{ value: 'value1', label: 'label1' }]} name={expectedName} label="test" - value="" isEditable={false} onChange={jest.fn()} />, @@ -71,13 +43,13 @@ describe('select with label form group', () => { it('should render the proper value', () => { const expectedName = 'test' - const expectedValue = 'expected value' + const expectedDefaultSelected = [{ value: 'value', label: 'label' }] const wrapper = shallow( , @@ -85,21 +57,18 @@ describe('select with label form group', () => { const select = wrapper.find(Select) expect(select).toHaveLength(1) - expect(select.prop('value')).toEqual(expectedValue) + expect(select.prop('defaultSelected')).toEqual(expectedDefaultSelected) }) }) describe('change handler', () => { it('should call the change handler on change', () => { - const expectedName = 'test' - const expectedValue = 'expected value' const handler = jest.fn() const wrapper = shallow( , diff --git a/src/__tests__/components/input/TestInputWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/TestInputWithLabelFormGroup.test.tsx similarity index 94% rename from src/__tests__/components/input/TestInputWithLabelFormGroup.test.tsx rename to src/__tests__/shared/components/input/TestInputWithLabelFormGroup.test.tsx index 4dd9841647..9b448fab06 100644 --- a/src/__tests__/components/input/TestInputWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/TestInputWithLabelFormGroup.test.tsx @@ -1,10 +1,8 @@ -import '../../../__mocks__/matchMediaMock' - import { Label, TextInput } from '@hospitalrun/components' import { shallow } from 'enzyme' import React from 'react' -import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../../shared/components/input/TextInputWithLabelFormGroup' describe('text input with label form group', () => { describe('layout', () => { diff --git a/src/__tests__/components/input/TextFieldWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/TextFieldWithLabelFormGroup.test.tsx similarity index 95% rename from src/__tests__/components/input/TextFieldWithLabelFormGroup.test.tsx rename to src/__tests__/shared/components/input/TextFieldWithLabelFormGroup.test.tsx index 5e033bb987..9980a076b6 100644 --- a/src/__tests__/components/input/TextFieldWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/TextFieldWithLabelFormGroup.test.tsx @@ -1,10 +1,8 @@ -import '../../../__mocks__/matchMediaMock' - import { Label, TextField } from '@hospitalrun/components' import { shallow } from 'enzyme' import React from 'react' -import TextFieldWithLabelFormGroup from '../../../components/input/TextFieldWithLabelFormGroup' +import TextFieldWithLabelFormGroup from '../../../../shared/components/input/TextFieldWithLabelFormGroup' describe('text field with label form group', () => { describe('layout', () => { diff --git a/src/__tests__/components/Navbar.test.tsx b/src/__tests__/shared/components/navbar/Navbar.test.tsx similarity index 91% rename from src/__tests__/components/Navbar.test.tsx rename to src/__tests__/shared/components/navbar/Navbar.test.tsx index a7234502bf..36d7487247 100644 --- a/src/__tests__/components/Navbar.test.tsx +++ b/src/__tests__/shared/components/navbar/Navbar.test.tsx @@ -1,5 +1,3 @@ -import '../../__mocks__/matchMediaMock' - import { Navbar as HospitalRunNavbar } from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -11,9 +9,9 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import Navbar from '../../components/Navbar' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import Navbar from '../../../../shared/components/navbar/Navbar' +import Permissions from '../../../../shared/model/Permissions' +import { RootState } from '../../../../shared/store' const mockStore = createMockStore([thunk]) @@ -61,9 +59,9 @@ describe('Navbar', () => { const hamberger = hospitalRunNavbar.find('.nav-hamberger') const { children } = hamberger.first().props() as any - expect(children[0].props.children).toEqual('dashboard.label') - expect(children[1].props.children).toEqual('patients.newPatient') - expect(children[children.length - 1].props.children).toEqual('settings.label') + expect(children[0].props.children).toEqual([undefined, 'dashboard.label']) + expect(children[1].props.children).toEqual([undefined, 'patients.newPatient']) + expect(children[children.length - 1].props.children).toEqual([undefined, 'settings.label']) }) it('should not show an item if user does not have a permission', () => { @@ -144,7 +142,7 @@ describe('Navbar', () => { const addNew = hospitalRunNavbar.find('.nav-add-new') const { children } = addNew.first().props() as any - expect(children[0].props.children).toEqual('patients.newPatient') + expect(children[0].props.children).toEqual([undefined, 'patients.newPatient']) }) it('should not show a shortcut if user does not have a permission', () => { @@ -168,7 +166,7 @@ describe('Navbar', () => { const accountLinkList = hospitalRunNavbar.find('.nav-account') const { children } = accountLinkList.first().props() as any - expect(children[0].props.children).toEqual('settings.label') + expect(children[0].props.children).toEqual([undefined, 'settings.label']) }) it('should navigate to /settings when the list option is selected', () => { diff --git a/src/__tests__/components/network-status/NetworkStatusMessage.test.tsx b/src/__tests__/shared/components/network-status/NetworkStatusMessage.test.tsx similarity index 82% rename from src/__tests__/components/network-status/NetworkStatusMessage.test.tsx rename to src/__tests__/shared/components/network-status/NetworkStatusMessage.test.tsx index cbd144510c..87cf13246a 100644 --- a/src/__tests__/components/network-status/NetworkStatusMessage.test.tsx +++ b/src/__tests__/shared/components/network-status/NetworkStatusMessage.test.tsx @@ -1,11 +1,11 @@ import { render, shallow } from 'enzyme' import React from 'react' -import { useTranslation } from '../../../__mocks__/react-i18next' -import { NetworkStatusMessage } from '../../../components/network-status' -import { useNetworkStatus } from '../../../components/network-status/useNetworkStatus' +import { useTranslation } from '../../../../__mocks__/react-i18next' +import { NetworkStatusMessage } from '../../../../shared/components/network-status' +import { useNetworkStatus } from '../../../../shared/components/network-status/useNetworkStatus' -jest.mock('../../../components/network-status/useNetworkStatus') +jest.mock('../../../../shared/components/network-status/useNetworkStatus') const useNetworkStatusMock = (useNetworkStatus as unknown) as jest.MockInstance< ReturnType, any diff --git a/src/__tests__/clients/db/AppointmentRepository.test.ts b/src/__tests__/shared/db/AppointmentRepository.test.ts similarity index 60% rename from src/__tests__/clients/db/AppointmentRepository.test.ts rename to src/__tests__/shared/db/AppointmentRepository.test.ts index 6877aa2083..e5cb51a852 100644 --- a/src/__tests__/clients/db/AppointmentRepository.test.ts +++ b/src/__tests__/shared/db/AppointmentRepository.test.ts @@ -1,33 +1,51 @@ -import AppointmentRepository from '../../../clients/db/AppointmentRepository' -import { appointments, patients } from '../../../config/pouchdb' -import Appointment from '../../../model/Appointment' +import { relationalDb } from '../../../shared/config/pouchdb' +import AppointmentRepository from '../../../shared/db/AppointmentRepository' +import Appointment from '../../../shared/model/Appointment' const uuidV4Regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i describe('Appointment Repository', () => { it('should create a repository with the database set to the appointments database', () => { - expect(AppointmentRepository.db).toEqual(appointments) + expect(AppointmentRepository.db).toEqual(relationalDb) }) describe('find', () => { + it('should create an id that is a uuid', async () => { + const newAppointment = await AppointmentRepository.save({ + patient: 'id', + } as Appointment) + + expect(uuidV4Regex.test(newAppointment.id)).toBeTruthy() + }) + it('should find an appointment by id', async () => { - await appointments.put({ _id: 'id5678' }) - const expectedAppointment = await appointments.put({ _id: 'id1234' }) + await relationalDb.rel.save('appointment', { id: 'id5678' }) + const expectedAppointment = await relationalDb.rel.save('appointment', { id: 'id1234' }) const actualAppointment = await AppointmentRepository.find('id1234') expect(actualAppointment).toBeDefined() expect(actualAppointment.id).toEqual(expectedAppointment.id) - await appointments.remove(await appointments.get('id1234')) - await appointments.remove(await appointments.get('id5678')) + await relationalDb.rel.del( + 'appointment', + await relationalDb.rel.find('appointment', 'id1234'), + ) + await relationalDb.rel.del( + 'appointment', + await relationalDb.rel.find('appointment', 'id5678'), + ) }) }) describe('searchPatientAppointments', () => { it('should escape all special chars from search text', async () => { - await patients.put({ _id: 'id2222' }) - await appointments.put({ _id: 'id3333', patientId: 'id2222', location: 'id-]?}(){*[$+.^\\' }) + await relationalDb.rel.save('patient', { id: 'id2222' }) + await relationalDb.rel.save('appointment', { + id: 'id3333', + patient: 'id2222', + location: 'id-]?}(){*[$+.^\\', + }) const result = await AppointmentRepository.searchPatientAppointments( 'id2222', @@ -40,19 +58,9 @@ describe('Appointment Repository', () => { }) describe('save', () => { - it('should create an id that is a uuid', async () => { - const newAppointment = await AppointmentRepository.save({ - patientId: 'id', - } as Appointment) - - expect(uuidV4Regex.test(newAppointment.id)).toBeTruthy() - - await appointments.remove(await appointments.get(newAppointment.id)) - }) - it('should generate a timestamp for created date and last updated date', async () => { const newAppointment = await AppointmentRepository.save({ - patientId: 'id', + patient: 'id', } as Appointment) expect(newAppointment.createdAt).toBeDefined() diff --git a/src/__tests__/clients/db/LabRepository.test.ts b/src/__tests__/shared/db/LabRepository.test.ts similarity index 51% rename from src/__tests__/clients/db/LabRepository.test.ts rename to src/__tests__/shared/db/LabRepository.test.ts index cbbc7238a0..c0e87333e0 100644 --- a/src/__tests__/clients/db/LabRepository.test.ts +++ b/src/__tests__/shared/db/LabRepository.test.ts @@ -1,11 +1,9 @@ -/* eslint "@typescript-eslint/camelcase": "off" */ - import shortid from 'shortid' -import LabRepository from '../../../clients/db/LabRepository' -import SortRequest from '../../../clients/db/SortRequest' -import { labs } from '../../../config/pouchdb' -import Lab from '../../../model/Lab' +import { relationalDb } from '../../../shared/config/pouchdb' +import LabRepository from '../../../shared/db/LabRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Lab from '../../../shared/model/Lab' interface SearchContainer { text: string @@ -13,17 +11,6 @@ interface SearchContainer { defaultSortRequest: SortRequest } -const removeAllDocs = async () => { - const allDocs = await labs.allDocs({ include_docs: true }) - await Promise.all( - allDocs.rows.map(async (row) => { - if (row.doc) { - await labs.remove(row.doc) - } - }), - ) -} - const defaultSortRequest: SortRequest = { sorts: [ { @@ -39,16 +26,26 @@ const searchObject: SearchContainer = { defaultSortRequest, } +async function removeAllDocs() { + const docs = await relationalDb.rel.find('lab') + docs.labs.forEach(async (d: any) => { + await relationalDb.rel.del('lab', d) + }) +} + describe('lab repository', () => { describe('find', () => { afterEach(async () => { - await removeAllDocs() + const docs = await relationalDb.rel.find('lab') + docs.labs.forEach(async (d: any) => { + await relationalDb.rel.del('lab', d) + }) }) it('should return a lab with the correct data', async () => { // first lab to store is to have mock data to make sure we are getting the expected - await labs.put({ _id: 'id1111' }) - const expectedLab = await labs.put({ _id: 'id2222' }) + await relationalDb.rel.save('lab', { _id: 'id1111' }) + const expectedLab = await relationalDb.rel.save('lab', { id: 'id2222' }) const actualLab = await LabRepository.find('id2222') @@ -58,7 +55,7 @@ describe('lab repository', () => { it('should generate a lab code', async () => { const newLab = await LabRepository.save({ - patientId: '123', + patient: '123', type: 'test', } as Lab) @@ -67,10 +64,22 @@ describe('lab repository', () => { }) describe('search', () => { + afterEach(async () => { + await removeAllDocs() + }) + it('should return all records that lab type matches search text', async () => { const expectedLabType = 'more tests' - await labs.put({ _id: 'someId1', type: expectedLabType, status: 'requested' }) - await labs.put({ _id: 'someId2', type: 'P00002', status: 'requested' }) + await relationalDb.rel.save('lab', { + id: 'someId1', + type: expectedLabType, + status: 'requested', + }) + await relationalDb.rel.save('lab', { + id: 'someId2', + type: 'P00002', + status: 'requested', + }) searchObject.text = expectedLabType @@ -82,9 +91,18 @@ describe('lab repository', () => { it('should return all records that contains search text in the type', async () => { const expectedLabType = 'Labs Tests' - await labs.put({ _id: 'someId3', type: expectedLabType }) - await labs.put({ _id: 'someId4', type: 'Sencond Lab labs tests' }) - await labs.put({ _id: 'someId5', type: 'not found' }) + await relationalDb.rel.save('lab', { + id: 'someId3', + type: expectedLabType, + }) + await relationalDb.rel.save('lab', { + id: 'someId4', + type: 'Sencond Lab labs tests', + }) + await relationalDb.rel.save('lab', { + id: 'someId5', + type: 'not found', + }) searchObject.text = expectedLabType @@ -97,9 +115,21 @@ describe('lab repository', () => { it('should return all records that contains search text in code', async () => { const expectedLabCode = 'L-CODE-sam445Pl' - await labs.put({ _id: 'theID13', type: 'Expected', code: 'L-CODE-sam445Pl' }) - await labs.put({ _id: 'theID14', type: 'Sencond Lab labs tests', code: 'L-4XXX' }) - await labs.put({ _id: 'theID15', type: 'not found', code: 'L-775YYxc' }) + await relationalDb.rel.save('lab', { + id: 'theID13', + type: 'Expected', + code: 'L-CODE-sam445Pl', + }) + await relationalDb.rel.save('lab', { + id: 'theID14', + type: 'Second Lab labs tests', + code: 'L-4XXX', + }) + await relationalDb.rel.save('lab', { + id: 'theID15', + type: 'not found', + code: 'L-775YYxc', + }) searchObject.text = expectedLabCode @@ -110,8 +140,14 @@ describe('lab repository', () => { }) it('should match search criteria with case insesitive match', async () => { - await labs.put({ _id: 'id3333', type: 'lab tests' }) - await labs.put({ _id: 'id4444', type: 'not found' }) + await relationalDb.rel.save('lab', { + id: 'id3333', + type: 'lab tests', + }) + await relationalDb.rel.save('lab', { + id: 'id4444', + type: 'not found', + }) searchObject.text = 'LAB TESTS' @@ -122,10 +158,26 @@ describe('lab repository', () => { }) it('should return all records that matches an specific status', async () => { - await labs.put({ _id: 'id5555', type: 'lab tests', status: 'requested' }) - await labs.put({ _id: 'id6666', type: 'lab tests', status: 'requested' }) - await labs.put({ _id: 'id7777', type: 'lab tests', status: 'completed' }) - await labs.put({ _id: 'id8888', type: 'not found', status: 'completed' }) + await relationalDb.rel.save('lab', { + id: 'id5555', + type: 'lab tests', + status: 'requested', + }) + await relationalDb.rel.save('lab', { + id: 'id6666', + type: 'lab tests', + status: 'requested', + }) + await relationalDb.rel.save('lab', { + id: 'id7777', + type: 'lab tests', + status: 'completed', + }) + await relationalDb.rel.save('lab', { + id: 'id8888', + type: 'not found', + status: 'completed', + }) searchObject.text = '' searchObject.status = 'completed' @@ -138,8 +190,12 @@ describe('lab repository', () => { }) it('should return records with search text and specific status', async () => { - await labs.put({ _id: 'theID09', type: 'the specific lab', status: 'requested' }) - await labs.put({ _id: 'theID10', type: 'not found', status: 'cancelled' }) + await relationalDb.rel.save('lab', { + id: 'theID09', + type: 'the specific lab', + status: 'requested', + }) + await relationalDb.rel.save('lab', { id: 'theID10', type: 'not found', status: 'cancelled' }) searchObject.text = 'the specific lab' searchObject.status = 'requested' @@ -155,18 +211,16 @@ describe('lab repository', () => { afterEach(async () => { await removeAllDocs() }) - it('should find all labs in the database sorted by their requestedOn', async () => { - const expectedLab1 = await labs.put({ _id: 'theID11' }) - setTimeout(async () => { - const expectedLab2 = await labs.put({ _id: 'theID12' }) + it('should find all labs in the database sorted by their requestedOn', async () => { + const expectedLab1 = await relationalDb.rel.save('lab', { id: 'theID11' }) + const expectedLab2 = await relationalDb.rel.save('lab', { id: 'theID12' }) - const result = await LabRepository.findAll() + const result = await LabRepository.findAll() - expect(result).toHaveLength(2) - expect(result[0].id).toEqual(expectedLab1.id) - expect(result[1].id).toEqual(expectedLab2.id) - }, 1000) + expect(result).toHaveLength(2) + expect(result[0].id).toEqual(expectedLab1.id) + expect(result[1].id).toEqual(expectedLab2.id) }) }) }) diff --git a/src/__tests__/shared/db/PatientRepository.test.ts b/src/__tests__/shared/db/PatientRepository.test.ts new file mode 100644 index 0000000000..d58d7d593f --- /dev/null +++ b/src/__tests__/shared/db/PatientRepository.test.ts @@ -0,0 +1,258 @@ +import { getTime, isAfter } from 'date-fns' +import shortid from 'shortid' + +import { relationalDb } from '../../../shared/config/pouchdb' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +const uuidV4Regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i + +async function removeAllDocs() { + const docs = await relationalDb.rel.find('patient') + docs.patients.forEach(async (d: any) => { + await relationalDb.rel.del('patient', d) + }) +} + +describe('patient repository', () => { + describe('find', () => { + afterEach(async () => { + await removeAllDocs() + }) + it('should return a patient with the correct data', async () => { + await relationalDb.rel.save('patient', { id: 'id1111' }) // store another patient just to make sure we pull back the right one + const expectedPatient = await relationalDb.rel.save('patient', { id: 'id2222' }) + + const actualPatient = await PatientRepository.find('id2222') + + expect(actualPatient).toBeDefined() + expect(actualPatient.id).toEqual(expectedPatient.id) + }) + }) + + describe('search', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should escape all special chars from search text', async () => { + await relationalDb.rel.save('patient', { + id: 'id9999', + code: 'P00001', + fullName: 'test -]?}(){*[\\$+.^test', + }) + + const result = await PatientRepository.search('test -]?}(){*[\\$+.^test') + + expect(result).toHaveLength(1) + expect(result[0].id).toEqual('id9999') + }) + + it('should return all records that patient code matches search text', async () => { + // same full name to prove that it is finding by patient code + const expectedPatientCode = 'P00001' + await relationalDb.rel.save('patient', { + id: 'someId1', + code: expectedPatientCode, + fullName: 'test test', + }) + await relationalDb.rel.save('patient', { + id: 'someId2', + code: 'P00002', + fullName: 'test test', + }) + + const result = await PatientRepository.search(expectedPatientCode) + + expect(result).toHaveLength(1) + expect(result[0].code).toEqual(expectedPatientCode) + }) + + it('should return all records that fullName contains search text', async () => { + await relationalDb.rel.save('patient', { + id: 'id3333', + code: 'P00002', + fullName: 'blh test test blah', + }) + await relationalDb.rel.save('patient', { + id: 'id4444', + code: 'P00001', + fullName: 'test test', + }) + await relationalDb.rel.save('patient', { + id: 'id5555', + code: 'P00003', + fullName: 'not found', + }) + + const result = await PatientRepository.search('test test') + + expect(result).toHaveLength(2) + expect(result[0].id).toEqual('id3333') + expect(result[1].id).toEqual('id4444') + }) + + it('should match search criteria with case insensitive match', async () => { + await relationalDb.rel.save('patient', { + id: 'id6666', + code: 'P00001', + fullName: 'test test', + }) + await relationalDb.rel.save('patient', { + id: 'id7777', + code: 'P00002', + fullName: 'not found', + }) + + const result = await PatientRepository.search('TEST TEST') + + expect(result).toHaveLength(1) + expect(result[0].id).toEqual('id6666') + }) + }) + + describe('findAll', () => { + afterEach(async () => { + await removeAllDocs() + }) + it('should find all patients in the database sorted by their ids', async () => { + const expectedPatient1 = await relationalDb.rel.save('patient', { id: 'id9999' }) + const expectedPatient2 = await relationalDb.rel.save('patient', { id: 'id8888' }) + + const result = await PatientRepository.findAll() + + expect(result).toHaveLength(2) + expect(result[0].id).toEqual(expectedPatient2.id) + expect(result[1].id).toEqual(expectedPatient1.id) + }) + }) + + describe('save', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should generate an id that is a uuid for the patient', async () => { + const newPatient = await PatientRepository.save({ + fullName: 'test test', + } as Patient) + + expect(uuidV4Regex.test(newPatient.id)).toBeTruthy() + }) + + it('should generate a patient code', async () => { + const newPatient = await PatientRepository.save({ + fullName: 'test1 test1', + } as Patient) + + expect(shortid.isValid(newPatient.code)).toBeTruthy() + }) + + it('should generate a timestamp for created date and last updated date', async () => { + const newPatient = await PatientRepository.save({ + fullName: 'test1 test1', + } as Patient) + + expect(newPatient.createdAt).toBeDefined() + expect(newPatient.updatedAt).toBeDefined() + }) + + it('should override the created date and last updated date even if one was passed in', async () => { + const unexpectedTime = new Date(2020, 2, 1).toISOString() + const newPatient = await PatientRepository.save({ + fullName: 'test1 test1', + createdAt: unexpectedTime, + updatedAt: unexpectedTime, + } as Patient) + + expect(newPatient.createdAt).not.toEqual(unexpectedTime) + expect(newPatient.updatedAt).not.toEqual(unexpectedTime) + }) + }) + + describe('saveOrUpdate', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should save the patient if an id was not on the entity', async () => { + const newPatient = await PatientRepository.saveOrUpdate({ + fullName: 'test4 test4', + } as Patient) + + expect(newPatient.id).toBeDefined() + }) + + it('should update the patient if one was already existing', async () => { + const existingPatient = await PatientRepository.save({ + fullName: 'test5 test5', + } as Patient) + + const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) + + expect(updatedPatient.id).toEqual(existingPatient.id) + }) + + it('should update the existing fields', async () => { + const existingPatient = await PatientRepository.save({ + fullName: 'test6 test6', + } as Patient) + existingPatient.fullName = 'changed' + + const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) + + expect(updatedPatient.fullName).toEqual('changed') + }) + + it('should add new fields without changing existing fields', async () => { + const existingPatient = await PatientRepository.save({ + fullName: 'test7 test7', + } as Patient) + existingPatient.givenName = 'givenName' + + const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) + + expect(updatedPatient.fullName).toEqual(existingPatient.fullName) + expect(updatedPatient.givenName).toEqual('givenName') + }) + + it('should update the last updated date', async () => { + const time = new Date(2020, 1, 1).toISOString() + await relationalDb.rel.save('patient', { id: 'id2222222', createdAt: time, updatedAt: time }) + const existingPatient = await PatientRepository.find('id2222222') + + const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) + + expect( + isAfter(new Date(updatedPatient.updatedAt), new Date(updatedPatient.createdAt)), + ).toBeTruthy() + expect(updatedPatient.updatedAt).not.toEqual(existingPatient.updatedAt) + }) + + it('should not update the created date', async () => { + const time = getTime(new Date(2020, 1, 1)) + await relationalDb.rel.save('patient', { id: 'id111111', createdAt: time, updatedAt: time }) + const existingPatient = await PatientRepository.find('id111111') + const updatedPatient = await PatientRepository.saveOrUpdate(existingPatient) + + expect(updatedPatient.createdAt).toEqual(existingPatient.createdAt) + }) + }) + + describe('delete', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should delete the patient', async () => { + const patientToDelete = await PatientRepository.save({ + fullName: 'test8 test8', + } as Patient) + + await PatientRepository.delete(patientToDelete) + + const patients = await PatientRepository.findAll() + expect(patients).toHaveLength(0) + }) + }) +}) diff --git a/src/__tests__/hooks/debounce.test.ts b/src/__tests__/shared/hooks/useDebounce.test.ts similarity index 95% rename from src/__tests__/hooks/debounce.test.ts rename to src/__tests__/shared/hooks/useDebounce.test.ts index d4ced8c57d..f4b2ab94b5 100644 --- a/src/__tests__/hooks/debounce.test.ts +++ b/src/__tests__/shared/hooks/useDebounce.test.ts @@ -1,6 +1,6 @@ import { renderHook, act } from '@testing-library/react-hooks' -import useDebounce from '../../hooks/debounce' +import useDebounce from '../../../shared/hooks/useDebounce' describe('useDebounce', () => { beforeAll(() => jest.useFakeTimers()) diff --git a/src/__tests__/hooks/useUpdateEffect.test.ts b/src/__tests__/shared/hooks/useUpdateEffect.test.ts similarity index 85% rename from src/__tests__/hooks/useUpdateEffect.test.ts rename to src/__tests__/shared/hooks/useUpdateEffect.test.ts index 06b044f946..f89ae78a63 100644 --- a/src/__tests__/hooks/useUpdateEffect.test.ts +++ b/src/__tests__/shared/hooks/useUpdateEffect.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks' -import useUpdateEffect from '../../hooks/useUpdateEffect' +import useUpdateEffect from '../../../shared/hooks/useUpdateEffect' describe('useUpdateEffect', () => { it('should call the function after udpate', () => { diff --git a/src/__tests__/utils/generateCode.test.ts b/src/__tests__/shared/utils/generateCode.test.ts similarity index 69% rename from src/__tests__/utils/generateCode.test.ts rename to src/__tests__/shared/utils/generateCode.test.ts index 22a44dd2a5..cf6d887128 100644 --- a/src/__tests__/utils/generateCode.test.ts +++ b/src/__tests__/shared/utils/generateCode.test.ts @@ -1,4 +1,4 @@ -import generateCode from '../../util/generateCode' +import generateCode from '../../../shared/util/generateCode' it('should generate a code with prefix A-', () => { const generatedCode = generateCode('A') diff --git a/src/__tests__/user/user-slice.test.ts b/src/__tests__/user/user-slice.test.ts index ec13b46bb4..4c388ef92d 100644 --- a/src/__tests__/user/user-slice.test.ts +++ b/src/__tests__/user/user-slice.test.ts @@ -1,14 +1,160 @@ -import Permissions from '../../model/Permissions' -import user, { fetchPermissions } from '../../user/user-slice' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import { remoteDb } from '../../shared/config/pouchdb' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import user, { + fetchPermissions, + getCurrentSession, + login, + loginSuccess, + loginError, + logout, + logoutSuccess, +} from '../../user/user-slice' + +const mockStore = configureMockStore([thunk]) describe('user slice', () => { - it('should handle the FETCH_PERMISSIONS action', () => { - const expectedPermissions = [Permissions.ReadPatients, Permissions.WritePatients] - const userStore = user(undefined, { - type: fetchPermissions.type, - payload: expectedPermissions, + describe('reducers', () => { + it('should handle the FETCH_PERMISSIONS action', () => { + const expectedPermissions = [Permissions.ReadPatients, Permissions.WritePatients] + const userStore = user(undefined, { + type: fetchPermissions.type, + payload: expectedPermissions, + }) + + expect(userStore.permissions).toEqual(expectedPermissions) + }) + + it('should handle the LOGIN_SUCCESS action', () => { + const expectedUser = { + familyName: 'firstName', + givenName: 'lastName', + id: 'id', + } + const expectedPermissions = [Permissions.WritePatients] + const userStore = user(undefined, { + type: loginSuccess.type, + payload: { user: expectedUser, permissions: expectedPermissions }, + }) + + expect(userStore.user).toEqual(expectedUser) + }) + + it('should handle the login error', () => { + const expectedError = 'error' + const userStore = user(undefined, { + type: loginError.type, + payload: expectedError, + }) + + expect(userStore.loginError).toEqual(expectedError) + }) + + it('should handle the logout success', () => { + const userStore = user( + { user: { givenName: 'given', familyName: 'family', id: 'id' }, permissions: [] }, + { + type: logoutSuccess.type, + }, + ) + + expect(userStore.user).toEqual(undefined) + expect(userStore.permissions).toEqual([]) + }) + }) + + describe('login', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should login with the username and password', async () => { + jest.spyOn(remoteDb, 'logIn').mockResolvedValue({ name: 'test', ok: true }) + jest.spyOn(remoteDb, 'getUser').mockResolvedValue({ + _id: 'userId', + metadata: { + givenName: 'test', + familyName: 'user', + }, + } as any) + const store = mockStore() + const expectedUsername = 'test' + const expectedPassword = 'password' + + await store.dispatch(login(expectedUsername, expectedPassword)) + + expect(remoteDb.logIn).toHaveBeenCalledTimes(1) + expect(remoteDb.logIn).toHaveBeenLastCalledWith(expectedUsername, expectedPassword) + expect(remoteDb.getUser).toHaveBeenCalledWith(expectedUsername) + expect(store.getActions()[0]).toEqual({ + type: loginSuccess.type, + payload: expect.objectContaining({ + user: { familyName: 'user', givenName: 'test', id: 'userId' }, + }), + }) + }) + + it('should dispatch login error if login was not successful', async () => { + jest.spyOn(remoteDb, 'logIn').mockRejectedValue({ status: '401' }) + jest.spyOn(remoteDb, 'getUser').mockResolvedValue({ + _id: 'userId', + metadata: { + givenName: 'test', + familyName: 'user', + }, + } as any) + const store = mockStore() + + await store.dispatch(login('user', 'password')) + + expect(remoteDb.getUser).not.toHaveBeenCalled() + expect(store.getActions()[0]).toEqual({ + type: loginError.type, + payload: 'user.login.error', + }) + }) + }) + + describe('logout', () => { + beforeEach(() => { + jest.resetAllMocks() }) - expect(userStore.permissions).toEqual(expectedPermissions) + it('should logout the user', async () => { + jest.spyOn(remoteDb, 'logOut').mockImplementation(jest.fn()) + const store = mockStore() + + await store.dispatch(logout()) + + expect(remoteDb.logOut).toHaveBeenCalledTimes(1) + expect(store.getActions()[0]).toEqual({ type: logoutSuccess.type }) + }) + }) + + describe('getCurrentSession', () => { + it('should get the detail of the current user and update the store', async () => { + jest.spyOn(remoteDb, 'getUser').mockResolvedValue({ + _id: 'userId', + metadata: { + givenName: 'test', + familyName: 'user', + }, + } as any) + const store = mockStore() + const expectedUsername = 'test' + + await store.dispatch(getCurrentSession(expectedUsername)) + + expect(remoteDb.getUser).toHaveBeenCalledWith(expectedUsername) + expect(store.getActions()[0]).toEqual({ + type: loginSuccess.type, + payload: expect.objectContaining({ + user: { familyName: 'user', givenName: 'test', id: 'userId' }, + }), + }) + }) }) }) diff --git a/src/clients/db/PatientRepository.ts b/src/clients/db/PatientRepository.ts deleted file mode 100644 index 5ad844e407..0000000000 --- a/src/clients/db/PatientRepository.ts +++ /dev/null @@ -1,105 +0,0 @@ -import escapeStringRegexp from 'escape-string-regexp' - -import { patients } from '../../config/pouchdb' -import Patient from '../../model/Patient' -import generateCode from '../../util/generateCode' -import Page from '../Page' -import PageRequest, { UnpagedRequest } from './PageRequest' -import Repository from './Repository' -import SortRequest, { Unsorted } from './SortRequest' - -class PatientRepository extends Repository { - constructor() { - super(patients) - patients.createIndex({ - index: { fields: ['index'] }, - }) - } - - async search(text: string): Promise { - const escapedString = escapeStringRegexp(text) - return super.search({ - selector: { - $or: [ - { - fullName: { - $regex: RegExp(escapedString, 'i'), - }, - }, - { - code: text, - }, - ], - }, - }) - } - - async searchPaged( - text: string, - pageRequest: PageRequest = UnpagedRequest, - sortRequest: SortRequest = Unsorted, - ): Promise> { - const selector: any = { - $or: [ - { - fullName: { - $regex: RegExp(text, 'i'), - }, - }, - { - code: text, - }, - ], - } - sortRequest.sorts.forEach((s) => { - selector[s.field] = { $gt: null } - }) - - const result = await super - .search({ - selector, - limit: pageRequest.size ? pageRequest.size + 1 : undefined, - skip: - pageRequest.number && pageRequest.size ? (pageRequest.number - 1) * pageRequest.size : 0, - sort: - sortRequest.sorts.length > 0 - ? sortRequest.sorts.map((s) => ({ [s.field]: s.direction })) - : undefined, - }) - .catch((err) => { - console.log(err) - return err - }) - - const pagedResult: Page = { - content: result.slice( - 0, - pageRequest.size - ? result.length < pageRequest.size - ? result.length - : pageRequest.size - : result.length, - ), - pageRequest, - hasNext: pageRequest.size !== undefined && result.length === pageRequest.size + 1, - hasPrevious: pageRequest.number !== undefined && pageRequest.number > 1, - } - - return pagedResult - } - - async save(entity: Patient): Promise { - const patientCode = generateCode('P') - entity.code = patientCode - entity.index = (entity.fullName ? entity.fullName : '') + patientCode - return super.save(entity) - } - - async createIndex() { - return this.db.createIndex({ - index: { fields: ['index'] }, - }) - } -} - -export default new PatientRepository() diff --git a/src/clients/db/Repository.ts b/src/clients/db/Repository.ts deleted file mode 100644 index 7a82d2631b..0000000000 --- a/src/clients/db/Repository.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* eslint "@typescript-eslint/camelcase": "off" */ -import { v4 as uuidv4 } from 'uuid' - -import AbstractDBModel from '../../model/AbstractDBModel' -import Page from '../Page' -import PageRequest, { UnpagedRequest } from './PageRequest' -import SortRequest, { Unsorted } from './SortRequest' - -function mapDocument(document: any): any { - const { _id, _rev, ...values } = document - return { - id: _id, - rev: _rev, - ...values, - } -} - -export default class Repository { - db: PouchDB.Database - - constructor(db: PouchDB.Database) { - this.db = db - } - - async find(id: string): Promise { - const document = await this.db.get(id) - return mapDocument(document) - } - - async findAll(sort = Unsorted): Promise { - const selector: any = { - _id: { $gt: null }, - } - - sort.sorts.forEach((s) => { - selector[s.field] = { $gt: null } - }) - - // Adds an index to each of the fields coming from the sorting object - // allowing the algorithm to sort by any given SortRequest, by avoiding the default index error (lack of index) - - await Promise.all( - sort.sorts.map( - async (s): Promise => { - await this.db.createIndex({ - index: { - fields: [s.field], - }, - }) - - return sort - }, - ), - ) - - const result = await this.db.find({ - selector, - sort: sort.sorts.length > 0 ? sort.sorts.map((s) => ({ [s.field]: s.direction })) : undefined, - }) - - return result.docs.map(mapDocument) - } - - async findAllPaged(sort = Unsorted, pageRequest: PageRequest = UnpagedRequest): Promise> { - const selector: any = { - _id: { $gt: null }, - } - if (pageRequest.direction === 'next') { - sort.sorts.forEach((s) => { - selector[s.field] = { - $gte: - pageRequest.nextPageInfo && pageRequest.nextPageInfo[s.field] - ? pageRequest.nextPageInfo[s.field] - : null, - } - }) - } else if (pageRequest.direction === 'previous') { - sort.sorts.forEach((s) => { - s.direction = s.direction === 'asc' ? 'desc' : 'asc' - selector[s.field] = { - $lte: - pageRequest.previousPageInfo && pageRequest.previousPageInfo[s.field] - ? pageRequest.previousPageInfo[s.field] - : null, - } - }) - } - - const result = await this.db.find({ - selector, - sort: sort.sorts.length > 0 ? sort.sorts.map((s) => ({ [s.field]: s.direction })) : undefined, - limit: pageRequest.size ? pageRequest.size + 1 : undefined, - }) - - const mappedResult = result.docs.map(mapDocument) - if (pageRequest.direction === 'previous') { - mappedResult.reverse() - } - - const nextPageInfo: { [key: string]: string } = {} - const previousPageInfo: { [key: string]: string } = {} - - if (mappedResult.length > 0) { - sort.sorts.forEach((s) => { - nextPageInfo[s.field] = mappedResult[mappedResult.length - 1][s.field] - }) - sort.sorts.forEach((s) => { - previousPageInfo[s.field] = mappedResult[0][s.field] - }) - } - - const hasNext: boolean = - pageRequest.size !== undefined && mappedResult.length === pageRequest.size + 1 - const hasPrevious: boolean = pageRequest.number !== undefined && pageRequest.number > 1 - - const pagedResult: Page = { - content: - pageRequest.size !== undefined && mappedResult.length === pageRequest.size + 1 - ? mappedResult.slice(0, mappedResult.length - 1) - : mappedResult, - hasNext, - hasPrevious, - pageRequest: { - size: pageRequest.size, - number: pageRequest.number, - nextPageInfo: hasNext ? nextPageInfo : undefined, - previousPageInfo: hasPrevious ? previousPageInfo : undefined, - }, - } - return pagedResult - } - - async search(criteria: any): Promise { - const response = await this.db.find(criteria) - return response.docs.map(mapDocument) - } - - async save(entity: T): Promise { - const currentTime = new Date().toISOString() - - const { id, rev, ...valuesToSave } = entity - const savedEntity = await this.db.put({ - _id: uuidv4(), - ...valuesToSave, - createdAt: currentTime, - updatedAt: currentTime, - }) - return this.find(savedEntity.id) - } - - async saveOrUpdate(entity: T): Promise { - if (!entity.id) { - return this.save(entity) - } - - const { id, rev, ...dataToSave } = entity - - try { - await this.find(entity.id) - const entityToUpdate = { - _id: id, - _rev: rev, - ...dataToSave, - updatedAt: new Date().toISOString(), - } - - await this.db.put(entityToUpdate) - return this.find(entity.id) - } catch (error) { - return this.save(entity) - } - } - - async delete(entity: T): Promise { - const e = entity as any - return mapDocument(this.db.remove(e.id, e.rev)) - } -} diff --git a/src/components/input/LanguageSelector.tsx b/src/components/input/LanguageSelector.tsx deleted file mode 100644 index 5fc90334b6..0000000000 --- a/src/components/input/LanguageSelector.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash' -import React from 'react' -import { useTranslation } from 'react-i18next' - -import i18n, { resources } from '../../i18n' -import SelectWithLabelFormGroup from './SelectWithLableFormGroup' - -const LanguageSelector = () => { - const { t } = useTranslation() - - let languageOptions = Object.keys(resources).map((abbr) => ({ - label: resources[abbr].name, - value: abbr, - })) - languageOptions = _.sortBy(languageOptions, (o) => o.label) - - const onLanguageChange = (event: React.ChangeEvent) => { - const selected = event.target.value - i18n.changeLanguage(selected) - } - - return ( - - ) -} - -export default LanguageSelector diff --git a/src/config/pouchdb.ts b/src/config/pouchdb.ts deleted file mode 100644 index b951e8f6ed..0000000000 --- a/src/config/pouchdb.ts +++ /dev/null @@ -1,32 +0,0 @@ -import PouchDB from 'pouchdb' - -/* eslint-disable */ -const memoryAdapter = require('pouchdb-adapter-memory') -const search = require('pouchdb-quick-search') -import PouchdbFind from 'pouchdb-find' -/* eslint-enable */ - -PouchDB.plugin(search) -PouchDB.plugin(memoryAdapter) -PouchDB.plugin(PouchdbFind) - -function createDb(name: string) { - if (process.env.NODE_ENV === 'test') { - return new PouchDB(name, { adapter: 'memory' }) - } - - const db = new PouchDB(name) - db.sync(`${process.env.REACT_APP_HOSPITALRUN_API}/_db/${name}`, { - live: true, - retry: true, - }).on('change', (info) => { - console.log(info) - }) - - return db -} - -export const patients = createDb('patients') -export const appointments = createDb('appointments') -export const labs = createDb('labs') -export const incidents = createDb('incidents') diff --git a/src/custom-pouchdb.d.ts b/src/custom-pouchdb.d.ts index 2910c943e6..adb49182b8 100644 --- a/src/custom-pouchdb.d.ts +++ b/src/custom-pouchdb.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ declare namespace PouchDB { interface SearchQuery { // Search string @@ -16,8 +17,8 @@ declare namespace PouchDB { filter?: (content: Content) => boolean - include_docs?: boolean highlighting?: boolean + include_docs?: boolean highlighting_pre?: string highlighting_post?: string @@ -36,10 +37,11 @@ declare namespace PouchDB { interface SearchResponse { rows: Array> + total_rows: number } - interface Database { + interface Database = Record> { search(query: SearchQuery): SearchResponse } } @@ -48,3 +50,5 @@ declare module 'pouchdb-quick-search' { const plugin: PouchDB.Plugin export = plugin } + +declare module 'relational-pouch' diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 8a26e4df9e..8cfefb9325 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import useTitle from '../page-header/useTitle' +import useTitle from '../page-header/title/useTitle' const Dashboard: React.FC = () => { const { t } = useTranslation() diff --git a/src/incidents/Incidents.tsx b/src/incidents/Incidents.tsx index 59d3a9cd0b..7b19355a56 100644 --- a/src/incidents/Incidents.tsx +++ b/src/incidents/Incidents.tsx @@ -2,10 +2,10 @@ import React from 'react' import { useSelector } from 'react-redux' import { Switch } from 'react-router-dom' -import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' -import PrivateRoute from '../components/PrivateRoute' -import Permissions from '../model/Permissions' -import { RootState } from '../store' +import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' +import PrivateRoute from '../shared/components/PrivateRoute' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' import ViewIncidents from './list/ViewIncidents' import ReportIncident from './report/ReportIncident' import ViewIncident from './view/ViewIncident' diff --git a/src/incidents/incident-slice.ts b/src/incidents/incident-slice.ts index dfb445c3d7..11a319ebe9 100644 --- a/src/incidents/incident-slice.ts +++ b/src/incidents/incident-slice.ts @@ -3,9 +3,9 @@ import { isAfter } from 'date-fns' import { isEmpty } from 'lodash' import shortid from 'shortid' -import IncidentRepository from '../clients/db/IncidentRepository' -import Incident from '../model/Incident' -import { AppThunk } from '../store' +import IncidentRepository from '../shared/db/IncidentRepository' +import Incident from '../shared/model/Incident' +import { AppThunk } from '../shared/store' interface Error { date?: string @@ -108,7 +108,7 @@ export const reportIncident = ( if (isEmpty(incidentError)) { incident.reportedOn = new Date(Date.now()).toISOString() incident.code = getIncidentCode() - incident.reportedBy = getState().user.user.id + incident.reportedBy = getState().user.user?.id || '' incident.status = 'reported' const newIncident = await IncidentRepository.save(incident) await dispatch(reportIncidentSuccess(newIncident)) diff --git a/src/incidents/incidents-slice.ts b/src/incidents/incidents-slice.ts index 1d35600312..96e69dcd9d 100644 --- a/src/incidents/incidents-slice.ts +++ b/src/incidents/incidents-slice.ts @@ -1,8 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import IncidentRepository from '../clients/db/IncidentRepository' -import Incident from '../model/Incident' -import { AppThunk } from '../store' +import IncidentRepository from '../shared/db/IncidentRepository' +import Incident from '../shared/model/Incident' +import { AppThunk } from '../shared/store' import IncidentFilter from './IncidentFilter' interface IncidentsState { diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index 4cb0213561..88c72fdf35 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -1,15 +1,16 @@ -import { Button } from '@hospitalrun/components' +import { Button, Table } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' -import Incident from '../../model/Incident' -import { useButtonToolbarSetter } from '../../page-header/ButtonBarProvider' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../page-header/title/useTitle' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import { RootState } from '../../shared/store' import IncidentFilter from '../IncidentFilter' import { searchIncidents } from '../incidents-slice' @@ -44,15 +45,7 @@ const ViewIncidents = () => { dispatch(searchIncidents(searchFilter)) }, [dispatch, searchFilter]) - const onTableRowClick = (incident: Incident) => { - history.push(`incidents/${incident.id}`) - } - - const onFilterChange = (event: React.ChangeEvent) => { - setSearchFilter(event.target.value as IncidentFilter) - } - - const filterOptions = Object.values(IncidentFilter).map((filter) => ({ + const filterOptions: Option[] = Object.values(IncidentFilter).map((filter) => ({ label: t(`incidents.status.${filter}`), value: `${filter}`, })) @@ -63,37 +56,35 @@ const ViewIncidents = () => {
value === searchFilter)} + onChange={(values) => setSearchFilter(values[0] as IncidentFilter)} + isEditable />
- - - - - - - - - - - - {incidents.map((incident: Incident) => ( - onTableRowClick(incident)} key={incident.id}> - - - - - - - ))} - -
{t('incidents.reports.code')}{t('incidents.reports.dateOfIncident')}{t('incidents.reports.reportedBy')}{t('incidents.reports.reportedOn')}{t('incidents.reports.status')}
{incident.code}{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}{incident.reportedBy}{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}{incident.status}
+ row.id} + data={incidents} + columns={[ + { label: t('incidents.reports.code'), key: 'code' }, + { + label: t('incidents.reports.dateOfIncident'), + key: 'date', + formatter: (row) => + row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('incidents.reports.reportedBy'), key: 'reportedBy' }, + { label: t('incidents.reports.reportedOn'), key: 'reportedOn' }, + { label: t('incidents.reports.status'), key: 'status' }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { label: t('actions.view'), action: (row) => history.push(`incidents/${row.id}`) }, + ]} + /> ) diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx index d7eeb35f45..b270cc7222 100644 --- a/src/incidents/report/ReportIncident.tsx +++ b/src/incidents/report/ReportIncident.tsx @@ -4,13 +4,13 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import DateTimePickerWithLabelFormGroup from '../../components/input/DateTimePickerWithLabelFormGroup' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Incident from '../../model/Incident' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Incident from '../../shared/model/Incident' +import { RootState } from '../../shared/store' import { reportIncident } from '../incident-slice' const ReportIncident = () => { diff --git a/src/incidents/view/ViewIncident.tsx b/src/incidents/view/ViewIncident.tsx index e68436cf26..a4797cb7c5 100644 --- a/src/incidents/view/ViewIncident.tsx +++ b/src/incidents/view/ViewIncident.tsx @@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import { RootState } from '../../shared/store' import { fetchIncident } from '../incident-slice' const ViewIncident = () => { diff --git a/src/index.tsx b/src/index.tsx index f9549cac06..9fc7aaacc0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,7 +5,8 @@ import '@hospitalrun/components/scss/main.scss' import './index.css' import App from './App' import * as serviceWorker from './serviceWorker' -import './i18n' + +import './shared/config/i18n' ReactDOM.render(, document.getElementById('root')) diff --git a/src/labs/Labs.tsx b/src/labs/Labs.tsx index 11252b0383..fadf4e4335 100644 --- a/src/labs/Labs.tsx +++ b/src/labs/Labs.tsx @@ -2,10 +2,10 @@ import React from 'react' import { useSelector } from 'react-redux' import { Switch } from 'react-router-dom' -import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' -import PrivateRoute from '../components/PrivateRoute' -import Permissions from '../model/Permissions' -import { RootState } from '../store' +import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' +import PrivateRoute from '../shared/components/PrivateRoute' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' import NewLabRequest from './requests/NewLabRequest' import ViewLab from './ViewLab' import LabRequests from './ViewLabs' diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index 0f654a0dde..b8328de69d 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' -import TextFieldWithLabelFormGroup from '../components/input/TextFieldWithLabelFormGroup' -import Lab from '../model/Lab' -import Patient from '../model/Patient' -import Permissions from '../model/Permissions' -import useTitle from '../page-header/useTitle' -import { RootState } from '../store' +import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../page-header/title/useTitle' +import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' +import Lab from '../shared/model/Lab' +import Patient from '../shared/model/Patient' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' import { cancelLab, completeLab, updateLab, fetchLab } from './lab-slice' const getTitle = (patient: Patient | undefined, lab: Lab | undefined) => diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx index 8587ae4739..36ab36c7c8 100644 --- a/src/labs/ViewLabs.tsx +++ b/src/labs/ViewLabs.tsx @@ -1,21 +1,23 @@ -import { Spinner, Button } from '@hospitalrun/components' +import { Button, Table } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import SelectWithLabelFormGroup from '../components/input/SelectWithLableFormGroup' -import TextInputWithLabelFormGroup from '../components/input/TextInputWithLabelFormGroup' -import useDebounce from '../hooks/debounce' -import Lab from '../model/Lab' -import Permissions from '../model/Permissions' -import { useButtonToolbarSetter } from '../page-header/ButtonBarProvider' -import useTitle from '../page-header/useTitle' -import { RootState } from '../store' +import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../page-header/title/useTitle' +import SelectWithLabelFormGroup, { + Option, +} from '../shared/components/input/SelectWithLableFormGroup' +import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' +import useDebounce from '../shared/hooks/useDebounce' +import Lab from '../shared/model/Lab' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' import { searchLabs } from './labs-slice' -type filter = 'requested' | 'completed' | 'canceled' | 'all' +type LabFilter = 'requested' | 'completed' | 'canceled' | 'all' const ViewLabs = () => { const { t } = useTranslation() @@ -25,8 +27,8 @@ const ViewLabs = () => { const { permissions } = useSelector((state: RootState) => state.user) const dispatch = useDispatch() - const { labs, isLoading } = useSelector((state: RootState) => state.labs) - const [searchFilter, setSearchFilter] = useState('all') + const { labs } = useSelector((state: RootState) => state.labs) + const [searchFilter, setSearchFilter] = useState('all') const [searchText, setSearchText] = useState('') const debouncedSearchText = useDebounce(searchText, 500) @@ -50,15 +52,6 @@ const ViewLabs = () => { return buttons }, [permissions, history, t]) - const setFilter = (filter: string) => - filter === 'requested' - ? 'requested' - : filter === 'completed' - ? 'completed' - : filter === 'canceled' - ? 'canceled' - : 'all' - useEffect(() => { dispatch(searchLabs(debouncedSearchText, searchFilter)) }, [dispatch, debouncedSearchText, searchFilter]) @@ -70,32 +63,20 @@ const ViewLabs = () => { } }, [dispatch, getButtons, setButtons]) - const loadingIndicator = - - const onTableRowClick = (lab: Lab) => { + const onViewClick = (lab: Lab) => { history.push(`/labs/${lab.id}`) } - const onSelectChange = (event: React.ChangeEvent) => { - setSearchFilter(setFilter(event.target.value)) - } - const onSearchBoxChange = (event: React.ChangeEvent) => { setSearchText(event.target.value) } - const listBody = ( - - {labs.map((lab) => ( - onTableRowClick(lab)} key={lab.id}> - - - - - - ))} - - ) + const filterOptions: Option[] = [ + { label: t('labs.status.requested'), value: 'requested' }, + { label: t('labs.status.completed'), value: 'completed' }, + { label: t('labs.status.canceled'), value: 'canceled' }, + { label: t('labs.filter.all'), value: 'all' }, + ] return ( <> @@ -103,18 +84,11 @@ const ViewLabs = () => {
value === searchFilter)} + onChange={(values) => setSearchFilter(values[0] as LabFilter)} isEditable - options={[ - { label: t('labs.status.requested'), value: 'requested' }, - { label: t('labs.status.completed'), value: 'completed' }, - { label: t('labs.status.canceled'), value: 'canceled' }, - { label: t('labs.filter.all'), value: 'all' }, - ]} - onChange={(event: React.ChangeEvent) => { - onSelectChange(event) - }} />
@@ -129,17 +103,23 @@ const ViewLabs = () => {
-
{lab.code}{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
- - - - - - - - - {isLoading ? loadingIndicator : listBody} -
{t('labs.lab.code')}{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
+ row.id} + columns={[ + { label: t('labs.lab.code'), key: 'code' }, + { label: t('labs.lab.type'), key: 'type' }, + { + label: t('labs.lab.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('labs.lab.status'), key: 'status' }, + ]} + data={labs} + actionsHeaderText={t('actions.label')} + actions={[{ label: t('actions.view'), action: (row) => onViewClick(row as Lab) }]} + /> ) diff --git a/src/labs/lab-slice.ts b/src/labs/lab-slice.ts index 4294448a87..c57ea7b165 100644 --- a/src/labs/lab-slice.ts +++ b/src/labs/lab-slice.ts @@ -1,10 +1,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import LabRepository from '../clients/db/LabRepository' -import PatientRepository from '../clients/db/PatientRepository' -import Lab from '../model/Lab' -import Patient from '../model/Patient' -import { AppThunk } from '../store' +import LabRepository from '../shared/db/LabRepository' +import PatientRepository from '../shared/db/PatientRepository' +import Lab from '../shared/model/Lab' +import Patient from '../shared/model/Patient' +import { AppThunk } from '../shared/store' interface Error { result?: string @@ -86,13 +86,13 @@ export const { export const fetchLab = (labId: string): AppThunk => async (dispatch) => { dispatch(fetchLabStart()) const fetchedLab = await LabRepository.find(labId) - const fetchedPatient = await PatientRepository.find(fetchedLab.patientId) + const fetchedPatient = await PatientRepository.find(fetchedLab.patient) dispatch(fetchLabSuccess({ lab: fetchedLab, patient: fetchedPatient })) } const validateLabRequest = (newLab: Lab): Error => { const labRequestError: Error = {} - if (!newLab.patientId) { + if (!newLab.patient) { labRequestError.patient = 'labs.requests.error.patientRequired' } @@ -116,7 +116,7 @@ export const requestLab = (newLab: Lab, onSuccess?: (lab: Lab) => void): AppThun } else { newLab.status = 'requested' newLab.requestedOn = new Date(Date.now().valueOf()).toISOString() - newLab.requestedBy = getState().user.user.id + newLab.requestedBy = getState().user.user?.id || '' const requestedLab = await LabRepository.save(newLab) dispatch(requestLabSuccess(requestedLab)) diff --git a/src/labs/labs-slice.ts b/src/labs/labs-slice.ts index 2951d739b6..0acd4c8a50 100644 --- a/src/labs/labs-slice.ts +++ b/src/labs/labs-slice.ts @@ -1,9 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import LabRepository from '../clients/db/LabRepository' -import SortRequest from '../clients/db/SortRequest' -import Lab from '../model/Lab' -import { AppThunk } from '../store' +import LabRepository from '../shared/db/LabRepository' +import SortRequest from '../shared/db/SortRequest' +import Lab from '../shared/model/Lab' +import { AppThunk } from '../shared/store' interface LabsState { isLoading: boolean diff --git a/src/labs/requests/NewLabRequest.tsx b/src/labs/requests/NewLabRequest.tsx index 5c34ec865d..08fe49b664 100644 --- a/src/labs/requests/NewLabRequest.tsx +++ b/src/labs/requests/NewLabRequest.tsx @@ -4,14 +4,14 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import PatientRepository from '../../clients/db/PatientRepository' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Lab from '../../model/Lab' -import Patient from '../../model/Patient' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' import { requestLab } from '../lab-slice' const NewLabRequest = () => { @@ -22,7 +22,7 @@ const NewLabRequest = () => { const { status, error } = useSelector((state: RootState) => state.lab) const [newLabRequest, setNewLabRequest] = useState({ - patientId: '', + patient: '', type: '', notes: '', status: 'requested', @@ -39,7 +39,7 @@ const NewLabRequest = () => { const onPatientChange = (patient: Patient) => { setNewLabRequest((previousNewLabRequest) => ({ ...previousNewLabRequest, - patientId: patient.id, + patient: patient.id, })) } diff --git a/src/login/Login.tsx b/src/login/Login.tsx new file mode 100644 index 0000000000..a7cf37aacc --- /dev/null +++ b/src/login/Login.tsx @@ -0,0 +1,90 @@ +import { Alert, Container, Panel } from '@hospitalrun/components' +import React, { useEffect, useState } from 'react' +import Button from 'react-bootstrap/Button' +import { useDispatch, useSelector } from 'react-redux' +import { Redirect } from 'react-router-dom' + +import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' +import { remoteDb } from '../shared/config/pouchdb' +import logo from '../shared/static/images/logo-on-transparent.png' +import { RootState } from '../shared/store' +import { getCurrentSession, login } from '../user/user-slice' + +const Login = () => { + const dispatch = useDispatch() + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const { loginError, user } = useSelector((root: RootState) => root.user) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const init = async () => { + try { + const session = await remoteDb.getSession() + if (session.userCtx.name) { + await dispatch(getCurrentSession(session.userCtx.name)) + } + } catch (e) { + console.log(e) + } + setLoading(false) + } + + init() + }, [dispatch]) + + const onUsernameChange = (event: React.ChangeEvent) => { + const { value } = event.currentTarget + setUsername(value) + } + + const onPasswordChange = (event: React.ChangeEvent) => { + const { value } = event.currentTarget + setPassword(value) + } + + const onSignInClick = async () => { + await dispatch(login(username, password)) + } + + if (loading) { + return null + } + + if (user) { + return + } + + return ( + <> + + HospitalRun +
+ + {loginError && } + + + + + +
+ + ) +} + +export default Login diff --git a/src/model/ContactInformation.ts b/src/model/ContactInformation.ts deleted file mode 100644 index 96a494861d..0000000000 --- a/src/model/ContactInformation.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default interface ContactInformation { - phoneNumber: string - email?: string - address?: string -} diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/page-header/breadcrumbs/Breadcrumbs.tsx similarity index 95% rename from src/breadcrumbs/Breadcrumbs.tsx rename to src/page-header/breadcrumbs/Breadcrumbs.tsx index 665c13fc5f..720126e4c9 100644 --- a/src/breadcrumbs/Breadcrumbs.tsx +++ b/src/page-header/breadcrumbs/Breadcrumbs.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import { RootState } from '../store' +import { RootState } from '../../shared/store' const Breadcrumbs = () => { const history = useHistory() diff --git a/src/breadcrumbs/breadcrumbs-slice.ts b/src/page-header/breadcrumbs/breadcrumbs-slice.ts similarity index 94% rename from src/breadcrumbs/breadcrumbs-slice.ts rename to src/page-header/breadcrumbs/breadcrumbs-slice.ts index 5785326efd..a0e29d3bc2 100644 --- a/src/breadcrumbs/breadcrumbs-slice.ts +++ b/src/page-header/breadcrumbs/breadcrumbs-slice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import Breadcrumb from '../model/Breadcrumb' +import Breadcrumb from '../../shared/model/Breadcrumb' interface BreadcrumbsState { breadcrumbs: Breadcrumb[] diff --git a/src/breadcrumbs/useAddBreadcrumbs.ts b/src/page-header/breadcrumbs/useAddBreadcrumbs.ts similarity index 92% rename from src/breadcrumbs/useAddBreadcrumbs.ts rename to src/page-header/breadcrumbs/useAddBreadcrumbs.ts index f3822cecf1..7a1b62ae8c 100644 --- a/src/breadcrumbs/useAddBreadcrumbs.ts +++ b/src/page-header/breadcrumbs/useAddBreadcrumbs.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { useDispatch } from 'react-redux' -import Breadcrumb from '../model/Breadcrumb' +import Breadcrumb from '../../shared/model/Breadcrumb' import { addBreadcrumbs, removeBreadcrumbs } from './breadcrumbs-slice' export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[], withDashboard = false): void { diff --git a/src/page-header/ButtonBarProvider.tsx b/src/page-header/button-toolbar/ButtonBarProvider.tsx similarity index 100% rename from src/page-header/ButtonBarProvider.tsx rename to src/page-header/button-toolbar/ButtonBarProvider.tsx diff --git a/src/page-header/ButtonToolBar.tsx b/src/page-header/button-toolbar/ButtonToolBar.tsx similarity index 100% rename from src/page-header/ButtonToolBar.tsx rename to src/page-header/button-toolbar/ButtonToolBar.tsx diff --git a/src/page-header/title-slice.ts b/src/page-header/title/title-slice.ts similarity index 92% rename from src/page-header/title-slice.ts rename to src/page-header/title/title-slice.ts index 1025ed4456..3b3bc3339b 100644 --- a/src/page-header/title-slice.ts +++ b/src/page-header/title/title-slice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { AppThunk } from '../store' +import { AppThunk } from '../../shared/store' interface TitleState { title: string diff --git a/src/page-header/useTitle.tsx b/src/page-header/title/useTitle.tsx similarity index 100% rename from src/page-header/useTitle.tsx rename to src/page-header/title/useTitle.tsx diff --git a/src/patients/ContactInfo.tsx b/src/patients/ContactInfo.tsx new file mode 100644 index 0000000000..8a5939f220 --- /dev/null +++ b/src/patients/ContactInfo.tsx @@ -0,0 +1,147 @@ +import { Spinner, Row, Column, Icon } from '@hospitalrun/components' +import React, { useEffect, ReactElement } from 'react' +import { useTranslation } from 'react-i18next' + +import SelectWithLabelFormGroup, { + Option, +} from '../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' +import { ContactInfoPiece } from '../shared/model/ContactInformation' +import { uuid } from '../shared/util/uuid' +import ContactInfoTypes from './ContactInfoTypes' + +interface Props { + component: 'TextInputWithLabelFormGroup' | 'TextFieldWithLabelFormGroup' + data: ContactInfoPiece[] + errors?: (string | undefined)[] + label: string + name: string + isEditable?: boolean + onChange?: (newData: ContactInfoPiece[]) => void +} + +const ContactInfo = (props: Props): ReactElement => { + const { component, data, errors, label, name, isEditable, onChange } = props + + const { t } = useTranslation() + + useEffect(() => { + if (onChange && data.length === 0) { + onChange([...data, { id: uuid(), value: '' }]) + } + }, [data, onChange]) + + const typeOptions: Option[] = Object.values(ContactInfoTypes).map((value) => ({ + label: t(`patient.contactInfoType.options.${value}`), + value: `${value}`, + })) + + const header = ( + + + {t('patient.contactInfoType.label')} + & {t(label)} + + + {t(label)} + + + ) + + const componentList = { + TextInputWithLabelFormGroup, + TextFieldWithLabelFormGroup, + } + const Component = componentList[component] + + const onTypeChange = (newType: string, index: number) => { + if (onChange) { + const currentContact = { ...data[index], type: newType } + const newContacts = [...data] + newContacts.splice(index, 1, currentContact) + onChange(newContacts) + } + } + + const onValueChange = ( + event: React.ChangeEvent, + index: number, + ) => { + if (onChange) { + const newValue = event.currentTarget.value + const currentContact = { ...data[index], value: newValue } + const newContacts = [...data] + newContacts.splice(index, 1, currentContact) + onChange(newContacts) + } + } + + const entries = data.map((entry, i) => { + const error = errors ? errors[i] : undefined + return ( + + + value === entry.type)} + onChange={(values) => onTypeChange(values[0], i)} + isEditable={isEditable} + /> + + + onValueChange(event, i)} + feedback={error && t(error)} + isInvalid={!!error} + /> + + + ) + }) + + const onAddClick = () => { + if (!onChange) { + return + } + + // 1. pick up only non-empty string + const newData = data.filter(({ value }) => value.trim() !== '') + + // 2. add a new entry + newData.push({ id: uuid(), value: '' }) + + // 3. send updates + onChange(newData) + } + + const addButton = ( +
+ +
+ ) + + if (isEditable && data.length === 0) { + return + } + + return ( +
+ {data.length > 0 ? header : null} + {entries} + {isEditable ? addButton : null} +
+ ) +} + +ContactInfo.defaultProps = { + data: [], +} + +export default ContactInfo diff --git a/src/patients/ContactInfoTypes.ts b/src/patients/ContactInfoTypes.ts new file mode 100644 index 0000000000..7a10cb9048 --- /dev/null +++ b/src/patients/ContactInfoTypes.ts @@ -0,0 +1,9 @@ +enum ContactInfoTypes { + home = 'home', + mobile = 'mobile', + work = 'work', + temporary = 'temporary', + old = 'old', +} + +export default ContactInfoTypes diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index 94cdcede91..5830d650ac 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -1,51 +1,78 @@ import { Panel, Checkbox, Alert } from '@hospitalrun/components' import { startOfDay, subYears, differenceInYears } from 'date-fns' -import React from 'react' +import React, { ReactElement } from 'react' import { useTranslation } from 'react-i18next' -import DatePickerWithLabelFormGroup from '../components/input/DatePickerWithLabelFormGroup' -import SelectWithLabelFormGroup from '../components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../components/input/TextInputWithLabelFormGroup' -import Patient from '../model/Patient' +import DatePickerWithLabelFormGroup from '../shared/components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../shared/components/input/SelectWithLableFormGroup' +import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' +import { ContactInfoPiece } from '../shared/model/ContactInformation' +import Patient from '../shared/model/Patient' +import ContactInfo from './ContactInfo' + +interface Error { + message?: string + prefix?: string + givenName?: string + familyName?: string + suffix?: string + dateOfBirth?: string + preferredLanguage?: string + phoneNumbers?: (string | undefined)[] + emails?: (string | undefined)[] +} interface Props { patient: Patient isEditable?: boolean - onFieldChange?: (key: string, value: string | boolean) => void - error?: any + onChange?: (newPatient: Partial) => void + error?: Error } -const GeneralInformation = (props: Props) => { +const GeneralInformation = (props: Props): ReactElement => { const { t } = useTranslation() - const { patient, isEditable, onFieldChange, error } = props - - const onSelectChange = (event: React.ChangeEvent, fieldName: string) => - onFieldChange && onFieldChange(fieldName, event.target.value) - - const onDateOfBirthChange = (date: Date) => - onFieldChange && onFieldChange('dateOfBirth', date.toISOString()) + const { patient, isEditable, onChange, error } = props - const onInputElementChange = (event: React.ChangeEvent, fieldName: string) => - onFieldChange && onFieldChange(fieldName, event.target.value) + const onFieldChange = (name: string, value: string | boolean | ContactInfoPiece[]) => { + if (onChange) { + const newPatient = { + ...patient, + [name]: value, + } + onChange(newPatient) + } + } - const onCheckboxChange = (event: React.ChangeEvent, fieldName: string) => - onFieldChange && onFieldChange(fieldName, event.target.checked) + const guessDateOfBirthFromApproximateAge = (value: string) => { + const age = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value) + const dateOfBirth = subYears(new Date(Date.now()), age) + return startOfDay(dateOfBirth).toISOString() + } const onApproximateAgeChange = (event: React.ChangeEvent) => { - let approximateAgeNumber - if (Number.isNaN(parseFloat(event.target.value))) { - approximateAgeNumber = 0 - } else { - approximateAgeNumber = parseFloat(event.target.value) - } + const { value } = event.currentTarget + onFieldChange('dateOfBirth', guessDateOfBirthFromApproximateAge(value)) + } - const approximateDateOfBirth = subYears(new Date(Date.now()), approximateAgeNumber) - if (onFieldChange) { - onFieldChange('dateOfBirth', startOfDay(approximateDateOfBirth).toISOString()) - } + const onUnknownDateOfBirthChange = (event: React.ChangeEvent) => { + const { checked } = event.currentTarget + onFieldChange('isApproximateDateOfBirth', checked) } + const sexOptions: Option[] = [ + { label: t('sex.male'), value: 'male' }, + { label: t('sex.female'), value: 'female' }, + { label: t('sex.other'), value: 'other' }, + { label: t('sex.unknown'), value: 'unknown' }, + ] + + const typeOptions: Option[] = [ + { label: t('patient.types.charity'), value: 'charity' }, + { label: t('patient.types.private'), value: 'private' }, + ] + return (
@@ -57,11 +84,9 @@ const GeneralInformation = (props: Props) => { name="prefix" value={patient.prefix} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'prefix') - }} - isInvalid={error?.prefix} - feedback={t(error?.prefix)} + onChange={(event) => onFieldChange('prefix', event.currentTarget.value)} + isInvalid={!!error?.prefix} + feedback={error ? (error.prefix ? t(error.prefix) : undefined) : undefined} />
@@ -70,12 +95,10 @@ const GeneralInformation = (props: Props) => { name="givenName" value={patient.givenName} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'givenName') - }} + onChange={(event) => onFieldChange('givenName', event.currentTarget.value)} isRequired - isInvalid={error?.givenName} - feedback={t(error?.givenName)} + isInvalid={!!error?.givenName} + feedback={error ? (error.givenName ? t(error.givenName) : undefined) : undefined} />
@@ -84,11 +107,9 @@ const GeneralInformation = (props: Props) => { name="familyName" value={patient.familyName} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'familyName') - }} - isInvalid={error?.familyName} - feedback={t(error?.familyName)} + onChange={(event) => onFieldChange('familyName', event.currentTarget.value)} + isInvalid={!!error?.familyName} + feedback={error ? (error.familyName ? t(error.familyName) : undefined) : undefined} />
@@ -97,11 +118,9 @@ const GeneralInformation = (props: Props) => { name="suffix" value={patient.suffix} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'suffix') - }} - isInvalid={error?.suffix} - feedback={t(error?.suffix)} + onChange={(event) => onFieldChange('suffix', event.currentTarget.value)} + isInvalid={!!error?.suffix} + feedback={error ? (error.suffix ? t(error.suffix) : undefined) : undefined} />
@@ -110,32 +129,20 @@ const GeneralInformation = (props: Props) => { value === patient.sex)} + onChange={(values) => onFieldChange('sex', values[0])} isEditable={isEditable} - options={[ - { label: t('sex.male'), value: 'male' }, - { label: t('sex.female'), value: 'female' }, - { label: t('sex.other'), value: 'other' }, - { label: t('sex.unknown'), value: 'unknown' }, - ]} - onChange={(event: React.ChangeEvent) => { - onSelectChange(event, 'sex') - }} />
value === patient.type)} + onChange={(values) => onFieldChange('type', values[0])} isEditable={isEditable} - options={[ - { label: t('patient.types.charity'), value: 'charity' }, - { label: t('patient.types.private'), value: 'private' }, - ]} - onChange={(event: React.ChangeEvent) => { - onSelectChange(event, 'type') - }} />
@@ -160,12 +167,12 @@ const GeneralInformation = (props: Props) => { ? new Date(patient.dateOfBirth) : undefined } - isInvalid={error?.dateOfBirth} maxDate={new Date(Date.now().valueOf())} - feedback={t(error?.dateOfBirth)} - onChange={(date: Date) => { - onDateOfBirthChange(date) - }} + onChange={(date: Date) => onFieldChange('dateOfBirth', date.toISOString())} + isInvalid={!!error?.dateOfBirth} + feedback={ + error ? (error.dateOfBirth ? t(error.dateOfBirth) : undefined) : undefined + } /> )} @@ -175,7 +182,7 @@ const GeneralInformation = (props: Props) => { label={t('patient.unknownDateOfBirth')} name="unknown" disabled={!isEditable} - onChange={(event) => onCheckboxChange(event, 'isApproximateDateOfBirth')} + onChange={onUnknownDateOfBirthChange} /> @@ -187,9 +194,7 @@ const GeneralInformation = (props: Props) => { name="occupation" value={patient.occupation} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'occupation') - }} + onChange={(event) => onFieldChange('occupation', event.currentTarget.value)} />
@@ -198,62 +203,58 @@ const GeneralInformation = (props: Props) => { name="preferredLanguage" value={patient.preferredLanguage} isEditable={isEditable} - onChange={(event: React.ChangeEvent) => { - onInputElementChange(event, 'preferredLanguage') - }} - isInvalid={error?.preferredLanguage} - feedback={t(error?.preferredLanguage)} + onChange={(event) => onFieldChange('preferredLanguage', event.currentTarget.value)} + isInvalid={!!error?.preferredLanguage} + feedback={ + error + ? error.preferredLanguage + ? t(error.preferredLanguage) + : undefined + : undefined + } />

-
-
- + + ) => { - onInputElementChange(event, 'phoneNumber') - }} - feedback={t(error?.phoneNumber)} - isInvalid={!!error?.phoneNumber} - type="tel" + onChange={(newPhoneNumbers) => onFieldChange('phoneNumbers', newPhoneNumbers)} /> -
-
- +
+
+ + ) => { - onInputElementChange(event, 'email') - }} - type="email" - feedback={t(error?.email)} - isInvalid={!!error?.email} + onChange={(newEmails) => onFieldChange('emails', newEmails)} /> -
+
-
-
- + + ) => - onFieldChange && onFieldChange('address', event.currentTarget.value) - // eslint-disable-next-line react/jsx-curly-newline - } + onChange={(newAddresses) => onFieldChange('addresses', newAddresses)} /> -
+
diff --git a/src/patients/Patients.tsx b/src/patients/Patients.tsx index be40c699df..92e0995b59 100644 --- a/src/patients/Patients.tsx +++ b/src/patients/Patients.tsx @@ -2,9 +2,9 @@ import React from 'react' import { useSelector } from 'react-redux' import { Switch } from 'react-router-dom' -import PrivateRoute from '../components/PrivateRoute' -import Permissions from '../model/Permissions' -import { RootState } from '../store' +import PrivateRoute from '../shared/components/PrivateRoute' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' import EditPatient from './edit/EditPatient' import ViewPatients from './list/ViewPatients' import NewPatient from './new/NewPatient' diff --git a/src/patients/allergies/Allergies.tsx b/src/patients/allergies/Allergies.tsx index 9a8d9d5cc6..f39ffaa60a 100644 --- a/src/patients/allergies/Allergies.tsx +++ b/src/patients/allergies/Allergies.tsx @@ -3,11 +3,11 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import Allergy from '../../model/Allergy' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import Allergy from '../../shared/model/Allergy' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import NewAllergyModal from './NewAllergyModal' interface AllergiesProps { diff --git a/src/patients/allergies/NewAllergyModal.tsx b/src/patients/allergies/NewAllergyModal.tsx index 9ea5374bf3..9a5383df24 100644 --- a/src/patients/allergies/NewAllergyModal.tsx +++ b/src/patients/allergies/NewAllergyModal.tsx @@ -3,9 +3,9 @@ import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Allergy from '../../model/Allergy' -import { RootState } from '../../store' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Allergy from '../../shared/model/Allergy' +import { RootState } from '../../shared/store' import { addAllergy } from '../patient-slice' interface NewAllergyModalProps { diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx index af13b72e87..93ee38e70d 100644 --- a/src/patients/appointments/AppointmentsList.tsx +++ b/src/patients/appointments/AppointmentsList.tsx @@ -1,12 +1,12 @@ -import { TextInput, Button, List, ListItem, Container, Row, Column } from '@hospitalrun/components' -import React, { useEffect, useState } from 'react' +import { Button, List, ListItem, Container, Row } from '@hospitalrun/components' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice' -import { RootState } from '../../store' +import { RootState } from '../../shared/store' interface Props { patientId: string @@ -19,7 +19,6 @@ const AppointmentsList = (props: Props) => { const { patientId } = props const { appointments } = useSelector((state: RootState) => state.appointments) - const [searchText, setSearchText] = useState('') const breadcrumbs = [ { @@ -44,15 +43,6 @@ const AppointmentsList = (props: Props) => { ) - const onSearchBoxChange = (event: React.ChangeEvent) => { - setSearchText(event.target.value) - } - - const onSearchFormSubmit = (event: React.FormEvent | React.MouseEvent) => { - event.preventDefault() - dispatch(fetchPatientAppointments(patientId, searchText)) - } - return ( <>
@@ -70,25 +60,6 @@ const AppointmentsList = (props: Props) => {

-
- - - - - - - - - - {list} diff --git a/src/patients/care-plans/AddCarePlanModal.tsx b/src/patients/care-plans/AddCarePlanModal.tsx index 923652775b..7ecb0a4938 100644 --- a/src/patients/care-plans/AddCarePlanModal.tsx +++ b/src/patients/care-plans/AddCarePlanModal.tsx @@ -4,8 +4,8 @@ import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import CarePlan from '../../model/CarePlan' -import { RootState } from '../../store' +import CarePlan from '../../shared/model/CarePlan' +import { RootState } from '../../shared/store' import { addCarePlan } from '../patient-slice' import CarePlanForm from './CarePlanForm' diff --git a/src/patients/care-plans/CarePlanForm.tsx b/src/patients/care-plans/CarePlanForm.tsx index 0845250a40..b658d9dd99 100644 --- a/src/patients/care-plans/CarePlanForm.tsx +++ b/src/patients/care-plans/CarePlanForm.tsx @@ -1,13 +1,15 @@ import { Alert, Column, Row } from '@hospitalrun/components' -import React from 'react' +import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import DatePickerWithLabelFormGroup from '../../components/input/DatePickerWithLabelFormGroup' -import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' -import Patient from '../../model/Patient' +import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../shared/model/CarePlan' +import Patient from '../../shared/model/Patient' interface Error { message?: string @@ -32,6 +34,10 @@ const CarePlanForm = (props: Props) => { const { t } = useTranslation() const { patient, carePlan, carePlanError, disabled, onChange } = props + const [condition, setCondition] = useState(carePlan.diagnosisId) + const [status, setStatus] = useState(carePlan.status) + const [intent, setIntent] = useState(carePlan.intent) + const onFieldChange = (name: string, value: string | CarePlanStatus | CarePlanIntent) => { if (onChange) { const newCarePlan = { @@ -42,6 +48,13 @@ const CarePlanForm = (props: Props) => { } } + const conditionOptions: Option[] = + patient.diagnoses?.map((d) => ({ label: d.name, value: d.id })) || [] + + const statusOptions: Option[] = Object.values(CarePlanStatus).map((v) => ({ label: v, value: v })) + + const intentOptions: Option[] = Object.values(CarePlanIntent).map((v) => ({ label: v, value: v })) + return (
{carePlanError?.message && } @@ -75,44 +88,51 @@ const CarePlanForm = (props: Props) => {
+ {/* add feedback in next round */} value === condition)} + onChange={(values) => { + onFieldChange('diagnosisId', values[0]) + setCondition(values[0]) + }} isEditable={!disabled} - onChange={(event) => onFieldChange('diagnosisId', event.currentTarget.value)} - options={patient.diagnoses?.map((d) => ({ label: d.name, value: d.id })) || []} + isInvalid={!!carePlanError?.condition} /> value === status)} + onChange={(values) => { + onFieldChange('status', values[0]) + setStatus(values[0] as CarePlanStatus) + }} isEditable={!disabled} - options={Object.values(CarePlanStatus).map((v) => ({ label: v, value: v }))} - onChange={(event) => onFieldChange('status', event.currentTarget.value)} + isInvalid={!!carePlanError?.status} /> value === intent)} + onChange={(values) => { + onFieldChange('intent', values[0]) + setIntent(values[0] as CarePlanIntent) + }} isEditable={!disabled} - options={Object.values(CarePlanIntent).map((v) => ({ label: v, value: v }))} - onChange={(event) => onFieldChange('intent', event.currentTarget.value)} + isInvalid={!!carePlanError?.intent} /> @@ -145,7 +165,6 @@ const CarePlanForm = (props: Props) => { { const history = useHistory() const { t } = useTranslation() const { patient } = useSelector((state: RootState) => state.patient) - const onViewClick = (carePlan: CarePlan) => { - history.push(`/patients/${patient.id}/care-plans/${carePlan.id}`) - } - return ( -
- - - - - - - - - - - {patient.carePlans?.map((carePlan) => ( - - - - - - - - ))} - -
{t('patient.carePlan.title')}{t('patient.carePlan.startDate')}{t('patient.carePlan.endDate')}{t('patient.carePlan.status')}{t('actions.label')}
{carePlan.title}{format(new Date(carePlan.startDate), 'yyyy-MM-dd')}{format(new Date(carePlan.endDate), 'yyyy-MM-dd')}{carePlan.status} - -
+ row.id} + data={patient.carePlans || []} + columns={[ + { label: t('patient.carePlan.title'), key: 'title' }, + { + label: t('patient.carePlan.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.carePlan.endDate'), + key: 'endDate', + formatter: (row) => format(new Date(row.endDate), 'yyyy-MM-dd'), + }, + { label: t('patient.carePlan.status'), key: 'status' }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: 'actions.view', + action: (row) => history.push(`/patients/${patient.id}/care-plans/${row.id}`), + }, + ]} + /> ) } diff --git a/src/patients/care-plans/ViewCarePlan.tsx b/src/patients/care-plans/ViewCarePlan.tsx index e94297e08b..c24f9b0bd5 100644 --- a/src/patients/care-plans/ViewCarePlan.tsx +++ b/src/patients/care-plans/ViewCarePlan.tsx @@ -1,10 +1,10 @@ import findLast from 'lodash/findLast' import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' -import { useParams } from 'react-router' +import { useParams } from 'react-router-dom' -import CarePlan from '../../model/CarePlan' -import { RootState } from '../../store' +import CarePlan from '../../shared/model/CarePlan' +import { RootState } from '../../shared/store' import CarePlanForm from './CarePlanForm' const ViewCarePlan = () => { diff --git a/src/patients/diagnoses/AddDiagnosisModal.tsx b/src/patients/diagnoses/AddDiagnosisModal.tsx index 110dab5f68..c42bbf5da5 100644 --- a/src/patients/diagnoses/AddDiagnosisModal.tsx +++ b/src/patients/diagnoses/AddDiagnosisModal.tsx @@ -3,10 +3,10 @@ import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import DatePickerWithLabelFormGroup from '../../components/input/DatePickerWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Diagnosis from '../../model/Diagnosis' -import { RootState } from '../../store' +import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Diagnosis from '../../shared/model/Diagnosis' +import { RootState } from '../../shared/store' import { addDiagnosis } from '../patient-slice' interface Props { diff --git a/src/patients/diagnoses/Diagnoses.tsx b/src/patients/diagnoses/Diagnoses.tsx index 78dd595e44..24465461af 100644 --- a/src/patients/diagnoses/Diagnoses.tsx +++ b/src/patients/diagnoses/Diagnoses.tsx @@ -3,11 +3,11 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import Diagnosis from '../../model/Diagnosis' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import Diagnosis from '../../shared/model/Diagnosis' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import AddDiagnosisModal from './AddDiagnosisModal' interface Props { diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index 57babfa477..8eccc03c9e 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -4,13 +4,13 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import Patient from '../../model/Patient' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' import GeneralInformation from '../GeneralInformation' import { updatePatient, fetchPatient } from '../patient-slice' -import { getPatientFullName, getPatientName } from '../util/patient-name-util' +import { getPatientFullName } from '../util/patient-name-util' const getPatientCode = (p: Patient): string => { if (p) { @@ -69,24 +69,11 @@ const EditPatient = () => { } const onSave = async () => { - await dispatch( - updatePatient( - { - ...patient, - fullName: getPatientName(patient.givenName, patient.familyName, patient.suffix), - index: - getPatientName(patient.givenName, patient.familyName, patient.suffix) + patient.code, - }, - onSuccessfulSave, - ), - ) + await dispatch(updatePatient(patient, onSuccessfulSave)) } - const onFieldChange = (key: string, value: string | boolean) => { - setPatient({ - ...patient, - [key]: value, - }) + const onPatientChange = (newPatient: Partial) => { + setPatient(newPatient as Patient) } if (status === 'loading') { @@ -96,17 +83,17 @@ const EditPatient = () => { return (
- -
diff --git a/src/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx index b6111146a2..2fd8c80db1 100644 --- a/src/patients/labs/LabsTab.tsx +++ b/src/patients/labs/LabsTab.tsx @@ -1,11 +1,11 @@ -import { Alert } from '@hospitalrun/components' +import { Alert, Table } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' -import LabRepository from '../../clients/db/LabRepository' -import Lab from '../../model/Lab' +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' interface Props { patientId: string @@ -20,17 +20,13 @@ const LabsTab = (props: Props) => { useEffect(() => { const fetch = async () => { - const fetchedLabs = await LabRepository.findAllByPatientId(patientId) + const fetchedLabs = await PatientRepository.getLabs(patientId) setLabs(fetchedLabs) } fetch() }, [patientId]) - const onTableRowClick = (lab: Lab) => { - history.push(`/labs/${lab.id}`) - } - return (
{(!labs || labs.length === 0) && ( @@ -41,24 +37,21 @@ const LabsTab = (props: Props) => { /> )} {labs && labs.length > 0 && ( -
- - - - - - - - - {labs.map((lab) => ( - onTableRowClick(lab)} key={lab.id}> - - - - - ))} - -
{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
+ row.id} + data={labs} + columns={[ + { label: t('labs.lab.type'), key: 'type' }, + { + label: t('labs.lab.requestedOn'), + key: 'requestedOn', + formatter: (row) => format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a'), + }, + { label: t('labs.lab.status'), key: 'status' }, + ]} + actions={[{ label: t('actions.view'), action: (row) => history.push(`/labs/${row.id}`) }]} + /> )} ) diff --git a/src/patients/list/ViewPatients.tsx b/src/patients/list/ViewPatients.tsx index 60d661dc9c..51aaa381e2 100644 --- a/src/patients/list/ViewPatients.tsx +++ b/src/patients/list/ViewPatients.tsx @@ -1,19 +1,17 @@ -import { Spinner, Button, Container, Row, TextInput, Column } from '@hospitalrun/components' +import { Spinner, Button, Container, Row, TextInput, Column, Table } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useEffect, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import PageRequest from '../../clients/db/PageRequest' -import SortRequest from '../../clients/db/SortRequest' -import PageComponent, { defaultPageSize } from '../../components/PageComponent' -import useDebounce from '../../hooks/debounce' -import useUpdateEffect from '../../hooks/useUpdateEffect' -import { useButtonToolbarSetter } from '../../page-header/ButtonBarProvider' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../page-header/title/useTitle' +import SortRequest from '../../shared/db/SortRequest' +import useDebounce from '../../shared/hooks/useDebounce' +import useUpdateEffect from '../../shared/hooks/useUpdateEffect' +import { RootState } from '../../shared/store' import { searchPatients } from '../patients-slice' const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }] @@ -28,41 +26,6 @@ const ViewPatients = () => { const setButtonToolBar = useButtonToolbarSetter() - const defaultPageRequest = useRef({ - size: defaultPageSize.value, - number: 1, - nextPageInfo: { index: null }, - previousPageInfo: { index: null }, - direction: 'next', - }) - - const [userPageRequest, setUserPageRequest] = useState(defaultPageRequest.current) - - const setNextPageRequest = () => { - setUserPageRequest(() => { - const newPageRequest: PageRequest = { - number: - patients.pageRequest && patients.pageRequest.number ? patients.pageRequest.number + 1 : 1, - size: patients.pageRequest ? patients.pageRequest.size : undefined, - nextPageInfo: patients.pageRequest?.nextPageInfo, - previousPageInfo: undefined, - direction: 'next', - } - return newPageRequest - }) - } - - const setPreviousPageRequest = () => { - setUserPageRequest(() => ({ - number: - patients.pageRequest && patients.pageRequest.number ? patients.pageRequest.number - 1 : 1, - size: patients.pageRequest ? patients.pageRequest.size : undefined, - nextPageInfo: undefined, - previousPageInfo: patients.pageRequest?.previousPageInfo, - direction: 'previous', - })) - } - const [searchText, setSearchText] = useState('') const debouncedSearchText = useDebounce(searchText, 500) @@ -72,8 +35,8 @@ const ViewPatients = () => { const sortRequest: SortRequest = { sorts: [{ field: 'index', direction: 'asc' }], } - dispatch(searchPatients(debouncedSearchTextRef.current, sortRequest, userPageRequest)) - }, [dispatch, userPageRequest]) + dispatch(searchPatients(debouncedSearchTextRef.current, sortRequest)) + }, [dispatch]) useEffect(() => { const sortRequest: SortRequest = { @@ -81,7 +44,7 @@ const ViewPatients = () => { } debouncedSearchTextRef.current = debouncedSearchText - dispatch(searchPatients(debouncedSearchText, sortRequest, defaultPageRequest.current)) + dispatch(searchPatients(debouncedSearchText, sortRequest)) }, [dispatch, debouncedSearchText]) useEffect(() => { @@ -104,45 +67,30 @@ const ViewPatients = () => { const loadingIndicator = const table = ( -
- - - - - - - - - - - {patients.content.map((p) => ( - history.push(`/patients/${p.id}`)}> - - - - - - - ))} - -
{t('patient.code')}{t('patient.givenName')}{t('patient.familyName')}{t('patient.sex')}{t('patient.dateOfBirth')}
{p.code}{p.givenName}{p.familyName}{p.sex}{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'yyyy-MM-dd') : ''}
+ row.id} + columns={[ + { label: t('patient.code'), key: 'code' }, + { label: t('patient.givenName'), key: 'givenName' }, + { label: t('patient.familyName'), key: 'familyName' }, + { label: t('patient.sex'), key: 'sex' }, + { + label: t('patient.dateOfBirth'), + key: 'dateOfBirth', + formatter: (row) => + row.dateOfBirth ? format(new Date(row.dateOfBirth), 'yyyy-MM-dd') : '', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[{ label: t('actions.view'), action: (row) => history.push(`/patients/${row.id}`) }]} + /> ) const onSearchBoxChange = (event: React.ChangeEvent) => { setSearchText(event.target.value) } - const onPageSizeChange = (event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10) - setUserPageRequest(() => ({ - size: newPageSize, - number: 1, - nextPageInfo: { index: null }, - previousPageInfo: { index: null }, - direction: 'next', - })) - } - return (
@@ -157,17 +105,8 @@ const ViewPatients = () => { /> - {isLoading ? loadingIndicator : table} -
) } diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index b7b7c827ea..3dbdb120dd 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -4,13 +4,12 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import Patient from '../../model/Patient' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' import GeneralInformation from '../GeneralInformation' import { createPatient } from '../patient-slice' -import { getPatientName } from '../util/patient-name-util' const breadcrumbs = [ { i18nKey: 'patients.label', location: '/patients' }, @@ -42,38 +41,27 @@ const NewPatient = () => { } const onSave = () => { - dispatch( - createPatient( - { - ...patient, - fullName: getPatientName(patient.givenName, patient.familyName, patient.suffix), - }, - onSuccessfulSave, - ), - ) + dispatch(createPatient(patient, onSuccessfulSave)) } - const onFieldChange = (key: string, value: string | boolean) => { - setPatient({ - ...patient, - [key]: value, - }) + const onPatientChange = (newPatient: Partial) => { + setPatient(newPatient as Patient) } return (
- -
diff --git a/src/patients/notes/NewNoteModal.tsx b/src/patients/notes/NewNoteModal.tsx index 4363d45ee3..913e9a9ad4 100644 --- a/src/patients/notes/NewNoteModal.tsx +++ b/src/patients/notes/NewNoteModal.tsx @@ -3,9 +3,9 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import Note from '../../model/Note' -import { RootState } from '../../store' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import Note from '../../shared/model/Note' +import { RootState } from '../../shared/store' import { addNote } from '../patient-slice' interface Props { diff --git a/src/patients/notes/NoteTab.tsx b/src/patients/notes/NoteTab.tsx index ead93598fc..b91970ddf1 100644 --- a/src/patients/notes/NoteTab.tsx +++ b/src/patients/notes/NoteTab.tsx @@ -3,10 +3,10 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import Note from '../../model/Note' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import Note from '../../shared/model/Note' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import NewNoteModal from './NewNoteModal' interface Props { diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 2e98e75b89..b0a3b21292 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -3,15 +3,16 @@ import { isAfter, isBefore, parseISO } from 'date-fns' import { isEmpty } from 'lodash' import validator from 'validator' -import PatientRepository from '../clients/db/PatientRepository' -import Allergy from '../model/Allergy' -import CarePlan from '../model/CarePlan' -import Diagnosis from '../model/Diagnosis' -import Note from '../model/Note' -import Patient from '../model/Patient' -import RelatedPerson from '../model/RelatedPerson' -import { AppThunk } from '../store' -import { uuid } from '../util/uuid' +import PatientRepository from '../shared/db/PatientRepository' +import Allergy from '../shared/model/Allergy' +import CarePlan from '../shared/model/CarePlan' +import Diagnosis from '../shared/model/Diagnosis' +import Note from '../shared/model/Note' +import Patient from '../shared/model/Patient' +import RelatedPerson from '../shared/model/RelatedPerson' +import { AppThunk } from '../shared/store' +import { uuid } from '../shared/util/uuid' +import { cleanupPatient } from './util/set-patient-helper' interface PatientState { status: 'loading' | 'error' | 'completed' @@ -35,8 +36,8 @@ interface Error { prefix?: string familyName?: string preferredLanguage?: string - email?: string - phoneNumber?: string + emails?: (string | undefined)[] + phoneNumbers?: (string | undefined)[] } interface AddRelatedPersonError { @@ -204,15 +205,33 @@ function validatePatient(patient: Patient) { } } - if (patient.email) { - if (!validator.isEmail(patient.email)) { - error.email = 'patient.errors.invalidEmail' + if (patient.emails) { + const errors: (string | undefined)[] = [] + patient.emails.forEach((email) => { + if (!validator.isEmail(email.value)) { + errors.push('patient.errors.invalidEmail') + } else { + errors.push(undefined) + } + }) + // Only add to error obj if there's an error + if (errors.some((value) => value !== undefined)) { + error.emails = errors } } - if (patient.phoneNumber) { - if (!validator.isMobilePhone(patient.phoneNumber)) { - error.phoneNumber = 'patient.errors.invalidPhoneNumber' + if (patient.phoneNumbers) { + const errors: (string | undefined)[] = [] + patient.phoneNumbers.forEach((phoneNumber) => { + if (!validator.isMobilePhone(phoneNumber.value)) { + errors.push('patient.errors.invalidPhoneNumber') + } else { + errors.push(undefined) + } + }) + // Only add to error obj if there's an error + if (errors.some((value) => value !== undefined)) { + error.phoneNumbers = errors } } @@ -225,10 +244,11 @@ export const createPatient = ( ): AppThunk => async (dispatch) => { dispatch(createPatientStart()) - const newPatientError = validatePatient(patient) + const cleanPatient = cleanupPatient(patient) + const newPatientError = validatePatient(cleanPatient) if (isEmpty(newPatientError)) { - const newPatient = await PatientRepository.save(patient) + const newPatient = await PatientRepository.save(cleanPatient) dispatch(createPatientSuccess()) if (onSuccess) { @@ -245,9 +265,12 @@ export const updatePatient = ( onSuccess?: (patient: Patient) => void, ): AppThunk => async (dispatch) => { dispatch(updatePatientStart()) - const updateError = validatePatient(patient) + + const cleanPatient = cleanupPatient(patient) + const updateError = validatePatient(cleanPatient) + if (isEmpty(updateError)) { - const updatedPatient = await PatientRepository.saveOrUpdate(patient) + const updatedPatient = await PatientRepository.saveOrUpdate(cleanPatient) dispatch(updatePatientSuccess(updatedPatient)) if (onSuccess) { @@ -434,10 +457,6 @@ function validateCarePlan(carePlan: CarePlan): AddCarePlanError { error.condition = 'patient.carePlan.error.conditionRequired' } - if (!carePlan.note) { - error.note = 'patient.carePlan.error.noteRequired' - } - return error } diff --git a/src/patients/patients-slice.ts b/src/patients/patients-slice.ts index 54901548f1..e3daa42d56 100644 --- a/src/patients/patients-slice.ts +++ b/src/patients/patients-slice.ts @@ -1,24 +1,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import PageRequest, { UnpagedRequest } from '../clients/db/PageRequest' -import PatientRepository from '../clients/db/PatientRepository' -import SortRequest, { Unsorted } from '../clients/db/SortRequest' -import Page from '../clients/Page' -import Patient from '../model/Patient' -import { AppThunk } from '../store' +import PatientRepository from '../shared/db/PatientRepository' +import SortRequest, { Unsorted } from '../shared/db/SortRequest' +import Patient from '../shared/model/Patient' +import { AppThunk } from '../shared/store' interface PatientsState { isLoading: boolean - patients: Page + patients: Patient[] } const initialState: PatientsState = { isLoading: false, - patients: { - content: [], - hasNext: false, - hasPrevious: false, - }, + patients: [], } function startLoading(state: PatientsState) { @@ -30,7 +24,7 @@ const patientsSlice = createSlice({ initialState, reducers: { fetchPatientsStart: startLoading, - fetchPatientsSuccess(state, { payload }: PayloadAction>) { + fetchPatientsSuccess(state, { payload }: PayloadAction) { state.isLoading = false state.patients = payload }, @@ -38,27 +32,26 @@ const patientsSlice = createSlice({ }) export const { fetchPatientsStart, fetchPatientsSuccess } = patientsSlice.actions -export const fetchPatients = ( - sortRequest: SortRequest = Unsorted, - pageRequest: PageRequest = UnpagedRequest, -): AppThunk => async (dispatch) => { +export const fetchPatients = (sortRequest: SortRequest = Unsorted): AppThunk => async ( + dispatch, +) => { dispatch(fetchPatientsStart()) - const patients = await PatientRepository.findAllPaged(sortRequest, pageRequest) + const patients = await PatientRepository.findAll(sortRequest) dispatch(fetchPatientsSuccess(patients)) } export const searchPatients = ( searchString: string, sortRequest: SortRequest = Unsorted, - pageRequest: PageRequest = UnpagedRequest, ): AppThunk => async (dispatch) => { dispatch(fetchPatientsStart()) + console.log(sortRequest) let patients if (searchString.trim() === '') { - patients = await PatientRepository.findAllPaged(sortRequest, pageRequest) + patients = await PatientRepository.findAll() } else { - patients = await PatientRepository.searchPaged(searchString, pageRequest) + patients = await PatientRepository.search(searchString) } dispatch(fetchPatientsSuccess(patients)) diff --git a/src/patients/related-persons/AddRelatedPersonModal.tsx b/src/patients/related-persons/AddRelatedPersonModal.tsx index 3c1ccf906a..e3bca43c37 100644 --- a/src/patients/related-persons/AddRelatedPersonModal.tsx +++ b/src/patients/related-persons/AddRelatedPersonModal.tsx @@ -1,13 +1,14 @@ import { Modal, Alert, Typeahead, Label } from '@hospitalrun/components' +import format from 'date-fns/format' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import PatientRepository from '../../clients/db/PatientRepository' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Patient from '../../model/Patient' -import RelatedPerson from '../../model/RelatedPerson' -import { RootState } from '../../store' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../shared/db/PatientRepository' +import Patient from '../../shared/model/Patient' +import RelatedPerson from '../../shared/model/RelatedPerson' +import { RootState } from '../../shared/store' import { addRelatedPerson } from '../patient-slice' interface Props { @@ -42,6 +43,11 @@ const AddRelatedPersonModal = (props: Props) => { setRelatedPerson({ ...relatedPerson, patientId: p[0].id }) } + const onSearch = async (query: string) => { + const patients: Patient[] = await PatientRepository.search(query) + return patients.filter((p: Patient) => p.id !== patient.id) + } + const body = ( {relatedPersonError?.message && ( @@ -57,14 +63,12 @@ const AddRelatedPersonModal = (props: Props) => { placeholder={t('patient.relatedPerson')} onChange={onPatientSelect} isInvalid={!!relatedPersonError?.relatedPerson} - onSearch={async (query: string) => PatientRepository.search(query)} - renderMenuItemChildren={(p: Patient) => { - if (patient.id === p.id) { - return
- } - - return
{`${p.fullName} (${p.code})`}
- }} + onSearch={onSearch} + renderMenuItemChildren={(p: Patient) => ( +
+ {`${p.fullName} - ${format(new Date(p.dateOfBirth), 'yyyy-MM-dd')} (${p.code})`} +
+ )} /> {relatedPersonError?.relatedPerson && (
diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 526abe4097..a2247f91d6 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -1,14 +1,14 @@ -import { Button, Alert, Spinner } from '@hospitalrun/components' +import { Button, Alert, Spinner, Table } from '@hospitalrun/components' import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import PatientRepository from '../../clients/db/PatientRepository' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import PatientRepository from '../../shared/db/PatientRepository' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import { removeRelatedPerson } from '../patient-slice' import AddRelatedPersonModal from './AddRelatedPersonModal' @@ -59,18 +59,11 @@ const RelatedPersonTab = (props: Props) => { setShowRelatedPersonModal(true) } - const onRelatedPersonClick = (id: string) => { - navigateTo(`/patients/${id}`) - } const closeNewRelatedPersonModal = () => { setShowRelatedPersonModal(false) } - const onRelatedPersonDelete = ( - event: React.MouseEvent, - relatedPerson: Patient, - ) => { - event.stopPropagation() + const onRelatedPersonDelete = (relatedPerson: Patient) => { dispatch(removeRelatedPerson(patient.id, relatedPerson.id)) } @@ -96,34 +89,24 @@ const RelatedPersonTab = (props: Props) => {
{relatedPersons ? ( relatedPersons.length > 0 ? ( -
- - - - - - - - - - {relatedPersons.map((r) => ( - onRelatedPersonClick(r.id)}> - - - - - - ))} - -
{t('patient.givenName')}{t('patient.familyName')}{t('patient.relatedPersons.relationshipType')}{t('actions.label')}
{r.givenName}{r.familyName}{r.type} - -
+ row.id} + data={relatedPersons} + columns={[ + { label: t('patient.givenName'), key: 'givenName' }, + { label: t('patient.familyName'), key: 'familyName' }, + { label: t('patient.relatedPersons.relationshipType'), key: 'type' }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { label: t('actions.view'), action: (row) => navigateTo(`/patients/${row.id}`) }, + { + label: t('actions.delete'), + action: (row) => onRelatedPersonDelete(row as Patient), + buttonColor: 'danger', + }, + ]} + /> ) : ( { if (!namePart) { diff --git a/src/patients/util/set-patient-helper.ts b/src/patients/util/set-patient-helper.ts new file mode 100644 index 0000000000..87af353bd8 --- /dev/null +++ b/src/patients/util/set-patient-helper.ts @@ -0,0 +1,39 @@ +import Patient from '../../shared/model/Patient' +import { getPatientName } from './patient-name-util' + +/** + * Add full name. Get rid of empty phone numbers, emails, and addresses. + * @param patient + */ +const cleanupPatient = (patient: Patient) => { + const newPatient = { ...patient } + + const { givenName, familyName, suffix } = patient + newPatient.fullName = getPatientName(givenName, familyName, suffix) + + type cik = 'phoneNumbers' | 'emails' | 'addresses' + const contactInformationKeys: cik[] = ['phoneNumbers', 'emails', 'addresses'] + contactInformationKeys.forEach((key) => { + if (key in newPatient) { + const nonEmpty = newPatient[key] + .filter(({ value }) => value.trim() !== '') + .map((entry) => { + const newValue = entry.value.trim() + if ('type' in entry) { + return { id: entry.id, value: newValue, type: entry.type } + } + return { id: entry.id, value: newValue } + }) + + if (nonEmpty.length > 0) { + newPatient[key] = nonEmpty + } else { + delete newPatient[key] + } + } + }) + + return newPatient +} + +export { cleanupPatient } diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 8f4270d9d4..0f26ebe03a 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -2,14 +2,21 @@ import { Panel, Spinner, TabsHeader, Tab, Button } from '@hospitalrun/components import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { useParams, withRouter, Route, useHistory, useLocation } from 'react-router-dom' +import { + useParams, + withRouter, + Route, + useHistory, + useLocation, + useRouteMatch, +} from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import Patient from '../../model/Patient' -import Permissions from '../../model/Permissions' -import { useButtonToolbarSetter } from '../../page-header/ButtonBarProvider' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../page-header/title/useTitle' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import Allergies from '../allergies/Allergies' import AppointmentsList from '../appointments/AppointmentsList' import CarePlanTab from '../care-plans/CarePlanTab' @@ -34,6 +41,7 @@ const ViewPatient = () => { const history = useHistory() const dispatch = useDispatch() const location = useLocation() + const { path } = useRouteMatch() const { patient, status } = useSelector((state: RootState) => state.patient) const { permissions } = useSelector((state: RootState) => state.user) @@ -127,28 +135,28 @@ const ViewPatient = () => { /> - + - + - + - + - + - + - + - + diff --git a/src/scheduling/appointments/AppointmentDetailForm.tsx b/src/scheduling/appointments/AppointmentDetailForm.tsx index b00bd77841..e8f4139b4f 100644 --- a/src/scheduling/appointments/AppointmentDetailForm.tsx +++ b/src/scheduling/appointments/AppointmentDetailForm.tsx @@ -2,13 +2,15 @@ import { Typeahead, Label, Alert } from '@hospitalrun/components' import React from 'react' import { useTranslation } from 'react-i18next' -import PatientRepository from '../../clients/db/PatientRepository' -import DateTimePickerWithLabelFormGroup from '../../components/input/DateTimePickerWithLabelFormGroup' -import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import Appointment from '../../model/Appointment' -import Patient from '../../model/Patient' +import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../shared/db/PatientRepository' +import Appointment from '../../shared/model/Appointment' +import Patient from '../../shared/model/Patient' interface Props { appointment: Appointment @@ -22,15 +24,20 @@ const AppointmentDetailForm = (props: Props) => { const { onFieldChange, appointment, patient, isEditable, error } = props const { t } = useTranslation() - const onSelectChange = (event: React.ChangeEvent, fieldName: string) => - onFieldChange && onFieldChange(fieldName, event.target.value) - const onDateChange = (date: Date, fieldName: string) => onFieldChange && onFieldChange(fieldName, date.toISOString()) const onInputElementChange = (event: React.ChangeEvent, fieldName: string) => onFieldChange && onFieldChange(fieldName, event.target.value) + const typeOptions: Option[] = [ + { label: t('scheduling.appointment.types.checkup'), value: 'checkup' }, + { label: t('scheduling.appointment.types.emergency'), value: 'emergency' }, + { label: t('scheduling.appointment.types.followUp'), value: 'follow up' }, + { label: t('scheduling.appointment.types.routine'), value: 'routine' }, + { label: t('scheduling.appointment.types.walkIn'), value: 'walk in' }, + ] + return ( <> {error?.message && } @@ -48,7 +55,7 @@ const AppointmentDetailForm = (props: Props) => { value={patient?.fullName} placeholder={t('scheduling.appointment.patient')} onChange={ - (p: Patient[]) => onFieldChange && p[0] && onFieldChange('patientId', p[0].id) + (p: Patient[]) => onFieldChange && p[0] && onFieldChange('patient', p[0].id) // eslint-disable-next-line react/jsx-curly-newline } onSearch={async (query: string) => PatientRepository.search(query)} @@ -112,18 +119,10 @@ const AppointmentDetailForm = (props: Props) => { value === appointment.type)} + onChange={(values) => onFieldChange && onFieldChange('type', values[0])} isEditable={isEditable} - options={[ - { label: t('scheduling.appointment.types.checkup'), value: 'checkup' }, - { label: t('scheduling.appointment.types.emergency'), value: 'emergency' }, - { label: t('scheduling.appointment.types.followUp'), value: 'follow up' }, - { label: t('scheduling.appointment.types.routine'), value: 'routine' }, - { label: t('scheduling.appointment.types.walkIn'), value: 'walk in' }, - ]} - onChange={(event: React.ChangeEvent) => { - onSelectChange(event, 'type') - }} /> diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 2166722519..b90ec73d19 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -2,9 +2,9 @@ import React from 'react' import { useSelector } from 'react-redux' import { Switch } from 'react-router-dom' -import PrivateRoute from '../../components/PrivateRoute' -import Permissions from '../../model/Permissions' -import { RootState } from '../../store' +import PrivateRoute from '../../shared/components/PrivateRoute' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' import EditAppointment from './edit/EditAppointment' import NewAppointment from './new/NewAppointment' import ViewAppointment from './view/ViewAppointment' diff --git a/src/scheduling/appointments/ViewAppointments.tsx b/src/scheduling/appointments/ViewAppointments.tsx index 824d2dd662..df60389573 100644 --- a/src/scheduling/appointments/ViewAppointments.tsx +++ b/src/scheduling/appointments/ViewAppointments.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' -import PatientRepository from '../../clients/db/PatientRepository' -import { useButtonToolbarSetter } from '../../page-header/ButtonBarProvider' -import useTitle from '../../page-header/useTitle' -import { RootState } from '../../store' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../page-header/title/useTitle' +import PatientRepository from '../../shared/db/PatientRepository' +import { RootState } from '../../shared/store' import { fetchAppointments } from './appointments-slice' interface Event { @@ -54,7 +54,7 @@ const ViewAppointments = () => { const getAppointments = async () => { const newEvents = await Promise.all( appointments.map(async (a) => { - const patient = await PatientRepository.find(a.patientId) + const patient = await PatientRepository.find(a.patient) return { id: a.id, start: new Date(a.startDateTime), diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index 2836775f12..413c8820f6 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -2,16 +2,16 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { isBefore } from 'date-fns' import _ from 'lodash' -import AppointmentRepository from '../../clients/db/AppointmentRepository' -import PatientRepository from '../../clients/db/PatientRepository' -import Appointment from '../../model/Appointment' -import Patient from '../../model/Patient' -import { AppThunk } from '../../store' +import AppointmentRepository from '../../shared/db/AppointmentRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Appointment from '../../shared/model/Appointment' +import Patient from '../../shared/model/Patient' +import { AppThunk } from '../../shared/store' function validateAppointment(appointment: Appointment) { const err: Error = {} - if (!appointment.patientId) { + if (!appointment.patient) { err.patient = 'scheduling.appointment.errors.patientRequired' } @@ -101,7 +101,7 @@ export const { export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentStart()) const appointment = await AppointmentRepository.find(id) - const patient = await PatientRepository.find(appointment.patientId) + const patient = await PatientRepository.find(appointment.patient) dispatch(fetchAppointmentSuccess({ appointment, patient })) } diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index 4a6f93f663..edf21ece5c 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -1,8 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import AppointmentRepository from '../../clients/db/AppointmentRepository' -import Appointment from '../../model/Appointment' -import { AppThunk } from '../../store' +import AppointmentRepository from '../../shared/db/AppointmentRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Appointment from '../../shared/model/Appointment' +import { AppThunk } from '../../shared/store' interface AppointmentsState { isLoading: boolean @@ -38,19 +39,10 @@ export const fetchAppointments = (): AppThunk => async (dispatch) => { dispatch(fetchAppointmentsSuccess(appointments)) } -export const fetchPatientAppointments = ( - patientId: string, - searchString?: string, -): AppThunk => async (dispatch) => { +export const fetchPatientAppointments = (patientId: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentsStart()) - let appointments - if (searchString === undefined || searchString.trim() === '') { - const query = { selector: { patientId } } - appointments = await AppointmentRepository.search(query) - } else { - appointments = await AppointmentRepository.searchPatientAppointments(patientId, searchString) - } + const appointments = await PatientRepository.getAppointments(patientId) dispatch(fetchAppointmentsSuccess(appointments)) } diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index 765700f8f5..94280e1ddd 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' -import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' -import Appointment from '../../../model/Appointment' -import useTitle from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../../page-header/title/useTitle' +import Appointment from '../../../shared/model/Appointment' +import { RootState } from '../../../shared/store' import { updateAppointment, fetchAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' import { getAppointmentLabel } from '../util/scheduling-appointment.util' diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 3573a59367..49c2687896 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' -import Appointment from '../../../model/Appointment' -import useTitle from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../../page-header/title/useTitle' +import Appointment from '../../../shared/model/Appointment' +import { RootState } from '../../../shared/store' import { createAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' @@ -29,7 +29,7 @@ const NewAppointment = () => { const { error } = useSelector((state: RootState) => state.appointment) const [appointment, setAppointment] = useState({ - patientId: '', + patient: '', startDateTime: startDateTime.toISOString(), endDateTime: endDateTime.toISOString(), location: '', diff --git a/src/scheduling/appointments/util/scheduling-appointment.util.ts b/src/scheduling/appointments/util/scheduling-appointment.util.ts index eb1d28bdd8..32604500a6 100644 --- a/src/scheduling/appointments/util/scheduling-appointment.util.ts +++ b/src/scheduling/appointments/util/scheduling-appointment.util.ts @@ -1,4 +1,4 @@ -import Appointment from '../../../model/Appointment' +import Appointment from '../../../shared/model/Appointment' const options = { year: 'numeric', diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index cf3782ee93..221fe0d868 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' -import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' -import Permissions from '../../../model/Permissions' -import { useButtonToolbarSetter } from '../../../page-header/ButtonBarProvider' -import useTitle from '../../../page-header/useTitle' -import { RootState } from '../../../store' +import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import { useButtonToolbarSetter } from '../../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../../page-header/title/useTitle' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' import { fetchAppointment, deleteAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' import { getAppointmentLabel } from '../util/scheduling-appointment.util' diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 74df245d45..d3fe2dc91f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -2,8 +2,8 @@ import { Row, Column } from '@hospitalrun/components' import React from 'react' import { useTranslation } from 'react-i18next' -import LanguageSelector from '../components/input/LanguageSelector' -import useTitle from '../page-header/useTitle' +import useTitle from '../page-header/title/useTitle' +import LanguageSelector from '../shared/components/input/LanguageSelector' const Settings = () => { const { t } = useTranslation() diff --git a/src/setupTests.js b/src/setupTests.js index df94f8bf90..c4dd3d9893 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -2,4 +2,8 @@ import Enzyme from 'enzyme' import Adapter from 'enzyme-adapter-react-16' +import './__mocks__/i18next' +import './__mocks__/matchMediaMock' +import './__mocks__/react-i18next' + Enzyme.configure({ adapter: new Adapter() }) diff --git a/src/components/PageComponent.tsx b/src/shared/components/PageComponent.tsx similarity index 81% rename from src/components/PageComponent.tsx rename to src/shared/components/PageComponent.tsx index 2c7716a755..1d7654837b 100644 --- a/src/components/PageComponent.tsx +++ b/src/shared/components/PageComponent.tsx @@ -18,7 +18,6 @@ const PageComponent = ({ pageNumber, setPreviousPageRequest, setNextPageRequest, - onPageSizeChange, }: any) => { const { t } = useTranslation() @@ -48,13 +47,7 @@ const PageComponent = ({ {t('actions.page')} {pageNumber}
- + - - {options.map((option) => ( - - ))} - + />
) } SelectWithLabelFormGroup.defaultProps = { - value: '', + placeholder: '-- Choose --', } export default SelectWithLabelFormGroup +export type { Option } diff --git a/src/components/input/TextFieldWithLabelFormGroup.tsx b/src/shared/components/input/TextFieldWithLabelFormGroup.tsx similarity index 88% rename from src/components/input/TextFieldWithLabelFormGroup.tsx rename to src/shared/components/input/TextFieldWithLabelFormGroup.tsx index 5fcd4b4863..857034706e 100644 --- a/src/components/input/TextFieldWithLabelFormGroup.tsx +++ b/src/shared/components/input/TextFieldWithLabelFormGroup.tsx @@ -3,10 +3,9 @@ import React from 'react' interface Props { value: string - label: string + label?: string name: string isEditable?: boolean - placeholder?: string onChange?: (event: React.ChangeEvent) => void isRequired?: boolean feedback?: string @@ -18,7 +17,7 @@ const TextFieldWithLabelFormGroup = (props: Props) => { const id = `${name}TextField` return (
-