Skip to content

Commit

Permalink
Merge pull request #95 from PDCMFinder/dev
Browse files Browse the repository at this point in the history
Asyncronous autocomplete for search and typeahead facets
  • Loading branch information
ficolo authored Jun 9, 2022
2 parents 911f939 + a15f98f commit 3b71a0a
Show file tree
Hide file tree
Showing 19 changed files with 154 additions and 174 deletions.
3 changes: 1 addition & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<link rel="icon" href="%PUBLIC_URL%/img/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="google-site-verification" content="JxxpC1XZfpJsYqc6JqN_EPmHDejrXCfaS3K2Dbo7v28" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand All @@ -16,7 +15,7 @@
"name": "PDCM Finder: An Open and Global Catalogue of Patient Tumor Derived Xenograft Models",
"description": "The open global research platform for Patient Derived Cancer Models. PDCM Finder is the largest open catalog of harmonised patient-derived cancer models and associated data from academic and commercial providers.
Find the perfect model for your next project. Explore and analyse the data. Connect with model providers.",
"keywords": "PDX, Patient derived xenografts, cancer models, cell lines, organoid, cancer, omic, drug response, oncology, preclinical",
"keywords": "PDX, Patient derived xenografts, cancer models, cell lines, organoid, cancer, omic, drug response, oncology, preclinical, Patient Derived cancer models",
"creator": {"@type": "Organization","name": "PDCM Finder", "contactPoint":{
"@type":"ContactPoint",
"contactType": "customer service",
Expand Down
96 changes: 51 additions & 45 deletions src/apis/Search.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,31 @@ export async function getFacetOptions(facetColumn: string) {
});
}

export async function autoCompleteFacetOptions(
facetColumn: string,
text: string
) {
const ilikeClause = text.length > 0 ? `, option.ilike."*${text}*"` : "";
let response = await fetch(
`${API_URL}/search_facet_options?and=(facet_column.eq.${facetColumn}${ilikeClause})&limit=20&order=option.asc`
);
return response.json().then((d: Array<any>) => {
return d.map(({ option }) => {
return option;
});
});
}

