Add support for full-text search (#7)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2020-10-13 07:54:14 -05:00
parent 6297f9d0f0
commit 28d6986eea
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
12 changed files with 243 additions and 6 deletions

View File

@ -37,6 +37,9 @@
}
],
"styles": [
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css"
},
{
"input": "src/theme/variables.scss"
},

5
package-lock.json generated
View File

@ -1847,6 +1847,11 @@
}
}
},
"@fortawesome/fontawesome-free": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
},
"@ionic-native/core": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.28.0.tgz",

View File

@ -20,6 +20,7 @@
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5",
"@circlon/angular-tree-component": "^10.0.0",
"@fortawesome/fontawesome-free": "^5.15.1",
"@ionic-native/core": "^5.0.0",
"@ionic-native/splash-screen": "^5.0.0",
"@ionic-native/status-bar": "^5.0.0",

View File

@ -1,4 +1,4 @@
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild, HostListener, Host} from '@angular/core';
import {AlertController, ModalController, Platform, PopoverController, LoadingController} from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
@ -11,6 +11,7 @@ import {OptionPickerComponent} from './components/option-picker/option-picker.co
import {OptionMenuComponent} from './components/option-menu/option-menu.component';
import {SelectorComponent} from './components/sharing/selector/selector.component';
import {SessionService} from './service/session.service';
import {SearchComponent} from "./components/search/Search.component";
@Component({
selector: 'app-root',
@ -67,6 +68,7 @@ export class AppComponent implements OnInit {
public darkMode = false;
protected loader?: any;
protected hasSearchOpen = false;
protected initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@ -115,6 +117,7 @@ export class AppComponent implements OnInit {
componentProps: {
toggleDark: () => this.toggleDark(),
isDark: () => this.isDark(),
showSearch: () => this.handleKeyboardEvent(),
}
}).then(popover => popover.present());
}
@ -134,6 +137,24 @@ export class AppComponent implements OnInit {
}
}
@HostListener('document:keyup.control./', ['$event'])
async handleKeyboardEvent() {
if ( this.hasSearchOpen ) {
return;
}
console.log('firing search');
const modal = await this.modal.create({
component: SearchComponent,
});
this.hasSearchOpen = true;
await modal.present();
await modal.onDidDismiss();
this.hasSearchOpen = false;
}
async onNodeMenuClick($event) {
let canManage = this.menuTarget.data.level === 'manage';
if ( !canManage ) {

View File

@ -24,6 +24,7 @@ import {DatetimeEditorComponent} from './editor/database/editors/datetime/dateti
import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component';
import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component';
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
import {SearchComponent} from './search/Search.component';
@NgModule({
declarations: [
@ -47,6 +48,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
],
imports: [
CommonModule,
@ -76,6 +78,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
],
exports: [
HostComponent,
@ -98,6 +101,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
DatetimeRendererComponent,
CurrencyRendererComponent,
BooleanRendererComponent,
SearchComponent,
]
})
export class ComponentsModule {}

View File

@ -1,14 +1,18 @@
<ion-list>
<ion-item button (click)="onSelect('search_everywhere')">
<i slot="start" class="fa fa-search"></i>
<ion-label>Search Everywhere</ion-label>
</ion-item>
<ion-item button (click)="onSelect('html_export')">
<ion-icon slot="start" name="menu"></ion-icon>
<i slot="start" class="fa fa-code"></i>
<ion-label>Export to HTML Site</ion-label>
</ion-item>
<ion-item button (click)="onSelect('toggle_darkmode')">
<ion-icon slot="start" [name]="isDark() ? 'sun' : 'moon'"></ion-icon>
<i slot="start" class="fa" [ngClass]="isDark() ? 'fa-sun' : 'fa-moon'"></i>
<ion-label>{{ isDark() ? 'To The Light!' : 'Go Dark...' }}</ion-label>
</ion-item>
<ion-item button (click)="onSelect('logout')">
<ion-icon slot="start" name="exit"></ion-icon>
<i slot="start" class="fa fa-sign-out-alt"></i>
<ion-label>Logout</ion-label>
</ion-item>
</ion-list>

View File

@ -1,6 +1,7 @@
import {Component, Input, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {ApiService} from '../../service/api.service';
import {PopoverController} from '@ionic/angular';
@Component({
selector: 'app-option-picker',
@ -10,10 +11,12 @@ import {ApiService} from '../../service/api.service';
export class OptionPickerComponent implements OnInit {
@Input() toggleDark: () => void;
@Input() isDark: () => boolean;
@Input() showSearch: () => void | Promise<void>;
constructor(
protected api: ApiService,
protected router: Router,
protected popover: PopoverController,
) { }
ngOnInit() {}
@ -25,6 +28,9 @@ export class OptionPickerComponent implements OnInit {
window.location.href = '/auth/logout';
} else if ( key === 'toggle_darkmode' ) {
this.toggleDark();
} else if ( key === 'search_everywhere' ) {
this.showSearch();
this.popover.dismiss();
}
}
}

View File

@ -0,0 +1,36 @@
<div class="container">
<ion-header class="search-header">
<ion-input
placeholder="Start typing to search..."
autofocus
#ionInput
class="search-input"
(ionChange)="onSearchChange($event)"
></ion-input>
</ion-header>
<div class="search-results">
<ion-list>
<ion-item
*ngFor="let result of (results | async)"
button
[title]="'Open '+result.type"
(click)="openResult(result)"
>
<ion-label class="search-label" [ngClass]="result.type">
<i class="search-icon" [ngClass]="typeIcons[result.type]"></i>
<div class="search-title">{{ result.short_title }}</div>
</ion-label>
<div
class="search-assoc"
[ngClass]="result.associated.type"
*ngIf="result.associated"
[title]="'Open associated '+result.associated.type"
(click)="openRelated($event, result)"
>
<i class="assoc-icon" [ngClass]="typeIcons[result.associated.type]"></i>
<div class="assoc-title">{{ result.associated.title }}</div>
</div>
</ion-item>
</ion-list>
</div>
</div>

View File

@ -0,0 +1,54 @@
.search-header {
padding: 7px;
.search-input {
font-size: 16pt;
}
}
.search-label {
display: flex;
.search-icon {
margin-right: 10px;
}
&.page {
.search-icon {
color: var(--noded-background-note);
}
}
&.node {
.search-icon {
color: var(--noded-background-node);
content: '\f10d';
}
}
&.db {
.search-icon {
color: var(--noded-background-db);
}
}
}
.search-assoc {
padding: 5px 10px;
border-radius: 4px;
display: flex;
.assoc-icon {
margin-right: 10px;
}
&.page {
background: var(--noded-background-note);
color: var(--noded-color-note);
&:hover {
cursor: pointer;
background: var(--noded-background-note-hover);
}
}
}

View File

@ -0,0 +1,85 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {IonInput, ModalController} from '@ionic/angular';
import {ApiService} from '../../service/api.service';
import {BehaviorSubject} from 'rxjs';
import {Router} from '@angular/router';
export interface SearchResult {
title: string;
short_title: string;
type: 'page' | 'node';
id: string;
associated?: {
title: string,
type: 'page' | 'node',
id: string
};
}
@Component({
selector: 'noded-search-modal',
templateUrl: './Search.component.html',
styleUrls: ['./Search.component.scss'],
})
export class SearchComponent implements OnInit {
@ViewChild('ionInput') ionInput: IonInput;
@Input() query = '';
public results: BehaviorSubject<SearchResult[]> = new BehaviorSubject<SearchResult[]>([]);
public typeIcons = {
node: 'fa fa-quote-left',
page: 'fa fa-sticky-note',
db: 'fa fa-database',
};
constructor(
protected modal: ModalController,
protected api: ApiService,
protected router: Router,
) { }
async dismiss() {
await this.modal.dismiss();
}
ngOnInit() {
setTimeout(() => {
this.ionInput.setFocus();
}, 750);
}
async onSearchChange($event) {
const query = $event.detail.value;
this.results.next(await this.search(query));
}
async search(query): Promise<SearchResult[]> {
return new Promise(resolve => {
this.api.get(`/search?query=${query}`).subscribe(res => {
resolve(res.data.results as SearchResult[]);
});
});
}
async openResult(result: SearchResult) {
if ( result.type === 'page' ) {
await this.router.navigate(['/editor', { id: result.id }]);
await this.dismiss();
} else if ( result.type === 'node' ) {
await this.router.navigate(['/editor', { id: result.associated.id, node_id: result.id }]);
await this.dismiss();
}
}
async openRelated(event, result: SearchResult) {
event.preventDefault();
event.stopPropagation();
if ( result.associated ) {
if ( result.associated.type === 'page' ) {
await this.router.navigate(['/editor', { id: result.associated.id }]);
await this.dismiss();
}
}
}
}

View File

@ -9,8 +9,11 @@
</ion-toolbar>
<ion-grid style="height: 100%; justify-content: center; display: flex; font-size: 24pt; color: #ccc;">
<ion-grid style="height: 100%; justify-content: center; display: flex; flex-direction: column; font-size: 24pt; color: #ccc;">
<ion-row align-items-center>
<ion-col>Hi, there! Select or create a page to get started.</ion-col>
</ion-row>
<ion-row align-items-center style="margin-top: 30px;">
<ion-col>(You can press <code>Ctrl</code> + <code>/</code> to search everywhere.)</ion-col>
</ion-row>
</ion-grid>

View File

@ -28,6 +28,21 @@
@import '~@circlon/angular-tree-component/css/angular-tree-component.css';
@import "~ag-grid-community/dist/styles/ag-grid.css";
@import "~ag-grid-community/dist/styles/ag-theme-balham.css";
@import "~@fortawesome/fontawesome-free/css/all.min.css";
:root {
--noded-background-note: #3A86FF;
--noded-color-note: white;
--noded-background-note-hover: #66a1ff;
--noded-background-db: #8338EC;
--noded-color-db: white;
--noded-background-db-hover: #a873f2;
--noded-background-node: #FB5607;
--noded-color-node: white;
--noded-background-node-hover: #fc864f;
}
div.picker-wrapper {
border: 2px solid lightgrey !important;