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
+
+
+
+
+ -
+ {{ filter.value }}
+
+
+
+
+
+
+
+
+ {{ 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
+ };
+ }
+}