From 1f003e4536950c5b15cd3a1a6eb5705fed3031d2 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Thu, 26 Feb 2026 10:51:41 -0500 Subject: [PATCH 1/4] feat: Add advanced search with Lucene syntax to search page Integrate advanced search into the existing /content/query page as a toggleable panel with tab-style navigation between Simple and Advanced modes. Advanced mode provides a textarea for Lucene query syntax (AND, OR, NOT, wildcards, proximity, grouping), inline syntax hints with link to Solr documentation, collapsible syntax help table, and upfront facet filter grid (species, types, compartments, keywords). - Add getAllFacets() to search service calling GET /search/facet - Add advanced mode state, toggle, and submit logic to search component - Add FormsModule for ngModel on textarea - Update nav-options.json Advanced Data Search link to /content/query?advanced=true - Add redirects for /content/advanced and /tools/advanced - Responsive facet grid (4 cols desktop, 2 tablet, 1 mobile) Co-Authored-By: Claude Opus 4.6 --- .../website-angular/src/app/app.routes.ts | 2 + .../src/app/search/search.component.html | 187 ++++++++++- .../src/app/search/search.component.scss | 302 +++++++++++++++++- .../src/app/search/search.component.ts | 70 +++- .../src/config/nav-options.json | 2 +- .../src/services/search.service.ts | 4 + 6 files changed, 561 insertions(+), 6 deletions(-) diff --git a/projects/website-angular/src/app/app.routes.ts b/projects/website-angular/src/app/app.routes.ts index 230cecb..70bb71b 100644 --- a/projects/website-angular/src/app/app.routes.ts +++ b/projects/website-angular/src/app/app.routes.ts @@ -27,6 +27,8 @@ export const routes: Routes = [ { path: 'content/reactome-research-spotlight/:slug', loadComponent: () => import('./article/article/article.component').then(m => m.ArticleComponent), pathMatch: 'full' }, //Search Page + { path: 'content/advanced', redirectTo: '/content/query?advanced=true' }, + { path: 'tools/advanced', redirectTo: '/content/query?advanced=true' }, { path: 'content/query', loadComponent: () => import('./search/search.component').then(m => m.SearchComponent) }, //404 Page diff --git a/projects/website-angular/src/app/search/search.component.html b/projects/website-angular/src/app/search/search.component.html index 4032000..b25c981 100644 --- a/projects/website-angular/src/app/search/search.component.html +++ b/projects/website-angular/src/app/search/search.component.html @@ -3,7 +3,43 @@ @if (query && searchSubmitted) {
- +
+ + +
+ + @if (advancedMode) { +
+
+ +
+ " " exact phrase + AND OR NOT boolean + ? * wildcards + "raf map"~4 proximity + ( ) grouping + Full syntax reference +
+
+ +
+ } @else { + + }
@@ -374,9 +410,154 @@

{{ group.typeName }} ({{ group.entriesCount }})

Search Reactome

Enter a search term to find pathways, reactions, proteins, and more.

- }
diff --git a/projects/website-angular/src/app/search/search.component.scss b/projects/website-angular/src/app/search/search.component.scss index 029cae8..37df87a 100644 --- a/projects/website-angular/src/app/search/search.component.scss +++ b/projects/website-angular/src/app/search/search.component.scss @@ -335,8 +335,300 @@ $border-radius: 8px; // Search bar at top .search-top { - max-width: 600px; + max-width: 700px; + margin: 0 auto $spacing; +} + +// Mode tabs (Simple / Advanced) +.search-mode-tabs { + display: flex; + justify-content: center; + gap: 0; + margin-bottom: 16px; + border-bottom: 2px solid rgba(0, 0, 0, 0.08); + + :host-context(.dark) & { + border-bottom-color: rgba(255, 255, 255, 0.08); + } +} + +.mode-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + border: none; + background: none; + color: var(--on-surface-variant); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + position: relative; + transition: color 0.15s ease; + + .material-symbols-rounded { + font-size: 18px; + } + + &::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: transparent; + transition: background 0.15s ease; + } + + &:hover { + color: var(--primary); + } + + &.active { + color: var(--primary); + font-weight: 600; + + &::after { + background: var(--primary); + } + } +} + +// Compact syntax hints (always visible in results view) +.syntax-hints { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 6px 16px; + margin-top: 8px; + font-size: 0.78rem; + color: var(--on-surface-variant); + + code { + background: color-mix(in srgb, var(--primary) 10%, transparent); + padding: 0 4px; + border-radius: 3px; + font-size: 0.8em; + } + + .syntax-hints-link { + color: var(--primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +// Advanced input area (in results view) +.advanced-input-area { + display: flex; + gap: 8px; + align-items: flex-start; + + .advanced-search-btn { + margin-top: 0; + flex-shrink: 0; + } +} + +.advanced-input-column { + flex: 1; + min-width: 0; +} + +// Advanced search panel (in empty state) +.advanced-panel { + max-width: 900px; margin: 0 auto $spacing; + text-align: left; +} + +.advanced-query-input { + width: 100%; + padding: 12px 16px; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: $border-radius; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9rem; + line-height: 1.5; + color: var(--on-surface); + background: white; + resize: vertical; + box-sizing: border-box; + transition: border-color 0.2s; + + &::placeholder { + color: var(--on-surface-variant); + opacity: 0.6; + } + + &:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); + } + + :host-context(.dark) & { + background: var(--surface); + border-color: rgba(255, 255, 255, 0.15); + } +} + +// Syntax help +.syntax-help-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 0; + border: none; + background: none; + color: var(--on-surface-variant); + font-size: 0.8rem; + cursor: pointer; + margin-top: 8px; + + .material-symbols-rounded { + font-size: 16px; + } + + &:hover { + color: var(--primary); + } +} + +.syntax-help { + background: color-mix(in srgb, var(--primary) 5%, transparent); + border-radius: $border-radius; + padding: 16px; + margin-top: 8px; + margin-bottom: 8px; + + :host-context(.dark) & { + background: rgba(255, 255, 255, 0.04); + } +} + +.syntax-help-footer { + margin: 12px 0 0; + font-size: 0.82rem; + color: var(--on-surface-variant); + + a { + color: var(--primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.syntax-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + + th { + text-align: left; + padding: 6px 12px; + border-bottom: 2px solid rgba(0, 0, 0, 0.1); + color: var(--on-surface); + font-weight: 600; + + :host-context(.dark) & { + border-bottom-color: rgba(255, 255, 255, 0.1); + } + } + + td { + padding: 6px 12px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + color: var(--on-surface); + + :host-context(.dark) & { + border-bottom-color: rgba(255, 255, 255, 0.05); + } + } + + code { + background: color-mix(in srgb, var(--primary) 10%, transparent); + padding: 1px 6px; + border-radius: 3px; + font-size: 0.85em; + } +} + +// Advanced facet grid +.advanced-facets { + margin-top: 16px; +} + +.advanced-facets-title { + margin: 0 0 12px; + font-size: 1rem; + font-weight: 600; + color: var(--on-surface); +} + +.advanced-facet-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} + +.advanced-facet-column { + h4 { + margin: 0 0 8px; + font-size: 0.9rem; + font-weight: 600; + color: var(--primary); + padding-bottom: 6px; + border-bottom: 2px solid var(--primary); + } +} + +.advanced-facet-list { + max-height: 250px; + overflow-y: auto; + padding-right: 4px; +} + +// Advanced search submit button +.advanced-search-btn { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 16px; + padding: 10px 24px; + border: none; + border-radius: $border-radius; + background: var(--primary); + color: var(--on-primary); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + .material-symbols-rounded { + font-size: 18px; + } + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--primary) 90%, black); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + } + + &:active:not(:disabled) { + transform: scale(0.98); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } } // Active filter chips @@ -692,6 +984,10 @@ $border-radius: 8px; .facet-sidebar { width: 100%; } + + .advanced-facet-grid { + grid-template-columns: repeat(2, 1fr); + } } @media (max-width: 768px) { @@ -702,4 +998,8 @@ $border-radius: 8px; .pagination { flex-wrap: wrap; } + + .advanced-facet-grid { + grid-template-columns: 1fr; + } } \ No newline at end of file diff --git a/projects/website-angular/src/app/search/search.component.ts b/projects/website-angular/src/app/search/search.component.ts index 096d69f..d6241ff 100644 --- a/projects/website-angular/src/app/search/search.component.ts +++ b/projects/website-angular/src/app/search/search.component.ts @@ -1,5 +1,6 @@ import { Component, inject, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef, NgZone } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; import { Subscription, forkJoin, catchError, Observable } from 'rxjs'; import { of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; @@ -18,7 +19,7 @@ import { @Component({ selector: 'app-search', standalone: true, - imports: [PageLayoutComponent, TileComponent, RouterLink, SearchBarComponent], + imports: [PageLayoutComponent, TileComponent, RouterLink, SearchBarComponent, FormsModule], templateUrl: './search.component.html', styleUrl: './search.component.scss', }) @@ -60,6 +61,12 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { collapsedFacets: Record = {}; collapsedGroups: Record = {}; + advancedMode = false; + allFacets: FacetResponse | null = null; + advancedQuery = ''; + advancedFilters: SearchFilters = {}; + syntaxHelpOpen = false; + private paramsSub!: Subscription; ngOnInit(): void { @@ -83,6 +90,12 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { keywords: toArray(params['keywords']), }; + if (params['advanced'] === 'true' && !this.advancedMode) { + this.toggleAdvancedMode(); + } else if (params['advanced'] !== 'true' && this.advancedMode) { + this.advancedMode = false; + } + if (this.query) { this.searchSubmitted = true; this.doSearch(); @@ -259,6 +272,61 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { this.collapsedGroups[group] = !this.collapsedGroups[group]; } + toggleAdvancedMode(): void { + this.advancedMode = !this.advancedMode; + if (this.advancedMode) { + this.advancedQuery = this.query || ''; + this.advancedFilters = { + species: [...(this.filters.species || [])], + types: [...(this.filters.types || [])], + compartments: [...(this.filters.compartments || [])], + keywords: [...(this.filters.keywords || [])], + }; + if (!this.allFacets) { + this.searchService.getAllFacets().subscribe({ + next: (facets) => this.allFacets = facets, + error: () => this.allFacets = null, + }); + } + this.updateQueryParams({ advanced: 'true' }); + } else { + this.updateQueryParams({ advanced: null }); + } + } + + toggleAdvancedFacet(category: string, value: string): void { + const key = category as keyof SearchFilters; + const current = this.advancedFilters[key] || []; + const index = current.indexOf(value); + if (index >= 0) { + current.splice(index, 1); + } else { + current.push(value); + } + this.advancedFilters[key] = current; + } + + isAdvancedFacetSelected(category: string, value: string): boolean { + return (this.advancedFilters[category as keyof SearchFilters] || []).includes(value); + } + + submitAdvancedSearch(): void { + if (!this.advancedQuery.trim()) return; + const params: Record = { + q: this.advancedQuery.trim(), + advanced: 'true', + page: null, + }; + for (const key of ['species', 'types', 'compartments', 'keywords'] as const) { + const values = this.advancedFilters[key]; + params[key] = values?.length ? values : null; + } + this.router.navigate([], { + relativeTo: this.route, + queryParams: params, + }); + } + private updateQueryParams(params: Record): void { this.router.navigate([], { relativeTo: this.route, diff --git a/projects/website-angular/src/config/nav-options.json b/projects/website-angular/src/config/nav-options.json index c5442d5..387c019 100644 --- a/projects/website-angular/src/config/nav-options.json +++ b/projects/website-angular/src/config/nav-options.json @@ -257,7 +257,7 @@ }, "advanced-data-search": { "label": "Advanced Data Search", - "link": "/tools/advanced" + "link": "/content/query?advanced=true" }, "site-search": { "label": "Site Search", diff --git a/projects/website-angular/src/services/search.service.ts b/projects/website-angular/src/services/search.service.ts index 0df1dfd..2f51072 100644 --- a/projects/website-angular/src/services/search.service.ts +++ b/projects/website-angular/src/services/search.service.ts @@ -73,6 +73,10 @@ export class SearchService { return this.http.get(`${this.baseUrl}/facet_query`, { params }); } + getAllFacets(): Observable { + return this.http.get(`${this.baseUrl}/facet`); + } + getSuggestedTerms(query: string): Observable { return this.http.get(`${this.baseUrl}/suggest?query=${encodeURIComponent(query)}`); } From 9315493b886fa1d2a4fd2c9bc243d62b1093373b Mon Sep 17 00:00:00 2001 From: el-rabies Date: Thu, 26 Feb 2026 12:25:20 -0500 Subject: [PATCH 2/4] Moved Advance search bar to search-bar-component --- .../search-bar/search-bar.component.html | 184 ++++++++++++- .../search-bar/search-bar.component.scss | 249 ++++++++++++------ .../search/search-bar/search-bar.component.ts | 114 ++++++-- .../src/app/search/search.component.html | 154 +---------- .../src/app/search/search.component.scss | 2 +- .../src/app/search/search.component.ts | 38 ++- 6 files changed, 466 insertions(+), 275 deletions(-) diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.html b/projects/website-angular/src/app/search/search-bar/search-bar.component.html index 870a435..2b2cc8d 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.html +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.html @@ -1,3 +1,4 @@ +@if (!advancedMode) {
- +
+} @else { +
+ + +
+} @if (query && suggestions.length > 0 && showSuggestions) {
    @for (s of suggestions; track s; let i = $index) { -
  • +
} + +@if (advancedMode) { + +} + +@if (syntaxHelpOpen) { + +} + +@if (filters && allFacets) { +
+

Filter by

+
+ @if (getFacetItems(allFacets.speciesFacet); as items) { @if (items.length) { +
+

Species

+
+ @for (item of items; track item.name) { + + } +
+
+ } } @if (getFacetItems(allFacets.typeFacet); as items) { @if (items.length) + { +
+

Types

+
+ @for (item of items; track item.name) { + + } +
+
+ } } @if (getFacetItems(allFacets.compartmentFacet); as items) { @if + (items.length) { +
+

Compartments

+
+ @for (item of items; track item.name) { + + } +
+
+ } } @if (getFacetItems(allFacets.keywordFacet); as items) { @if + (items.length) { +
+

Keywords

+
+ @for (item of items; track item.name) { + + } +
+
+ } } +
+
+ +} + + diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.scss b/projects/website-angular/src/app/search/search-bar/search-bar.component.scss index 2000f65..bf404a9 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.scss +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.scss @@ -1,97 +1,194 @@ +$spacing: 24px; +$border-radius: 8px; + :host { - display: block; - position: relative; - width: 100%; + display: block; + position: relative; + width: 100%; } .search-bar-container { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - margin-bottom: 8px; + display: flex; + align-items: stretch; + justify-content: center; + width: 100%; + margin-bottom: 8px; } .search-input { - flex: 1; - padding: 4px 16px; - border: 2px solid var(--primary); - border-radius: 4px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - font-size: 20px; + flex: 1; + padding: 4px 16px; + border: 2px solid var(--primary); + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + font-size: 20px; +} + +.advanced-input { + flex: 1; + padding: 8px 16px; + border: 2px solid var(--primary); + border-radius: 4px 0 0 4px; + font-size: 20px; + resize: vertical; + line-height: 1.3; } .search-button { - padding: 4px; - border: 2px solid var(--primary); - border-radius: 4px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - background-color: var(--primary); - color: var(--on-primary); - font-size: 20px; - cursor: pointer; + padding: 4px; + border: 2px solid var(--primary); + border-radius: 4px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + background-color: var(--primary); + color: var(--on-primary); + font-size: 20px; + cursor: pointer; } .hide { - display: none; + display: none; } .suggestions-container { - display: block; - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 100; - background: var(--on-primary); - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 0 0 8px 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - overflow: hidden; + display: block; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 100; + background: var(--on-primary); + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0 0 8px 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow: hidden; + + :host-context(.dark) & { + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + ul { + list-style: none; + padding: 0; + margin: 0; + + li { + padding: 8px 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + cursor: pointer; + transition: background 0.1s ease; + + :host-context(.dark) & { + border-bottom-color: rgba(255, 255, 255, 0.08); + } + + &.highlighted { + background: var(--primary); + color: var(--on-primary); + } + + &:last-child { + border-bottom: none; + } + + &:hover { + background: var(--primary); + color: var(--on-primary); + } + + a { + display: block; + width: 100%; + text-decoration: none; + color: inherit; + font-size: 15px; + } + } + } +} + +// Syntax help +.syntax-help-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 0; + border: none; + background: none; + color: var(--on-surface-variant); + font-size: 0.8rem; + cursor: pointer; + margin-top: 8px; + + .material-symbols-rounded { + font-size: 16px; + } + + &:hover { + color: var(--primary); + } +} + +.syntax-help { + background: color-mix(in srgb, var(--primary) 5%, transparent); + border-radius: $border-radius; + padding: 16px; + margin-top: 8px; + margin-bottom: 8px; + + :host-context(.dark) & { + background: rgba(255, 255, 255, 0.04); + } +} + +.syntax-help-footer { + margin: 12px 0 0; + font-size: 0.82rem; + color: var(--on-surface-variant); + + a { + color: var(--primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} + +.syntax-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + + th { + text-align: left; + padding: 6px 12px; + border-bottom: 2px solid rgba(0, 0, 0, 0.1); + color: var(--on-surface); + font-weight: 600; :host-context(.dark) & { - border-color: rgba(255, 255, 255, 0.15); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border-bottom-color: rgba(255, 255, 255, 0.1); } + } + + td { + padding: 6px 12px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + color: var(--on-surface); - ul { - list-style: none; - padding: 0; - margin: 0; - - li { - padding: 8px 16px; - border-bottom: 1px solid rgba(0, 0, 0, 0.06); - cursor: pointer; - transition: background 0.1s ease; - - :host-context(.dark) & { - border-bottom-color: rgba(255, 255, 255, 0.08); - } - - &.highlighted { - background: var(--primary); - color: var(--on-primary); - } - - &:last-child { - border-bottom: none; - } - - &:hover { - background: var(--primary); - color: var(--on-primary); - } - - a { - display: block; - width: 100%; - text-decoration: none; - color: inherit; - font-size: 15px; - } - } + :host-context(.dark) & { + border-bottom-color: rgba(255, 255, 255, 0.05); } -} \ No newline at end of file + } + + code { + background: color-mix(in srgb, var(--primary) 10%, transparent); + padding: 1px 6px; + border-radius: 3px; + font-size: 0.85em; + } +} diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.ts b/projects/website-angular/src/app/search/search-bar/search-bar.component.ts index c6fc30c..138fd15 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.ts +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.ts @@ -9,7 +9,12 @@ import { HostListener, } from '@angular/core'; import { Router } from '@angular/router'; -import { SearchService } from 'projects/website-angular/src/services/search.service'; +import { + FacetCount, + FacetResponse, + SearchFilters, + SearchService, +} from 'projects/website-angular/src/services/search.service'; @Component({ selector: 'app-search-bar', @@ -22,14 +27,24 @@ export class SearchBarComponent implements OnChanges { private router = inject(Router); private searchService = inject(SearchService); @Input() query: string = ''; + @Input() advancedMode = false; + @Input() filters = false; @Output() queryChange = new EventEmitter(); suggestions: string[] = []; highlightedIndex: number = -1; - + syntaxHelpOpen = false; + allFacets: FacetResponse | null = null; + advancedFilters: SearchFilters = {}; showSuggestions = false; + ngOnInit(): void { + if (this.filters) { + this.getAllFacets(); + } + } + onInput(event: Event): void { const value = (event.target as HTMLInputElement).value; this.query = value; @@ -47,14 +62,25 @@ export class SearchBarComponent implements OnChanges { onSubmit(event: Event): void { event.preventDefault(); - // Only submit if the query is not empty or whitespace this.showSuggestions = false; - + const q = this.query.trim(); if (!q) { return; } - this.router.navigate(['/content/query'], { queryParams: { q: q }}); + + const params: Record = { + q: q, + advanced: 'true', + page: null, + }; + + for (const key of ['species', 'types', 'compartments', 'keywords'] as const) { + const values = this.advancedFilters[key]; + params[key] = values?.length ? values : null; + } + + this.router.navigate(['/content/query'], { queryParams: params }); this.highlightedIndex = -1; this.queryChange.emit(q); @@ -80,33 +106,77 @@ export class SearchBarComponent implements OnChanges { return; } + const params: Record = { + q: s, + advanced: 'true', + page: null, + }; + + for (const key of ['species', 'types', 'compartments', 'keywords'] as const) { + const values = this.advancedFilters[key]; + params[key] = values?.length ? values : null; + } + this.highlightedIndex = -1; this.query = s; - - this.router.navigate(['/content/query'], { queryParams: { q: s } }); + + this.router.navigate(['/content/query'], { queryParams: params }); this.queryChange.emit(s); } private getSuggestions(query: string): void { - if (!query) { + if (!query) { + this.suggestions = []; + return; + } + this.searchService.getSuggestedTerms(query).subscribe({ + next: (terms) => { + this.suggestions = terms || []; + }, + error: () => { this.suggestions = []; - return; - } - this.searchService.getSuggestedTerms(query).subscribe({ - next: (terms) => { - this.suggestions = terms || []; - }, - error: () => { - this.suggestions = []; - }, - }); + }, + }); + } + + private getAllFacets(): void { + this.searchService.getAllFacets().subscribe({ + next: (facets) => (this.allFacets = facets), + error: () => (this.allFacets = null), + }); + } + + getFacetItems( + facet: { selected: FacetCount[]; available: FacetCount[] } | undefined + ): FacetCount[] { + if (!facet) return []; + return [...(facet.selected || []), ...(facet.available || [])]; + } + + isAdvancedFacetSelected(category: string, value: string): boolean { + return ( + this.advancedFilters[category as keyof SearchFilters] || [] + ).includes(value); + } + + toggleAdvancedFacet(category: string, value: string): void { + const key = category as keyof SearchFilters; + const current = this.advancedFilters[key] || []; + const index = current.indexOf(value); + if (index >= 0) { + current.splice(index, 1); + } else { + current.push(value); } - + this.advancedFilters[key] = current; + } + @HostListener('window:keydown.arrowdown', ['$event']) onKeyDownArrowDown(event: KeyboardEvent): void { event.preventDefault(); if (this.suggestions.length > 0 && this.showSuggestions) { - this.highlightedIndex = (this.highlightedIndex + 1) % this.suggestions.length; + this.highlightedIndex = + (this.highlightedIndex + 1) % this.suggestions.length; this.query = this.suggestions[this.highlightedIndex]; } } @@ -115,7 +185,9 @@ export class SearchBarComponent implements OnChanges { onKeyDownArrowUp(event: KeyboardEvent): void { event.preventDefault(); if (this.suggestions.length > 0 && this.showSuggestions) { - this.highlightedIndex = (this.highlightedIndex - 1 + this.suggestions.length) % this.suggestions.length; + this.highlightedIndex = + (this.highlightedIndex - 1 + this.suggestions.length) % + this.suggestions.length; this.query = this.suggestions[this.highlightedIndex]; } } diff --git a/projects/website-angular/src/app/search/search.component.html b/projects/website-angular/src/app/search/search.component.html index b25c981..a39305d 100644 --- a/projects/website-angular/src/app/search/search.component.html +++ b/projects/website-angular/src/app/search/search.component.html @@ -15,28 +15,7 @@ @if (advancedMode) { -
-
- -
- " " exact phrase - AND OR NOT boolean - ? * wildcards - "raf map"~4 proximity - ( ) grouping - Full syntax reference -
-
- -
+ } @else { } @@ -424,138 +403,11 @@

Search Reactome

@if (advancedMode) {
- - - - - @if (syntaxHelpOpen) { -
- - - - - - - - - - - - - - -
SyntaxDescriptionExample
"..."Exact phrase match"apoptotic process"
ANDBoth terms must be presentapoptosis AND TP53
OREither term can be presentapoptosis OR autophagy
NOTExclude a termapoptosis NOT cancer
?Single character wildcardte?t
*Multi-character wildcardapopt*
"..."~NProximity search (within N words)"cell death"~3
( )Grouping for complex queries(apoptosis OR autophagy) AND TP53
- -
- } - - @if (allFacets) { -
-

Filter by

-
- @if (getFacetItems(allFacets.speciesFacet); as items) { - @if (items.length) { -
-

Species

-
- @for (item of items; track item.name) { - - } -
-
- } - } - @if (getFacetItems(allFacets.typeFacet); as items) { - @if (items.length) { -
-

Types

-
- @for (item of items; track item.name) { - - } -
-
- } - } - @if (getFacetItems(allFacets.compartmentFacet); as items) { - @if (items.length) { -
-

Compartments

-
- @for (item of items; track item.name) { - - } -
-
- } - } - @if (getFacetItems(allFacets.keywordFacet); as items) { - @if (items.length) { -
-

Keywords

-
- @for (item of items; track item.name) { - - } -
-
- } - } -
-
- } - - +
} @else { } diff --git a/projects/website-angular/src/app/search/search.component.scss b/projects/website-angular/src/app/search/search.component.scss index 37df87a..ed74920 100644 --- a/projects/website-angular/src/app/search/search.component.scss +++ b/projects/website-angular/src/app/search/search.component.scss @@ -683,7 +683,7 @@ $border-radius: 8px; } .empty-search-bar { - max-width: 600px; + max-width: 900px; margin: 0 auto; } diff --git a/projects/website-angular/src/app/search/search.component.ts b/projects/website-angular/src/app/search/search.component.ts index d6241ff..328f96a 100644 --- a/projects/website-angular/src/app/search/search.component.ts +++ b/projects/website-angular/src/app/search/search.component.ts @@ -62,7 +62,7 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { collapsedGroups: Record = {}; advancedMode = false; - allFacets: FacetResponse | null = null; + advancedQuery = ''; advancedFilters: SearchFilters = {}; syntaxHelpOpen = false; @@ -282,33 +282,27 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { compartments: [...(this.filters.compartments || [])], keywords: [...(this.filters.keywords || [])], }; - if (!this.allFacets) { - this.searchService.getAllFacets().subscribe({ - next: (facets) => this.allFacets = facets, - error: () => this.allFacets = null, - }); - } this.updateQueryParams({ advanced: 'true' }); } else { this.updateQueryParams({ advanced: null }); } } - toggleAdvancedFacet(category: string, value: string): void { - const key = category as keyof SearchFilters; - const current = this.advancedFilters[key] || []; - const index = current.indexOf(value); - if (index >= 0) { - current.splice(index, 1); - } else { - current.push(value); - } - this.advancedFilters[key] = current; - } - - isAdvancedFacetSelected(category: string, value: string): boolean { - return (this.advancedFilters[category as keyof SearchFilters] || []).includes(value); - } + // toggleAdvancedFacet(category: string, value: string): void { + // const key = category as keyof SearchFilters; + // const current = this.advancedFilters[key] || []; + // const index = current.indexOf(value); + // if (index >= 0) { + // current.splice(index, 1); + // } else { + // current.push(value); + // } + // this.advancedFilters[key] = current; + // } + + // isAdvancedFacetSelected(category: string, value: string): boolean { + // return (this.advancedFilters[category as keyof SearchFilters] || []).includes(value); + // } submitAdvancedSearch(): void { if (!this.advancedQuery.trim()) return; From 493010185a82d085cade9022f0fe6c19604e7830 Mon Sep 17 00:00:00 2001 From: el-rabies Date: Thu, 26 Feb 2026 13:55:31 -0500 Subject: [PATCH 3/4] feat: Refactor advanced search components and implement dropdown toggle for facets --- .../resources/resources.component.ts | 1 - .../dropdown-toggle.component.html | 13 ++++ .../dropdown-toggle.component.scss | 77 +++++++++++++++++++ .../dropdown-toggle.component.spec.ts | 23 ++++++ .../dropdown-toggle.component.ts | 19 +++++ .../search-bar/search-bar.component.html | 76 +++++++----------- .../search-bar/search-bar.component.scss | 40 ++++++++++ .../search/search-bar/search-bar.component.ts | 3 +- .../src/app/search/search.component.ts | 49 ------------ 9 files changed, 203 insertions(+), 98 deletions(-) create mode 100644 projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.html create mode 100644 projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.scss create mode 100644 projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.spec.ts create mode 100644 projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.ts diff --git a/projects/website-angular/src/app/community/resources/resources.component.ts b/projects/website-angular/src/app/community/resources/resources.component.ts index 6befea3..f5e63cd 100644 --- a/projects/website-angular/src/app/community/resources/resources.component.ts +++ b/projects/website-angular/src/app/community/resources/resources.component.ts @@ -28,7 +28,6 @@ export class ResourcesComponent { next: (html) => { this.entries = this.parseHtml(html); this.loading = false; - console.log(this.entries); }, error: () => { this.error = true; diff --git a/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.html b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.html new file mode 100644 index 0000000..1ca824b --- /dev/null +++ b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.html @@ -0,0 +1,13 @@ +
+ + @if (open) { +
+ +
+ } +
diff --git a/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.scss b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.scss new file mode 100644 index 0000000..fe4f3bc --- /dev/null +++ b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.scss @@ -0,0 +1,77 @@ +$spacing: 24px; +$border-radius: 8px; + +.facet-section { + background: white; + border-radius: $border-radius; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-bottom: 16px; + overflow: hidden; + + :host-context(.dark) & { + background-color: var(--surface); + box-shadow: 0 2px 8px rgba(255, 255, 255, 0.08); + } +} + +.facet-title { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 12px 16px; + border: none; + background: var(--primary); + color: var(--on-primary); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + text-align: left; +} + +.facet-toggle { + font-size: 1.2rem; + line-height: 1; +} + +.facet-options { + padding: 8px 0; + max-height: 300px; + overflow-y: auto; +} + +.facet-option { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 16px; + cursor: pointer; + font-size: 0.85rem; + line-height: 1.4; + + &:hover { + background: rgba(0, 0, 0, 0.04); + + :host-context(.dark) & { + background: rgba(255, 255, 255, 0.04); + } + } + + input[type='checkbox'] { + flex-shrink: 0; + accent-color: var(--primary); + } +} + +.facet-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.facet-count { + color: var(--on-surface-variant); + flex-shrink: 0; +} \ No newline at end of file diff --git a/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.spec.ts b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.spec.ts new file mode 100644 index 0000000..7339d7b --- /dev/null +++ b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DropdownToggleComponent } from './dropdown-toggle.component'; + +describe('DropdownToggleComponent', () => { + let component: DropdownToggleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DropdownToggleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DropdownToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.ts b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.ts new file mode 100644 index 0000000..fca1260 --- /dev/null +++ b/projects/website-angular/src/app/reactome-components/dropdown-toggle/dropdown-toggle.component.ts @@ -0,0 +1,19 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-dropdown-toggle', + imports: [], + templateUrl: './dropdown-toggle.component.html', + styleUrl: './dropdown-toggle.component.scss' +}) +export class DropdownToggleComponent { + @Input() name: string = ''; + @Output() toggleEvent = new EventEmitter(); + + open = true; + + toggle() { + this.open = !this.open; + this.toggleEvent.emit(this.open); + } +} diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.html b/projects/website-angular/src/app/search/search-bar/search-bar.component.html index 2b2cc8d..c664b90 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.html +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.html @@ -28,9 +28,7 @@ > -} - -@if (query && suggestions.length > 0 && showSuggestions) { +} @if (query && suggestions.length > 0 && showSuggestions) {
    @for (s of suggestions; track s; let i = $index) { @@ -49,19 +47,15 @@ }
-} - -@if (advancedMode) { - -} - -@if (syntaxHelpOpen) { +} @if (syntaxHelpOpen) {
@@ -124,34 +118,31 @@ >.

-} - -@if (filters && allFacets) { +} @if (filters && allFacets) {

Filter by

-
+
@if (getFacetItems(allFacets.speciesFacet); as items) { @if (items.length) { -
-

Species

-
- @for (item of items; track item.name) { - - } -
-
+ + @for (item of items; track item.name) { + + } + } } @if (getFacetItems(allFacets.typeFacet); as items) { @if (items.length) { -
-

Types

-
+ + @for (item of items; track item.name) { } -
-
+ } } @if (getFacetItems(allFacets.compartmentFacet); as items) { @if (items.length) { -
-

Compartments

-
+ @for (item of items; track item.name) { } -
-
+ } } @if (getFacetItems(allFacets.keywordFacet); as items) { @if (items.length) { -
-

Keywords

-
+ @for (item of items; track item.name) { } -
-
+ } }
} - - diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.scss b/projects/website-angular/src/app/search/search-bar/search-bar.component.scss index bf404a9..6913ea3 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.scss +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.scss @@ -192,3 +192,43 @@ $border-radius: 8px; font-size: 0.85em; } } + +.advanced-facets-title { + text-align: left; +} + +.facet-option { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 16px; + cursor: pointer; + font-size: 0.85rem; + line-height: 1.4; + + &:hover { + background: rgba(0, 0, 0, 0.04); + + :host-context(.dark) & { + background: rgba(255, 255, 255, 0.04); + } + } + + input[type='checkbox'] { + flex-shrink: 0; + accent-color: var(--primary); + } +} + +.facet-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.facet-count { + color: var(--on-surface-variant); + flex-shrink: 0; +} \ No newline at end of file diff --git a/projects/website-angular/src/app/search/search-bar/search-bar.component.ts b/projects/website-angular/src/app/search/search-bar/search-bar.component.ts index 138fd15..f08555f 100644 --- a/projects/website-angular/src/app/search/search-bar/search-bar.component.ts +++ b/projects/website-angular/src/app/search/search-bar/search-bar.component.ts @@ -15,11 +15,12 @@ import { SearchFilters, SearchService, } from 'projects/website-angular/src/services/search.service'; +import { DropdownToggleComponent } from "../../reactome-components/dropdown-toggle/dropdown-toggle.component"; @Component({ selector: 'app-search-bar', standalone: true, - imports: [], + imports: [DropdownToggleComponent], templateUrl: './search-bar.component.html', styleUrl: './search-bar.component.scss', }) diff --git a/projects/website-angular/src/app/search/search.component.ts b/projects/website-angular/src/app/search/search.component.ts index 328f96a..c0be74e 100644 --- a/projects/website-angular/src/app/search/search.component.ts +++ b/projects/website-angular/src/app/search/search.component.ts @@ -63,10 +63,6 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { advancedMode = false; - advancedQuery = ''; - advancedFilters: SearchFilters = {}; - syntaxHelpOpen = false; - private paramsSub!: Subscription; ngOnInit(): void { @@ -274,51 +270,6 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { toggleAdvancedMode(): void { this.advancedMode = !this.advancedMode; - if (this.advancedMode) { - this.advancedQuery = this.query || ''; - this.advancedFilters = { - species: [...(this.filters.species || [])], - types: [...(this.filters.types || [])], - compartments: [...(this.filters.compartments || [])], - keywords: [...(this.filters.keywords || [])], - }; - this.updateQueryParams({ advanced: 'true' }); - } else { - this.updateQueryParams({ advanced: null }); - } - } - - // toggleAdvancedFacet(category: string, value: string): void { - // const key = category as keyof SearchFilters; - // const current = this.advancedFilters[key] || []; - // const index = current.indexOf(value); - // if (index >= 0) { - // current.splice(index, 1); - // } else { - // current.push(value); - // } - // this.advancedFilters[key] = current; - // } - - // isAdvancedFacetSelected(category: string, value: string): boolean { - // return (this.advancedFilters[category as keyof SearchFilters] || []).includes(value); - // } - - submitAdvancedSearch(): void { - if (!this.advancedQuery.trim()) return; - const params: Record = { - q: this.advancedQuery.trim(), - advanced: 'true', - page: null, - }; - for (const key of ['species', 'types', 'compartments', 'keywords'] as const) { - const values = this.advancedFilters[key]; - params[key] = values?.length ? values : null; - } - this.router.navigate([], { - relativeTo: this.route, - queryParams: params, - }); } private updateQueryParams(params: Record): void { From f2204e9ee27c63d345abeaebcb8a030f0d3ae2ad Mon Sep 17 00:00:00 2001 From: el-rabies Date: Thu, 26 Feb 2026 14:00:38 -0500 Subject: [PATCH 4/4] chore: removed outdated advance search route --- projects/website-angular/src/app/app.routes.ts | 1 - projects/website-angular/src/config/nav-options.json | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/projects/website-angular/src/app/app.routes.ts b/projects/website-angular/src/app/app.routes.ts index 70bb71b..07838fa 100644 --- a/projects/website-angular/src/app/app.routes.ts +++ b/projects/website-angular/src/app/app.routes.ts @@ -28,7 +28,6 @@ export const routes: Routes = [ //Search Page { path: 'content/advanced', redirectTo: '/content/query?advanced=true' }, - { path: 'tools/advanced', redirectTo: '/content/query?advanced=true' }, { path: 'content/query', loadComponent: () => import('./search/search.component').then(m => m.SearchComponent) }, //404 Page diff --git a/projects/website-angular/src/config/nav-options.json b/projects/website-angular/src/config/nav-options.json index 387c019..0307547 100644 --- a/projects/website-angular/src/config/nav-options.json +++ b/projects/website-angular/src/config/nav-options.json @@ -255,9 +255,9 @@ "label": "ReactomeFIViz", "link": "/tools/reactome-fiviz" }, - "advanced-data-search": { - "label": "Advanced Data Search", - "link": "/content/query?advanced=true" + "search": { + "label": "Search", + "link": "/content/query" }, "site-search": { "label": "Site Search",