Pull request #283: rip mac style spotlight search

Merge in RED/ui from RED-1991 to master

* commit '488d664d4589f581f8fb714f040ac273443b5a96':
  rip mac style spotlight search
This commit is contained in:
Dan Percic 2021-08-30 07:38:13 +02:00 committed by Timo Bejan
commit dfe1687c7f
10 changed files with 116 additions and 135 deletions

View File

@ -3,6 +3,7 @@
<div class="red-top-bar">
<div class="top-bar-row">
<div *ngIf="!currentUser.isUser" class="menu-placeholder"></div>
<div *ngIf="currentUser.isUser" class="menu visible-lt-lg">
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
<mat-icon svgIcon="red:menu"></mat-icon>
@ -25,11 +26,13 @@
</button>
</mat-menu>
</div>
<div *ngIf="currentUser.isUser" class="menu flex-2 visible-lg breadcrumbs-container">
<a *ngIf="(isDossiersView$ | async) === false" class="breadcrumb back" redactionNavigateLastDossiersScreen>
<mat-icon svgIcon="red:expand"></mat-icon>
{{ 'top-bar.navigation-items.back' | translate }}
</a>
<ng-container *ngIf="isDossiersView$ | async">
<a
[routerLinkActiveOptions]="{ exact: true }"
@ -38,7 +41,9 @@
routerLinkActive="active"
translate="top-bar.navigation-items.dossiers"
></a>
<mat-icon *ngIf="appStateService.activeDossier" svgIcon="red:arrow-right"></mat-icon>
<a
*ngIf="appStateService.activeDossier"
[routerLinkActiveOptions]="{ exact: true }"
@ -48,7 +53,9 @@
>
{{ appStateService.activeDossier.dossierName }}
</a>
<mat-icon *ngIf="appStateService.activeFile" svgIcon="red:arrow-right"></mat-icon>
<a
*ngIf="appStateService.activeFile"
[routerLink]="'/main/dossiers/' + appStateService.activeDossierId + '/file/' + appStateService.activeFile.fileId"
@ -59,22 +66,22 @@
</a>
</ng-container>
</div>
<div class="center logo-wrapper">
<redaction-hidden-action (action)="userPreferenceService.toggleDevFeatures()">
<redaction-logo></redaction-logo>
</redaction-hidden-action>
<div class="app-name">{{ titleService.getTitle() }}</div>
</div>
<div class="menu right flex-2">
<div class="buttons">
<iqser-circle-button
(action)="openSpotlightSearch()"
<redaction-spotlight-search
*ngIf="(isSearchScreen$ | async) === false"
[icon]="'iqser:search'"
[tooltip]="'search.header-label' | translate"
tooltipPosition="below"
[placeholder]="'search.placeholder' | translate"
[actions]="searchActions"
iqserHelpMode="search"
></iqser-circle-button>
></redaction-spotlight-search>
<redaction-notifications iqserHelpMode="notifications"></redaction-notifications>
</div>

View File

@ -33,3 +33,7 @@
margin-right: 2px;
}
}
redaction-spotlight-search {
margin-right: 16px !important;
}

View File

