Skip to content

Commit

Permalink
global search (#2154)
Browse files Browse the repository at this point in the history
  • Loading branch information
bastianjoel authored May 22, 2023
1 parent 78cf482 commit 962d5d7
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
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`,
templateUrl: `./global-headbar.component.html`,
styleUrls: [`./global-headbar.component.scss`]
})
export class GlobalHeadbarComponent {
public isSearchEnabled = false;
public isSearchEnabled = true;

public get displayName(): string {
if (this.activeMeeting.meeting) {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
<p>global-search works!</p>
<div>
<mat-form-field class="search-field" appearance="outline" floatLabel="never">
<input
matInput
[(ngModel)]="searchTerm"
(change)="searchChange()"
placeholder="{{ 'Search' | translate }}"
autofocus>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>

<section class="filter-section" [formGroup]="currentFilters">
<ul class="filters-list">
<li *ngFor="let filter of availableFilters | keyvalue">
<mat-checkbox formControlName="{{ filter.key }}">{{ filter.value }}</mat-checkbox>
</li>
</ul>
</section>
</div>

<mat-dialog-content class="search-results">
<section *ngFor="let category of results | keyvalue" class="search-results-category">
<h2>
<span
[matBadge]="category.value.length < 100 ? category.value.length : '99+'"
[matBadgeOverlap]="false">{{ availableFilters[category.key] | translate }}</span>
</h2>
<ng-container *ngFor="let result of category.value">
<a
*ngIf="result.fqid.startsWith('mediafile')"
class="search-results-entry"
[href]="result.url"
target="blank"
mat-dialog-close>
<h3 *ngIf="result.title">{{ result.title }}</h3>
</a>
<router-link
*ngIf="!result.fqid.startsWith('mediafile')"
class="search-results-entry"
[routerLink]="result.url"
mat-dialog-close>
<h3 *ngIf="result.title">{{ result.title }}</h3>
<p *ngIf="result.text" [innerHtml]="result.text"></p>
</router-link>
</ng-container>
</section>
</mat-dialog-content>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +31,8 @@ const MODULES = [
MatMenuModule,
MatTooltipModule,
MatDialogModule,
MatCheckboxModule,
MatBadgeModule,
PortalModule
];
const DECLARATIONS = [GlobalHeadbarComponent];
Expand All @@ -41,6 +46,8 @@ const DECLARATIONS = [GlobalHeadbarComponent];
UserComponentsModule,
RouterModule,
ScrollingModule,
FormsModule,
ReactiveFormsModule,
...MODULES
]
})
Expand Down
90 changes: 90 additions & 0 deletions client/src/app/site/services/global-search.service.ts
Original file line number Diff line number Diff line change
@@ -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
};
}
}

0 comments on commit 962d5d7

Please sign in to comment.