Skip to content

Commit

Permalink
Merge pull request #1953 from bcgov/feature/ALCS-1874-tags-on-advance…
Browse files Browse the repository at this point in the history
…d-search

Feature/alcs 1874 tags on advanced search
  • Loading branch information
fbarreta authored Nov 5, 2024
2 parents 0076649 + 329bf54 commit b6b0884
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 423 deletions.
1 change: 1 addition & 0 deletions alcs-frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"no-octal-escape": 0,
"overrides": [
{
"files": ["*.ts"],
Expand Down
59 changes: 59 additions & 0 deletions alcs-frontend/src/app/features/search/search.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,65 @@ <h4>File Details</h4>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="column">
<div
class="tag-container"
[ngClass]="{ hovered: hovered, clicked: clicked }"
(mouseleave)="hovered = false"
(mouseenter)="hovered = true"
(click)="onClick()"
>
<mat-form-field class="tag-field" appearance="outline">
<mat-label>Tags</mat-label>
<mat-chip-grid #chipGrid>
<app-tag-chip *ngFor="let tag of tags" (removeClicked)="removeTag(tag)" [tag]="tag"></app-tag-chip>
<input
#tagInput
[matChipInputFor]="chipGrid"
[formControl]="tagControl"
[matAutocomplete]="auto"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(focus)="autoCompleteTrigger.openPanel()"
(click)="clearSearch()"
(blur)="checkDirty()"
/>
@if (tags.length > 0) {
<button class="clear-button" matSuffix mat-icon-button aria-label="Clear" (click)="clearTags()">
<mat-icon>close</mat-icon>
</button>
}
</mat-chip-grid>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="selectTag($event)"
autoActiveFirstOption
class="auto-complete"
>
<mat-option *ngFor="let tag of filteredTags | async" [value]="tag">
{{ tag.name }} <span class="category-label" *ngIf="tag.category"> : {{ tag.category.name }}</span>
<span *ngIf="!tag.isActive" class="inactive-label">&nbsp;Inactive</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
<div class="column">
<ng-select
appearance="outline"
[items]="tagCategories"
appendTo="body"
placeholder="Tag Categories"
bindLabel="name"
bindValue="uuid"
formControlName="tagCategory"
>
<ng-template ng-option-tmp let-item="item" let-search="searchTerm">
<div [ngOptionHighlight]="search">{{ item.name }}</div>
</ng-template>
</ng-select>
</div>
</div>
</div>

<div class="search-fields-wrapper">
Expand Down
17 changes: 17 additions & 0 deletions alcs-frontend/src/app/features/search/search.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ h4 {
margin-top: 48px !important;
}

.category-label {
color: colors.$grey;
}

.inactive-label {
float: right;
}

.tag-field {
width: 100%;
margin-top: 2px;
}

.info-banner {
margin-top: -16px;
background-color: rgba(#F7F7F7, 0.5);
Expand Down Expand Up @@ -36,6 +49,10 @@ h4 {
}
}

::ng-deep .mat-mdc-option .mdc-list-item__primary-text {
width: 100%;
}

:host::ng-deep {
h3,
div,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ToastService } from '../../services/toast/toast.service';

import { SearchComponent } from './search.component';
import { AuthenticationService, ICurrentUser } from '../../services/authentication/authentication.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('SearchComponent', () => {
let component: SearchComponent;
Expand Down Expand Up @@ -76,7 +77,7 @@ describe('SearchComponent', () => {
},
],
declarations: [SearchComponent],
imports: [MatAutocompleteModule],
imports: [MatAutocompleteModule, HttpClientTestingModule],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();

Expand Down
105 changes: 102 additions & 3 deletions alcs-frontend/src/app/features/search/search.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort';
import { MatTabGroup } from '@angular/material/tabs';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import moment from 'moment';
import { Observable, Subject, combineLatestWith, map, startWith, takeUntil } from 'rxjs';
import { Observable, Subject, combineLatestWith, map, of, startWith, takeUntil } from 'rxjs';
import { ApplicationRegionDto } from '../../services/application/application-code.dto';
import { ApplicationLocalGovernmentDto } from '../../services/application/application-local-government/application-local-government.dto';
import { ApplicationLocalGovernmentService } from '../../services/application/application-local-government/application-local-government.service';
Expand All @@ -33,6 +33,12 @@ import { formatDateForApi } from '../../shared/utils/api-date-formatter';
import { FileTypeFilterDropDownComponent } from './file-type-filter-drop-down/file-type-filter-drop-down.component';
import { TableChange } from './search.interface';
import { AuthenticationService, ROLES } from '../../services/authentication/authentication.service';
import { TagCategoryService } from '../../services/tag/tag-category/tag-category.service';
import { TagCategoryDto } from '../../services/tag/tag-category/tag-category.dto';
import { TagDto } from '../../services/tag/tag.dto';
import { TagService } from '../../services/tag/tag.service';
import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { COMMA, ENTER } from '@angular/cdk/keycodes';

export const defaultStatusBackgroundColour = '#ffffff';
export const defaultStatusColour = '#313132';
Expand All @@ -50,6 +56,15 @@ export class SearchComponent implements OnInit, OnDestroy {
@ViewChild('searchResultTabs') tabGroup!: MatTabGroup;
@ViewChild('fileTypeDropDown') fileTypeFilterDropDownComponent!: FileTypeFilterDropDownComponent;
@ViewChild('statusTypeDropDown') portalStatusFilterDropDownComponent!: FileTypeFilterDropDownComponent;
@ViewChild(MatAutocompleteTrigger) autoCompleteTrigger!: MatAutocompleteTrigger;
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement> | undefined;

hovered = false;
clicked = false;
firstClicked = false;
showPlaceholder = false;
separatorKeysCodes: number[] = [ENTER, COMMA];
filteredTags: Observable<TagDto[]> = of([]);

applications: ApplicationSearchResultDto[] = [];
applicationTotal = 0;
Expand All @@ -70,7 +85,7 @@ export class SearchComponent implements OnInit, OnDestroy {
itemsPerPage = 20;
sortDirection: SortDirection = 'desc';
sortField = 'dateSubmitted';

tagControl = new FormControl();
localGovernmentControl = new FormControl<string | undefined>(undefined);
portalStatusControl = new FormControl<string[]>([]);
componentTypeControl = new FormControl<string[] | undefined>(undefined);
Expand All @@ -93,11 +108,16 @@ export class SearchComponent implements OnInit, OnDestroy {
dateSubmittedTo: new FormControl(undefined),
dateDecidedFrom: new FormControl(undefined),
dateDecidedTo: new FormControl(undefined),
tagCategory: new FormControl<string | undefined>(undefined),
tag: new FormControl<string[]>([]),
});
resolutionYears: number[] = [];
localGovernments: ApplicationLocalGovernmentDto[] = [];
filteredLocalGovernments!: Observable<ApplicationLocalGovernmentDto[]>;
regions: ApplicationRegionDto[] = [];
tags: TagDto[] = [];
allTags: TagDto[] = [];
tagCategories: TagCategoryDto[] = [];
applicationStatuses: ApplicationStatusDto[] = [];
allStatuses: (ApplicationStatusDto | NoticeOfIntentStatusDto | NotificationSubmissionStatusDto)[] = [];

Expand All @@ -123,13 +143,17 @@ export class SearchComponent implements OnInit, OnDestroy {
private authService: AuthenticationService,
public fileTypeService: FileTypeDataSourceService,
public portalStatusDataService: PortalStatusDataSourceService,
public tagCategoryService: TagCategoryService,
public tagService: TagService,
) {
this.titleService.setTitle('ALCS | Search');
}

ngOnInit(): void {
this.setup();

this.updateFilteredTags();

this.applicationService.$applicationRegions
.pipe(takeUntil(this.$destroy))
.pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.activatedRoute.queryParamMap))
Expand All @@ -146,6 +170,18 @@ export class SearchComponent implements OnInit, OnDestroy {
}
});

this.tagCategoryService.$categories
.pipe(takeUntil(this.$destroy))
.subscribe((result: { data: TagCategoryDto[]; total: number }) => {
this.tagCategories = result.data;
});

this.tagService.$tags
.pipe(takeUntil(this.$destroy))
.subscribe((result: { data: TagDto[]; total: number }) => {
this.allTags = result.data;
});

this.searchForm.valueChanges.pipe(takeUntil(this.$destroy)).subscribe(() => {
let isEmpty = true;
for (let key in this.searchForm.controls) {
Expand Down Expand Up @@ -189,6 +225,9 @@ export class SearchComponent implements OnInit, OnDestroy {
this.applicationService.setup();
this.loadStatuses();

this.tagCategoryService.fetch(0, 0);
this.tagService.fetch(0, 0);

this.filteredLocalGovernments = this.localGovernmentControl.valueChanges.pipe(
startWith(''),
map((value) => this.filterLocalGovernment(value || '')),
Expand Down Expand Up @@ -296,6 +335,8 @@ export class SearchComponent implements OnInit, OnDestroy {
? formatDateForApi(this.searchForm.controls.dateDecidedTo.value)
: undefined,
fileTypes: fileTypes,
tagCategoryId: this.searchForm.controls.tagCategory.value ?? undefined,
tagIds: this.searchForm.controls.tag.value ?? undefined,
};
}

Expand Down Expand Up @@ -374,6 +415,64 @@ export class SearchComponent implements OnInit, OnDestroy {
this.portalStatusControl.setValue(statusCodes);
}

onClick(): void {
this.clicked = true;
if (!this.firstClicked) {
this.firstClicked = true;
this.tagControl.setValue('');
}
}

removeTag(tag: TagDto): void {
this.tags = this.tags.filter((t) => t.uuid !== tag.uuid)
this.updateFilteredTags();
}

clearTags() {
this.tags = [];
this.updateFilteredTags();
}

clearSearch() {
this.tagInput!.nativeElement.value = '';
}

checkDirty() {
this.tagInput!.nativeElement.value = this.tags.length > 0 ? ' ' : '';
}

selectTag(event: MatAutocompleteSelectedEvent) {
const selectedTag = event.option.value as TagDto;

if (!this.tags.find((tag) => tag.uuid === selectedTag.uuid)) {
this.tags.push(selectedTag);
this.tagInput!.nativeElement.value = ' ';
this.tagControl.setValue('');
this.searchForm.controls.tag.setValue(this.tags.map((t) => t.uuid));
}
}

private filterTags(value: string): TagDto[] {
const filterValue = value.toLowerCase();
return this.allTags.filter(
(tag) => {
if (filterValue) {
return !this.tags.includes(tag) && tag.name.toLowerCase().startsWith(filterValue)
} else {
return !this.tags.includes(tag);
}
}
);
}

private updateFilteredTags(): void {
this.filteredTags = this.tagControl.valueChanges.pipe(
startWith(''),
map((value) => (typeof value === 'string' ? value : value?.name || '')),
map((name) => this.filterTags(name || '')),
);
}

private async loadGovernments() {
const governments = await this.localGovernmentService.list();
this.localGovernments = governments.sort((a, b) => (a.name > b.name ? 1 : -1));
Expand Down
2 changes: 2 additions & 0 deletions alcs-frontend/src/app/features/search/search.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-ta
import { NotificationSearchTableComponent } from './notification-search-table/notification-search-table.component';
import { SearchComponent } from './search.component';
import { InquirySearchTableComponent } from './inquiry-search-table/inquiry-search-table.component';
import { MatChipsModule } from '@angular/material/chips';

const routes: Routes = [
{
Expand All @@ -37,6 +38,7 @@ const routes: Routes = [
MatTabsModule,
MatPaginatorModule,
MatTreeModule,
MatChipsModule,
],
})
export class SearchModule {}
2 changes: 2 additions & 0 deletions alcs-frontend/src/app/services/search/search.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface SearchRequestDto extends PagingRequestDto {
dateDecidedFrom?: number;
dateDecidedTo?: number;
fileTypes: string[];
tagIds?: string[];
tagCategoryId?: string;
}

export interface SearchResultDto {
Expand Down
Loading

0 comments on commit b6b0884

Please sign in to comment.