From 3f23a542823ecc8aa13732a065e7a70cdb5a04cc Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:12:17 -0500 Subject: [PATCH 1/3] Create FilterFieldOption to store search field as displayValue and value --- .../query/filteredSearch/Phrase.tsx | 5 +- .../field/CheckboxFilterField.spec.ts | 12 +++- .../field/CheckboxFilterField.tsx | 59 ++++++++++--------- .../filteredSearch/field/FilterFieldOption.ts | 12 ++++ .../filteredSearch/field/ListFormField.tsx | 8 +-- src/shared/lib/query/QueryParser.ts | 5 +- 6 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 src/shared/components/query/filteredSearch/field/FilterFieldOption.ts diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index b9664643d4d..7beeef04b76 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -165,7 +165,8 @@ function matchPhrase(phrase: string, fullText: string) { /** * Full match using lowercase + * Need to convert boolean to string before applying lowercase */ -function matchPhraseFull(phrase: string, fullText: string) { - return fullText.toLowerCase() === phrase.toLowerCase(); +function matchPhraseFull(phrase: string, toMatch: boolean | string | number) { + return _.toString(toMatch).toLowerCase() === phrase.toLowerCase(); } diff --git a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts index 08267d28f94..f7c98bfa681 100644 --- a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts +++ b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.spec.ts @@ -4,6 +4,10 @@ import { } from 'shared/components/query/filteredSearch/field/CheckboxFilterField'; import { CancerTreeSearchFilter } from 'shared/lib/query/textQueryUtils'; import { ListPhrase } from 'shared/components/query/filteredSearch/Phrase'; +import { + toFilterFieldOption, + toFilterFieldValue, +} from 'shared/components/query/filteredSearch/field/FilterFieldOption'; describe('CheckboxFilterField', () => { describe('createQueryUpdate', () => { @@ -12,7 +16,7 @@ describe('CheckboxFilterField', () => { nodeFields: ['studyId'], form: { input: FilterCheckbox, - options: ['a', 'b', 'c', 'd', 'e'], + options: ['a', 'b', 'c', 'd', 'e'].map(toFilterFieldOption), label: 'Test label', }, } as CancerTreeSearchFilter; @@ -48,7 +52,11 @@ describe('CheckboxFilterField', () => { it('removes all update when only And', () => { const checked = dummyFilter.form.options; const toRemove: ListPhrase[] = []; - const result = createQueryUpdate(toRemove, checked, dummyFilter); + const result = createQueryUpdate( + toRemove, + checked.map(toFilterFieldValue), + dummyFilter + ); expect(result.toAdd?.length).toEqual(0); }); diff --git a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx index 4c9bf651650..46c1c859510 100644 --- a/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx +++ b/src/shared/components/query/filteredSearch/field/CheckboxFilterField.tsx @@ -13,11 +13,15 @@ import { } from 'shared/lib/query/textQueryUtils'; import { FieldProps } from 'shared/components/query/filteredSearch/field/FilterFormField'; import { ListPhrase } from 'shared/components/query/filteredSearch/Phrase'; +import { + FilterFieldOption, + toFilterFieldValue, +} from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export type CheckboxFilterField = { input: typeof FilterCheckbox; label: string; - options: string[]; + options: FilterFieldOption[]; }; export const FilterCheckbox: FunctionComponent = props => { @@ -40,18 +44,18 @@ export const FilterCheckbox: FunctionComponent = props => { } }); for (const option of options) { - const isChecked = isOptionChecked(option, relevantClauses); + const isChecked = isOptionChecked(option.value, relevantClauses); if (isChecked) { - checkedOptions.push(option); + checkedOptions.push(option.value); } } return (
{props.filter.form.label}
- {options.map((option: string) => { - const id = `input-${option}`; - let isChecked = checkedOptions.includes(option); + {options.map((option: FilterFieldOption) => { + const id = `input-${option.displayValue}-${option.value}`; + let isChecked = checkedOptions.includes(option.value); return (
= props => { padding: '0 1em 0 0', }} > - { - isChecked = !isChecked; - updatePhrases(option, isChecked); - const update = createQueryUpdate( - toRemove, - checkedOptions, - props.filter - ); - props.onChange(update); - }} - style={{ - display: 'inline-block', - }} - />
); @@ -152,7 +156,8 @@ export function createQueryUpdate( toAdd = []; } else if (onlyNot || moreAnd) { const phrase = options - .filter(o => !optionsToAdd.includes(o)) + .filter(o => !optionsToAdd.includes(o.value)) + .map(toFilterFieldValue) .join(FILTER_VALUE_SEPARATOR); toAdd = [new NotSearchClause(createListPhrase(prefix, phrase, fields))]; diff --git a/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts b/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts new file mode 100644 index 00000000000..facc45c616b --- /dev/null +++ b/src/shared/components/query/filteredSearch/field/FilterFieldOption.ts @@ -0,0 +1,12 @@ +export type FilterFieldOption = { + value: string; + displayValue: string; +}; + +export function toFilterFieldOption(option: string) { + return { value: option, displayValue: option }; +} + +export function toFilterFieldValue(option: FilterFieldOption) { + return option.value; +} diff --git a/src/shared/components/query/filteredSearch/field/ListFormField.tsx b/src/shared/components/query/filteredSearch/field/ListFormField.tsx index e3bba5be7f1..8bfcde3e238 100644 --- a/src/shared/components/query/filteredSearch/field/ListFormField.tsx +++ b/src/shared/components/query/filteredSearch/field/ListFormField.tsx @@ -5,22 +5,22 @@ import { SearchClause } from 'shared/components/query/filteredSearch/SearchClaus import { Phrase } from 'shared/components/query/filteredSearch/Phrase'; import './ListFormField.scss'; import { toQueryString } from 'shared/lib/query/textQueryUtils'; +import { FilterFieldOption } from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export type ListFilterField = { label: string; input: typeof FilterList; - options: string[]; + options: FilterFieldOption[]; }; export const FilterList: FunctionComponent = props => { const form = props.filter.form as ListFilterField; const allPhrases = toUniquePhrases(props.query); - const queryString = toQueryString(props.query); return (
{props.filter.form.label}
{form.options.map(option => { - const update = props.parser.parseSearchQuery(option); + const update = props.parser.parseSearchQuery(option.value); return (
  • = props => { }); }} > - {option} + {option.displayValue}
  • ); diff --git a/src/shared/lib/query/QueryParser.ts b/src/shared/lib/query/QueryParser.ts index 965ab857591..5699da66048 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -18,6 +18,7 @@ import { ListPhrase, Phrase, } from 'shared/components/query/filteredSearch/Phrase'; +import { toFilterFieldOption } from 'shared/components/query/filteredSearch/field/FilterFieldOption'; export class QueryParser { /** @@ -38,7 +39,7 @@ export class QueryParser { input: FilterList, options: ServerConfigHelpers.skin_example_study_queries( getServerConfig()!.skin_example_study_queries || '' - ), + ).map(toFilterFieldOption), }, }, /** @@ -49,7 +50,7 @@ export class QueryParser { nodeFields: ['referenceGenome'], form: { input: FilterCheckbox, - options: [...referenceGenomes], + options: [...referenceGenomes].map(toFilterFieldOption), label: 'Reference genome', }, }, From 284baba665a2b422f286511f6e534fd6b304e29d Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Thu, 4 Jan 2024 12:59:30 -0500 Subject: [PATCH 2/3] Added study authorization as an option in search filter --- src/shared/components/query/QueryStore.ts | 12 ++++++++++- src/shared/lib/query/QueryParser.spec.ts | 2 +- src/shared/lib/query/QueryParser.ts | 23 ++++++++++++++++++++- src/shared/lib/query/textQueryUtils.spec.ts | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index a014fbc7c46..cf72e89d33a 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -151,7 +151,7 @@ export class QueryStore { @computed get queryParser() { - return new QueryParser(this.referenceGenomes); + return new QueryParser(this.referenceGenomes, this.readPermissions); } initialize(urlWithInitialParams?: string) { @@ -1511,6 +1511,16 @@ export class QueryStore { return new Set(referenceGenomes); } + @computed get readPermissions(): Set { + const studies = Array.from(this.treeData.map_node_meta.keys()).filter( + s => typeof (s as CancerStudy).readPermission !== 'undefined' + ); + const readPermissions = studies.map(n => + (n as CancerStudy).readPermission.toString() + ); + return new Set(readPermissions); + } + @computed get selectableSelectedStudies() { return this.selectableSelectedStudyIds .map( diff --git a/src/shared/lib/query/QueryParser.spec.ts b/src/shared/lib/query/QueryParser.spec.ts index 4c655a9d60e..2e84ec80562 100644 --- a/src/shared/lib/query/QueryParser.spec.ts +++ b/src/shared/lib/query/QueryParser.spec.ts @@ -11,7 +11,7 @@ import { QueryParser } from 'shared/lib/query/QueryParser'; import { StringPhrase } from 'shared/components/query/filteredSearch/Phrase'; describe('QueryParser', () => { - const parser = new QueryParser(new Set()); + const parser = new QueryParser(new Set(), new Set()); const referenceGenomeFields = parser.searchFilters.find( f => f.phrasePrefix === 'reference-genome' )!.nodeFields; diff --git a/src/shared/lib/query/QueryParser.ts b/src/shared/lib/query/QueryParser.ts index 5699da66048..ee77942d3c7 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -26,7 +26,7 @@ export class QueryParser { */ private readonly _searchFilters: CancerTreeSearchFilter[]; - constructor(referenceGenomes: Set) { + constructor(referenceGenomes: Set, readPermissions: Set) { this._searchFilters = [ /** * Example queries: @@ -54,6 +54,27 @@ export class QueryParser { label: 'Reference genome', }, }, + /** + * Show Authorized Studies + */ + { + phrasePrefix: 'authorized', + nodeFields: ['readPermission'], + form: { + input: FilterCheckbox, + options: + readPermissions.size >= 1 + ? [ + { value: 'true', displayValue: 'Authorized' }, + { + value: 'false', + displayValue: 'Unauthorized', + }, + ] + : [], + label: 'Controlled access', + }, + }, ]; } diff --git a/src/shared/lib/query/textQueryUtils.spec.ts b/src/shared/lib/query/textQueryUtils.spec.ts index b083f5546a5..4ef8b9c150f 100644 --- a/src/shared/lib/query/textQueryUtils.spec.ts +++ b/src/shared/lib/query/textQueryUtils.spec.ts @@ -15,7 +15,7 @@ import { QueryParser } from 'shared/lib/query/QueryParser'; import { StringPhrase } from 'shared/components/query/filteredSearch/Phrase'; describe('textQueryUtils', () => { - const parser = new QueryParser(new Set()); + const parser = new QueryParser(new Set(), new Set()); const referenceGenomeFields = parser.searchFilters.find( f => f.phrasePrefix === 'reference-genome' )!.nodeFields; From 81484bde105fea6f3dbd69a153e51c2da8bb2b52 Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:35:28 -0500 Subject: [PATCH 3/3] Add option to search based on authorized and unauthorized studies --- .../query/filteredSearch/Phrase.tsx | 2 +- .../unknownStudies/UnknownStudiesWarning.tsx | 58 ++++++++++++++++--- src/shared/lib/query/QueryParser.ts | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index 7beeef04b76..3b795975ca1 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,7 +129,7 @@ export class ListPhrase implements Phrase { for (const fieldName of this.fields) { let anyPhraseMatch = false; const fieldValue = study[fieldName]; - if (fieldValue) { + if (typeof fieldValue !== 'undefined') { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || diff --git a/src/shared/components/unknownStudies/UnknownStudiesWarning.tsx b/src/shared/components/unknownStudies/UnknownStudiesWarning.tsx index babb75fb54c..a8e4635de03 100644 --- a/src/shared/components/unknownStudies/UnknownStudiesWarning.tsx +++ b/src/shared/components/unknownStudies/UnknownStudiesWarning.tsx @@ -1,12 +1,29 @@ import * as React from 'react'; import { observer } from 'mobx-react'; +import { Collapse } from 'react-collapse'; +import classnames from 'classnames'; @observer export default class UnknownStudiesWarning extends React.Component< { ids: String[] }, + { studiesCollapsed: boolean }, {} > { + constructor(props: { ids: String[] }) { + super(props); + this.state = { + studiesCollapsed: true + }; + } + + toggleStudiesCollapse = () => { + this.setState(prevState => ({ + studiesCollapsed: !prevState.studiesCollapsed + })); + } + render() { + const { studiesCollapsed } = this.state; if (this.props.ids.length > 0) { return (
    - The following - studies do not exist or you do not have access to them: -
      - {this.props.ids.map(id => ( -
    • {id}
    • - ))} -
    + +
    this.toggleStudiesCollapse()} + > + The following + studies do not exist or you do not have access to them. + + {studiesCollapsed ? ( + + ) : ( + + )} + +
    + +
      + {this.props.ids.map(id => ( +
    • {id}
    • + ))} +
    +
    Please resubmit your query below.
    ); diff --git a/src/shared/lib/query/QueryParser.ts b/src/shared/lib/query/QueryParser.ts index ee77942d3c7..536dc42abc5 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -63,7 +63,7 @@ export class QueryParser { form: { input: FilterCheckbox, options: - readPermissions.size >= 1 + readPermissions.size > 1 ? [ { value: 'true', displayValue: 'Authorized' }, {