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(resources): client-side search #223

Merged
merged 11 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions apps/picsa-tools/resources-tool/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

import { ResourceSearchComponent } from './pages/search/search.component';

export const ROUTES_COMMON: Routes = [
{
path: '',
Expand All @@ -15,6 +17,10 @@ export const ROUTES_COMMON: Routes = [
path: 'downloads',
loadChildren: () => import('./pages/downloads/downloads.module').then((m) => m.DownloadsModule),
},
{
path: 'search',
component: ResourceSearchComponent,
},
];
/** Routes only registered in standalone mode */
const ROUTES_STANDALONE: Routes = [{ path: '**', redirectTo: '' }];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { _wait } from '@picsa/utils';
import { RxAttachment, RxDocument } from 'rxdb';

Expand All @@ -20,7 +20,7 @@ export class ResourceItemFileComponent implements OnInit, OnDestroy {
public attachment: RxAttachment<IResourceFile> | undefined;
public fileURI: string;

constructor(private service: ResourcesToolService) {}
constructor(private service: ResourcesToolService, private cdr: ChangeDetectorRef) {}

async ngOnInit() {
await this.service.ready();
Expand All @@ -47,6 +47,7 @@ export class ResourceItemFileComponent implements OnInit, OnDestroy {
}
}
this.attachmentChange.next({ attachment, uri: this.fileURI });
this.cdr.markForCheck();
}

/** Display file in resource link format */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,19 @@ export class ResourceItemLinkComponent {

/** Internal collection urls may have to replace current page id depending on */
private goToCollection(id: string) {
// Route from existing collection
if (this.route.snapshot.paramMap.get('collectionId')) {
this.router.navigate([id], {
return this.router.navigate([id], {
relativeTo: this.route,
});
}
// Route from base to collection
else {
this.router.navigate(['collection', id], {
relativeTo: this.route,
});
// Route from /search
if (this.route.snapshot.routeConfig?.path?.endsWith('/search')) {
return this.router.navigate(['../', 'collection', id], { relativeTo: this.route });
}
// Route from base to collection
return this.router.navigate(['collection', id], {
relativeTo: this.route,
});
}
}
5 changes: 4 additions & 1 deletion apps/picsa-tools/resources-tool/src/app/material.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS, MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
Expand All @@ -13,6 +14,8 @@ import { DomSanitizer } from '@angular/platform-browser';
const Modules = [
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatMenuModule,
MatProgressBarModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export class CollectionComponent implements OnInit, OnDestroy {
const fileDocs = await this.service.dbFiles.findByIds(files).sort('priority').exec();
this.files = this.processDocs(fileDocs);
}

private processDocs(docs: Map<string, RxDocument<any>>) {
const entries = [...docs.values()];
return this.service.filterLocalisedResources(entries).map((d) => d._data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<div class="page-content">
<div class="page-columns" *mobxAutorun>
<!-- <div class="search-container">
<h2>Search</h2>
</div> -->
<div class="content-container">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">{{ 'Collections' | translate }}</h2>
<button mat-stroked-button color="primary" routerLink="search" style="margin-right: 1rem">
<mat-icon>search</mat-icon>
{{ 'Search' | translate }}
</button>
<button mat-stroked-button color="primary" class="downloads-button" routerLink="downloads">
<mat-icon>download</mat-icon>
{{ 'Manage Downloads' | translate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
display: flex;
flex-wrap: wrap;
}
.search-container {
min-width: 320px;
}

.content-container {
flex: 1;
min-width: 320px;
Expand All @@ -16,14 +14,6 @@
row-gap: 1em;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.downloads-button {
height: 36px;
mat-icon {
height: 24px;
width: 24px;
font-size: 24px;
}
}

// .video-resource-container {
// display: none;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<div class="page-content">
<mat-form-field>
<mat-label>{{ 'Search Resources' | translate }}...</mat-label>
<input matInput type="text" [(ngModel)]="query" (input)="onSearchInputChange()" autofocus />
</mat-form-field>
@if(totalResults===0){
<div>
@if(query.length > 2){
<span>{{ 'No results found' | translate }}</span>
} @else {
<span>{{ 'Input at least 3 letters to search' | translate }}</span>
}
</div>
}

<!-- Collection Results -->
@if(searchResults.collection.length > 0){
<h2>{{ 'Collections' | translate }} ({{ searchResults.collection.length }})</h2>
<div class="list-container">
@for(collection of searchResults.collection; track collection.id){
<resource-item-collection [collection]="collection"></resource-item-collection>
}
</div>
}

<!-- File Results -->
@if(searchResults.file.length > 0){
<h2>{{ 'Files' | translate }} ({{ searchResults.file.length }})</h2>
<div class="list-container">
@for(file of searchResults.file; track file.id){
<resource-item-file [resource]="file"></resource-item-file>
}
</div>
}

<!-- Link Results -->
@if(searchResults.link.length > 0){
<h2>{{ 'Links' | translate }} ({{ searchResults.link.length }})</h2>
<div class="list-container">
@for(link of searchResults.link; track link.id){
<resource-item-link [resource]="link"></resource-item-link>
}
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.list-container {
display: grid;
column-gap: 1em;
row-gap: 1em;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResourceSearchComponent } from './search.component';

describe('ResourceSearchComponent', () => {
let component: ResourceSearchComponent;
let fixture: ComponentFixture<ResourceSearchComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ResourceSearchComponent],
}).compileComponents();

fixture = TestBed.createComponent(ResourceSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PicsaTranslateModule } from '@picsa/shared/modules';
import Fuse, { FuseResult, IFuseOptions } from 'fuse.js';

import { ResourcesComponentsModule } from '../../components/components.module';
import { IResourceBase, IResourceCollection, IResourceFile, IResourceLink } from '../../schemas';
import { ResourcesToolService } from '../../services/resources-tool.service';

interface ISearchResultsByType {
collection: IResourceCollection[];
file: IResourceFile[];
link: IResourceLink[];
}

@Component({
selector: 'resource-search-component',
standalone: true,
imports: [CommonModule, FormsModule, ResourcesComponentsModule, PicsaTranslateModule],
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResourceSearchComponent implements OnInit {
query: string = '';

// https://www.fusejs.io/api/options.html
fuseOptions: IFuseOptions<IResourceBase> = {
keys: [
{ name: 'title', weight: 0.7 },
{ name: 'description', weight: 0.3 },
],
includeScore: true,
includeMatches: true,
findAllMatches: true,
minMatchCharLength: 3,
threshold: 0.5,
ignoreLocation: true,
};

fuse: Fuse<IResourceBase>;

searchResults: ISearchResultsByType = { collection: [], file: [], link: [] };

/** Store total number of results across types */
public totalResults?: number;

constructor(private service: ResourcesToolService, private cdr: ChangeDetectorRef) {}

async ngOnInit() {
await this.service.ready();
const fileDocs = await this.service.dbFiles.find().exec();
const linkDocs = await this.service.dbLinks.find().exec();
const collectionDocs = await this.service.dbCollections.find().exec();
const allResources = [...fileDocs, ...linkDocs, ...collectionDocs].map((doc) => doc._data);
// TODO - add support for translations
this.fuse = new Fuse(allResources, this.fuseOptions);
}

onSearchInputChange() {
// Only display search results if user has typed more than 2 characters
if (this.query.length > 2) {
const searchResults = this.fuse.search(this.query);
this.setSearchResultsByType(searchResults);
this.totalResults = searchResults.length;
} else {
this.searchResults = { collection: [], file: [], link: [] };
this.totalResults = undefined;
}
this.cdr.markForCheck();
}

private setSearchResultsByType(results: FuseResult<IResourceBase>[]) {
const searchResults: ISearchResultsByType = { collection: [], file: [], link: [] };
for (const result of results) {
if (result.item.type === 'collection') {
searchResults.collection.push(result.item as IResourceCollection);
}
if (result.item.type === 'file') {
searchResults.file.push(result.item as IResourceFile);
}
if (result.item.type === 'link') {
searchResults.link.push(result.item as IResourceLink);
}
}
this.searchResults = searchResults;
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"fast-xml-parser": "^4.2.2",
"firebase": "^9.16.0",
"form-data": "^4.0.0",
"fuse.js": "^7.0.0",
"glob": "^8.1.0",
"hls.js": "^1.4.12",
"html2canvas": "^1.4.1",
Expand Down
Loading
Loading