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) {
+
+
+
+
+ | Syntax |
+ Description |
+ Example |
+
+
+
+
+ "..." |
+ Exact phrase match |
+ "apoptotic process" |
+
+
+ AND |
+ Both terms must be present |
+ apoptosis AND TP53 |
+
+
+ OR |
+ Either term can be present |
+ apoptosis OR autophagy |
+
+
+ NOT |
+ Exclude a term |
+ apoptosis NOT cancer |
+
+
+ ? |
+ Single character wildcard |
+ te?t |
+
+
+ * |
+ Multi-character wildcard |
+ apopt* |
+
+
+ "..."~N |
+ Proximity search (within N words) |
+ "cell death"~3 |
+
+
+ ( ) |
+ Grouping for complex queries |
+ (apoptosis OR autophagy) AND TP53 |
+
+
+
+
+
+} @if (filters && allFacets) {
+
+
}
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.
-
-
+
+
+
+
+
+ @if (advancedMode) {
+
+ } @else {
+
+ }
}
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)}`);
}