Add support for full-text search (#7)
This commit is contained in:
parent
6297f9d0f0
commit
28d6986eea
@ -37,6 +37,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
{
|
||||||
|
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"input": "src/theme/variables.scss"
|
"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": {
|
"@ionic-native/core": {
|
||||||
"version": "5.28.0",
|
"version": "5.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.28.0.tgz",
|
"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/platform-browser-dynamic": "~10.1.5",
|
||||||
"@angular/router": "~10.1.5",
|
"@angular/router": "~10.1.5",
|
||||||
"@circlon/angular-tree-component": "^10.0.0",
|
"@circlon/angular-tree-component": "^10.0.0",
|
||||||
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
"@ionic-native/core": "^5.0.0",
|
"@ionic-native/core": "^5.0.0",
|
||||||
"@ionic-native/splash-screen": "^5.0.0",
|
"@ionic-native/splash-screen": "^5.0.0",
|
||||||
"@ionic-native/status-bar": "^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 {AlertController, ModalController, Platform, PopoverController, LoadingController} from '@ionic/angular';
|
||||||
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
|
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 {OptionMenuComponent} from './components/option-menu/option-menu.component';
|
||||||
import {SelectorComponent} from './components/sharing/selector/selector.component';
|
import {SelectorComponent} from './components/sharing/selector/selector.component';
|
||||||
import {SessionService} from './service/session.service';
|
import {SessionService} from './service/session.service';
|
||||||
|
import {SearchComponent} from "./components/search/Search.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -67,6 +68,7 @@ export class AppComponent implements OnInit {
|
|||||||
|
|
||||||
public darkMode = false;
|
public darkMode = false;
|
||||||
protected loader?: any;
|
protected loader?: any;
|
||||||
|
protected hasSearchOpen = false;
|
||||||
|
|
||||||
protected initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
protected initialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
@ -115,6 +117,7 @@ export class AppComponent implements OnInit {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
toggleDark: () => this.toggleDark(),
|
toggleDark: () => this.toggleDark(),
|
||||||
isDark: () => this.isDark(),
|
isDark: () => this.isDark(),
|
||||||
|
showSearch: () => this.handleKeyboardEvent(),
|
||||||
}
|
}
|
||||||
}).then(popover => popover.present());
|
}).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) {
|
async onNodeMenuClick($event) {
|
||||||
let canManage = this.menuTarget.data.level === 'manage';
|
let canManage = this.menuTarget.data.level === 'manage';
|
||||||
if ( !canManage ) {
|
if ( !canManage ) {
|
||||||
|
@ -24,6 +24,7 @@ import {DatetimeEditorComponent} from './editor/database/editors/datetime/dateti
|
|||||||
import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component';
|
import {DatetimeRendererComponent} from './editor/database/renderers/datetime-renderer.component';
|
||||||
import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component';
|
import {CurrencyRendererComponent} from './editor/database/renderers/currency-renderer.component';
|
||||||
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
|
import {BooleanRendererComponent} from './editor/database/renderers/boolean-renderer.component';
|
||||||
|
import {SearchComponent} from './search/Search.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -47,6 +48,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
|
|||||||
DatetimeRendererComponent,
|
DatetimeRendererComponent,
|
||||||
CurrencyRendererComponent,
|
CurrencyRendererComponent,
|
||||||
BooleanRendererComponent,
|
BooleanRendererComponent,
|
||||||
|
SearchComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -76,6 +78,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
|
|||||||
DatetimeRendererComponent,
|
DatetimeRendererComponent,
|
||||||
CurrencyRendererComponent,
|
CurrencyRendererComponent,
|
||||||
BooleanRendererComponent,
|
BooleanRendererComponent,
|
||||||
|
SearchComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
HostComponent,
|
HostComponent,
|
||||||
@ -98,6 +101,7 @@ import {BooleanRendererComponent} from './editor/database/renderers/boolean-rend
|
|||||||
DatetimeRendererComponent,
|
DatetimeRendererComponent,
|
||||||
CurrencyRendererComponent,
|
CurrencyRendererComponent,
|
||||||
BooleanRendererComponent,
|
BooleanRendererComponent,
|
||||||
|
SearchComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ComponentsModule {}
|
export class ComponentsModule {}
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
<ion-list>
|
<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-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-label>Export to HTML Site</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button (click)="onSelect('toggle_darkmode')">
|
<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-label>{{ isDark() ? 'To The Light!' : 'Go Dark...' }}</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button (click)="onSelect('logout')">
|
<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-label>Logout</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {ApiService} from '../../service/api.service';
|
import {ApiService} from '../../service/api.service';
|
||||||
|
import {PopoverController} from '@ionic/angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-option-picker',
|
selector: 'app-option-picker',
|
||||||
@ -10,10 +11,12 @@ import {ApiService} from '../../service/api.service';
|
|||||||
export class OptionPickerComponent implements OnInit {
|
export class OptionPickerComponent implements OnInit {
|
||||||
@Input() toggleDark: () => void;
|
@Input() toggleDark: () => void;
|
||||||
@Input() isDark: () => boolean;
|
@Input() isDark: () => boolean;
|
||||||
|
@Input() showSearch: () => void | Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected api: ApiService,
|
protected api: ApiService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected popover: PopoverController,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
@ -25,6 +28,9 @@ export class OptionPickerComponent implements OnInit {
|
|||||||
window.location.href = '/auth/logout';
|
window.location.href = '/auth/logout';
|
||||||
} else if ( key === 'toggle_darkmode' ) {
|
} else if ( key === 'toggle_darkmode' ) {
|
||||||
this.toggleDark();
|
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-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-row align-items-center>
|
||||||
<ion-col>Hi, there! Select or create a page to get started.</ion-col>
|
<ion-col>Hi, there! Select or create a page to get started.</ion-col>
|
||||||
</ion-row>
|
</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>
|
</ion-grid>
|
@ -28,6 +28,21 @@
|
|||||||
@import '~@circlon/angular-tree-component/css/angular-tree-component.css';
|
@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-grid.css";
|
||||||
@import "~ag-grid-community/dist/styles/ag-theme-balham.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 {
|
div.picker-wrapper {
|
||||||
border: 2px solid lightgrey !important;
|
border: 2px solid lightgrey !important;
|
||||||
|
Loading…
Reference in New Issue
Block a user