Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add filter terms by label #444

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions api/src/controllers/term.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,44 @@ export default class TermController {
};
}

@Get('/filter-by-label')
@ApiOperation({ summary: 'Filter terms by a single label' })
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ListProjectTermsResponse })
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Bad request' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Project not found' })
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
async findByLabel(@Req() req, @Param('projectId') projectId: string, @Query('labelId') labelId: string) {
const user = this.auth.getRequestUserOrClient(req);
const membership = await this.auth.authorizeProjectAction(user, projectId, ProjectAction.ViewTerm);

if (!labelId) {
return {
data: [],
message: 'No label provided for filtering',
};
}

const terms = await this.termRepo
.createQueryBuilder('term')
.leftJoinAndSelect('term.labels', 'label')
.leftJoinAndSelect('term.project', 'project')
.where('term.project.id = :projectId', { projectId: membership.project.id })
.andWhere('label.id = :labelId', { labelId })
.orderBy('term.value', 'ASC')
.getMany();

const data = terms.map(t => ({
id: t.id,
value: t.value,
context: t.context,
labels: t.labels,
date: t.date,
}));

return {
data,
};
}
@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Add a new project term' })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.filter-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
}


.label-filter {
display: flex;
flex-direction: column;
gap: 0.5rem;
}




.label-filter__select {
position: relative;
}

select {
appearance: none;
-webkit-appearance: none;
width: 100%;
font-size: 1.15rem;
padding: 0.3em 6em 0.3em 1em;
height: calc(1.5em + 0.5rem + 2px);
}

.label-filter__select::before,
.label-filter__select::after {
--size: 0.3rem;
content: "";
position: absolute;
right: 1rem;
pointer-events: none;
}

.label-filter__select::before {
border-left: var(--size) solid transparent;
border-right: var(--size) solid transparent;
border-bottom: var(--size) solid black;
top: 40%;
}

.label-filter__select::after {
border-left: var(--size) solid transparent;
border-right: var(--size) solid transparent;
border-top: var(--size) solid black;
top: 55%;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@
<div *ngIf="project$ | async as project" class="h-100">
<div class="d-flex mb-4 justify-content-between align-items-center section-title-row">
<h5 class="font-serif m-0">Terms</h5>
<app-new-term *ngIf="true | can: 'AddTerm' | async" [project]="project"></app-new-term>
<div class="filter-container">
<div class="label-filter">
<div class="label-filter__select">
<select id="labelFilter" class="form-control" [formControl]="labelFilterControl">
<option value="" selected>Filter by Label</option>
<option *ngFor="let label of projectLabels$ | async" [value]="label.id">
{{ label.value }}
</option>
</select>
</div>
</div>
<app-new-term *ngIf="true | can: 'AddTerm' | async" [project]="project"></app-new-term>
</div>
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
</div>

<app-search [items]="projectTerms$" [key]="searchKey" [trackBy]="trackElement">
<app-search [items]="filteredTerms$" [key]="searchKey" [trackBy]="trackElement">
<ng-template #searchResultsItem let-term="result">
<div class="card mb-3">
<div class="card-body">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators';
import { Project } from '../../models/project';
import { Term } from '../../models/term';
import { ProjectsState } from '../../stores/projects.state';
import { ClearMessages, CreateTerm, DeleteTerm, GetTerms, TermsState, UpdateTerm } from '../../stores/terms.state';
import { LabelTerm, UnlabelTerm, ProjectLabelState, GetProjectLabels } from '../../stores/project-label.state';
import { Label } from '../../models/label';
import { FormControl } from '@angular/forms';
import { ProjectTermsService } from '../../services/terms.service';

@Component({
selector: 'app-terms-list',
Expand All @@ -32,6 +34,10 @@ export class TermsListComponent implements OnInit, OnDestroy {

newValue = '';

labelFilterControl = new FormControl('');

filteredTerms$: Observable<Term[]>;

page = 0;
pageSize = 10;

Expand Down Expand Up @@ -77,12 +83,24 @@ export class TermsListComponent implements OnInit, OnDestroy {
return [item.value, ...item.labels.map(v => v.value)].join('').toLowerCase();
};

constructor(private store: Store) {}
constructor(
private store: Store,
private projectTermsService: ProjectTermsService,
) {}

ngOnInit() {
this.sub = this.project$
.pipe(
tap(project => {
if (project) {
this.filteredTerms$ = this.labelFilterControl.valueChanges.pipe(
startWith(''),
debounceTime(300),
switchMap((selectedLabel: string) =>
selectedLabel ? this.projectTermsService.fetchFilteredTerms(project.id, selectedLabel) : this.projectTerms$,
),
);
}
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
this.store.dispatch(new GetTerms(project.id));
this.store.dispatch(new GetProjectLabels(project.id));
}),
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/app/projects/services/terms.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export class ProjectTermsService {
return this.http.get<Payload<Term[]>>(`${this.endpoint}/projects/${projectId}/terms`).pipe(map(res => res.data));
}

fetchFilteredTerms(projectId: string, labelId: string): Observable<Term[]> {
const url = `/api/v1/projects/${projectId}/terms`;
return this.http
.get<Payload<Term[]>>(`${this.endpoint}/projects/${projectId}/terms/filter-by-label?labelId=${labelId}`)
.pipe(map(res => res.data));
}
samuelmbabhazi marked this conversation as resolved.
Show resolved Hide resolved
create(projectId: string, value: string, context?: string | null): Observable<Term> {
return this.http.post<Payload<Term>>(`${this.endpoint}/projects/${projectId}/terms`, { value, context }).pipe(map(res => res.data));
}
Expand Down