diff --git a/projects/website-angular/src/app/app.routes.ts b/projects/website-angular/src/app/app.routes.ts index 230cecb..07838fa 100644 --- a/projects/website-angular/src/app/app.routes.ts +++ b/projects/website-angular/src/app/app.routes.ts @@ -27,6 +27,7 @@ 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: 'content/query', loadComponent: () => import('./search/search.component').then(m => m.SearchComponent) }, //404 Page 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 870a435..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 @@ -1,3 +1,4 @@ +@if (!advancedMode) {
- +
- -@if (query && suggestions.length > 0 && showSuggestions) { +} @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) { + + @for (item of items; track item.name) { + + } + + } } @if (getFacetItems(allFacets.typeFacet); as items) { @if (items.length) + { + + + @for (item of items; track item.name) { + + } + + } } @if (getFacetItems(allFacets.compartmentFacet); as items) { @if + (items.length) { + + @for (item of items; track item.name) { + + } + + } } @if (getFacetItems(allFacets.keywordFacet); as items) { @if + (items.length) { + + @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..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 @@ -1,97 +1,234 @@ +$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); } + } + + code { + background: color-mix(in srgb, var(--primary) 10%, transparent); + padding: 1px 6px; + border-radius: 3px; + 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 c6fc30c..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 @@ -9,12 +9,18 @@ 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'; +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', }) @@ -22,14 +28,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 +63,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 +107,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 +186,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 4032000..a39305d 100644 --- a/projects/website-angular/src/app/search/search.component.html +++ b/projects/website-angular/src/app/search/search.component.html @@ -3,7 +3,22 @@ @if (query && searchSubmitted) {
- +
+ + +
+ + @if (advancedMode) { + + } @else { + + }
@@ -374,9 +389,27 @@

{{ 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..ed74920 100644 --- a/projects/website-angular/src/app/search/search.component.scss +++ b/projects/website-angular/src/app/search/search.component.scss @@ -335,10 +335,302 @@ $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 .active-filters { display: flex; @@ -391,7 +683,7 @@ $border-radius: 8px; } .empty-search-bar { - max-width: 600px; + max-width: 900px; margin: 0 auto; } @@ -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..c0be74e 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,8 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { collapsedFacets: Record = {}; collapsedGroups: Record = {}; + advancedMode = false; + private paramsSub!: Subscription; ngOnInit(): void { @@ -83,6 +86,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 +268,10 @@ export class SearchComponent implements OnInit, OnDestroy, AfterViewInit { this.collapsedGroups[group] = !this.collapsedGroups[group]; } + toggleAdvancedMode(): void { + this.advancedMode = !this.advancedMode; + } + 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..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": "/tools/advanced" + "search": { + "label": "Search", + "link": "/content/query" }, "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)}`); }