diff --git a/.github/workflows/buildReleaseContainers.yaml b/.github/workflows/buildReleaseContainers.yaml deleted file mode 100644 index c809d5c37..000000000 --- a/.github/workflows/buildReleaseContainers.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Release Specific Containers - -on: - workflow_call: - inputs: - container-tag: - type: string - required: true - workflow_dispatch: - inputs: - container-tag: - type: string - required: true - -jobs: - list-containers: - uses: ./.github/workflows/listContainers.yaml - build: - name: Build containers - needs: list-containers - runs-on: ubuntu-latest - strategy: - matrix: - container-to-build: ${{fromJson(needs.list-containers.outputs.all-containers)}} - permissions: - contents: "read" - id-token: "write" - packages: "write" - steps: - - name: Check Out Changes - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.container-tag }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to the Container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - ref: ${{ inputs.container-tag }} - images: ghcr.io/${{ github.repository }}/${{matrix.container-to-build}} - # this sets the version for tags and labels for each of the containers to be - # be the same as the version/tag where the code was pulled from - tags: | - type=semver,pattern={{raw}},value=${{ inputs.container-tag }} - type=ref,event=branch - type=ref,event=tag,pattern={{raw}},value=${{ inputs.container-tag }} - labels: | - org.opencontainers.image.version=${{ inputs.container-tag }} - - - name: Build and push - uses: docker/build-push-action@v3 - if: ${{ !contains(fromJSON('["ecr-viewer", "tefca-viewer"]'), matrix.container-to-build) }} - with: - context: ./containers/${{matrix.container-to-build}} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Build and push with shared-resources - uses: docker/build-push-action@v3 - if: ${{ contains(fromJSON('["ecr-viewer", "tefca-viewer"]'), matrix.container-to-build) }} - with: - context: . - file: ./containers/${{ matrix.container-to-build }}/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 830742367..6e907f625 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -35,9 +35,16 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: lowercase repo name to use in following step + env: + REPO: ${{ github.repository }} + run: | + echo "REPO=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + - name: Build and push uses: docker/build-push-action@v3 with: context: ./query-connector push: true - tags: ghcr.io/${{ github.repository }}/query-connector:main, ghcr.io/${{ github.repository }}/query-connector:latest + tags: | + ghcr.io/${{ env.REPO }}/query-connector:main, ghcr.io/${{ env.REPO }}/query-connector:latest diff --git a/.github/workflows/createNewRelease.yaml b/.github/workflows/createNewRelease.yaml deleted file mode 100644 index 8468c7229..000000000 --- a/.github/workflows/createNewRelease.yaml +++ /dev/null @@ -1,285 +0,0 @@ -name: Create New Release -on: - workflow_dispatch: - push: - branches: - - main - -# Run all tests before making a release -jobs: - # Only make a release if commit contains [RELEASE] - check-commit-message: - runs-on: ubuntu-latest - outputs: - contains_release: ${{ steps.commit_check.outputs.contains_release }} - steps: - - name: Check out code - uses: actions/checkout@v4 - - name: Check commit message for [RELEASE] - id: commit_check - run: | - if [[ "${{ github.event.head_commit.message }}" == *"[RELEASE]"* ]]; then - echo "contains_release=true" >> $GITHUB_OUTPUT - else - echo "contains_release=false" >> $GITHUB_OUTPUT - fi - list-containers: - needs: check-commit-message - if: ${{ needs.check-commit-message.outputs.contains_release == 'true' }} - uses: ./.github/workflows/listContainers.yaml - - test-for-release: - needs: - - check-commit-message - - list-containers - if: ${{ needs.check-commit-message.outputs.contains_release == 'true' }} - strategy: - matrix: - container: ${{fromJson(needs.list-containers.outputs.all-containers)}} - uses: ./.github/workflows/run-container-workflow.yaml - with: - container: ${{ matrix.container }} - secrets: inherit - - tag-release: - name: Update phdi init version number - needs: test-for-release - permissions: - contents: write - outputs: - version: ${{ steps.get_version.outputs.version }} - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "0" - ref: "main" - - name: Install poetry and dependencies - run: | - pip install poetry - # update the version number in the phdi/__init.py__ file - - name: Get PHDI Version - id: get_version - run: | - VERSION_WITH_PHDI=$(poetry version) - echo "version=${VERSION_WITH_PHDI:5}" >> $GITHUB_OUTPUT - # Create new release tag - - name: Set up Git user - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - name: Tag Release - uses: EndBug/latest-tag@latest - with: - ref: ${{ steps.get_version.outputs.version }} - # Generate release notes - - name: Generate release notes - id: release_notes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - latest_release_date=$(gh api "/repos/$GITHUB_REPOSITORY/releases?per_page=1" | jq ".[0].created_at") - prs=$(gh pr list --base main --state closed --json title,number,url,closedAt -q ".[] | select(.closedAt > $latest_release_date) | select(.title | contains(\"RELEASE\") | not)") - - echo "# Release Notes" > release-notes.md - echo "" >> release-notes.md - echo "" >> release-notes.md - - while IFS= read -r pr; do - pr_title=$(echo "$pr" | jq -r ".title") - pr_number=$(echo "$pr" | jq -r ".number") - pr_url=$(echo "$pr" | jq -r ".url") - line_text="- $pr_title ([#$pr_number]($pr_url))" - echo $line_text >> release-notes.md - done < <(printf '%s' "$prs") - - # Create new release based upon the latest created tag - - name: Create Release - id: create_release - uses: actions/create-release@v1.1.4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.get_version.outputs.version }} - release_name: ${{ steps.get_version.outputs.version }} - body_path: release-notes.md - - release-to-pypi: - name: Build and publish PHDI to PyPI - needs: tag-release - runs-on: ubuntu-latest - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "0" - ref: ${{ needs.tag-release.outputs.version }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - # Rebuild all containers for the new release - build-containers-for-release: - needs: - - tag-release - - release-to-pypi - permissions: - contents: read - packages: write - id-token: write - uses: ./.github/workflows/buildReleaseContainers.yaml - with: - container-tag: ${{ needs.tag-release.outputs.version }} - - # Create updated PHDI docs for the latest release - generate-and-update-phdi-docs: - needs: - - tag-release - - build-containers-for-release - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "0" - ref: ${{ needs.tag-release.outputs.version }} - - - name: Install poetry and dependencies - run: | - pip install poetry - poetry install - - - name: Set environment variable for pdoc - run: echo "PDOC_ALLOW_EXEC=1" >> $GITHUB_ENV - - - name: Clear Poetry Cache - run: | - poetry cache clear pypi --all - poetry cache clear virtualenvs --all - - - name: Generate docs and move to docs branch - run: | - poetry run pdoc ./phdi -o ./docs/${{ needs.tag-release.outputs.version }}/sdk - - - uses: actions/upload-artifact@v3 - with: - name: phdi-docs - path: ./docs/${{ needs.tag-release.outputs.version }}/sdk - - # Create updated container docs for the latest release - generate-and-update-container-docs: - needs: - - tag-release - - list-containers - - generate-and-update-phdi-docs - permissions: - contents: write - runs-on: ubuntu-latest - services: - test-db: - image: postgres:13-alpine3.16 - env: - POSTGRES_PASSWORD: pw - POSTGRES_DB: testdb - POSTGRES_USER: postgres - # Set health checks to wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --name testdb - ports: - - 5432:5432 - strategy: - matrix: - container: ${{fromJson(needs.list-containers.outputs.python-containers)}} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "0" - ref: ${{ needs.tag-release.outputs.version }} - - - name: Update Container Documentation - env: - MPI_DBNAME: testdb - MPI_PASSWORD: pw - MPI_DB_TYPE: postgres - MPI_HOST: localhost - MPI_USER: postgres - MPI_PORT: 5432 - MPI_PATIENT_TABLE: patient - MPI_PERSON_TABLE: person - run: | - npm i -g redoc-cli - CONTAINER=${{ matrix.container }} - cd $GITHUB_WORKSPACE/containers/$CONTAINER - cp $GITHUB_WORKSPACE/utils/make_openapi_json.py . - pip install -r requirements.txt - python make_openapi_json.py - redoc-cli build -o $GITHUB_WORKSPACE/docs/${{ needs.tag-release.outputs.version }}/containers/$CONTAINER.html openapi.json - - - uses: actions/upload-artifact@v3 - with: - name: container-docs - path: ./docs/${{ needs.tag-release.outputs.version }}/containers - - commit-docs: - needs: - - tag-release - - generate-and-update-phdi-docs - - generate-and-update-container-docs - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: docs - - - name: Download phdi docs from artifacts - uses: actions/download-artifact@v4.1.7 - with: - name: phdi-docs - path: ./docs/${{ needs.tag-release.outputs.version }}/sdk - - - name: Download container docs from artifacts - uses: actions/download-artifact@v4.1.7 - with: - name: container-docs - path: ./docs/${{ needs.tag-release.outputs.version }}/containers - - - name: Copy to latest folder - run: | - rm -rf ./docs/latest - mkdir -p ./docs/latest/sdk - mkdir -p ./docs/latest/containers - cp -r ./docs/${{ needs.tag-release.outputs.version }}/sdk/* ./docs/latest/sdk - cp -r ./docs/${{ needs.tag-release.outputs.version }}/containers/* ./docs/latest/containers - - - name: Commit New Documentation - uses: EndBug/add-and-commit@v9 - with: - add: docs - message: Automated update of docs for ${{ needs.tag-release.outputs.version }} release. diff --git a/.github/workflows/run-container-workflow.yaml b/.github/workflows/run-container-workflow.yaml deleted file mode 100644 index fd942e81f..000000000 --- a/.github/workflows/run-container-workflow.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: "Run Container Workflow" -on: - workflow_call: - inputs: - container: - required: true - type: string -jobs: - run-container-workflow: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run specified workflow - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh workflow run .github/workflows/container-${{ inputs.container }}.yaml diff --git a/.gitignore b/.gitignore index 1e50b12b3..7b750b314 100644 --- a/.gitignore +++ b/.gitignore @@ -78,15 +78,9 @@ persistent_storage/ *.egg-info build/ -## ECR Viewer Seed data -/containers/ecr-viewer/seed-scripts/fhir_data/ +## query-connector playwright tests +/test-results/ -.turbo -out -tsconfig.tsbuildinfo .env .local.env - -## Make local config file -/containers/local-config.env \ No newline at end of file diff --git a/query-connector/design-review/README.MD b/query-connector/design-review/README.MD index 4126ef31b..f372baa1d 100644 --- a/query-connector/design-review/README.MD +++ b/query-connector/design-review/README.MD @@ -1,22 +1,22 @@ # Design Review Script -A bash script intended to allow designers and members of the team to easily spin up an instance of the TEFCA Viewer on their local machines for UI/UX review. Currently only MacOS is supported. +A bash script intended to allow designers and members of the team to easily spin up an instance of the Query Connector on their local machines for UI/UX review. Currently only MacOS is supported. ## Initial Setup In order to use this script a copy must be downloaded onto your computer from GitHub . To do this follow these instructions. + 1. Open the Terminal application. -2. Copy and paste `curl https://raw.githubusercontent.com/CDCgov/phdi/main/containers/tefca-viewer/design-review/design-review.sh -O` into the Terminal prompt and press enter. This command uses the wget program to download a copy the `design-review.sh` file from this directory to the root level of your user directory e.g. `Users/johndoe`. +2. Copy and paste `curl https://raw.githubusercontent.com/CDCgov/dibbs-query-connector/main/query-connector/design-review/design-review.sh -O` into the Terminal prompt and press enter. This command uses the wget program to download a copy the `design-review.sh` file from this directory to the root level of your user directory e.g. `Users/johndoe`. 3. Copy and paste `chmod +x design-review.sh` into Terminal and press enter. This command assigns executable permissions to `design-review.sh` allowing it to be run. - ## Usage -Follow these steps to run script and spin up a local instance of the TEFCA Viewer. +Follow these steps to run script and spin up a local instance of the Query Connector. 1. Ensure you have completed the initial setup instructions. 2. Open the Terminal application. 3. Copy and paste `./design-review.sh ` into the Terminal prompt. -4. Replace `` with the name of the GitHub branch you would like to conduct a review on. For example, `./design-review.sh main` will spin up an instance of the TEFCA Viewer based on the current state of the `main` branch of repository. -5. Press enter. The script will now ensure that all required dependencies are installed on your machine, build and run the TEFCA Viewer, and finally navigate to the landing page in your system's default browser. Please note that because certain dependencies may need to be installed the script make take a few minutes the first time it is run on a machine. Additionally, depending on what needs to be installed you may be prompted at points during installation of dependencies to provide a password or click through some installation screens. -6. When you are done with your review to shut the TEFCA Viewer down return to Terminal and press enter. +4. Replace `` with the name of the GitHub branch you would like to conduct a review on. For example, `./design-review.sh main` will spin up an instance of the Query Connector based on the current state of the `main` branch of repository. +5. Press enter. The script will now ensure that all required dependencies are installed on your machine, build and run the Query Connector, and finally navigate to the landing page in your system's default browser. Please note that because certain dependencies may need to be installed the script make take a few minutes the first time it is run on a machine. Additionally, depending on what needs to be installed you may be prompted at points during installation of dependencies to provide a password or click through some installation screens. +6. When you are done with your review to shut the Query Connector down return to Terminal and press enter. diff --git a/query-connector/design-review/design-review.sh b/query-connector/design-review/design-review.sh index cb8116676..3c66ca101 100755 --- a/query-connector/design-review/design-review.sh +++ b/query-connector/design-review/design-review.sh @@ -45,8 +45,8 @@ if ! command_exists docker-compose; then fi # Clone the repository if it doesn't exist, otherwise pull the latest changes -REPO_URL="https://github.com/CDCgov/phdi.git" -REPO_DIR="phdi" +REPO_URL="https://github.com/CDCgov/dibbs-query-connector.git" +REPO_DIR="dibbs-query-connector" if [ ! -d "$REPO_DIR" ]; then git clone $REPO_URL @@ -56,7 +56,7 @@ else git pull fi -cd ./containers/tefca-viewer +cd ./query-connector # Checkout the specified branch git checkout $BRANCH_NAME diff --git a/query-connector/e2e/alternate_queries.spec.ts b/query-connector/e2e/alternate_queries.spec.ts new file mode 100644 index 000000000..3601b3fec --- /dev/null +++ b/query-connector/e2e/alternate_queries.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from "@playwright/test"; +import { TEST_URL } from "../playwright-setup"; +import { STEP_TWO_PAGE_TITLE } from "@/app/query/components/patientSearchResults/PatientSearchResultsTable"; +import { STEP_THREE_PAGE_TITLE } from "@/app/query/components/selectQuery/SelectSavedQuery"; +import { TEST_PATIENT, TEST_PATIENT_NAME } from "./constants"; + +test.describe("alternate queries with the Query Connector", () => { + test.beforeEach(async ({ page }) => { + // Start every test on our main landing page + await page.goto(TEST_URL); + }); + + test("query using form-fillable demo patient by phone number", async ({ + page, + }) => { + await page.getByRole("button", { name: "Go to the demo" }).click(); + await page.getByRole("button", { name: "Fill fields" }).click(); + + // Delete last name and MRN to force phone number as one of the 3 fields + await page.getByLabel("Last Name").clear(); + await page.getByLabel("Medical Record Number").clear(); + + // Among verification, make sure phone number is right + await page.getByRole("button", { name: "Search for patient" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await expect( + page.getByRole("heading", { name: STEP_TWO_PAGE_TITLE }), + ).toBeVisible(); + await page.getByRole("link", { name: "Select patient" }).click(); + await expect( + page.getByRole("heading", { name: STEP_THREE_PAGE_TITLE }), + ).toBeVisible(); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + + await expect(page.getByText("Patient Name")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT_NAME)).toBeVisible(); + await expect(page.getByText("Contact")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT.Phone)).toBeVisible(); + await expect(page.getByText("Patient Identifiers")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT.MRN)).toBeVisible(); + }); + + test("social determinants query with generalized function", async ({ + page, + }) => { + await page.getByRole("button", { name: "Go to the demo" }).click(); + await page.getByRole("button", { name: "Fill fields" }).click(); + await page.getByRole("button", { name: "Search for patient" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + + await page.getByRole("link", { name: "Select patient" }).click(); + await expect( + page.getByRole("heading", { name: "Select a query" }), + ).toBeVisible(); + await page.getByTestId("Select").selectOption("social-determinants"); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + + await expect( + page.getByRole("heading", { name: "Patient Record" }), + ).toBeVisible(); + }); + + test("form-fillable STI query using generalized function", async ({ + page, + }) => { + await page.getByRole("button", { name: "Go to the demo" }).click(); + await page.getByRole("button", { name: "Fill fields" }).click(); + await page.getByRole("button", { name: "Search for patient" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + await page.getByRole("link", { name: "Select patient" }).click(); + await page.getByTestId("Select").selectOption("chlamydia"); + await page.getByRole("button", { name: "Submit" }).click(); + await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); + + await expect( + page.getByRole("heading", { name: "Patient Record" }), + ).toBeVisible(); + }); +}); diff --git a/query-connector/e2e/constants.ts b/query-connector/e2e/constants.ts new file mode 100644 index 000000000..af47a4598 --- /dev/null +++ b/query-connector/e2e/constants.ts @@ -0,0 +1,5 @@ +import { hyperUnluckyPatient } from "@/app/constants"; + +export const TEST_PATIENT = hyperUnluckyPatient; +export const TEST_PATIENT_NAME = + hyperUnluckyPatient.FirstName + " A. " + hyperUnluckyPatient.LastName; diff --git a/query-connector/e2e/example.spec.ts b/query-connector/e2e/example.spec.ts deleted file mode 100644 index b60fe7cd0..000000000 --- a/query-connector/e2e/example.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("has title", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test("get started link", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Click the get started link. - await page.getByRole("link", { name: "Get started" }).click(); - - // Expects page to have a heading with the name of Installation. - await expect( - page.getByRole("heading", { name: "Installation" }), - ).toBeVisible(); -}); diff --git a/query-connector/e2e/load.spec.ts b/query-connector/e2e/load.spec.ts new file mode 100644 index 000000000..e11565471 --- /dev/null +++ b/query-connector/e2e/load.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from "@playwright/test"; +import { TEST_URL } from "../playwright-setup"; +import { metadata } from "@/app/constants"; + +test("landing page loads", async ({ page }) => { + await page.goto(TEST_URL); + + // Check that each expected text section is present + await expect( + page.getByRole("heading", { name: "Data collection made easier" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "What is it?" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "How does it work?" }), + ).toBeVisible(); + + // Check that interactable elements are present (header and Get Started) + await expect(page.getByRole("link", { name: metadata.title })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Go to the demo" }), + ).toBeVisible(); +}); diff --git a/query-connector/e2e/query_workflow.spec.ts b/query-connector/e2e/query_workflow.spec.ts index 5fd47b08f..e7dc43212 100644 --- a/query-connector/e2e/query_workflow.spec.ts +++ b/query-connector/e2e/query_workflow.spec.ts @@ -2,48 +2,24 @@ import { test, expect } from "@playwright/test"; import { TEST_URL } from "../playwright-setup"; - -test.describe("querying with the TryTEFCA viewer", () => { +import { STEP_ONE_PAGE_TITLE } from "@/app/query/components/searchForm/SearchForm"; +import { + CONTACT_US_DISCLAIMER_EMAIL, + CONTACT_US_DISCLAIMER_TEXT, +} from "@/app/query/designSystem/SiteAlert"; +import { TEST_PATIENT, TEST_PATIENT_NAME } from "./constants"; + +test.describe("querying with the Query Connector", () => { test.beforeEach(async ({ page }) => { // Start every test on our main landing page await page.goto(TEST_URL); }); - test("landing page loads", async ({ page }) => { - // Check that each expected text section is present - await expect( - page.getByRole("heading", { name: "Data collection made easier" }), - ).toBeVisible(); - await expect( - page.getByRole("heading", { name: "What is it?" }), - ).toBeVisible(); - await expect( - page.getByRole("heading", { name: "How does it work?" }), - ).toBeVisible(); - - // Check that interactable elements are present (header and Get Started) - await expect( - page.getByRole("link", { name: "TEFCA Viewer" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Go to the demo" }), - ).toBeVisible(); - }); - test("unsuccessful user query: no patients", async ({ page }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); - await page - .getByLabel("Query", { exact: true }) - .selectOption("social-determinants"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("HELIOS Meld: Direct"); - - await page.getByLabel("First Name").fill("Ellie"); - await page.getByLabel("Last Name").fill("Williams"); - await page.getByLabel("Phone Number").fill("5555555555"); - await page.getByLabel("Medical Record Number").fill("TLOU1TLOU2"); + await page.getByRole("button", { name: "Fill fields" }).click(); + await page.getByLabel("First Name").fill("Shouldnt"); + await page.getByLabel("Last Name").fill("Findanyone"); await page.getByRole("button", { name: "Search for patient" }).click(); // Better luck next time, user! @@ -58,9 +34,7 @@ test.describe("querying with the TryTEFCA viewer", () => { .click(); }); - test("successful demo user query: the quest for watermelon mcgee", async ({ - page, - }) => { + test("successful demo user query", async ({ page }) => { await page.getByRole("button", { name: "Go to the demo" }).click(); // Check that the info alert is visible and contains the correct text @@ -70,225 +44,78 @@ test.describe("querying with the TryTEFCA viewer", () => { "This site is for demo purposes only. Please do not enter PII on this website.", ); await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), + page.getByRole("heading", { name: STEP_ONE_PAGE_TITLE, exact: true }), ).toBeVisible(); - // Put in the search parameters for the elusive fruit person - await page - .getByLabel("Query", { exact: true }) - .selectOption("newborn-screening"); - await page - .getByLabel("Patient", { exact: true }) - .selectOption("newborn-screening-referral"); await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByLabel("First Name").fill("Watermelon"); - await page.getByLabel("Last Name").fill("McGee"); - await page.getByLabel("Date of Birth").fill("2024-07-12"); - await page.getByLabel("Medical Record Number").fill("18091"); - await page.getByLabel("Phone Number").fill("5555555555"); + await page.getByLabel("First Name").fill(TEST_PATIENT.FirstName); + await page.getByLabel("Last Name").fill(TEST_PATIENT.LastName); + await page.getByLabel("Date of Birth").fill(TEST_PATIENT.DOB); + await page.getByLabel("Medical Record Number").fill(TEST_PATIENT.MRN); + await page.getByLabel("Phone Number").fill(TEST_PATIENT.Phone); await page.getByRole("button", { name: "Search for patient" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); - // Make sure we have a results page with a single patient - // Non-interactive 'div' elements in the table should be located by text - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); - await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); - - // Check that the info alert is visible and has updated to the correct text - const alert2 = page.locator(".custom-alert"); - await expect(alert2).toBeVisible(); - await expect(alert2).toHaveText( - "Interested in learning more about using the TEFCA Query Connector for your jurisdiction? Send us an email at dibbs@cdc.gov", - ); - - // Check to see if the accordion button is open + await page.getByRole("link", { name: "Select patient" }).click(); await expect( - page.getByRole("button", { name: "Observations", expanded: true }), + page.getByRole("heading", { name: "Select a query" }), ).toBeVisible(); + await page.getByTestId("Select").selectOption("chlamydia"); - // We can also just directly ask the page to find us filtered table rows - await expect(page.locator("tbody").locator("tr")).toHaveCount(5); - - // Now let's use the return to search to go back to a blank form - await page.getByRole("button", { name: "New patient search" }).click(); + await page.getByRole("button", { name: "Customize Query" }).click(); await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), + page.getByRole("heading", { name: "Customize Query" }), ).toBeVisible(); - }); - test("query using form-fillable demo patient by phone number", async ({ - page, - }) => { - await page.getByRole("button", { name: "Go to the demo" }).click(); - - await page.getByLabel("Query", { exact: true }).selectOption("syphilis"); - await page - .getByLabel("Patient", { exact: true }) - .selectOption("sti-syphilis-positive"); - await page.getByRole("button", { name: "Fill fields" }).click(); + // For some reason only in Chromium (ie not in firefox / webkit) there were + // issues connecting to the database for the cancer use case, which was resulting + // in errors on the results view screen when checking for the query result. + // Switching to chlymdia seemed to solve the issue, but leaving this check + // in just in case something similar happens in the future so the unlucky + // dev can have a note to help debug. - // Delete last name and MRN to force phone number as one of the 3 fields - await page.getByLabel("Last Name").clear(); - await page.getByLabel("Medical Record Number").clear(); - - // Among verification, make sure phone number is right - await page.getByRole("button", { name: "Search for patient" }).click(); - await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("Hyper A. Unlucky")).toBeVisible(); - await expect(page.getByText("Contact")).toBeVisible(); - await expect(page.getByText("517-425-1398")).toBeVisible(); - await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("8692756")).toBeVisible(); - }); + page.getByText("0 labs found, 0 medications found, 0 conditions found."), + ).not.toBeVisible(); - test("social determinants query with generalized function", async ({ - page, - }) => { - await page.getByRole("button", { name: "Go to the demo" }).click(); - await page - .getByLabel("Query", { exact: true }) - .selectOption("social-determinants"); - await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByRole("button", { name: "Search for patient" }).click(); - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - }); + await page.getByText("Return to Select query").click(); - test("form-fillable STI query using generalized function", async ({ - page, - }) => { - await page.getByRole("button", { name: "Go to the demo" }).click(); - await page.getByLabel("Query", { exact: true }).selectOption("chlamydia"); - await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByRole("button", { name: "Search for patient" }).click(); + await page.getByRole("button", { name: "Submit" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); - await expect( - page.getByRole("heading", { name: "Patient Record" }), - ).toBeVisible(); - }); -}); - -test.describe("Test the user journey of a 'tester'", () => { - test.beforeEach(async ({ page }) => { - // Start every test on direct tester page - await page.goto("http://localhost:3000/tefca-viewer/query/test", { - waitUntil: "load", - }); - }); - - test("query/test page loads", async ({ page }) => { - // Check that interactable elements are present - await expect( - page.getByRole("button", { name: "Data Usage Policy" }), - ).toBeVisible(); - await expect( - page.getByRole("link", { name: "TEFCA Viewer" }), - ).toBeVisible(); - - // Check that each expected text section is present - await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), - ).toBeVisible(); - await expect(page.getByLabel("Query", { exact: true })).toBeVisible(); - await expect(page.getByLabel("Patient", { exact: true })).toBeVisible(); - await expect(page.getByRole("button", { name: "Advanced" })).toBeVisible(); - }); - - test("Query for patient using auto-filled data", async ({ page }) => { - await page - .getByLabel("Query", { exact: true }) - .selectOption({ value: "newborn-screening" }); - await page - .getByLabel("Patient", { exact: true }) - .selectOption({ value: "newborn-screening-referral" }); - await page.getByRole("button", { name: "Fill fields" }).click(); - await page.getByRole("button", { name: "Search for patient" }).click(); - // Make sure we have a results page with a single patient + // Non-interactive 'div' elements in the table should be located by text await expect( page.getByRole("heading", { name: "Patient Record" }), ).toBeVisible(); await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); + await expect(page.getByText(TEST_PATIENT_NAME)).toBeVisible(); await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); - }); - - test("Query for patient by filling in data", async ({ page }) => { - await page - .getByLabel("Query", { exact: true }) - .selectOption("Newborn screening follow-up"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("HELIOS Meld: Direct"); - await page.getByLabel("First Name").fill("Watermelon"); - await page.getByLabel("Last Name").fill("McGee"); - await page.getByLabel("Phone Number").fill("5555555555"); - await page.getByLabel("Date of Birth").fill("2024-07-12"); - await page.getByLabel("Medical Record Number").fill("18091"); - - await page.getByRole("button", { name: "Search for patient" }).click(); - - // Make sure we have a results page with a single patient await expect( - page.getByRole("heading", { name: "Patient Record" }), + page.getByText(`Medical Record Number: ${TEST_PATIENT.MRN}`), ).toBeVisible(); - await expect(page.getByText("Patient Name")).toBeVisible(); - await expect(page.getByText("WATERMELON SPROUT MCGEE")).toBeVisible(); - await expect(page.getByText("Patient Identifiers")).toBeVisible(); - await expect(page.getByText("MRN: 18091")).toBeVisible(); - }); - - test("Query with multiple patients returned", async ({ page }) => { - // Query for a patient with multiple results - await page - .getByLabel("Query", { exact: true }) - .selectOption("Chlamydia case investigation"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("JMC Meld: Direct"); - await page.getByLabel("Last Name").fill("JMC"); - await page.getByRole("button", { name: "Search for patient" }).click(); - // Make sure all the elements for the multiple patients view appear - await expect( - page.getByRole("heading", { name: "Select a patient" }), - ).toBeVisible(); - // Check that there is a Table element with the correct headers - await expect(page.locator("thead").locator("tr")).toHaveText( - "NameDOBContactAddressMRNActions", + // Check that the info alert is visible and has updated to the correct text + const alert2 = page.locator(".custom-alert"); + await expect(alert2).toBeVisible(); + await expect(alert2).toHaveText( + `${CONTACT_US_DISCLAIMER_TEXT} ${CONTACT_US_DISCLAIMER_EMAIL}`, ); - // Check that there are multiple rows in the table - await expect(page.locator("tbody").locator("tr")).toHaveCount(10); - - // Click on the first patient's "Select patient" button - await page.locator(':nth-match(:text("Select patient"), 1)').click(); - - // Make sure we have a results page with a single patient & appropriate back buttons await expect( - page.getByRole("heading", { name: "Patient Record" }), + page.getByRole("button", { name: "Observations", expanded: true }), ).toBeVisible(); await expect( - page.getByRole("button", { name: "New patient search" }), + page.getByRole("button", { name: "Medication Requests", expanded: true }), ).toBeVisible(); + // We can also just directly ask the page to find us filtered table rows + await expect(page.locator("tbody").locator("tr")).toHaveCount(32); + + // Now let's use the return to search to go back to a blank form await page.getByRole("button", { name: "New patient search" }).click(); await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), + page.getByRole("heading", { name: STEP_ONE_PAGE_TITLE, exact: true }), ).toBeVisible(); }); }); diff --git a/query-connector/next-env.d.ts b/query-connector/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/query-connector/next-env.d.ts +++ b/query-connector/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/query-connector/src/app/CustomQuery.ts b/query-connector/src/app/CustomQuery.ts index beb1396cb..3f72897bb 100644 --- a/query-connector/src/app/CustomQuery.ts +++ b/query-connector/src/app/CustomQuery.ts @@ -75,7 +75,7 @@ export class CustomQuery { : ""; this.medicationRequestQuery = rxnormFilter !== "" - ? `/MedicationRequest?subject=${patientId}&code=${rxnormFilter}&_include=MedicationRequest:medication&_include=MedicationRequest:medication.administration` + ? `/MedicationRequest?subject=${patientId}&code=${rxnormFilter}&_include=MedicationRequest:medication&_include=MedicationRequest:intended-performer` : ""; this.socialHistoryQuery = `/Observation?subject=${patientId}&category=social-history`; this.encounterQuery = diff --git a/query-connector/src/app/constants.ts b/query-connector/src/app/constants.ts index d61ed2410..46ae1880e 100644 --- a/query-connector/src/app/constants.ts +++ b/query-connector/src/app/constants.ts @@ -37,6 +37,14 @@ export const demoQueryOptions = [ { value: "syphilis", label: "Syphilis case investigation" }, ]; +type DemoQueryOptionValue = (typeof demoQueryLabels)[number]; +export const demoQueryValToLabelMap = demoQueryOptions.reduce( + (acc, curVal) => { + acc[curVal.value as DemoQueryOptionValue] = curVal.label; + return acc; + }, + {} as Record, +); /* * Map between the queryType property used to define a demo use case's options, * and the name of that query for purposes of searching the DB. @@ -90,16 +98,17 @@ export type PatientType = | "social-determinants" | "sti-syphilis-positive"; +export const DEFAULT_DEMO_FHIR_SERVER = "Public HAPI: Direct"; /* * Common "Hyper Unlucky" patient data used for all non-newborn screening use cases */ -const hyperUnluckyPatient: DemoDataFields = { +export const hyperUnluckyPatient: DemoDataFields = { FirstName: "Hyper", LastName: "Unlucky", DOB: "1975-12-06", MRN: "8692756", Phone: "517-425-1398", - FhirServer: "Public HAPI: Direct", + FhirServer: DEFAULT_DEMO_FHIR_SERVER, UseCase: "cancer", // UseCase will be updated per case }; @@ -276,12 +285,7 @@ export const stateOptions = [ ]; /* Mode that pages can be in; determines what is displayed to the user */ -export type Mode = - | "search" - | "results" - | "customize-queries" - | "select-query" - | "patient-results"; +export type Mode = "search" | "results" | "select-query" | "patient-results"; /*Type to specify the expected components for each item in a value set that will be displayed in the CustomizeQuery component*/ @@ -310,3 +314,8 @@ export const valueSetTypeToClincalServiceTypeMap = { medications: ["mrtc"], conditions: ["dxtc", "sdtc"], }; + +export const metadata = { + title: "Query Connector", + description: "Try out TEFCA with queries for public health use cases.", +}; diff --git a/query-connector/src/app/layout.tsx b/query-connector/src/app/layout.tsx index 97ab8293f..e360e1dca 100644 --- a/query-connector/src/app/layout.tsx +++ b/query-connector/src/app/layout.tsx @@ -3,11 +3,6 @@ import Header from "./query/components/header/header"; import Footer from "./footer"; import { DataProvider } from "./utils"; -export const metadata = { - title: "TEFCA Viewer", - description: "Try out TEFCA with queries for public health use cases.", -}; - /** * Establishes the layout for the application. * @param props - Props for the component. diff --git a/query-connector/src/app/query/components/CustomizeQuery.tsx b/query-connector/src/app/query/components/CustomizeQuery.tsx index 7e4e62a78..ff26755b2 100644 --- a/query-connector/src/app/query/components/CustomizeQuery.tsx +++ b/query-connector/src/app/query/components/CustomizeQuery.tsx @@ -2,7 +2,12 @@ import React, { useState, useEffect } from "react"; import { Button } from "@trussworks/react-uswds"; -import { ValueSetType, ValueSetItem } from "../../constants"; +import { + ValueSetType, + ValueSetItem, + USE_CASES, + demoQueryValToLabelMap, +} from "../../constants"; import { UseCaseQueryResponse } from "@/app/query-service"; import LoadingView from "./LoadingView"; import { showRedirectConfirmation } from "../designSystem/redirectToast/RedirectToast"; @@ -16,7 +21,7 @@ import Backlink from "./backLink/Backlink"; interface CustomizeQueryProps { useCaseQueryResponse: UseCaseQueryResponse; - queryType: string; + queryType: USE_CASES; queryValuesets: ValueSetItem[]; setQueryValuesets: (queryVS: ValueSetItem[]) => void; goBack: () => void; @@ -125,11 +130,7 @@ const CustomizeQuery: React.FC = ({ const handleApplyChanges = () => { const selectedItems = Object.keys(valueSetOptions).reduce((acc, key) => { const items = valueSetOptions[key as ValueSetType]; - acc = acc.concat( - Object.values(items) - .flatMap((dict) => dict.items) - .filter((item) => item.include), - ); + acc = acc.concat(Object.values(items).flatMap((dict) => dict.items)); return acc; }, [] as ValueSetItem[]); setQueryValuesets(selectedItems); @@ -158,14 +159,14 @@ const CustomizeQuery: React.FC = ({ return (
- +

