From 8b211a8f3cfcb17995c630f08de532ab50f50e88 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 1/4] 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 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. --- .../components/query/CancerStudySelector.tsx | 143 +++++++++++++----- src/shared/components/query/QueryStore.ts | 11 +- src/shared/components/query/StudyListLogic.ts | 36 +++++ .../query/filteredSearch/Phrase.tsx | 13 +- .../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 | 27 +++- src/shared/lib/query/textQueryUtils.spec.ts | 2 +- 11 files changed, 241 insertions(+), 84 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..8e2b562fb17 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,6 +196,9 @@ 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 ); @@ -307,45 +313,104 @@ export default class CancerStudySelector extends React.Component< - + + + + + + + + diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 8a148cf794c..574f8120c7c 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,15 @@ 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' ); + console.log(studies); + const readPermissions = studies + .map(n => (!!((n as CancerStudy).readPermission)).toString()) + .filter(n => !!n); + 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..58b48c65d1e 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,9 +129,11 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { - let anyPhraseMatch = false; - const fieldValue = study[fieldName]; - if (fieldValue) { + if (!_.has(study, fieldName)) { + continue; + } + const fieldValue = (study as any)[fieldName]; + if (typeof fieldValue !== 'undefined') { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || @@ -167,7 +169,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..1192dac26a1 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..f95fe8fc8d6 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,10 @@ export class QueryParser { */ private readonly _searchFilters: CancerTreeSearchFilter[]; - constructor(referenceGenomes: Set) { + constructor(referenceGenomes: Set, readPermissions: Set) { + console.log('readPermissions'); + console.log(readPermissions); + console.log(readPermissions.size); this._searchFilters = [ /** * Example queries: @@ -38,7 +42,7 @@ export class QueryParser { input: FilterList, options: ServerConfigHelpers.skin_example_study_queries( getServerConfig()!.skin_example_study_queries || '' - ), + ).map(toFilterFieldOption), }, }, /** @@ -49,10 +53,25 @@ 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; From 6c849ad844ec5fa4e6163b5a9aad068692208b5b Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:25:15 -0400 Subject: [PATCH 2/4] Updated logic and refactoring and prettier changes 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. Update Phrase.tsx conflict resolution fix due to merge changes for prettier prettier changes --- .../components/query/CancerStudySelector.tsx | 52 +++---------------- src/shared/components/query/QueryStore.ts | 18 ++++--- .../query/filteredSearch/Phrase.tsx | 8 ++- src/shared/lib/query/QueryParser.spec.ts | 2 +- src/shared/lib/query/QueryParser.ts | 17 +++--- 5 files changed, 34 insertions(+), 63 deletions(-) diff --git a/src/shared/components/query/CancerStudySelector.tsx b/src/shared/components/query/CancerStudySelector.tsx index 8e2b562fb17..0ff76b86e9f 100644 --- a/src/shared/components/query/CancerStudySelector.tsx +++ b/src/shared/components/query/CancerStudySelector.tsx @@ -202,6 +202,10 @@ export default class CancerStudySelector extends React.Component< const quickSetButtons = this.logic.mainView.quickSelectButtons( getServerConfig().skin_quick_select_buttons ); + const shownStudiesLengthstring = + shownStudies.length < this.store.cancerStudies.result.length + ? 'matching filter' + : ''; return ( {shownAndSelectedStudies.length === shownStudies.length - ? `Deselect all listed studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownStudies.length - })` - : `Select all listed studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownStudies.length - })`} + ? `Deselect all listed studies ${shownStudiesLengthstring} (${shownStudies.length})` + : `Select all listed studies ${shownStudiesLengthstring} (${shownStudies.length})`} @@ -385,28 +369,8 @@ export default class CancerStudySelector extends React.Component< shownAndAuthorizedStudies.length && shownAndAuthorizedStudies.length > 0 - ? `Deselect all authorized studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownAndAuthorizedStudies.length - })` - : `Select all authorized studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownAndAuthorizedStudies.length - })`} + ? `Deselect all authorized studies ${shownStudiesLengthstring} (${shownAndAuthorizedStudies.length})` + : `Select all authorized studies ${shownStudiesLengthstring} (${shownAndAuthorizedStudies.length})`} diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 574f8120c7c..25f831730fe 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -1236,7 +1236,12 @@ export class QueryStore { return client .getAllSamplesOfPatientInStudyUsingGET({ studyId, patientId }) .then( - samples => ({ studyId, patientId, samples, error: undefined }), + samples => ({ + studyId, + patientId, + samples, + error: undefined, + }), error => ({ studyId, patientId, @@ -1480,11 +1485,12 @@ export class QueryStore { } @computed get readPermissions(): Set { - const studies = Array.from(this.treeData.map_node_meta.keys()).filter( s => typeof((s as CancerStudy).readPermission) !== 'undefined' ); - console.log(studies); - const readPermissions = studies - .map(n => (!!((n as CancerStudy).readPermission)).toString()) - .filter(n => !!n); + 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); } diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index 58b48c65d1e..010b4096cd9 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,11 +129,9 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { - if (!_.has(study, fieldName)) { - continue; - } - const fieldValue = (study as any)[fieldName]; - if (typeof fieldValue !== 'undefined') { + let anyPhraseMatch = false; + const fieldValue = study[fieldName]; + if (fieldValue) { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || diff --git a/src/shared/lib/query/QueryParser.spec.ts b/src/shared/lib/query/QueryParser.spec.ts index 1192dac26a1..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(),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 f95fe8fc8d6..d9a317522be 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -27,9 +27,6 @@ export class QueryParser { private readonly _searchFilters: CancerTreeSearchFilter[]; constructor(referenceGenomes: Set, readPermissions: Set) { - console.log('readPermissions'); - console.log(readPermissions); - console.log(readPermissions.size); this._searchFilters = [ /** * Example queries: @@ -65,10 +62,16 @@ export class QueryParser { nodeFields: ['readPermission'], form: { input: FilterCheckbox, - options: readPermissions.size > 1 ? [ - { value: 'true', displayValue: 'Authorized' }, - { value: 'false', displayValue: 'Unauthorized' }, - ]:[], + options: + readPermissions.size > 1 + ? [ + { value: 'true', displayValue: 'Authorized' }, + { + value: 'false', + displayValue: 'Unauthorized', + }, + ] + : [], label: 'Controlled access', }, }, From 2961c91583e69bdc83a5aecef6bb47fa3c17f099 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 3/4] 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 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. --- .../components/query/CancerStudySelector.tsx | 143 +++++++++++++----- src/shared/components/query/QueryStore.ts | 11 +- src/shared/components/query/StudyListLogic.ts | 36 +++++ .../query/filteredSearch/Phrase.tsx | 13 +- .../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 | 27 +++- src/shared/lib/query/textQueryUtils.spec.ts | 2 +- 11 files changed, 241 insertions(+), 84 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..8e2b562fb17 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,6 +196,9 @@ 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 ); @@ -307,45 +313,104 @@ export default class CancerStudySelector extends React.Component<
    - + + + + + + + + diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 8a148cf794c..574f8120c7c 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,15 @@ 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' ); + console.log(studies); + const readPermissions = studies + .map(n => (!!((n as CancerStudy).readPermission)).toString()) + .filter(n => !!n); + 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..58b48c65d1e 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,9 +129,11 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { - let anyPhraseMatch = false; - const fieldValue = study[fieldName]; - if (fieldValue) { + if (!_.has(study, fieldName)) { + continue; + } + const fieldValue = (study as any)[fieldName]; + if (typeof fieldValue !== 'undefined') { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || @@ -167,7 +169,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..1192dac26a1 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..f95fe8fc8d6 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,10 @@ export class QueryParser { */ private readonly _searchFilters: CancerTreeSearchFilter[]; - constructor(referenceGenomes: Set) { + constructor(referenceGenomes: Set, readPermissions: Set) { + console.log('readPermissions'); + console.log(readPermissions); + console.log(readPermissions.size); this._searchFilters = [ /** * Example queries: @@ -38,7 +42,7 @@ export class QueryParser { input: FilterList, options: ServerConfigHelpers.skin_example_study_queries( getServerConfig()!.skin_example_study_queries || '' - ), + ).map(toFilterFieldOption), }, }, /** @@ -49,10 +53,25 @@ 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; From 9211a095b82acaa2332ff941762d8b85c52a36f0 Mon Sep 17 00:00:00 2001 From: Prasanna Kumar Jagannathan <37613906+jagnathan@users.noreply.github.com> Date: Fri, 21 Apr 2023 13:25:15 -0400 Subject: [PATCH 4/4] Updated logic and refactoring and prettier changes 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. Update Phrase.tsx conflict resolution fix due to merge changes for prettier prettier changes --- .../components/query/CancerStudySelector.tsx | 52 +++---------------- src/shared/components/query/QueryStore.ts | 18 ++++--- .../query/filteredSearch/Phrase.tsx | 8 ++- src/shared/lib/query/QueryParser.spec.ts | 2 +- src/shared/lib/query/QueryParser.ts | 17 +++--- 5 files changed, 34 insertions(+), 63 deletions(-) diff --git a/src/shared/components/query/CancerStudySelector.tsx b/src/shared/components/query/CancerStudySelector.tsx index 8e2b562fb17..0ff76b86e9f 100644 --- a/src/shared/components/query/CancerStudySelector.tsx +++ b/src/shared/components/query/CancerStudySelector.tsx @@ -202,6 +202,10 @@ export default class CancerStudySelector extends React.Component< const quickSetButtons = this.logic.mainView.quickSelectButtons( getServerConfig().skin_quick_select_buttons ); + const shownStudiesLengthstring = + shownStudies.length < this.store.cancerStudies.result.length + ? 'matching filter' + : ''; return ( {shownAndSelectedStudies.length === shownStudies.length - ? `Deselect all listed studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownStudies.length - })` - : `Select all listed studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownStudies.length - })`} + ? `Deselect all listed studies ${shownStudiesLengthstring} (${shownStudies.length})` + : `Select all listed studies ${shownStudiesLengthstring} (${shownStudies.length})`} @@ -385,28 +369,8 @@ export default class CancerStudySelector extends React.Component< shownAndAuthorizedStudies.length && shownAndAuthorizedStudies.length > 0 - ? `Deselect all authorized studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownAndAuthorizedStudies.length - })` - : `Select all authorized studies ${ - shownStudies.length < - this.store - .cancerStudies - .result - .length - ? 'matching filter' - : '' - } (${ - shownAndAuthorizedStudies.length - })`} + ? `Deselect all authorized studies ${shownStudiesLengthstring} (${shownAndAuthorizedStudies.length})` + : `Select all authorized studies ${shownStudiesLengthstring} (${shownAndAuthorizedStudies.length})`} diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 574f8120c7c..25f831730fe 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -1236,7 +1236,12 @@ export class QueryStore { return client .getAllSamplesOfPatientInStudyUsingGET({ studyId, patientId }) .then( - samples => ({ studyId, patientId, samples, error: undefined }), + samples => ({ + studyId, + patientId, + samples, + error: undefined, + }), error => ({ studyId, patientId, @@ -1480,11 +1485,12 @@ export class QueryStore { } @computed get readPermissions(): Set { - const studies = Array.from(this.treeData.map_node_meta.keys()).filter( s => typeof((s as CancerStudy).readPermission) !== 'undefined' ); - console.log(studies); - const readPermissions = studies - .map(n => (!!((n as CancerStudy).readPermission)).toString()) - .filter(n => !!n); + 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); } diff --git a/src/shared/components/query/filteredSearch/Phrase.tsx b/src/shared/components/query/filteredSearch/Phrase.tsx index 58b48c65d1e..010b4096cd9 100644 --- a/src/shared/components/query/filteredSearch/Phrase.tsx +++ b/src/shared/components/query/filteredSearch/Phrase.tsx @@ -129,11 +129,9 @@ export class ListPhrase implements Phrase { public match(study: FullTextSearchNode): boolean { let anyFieldMatch = false; for (const fieldName of this.fields) { - if (!_.has(study, fieldName)) { - continue; - } - const fieldValue = (study as any)[fieldName]; - if (typeof fieldValue !== 'undefined') { + let anyPhraseMatch = false; + const fieldValue = study[fieldName]; + if (fieldValue) { for (const phrase of this._phraseList) { anyPhraseMatch = anyPhraseMatch || diff --git a/src/shared/lib/query/QueryParser.spec.ts b/src/shared/lib/query/QueryParser.spec.ts index 1192dac26a1..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(),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 f95fe8fc8d6..d9a317522be 100644 --- a/src/shared/lib/query/QueryParser.ts +++ b/src/shared/lib/query/QueryParser.ts @@ -27,9 +27,6 @@ export class QueryParser { private readonly _searchFilters: CancerTreeSearchFilter[]; constructor(referenceGenomes: Set, readPermissions: Set) { - console.log('readPermissions'); - console.log(readPermissions); - console.log(readPermissions.size); this._searchFilters = [ /** * Example queries: @@ -65,10 +62,16 @@ export class QueryParser { nodeFields: ['readPermission'], form: { input: FilterCheckbox, - options: readPermissions.size > 1 ? [ - { value: 'true', displayValue: 'Authorized' }, - { value: 'false', displayValue: 'Unauthorized' }, - ]:[], + options: + readPermissions.size > 1 + ? [ + { value: 'true', displayValue: 'Authorized' }, + { + value: 'false', + displayValue: 'Unauthorized', + }, + ] + : [], label: 'Controlled access', }, },