@ -6,10 +6,7 @@ import { NavigationStart, Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { SpotlightSearchComponent } from '@components/spotlight-search/spotlight-search.component';
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
import { SpotlightSearchDialogData } from '@components/spotlight-search/spotlight-search-dialog-data';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { distinctUntilChanged, filter, map, startWith } from 'rxjs/operators';
@ -38,7 +35,7 @@ export class BaseScreenComponent {
readonly currentUser = this.userService.currentUser;
readonly isDossiersView$ = this._navigationStart$.pipe(map(isDossiersView));
readonly isSearchScreen$ = this._navigationStart$.pipe(map(isSearchScreen));
readonly userMenuItems: MenuItem[] = [
readonly userMenuItems: readonly MenuItem[] = [
{
name: _('top-bar.navigation-items.my-account.children.my-profile'),
routerLink: '/main/my-profile',
@ -61,6 +58,19 @@ export class BaseScreenComponent {
show: this.currentUser.isManager
}
];
readonly searchActions: readonly SpotlightSearchAction[] = [
{
text: this._translateService.instant('search.this-dossier'),
icon: 'red:enter',
hide: (): boolean => !this.appStateService.activeDossier,
action: (query): void => this._search(query, this.appStateService.activeDossier.dossierId)
},
{
text: this._translateService.instant('search.entire-platform'),
icon: 'red:enter',
action: (query): void => this._search(query)
}
];
constructor(
readonly appStateService: AppStateService,
@ -69,33 +79,9 @@ export class BaseScreenComponent {
readonly titleService: Title,
readonly fileDownloadService: FileDownloadService,
private readonly _router: Router,
private readonly _translateService: TranslateService,
private readonly _dialog: MatDialog
private readonly _translateService: TranslateService
) {}
openSpotlightSearch() {
const spotlightSearchActions: SpotlightSearchAction[] = [
{
text: this._translateService.instant('search.this-dossier'),
icon: 'red:enter',
hide: !this.appStateService.activeDossier,
action: query => this._search(query, this.appStateService.activeDossier.dossierId)
},
{
text: this._translateService.instant('search.entire-platform'),
icon: 'red:enter',
action: query => this._search(query)
}
];
this._dialog.open(SpotlightSearchComponent, {
data: {
actionsConfig: spotlightSearchActions,
placeholder: this._translateService.instant('search.placeholder')
} as SpotlightSearchDialogData
});
}
private _search(query: string, dossierId?: string) {
const queryParams = { query, dossierId };
this._router.navigate(['main/dossiers/search'], { queryParams }).then();

View File

@ -2,5 +2,5 @@ export interface SpotlightSearchAction {
readonly text: string;
readonly action: (query: string) => void;
readonly icon?: string;
readonly hide?: boolean;
readonly hide?: () => boolean;
}

View File

@ -1,6 +0,0 @@
import { SpotlightSearchAction } from './spotlight-search-action';
export interface SpotlightSearchDialogData {
readonly actionsConfig: SpotlightSearchAction[];
readonly placeholder: string;
}

View File

@ -1,28 +1,28 @@
<form [formGroup]="formGroup" class="spotlight-wrapper">
<div class="search d-flex">
<input [placeholder]="data.placeholder" autocomplete="off" class="spotlight-row" formControlName="query" id="query" type="text" />
<iqser-input-with-action
(click)="openMenuIfValue()"
(valueChange)="valueChanges$.next($event)"
[placeholder]="placeholder"
></iqser-input-with-action>
<mat-icon *ngIf="(showActions$ | async) === false" [svgIcon]="'iqser:search'" class="mr-34"></mat-icon>
<mat-menu #menu="matMenu" xPosition="after">
<ng-template matMenuContent>
<div class="wrapper">
<button
(click)="item.action(valueChanges$.getValue())"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon"></mat-icon>
<span>{{ item.text }}</span>
</button>
</div>
</ng-template>
</mat-menu>
<iqser-circle-button
(action)="dialogRef.close()"
*ngIf="showActions$ | async"
class="mr-24"
icon="iqser:close"
></iqser-circle-button>
</div>
<!-- https://material.angular.io/components/menu/overview#toggling-the-menu-programmatically -->
<!-- To toggle menu programmatically a matMenuTriggerFor directive is needed -->
<div [matMenuTriggerFor]="menu"></div>
<div class="divider"></div>
<ng-container *ngIf="showActions$ | async">
<button
(click)="item.action(formGroup.get('query').value); dialogRef.close()"
*ngFor="let item of shownActions; let index = index"
[class.highlight]="(currentActionIdx$ | async) === index"
class="spotlight-row pointer"
>
<mat-icon [svgIcon]="item.icon" class="mr-16"></mat-icon>
<span>{{ item.text }}</span>
</button>
</ng-container>
</form>
<!-- A hack to avoid subscribing in component -->
<ng-container *ngIf="showActions$ | async"></ng-container>

View File

@ -1,50 +1,38 @@
@import '../../../assets/styles/variables';
.spotlight-wrapper {
overflow: hidden;
position: absolute;
top: 15%;
left: 0;
right: 0;
border-radius: 10px;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.3);
}
.spotlight-row {
width: 300px;
display: flex;
align-items: center;
height: 60px;
height: 40px;
text-align: left;
font-size: 16px;
font-weight: 500;
font-size: 13px;
border: none;
outline: none;
color: $accent;
padding: 0 24px;
background-color: $white;
}
.spotlight-row,
.spotlight-wrapper {
width: 750px;
margin: auto;
}
.highlight {
border-radius: 4px;
background-color: $grey-2;
}
.search {
background-color: $white;
align-items: center;
.wrapper {
width: 300px;
}
.divider {
height: 1px;
background-color: rgba(226, 228, 233, 0.9);
::ng-deep .mat-menu-content {
padding: 8px !important;
}
button {
padding: 0;
}
mat-icon {
width: 14px;
margin-left: 13px;
margin-right: 8px;
height: 14px;
width: 14px;
}

View File

@ -1,59 +1,63 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SpotlightSearchDialogData } from '@components/spotlight-search/spotlight-search-dialog-data';
import { ChangeDetectionStrategy, Component, HostListener, Input, ViewChild } from '@angular/core';
import { debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { SpotlightSearchAction } from '@components/spotlight-search/spotlight-search-action';
import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'redaction-spotlight-search',
selector: 'redaction-spotlight-search [actions]',
templateUrl: './spotlight-search.component.html',
styleUrls: ['./spotlight-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpotlightSearchComponent {
private readonly _currentActionIdx$ = new BehaviorSubject(0);
@Input() actions: readonly SpotlightSearchAction[];
@Input() placeholder?: string;
@ViewChild(MatMenuTrigger) private readonly _menuTrigger!: MatMenuTrigger;
private readonly _currentActionIdx$ = new BehaviorSubject(0);
readonly currentActionIdx$ = this._currentActionIdx$.asObservable().pipe(distinctUntilChanged());
readonly shownActions = this.data.actionsConfig.filter(a => !a.hide);
readonly formGroup = this._formBuilder.group({ query: [''] });
readonly showActions$ = this.formGroup.get('query').valueChanges.pipe(
startWith(''),
readonly valueChanges$ = new BehaviorSubject<string>('');
readonly showActions$ = this.valueChanges$.pipe(
debounceTime(300),
map(value => value !== '')
map(value => value !== ''),
tap(show => (show ? this._menuTrigger.openMenu() : this._menuTrigger.closeMenu()))
);
constructor(
private readonly _formBuilder: FormBuilder,
readonly dialogRef: MatDialogRef<SpotlightSearchComponent>,
@Inject(MAT_DIALOG_DATA)
readonly data: SpotlightSearchDialogData
) {}
get shownActions(): readonly SpotlightSearchAction[] {
return this.actions.filter(a => !a.hide?.());
}
@HostListener('document:keydown', ['$event'])
handleKeyDown(event: KeyboardEvent): void {
if (['ArrowDown', 'ArrowUp'].includes(event.code)) {
event.preventDefault();
return event.stopPropagation();
openMenuIfValue(): void {
const value = this.valueChanges$.getValue();
if (value && value !== '') {
this._menuTrigger.openMenu();
}
}
@HostListener('document:keyup', ['$event'])
handleKeyUp(event: KeyboardEvent): void {
if (['ArrowDown', 'ArrowUp'].includes(event.code)) {
event.preventDefault();
event.stopPropagation();
const index = this._currentActionIdx + event.code === 'ArrowDown' ? 1 : -1;
return this._currentActionIdx$.next(this._fixIndexOutOfBound(index));
}
@HostListener('document:keydown.arrowDown', ['$event'])
@HostListener('document:keydown.arrowUp', ['$event'])
handleKeyDown(event: KeyboardEvent): void {
event.preventDefault();
event.stopPropagation();
}
@HostListener('document:keyup.arrowDown', ['$event'])
handleKeyUpArrowDown(event: KeyboardEvent): void {
this.handleKeyDown(event);
const index = this._currentActionIdx + 1;
this._currentActionIdx$.next(index >= this.actions.length ? 0 : index);
}
@HostListener('document:keyup.arrowUp', ['$event'])
handleKeyUpArrowUp(event: KeyboardEvent): void {
this.handleKeyDown(event);
const index = this._currentActionIdx - 1;
this._currentActionIdx$.next(index < 0 ? this.actions.length - 1 : index);
}
private get _currentActionIdx(): number {
return this._currentActionIdx$.getValue();
}
private _fixIndexOutOfBound(index: number): number {
const indexOutOfBound = index < 0 || index >= this.shownActions.length;
return indexOutOfBound ? Math.abs(this.shownActions.length - Math.abs(index)) : index;
}
}

View File

@ -1212,7 +1212,6 @@
},
"search": {
"entire-platform": "",
"header-label": "",
"placeholder": "",
"this-dossier": ""
},

View File

@ -1349,10 +1349,9 @@
"table-header": "{length} search {length, plural, one{result} other{results}}"
},
"search": {
"entire-platform": "entire platform",
"header-label": "Search entire platform",
"placeholder": "Search for documents or document content",
"this-dossier": "in this dossier"
"entire-platform": "all documents",
"placeholder": "Search...",
"this-dossier": "documents in this dossier"
},
"smtp-auth-config": {
"actions": {