Customize query

- Query: {queryType} + Query: {demoQueryValToLabelMap[queryType]}
{countLabs} labs found, {countMedications} medications found,{" "} diff --git a/query-connector/src/app/query/components/PatientSearchResults.tsx b/query-connector/src/app/query/components/PatientSearchResults.tsx index ee41abc7f..5d01f6e76 100644 --- a/query-connector/src/app/query/components/PatientSearchResults.tsx +++ b/query-connector/src/app/query/components/PatientSearchResults.tsx @@ -1,12 +1,7 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { Patient } from "fhir/r4"; -import { - UseCaseQueryResponse, - UseCaseQuery, - UseCaseQueryRequest, -} from "../../query-service"; -import { Mode, ValueSetItem } from "@/app/constants"; +import { Mode } from "@/app/constants"; import Backlink from "./backLink/Backlink"; import PatientSearchResultsTable from "./patientSearchResults/PatientSearchResultsTable"; import NoPatientsFound from "./patientSearchResults/NoPatientsFound"; @@ -16,94 +11,58 @@ import NoPatientsFound from "./patientSearchResults/NoPatientsFound"; */ export interface PatientSearchResultsProps { patients: Patient[]; - originalRequest: UseCaseQueryRequest; - queryValueSets: ValueSetItem[]; - setLoading: (loading: boolean) => void; goBack: () => void; setMode: (mode: Mode) => void; - setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void; + setPatientForQueryResponse: (patient: Patient) => void; } /** * Displays multiple patient search results in a table. * @param root0 - PatientSearchResults props. * @param root0.patients - The array of Patient resources. - * @param root0.originalRequest - The original request object. - * @param root0.queryValueSets - The stateful collection of value sets to include - * in the query. - * @param root0.setLoading - The function to set the loading state. * @param root0.goBack - The function to go back to the previous page. - * @param root0.setUseCaseQueryResponse - State update function to pass the - * data needed for the results view back up to the parent component * @param root0.setMode - Redirect function to handle results view routing + * @param root0.setPatientForQueryResponse - Callback function to update the + * patient being searched for * @returns - The PatientSearchResults component. */ const PatientSearchResults: React.FC = ({ patients, - originalRequest, - queryValueSets, - setLoading, goBack, - setUseCaseQueryResponse, + setPatientForQueryResponse, setMode, }) => { useEffect(() => { window.scrollTo(0, 0); }, []); - const [patientForQuery, setPatientForQueryResponse] = useState(); - useEffect(() => { - let isSubscribed = true; - - const fetchQuery = async () => { - if (patientForQuery && isSubscribed) { - setLoading(true); - const queryResponse = await UseCaseQuery( - originalRequest, - queryValueSets, - { - Patient: [patientForQuery], - }, - ); - setUseCaseQueryResponse(queryResponse); - setMode("results"); - setLoading(false); - } - }; - - fetchQuery().catch(console.error); - - // Destructor hook to prevent future state updates - return () => { - isSubscribed = false; - }; - }, [patientForQuery]); + function handlePatientSelect(patient: Patient) { + setPatientForQueryResponse(patient); + setMode("select-query"); + } return ( <> - {patients.length === 0 && ( <> - + Revise your patient search )} {patients.length > 0 && ( <> + + - -

- Not seeing what you're looking for? -

- - - Return to patient search - )} diff --git a/query-connector/src/app/query/components/ResultsView.tsx b/query-connector/src/app/query/components/ResultsView.tsx index 23ad78470..0ddd5741d 100644 --- a/query-connector/src/app/query/components/ResultsView.tsx +++ b/query-connector/src/app/query/components/ResultsView.tsx @@ -11,11 +11,14 @@ import DiagnosticReportTable from "./resultsView/tableComponents/DiagnosticRepor import EncounterTable from "./resultsView/tableComponents/EncounterTable"; import MedicationRequestTable from "./resultsView/tableComponents/MedicationRequestTable"; import ObservationTable from "./resultsView/tableComponents/ObservationTable"; +import Backlink from "./backLink/Backlink"; +import { USE_CASES, demoQueryValToLabelMap } from "@/app/constants"; type ResultsViewProps = { useCaseQueryResponse: UseCaseQueryResponse; + selectedQuery: USE_CASES; goBack: () => void; - queryName: string; + goToBeginning: () => void; }; export type ResultsViewAccordionItem = { @@ -29,13 +32,15 @@ export type ResultsViewAccordionItem = { * @param props - The props for the QueryView component. * @param props.useCaseQueryResponse - The response from the query service. * @param props.goBack - The function to go back to the previous page. - * @param props.queryName - The name of the saved query to display to the user + * @param props.goToBeginning - Function to return to patient discover + * @param props.selectedQuery - query that's been selected to view for results * @returns The QueryView component. */ const ResultsView: React.FC = ({ useCaseQueryResponse, + selectedQuery, goBack, - queryName, + goToBeginning, }) => { useEffect(() => { window.scrollTo(0, 0); @@ -56,9 +61,13 @@ const ResultsView: React.FC = ({ <>
+ goBack()} + label={"Return to query selection"} + /> @@ -70,7 +79,9 @@ const ResultsView: React.FC = ({

Query:{" "} - {queryName} + + {demoQueryValToLabelMap[selectedQuery]} +

diff --git a/query-connector/src/app/query/components/SelectQuery.tsx b/query-connector/src/app/query/components/SelectQuery.tsx new file mode 100644 index 000000000..8617ef3e1 --- /dev/null +++ b/query-connector/src/app/query/components/SelectQuery.tsx @@ -0,0 +1,123 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { FHIR_SERVERS, USE_CASES, ValueSetItem } from "../../constants"; +import CustomizeQuery from "./CustomizeQuery"; +import SelectSavedQuery from "./selectQuery/SelectSavedQuery"; + +import { QueryResponse } from "@/app/query-service"; +import { Patient } from "fhir/r4"; +import { + fetchQueryResponse, + fetchUseCaseValueSets, +} from "./selectQuery/queryHooks"; +import LoadingView from "./LoadingView"; + +interface SelectQueryProps { + goForward: () => void; + goBack: () => void; + selectedQuery: USE_CASES; + setSelectedQuery: React.Dispatch>; + patientForQuery: Patient | undefined; + resultsQueryResponse: QueryResponse; + setResultsQueryResponse: React.Dispatch>; + fhirServer: FHIR_SERVERS; + setFhirServer: React.Dispatch>; + setLoading: (isLoading: boolean) => void; +} + +/** + * @param root0 - SelectQueryProps + * @param root0.goBack - Callback to return to previous page + * @param root0.goForward - Callback to go to the next page + * @param root0.selectedQuery - query we chose for further customization + * @param root0.setSelectedQuery - callback function to update the selected query + * @param root0.patientForQuery - patient to apply a particular query for + * @param root0.resultsQueryResponse - Response of selected query + * @param root0.setResultsQueryResponse - Callback function to update selected + * query + * @param root0.fhirServer - the FHIR server that we're running the query against + * @param root0.setFhirServer - callback function to update the FHIR server + * @returns - The selectQuery component. + */ +const SelectQuery: React.FC = ({ + selectedQuery, + patientForQuery, + resultsQueryResponse, + fhirServer, + goForward, + goBack, + setSelectedQuery, + setResultsQueryResponse, + setFhirServer, +}) => { + const [showCustomizeQuery, setShowCustomizedQuery] = useState(false); + const [queryValueSets, setQueryValueSets] = useState( + [] as ValueSetItem[], + ); + const [loadingQueryValueSets, setLoadingQueryValueSets] = + useState(false); + + const [loadingResultResponse, setLoadingResultResponse] = + useState(false); + + useEffect(() => { + // Gate whether we actually update state after fetching so we + // avoid name-change race conditions + let isSubscribed = true; + + fetchUseCaseValueSets( + selectedQuery, + setQueryValueSets, + isSubscribed, + setLoadingQueryValueSets, + ).catch(console.error); + + // Destructor hook to prevent future state updates + return () => { + isSubscribed = false; + }; + }, [selectedQuery, setQueryValueSets]); + + async function onSubmit() { + await fetchQueryResponse({ + patientForQuery: patientForQuery, + selectedQuery: selectedQuery, + queryValueSets: queryValueSets, + fhirServer: fhirServer, + queryResponseStateCallback: setResultsQueryResponse, + setIsLoading: setLoadingResultResponse, + }).catch(console.error); + goForward(); + } + + const displayLoading = loadingResultResponse || loadingQueryValueSets; + return ( + <> + {displayLoading && } + + {showCustomizeQuery ? ( + setShowCustomizedQuery(false)} + > + ) : ( + + )} + + ); +}; + +export default SelectQuery; +export const RETURN_TO_STEP_ONE_LABEL = "Return to Select patient"; diff --git a/query-connector/src/app/query/components/header/header.tsx b/query-connector/src/app/query/components/header/header.tsx index b36e3017e..5667dfab3 100644 --- a/query-connector/src/app/query/components/header/header.tsx +++ b/query-connector/src/app/query/components/header/header.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { Modal, ModalButton } from "../../designSystem/Modal"; import { ModalRef } from "@trussworks/react-uswds"; import styles from "./header.module.css"; +import { metadata } from "@/app/constants"; /** * Produces the header. * @returns The HeaderComponent component. @@ -33,9 +34,9 @@ export default function HeaderComponent() { - TEFCA Viewer + {metadata.title}
diff --git a/query-connector/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx b/query-connector/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx index 53fabf08b..59bdf38b7 100644 --- a/query-connector/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx +++ b/query-connector/src/app/query/components/patientSearchResults/PatientSearchResultsTable.tsx @@ -9,7 +9,7 @@ import { type PatientSeacrchResultsTableProps = { patients: Patient[]; - setPatientForQueryResponse: (patient: Patient) => void; + handlePatientSelect: (patient: Patient) => void; }; /** @@ -17,18 +17,18 @@ type PatientSeacrchResultsTableProps = { * include in their query * @param param0 - props * @param param0.patients - Patient[] from the FHIR spec to display as rows - * @param param0.setPatientForQueryResponse - state setter function to redirect + * @param param0.handlePatientSelect - state setter function to redirect * to the results view * @returns The patient search results view */ const PatientSearchResultsTable: React.FC = ({ patients, - setPatientForQueryResponse, + handlePatientSelect: setPatientForQueryResponse, }) => { return ( <>

- Select a patient + {STEP_TWO_PAGE_TITLE}

The following records match your search. Select a patient to continue. @@ -74,3 +74,4 @@ const PatientSearchResultsTable: React.FC = ({ }; export default PatientSearchResultsTable; +export const STEP_TWO_PAGE_TITLE = "Step 2: Select a patient"; diff --git a/query-connector/src/app/query/components/resultsView/ResultsViewTable.tsx b/query-connector/src/app/query/components/resultsView/ResultsViewTable.tsx index 5481ba517..b828b46c3 100644 --- a/query-connector/src/app/query/components/resultsView/ResultsViewTable.tsx +++ b/query-connector/src/app/query/components/resultsView/ResultsViewTable.tsx @@ -57,5 +57,5 @@ export default ResultsViewTable; * @returns - A hyphenated id that can be linked as an anchor tag */ export function formatIdForAnchorTag(title: string) { - return title.toLocaleLowerCase().replace(" ", "-"); + return title?.toLocaleLowerCase().replace(" ", "-"); } diff --git a/query-connector/src/app/query/components/searchForm/SearchForm.tsx b/query-connector/src/app/query/components/searchForm/SearchForm.tsx index aedac810e..cc12ab403 100644 --- a/query-connector/src/app/query/components/searchForm/SearchForm.tsx +++ b/query-connector/src/app/query/components/searchForm/SearchForm.tsx @@ -10,18 +10,10 @@ import { USE_CASES, FHIR_SERVERS, demoData, - PatientType, - demoQueryOptions, - patientOptions, stateOptions, Mode, - ValueSetItem, } from "../../../constants"; -import { - UseCaseQueryResponse, - UseCaseQuery, - UseCaseQueryRequest, -} from "../../../query-service"; +import { UseCaseQueryResponse, UseCaseQuery } from "../../../query-service"; import { fhirServers } from "../../../fhir-servers"; import styles from "./searchForm.module.css"; @@ -29,45 +21,41 @@ import { FormatPhoneAsDigits } from "@/app/format-service"; interface SearchFormProps { useCase: USE_CASES; - queryValueSets: ValueSetItem[]; setUseCase: (useCase: USE_CASES) => void; - setOriginalRequest: (originalRequest: UseCaseQueryRequest) => void; - setUseCaseQueryResponse: (UseCaseQueryResponse: UseCaseQueryResponse) => void; + setPatientDiscoveryQueryResponse: ( + UseCaseQueryResponse: UseCaseQueryResponse, + ) => void; setMode: (mode: Mode) => void; setLoading: (loading: boolean) => void; - setQueryType: (queryType: string) => void; + fhirServer: FHIR_SERVERS; + setFhirServer: React.Dispatch>; } /** * @param root0 - SearchFormProps * @param root0.useCase - The use case this query will cover. - * @param root0.queryValueSets - Stateful collection of valuesets to use in the query. * @param root0.setUseCase - Update stateful use case. - * @param root0.setOriginalRequest - The function to set the original request. - * @param root0.setUseCaseQueryResponse - The function to set the use case query response. * @param root0.setMode - The function to set the mode. * @param root0.setLoading - The function to set the loading state. - * @param root0.setQueryType - The function to set the query type. + * @param root0.setPatientDiscoveryQueryResponse - callback function to set the + * patient for use in future steps + * @param root0.fhirServer - server to do the query against + * @param root0.setFhirServer - callback function to update specified query * @returns - The SearchForm component. */ const SearchForm: React.FC = ({ useCase, - queryValueSets, setUseCase, - setOriginalRequest, - setUseCaseQueryResponse, + setPatientDiscoveryQueryResponse, setMode, setLoading, - setQueryType, + fhirServer, + setFhirServer, }) => { //Set the patient options based on the demoOption - const [patientOption, setPatientOption] = useState( - patientOptions[useCase]?.[0]?.value || "", - ); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); - const [fhirServer, setFhirServer] = useState(); - // "Public HAPI: Direct", + const [phone, setPhone] = useState(""); const [dob, setDOB] = useState(""); const [mrn, setMRN] = useState(""); @@ -75,10 +63,10 @@ const SearchForm: React.FC = ({ const [showAdvanced, setShowAdvanced] = useState(false); const [autofilled, setAutofilled] = useState(false); // boolean indicating if the form was autofilled, changes color if true - // Fills fields with sample data based on the selected patientOption + // Fills fields with sample data based on the selected const fillFields = useCallback( - (patientOption: PatientType, highlightAutofilled = true) => { - const data = demoData[patientOption]; + (highlightAutofilled = true) => { + const data = demoData["cancer"]; if (data) { setFirstName(data.FirstName); setLastName(data.LastName); @@ -90,23 +78,9 @@ const SearchForm: React.FC = ({ setAutofilled(highlightAutofilled); } }, - [patientOption, setUseCase, setQueryType], + [setUseCase], ); - // Change the selectedDemoOption in the dropdown and update the - // query type (which governs the DB fetch) accordingly - const handleDemoQueryChange = (selectedDemoOption: string) => { - setPatientOption(patientOptions[selectedDemoOption][0].value); - setQueryType( - demoQueryOptions.find((dqo) => dqo.value == selectedDemoOption)?.label || - "", - ); - }; - - const handleClick = () => { - setMode("customize-queries"); - }; - async function HandleSubmit(event: React.FormEvent) { event.preventDefault(); if (!useCase || !fhirServer) { @@ -124,15 +98,10 @@ const SearchForm: React.FC = ({ use_case: useCase, phone: FormatPhoneAsDigits(phone), }; - setOriginalRequest(originalRequest); - const queryResponse = await UseCaseQuery(originalRequest, queryValueSets); - setUseCaseQueryResponse(queryResponse); + const queryResponse = await UseCaseQuery(originalRequest, []); + setPatientDiscoveryQueryResponse(queryResponse); - if (queryResponse.Patient && queryResponse.Patient.length === 1) { - setMode("results"); - } else { - setMode("patient-results"); - } + setMode("patient-results"); setLoading(false); } useEffect(() => { @@ -143,7 +112,7 @@ const SearchForm: React.FC = ({ <>

- Search for a Patient + {STEP_ONE_PAGE_TITLE}

Enter patient information below to search for a patient. We will query @@ -156,76 +125,22 @@ const SearchForm: React.FC = ({ htmlFor="query" > The demo site uses synthetic data to provide examples of possible - queries that you can make with the TEFCA Viewer. Select a query + queries that you can make with the Query Connector. Select a query use case, a sample patient, and then click “fill fields” below. -
-
- -
- -
-
-
- -
- -
-
-
-

} -
+
{showAdvanced && ( -
-