export async function getSearchResults(
searchValues: Array<IOptionProps> = [],
searchValues: Array<string> = [],
facetSelections: any,
facetOperators: any,
page: number,
pageSize: number = 10
): Promise<[number, Array<SearchResult>]> {
let query =
searchValues.length > 0
? `search_terms=ov.{${searchValues
.map((d: IOptionProps) => '"' + d.name + '"')
.join(",")}}`
? `search_terms=ov.{${searchValues.join(",")}}`
: "";

if (facetSelections) {
Expand All @@ -99,9 +112,7 @@ export async function getSearchResults(

for (let facetColumn in facet) {
const options = facetSelections[key][facetColumn]
? facetSelections[key][facetColumn].map(
(d: IOptionProps) => '"' + d.name + '"'
)
? facetSelections[key][facetColumn].map((d: string) => '"' + d + '"')
: [];
let apiOperator = "in";
const hasOperator =
Expand Down Expand Up @@ -183,54 +194,52 @@ function mapApiFacet(apiFacet: any): IFacetProps {
facetType = "multivalued";

return {
key: apiFacet.facet_column,
facetId: apiFacet.facet_column,
name: apiFacet.facet_name,
type: facetType,
options: apiFacet.facet_options
? apiFacet.facet_options
.sort((a: String, b: String) => {
if (apiFacet.facet_column !== "patient_age")
return a.toLocaleLowerCase().trim() > b.toLocaleLowerCase().trim()
? 1
: -1;
else {
if (a.includes("months")) return -1;
if (b.includes("specified")) return -1;
let aa = a.split(" - ");
let bb = b.split(" - ");
if (+aa[0] > +bb[0]) return 1;
else if (+aa[0] < +bb[0]) return -1;
else return 0;
}
})
.map((option: String) => {
return {
key: option.replace(/[\W_]+/g, "_").toLowerCase(),
name: option,
};
})
? apiFacet.facet_options.sort((a: String, b: String) => {
if (apiFacet.facet_column !== "patient_age")
return a.toLocaleLowerCase().trim() > b.toLocaleLowerCase().trim()
? 1
: -1;
else {
if (a.includes("months")) return -1;
if (b.includes("specified")) return -1;
let aa = a.split(" - ");
let bb = b.split(" - ");
if (+aa[0] > +bb[0]) return 1;
else if (+aa[0] < +bb[0]) return -1;
else return 0;
}
})
: [],
};
}

export function getSearchParams(
searchValues: Array<IOptionProps>,
searchValues: Array<string>,
facetSelection: any,
facetOperators: any
) {
console.log("getSearchParams", searchValues, facetSelection, facetOperators);
let search = "";
if (searchValues.length > 0) {
search += "?q=" + searchValues.map((o) => o.key).join(",");
search +=
"?q=" +
searchValues.map((o) => encodeURIComponent('"' + o + '"')).join(",");
}
let facetString = "";
console.log("facetSelection", facetSelection);

Object.keys(facetSelection).forEach((facetSectionKey) => {
Object.keys(facetSelection[facetSectionKey]).forEach((facetKey) => {
if (facetSelection[facetSectionKey][facetKey].length === 0) return;
facetString += `${
facetString === "" ? "" : " AND "
}${facetSectionKey}.${facetKey}:${facetSelection[facetSectionKey][
facetKey
].map((o: IOptionProps) => o.key)}`;
}${facetSectionKey}.${facetKey}:${
facetSelection[facetSectionKey][facetKey]
}`;
});
});
if (facetString !== "")
Expand Down Expand Up @@ -262,10 +271,13 @@ export function getSearchParams(
// the query string for you.
export function useQueryParams() {
const search = new URLSearchParams(useLocation().search);
let searchTermKeys: Array<string> = [];
let searchTermValues: Array<string> = [];
const queryParam = search.get("q");

if (queryParam !== null) {
searchTermKeys = queryParam.split(",");
searchTermValues = queryParam
.split('","')
.map((o) => o.replace(/["]+/g, ""));
}
let facetSelection: any = {};
const facets = search.get("facets")?.split(" AND ") || [];
Expand All @@ -280,26 +292,20 @@ export function useQueryParams() {
const [key, value] = facetString.split(":");
facetOperators[key] = value;
});
return [searchTermKeys, facetSelection, facetOperators];
return [searchTermValues, facetSelection, facetOperators];
}

export function parseSelectedFacetFromUrl(
facetSections: Array<IFacetSectionProps>,
facetsByKey: any
): IFacetSidebarSelection {
const facetSidebarSelection: IFacetSidebarSelection = {};
Object.keys(facetsByKey).forEach((compoundKey: string) => {
const [sectionKey, facetKey] = compoundKey.split(".");
const urlFacetSelection = facetsByKey[compoundKey];
const facetSection = facetSections?.find(({ key }) => sectionKey === key);
const facet = facetSection?.facets?.find(({ key }) => facetKey === key);
if (!facetSidebarSelection[sectionKey]) {
facetSidebarSelection[sectionKey] = {};
}
facetSidebarSelection[sectionKey][facetKey] =
facet?.options.filter((option) =>
urlFacetSelection.includes(option.key)
) || [];
facetSidebarSelection[sectionKey][facetKey] = urlFacetSelection || [];
});
return facetSidebarSelection;
}
Expand Down
1 change: 1 addition & 0 deletions src/components/explore/ExploreCirclePacking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const ExploreCirclePacking: FunctionComponent<{ data: any }> = ({
}}
labelsSkipRadius={10}
labelTextColor={{ from: "color", modifiers: [["brighter", 100]] }}
animate={false}
motionConfig="slow"
// onClick={(node) => {
// setZoomedId(zoomedId === node.id ? null : node.id);
Expand Down
5 changes: 1 addition & 4 deletions src/components/search/QueryViewer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ const Template: Story<IQueryViewerProps> = (args) => (

export const Default = Template.bind({});
Default.args = {
searchTerms: [
{ key: "diagnosis_1", name: "Diagnosits 1" },
{ key: "diagnosis_2", name: "Diagnosits 2" },
],
searchTerms: ["Diagnosits 1", "Diagnosits 2"],
facetSelection: {},
};
8 changes: 4 additions & 4 deletions src/components/search/QueryViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../../models/Facet.model";

export interface IQueryViewerProps {
searchTerms: Array<IOptionProps>;
searchTerms: Array<string>;
facetSelection: IFacetSidebarSelection;
facetOperators: IFacetSidebarOperators;
facetNames: { [sectionFacetKey: string]: string };
Expand Down Expand Up @@ -116,12 +116,12 @@ const QuerySection: FunctionComponent<{
<b>{operator}</b> ({" "}
{options.map((option) => (
<Token
key={option?.key}
key={option}
option={option}
readOnly={false}
onRemove={() => onRemove(option.key)}
onRemove={() => onRemove(option)}
>
{option?.name}
{option}
</Token>
))}
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/search/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const ResultsTable: FunctionComponent<IResultsTableProps> = ({
</div>
) : null}
{results.map((result) => (
<div key={result.pdcmId} className="my-3">
<div key={`${result.pdcmId}-${result.datasource}`} className="my-3">
<SearchResultCard {...result} />
</div>
))}
Expand Down
2 changes: 0 additions & 2 deletions src/components/search/SearchBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,13 @@ const Template: Story<ISearchBarProps> = (args) => (
export const Default = Template.bind({});
Default.args = {
searchValues: [],
searchOptions: options,
searchAllowMultipleTerms: false,
onSearchChange: (value) => console.log(value),
};

export const Multiple = Template.bind({});
Multiple.args = {
searchValues: [],
searchOptions: options,
searchAllowMultipleTerms: true,
onSearchChange: (value) => console.log(value),
};
30 changes: 17 additions & 13 deletions src/components/search/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
// @ts-nocheck
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import React, { FunctionComponent } from "react";
import React, { FunctionComponent, useState } from "react";
import { Form, Col, InputGroup } from "react-bootstrap";
import { Option, Typeahead } from "react-bootstrap-typeahead";
import { AsyncTypeahead, Option, Typeahead } from "react-bootstrap-typeahead";
import "./SearchBar.scss";
import "react-bootstrap-typeahead/css/Typeahead.css";
import { IOptionProps } from "../../models/Facet.model";
import { useQuery } from "react-query";
import { autoCompleteFacetOptions } from "../../apis/Search.api";

export interface ISearchBarProps {
searchValues?: Array<Option>;
searchOptions?: Array<Option>;
searchAllowMultipleTerms?: boolean;
isLoading: boolean;
onSearchChange?(newValue: Array<IOptionProps>): void;
onSearchChange?(newValue: Array<string>): void;
}

export const SearchBar: FunctionComponent<ISearchBarProps> = ({
searchValues = [],
searchOptions = [],
searchValues,
searchAllowMultipleTerms,
isLoading,
onSearchChange,
}) => {
const [query, setQuery] = useState("");
const optionsQuery = useQuery(query, () =>
autoCompleteFacetOptions("search_terms", query)
);
return (
<Form className="w-100">
<div className="align-items-center">
<Form.Group as={Col} xs={12}>
<InputGroup>
<Typeahead
<AsyncTypeahead
id="search-bar-type-ahead"
single={!searchAllowMultipleTerms}
multiple={searchAllowMultipleTerms}
onChange={(s) => {
onSearchChange(s);
}}
options={searchOptions}
options={optionsQuery.data}
placeholder="Search by cancer diagnosis (e.g. Melanoma)"
selected={searchValues || []}
selected={searchValues}
clearButton
style={{ minHeight: "50px" }}
className="w-100 search-bar-type-ahead"
labelKey="name"
isLoading={isLoading}
isLoading={optionsQuery.isLoading}
caseSensitive={false}
onSearch={(query) => {
setQuery(query);
}}
/>
<InputGroup.Text
className="bg-primary text-white text-center"
Expand Down
10 changes: 7 additions & 3 deletions src/components/search/SearchResultCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const SearchResultCard: React.FC<SearchResult> = ({
</h4>
</Link>
<div className="fw-light" style={{ fontSize: "larger" }}>
{histology}
{histology.replace("/", " / ")}
</div>
<div style={{ textTransform: "capitalize" }}>{modelType} model</div>
<div style={{ textTransform: "capitalize" }}>
Expand All @@ -122,11 +122,15 @@ export const SearchResultCard: React.FC<SearchResult> = ({
>
{modelInfoCategories.map((category) => {
return (
<div className="d-inline-flex align-items-center justify-content-start">
<div
key={category.name}
className="d-inline-flex align-items-center justify-content-start"
>
<FontAwesomeIcon icon={category.icon} className="h5" />
<div className="my-2 mx-2" style={{ lineHeight: "1.2rem" }}>
<div className="text-capitalize">
{modelInfo[category.key] || "Not Specified"}
{modelInfo[category.key]?.replace("/", " / ") ||
"Not Specified"}
</div>
<div className="small text-muted">{category.name}</div>
</div>
Expand Down
11 changes: 6 additions & 5 deletions src/components/search/facets/Facet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IFacetProps } from "../../../models/Facet.model";
import { TypeaheadFacet } from "./TypeaheadFacet";

export const Facet: FunctionComponent<IFacetProps> = ({
facetId,
name,
type,
options,
Expand All @@ -23,10 +24,10 @@ export const Facet: FunctionComponent<IFacetProps> = ({
return options.map((option) => {
return (
<Form.Check
key={option.key}
key={option}
type="checkbox"
label={option.name}
id={option.key}
label={option}
id={option}
checked={selection.includes(option)}
onChange={(e) => {
let newSelection = [...selection];
Expand All @@ -46,8 +47,8 @@ export const Facet: FunctionComponent<IFacetProps> = ({
case "multivalued":
return (
<TypeaheadFacet
facetId={facetId}
name={name}
options={options}
values={selection}
onSelectionChange={onSelectionChange}
operator={operator ? operator : "any"}
Expand All @@ -59,8 +60,8 @@ export const Facet: FunctionComponent<IFacetProps> = ({
case "autocomplete":
return (
<TypeaheadFacet
facetId={facetId}
name={name}
options={options}
values={selection}
onSelectionChange={onSelectionChange}
operator={operator}
Expand Down
Loading

0 comments on commit 3b71a0a

Please sign in to comment.