Add support for full-text search (#7)
This commit is contained in:
parent
6297f9d0f0
commit
28d6986eea
@ -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
5
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 ) {
|
||||
|
@ -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 {}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
36
src/app/components/search/Search.component.html
Normal file
36
src/app/components/search/Search.component.html
Normal 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>
|
54
src/app/components/search/Search.component.scss
Normal file
54
src/app/components/search/Search.component.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
85
src/app/components/search/Search.component.ts
Normal file
85
src/app/components/search/Search.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user