diff --git a/query-connector/e2e/alternate_queries.spec.ts b/query-connector/e2e/alternate_queries.spec.ts index 398bde4bb..e6d80eb8f 100644 --- a/query-connector/e2e/alternate_queries.spec.ts +++ b/query-connector/e2e/alternate_queries.spec.ts @@ -65,7 +65,6 @@ test.describe("alternate queries with the Query Connector", () => { await expect( page.getByRole("heading", { name: "Select a query" }), ).toBeVisible(); - // await page.getByTestId("Select").selectOption("social-determinants"); await page.getByTestId("Select").selectOption("cancer"); await page.getByRole("button", { name: "Submit" }).click(); await expect(page.getByText("Loading")).toHaveCount(0, { timeout: 10000 }); diff --git a/query-connector/src/app/api/query/route.ts b/query-connector/src/app/api/query/route.ts index 365be0a3b..87bf01701 100644 --- a/query-connector/src/app/api/query/route.ts +++ b/query-connector/src/app/api/query/route.ts @@ -11,8 +11,7 @@ import { USE_CASES, FHIR_SERVERS, FhirServers, - UseCases, - UseCaseToQueryName, + USE_CASE_DETAILS, } from "../../constants"; import { handleRequestError } from "./error-handling-service"; @@ -75,8 +74,10 @@ export async function POST(request: NextRequest) { const diagnostics_message = "Missing use_case or fhir_server."; const OperationOutcome = await handleRequestError(diagnostics_message); return NextResponse.json(OperationOutcome); - } else if (!Object.values(UseCases).includes(use_case as USE_CASES)) { - const diagnostics_message = `Invalid use_case. Please provide a valid use_case. Valid use_cases include ${UseCases}.`; + } else if (!Object.keys(USE_CASE_DETAILS).includes(use_case)) { + const diagnostics_message = `Invalid use_case. Please provide a valid use_case. Valid use_cases include ${Object.keys( + USE_CASE_DETAILS, + )}.`; const OperationOutcome = await handleRequestError(diagnostics_message); return NextResponse.json(OperationOutcome); } else if ( @@ -88,7 +89,7 @@ export async function POST(request: NextRequest) { } // Lookup default parameters for particular use-case search - const queryName = UseCaseToQueryName[use_case as USE_CASES]; + const queryName = USE_CASE_DETAILS[use_case as USE_CASES].queryName; const queryResults = await getSavedQueryByName(queryName); const valueSets = unnestValueSetsFromQuery(queryResults); diff --git a/query-connector/src/app/backend/dbClient.ts b/query-connector/src/app/backend/dbClient.ts new file mode 100644 index 000000000..6afb64069 --- /dev/null +++ b/query-connector/src/app/backend/dbClient.ts @@ -0,0 +1,25 @@ +import { PoolConfig, Pool } from "pg"; + +// Load environment variables from .env and establish a Pool configuration +const dbConfig: PoolConfig = { + connectionString: process.env.DATABASE_URL, + max: 10, // Maximum # of connections in the pool + idleTimeoutMillis: 30000, // A client must sit idle this long before being released + connectionTimeoutMillis: process.env.LOCAL_DB_CLIENT_TIMEOUT + ? Number(process.env.LOCAL_DB_CLIENT_TIMEOUT) + : 3000, // Wait this long before timing out when connecting new client +}; + +let cachedDbClient: Pool | null = null; + +/** + * Getter function to retrieve the DB client from a naive cache and create a new one + * if one doesn't exist + * @returns a cached version of the DB client + */ +export const getDbClient = () => { + if (!cachedDbClient) { + cachedDbClient = new Pool(dbConfig); + } + return cachedDbClient; +}; diff --git a/query-connector/src/app/backend/query-building.ts b/query-connector/src/app/backend/query-building.ts new file mode 100644 index 000000000..a78ec186e --- /dev/null +++ b/query-connector/src/app/backend/query-building.ts @@ -0,0 +1,30 @@ +"use server"; + +import { getDbClient } from "./dbClient"; +import { QueryDetailsResult } from "../queryBuilding/utils"; +const dbClient = getDbClient(); + +/** + * Getter function to grab saved query details from the DB + * @param queryId - Query ID to grab data from the db with + * @returns The query name, data, and conditions list from the query table + */ +export async function getSavedQueryDetails(queryId: string) { + const id = queryId; + const queryString = ` + select q.query_name, q.id, q.query_data, q.conditions_list + from query q + where q.id = $1; + `; + + try { + const result = await dbClient.query(queryString, [id]); + if (result.rows.length > 0) { + return result.rows as unknown as QueryDetailsResult[]; + } + console.error("No results found for query:", id); + return []; + } catch (error) { + console.error("Error retrieving query", error); + } +} diff --git a/query-connector/src/app/constants.ts b/query-connector/src/app/constants.ts index fe8025bb9..9d937b837 100644 --- a/query-connector/src/app/constants.ts +++ b/query-connector/src/app/constants.ts @@ -8,69 +8,31 @@ import { MedicationAdministration, MedicationRequest, } from "fhir/r4"; -/** - * The use cases that can be used in the app - */ -export const UseCases = [ - "social-determinants", - "newborn-screening", - "syphilis", - "gonorrhea", - "chlamydia", - "cancer", -] as const; -export type USE_CASES = (typeof UseCases)[number]; - -export const UseCaseToQueryName: { - [key in USE_CASES]: string; -} = { - "social-determinants": "Gather social determinants of health", - "newborn-screening": "Newborn screening follow-up", - syphilis: "Syphilis case investigation", - gonorrhea: "Gonorrhea case investigation", - chlamydia: "Chlamydia case investigation", - cancer: "Cancer case investigation", -}; -/** - * Labels and values for the query options dropdown on the query page - */ -export const demoQueryOptions = [ - { value: "cancer", label: "Cancer case investigation" }, - { value: "chlamydia", label: "Chlamydia case investigation" }, - { value: "gonorrhea", label: "Gonorrhea case investigation" }, - { value: "newborn-screening", label: "Newborn screening follow-up" }, - // Temporarily remove social determinants - // { - // value: "social-determinants", - // label: "Gather social determinants of health", - // }, - { 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; +export const USE_CASE_DETAILS = { + "newborn-screening": { + queryName: "Newborn screening follow-up", + condition: "Newborn Screening", }, - {} 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. - */ -export const demoQueryLabels = demoQueryOptions.map((dqo) => dqo.label); -export const QueryTypeToQueryName: { - [key in (typeof demoQueryLabels)[number]]: string; -} = { - "Gather social determinants of health": "Social Determinants of Health", - "Newborn screening follow-up": "Newborn Screening", - "Syphilis case investigation": "Congenital syphilis (disorder)", - "Gonorrhea case investigation": "Gonorrhea (disorder)", - "Chlamydia case investigation": "Chlamydia trachomatis infection (disorder)", - "Cancer case investigation": "Cancer (Leukemia)", -}; + syphilis: { + queryName: "Syphilis case investigation", + condition: "Congenital syphilis (disorder)", + }, + gonorrhea: { + queryName: "Gonorrhea case investigation", + condition: "Gonorrhea (disorder)", + }, + chlamydia: { + queryName: "Chlamydia case investigation", + condition: "Chlamydia trachomatis infection (disorder)", + }, + cancer: { + queryName: "Cancer case investigation", + condition: "Cancer (Leukemia)", + }, +} as const; + +export type USE_CASES = keyof typeof USE_CASE_DETAILS; /** * The FHIR servers that can be used in the app @@ -107,7 +69,6 @@ export type PatientType = | "newborn-screening-technical-fail" | "newborn-screening-referral" | "newborn-screening-pass" - | "social-determinants" | "sti-syphilis-positive"; export const DEFAULT_DEMO_FHIR_SERVER = "HELIOS Meld: Direct"; @@ -131,26 +92,7 @@ export const demoData: Record = { cancer: { ...hyperUnluckyPatient, UseCase: "cancer" }, "sti-chlamydia-positive": { ...hyperUnluckyPatient, UseCase: "chlamydia" }, "sti-gonorrhea-positive": { ...hyperUnluckyPatient, UseCase: "gonorrhea" }, - "social-determinants": { - ...hyperUnluckyPatient, - UseCase: "social-determinants", - }, "sti-syphilis-positive": { ...hyperUnluckyPatient, UseCase: "syphilis" }, - - // Newborn screening data remains unchanged - // We need to figure how to display specific cases for specific referral, fail, pass - // "newborn-screening-technical-fail": { - // ...hyperUnluckyPatient, - // UseCase: "newborn-screening", - // }, - // "newborn-screening-referral": { - // ...hyperUnluckyPatient, - // UseCase: "newborn-screening", - // }, - // "newborn-screening-pass": { - // ...hyperUnluckyPatient, - // UseCase: "newborn-screening", - // }, "newborn-screening-technical-fail": { FirstName: "Mango", LastName: "Smith", @@ -215,12 +157,6 @@ export const patientOptions: Record = { label: "A newborn with a passed screening", }, ], - "social-determinants": [ - { - value: "social-determinants", - label: "A patient with housing insecurity", - }, - ], syphilis: [ { value: "sti-syphilis-positive", @@ -300,7 +236,7 @@ export const stateOptions = [ export type Mode = "search" | "results" | "select-query" | "patient-results"; /* Mode that query building pages can be in; determines what is displayed to the user */ -export type BuildStep = "condition" | "valueset" | "concept"; +export type BuildStep = "selection" | "condition" | "valueset"; export const metadata = { title: "Query Connector", diff --git a/query-connector/src/app/database-service.ts b/query-connector/src/app/database-service.ts index 4ebbf2ce8..4d5bbeae8 100644 --- a/query-connector/src/app/database-service.ts +++ b/query-connector/src/app/database-service.ts @@ -1,5 +1,4 @@ "use server"; -import { Pool, PoolConfig } from "pg"; import { Bundle, OperationOutcome, @@ -45,6 +44,7 @@ import { ValuesetStruct, ValuesetToConceptStruct, } from "./seedSqlStructs"; +import { getDbClient } from "./backend/dbClient"; const getQuerybyNameSQL = ` select q.query_name, q.id, q.query_data, q.conditions_list @@ -61,14 +61,7 @@ SELECT c.display, c.code_system, c.code, vs.name as valueset_name, vs.id as valu WHERE ctvs.condition_id IN ( `; -// Load environment variables from .env and establish a Pool configuration -const dbConfig: PoolConfig = { - connectionString: process.env.DATABASE_URL, - max: 10, // Maximum # of connections in the pool - idleTimeoutMillis: 30000, // A client must sit idle this long before being released - connectionTimeoutMillis: 3000, // Wait this long before timing out when connecting new client -}; -const dbClient = new Pool(dbConfig); +const dbClient = getDbClient(); /** * Executes a search for a ValueSets and Concepts against the Postgres diff --git a/query-connector/src/app/query-service.ts b/query-connector/src/app/query-service.ts index 18b2b1dfb..6843f32b9 100644 --- a/query-connector/src/app/query-service.ts +++ b/query-connector/src/app/query-service.ts @@ -199,10 +199,8 @@ async function generalizedQuery( const builtQuery = new CustomQuery(querySpec, patientId); let response: fetch.Response | fetch.Response[]; - // Special cases for plain SDH or newborn screening, which just use one query - if (useCase === "social-determinants") { - response = await fhirClient.get(builtQuery.getQuery("social")); - } else if (useCase === "newborn-screening") { + // Special cases for newborn screening, which just use one query + if (useCase === "newborn-screening") { response = await fhirClient.get(builtQuery.getQuery("observation")); } else { const queryRequests: string[] = builtQuery.getAllQueries(); diff --git a/query-connector/src/app/query/components/CustomizeQuery.tsx b/query-connector/src/app/query/components/CustomizeQuery.tsx index d46fea4de..ddb1cfb00 100644 --- a/query-connector/src/app/query/components/CustomizeQuery.tsx +++ b/query-connector/src/app/query/components/CustomizeQuery.tsx @@ -6,8 +6,8 @@ import { DibbsConceptType, DibbsValueSetType, USE_CASES, + USE_CASE_DETAILS, ValueSet, - demoQueryValToLabelMap, } from "../../constants"; import { UseCaseQueryResponse } from "@/app/query-service"; import LoadingView from "./LoadingView"; @@ -199,7 +199,7 @@ const CustomizeQuery: React.FC = ({

Customize query

- Query: {demoQueryValToLabelMap[queryType]} + Query: {USE_CASE_DETAILS[queryType].condition}

{countLabs} labs found, {countMedications} medications found,{" "} diff --git a/query-connector/src/app/query/components/ResultsView.tsx b/query-connector/src/app/query/components/ResultsView.tsx index 095be31e3..eb2e927b3 100644 --- a/query-connector/src/app/query/components/ResultsView.tsx +++ b/query-connector/src/app/query/components/ResultsView.tsx @@ -12,7 +12,7 @@ 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"; +import { USE_CASES, USE_CASE_DETAILS } from "@/app/constants"; import { PAGE_TITLES, RETURN_LABEL, @@ -78,7 +78,7 @@ const ResultsView: React.FC = ({

Query: - {demoQueryValToLabelMap[selectedQuery]} + {USE_CASE_DETAILS[selectedQuery].condition}

diff --git a/query-connector/src/app/query/components/SelectQuery.tsx b/query-connector/src/app/query/components/SelectQuery.tsx index 0adfe46b6..b06f37a0d 100644 --- a/query-connector/src/app/query/components/SelectQuery.tsx +++ b/query-connector/src/app/query/components/SelectQuery.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import { FHIR_SERVERS, USE_CASES, - UseCaseToQueryName, + USE_CASE_DETAILS, ValueSet, } from "../../constants"; import CustomizeQuery from "./CustomizeQuery"; @@ -78,7 +78,8 @@ const SelectQuery: React.FC = ({ const fetchDataAndUpdateState = async () => { if (selectedQuery) { - const queryName = UseCaseToQueryName[selectedQuery as USE_CASES]; + const queryName = + USE_CASE_DETAILS[selectedQuery as USE_CASES].queryName; const valueSets = await fetchUseCaseValueSets(queryName); // Only update if the fetch hasn't altered state yet if (isSubscribed) { diff --git a/query-connector/src/app/query/components/header/header.tsx b/query-connector/src/app/query/components/header/header.tsx index 8886b051d..1b9ef8a03 100644 --- a/query-connector/src/app/query/components/header/header.tsx +++ b/query-connector/src/app/query/components/header/header.tsx @@ -139,8 +139,4 @@ export default function HeaderComponent() { ); } -const LOGGED_IN_PATHS = [ - "/query", - "/queryBuilding", - "/queryBuilding/buildFromTemplates", -]; +const LOGGED_IN_PATHS = ["/query", "/queryBuilding"]; diff --git a/query-connector/src/app/query/components/selectQuery/SelectSavedQuery.tsx b/query-connector/src/app/query/components/selectQuery/SelectSavedQuery.tsx index 9b2094087..7b0ebce7c 100644 --- a/query-connector/src/app/query/components/selectQuery/SelectSavedQuery.tsx +++ b/query-connector/src/app/query/components/selectQuery/SelectSavedQuery.tsx @@ -2,7 +2,7 @@ import { FHIR_SERVERS, FhirServers, USE_CASES, - demoQueryOptions, + USE_CASE_DETAILS, } from "@/app/constants"; import { Select, Button } from "@trussworks/react-uswds"; import Backlink from "../backLink/Backlink"; @@ -75,9 +75,9 @@ const SelectSavedQuery: React.FC = ({ - {demoQueryOptions.map((option) => ( - ))} diff --git a/query-connector/src/app/queryBuilding/buildFromTemplates/page.tsx b/query-connector/src/app/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx similarity index 71% rename from query-connector/src/app/queryBuilding/buildFromTemplates/page.tsx rename to query-connector/src/app/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx index e0a792220..9adfb6d8b 100644 --- a/query-connector/src/app/queryBuilding/buildFromTemplates/page.tsx +++ b/query-connector/src/app/queryBuilding/buildFromTemplates/BuildFromTemplates.tsx @@ -2,9 +2,8 @@ import Backlink from "@/app/query/components/backLink/Backlink"; import styles from "../buildFromTemplates/buildfromTemplate.module.scss"; -import { useRouter } from "next/navigation"; import { Label, TextInput, Button } from "@trussworks/react-uswds"; -import { useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { getConditionsData, @@ -13,6 +12,8 @@ import { import { CategoryNameToConditionOptionMap, ConditionIdToValueSetArray, + EMPTY_QUERY_SELECTION, + generateConditionNameToIdAndCategoryMap, groupConditionDataByCategoryName, } from "../utils"; import { ConditionSelection } from "../components/ConditionSelection"; @@ -22,26 +23,48 @@ import { BuildStep } from "../../constants"; import LoadingView from "../../query/components/LoadingView"; import classNames from "classnames"; import { groupConditionConceptsIntoValueSets } from "@/app/utils"; -import { batchToggleConcepts } from "../utils"; +import { SelectedQueryDetails } from "../querySelection/utils"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import { getSavedQueryDetails } from "@/app/backend/query-building"; export type FormError = { queryName: boolean; selectedConditions: boolean; }; +type BuildFromTemplatesProps = { + buildStep: BuildStep; + setBuildStep: Dispatch>; + selectedQuery: SelectedQueryDetails; + setSelectedQuery: Dispatch>; +}; + /** * The query building page + * @param root0 params + * @param root0.selectedQuery - the query to edit or the "create" mode if it + * doesn't previously + * @param root0.buildStep - the stage in the build process, used to render + * subsequent steps + * @param root0.setBuildStep - setter function to move the app forward + * @param root0.setSelectedQuery - setter function to update / reset the query + * being built * @returns the component for the query building page */ -export default function QueryTemplateSelection() { - const router = useRouter(); +const BuildFromTemplates: React.FC = ({ + selectedQuery, + buildStep, + setBuildStep, + setSelectedQuery, +}) => { const focusRef = useRef(null); - const [buildStep, setBuildStep] = useState("condition"); const [loading, setLoading] = useState(false); - const [queryName, setQueryName] = useState(""); + const [queryName, setQueryName] = useState( + selectedQuery.queryName, + ); + const [fetchedConditions, setFetchedConditions] = useState(); const [selectedConditions, setSelectedConditions] = @@ -53,6 +76,12 @@ export default function QueryTemplateSelection() { const [conditionValueSets, setConditionValueSets] = useState(); + function goBack() { + setQueryName(null); + setSelectedQuery(EMPTY_QUERY_SELECTION); + setBuildStep("selection"); + } + const checkForAddedConditions = (selectedIds: string[]) => { const alreadyRetrieved = conditionValueSets && Object.keys(conditionValueSets); @@ -88,13 +117,6 @@ export default function QueryTemplateSelection() { const formattedResults = results && groupConditionConceptsIntoValueSets(results); - // when fetching directly from conditions table (as opposed to a saved query), - // default to including all value sets - formattedResults.forEach((result) => { - batchToggleConcepts(result); - return (result.includeValueSet = true); - }); - // group by Condition ID: return Object.values(formattedResults).reduce((acc, resultObj) => { if (resultObj.conditionId) { @@ -154,6 +176,49 @@ export default function QueryTemplateSelection() { }; }, [selectedConditions, queryName]); + useEffect(() => { + let isSubscribed = true; + + async function setDefaultSelectedConditions() { + if (selectedQuery.queryId && fetchedConditions) { + const result = await getSavedQueryDetails(selectedQuery.queryId); + const conditionNameToIdMap = + generateConditionNameToIdAndCategoryMap(fetchedConditions); + const queryConditions = result?.map((r) => r.conditions_list).flat(); + + const updatedConditions: CategoryNameToConditionOptionMap = {}; + queryConditions && + queryConditions.forEach((conditionName) => { + const { category, conditionId } = + conditionNameToIdMap[conditionName]; + + updatedConditions[category] = { + ...updatedConditions[category], + [conditionId]: { + name: conditionName, + include: true, + }, + }; + }); + + if (isSubscribed) { + setSelectedConditions((prevState) => { + // Avoid unnecessary updates if the state is already the same + const newState = { ...prevState, ...updatedConditions }; + return JSON.stringify(prevState) !== JSON.stringify(newState) + ? newState + : prevState; + }); + } + } + } + setDefaultSelectedConditions().catch(console.error); + + return () => { + isSubscribed = false; + }; + }, [fetchedConditions]); + // ensures the fetchedConditions' checkbox statuses match // the data in selectedCondtiions function updateFetchedConditionIncludeStatus( @@ -219,7 +284,7 @@ export default function QueryTemplateSelection() { setBuildStep("condition"); updateFetchedConditionIncludeStatus(selectedConditions ?? {}); } else { - router.push("/queryBuilding"); + goBack(); } }} // TODO: tidy this too @@ -246,6 +311,7 @@ export default function QueryTemplateSelection() { name="queryNameInput" type="text" className="maxw-mobile" + defaultValue={queryName ?? ""} required onChange={(event) => { setQueryName(event.target.value); @@ -282,25 +348,27 @@ export default function QueryTemplateSelection() {
{/* Step One: Select Conditions */} - {buildStep == "condition" && fetchedConditions && ( - - )} + {buildStep == "condition" && + fetchedConditions && + queryName !== null && ( + + )} {/* Step Two: Select ValueSets */} - {buildStep == "valueset" && ( + {buildStep == "valueset" && queryName !== null && ( ); -} +}; + +export default BuildFromTemplates; diff --git a/query-connector/src/app/queryBuilding/buildFromTemplates/ConditionColumnDisplay.tsx b/query-connector/src/app/queryBuilding/buildFromTemplates/ConditionColumnDisplay.tsx index ca798b6cd..b4f20dc0b 100644 --- a/query-connector/src/app/queryBuilding/buildFromTemplates/ConditionColumnDisplay.tsx +++ b/query-connector/src/app/queryBuilding/buildFromTemplates/ConditionColumnDisplay.tsx @@ -6,7 +6,7 @@ import { import styles from "./buildfromTemplate.module.scss"; import ConditionOption from "./ConditionOption"; import classNames from "classnames"; -import { FormError } from "./page"; +import { FormError } from "./BuildFromTemplates"; type ConditionColumnDisplayProps = { fetchedConditions: CategoryNameToConditionOptionMap; diff --git a/query-connector/src/app/queryBuilding/buildFromTemplates/buildfromTemplate.module.scss b/query-connector/src/app/queryBuilding/buildFromTemplates/buildfromTemplate.module.scss index 716d34ee9..a4a782a30 100644 --- a/query-connector/src/app/queryBuilding/buildFromTemplates/buildfromTemplate.module.scss +++ b/query-connector/src/app/queryBuilding/buildFromTemplates/buildfromTemplate.module.scss @@ -15,6 +15,7 @@ padding: 3.5rem 5rem; border-radius: 4px; min-height: 30rem; + min-width: 100%; } .conditionSelectionForm { diff --git a/query-connector/src/app/queryBuilding/components/ConditionSelection.tsx b/query-connector/src/app/queryBuilding/components/ConditionSelection.tsx index eeac6124d..86fc36204 100644 --- a/query-connector/src/app/queryBuilding/components/ConditionSelection.tsx +++ b/query-connector/src/app/queryBuilding/components/ConditionSelection.tsx @@ -12,7 +12,7 @@ import { import ConditionColumnDisplay from "../buildFromTemplates/ConditionColumnDisplay"; import SearchField from "@/app/query/designSystem/searchField/SearchField"; import { BuildStep } from "@/app/constants"; -import { FormError } from "../buildFromTemplates/page"; +import { FormError } from "../buildFromTemplates/BuildFromTemplates"; type ConditionSelectionProps = { fetchedConditions: CategoryNameToConditionOptionMap; diff --git a/query-connector/src/app/queryBuilding/components/SelectionTable.tsx b/query-connector/src/app/queryBuilding/components/SelectionTable.tsx index 7e0cdc7b3..841bc1e77 100644 --- a/query-connector/src/app/queryBuilding/components/SelectionTable.tsx +++ b/query-connector/src/app/queryBuilding/components/SelectionTable.tsx @@ -4,7 +4,7 @@ import styles from "../buildFromTemplates/buildfromTemplate.module.scss"; import { ValueSetsByGroup, batchToggleConcepts, - tallyConcpetsForValueSetGroup, + tallyConceptsForValueSetGroup, ConditionToValueSetMap, } from "../utils"; import { DibbsValueSetType } from "@/app/constants"; @@ -112,11 +112,11 @@ export const SelectionTable: React.FC = ({ const valueSetsForType = Object.values( groupedValueSetsForCondition[valueSetType], ); - const totalCount = tallyConcpetsForValueSetGroup( + const totalCount = tallyConceptsForValueSetGroup( valueSetsForType, false, ); - const selectedCount = tallyConcpetsForValueSetGroup( + const selectedCount = tallyConceptsForValueSetGroup( valueSetsForType, true, ); diff --git a/query-connector/src/app/queryBuilding/page.tsx b/query-connector/src/app/queryBuilding/page.tsx index 8dcc78f57..e6ac10ea9 100644 --- a/query-connector/src/app/queryBuilding/page.tsx +++ b/query-connector/src/app/queryBuilding/page.tsx @@ -1,60 +1,38 @@ "use client"; -import { useContext, useEffect, useState } from "react"; -import UserQueriesDisplay from "./dataState/UserQueriesDisplay"; -import EmptyQueriesDisplay from "./emptyState/EmptyQueriesDisplay"; -import { CustomUserQuery } from "@/app/query-building"; -import { getCustomQueries } from "@/app/database-service"; -import { DataContext } from "@/app/utils"; -import styles from "@/app/queryBuilding/queryBuilding.module.scss"; -import { ToastContainer } from "react-toastify"; -import LoadingView from "@/app/query/components/LoadingView"; +import { SelectedQueryDetails } from "./querySelection/utils"; +import BuildFromTemplates from "./buildFromTemplates/BuildFromTemplates"; +import QuerySelection from "./querySelection/QuerySelection"; +import { BuildStep } from "../constants"; import "react-toastify/dist/ReactToastify.css"; +import { useState } from "react"; +import { EMPTY_QUERY_SELECTION } from "./utils"; /** * Component for Query Building Flow * @returns The Query Building component flow */ const QueryBuilding: React.FC = () => { - const context = useContext(DataContext); - const [loading, setLoading] = useState(true); - - // Check whether custom queries exist in DB - useEffect(() => { - if (context?.data === null) { - const fetchQueries = async () => { - try { - const queries = await getCustomQueries(); - context.setData(queries); - } catch (error) { - console.error("Failed to fetch queries:", error); - } finally { - setLoading(false); - } - }; - fetchQueries(); - } else { - setLoading(false); // Data already exists, no need to fetch again - } - }, [context]); - - if (loading) { - return ; - } - - const queries = (context?.data || []) as CustomUserQuery[]; + const [selectedQuery, setSelectedQuery] = useState( + EMPTY_QUERY_SELECTION, + ); + const [buildStep, setBuildStep] = useState("selection"); return ( <> - {queries.length === 0 ? ( -
-

My queries

- -
- ) : ( -
- - -
+ {buildStep === "selection" && ( + + )} + {buildStep !== "selection" && ( + )} ); diff --git a/query-connector/src/app/queryBuilding/emptyState/EmptyQueriesDisplay.tsx b/query-connector/src/app/queryBuilding/querySelection/EmptyQueriesDisplay.tsx similarity index 84% rename from query-connector/src/app/queryBuilding/emptyState/EmptyQueriesDisplay.tsx rename to query-connector/src/app/queryBuilding/querySelection/EmptyQueriesDisplay.tsx index 8005916cb..b027c053e 100644 --- a/query-connector/src/app/queryBuilding/emptyState/EmptyQueriesDisplay.tsx +++ b/query-connector/src/app/queryBuilding/querySelection/EmptyQueriesDisplay.tsx @@ -1,16 +1,14 @@ import { Button, Icon } from "@trussworks/react-uswds"; import { useState } from "react"; -import styles from "../queryBuilding.module.scss"; -import { useRouter } from "next/navigation"; +import styles from "./querySelection.module.scss"; import classNames from "classnames"; -import WorkSpaceSetUpView from "../loadingState/WorkspaceSetUp"; +import WorkSpaceSetUpView from "./WorkspaceSetUp"; import { createDibbsDB } from "@/db-creation"; /** * Empty-state component for query building * @returns the EmptyQueriesDisplay to render the empty state status */ export const EmptyQueriesDisplay: React.FC = () => { - const router = useRouter(); const [loading, setLoading] = useState(false); const handleClick = async () => { @@ -24,8 +22,8 @@ export const EmptyQueriesDisplay: React.FC = () => { // Stop loading and redirect once function is complete setLoading(false); - // Redirect to query building page - router.push("/queryBuilding/buildFromTemplates"); + // Refresh query building page to display the now seeded values + location.reload(); }; if (loading) { diff --git a/query-connector/src/app/queryBuilding/querySelection/QuerySelection.tsx b/query-connector/src/app/queryBuilding/querySelection/QuerySelection.tsx new file mode 100644 index 000000000..914ee4b25 --- /dev/null +++ b/query-connector/src/app/queryBuilding/querySelection/QuerySelection.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { getCustomQueries } from "@/app/database-service"; +import { CustomUserQuery } from "@/app/query-building"; +import LoadingView from "@/app/query/components/LoadingView"; +import { DataContext } from "@/app/utils"; +import { + useContext, + useState, + useEffect, + SetStateAction, + Dispatch, +} from "react"; +import { ToastContainer } from "react-toastify"; +import EmptyQueriesDisplay from "./EmptyQueriesDisplay"; +import UserQueriesDisplay from "./UserQueriesDisplay"; +import { SelectedQueryDetails, SelectedQueryState } from "./utils"; +import styles from "./querySelection.module.scss"; +import { BuildStep } from "@/app/constants"; + +type QuerySelectionProps = { + selectedQuery: SelectedQueryState; + setBuildStep: Dispatch>; + setSelectedQuery: Dispatch>; +}; + +/** + * Component for Query Building Flow + * @param root0 - params + * @param root0.selectedQuery - the query object we're building + * @param root0.setBuildStep - setter function to progress the stage of the query + * building flow + * @param root0.setSelectedQuery - setter function to update the query for editing + * @returns The Query Building component flow + */ +const QuerySelection: React.FC = ({ + selectedQuery, + setBuildStep, + setSelectedQuery, +}) => { + const context = useContext(DataContext); + const [loading, setLoading] = useState(true); + + // Check whether custom queries exist in DB + useEffect(() => { + if (context?.data === null) { + const fetchQueries = async () => { + try { + const queries = await getCustomQueries(); + context.setData(queries); + } catch (error) { + console.error("Failed to fetch queries:", error); + } finally { + setLoading(false); + } + }; + fetchQueries(); + } else { + setLoading(false); // Data already exists, no need to fetch again + } + }, [context]); + + if (loading) { + return ; + } + + const queries = (context?.data || []) as CustomUserQuery[]; + + return ( + <> + {queries.length === 0 ? ( +
+

My queries

+ +
+ ) : ( +
+ + +
+ )} + + ); +}; + +export default QuerySelection; diff --git a/query-connector/src/app/queryBuilding/dataState/UserQueriesDisplay.tsx b/query-connector/src/app/queryBuilding/querySelection/UserQueriesDisplay.tsx similarity index 71% rename from query-connector/src/app/queryBuilding/dataState/UserQueriesDisplay.tsx rename to query-connector/src/app/queryBuilding/querySelection/UserQueriesDisplay.tsx index f44e06079..7d4dfaac0 100644 --- a/query-connector/src/app/queryBuilding/dataState/UserQueriesDisplay.tsx +++ b/query-connector/src/app/queryBuilding/querySelection/UserQueriesDisplay.tsx @@ -1,43 +1,66 @@ -import React, { useState, useContext, useRef } from "react"; +import React, { + useState, + useContext, + useRef, + Dispatch, + SetStateAction, +} from "react"; import { Button, Icon, Table } from "@trussworks/react-uswds"; import { ModalRef } from "@/app/query/designSystem/modal/Modal"; -import { useRouter } from "next/navigation"; -import styles from "@/app/queryBuilding/queryBuilding.module.scss"; +import styles from "./querySelection.module.scss"; import { CustomUserQuery } from "@/app/query-building"; import { DataContext } from "@/app/utils"; + +import { BuildStep } from "@/app/constants"; import { + SelectedQueryState, + renderModal, handleDelete, + handleCreationConfirmation, confirmDelete, handleCopy, - handleClick, - renderModal, -} from "@/app/queryBuilding/dataState/utils"; + SelectedQueryDetails, +} from "./utils"; +import LoadingView from "@/app/query/components/LoadingView"; interface UserQueriesDisplayProps { queries: CustomUserQuery[]; + selectedQuery: SelectedQueryState; + setSelectedQuery: Dispatch>; + setBuildStep: Dispatch>; } /** * Component for query building when user-generated queries already exist * @param root0 - The props object. * @param root0.queries - Array of user-generated queries to display. + * @param root0.selectedQuery - the query object we're building + * @param root0.setBuildStep - setter function to progress the stage of the query + * building flow + * @param root0.setSelectedQuery - setter function to update the query for editing * @returns the UserQueriesDisplay to render the queries with edit/delete options */ export const UserQueriesDisplay: React.FC = ({ queries: initialQueries, + selectedQuery, + setSelectedQuery, + setBuildStep, }) => { - const router = useRouter(); const context = useContext(DataContext); const [queries, setQueries] = useState(initialQueries); - const [_, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const modalRef = useRef(null); - const [selectedQuery, setSelectedQuery] = useState<{ - queryName: string; - queryId: string; - } | null>(null); + const handleEdit = (queryName: string, queryId: string) => { + setSelectedQuery({ + queryName: queryName, + queryId: queryId, + }); + setBuildStep("condition"); + }; return (
+ {} {context && renderModal( modalRef, @@ -48,10 +71,15 @@ export const UserQueriesDisplay: React.FC = ({ context, )}
-

My queries

+

My queries