From 2f10d41e217be827c59cfa65c50a368a07b381af Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Wed, 15 Feb 2023 12:19:12 -0500 Subject: [PATCH] improve search of authorized and unauthorized studies fix 9915 improve search of authorized studies add new search filter based on readPermission flag called Controlled access authorized. Modified the search algorithm to accept booleans instead of just strings esp. false value fix 9915 improve search of authorized studies fix 9915 improve search of authorized studies add new search filter based on readPermission flag called Controlled access authorized. Modified the search algorithm to accept booleans instead of just strings esp. false value Use _.toString in matchPhraseFull, and specify type Unnest fieldName check in Phrase.match Introduce FilterFieldOption type that includes a displayValue Update QueryParser.ts Update CheckboxFilterField.tsx id has to be unique for each filter when there are multiple filters. Modify the Select all checkbox to select only authorized studies Update checkbox label text Update checkbox label text when filter is applied vs not applied fix all Authorized or Unauthorized studies scenario fix all Authorized or Unauthorized studies scenario. Hide the option if the studies that are all authorized or all unauthorized. Updated logic and refactoring Only CancerStudy objects from treeData are filtered based on whether the readPermission field has a value. Refactoring: New const created as shownStudiesLengthstring to identify if there is a filter applied or not. prettier --- .../components/query/CancerStudySelector.tsx | 107 +++++++++++------- src/shared/components/query/QueryStore.ts | 12 +- src/shared/components/query/StudyListLogic.ts | 36 ++++++ .../query/filteredSearch/Phrase.tsx | 10 +- .../field/CheckboxFilterField.spec.ts | 12 +- .../field/CheckboxFilterField.tsx | 59 +++++----- .../filteredSearch/field/FilterFieldOption.ts | 12 ++ .../filteredSearch/field/ListFormField.tsx | 8 +- src/shared/lib/query/QueryParser.spec.ts | 2 +- src/shared/lib/query/QueryParser.ts | 30 ++++- src/shared/lib/query/textQueryUtils.spec.ts | 2 +- 11 files changed, 208 insertions(+), 82 deletions(-) create mode 100644 src/shared/components/query/filteredSearch/field/FilterFieldOption.ts diff --git a/src/shared/components/query/CancerStudySelector.tsx b/src/shared/components/query/CancerStudySelector.tsx index d1d1417e8a7..0ff76b86e9f 100644 --- a/src/shared/components/query/CancerStudySelector.tsx +++ b/src/shared/components/query/CancerStudySelector.tsx @@ -57,6 +57,9 @@ export default class CancerStudySelector extends React.Component< onCheckAllFiltered: () => { this.logic.mainView.toggleAllFiltered(); }, + onCheckAuthorizedFiltered: () => { + this.logic.mainView.toggleAllAuthorizedAndFiltered(); + }, onClearFilter: () => { this.store.setSearchText(''); }, @@ -193,9 +196,16 @@ export default class CancerStudySelector extends React.Component< shownAndSelectedStudies, } = this.logic.mainView.getSelectionReport(); + const shownAndAuthorizedStudies = shownStudies.filter(study => { + return study.readPermission; + }); const quickSetButtons = this.logic.mainView.quickSelectButtons( getServerConfig().skin_quick_select_buttons ); + const shownStudiesLengthstring = + shownStudies.length < this.store.cancerStudies.result.length + ? 'matching filter' + : ''; return ( - + + + + + + + + diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 8a148cf794c..7beb6297a89 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -129,7 +129,7 @@ export class QueryStore { @computed get queryParser() { - return new QueryParser(this.referenceGenomes); + return new QueryParser(this.referenceGenomes, this.readPermissions); } initialize(urlWithInitialParams?: string) { @@ -1479,6 +1479,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/components/query/StudyListLogic.ts b/src/shared/components/query/StudyListLogic.ts index 53b9b27f0e8..549f0b1359a 100644 --- a/src/shared/components/query/StudyListLogic.ts +++ b/src/shared/components/query/StudyListLogic.ts @@ -400,6 +400,42 @@ export class FilteredCancerTreeView { ); } + @action toggleAllAuthorizedAndFiltered() { + const { + selectableSelectedStudyIds, + selectableSelectedStudies, + shownStudies, + shownAndSelectedStudies, + } = this.getSelectionReport(); + + let updatedSelectableSelectedStudyIds: string[] = []; + const shownAndAuthorizedStudies = shownStudies.filter(study => { + return study.readPermission; + }); + if ( + shownAndAuthorizedStudies.length === shownAndSelectedStudies.length + ) { + // deselect + updatedSelectableSelectedStudyIds = _.without( + this.store.selectableSelectedStudyIds, + ...shownAndAuthorizedStudies.map( + (study: CancerStudy) => study.studyId + ) + ); + } else { + updatedSelectableSelectedStudyIds = _.union( + this.store.selectableSelectedStudyIds, + shownAndAuthorizedStudies.map( + (study: CancerStudy) => study.studyId + ) + ); + } + + this.store.selectableSelectedStudyIds = updatedSelectableSelectedStudyIds.filter( + id => !_.includes(this.store.deletedVirtualStudies, id) + ); + } + @action selectAllMatchingStudies(match: string | string[]) { const { selectableSelectedStudyIds, diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index 81471833e85..7e98d668603 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,9 +129,12 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { + if (!_.has(study, fieldName)) { + continue; + } let anyPhraseMatch = false; const fieldValue = study[fieldName]; - if (fieldValue) { + if (typeof fieldValue !== 'undefined') { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || @@ -167,7 +170,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 facc250cc6a..e16ba935be0 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 => { @@ -43,9 +47,9 @@ 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); } } @@ -53,9 +57,9 @@ export const FilterCheckbox: FunctionComponent = props => {
{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', - }} - />
); @@ -159,7 +163,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))]; } else { 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.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 965ab857591..d9a317522be 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -6,9 +6,9 @@ import { import { AndSearchClause, FILTER_SEPARATOR, - SearchClause, NOT_PREFIX, NotSearchClause, + SearchClause, } from 'shared/components/query/filteredSearch/SearchClause'; import { FilterCheckbox } from 'shared/components/query/filteredSearch/field/CheckboxFilterField'; import { getServerConfig, ServerConfigHelpers } from 'config/config'; @@ -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 { /** @@ -25,7 +26,7 @@ export class QueryParser { */ private readonly _searchFilters: CancerTreeSearchFilter[]; - constructor(referenceGenomes: Set) { + constructor(referenceGenomes: Set, readPermissions: Set) { this._searchFilters = [ /** * Example queries: @@ -38,7 +39,7 @@ export class QueryParser { input: FilterList, options: ServerConfigHelpers.skin_example_study_queries( getServerConfig()!.skin_example_study_queries || '' - ), + ).map(toFilterFieldOption), }, }, /** @@ -49,10 +50,31 @@ export class QueryParser { nodeFields: ['referenceGenome'], form: { input: FilterCheckbox, - options: [...referenceGenomes], + options: [...referenceGenomes].map(toFilterFieldOption), 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;