Skip to content

Commit

Permalink
Merge pull request #1068 from griffithlab/improved-quicksearch
Browse files Browse the repository at this point in the history
Quicksearch improvements
  • Loading branch information
acoffman committed Jun 24, 2024
2 parents 82e2b69 + 9b07582 commit e7b9e3c
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
nzPlaceHolder="Search CIViC"
[nzOptionHeightPx]="28"
[nzShowArrow]="false"
[nzDropdownRender]="searchEntities"
[nzDropdownMatchSelectWidth]="false">
<nz-option
*ngFor="let o of (option$ | ngrxPush)"
nzCustomContent
[nzValue]="o.result">
<i
nz-icon
style="margin-right: 0.5em;"
style="margin-right: 0.5em"
nzTheme="twotone"
[nzTwotoneColor]="converter(o.result.resultType) | entityColor"
[nzType]="o.result.resultType | iconNameForSubscribableEntity"></i>
Expand All @@ -39,3 +40,26 @@
Loading Data...
</nz-option>
</nz-select>

<ng-template #searchEntities>
<hr />
<nz-checkbox-wrapper (nzOnChange)="selectedEntitiesChanged($event)">
<div class="entity-select">
@for(entity of searchableEntities; track entity) {
<label
nz-checkbox
[nzValue]="entity"
nz-tooltip
[nzTooltipTitle]="entity"
[ngModel]="isSelected(entity)">
<i
nz-icon
nzTheme="twotone"
nzTool
[nzTwotoneColor]="entity | entityColor"
[nzType]="entity | iconNameForSubscribableEntity"></i>
</label>
}
</div>
</nz-checkbox-wrapper>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@
nz-select {
width: 100%;
}

hr {
border-color: #6666
}

