diff --git a/client/src/app/site/modules/global-headbar/components/global-headbar/global-headbar.component.ts b/client/src/app/site/modules/global-headbar/components/global-headbar/global-headbar.component.ts index 335bc3aa10..7c26bc3093 100644 --- a/client/src/app/site/modules/global-headbar/components/global-headbar/global-headbar.component.ts +++ b/client/src/app/site/modules/global-headbar/components/global-headbar/global-headbar.component.ts @@ -1,8 +1,10 @@ import { Component } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { ActiveMeetingService } from 'src/app/site/pages/meetings/services/active-meeting.service'; import { OrganizationService } from 'src/app/site/pages/organization/services/organization.service'; import { GlobalHeadbarService } from '../../global-headbar.service'; +import { GlobalSearchComponent } from '../global-search/global-search.component'; @Component({ selector: `os-global-headbar`, @@ -10,7 +12,7 @@ import { GlobalHeadbarService } from '../../global-headbar.service'; styleUrls: [`./global-headbar.component.scss`] }) export class GlobalHeadbarComponent { - public isSearchEnabled = false; + public isSearchEnabled = true; public get displayName(): string { if (this.activeMeeting.meeting) { @@ -25,8 +27,11 @@ export class GlobalHeadbarComponent { public constructor( private activeMeeting: ActiveMeetingService, private orgaService: OrganizationService, + private dialog: MatDialog, public headbarService: GlobalHeadbarService ) {} - public openSearch(): void {} + public openSearch(): void { + this.dialog.open(GlobalSearchComponent); + } } diff --git a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.html b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.html index 9f29757f44..1dce3817a4 100644 --- a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.html +++ b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.html @@ -1 +1,47 @@ -

global-search works!

+
+ + + search + + +
+ +
+
+ + +
+

+ {{ availableFilters[category.key] | translate }} +

+ + +

{{ result.title }}

+
+ +

{{ result.title }}

+

+
+
+
+
diff --git a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.scss b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.scss index e69de29bb2..e48934747e 100644 --- a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.scss +++ b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.scss @@ -0,0 +1,40 @@ +.search-field { + width: 100%; +} + +.search-results { + padding: 0; + + .search-results-category h2 { + padding: 0 24px; + margin-top: 1em; + margin-bottom: 8px; + } + + .search-results-entry { + display: block; + padding: 1em 24px; + cursor: pointer; + text-decoration: none; + > :first-of-type { + margin-top: 0; + } + > :last-of-type { + margin-bottom: 0; + } + } + + .search-results-entry:nth-of-type(2n + 1) { + background-color: #eee; + } +} + +.filters-list { + list-style: none; + padding: 0; + display: flex; + flex-wrap: wrap; + > li { + padding: 0 12px; + } +} diff --git a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.ts b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.ts index 482ff13bb6..97eb9a63c0 100644 --- a/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.ts +++ b/client/src/app/site/modules/global-headbar/components/global-search/global-search.component.ts @@ -1,9 +1,43 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; - +import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { GlobalSearchEntry, GlobalSearchService } from 'src/app/site/services/global-search.service'; @Component({ selector: `os-global-search`, templateUrl: `./global-search.component.html`, styleUrls: [`./global-search.component.scss`], changeDetection: ChangeDetectionStrategy.OnPush }) -export class GlobalSearchComponent {} +export class GlobalSearchComponent { + public searchTerm = ``; + + public readonly availableFilters = { + committee: `Committees`, + meeting: `Meetings`, + motion: `Motions`, + assignment: `Elections`, + mediafile: `Files`, + user: `Participants` + }; + + public currentFilters = this.formBuilder.group( + Object.fromEntries(Object.keys(this.availableFilters).map(field => [field, true])) + ); + + public results: { [key: string]: GlobalSearchEntry[] } = {}; + + public constructor( + private globalSearchService: GlobalSearchService, + private formBuilder: FormBuilder, + private cd: ChangeDetectorRef + ) {} + + public async searchChange() { + this.results = await this.globalSearchService.searchChange( + this.searchTerm, + Object.keys(this.availableFilters).filter( + field => this.currentFilters.get(field) && this.currentFilters.get(field).getRawValue() + ) + ); + this.cd.markForCheck(); + } +} diff --git a/client/src/app/site/modules/global-headbar/global-headbar.module.ts b/client/src/app/site/modules/global-headbar/global-headbar.module.ts index 5fb7ffaf7a..0469cc9396 100644 --- a/client/src/app/site/modules/global-headbar/global-headbar.module.ts +++ b/client/src/app/site/modules/global-headbar/global-headbar.module.ts @@ -2,7 +2,10 @@ import { PortalModule } from '@angular/cdk/portal'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; @@ -28,6 +31,8 @@ const MODULES = [ MatMenuModule, MatTooltipModule, MatDialogModule, + MatCheckboxModule, + MatBadgeModule, PortalModule ]; const DECLARATIONS = [GlobalHeadbarComponent]; @@ -41,6 +46,8 @@ const DECLARATIONS = [GlobalHeadbarComponent]; UserComponentsModule, RouterModule, ScrollingModule, + FormsModule, + ReactiveFormsModule, ...MODULES ] }) diff --git a/client/src/app/site/services/global-search.service.ts b/client/src/app/site/services/global-search.service.ts new file mode 100644 index 0000000000..4dd69f67dd --- /dev/null +++ b/client/src/app/site/services/global-search.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { Fqid } from 'src/app/domain/definitions/key-types'; +import { HttpService } from 'src/app/gateways/http.service'; +import { collectionFromFqid, idFromFqid } from 'src/app/infrastructure/utils/transform-functions'; + +import { ActiveMeetingService } from '../pages/meetings/services/active-meeting.service'; + +export interface GlobalSearchEntry { + title: string; + text: string; + fqid: string; + url?: string; +} + +@Injectable({ + providedIn: `root` +}) +export class GlobalSearchService { + public constructor(private http: HttpService, private activeMeeting: ActiveMeetingService) {} + + public async searchChange( + searchTerm: string, + collections: string[] = [] + ): Promise<{ [key: string]: GlobalSearchEntry[] }> { + const rawResults: { [fqid: string]: any } = await this.http.get(`/system/search`, null, { q: searchTerm }); + let results = Object.keys(rawResults) + .filter(fqid => { + const collection = collectionFromFqid(fqid); + return collections.includes(collection); + }) + .map(fqid => this.getResult(fqid, rawResults[fqid])); + + let collectionMap: { [key: string]: GlobalSearchEntry[] } = {}; + for (let result of results) { + const collection = collectionFromFqid(result.fqid); + if (!collectionMap[collection]) { + collectionMap[collection] = []; + } + + collectionMap[collection].push(result); + } + + return collectionMap; + } + + private getResult(fqid: Fqid, content: any) { + const collection = collectionFromFqid(fqid); + const id = content.sequential_number || idFromFqid(fqid); + let title = content.title || content.name; + let text = content.text || content.description; + let url = ``; + + switch (collection) { + case `committee`: + url = `/committees/${id}`; + break; + case `meeting`: + url = `/${id}`; + break; + case `motion`: + url = `/${content.meeting_id}/motions/${id}`; + break; + case `assignment`: + url = `/${content.meeting_id}/assignments/${id}`; + break; + case `mediafile`: + url = `/system/media/get/${id}`; + break; + case `user`: + const firstName = content.first_name?.trim() || ``; + const lastName = content.last_name?.trim() || ``; + const userName = content.username?.trim() || ``; + const name = firstName || lastName ? `${firstName} ${lastName}` : userName; + title = name?.trim() || ``; + if (this.activeMeeting.meetingId && content.meeting_ids?.includes(this.activeMeeting.meetingId)) { + url = `/${this.activeMeeting.meetingId}/participants/${id}`; + } else { + url = `/accounts/${id}`; + } + break; + } + + return { + title, + text, + fqid, + url + }; + } +}