Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

query building part one #120

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
generateQueryToValueSetInsertionSql,
} from "./query-building";
import { UUID } from "crypto";
import {
CategoryToConditionArrayMap,
ConditionIdToNameMap,
} from "./queryBuilding/utils";

const getQuerybyNameSQL = `
select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.oid as valueset_external_id, vs.version, vs.author as author, vs.type, vs.dibbs_concept_type as dibbs_concept_type, qic.concept_id, qic.include, c.code, c.code_system, c.display
Expand Down Expand Up @@ -579,7 +583,7 @@ export async function getConditionsData() {
const rows = result.rows;

// 1. Grouped by category with id:name pairs
const conditionCatergories = rows.reduce(
const categoryToConditionArrayMap: CategoryToConditionArrayMap = rows.reduce(
(acc, row) => {
const { category, id, name } = row;
if (!acc[category]) {
Expand All @@ -588,17 +592,16 @@ export async function getConditionsData() {
acc[category].push({ [id]: name });
return acc;
},
{} as Record<string, Array<Record<string, string>>>,
{} as CategoryToConditionArrayMap,
);

// 2. ID-Name mapping
const conditionLookup = rows.reduce(
(acc, row) => {
acc[row.id] = row.name;
return acc;
},
{} as Record<string, string>,
);

return { conditionCatergories, conditionLookup };
const conditionIdToNameMap: ConditionIdToNameMap = rows.reduce((acc, row) => {
acc[row.id] = row.name;
return acc;
}, {} as ConditionIdToNameMap);
return {
categoryToConditionArrayMap,
conditionIdToNameMap,
} as const;
}
38 changes: 38 additions & 0 deletions query-connector/src/app/query/designSystem/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Checkbox as TrussCheckbox } from "@trussworks/react-uswds";
import classNames from "classnames";
import styles from "./checkbox.module.css";

type CheckboxProps = {
id: string;
label: string;
className?: string;
onClick?: () => void;
};

/**
*
* @param root0 Checkbox styled according to our design system
* @param root0.label The string labeled next to the checkbox
* @param root0.id HTML id used to reference the checkbox
* @param root0.className Optional styling classes
* @param root0.onClick Event listener for checkbox click
* @returns A checkbox styled according to our design system
*/
const Checkbox: React.FC<CheckboxProps> = ({
label,
id,
className,
onClick,
}) => {
return (
<TrussCheckbox
label={label}
id={id}
name={id}
className={classNames(styles.checkbox, className)}
onClick={onClick}
></TrussCheckbox>
);
};

export default Checkbox;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.checkbox > label::before {
box-shadow: 0 0 0 1px #919191;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TextInput } from "@trussworks/react-uswds";
import { ChangeEvent } from "react";
import styles from "./searchField.module.scss";
import classNames from "classnames";

type SearchFieldProps = {
id: string;
placeholder?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
className?: string;
};
/**
* A search component bar styled according to our design system
* @param root0 - params
* @param root0.id - HTML id
* @param root0.placeholder - String to label the search bar in the empty state.
* Defaults to "Search"
* @param root0.onChange - change event listener
* @param root0.className - optional styling classes
* @returns A search field component styled according to our design system
*/
const SearchField: React.FC<SearchFieldProps> = ({
id,
placeholder,
onChange,
className,
}) => {
return (
<TextInput
placeholder={placeholder ?? "Search"}
type="search"
id={id}
name={id}
onChange={onChange}
className={classNames(styles.searchField, className)}
></TextInput>
);
};

export default SearchField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use "../../../../styles/_variables" as *;

.searchField {
border-radius: 0.25rem;
border: 1px solid $gray-500;
height: 3%;
padding: 0.75rem;
padding-left: 2.4rem;
background: url("../../../../styles/assets/search.svg") center / contain
no-repeat;
background-position: 12px 12px;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { Button, Icon } from "@trussworks/react-uswds";
import styles from "../query.module.scss";
import styles from "../queryBuilding.module.scss";
import { useRouter } from "next/navigation";
import classNames from "classnames";

/**
* Empty-state component for query building
* @returns the EmptyQueriesDisplay to render the empty state status
*/
export const EmptyQueriesDisplay: React.FC = () => {
const router = useRouter();

return (
<>
<div className={styles.emptyStateQueryContainer}>
<div
className={classNames(
"bg-gray-5",
"display-flex",
"flex-align-center",
"flex-justify-center",
styles.emptyStateQueryContainer,
)}
>
<div className="display-flex flex-column flex-align-center">
<Icon.GridView
aria-label="Icon of four boxes in a grid to indicate empty query state"
Expand All @@ -18,7 +30,11 @@ export const EmptyQueriesDisplay: React.FC = () => {
No custom queries available
</h2>

<Button className={styles.createQueryButton} type={"button"}>
<Button
// onClick={() => router.push(`/queryBuilding/buildFromTemplates`)}
className={styles.createQueryButton}
type={"button"}
>
Create Query
</Button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion query-connector/src/app/queryBuilding/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import styles from "./query.module.scss";
import styles from "./queryBuilding.module.scss";
import EmptyQueriesDisplay from "./emptyState/EmptyQueriesDisplay";
/**
* Component for Query Building Flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
}

.emptyStateQueryContainer {
background-color: #edeff0;
height: 27.75rem;
display: flex;
justify-content: center;
align-items: center;
}

.emptyQueryTitle {
Expand Down
96 changes: 96 additions & 0 deletions query-connector/src/app/queryBuilding/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// The structure of the data that's coming from the backend
export type ConditionIdToNameMap = {
[conditionId: string]: string;
};
export type CategoryToConditionArrayMap = {
[categoryName: string]: ConditionIdToNameMap[];
};

// The transform structs for use on the frontend, which is a grandparent - parent
// - child mapping from category (indexed by name) - conditions (indexed by condition ID)
// and - condition option (name and whether to include it in the query we're building).
export type ConditionOption = {
name: string;
include: boolean;
};
export type ConditionOptionMap = {
[conditionId: string]: ConditionOption;
};
export type CategoryNameToConditionOptionMap = {
[categoryName: string]: ConditionOptionMap;
};

/**
* Translation function format backend response to something more manageable for the
* frontend
* @param fetchedData - data returned from the backend function grabbing condition <>
* category mapping
* @returns - The data in a CategoryNameToConditionOptionMap shape
*/
export function mapFetchedDataToFrontendStructure(fetchedData: {
[categoryName: string]: ConditionIdToNameMap[];
}) {
const result: CategoryNameToConditionOptionMap = {};
Object.entries(fetchedData).forEach(
([categoryName, conditionIdToNameMapArray]) => {
const curCategoryMap: ConditionOptionMap = {};
conditionIdToNameMapArray.forEach((e) => {
(curCategoryMap[Object.keys(e)[0]] = {
name: Object.values(e)[0],
include: false,
}),
(result[categoryName] = curCategoryMap);
});
},
);
return result;
}

/**
* Filtering function that checks filtering at the category and the condition level
* @param filterString - string to filter by
* @param fetchedConditions - unfiltered list of conditions fetched from the backend
* @returns - The subset of fetched conditions that contain the filter string
*/
export function filterSearchByCategoryAndCondition(
filterString: string,
fetchedConditions: CategoryNameToConditionOptionMap,
): CategoryNameToConditionOptionMap {
const result: CategoryNameToConditionOptionMap = {};

Object.entries(fetchedConditions).forEach(
([categoryName, conditionNameArray]) => {
if (
categoryName
.toLocaleLowerCase()
.includes(filterString.toLocaleLowerCase())
) {
result[categoryName] = fetchedConditions[categoryName];
}
Object.entries(conditionNameArray).forEach(
([conditionId, conditionNameAndInclude]) => {
if (
conditionNameAndInclude.name
.toLocaleLowerCase()
.includes(filterString.toLocaleLowerCase())
) {
result[categoryName] = result[categoryName] ?? {};
result[categoryName][conditionId] = conditionNameAndInclude;
}
},
);
},
);

return result;
}

/**
* Utility method that strips the (disorder) string that comes back from the
* APHL list on the query building page
* @param diseaseName - name of the disease
* @returns A disease display string for display
*/
export function formatDiseaseDisplay(diseaseName: string) {
return diseaseName.replace("(disorder)", "");
}
Loading
Loading