.entity-select {
margin-right: 10px;
margin-left: 10px;

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export class CvcQuicksearchComponent {
@ViewChild(NzSelectComponent, { static: true })
selectNode!: NzSelectComponent

selectedEntities: SearchableEntities[] = Object.values(SearchableEntities)
searchableEntities = Object.keys(SearchableEntities)
currentSearchTerm?: string

// SOURCE STREAMS
onSearch$: Subject<string>
onSelect$: Subject<void>
Expand All @@ -62,25 +66,29 @@ export class CvcQuicksearchComponent {
option$: Observable<QuicksearchOption[]>
isLoading$: Observable<boolean>


converter = entityTypeToTypename

constructor(private gql: QuicksearchGQL, private router: Router) {
constructor(
private gql: QuicksearchGQL,
private router: Router
) {
this.onSearch$ = new Subject<string>()
this.onSelect$ = new Subject<void>()

this.response$ = this.onSearch$.pipe(
skip(1), // drop initial empty string from input init
throttleTime(300, asyncScheduler, { leading: false, trailing: true }),
switchMap((str: string) => {
this.currentSearchTerm = str
let selectedEntities = this.selectedEntities
// if queryRef doesn't exist, create it with watchQuery observable
// if it does, refetch with fetchQuery observable.
// using defer() ensures functions are not called until
// values are emitted. otherwise they'll be called on subscribe.
return iif(
() => this.queryRef === undefined, // predicate
defer(() => watchQuery(str)), // true
defer(() => fetchQuery(str))
defer(() => watchQuery(str, selectedEntities)), // true
defer(() => fetchQuery(str, selectedEntities))
) // false
})
)
Expand Down Expand Up @@ -114,14 +122,19 @@ export class CvcQuicksearchComponent {
})

// set queryRef with watch(), return its valueChanges observable
const watchQuery = (str: string) => {
this.queryRef = this.gql.watch({ query: str, highlightMatches: true })
const watchQuery = (str: string, entities: Maybe<SearchableEntities[]>) => {
this.queryRef = this.gql.watch({
query: str,
highlightMatches: true,
types: entities,
})
return this.queryRef.valueChanges
}

// return observable from refetch() promise
const fetchQuery = (str: string) =>
from(this.queryRef.refetch({ query: str }))
const fetchQuery = (str: string, entities: Maybe<SearchableEntities[]>) => {
return from(this.queryRef.refetch({ query: str, types: entities }))
}
}

urlForResult(res: SearchResult): string {
Expand All @@ -136,10 +149,28 @@ export class CvcQuicksearchComponent {
case SearchableEntities.MolecularProfile:
name = 'molecular-profiles'
break
case SearchableEntities.Therapy:
name = 'therapies'
break
default:
name = `${res.resultType.toLowerCase()}s`
}

return `/${name}/${res.id}/summary`
}

selectedEntitiesChanged(selectedEntities: string[]) {
this.selectedEntities = selectedEntities.map(
(x) => SearchableEntities[x as keyof typeof SearchableEntities]
)

if (this.currentSearchTerm) {
this.onSearch$.next(this.currentSearchTerm)
}
}

isSelected(entity: string): boolean {
const x = SearchableEntities[entity as keyof typeof SearchableEntities]
return this.selectedEntities.includes(x)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { FormsModule } from '@angular/forms'
import { NzSelectModule } from 'ng-zorro-antd/select'
import { NzTypographyModule } from 'ng-zorro-antd/typography'
import { CvcPipesModule } from '@app/core/pipes/pipes.module'
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'
import { NzToolTipModule } from 'ng-zorro-antd/tooltip'

@NgModule({
declarations: [CvcQuicksearchComponent],
Expand All @@ -27,6 +29,8 @@ import { CvcPipesModule } from '@app/core/pipes/pipes.module'
NzFormModule,
NzIconModule,
NzAutocompleteModule,
NzCheckboxModule,
NzToolTipModule,
],
exports: [CvcQuicksearchComponent],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ export class IconNameForSubscribableEntity implements PipeTransform {
case SubscribableEntities.VariantGroup:
case 'VARIANT_GROUP':
case 'VariantGroup':
return 'civic-variant-group'
return 'civic-variantgroup'
case SubscribableEntities.MolecularProfile:
case 'MOLECULAR_PROFILE':
case 'MolecularProfile':
return 'civic-molecularprofile'
case 'THERAPY':
case 'Therapy':
return 'civic-therapy'
case 'DISEASE':
case 'Disease':
return 'civic-disease'
default:
console.log('String No icon name found for ' + e)
return 'border-outer'
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/generated/civic.apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5195,10 +5195,12 @@ export type SearchResult = {

export enum SearchableEntities {
Assertion = 'ASSERTION',
Disease = 'DISEASE',
EvidenceItem = 'EVIDENCE_ITEM',
Feature = 'FEATURE',
MolecularProfile = 'MOLECULAR_PROFILE',
Revision = 'REVISION',
Therapy = 'THERAPY',
Variant = 'VARIANT',
VariantGroup = 'VARIANT_GROUP'
}
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/generated/server.model.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8763,10 +8763,12 @@ type SearchResult {

enum SearchableEntities {
ASSERTION
DISEASE
EVIDENCE_ITEM
FEATURE
MOLECULAR_PROFILE
REVISION
THERAPY
VARIANT
VARIANT_GROUP
}
Expand Down
12 changes: 12 additions & 0 deletions client/src/app/generated/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40478,6 +40478,18 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "THERAPY",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DISEASE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
Expand Down
17 changes: 13 additions & 4 deletions server/app/graphql/resolvers/quicksearch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def resolve(query:, types: nil, highlight_matches: false)
Assertion,
VariantGroup,
Revision,
MolecularProfile
MolecularProfile,
Therapy,
Disease
]
else
types
Expand All @@ -33,8 +35,15 @@ def resolve(query:, types: nil, highlight_matches: false)
models: query_targets,
highlight: tag,
limit: 10,
fields: ['id^10', 'name', 'aliases', 'feature', 'feature_type']
).with_highlights(multiple: true)
fields: [
{'id^10' => :word },
{'ncit_id' => :word },
{'doid' => :word },
{'name^10' => :word },
{'aliases' => :word_start },
{'feature' => :word },
{'feature_type' => :word }
]).with_highlights(multiple: true)

results.map do |res, highlights|
{
Expand All @@ -59,7 +68,7 @@ def format_name(name, highlights)
def format_highlights(highlights)
rows = highlights.map do |field_name, matches|
next if field_name == :name
name = field_name.to_s.titleize
name = field_name.to_s.split(".").first&.titleize
match_string = matches.join(', ')
"#{name}: #{match_string}"
end
Expand Down
4 changes: 2 additions & 2 deletions server/app/graphql/resolvers/top_level_therapies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ class Resolvers::TopLevelTherapies < GraphQL::Schema::Resolver
description 'List and filter Therapies from the NCI Thesaurus.'

scope do
Therapy.select('therapies.id, therapies.name, max(therapies.ncit_id) as ncit_id, count(distinct(assertions.id)) as assertion_count, count(distinct(evidence_items.id)) as evidence_count')
Therapy.select('therapies.id, therapies.name, therapies.deprecated, max(therapies.ncit_id) as ncit_id, count(distinct(assertions.id)) as assertion_count, count(distinct(evidence_items.id)) as evidence_count')
.left_outer_joins(:assertions, :evidence_items)
.where("evidence_items.status != 'rejected' OR assertions.status != 'rejected'")
.where(deprecated: false)
.group('therapies.id, therapies.name')
.group('therapies.id, therapies.name, therapies.deprecated')
.having('COUNT(evidence_items.id) > 0 OR COUNT(assertions.id) > 0')
.order('evidence_count DESC', :id)
end
Expand Down
2 changes: 2 additions & 0 deletions server/app/graphql/types/quicksearch/searchable_entities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ class SearchableEntities < Types::BaseEnum
value 'VARIANT_GROUP', value: VariantGroup
value 'REVISION', value: Revision
value 'MOLECULAR_PROFILE', value: MolecularProfile
value 'THERAPY', value: Therapy
value 'DISEASE', value: Disease
end
end
16 changes: 16 additions & 0 deletions server/app/models/disease.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ class Disease < ApplicationRecord
has_many :source_suggestions
has_and_belongs_to_many :disease_aliases

searchkick highlight: [:name, :aliases], callbacks: :async, word_start: [:name, :aliases]
scope :search_import, -> { includes(:disease_aliases) }

def search_data
{
name: name,
doid: "DOID:#{doid}",
aliases: disease_aliases.map(&:name)
}
end

def should_index?
evidence_items.any?
end


def disease_url
Disease.url_for_doid(doid)
end
Expand Down
15 changes: 15 additions & 0 deletions server/app/models/therapy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ class Therapy < ApplicationRecord

validates :ncit_id, uniqueness: true, allow_nil: true

searchkick highlight: [:name, :aliases], callbacks: :async, word_start: [:name, :aliases]
scope :search_import, -> { includes(:therapy_aliases) }

def search_data
{
name: name,
ncit_id: ncit_id,
aliases: therapy_aliases.map(&:name)
}
end

def should_index?
evidence_items.any?
end

def self.url_for(ncit_id:)
if ncit_id.nil?
nil
Expand Down

0 comments on commit e7b9e3c

Please sign in to comment.