Merge pull request #4890 from GabrielInTheWorld/freshSearch
Implements the 'global search' and moves the 'global spinner' to 'site.component'
This commit is contained in:
commit
e3a7cbf935
@ -1,4 +1,3 @@
|
||||
<div class="content">
|
||||
<router-outlet></router-outlet>
|
||||
<os-global-spinner></os-global-spinner>
|
||||
</div>
|
||||
|
@ -11,22 +11,30 @@ import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade
|
||||
import { LoadFontService } from './core/ui-services/load-font.service';
|
||||
import { LoginDataService } from './core/ui-services/login-data.service';
|
||||
import { OperatorService } from './core/core-services/operator.service';
|
||||
import { OverlayService } from './core/ui-services/overlay.service';
|
||||
import { PingService } from './core/core-services/ping.service';
|
||||
import { PrioritizeService } from './core/core-services/prioritize.service';
|
||||
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
||||
import { ServertimeService } from './core/core-services/servertime.service';
|
||||
import { SpinnerService } from './core/ui-services/spinner.service';
|
||||
import { ThemeService } from './core/ui-services/theme.service';
|
||||
import { ViewUser } from './site/users/models/view-user';
|
||||
|
||||
/**
|
||||
* Enhance array with own functions
|
||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||
*/
|
||||
declare global {
|
||||
/**
|
||||
* Enhance array with own functions
|
||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||
*/
|
||||
interface Array<T> {
|
||||
flatMap(o: any): any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the number object to calculate real modulo operations.
|
||||
* (not remainder)
|
||||
*/
|
||||
interface Number {
|
||||
modulo(n: number): number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -75,7 +83,7 @@ export class AppComponent {
|
||||
loginDataService: LoginDataService,
|
||||
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
||||
themeService: ThemeService,
|
||||
private spinnerService: SpinnerService,
|
||||
private overlayService: OverlayService,
|
||||
countUsersService: CountUsersService, // Needed to register itself.
|
||||
configService: ConfigService,
|
||||
loadFontService: LoadFontService,
|
||||
@ -95,8 +103,9 @@ export class AppComponent {
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
this.overloadFlatMap();
|
||||
this.overloadModulo();
|
||||
|
||||
// Show the spinner initial
|
||||
spinnerService.setVisibility(true, translate.instant('Loading data. Please wait ...'));
|
||||
|
||||
appRef.isStable
|
||||
.pipe(
|
||||
@ -173,13 +182,23 @@ export class AppComponent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the number object with a real modulo operation (not remainder).
|
||||
* TODO: Remove this, if the remainder operation is changed to modulo.
|
||||
*/
|
||||
private overloadModulo(): void {
|
||||
Number.prototype.modulo = function(n: number): number {
|
||||
return ((this % n) + n) % n;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check if the user is existing and the app is already stable.
|
||||
* If both conditions true, hide the spinner.
|
||||
*/
|
||||
private checkConnectionProgress(): void {
|
||||
if ((this.user || this.operator.isAnonymous) && this.isStable) {
|
||||
this.spinnerService.setVisibility(false);
|
||||
this.overlayService.setSpinner(false, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { environment } from '../environments/environment';
|
||||
import { GlobalSpinnerComponent } from './site/common/components/global-spinner/global-spinner.component';
|
||||
import { LoginModule } from './site/login/login.module';
|
||||
import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module';
|
||||
import { SlidesModule } from './slides/slides.module';
|
||||
@ -28,7 +27,7 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
||||
* Global App Module. Keep it as clean as possible.
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [AppComponent, GlobalSpinnerComponent],
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
|
12
client/src/app/core/ui-services/overlay.service.spec.ts
Normal file
12
client/src/app/core/ui-services/overlay.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OverlayService } from './overlay.service';
|
||||
|
||||
describe('OverlayService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: OverlayService = TestBed.get(OverlayService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
121
client/src/app/core/ui-services/overlay.service.ts
Normal file
121
client/src/app/core/ui-services/overlay.service.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef, ProgressSpinnerMode } from '@angular/material';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { largeDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
import { SuperSearchComponent } from 'app/site/common/components/super-search/super-search.component';
|
||||
|
||||
/**
|
||||
* Optional configuration for the spinner.
|
||||
*/
|
||||
export interface SpinnerConfig {
|
||||
/**
|
||||
* The mode of the spinner. Defaults to `'indeterminate'`
|
||||
*/
|
||||
mode?: ProgressSpinnerMode;
|
||||
/**
|
||||
* The diameter of the svg.
|
||||
*/
|
||||
diameter?: number;
|
||||
/**
|
||||
* The width of the stroke of the spinner.
|
||||
*/
|
||||
stroke?: number;
|
||||
/**
|
||||
* An optional value, if the spinner is in `'determinate'-mode`.
|
||||
*/
|
||||
value?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to control the visibility of components, that overlay the whole window.
|
||||
* Like `global-spinner.component` and `super-search.component`.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OverlayService {
|
||||
/**
|
||||
* Holds the reference to the search-dialog.
|
||||
* Necessary to prevent opening multiple dialogs at once.
|
||||
*/
|
||||
private searchReference: MatDialogRef<SuperSearchComponent> = null;
|
||||
|
||||
/**
|
||||
* Subject, that holds the visibility and message. The component can observe this.
|
||||
*/
|
||||
private spinner: Subject<{ isVisible: boolean; text?: string; config?: SpinnerConfig }> = new Subject();
|
||||
|
||||
/**
|
||||
* Boolean, whether appearing of the spinner should be prevented next time.
|
||||
*/
|
||||
private preventAppearingSpinner: boolean;
|
||||
|
||||
/**
|
||||
* Boolean to indicate, if the spinner has already appeared.
|
||||
*/
|
||||
private spinnerHasAppeared = false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dialogService Injects the `MatDialog` to show the `super-search.component`
|
||||
*/
|
||||
public constructor(private dialogService: MatDialog) {}
|
||||
|
||||
/**
|
||||
* Function to change the visibility of the `global-spinner.component`.
|
||||
*
|
||||
* @param isVisible flag, if the spinner should be shown.
|
||||
* @param text optional. If the spinner should show a message.
|
||||
* @param preventAppearing optional. Wether to prevent showing the spinner the next time.
|
||||
*/
|
||||
public setSpinner(isVisible: boolean, text?: string, preventAppearing?: boolean, config?: SpinnerConfig): void {
|
||||
if (!(this.preventAppearingSpinner && !this.spinnerHasAppeared && isVisible)) {
|
||||
setTimeout(() => this.spinner.next({ isVisible, text, config }));
|
||||
if (isVisible) {
|
||||
this.spinnerHasAppeared = true;
|
||||
}
|
||||
}
|
||||
this.preventAppearingSpinner = preventAppearing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the visibility as observable.
|
||||
*
|
||||
* @returns class member `visibility`.
|
||||
*/
|
||||
public getSpinner(): Observable<{ isVisible: boolean; text?: string; config?: SpinnerConfig }> {
|
||||
return this.spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of the `SuperSearchComponent`.
|
||||
*
|
||||
* @param isVisible If the component should be shown or not.
|
||||
*/
|
||||
public showSearch(data?: any): void {
|
||||
if (!this.searchReference) {
|
||||
this.searchReference = this.dialogService.open(SuperSearchComponent, {
|
||||
...largeDialogSettings,
|
||||
data: data ? data : null,
|
||||
disableClose: false,
|
||||
panelClass: 'super-search-container'
|
||||
});
|
||||
this.searchReference.afterClosed().subscribe(() => {
|
||||
this.searchReference = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to reset the properties for the spinner.
|
||||
*
|
||||
* Necessary to get the initial state, if the user logs out
|
||||
* and still stays at the website.
|
||||
*/
|
||||
public logout(): void {
|
||||
this.spinnerHasAppeared = false;
|
||||
this.preventAppearingSpinner = false;
|
||||
}
|
||||
}
|
@ -8,9 +8,50 @@ import { Searchable } from '../../site/base/searchable';
|
||||
import { ViewModelStoreService } from '../core-services/view-model-store.service';
|
||||
|
||||
/**
|
||||
* The representation every searchable model should use to represent their data.
|
||||
* Defines, how the properties look like
|
||||
*/
|
||||
export type SearchRepresentation = string[];
|
||||
export interface SearchProperty {
|
||||
/**
|
||||
* A string, that contains the specific value.
|
||||
*/
|
||||
key: string | null;
|
||||
|
||||
/**
|
||||
* The value of the property as string.
|
||||
*/
|
||||
value: string | null;
|
||||
|
||||
/**
|
||||
* If some properties should be grouped into one card (for the preview),
|
||||
* they can be unified to `blockProperties`.
|
||||
*/
|
||||
blockProperties?: SearchProperty[];
|
||||
|
||||
/**
|
||||
* A flag to specify, if a value could be rendered `innerHTML`.
|
||||
*/
|
||||
trusted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SearchRepresentation the system looks by.
|
||||
*/
|
||||
export interface SearchRepresentation {
|
||||
/**
|
||||
* The representation every searchable model should use to represent their data.
|
||||
*/
|
||||
searchValue: string[];
|
||||
|
||||
/**
|
||||
* The properties the representation contains.
|
||||
*/
|
||||
properties: SearchProperty[];
|
||||
|
||||
/**
|
||||
* An optional type. This is useful for mediafiles to decide which type they have.
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Our representation of a searchable model for external use.
|
||||
@ -47,7 +88,7 @@ export interface SearchResult {
|
||||
collectionString: string;
|
||||
|
||||
/**
|
||||
* This verbodeName must have the right cardianlity. If there is exactly one model in `models`,
|
||||
* This verboseName must have the right cardianlity. If there is exactly one model in `models`,
|
||||
* it should have a singular value, else a plural name.
|
||||
*/
|
||||
verboseName: string;
|
||||
@ -63,6 +104,21 @@ export interface SearchResult {
|
||||
models: (BaseViewModel & Searchable)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface, that describes a pair of a (translated) value and a relating collection.
|
||||
*/
|
||||
export interface TranslatedCollection {
|
||||
/**
|
||||
* The value
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* The collectionString, the value relates to.
|
||||
*/
|
||||
collection: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service cares about searching the DataStore and managing models, that are searchable.
|
||||
*/
|
||||
@ -137,18 +193,28 @@ export class SearchService {
|
||||
* @param query The search query
|
||||
* @param inCollectionStrings All connection strings which should be used for searching.
|
||||
* @param sortingProperty Sorting by `id` or `title`.
|
||||
* @param dedicatedId Optional parameter. Useful to look for a specific id in the given collectionStrings.
|
||||
*
|
||||
* @returns All search results sorted by the model's title (via `getTitle()`).
|
||||
*/
|
||||
public search(query: string, inCollectionStrings: string[], sortingProperty: 'id' | 'title'): SearchResult[] {
|
||||
public search(
|
||||
query: string,
|
||||
inCollectionStrings: string[],
|
||||
sortingProperty: 'title' | 'id' = 'title',
|
||||
dedicatedId?: number
|
||||
): SearchResult[] {
|
||||
query = query.toLowerCase();
|
||||
return this.searchModels
|
||||
.filter(s => inCollectionStrings.includes(s.collectionString))
|
||||
.filter(s => inCollectionStrings.indexOf(s.collectionString) !== -1)
|
||||
.map(searchModel => {
|
||||
const results = this.viewModelStore
|
||||
.getAll(searchModel.collectionString)
|
||||
.map(x => x as (BaseViewModel & Searchable))
|
||||
.filter(model => model.formatForSearch().some(text => text.toLowerCase().includes(query)))
|
||||
.filter(model =>
|
||||
dedicatedId
|
||||
? model.id === dedicatedId
|
||||
: model.formatForSearch().searchValue.some(text => text.toLowerCase().indexOf(query) !== -1)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (sortingProperty) {
|
||||
case 'id':
|
||||
@ -165,4 +231,32 @@ export class SearchService {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given collections and translates the single values.
|
||||
*
|
||||
* @param collections All the collections, that should be translated.
|
||||
*
|
||||
* @returns {Array} An array containing the single values of the collections and the translated ones.
|
||||
* These values point to the `collectionString` the user can search for.
|
||||
*/
|
||||
public getTranslatedCollectionStrings(): TranslatedCollection[] {
|
||||
const nextCollections: TranslatedCollection[] = this.searchModels.flatMap((model: SearchModel) => [
|
||||
{ value: model.verboseNamePlural, collection: model.collectionString },
|
||||
{ value: model.verboseNameSingular, collection: model.collectionString }
|
||||
]);
|
||||
const tmpCollections = [...nextCollections];
|
||||
for (const entry of tmpCollections) {
|
||||
const translatedValue = this.translate.instant(entry.value);
|
||||
if (!nextCollections.find(item => item.value === translatedValue)) {
|
||||
nextCollections.push({ value: translatedValue, collection: entry.collection });
|
||||
}
|
||||
}
|
||||
const sequentialNumber = 'Sequential number';
|
||||
nextCollections.push(
|
||||
{ value: sequentialNumber, collection: 'motions/motion' },
|
||||
{ value: this.translate.instant(sequentialNumber), collection: 'motions/motion' }
|
||||
);
|
||||
return nextCollections;
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { SpinnerService } from './spinner.service';
|
||||
|
||||
describe('SpinnerService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [SpinnerService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([SpinnerService], (service: SpinnerService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,41 +0,0 @@
|
||||
// External imports
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Service for the `global-spinner.component`
|
||||
*
|
||||
* Handles the visibility of the global-spinner.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SpinnerService {
|
||||
/**
|
||||
* Subject, that holds the visibility and message. The component can observe this.
|
||||
*/
|
||||
private visibility: Subject<{ isVisible: boolean; text?: string }> = new Subject<{
|
||||
isVisible: boolean;
|
||||
text?: string;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Function to change the visibility of the `global-spinner.component`.
|
||||
*
|
||||
* @param isVisible flag, if the spinner should be shown.
|
||||
* @param text optional. If the spinner should show a message.
|
||||
*/
|
||||
public setVisibility(isVisible: boolean, text?: string): void {
|
||||
setTimeout(() => this.visibility.next({ isVisible, text }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the visibility as observable.
|
||||
*
|
||||
* @returns class member `visibility`.
|
||||
*/
|
||||
public getVisibility(): Observable<{ isVisible: boolean; text?: string }> {
|
||||
return this.visibility;
|
||||
}
|
||||
}
|
@ -28,6 +28,11 @@
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<!-- Button to open the global search -->
|
||||
<button *ngIf="!vp.isMobile" mat-icon-button (click)="openSearch()">
|
||||
<mat-icon>search</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<!-- Extra controls slot -->
|
||||
<ng-content select=".extra-controls-slot"></ng-content>
|
||||
|
@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { MainMenuService } from 'app/core/core-services/main-menu.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { RoutingStateService } from 'app/core/ui-services/routing-state.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
|
||||
@ -155,7 +156,8 @@ export class HeadBarComponent implements OnInit {
|
||||
private menu: MainMenuService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private routingState: RoutingStateService
|
||||
private routingState: RoutingStateService,
|
||||
private overlayService: OverlayService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -193,6 +195,13 @@ export class HeadBarComponent implements OnInit {
|
||||
this.saveEvent.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the `super-search.component`.
|
||||
*/
|
||||
public openSearch(): void {
|
||||
this.overlayService.showSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the view to return to the previous page or
|
||||
* visit the parent view again.
|
||||
|
@ -0,0 +1,41 @@
|
||||
<ng-content></ng-content>
|
||||
<div *ngIf="model" [ngSwitch]="model.collectionString" class="preview-container">
|
||||
<div *ngSwitchCase="'mediafiles/mediafile'">
|
||||
<div *ngIf="modelType.includes('image')">
|
||||
<img [src]="model.getDetailStateURL()" />
|
||||
</div>
|
||||
<div *ngIf="modelType.includes('pdf')">
|
||||
<pdf-viewer
|
||||
[original-size]="false"
|
||||
[fit-to-page]="true"
|
||||
[autoresize]="true"
|
||||
[src]="model.getDetailStateURL()"
|
||||
[style.display]="'block'"
|
||||
></pdf-viewer>
|
||||
</div>
|
||||
<div *ngIf="modelType.includes('directory')">
|
||||
<div *ngFor="let entry of formattedSearchValue">
|
||||
<mat-card class="os-card" *ngIf="entry.value !== ''">
|
||||
<div class="key-part">{{ entry.key | translate }}</div>
|
||||
<div>{{ entry.value }}</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngSwitchDefault>
|
||||
<div *ngFor="let entry of formattedSearchValue">
|
||||
<mat-card class="os-card" *ngIf="entry.value !== '' && !entry.blockProperties">
|
||||
<div class="key-part">{{ entry.key | translate }}</div>
|
||||
<div *ngIf="!entry.trusted">{{ entry.value }}</div>
|
||||
<div *ngIf="entry.trusted" [innerHTML]="sanitize(entry.value)"></div>
|
||||
</mat-card>
|
||||
<mat-card class="os-card" *ngIf="entry.blockProperties">
|
||||
<div *ngFor="let property of entry.blockProperties">
|
||||
<div class="key-part">{{ property.key | translate }}</div>
|
||||
<div *ngIf="!property.trusted">{{ property.value }}</div>
|
||||
<div *ngIf="property.trusted" [innerHTML]="sanitize(property.value)"></div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,18 @@
|
||||
.preview-container {
|
||||
padding: 0 8px;
|
||||
|
||||
.key-part {
|
||||
font-size: 11px;
|
||||
color: #666666;
|
||||
|
||||
& + div {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 8px 0;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { PreviewComponent } from './preview.component';
|
||||
|
||||
fdescribe('PreviewComponent', () => {
|
||||
let component: PreviewComponent;
|
||||
let fixture: ComponentFixture<PreviewComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PreviewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,61 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
import { SearchProperty } from 'app/core/ui-services/search.service';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { Searchable } from 'app/site/base/searchable';
|
||||
|
||||
@Component({
|
||||
selector: 'os-preview',
|
||||
templateUrl: './preview.component.html',
|
||||
styleUrls: ['./preview.component.scss']
|
||||
})
|
||||
export class PreviewComponent {
|
||||
/**
|
||||
* Sets the view-model, whose properties are displayed.
|
||||
*
|
||||
* @param model The view-model. Typeof `BaseViewModel & Searchable`.
|
||||
*/
|
||||
@Input()
|
||||
public set viewModel(model: BaseViewModel & Searchable) {
|
||||
if (model) {
|
||||
this.model = model;
|
||||
const representation = model.formatForSearch();
|
||||
this.formattedSearchValue = representation.properties;
|
||||
this.modelType = representation.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The view-model.
|
||||
*/
|
||||
public model: BaseViewModel & Searchable;
|
||||
|
||||
/**
|
||||
* An array of `SearchProperty`. This contains all key-value-pair attributes of the model.
|
||||
*/
|
||||
public formattedSearchValue: SearchProperty[];
|
||||
|
||||
/**
|
||||
* The type of the model. This is only set, if the model is from type 'mediafile'.
|
||||
*/
|
||||
public modelType: string;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param sanitizer DomSanitizer
|
||||
*/
|
||||
public constructor(private sanitizer: DomSanitizer) {}
|
||||
|
||||
/**
|
||||
* Function to sanitize any text to show html.
|
||||
*
|
||||
* @param text The text to sanitize.
|
||||
*
|
||||
* @returns {SafeHtml} The sanitized text as `HTML`.
|
||||
*/
|
||||
public sanitize(text: string): SafeHtml {
|
||||
return this.sanitizer.bypassSecurityTrustHtml(text);
|
||||
}
|
||||
}
|
@ -7,11 +7,12 @@
|
||||
[autofocus]="autofocus"
|
||||
[placeholder]="placeholder"
|
||||
class="rounded-input"
|
||||
[ngClass]="[size]"
|
||||
[ngClass]="[size, borderRadius, hasChildren ? 'children-bottom' : '']"
|
||||
[formControl]="modelForm"
|
||||
(keyup)="keyPressed($event)"
|
||||
(blur)="blur()"
|
||||
/>
|
||||
<div *ngIf="modelForm.value !== ''" class="input-suffix">
|
||||
<mat-icon (mouseup)="clear()">close</mat-icon>
|
||||
<mat-icon (click)="clear()">close</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,47 +1,74 @@
|
||||
.input-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
&,
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
&.input-prefix {
|
||||
left: 8px;
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
:host.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@mixin os-rounded-input-style($theme) {
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
$foreground-color: mat-color($foreground, icon);
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
&,
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
&.input-suffix {
|
||||
right: 8px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.rounded-input {
|
||||
outline: 0;
|
||||
z-index: 0;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
padding: 8px 39px;
|
||||
border-radius: 32px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
color: #666;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&.small {
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
|
||||
&:focus {
|
||||
width: 200px;
|
||||
div {
|
||||
position: absolute;
|
||||
&.input-prefix {
|
||||
left: 8px;
|
||||
}
|
||||
&.input-suffix {
|
||||
right: 8px;
|
||||
color: $foreground-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
.rounded-input {
|
||||
outline: 0;
|
||||
z-index: 0;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
padding: 8px 39px;
|
||||
border-radius: 32px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
background: mat-color($background, background);
|
||||
color: $foreground-color;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&.small {
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
|
||||
&:focus {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium-border-radius {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
&.small-border-radius {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.children-bottom {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,38 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Type declared to see, which values are possible for some inputs.
|
||||
*/
|
||||
export type Size = 'small' | 'medium' | 'large';
|
||||
|
||||
@Component({
|
||||
selector: 'os-rounded-input',
|
||||
templateUrl: './rounded-input.component.html',
|
||||
styleUrls: ['./rounded-input.component.scss']
|
||||
})
|
||||
export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Binds the class to the parent-element.
|
||||
*/
|
||||
@HostBinding('class')
|
||||
public get classes(): string {
|
||||
return this.fullWidth ? 'full-width' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference to the `<input />`-element.
|
||||
*/
|
||||
@ -45,7 +68,13 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
* Defaults to `'medium'`.
|
||||
*/
|
||||
@Input()
|
||||
public size: 'small' | 'medium' | 'large' = 'medium';
|
||||
public size: Size = 'medium';
|
||||
|
||||
/**
|
||||
* Whether this component should render over the full width.
|
||||
*/
|
||||
@Input()
|
||||
public fullWidth = true;
|
||||
|
||||
/**
|
||||
* Custom `FormControl`.
|
||||
@ -59,6 +88,12 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
public autofocus = false;
|
||||
|
||||
/**
|
||||
* Boolean, whether the input should keep the focus, even if it loses the focus.
|
||||
*/
|
||||
@Input()
|
||||
public keepFocus = false;
|
||||
|
||||
/**
|
||||
* Boolean, whether the input should fire the value-change-event after a specific time.
|
||||
*/
|
||||
@ -77,6 +112,20 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
public clearOnEscape = true;
|
||||
|
||||
/**
|
||||
* Boolean to indicate, whether the input should have rounded borders at the bottom or not.
|
||||
*/
|
||||
@Input()
|
||||
public hasChildren = false;
|
||||
|
||||
/**
|
||||
* Boolean to indicate, whether the borders should be rounded with a smaller size.
|
||||
*/
|
||||
@Input()
|
||||
public set typeBorderRadius(radius: Size) {
|
||||
this._borderRadius = radius + '-border-radius';
|
||||
}
|
||||
|
||||
/**
|
||||
* EventHandler for the input-changes.
|
||||
*/
|
||||
@ -89,11 +138,24 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
@Output()
|
||||
public onkeyup: EventEmitter<KeyboardEvent> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Getter to get the border-radius as a string.
|
||||
*
|
||||
* @returns {string} The border-radius as class.
|
||||
*/
|
||||
public get borderRadius(): string {
|
||||
return this._borderRadius;
|
||||
}
|
||||
/**
|
||||
* Subscription, that will handle the value-changes of the input.
|
||||
*/
|
||||
private subscription: Subscription;
|
||||
|
||||
/**
|
||||
* Variable for the border-radius as class.
|
||||
*/
|
||||
private _borderRadius = 'large-border-radius';
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
@ -107,6 +169,9 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
* Overwrites `OnInit` - initializes the subscription.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
if (this.autofocus) {
|
||||
this.focus();
|
||||
}
|
||||
this.subscription = this.modelForm.valueChanges
|
||||
.pipe(debounceTime(this.lazyInput ? 250 : 0))
|
||||
.subscribe(nextValue => {
|
||||
@ -128,10 +193,26 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
||||
* Function to clear the input and refocus it.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.osInput.nativeElement.focus();
|
||||
this.focus();
|
||||
this.modelForm.setValue('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to programmatically focus the input.
|
||||
*/
|
||||
public focus(): void {
|
||||
this.osInput.nativeElement.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called, if the input loses its focus.
|
||||
*/
|
||||
public blur(): void {
|
||||
if (this.keepFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle typing.
|
||||
* Useful to listen to special keys.
|
||||
|
@ -12,6 +12,9 @@
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
os-rounded-input {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.active-filter {
|
||||
@ -74,7 +77,3 @@ span.right-with-margin {
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
os-rounded-input {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
|
||||
public title: string;
|
||||
public mediafile?: FileMetadata;
|
||||
public media_url_prefix: string;
|
||||
public filesize: string;
|
||||
public filesize?: string;
|
||||
public access_groups_id: number[];
|
||||
public create_timestamp: string;
|
||||
public parent_id: number | null;
|
||||
|
@ -97,6 +97,11 @@ import { ExtensionFieldComponent } from './components/extension-field/extension-
|
||||
import { AttachmentControlComponent } from './components/attachment-control/attachment-control.component';
|
||||
import { RoundedInputComponent } from './components/rounded-input/rounded-input.component';
|
||||
import { ProgressSnackBarComponent } from './components/progress-snack-bar/progress-snack-bar.component';
|
||||
import { SuperSearchComponent } from 'app/site/common/components/super-search/super-search.component';
|
||||
import { OverlayComponent } from 'app/site/common/components/overlay/overlay.component';
|
||||
import { PreviewComponent } from './components/preview/preview.component';
|
||||
import { PdfViewerModule } from 'ng2-pdf-viewer';
|
||||
import { GlobalSpinnerComponent } from 'app/site/common/components/global-spinner/global-spinner.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -157,7 +162,8 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
||||
ScrollingModule,
|
||||
PblNgridModule,
|
||||
PblNgridMaterialModule,
|
||||
PblNgridTargetEventsModule
|
||||
PblNgridTargetEventsModule,
|
||||
PdfViewerModule
|
||||
],
|
||||
exports: [
|
||||
FormsModule,
|
||||
@ -197,6 +203,7 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
||||
NgxFileDropModule,
|
||||
TranslateModule,
|
||||
OpenSlidesTranslateModule,
|
||||
PdfViewerModule,
|
||||
PermsDirective,
|
||||
IsSuperAdminDirective,
|
||||
DomChangeDirective,
|
||||
@ -236,7 +243,10 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
||||
ListViewTableComponent,
|
||||
AgendaContentObjectFormComponent,
|
||||
ExtensionFieldComponent,
|
||||
RoundedInputComponent
|
||||
RoundedInputComponent,
|
||||
GlobalSpinnerComponent,
|
||||
OverlayComponent,
|
||||
PreviewComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -276,7 +286,11 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
||||
ExtensionFieldComponent,
|
||||
AttachmentControlComponent,
|
||||
RoundedInputComponent,
|
||||
ProgressSnackBarComponent
|
||||
ProgressSnackBarComponent,
|
||||
GlobalSpinnerComponent,
|
||||
SuperSearchComponent,
|
||||
OverlayComponent,
|
||||
PreviewComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
@ -294,7 +308,8 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
||||
PromptDialogComponent,
|
||||
ChoiceDialogComponent,
|
||||
ProjectionDialogComponent,
|
||||
ProgressSnackBarComponent
|
||||
ProgressSnackBarComponent,
|
||||
SuperSearchComponent
|
||||
]
|
||||
})
|
||||
export class SharedModule {}
|
||||
|
@ -127,7 +127,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
|
||||
}
|
||||
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title];
|
||||
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -16,6 +16,14 @@ export function isSearchable(object: any): object is Searchable {
|
||||
export interface Searchable extends DetailNavigable {
|
||||
/**
|
||||
* Should return strings that represents the object.
|
||||
*
|
||||
* The result contains two properties: The `searchValue`, `properties` and optional `type`.
|
||||
*
|
||||
* `searchValue` is an array as summary of the properties.
|
||||
*
|
||||
* `properties` is an array of key-value pair.
|
||||
*
|
||||
* `type` - in case of mediafiles - describes, which type the mediafile has.
|
||||
*/
|
||||
formatForSearch: () => SearchRepresentation;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { ErrorComponent } from './components/error/error.component';
|
||||
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
|
||||
const routes: Routes = [
|
||||
@ -22,10 +21,6 @@ const routes: Routes = [
|
||||
path: 'privacypolicy',
|
||||
component: PrivacyPolicyComponent
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: SearchComponent
|
||||
},
|
||||
{
|
||||
path: 'error',
|
||||
component: ErrorComponent
|
||||
|
@ -1,11 +1,12 @@
|
||||
<div
|
||||
*ngIf="isVisible"
|
||||
class="global-spinner-component">
|
||||
<div class="spinner-container">
|
||||
<div>
|
||||
<div class="spinner"></div>
|
||||
<div class="text">{{ text }}</div>
|
||||
</div>
|
||||
<os-overlay *ngIf="isVisible">
|
||||
<div>
|
||||
<mat-progress-spinner
|
||||
[mode]="mode"
|
||||
[diameter]="diameter"
|
||||
[strokeWidth]="stroke"
|
||||
[value]="value"
|
||||
class="spinner"
|
||||
></mat-progress-spinner>
|
||||
<div class="text">{{ text }}</div>
|
||||
</div>
|
||||
<div class="backdrop"></div>
|
||||
</div>
|
||||
</os-overlay>
|
||||
|
@ -1,83 +1,13 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-global-spinner-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
$contrast-primary: map-get($primary, contrast);
|
||||
$contrast-accent: map-get($accent, contrast);
|
||||
|
||||
.global-spinner-component,
|
||||
.backdrop,
|
||||
.spinner-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.global-spinner-component {
|
||||
position: fixed;
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -136px 0 0 -53px;
|
||||
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border: 6px solid #000;
|
||||
border-radius: 100%;
|
||||
opacity: 0.2;
|
||||
|
||||
animation: rotation 1s infinite linear;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 100%;
|
||||
border-style: solid;
|
||||
border-width: 6px;
|
||||
border-color: white transparent transparent;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
.backdrop {
|
||||
z-index: 899;
|
||||
background-color: #303030;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.text {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
// External imports
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ProgressSpinnerMode } from '@angular/material';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
|
||||
// Internal imports
|
||||
import { OverlayService, SpinnerConfig } from 'app/core/ui-services/overlay.service';
|
||||
|
||||
/**
|
||||
* Component for the global spinner.
|
||||
@ -17,6 +16,26 @@ import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
styleUrls: ['./global-spinner.component.scss']
|
||||
})
|
||||
export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Defines the mode of the spinner. In `'determinate'-mode` a value can be passed to the spinner.
|
||||
*/
|
||||
public mode: ProgressSpinnerMode = 'indeterminate';
|
||||
|
||||
/**
|
||||
* Defines the diameter of the spinner. Defaults to `140`.
|
||||
*/
|
||||
public diameter = 140;
|
||||
|
||||
/**
|
||||
* Defines the stroke-width of the spinner. Defaults to `10`.
|
||||
*/
|
||||
public stroke = 10;
|
||||
|
||||
/**
|
||||
* If the `'determinate'-mode` is applied, a value can be given to the spinner to indicate a progress.
|
||||
*/
|
||||
public value: number;
|
||||
|
||||
/**
|
||||
* Text, which will be shown if the spinner is shown.
|
||||
*/
|
||||
@ -39,12 +58,12 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param spinnerService Reference to the service for this spinner.
|
||||
* @param overlayService Reference to the service for this spinner.
|
||||
* @param translate Service to get translations for the messages.
|
||||
* @param cd Service to manual initiate a change of the UI.
|
||||
*/
|
||||
public constructor(
|
||||
private spinnerService: SpinnerService,
|
||||
private overlayService: OverlayService,
|
||||
protected translate: TranslateService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {}
|
||||
@ -53,14 +72,17 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||
* Init method
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.spinnerSubscription = this.spinnerService // subscribe to the service.
|
||||
.getVisibility()
|
||||
.subscribe((value: { isVisible: boolean; text: string }) => {
|
||||
this.spinnerSubscription = this.overlayService // subscribe to the service.
|
||||
.getSpinner()
|
||||
.subscribe((value: { isVisible: boolean; text: string; config?: SpinnerConfig }) => {
|
||||
this.isVisible = value.isVisible;
|
||||
this.text = this.translate.instant(value.text);
|
||||
if (!this.text) {
|
||||
this.text = this.LOADING;
|
||||
}
|
||||
if (value.config) {
|
||||
this.setConfig(value.config);
|
||||
}
|
||||
this.cd.detectChanges();
|
||||
});
|
||||
}
|
||||
@ -77,4 +99,16 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.spinnerSubscription = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to set properties to the spinner.
|
||||
*
|
||||
* @param config The `SpinnerConfig`.
|
||||
*/
|
||||
private setConfig(config?: SpinnerConfig): void {
|
||||
this.mode = config.mode || this.mode;
|
||||
this.diameter = config.diameter || this.diameter;
|
||||
this.stroke = config.stroke || this.stroke;
|
||||
this.value = config.value || this.value;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
<div class="overlay-component stretch-to-fill-parent">
|
||||
<div class="overlay-content" [ngClass]="position">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="overlay-backdrop stretch-to-fill-parent" (click)="backdrop.emit()"></div>
|
||||
</div>
|
@ -0,0 +1,33 @@
|
||||
.overlay-component {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.overlay-component {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.overlay-content {
|
||||
z-index: 900;
|
||||
position: absolute;
|
||||
transition: all 0.25s ease;
|
||||
|
||||
&.top {
|
||||
top: 20%;
|
||||
}
|
||||
&.right {
|
||||
right: 32px;
|
||||
}
|
||||
&.left {
|
||||
left: 32px;
|
||||
}
|
||||
&.bottom {
|
||||
bottom: 20%;
|
||||
}
|
||||
}
|
||||
.overlay-backdrop {
|
||||
z-index: 899;
|
||||
background-color: #303030;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OverlayComponent } from './overlay.component';
|
||||
|
||||
describe('OverlayComponent', () => {
|
||||
let component: OverlayComponent;
|
||||
let fixture: ComponentFixture<OverlayComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [OverlayComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OverlayComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-overlay',
|
||||
templateUrl: './overlay.component.html',
|
||||
styleUrls: ['./overlay.component.scss']
|
||||
})
|
||||
export class OverlayComponent implements OnInit {
|
||||
/**
|
||||
* Optional set the position of the component overlying on this overlay.
|
||||
*
|
||||
* Defaults to `'center'`.
|
||||
*/
|
||||
@Input()
|
||||
public position: 'center' | 'left' | 'top' | 'right' | 'bottom' = 'center';
|
||||
|
||||
/**
|
||||
* EventEmitter to handle a click on the backdrop.
|
||||
*/
|
||||
@Output()
|
||||
public backdrop = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* EventEmitter to handle clicking `escape`.
|
||||
*/
|
||||
@Output()
|
||||
public escape = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* OnInit
|
||||
*/
|
||||
public ngOnInit(): void {}
|
||||
|
||||
/**
|
||||
* Listens to keyboard inputs.
|
||||
*
|
||||
* If the user presses `escape`, the EventEmitter will emit a signal.
|
||||
*
|
||||
* @param event `KeyboardEvent`.
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
public keyListener(event: KeyboardEvent): void {
|
||||
if (event.code === 'Escape') {
|
||||
this.escape.emit();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
<os-head-bar [nav]="false" [goBack]="true">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Search results</h2></div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="menu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<!-- search-field -->
|
||||
<div class="search-container">
|
||||
<div class="search-field">
|
||||
<mat-form-field appearance="outline" class="search-component">
|
||||
<mat-label>{{ 'Search' | translate }}</mat-label>
|
||||
<input matInput [formControl]="searchForm" />
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<button *ngIf="searchForm.value !== ''" mat-icon-button matSuffix (click)="searchForm.setValue('')">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let registeredModel of registeredModels"
|
||||
(click)="toggleModel($event, registeredModel)"
|
||||
>
|
||||
<mat-checkbox [checked]="registeredModel.enabled" (click)="$event.preventDefault()">
|
||||
<span>{{ registeredModel.verboseNamePlural | translate }}</span>
|
||||
</mat-checkbox>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<h4 matSubheader>{{ 'Sort' | translate }}</h4>
|
||||
<mat-list-item *ngFor="let option of sortingOptionsList" (click)="toggleSorting($event, option.option)">
|
||||
<button mat-menu-item>
|
||||
<mat-icon matListIcon>{{ sortingProperty === option.option ? 'check' : '' }}</mat-icon>
|
||||
<span>{{ option.label | translate }}</span>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-menu>
|
||||
|
||||
<div *ngIf="searchResults.length > 0">
|
||||
<div class="os-card">
|
||||
{{ searchResultCount }}
|
||||
<span *ngIf="searchResultCount === 1" translate>result</span>
|
||||
<span *ngIf="searchResultCount !== 1" translate>results</span>
|
||||
</div>
|
||||
<div class="os-card">
|
||||
<div class="noSearchResults" *ngIf="searchResultCount === 0 && query !== ''">
|
||||
<span translate>No search result found for</span> "{{ query }}"
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let searchResult of searchResults">
|
||||
<mat-card *ngIf="searchResult.models.length > 0">
|
||||
<mat-card-title>
|
||||
{{ searchResult.models.length }} {{ searchResult.verboseName | translate }}
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let model of searchResult.models">
|
||||
<a *ngIf="!searchResult.openInNewTab" [routerLink]="model.getDetailStateURL()">
|
||||
{{ model.getTitle() }}
|
||||
</a>
|
||||
<a
|
||||
*ngIf="searchResult.openInNewTab"
|
||||
[routerLink]="model.getDetailStateURL()"
|
||||
target="_blank"
|
||||
>
|
||||
{{ model.getTitle() }}
|
||||
</a>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,42 +0,0 @@
|
||||
// Variables
|
||||
$border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
|
||||
// Definitions
|
||||
.search-field {
|
||||
display: flex;
|
||||
max-width: 50%;
|
||||
margin: 15px auto;
|
||||
|
||||
mat-form-field.search-component {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.search-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.search-field {
|
||||
display: block;
|
||||
|
||||
mat-form-field.search-sort {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
height: auto !important;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { SearchComponent } from './search.component';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
let component: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [SearchComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,137 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { auditTime, debounceTime } from 'rxjs/operators';
|
||||
|
||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||
import { SearchModel, SearchResult, SearchService } from 'app/core/ui-services/search.service';
|
||||
import { BaseViewComponent } from '../../../base/base-view';
|
||||
|
||||
type SearchModelEnabled = SearchModel & { enabled: boolean };
|
||||
|
||||
/**
|
||||
* Component for the full search text.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-search',
|
||||
templateUrl: './search.component.html',
|
||||
styleUrls: ['./search.component.scss']
|
||||
})
|
||||
export class SearchComponent extends BaseViewComponent implements OnInit {
|
||||
/**
|
||||
* List with all options for sorting by.
|
||||
*/
|
||||
public sortingOptionsList = [{ option: 'title', label: 'Title' }, { option: 'id', label: 'ID' }];
|
||||
|
||||
/**
|
||||
* the search term
|
||||
*/
|
||||
public query = '';
|
||||
|
||||
/**
|
||||
* The amout of search results.
|
||||
*/
|
||||
public searchResultCount: number;
|
||||
|
||||
/**
|
||||
* The search results for the ui
|
||||
*/
|
||||
public searchResults: SearchResult[] = [];
|
||||
|
||||
/**
|
||||
* A list of models, that are registered to be searched. Used for
|
||||
* enable and disable these models.
|
||||
*/
|
||||
public registeredModels: (SearchModelEnabled)[];
|
||||
|
||||
/**
|
||||
* Property to decide what to sort by.
|
||||
*/
|
||||
public sortingProperty: 'id' | 'title' = 'title';
|
||||
|
||||
/**
|
||||
* Form-control for the input-field.
|
||||
*/
|
||||
public searchForm = new FormControl('');
|
||||
|
||||
/**
|
||||
* Inits the quickSearchForm, gets the registered models from the search service
|
||||
* and watches the data store for any changes to initiate a new search if models changes.
|
||||
*
|
||||
* @param title
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @param DS DataStorService
|
||||
* @param searchService For searching in the models
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private DS: DataStoreService,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
|
||||
this.registeredModels = this.searchService.getRegisteredModels().map(rm => ({ ...rm, enabled: true }));
|
||||
|
||||
this.DS.modifiedObservable.pipe(auditTime(100)).subscribe(() => this.search());
|
||||
this.searchForm.valueChanges.pipe(debounceTime(250)).subscribe(query => {
|
||||
this.query = query;
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the search query from the URL and does the initial search.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Search');
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for the query in `this.query` or the query given.
|
||||
*/
|
||||
public search(): void {
|
||||
if (!this.query || this.query === '') {
|
||||
this.searchResults = [];
|
||||
} else {
|
||||
// Just search for enabled models.
|
||||
const collectionStrings = this.registeredModels.filter(rm => rm.enabled).map(rm => rm.collectionString);
|
||||
|
||||
// Get all results
|
||||
this.searchResults = this.searchService.search(this.query, collectionStrings, this.sortingProperty);
|
||||
|
||||
// Because the results are per model, we need to accumulate the total number of all search results.
|
||||
this.searchResultCount = this.searchResults
|
||||
.map(sr => sr.models.length)
|
||||
.reduce((acc, current) => acc + current, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles a model, if it should be used during the search. Initiates a new search afterwards.
|
||||
*
|
||||
* @param registeredModel The model to toggle
|
||||
*/
|
||||
public toggleModel(event: MouseEvent, registeredModel: SearchModelEnabled): void {
|
||||
event.stopPropagation();
|
||||
registeredModel.enabled = !registeredModel.enabled;
|
||||
this.search();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to switch between sorting-options.
|
||||
*
|
||||
* @param event The `MouseEvent`
|
||||
* @param option The sorting-option
|
||||
*/
|
||||
public toggleSorting(event: MouseEvent, option: 'id' | 'title'): void {
|
||||
event.stopPropagation();
|
||||
this.sortingProperty = option;
|
||||
this.search();
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<div class="query-container">
|
||||
<div class="super-search-input">
|
||||
<os-rounded-input
|
||||
[autofocus]="true"
|
||||
placeholder="{{ 'Search' | translate }}"
|
||||
[modelForm]="searchForm"
|
||||
[keepFocus]="true"
|
||||
></os-rounded-input>
|
||||
<button mat-icon-button [matMenuTriggerFor]="filterMenu"><mat-icon>filter_list</mat-icon></button>
|
||||
<mat-menu #filterMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let model of registeredModels"
|
||||
(click)="setCollection(model.verboseNamePlural)"
|
||||
>
|
||||
<mat-icon>{{ model.verboseNamePlural === searchCollection ? 'checked' : '' }}</mat-icon>
|
||||
{{ model.verboseNamePlural | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<h4 *ngIf="searchResultCount > 0" class="result-count">
|
||||
{{ searchResultCount }} {{ (searchResultCount === 1 ? 'result' : 'results') | translate }}
|
||||
</h4>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="result-view" *ngIf="searchResults.length > 0">
|
||||
<div class="result-list">
|
||||
<mat-accordion [displayMode]="'flat'">
|
||||
<ng-container *ngFor="let result of searchResults">
|
||||
<mat-expansion-panel
|
||||
*ngIf="result.models.length > 0"
|
||||
[expanded]="selectedCollection === result.collectionString"
|
||||
>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
{{ result.verboseName | translate }}
|
||||
<mat-basic-chip
|
||||
class="lightblue filter-count"
|
||||
disableRipple
|
||||
matTooltip="{{ (result.models.length === 1 ? 'result' : 'results') | translate }}"
|
||||
>{{ result.models.length }}</mat-basic-chip
|
||||
>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<mat-list role="list">
|
||||
<mat-list-item
|
||||
[ngClass]="selectedModel === model ? 'selected' : ''"
|
||||
role="listitem"
|
||||
(click)="changeModel(model)"
|
||||
(dblclick)="viewResult(model)"
|
||||
*ngFor="let model of result.models"
|
||||
>
|
||||
<div class="flex-1 ellipsis-overflow">
|
||||
{{ model.getTitle() }}
|
||||
</div>
|
||||
<div *ngIf="selectedModel === model">
|
||||
<button
|
||||
*ngIf="!vp.isMobile"
|
||||
mat-icon-button
|
||||
(click)="showPreview = !showPreview"
|
||||
matTooltip="{{ 'Show preview' | translate }}"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ showPreview ? 'visibility_off' : 'visibility_on' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="viewResult(model)"
|
||||
matTooltip="{{ 'View' | translate }}"
|
||||
>
|
||||
<mat-icon>
|
||||
present_to_all
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
</mat-accordion>
|
||||
<div class="no-results" *ngIf="!selectedModel && searchString.length > 0">
|
||||
<span translate>No search result found for</span> "{{ searchString }}"
|
||||
<span *ngIf="searchCollection">in {{ searchCollection | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<div *ngIf="showPreview" class="result-preview flex-1">
|
||||
<div *ngIf="!!selectedModel">
|
||||
<os-preview [viewModel]="selectedModel"> </os-preview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,97 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-super-search-style($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
.super-search-container > mat-dialog-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.query-container {
|
||||
display: block;
|
||||
|
||||
.super-search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
|
||||
os-rounded-input {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.result-count {
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-view {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: mat-color($background, background);
|
||||
max-height: calc(90vh - 93px);
|
||||
|
||||
.filter-count {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.result-model-name {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 12px 16px;
|
||||
background: darkgray;
|
||||
z-index: 2;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
mat-list,
|
||||
mat-selection-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
&.selected,
|
||||
.mat-list-item-content.selected {
|
||||
&,
|
||||
mat-icon {
|
||||
color: white;
|
||||
}
|
||||
background: mat-color($primary);
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
color: mat-color($foreground, icon);
|
||||
}
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
overflow-y: auto;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
|
||||
// import { SuperSearchComponent } from './super-search.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
fdescribe('SuperSearchComponent', () => {
|
||||
// let component: SuperSearchComponent;
|
||||
// let fixture: ComponentFixture<SuperSearchComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
// fixture = TestBed.createComponent(SuperSearchComponent);
|
||||
// component = fixture.componentInstance;
|
||||
// fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,320 @@
|
||||
import { Component, HostListener, Inject, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { auditTime, debounceTime } from 'rxjs/operators';
|
||||
|
||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||
import { StorageService } from 'app/core/core-services/storage.service';
|
||||
import { SearchModel, SearchResult, SearchService, TranslatedCollection } from 'app/core/ui-services/search.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { Searchable } from 'app/site/base/searchable';
|
||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||
|
||||
@Component({
|
||||
selector: 'os-super-search',
|
||||
templateUrl: './super-search.component.html',
|
||||
styleUrls: ['./super-search.component.scss']
|
||||
})
|
||||
export class SuperSearchComponent implements OnInit {
|
||||
/**
|
||||
* The reference to the form-control used for the `rounded-input.component`.
|
||||
*/
|
||||
public searchForm = new FormControl('');
|
||||
|
||||
/**
|
||||
* The user's input as query: `string`.
|
||||
*/
|
||||
public searchString = '';
|
||||
|
||||
/**
|
||||
* Variable to hold the verbose name of a specific collection.
|
||||
*/
|
||||
public searchCollection = '';
|
||||
|
||||
/**
|
||||
* Holds the collection-string of the specific collection.
|
||||
*/
|
||||
private specificCollectionString: string = null;
|
||||
|
||||
/**
|
||||
* The results for the given query.
|
||||
*
|
||||
* An array of `SearchResult`.
|
||||
*/
|
||||
public searchResults: SearchResult[] = [];
|
||||
|
||||
/**
|
||||
* Number of all found results.
|
||||
*/
|
||||
public searchResultCount = 0;
|
||||
|
||||
/**
|
||||
* The model, the user selected to see its preview.
|
||||
*/
|
||||
public selectedModel: (BaseViewModel & Searchable) | null = null;
|
||||
|
||||
/**
|
||||
* The current collection of the selected model.
|
||||
*/
|
||||
public selectedCollection: string;
|
||||
|
||||
/**
|
||||
* Boolean to indicate, if the preview should be shown.
|
||||
*/
|
||||
public showPreview = false;
|
||||
|
||||
/**
|
||||
* All registered model-collections.
|
||||
*/
|
||||
public registeredModels: SearchModel[];
|
||||
|
||||
/**
|
||||
* Stores all the collectionStrings registered by the `search.service`.
|
||||
*/
|
||||
private collectionStrings: string[];
|
||||
|
||||
/**
|
||||
* Stores all the collections with translated names.
|
||||
*/
|
||||
private translatedCollectionStrings: TranslatedCollection[];
|
||||
|
||||
/**
|
||||
* Key to store the query in the local-storage.
|
||||
*/
|
||||
private storageKey = 'SuperSearchQuery';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param vp The viewport-service.
|
||||
* @param overlayService Service to handle the overlaying background.
|
||||
* @param searchService Service required for searching events.
|
||||
* @param DS Reference to the `DataStore`.
|
||||
* @param router Reference to the `Router`.
|
||||
* @param store The reference to the storage-service.
|
||||
* @param dialogRef Reference for the material-dialog.
|
||||
*/
|
||||
public constructor(
|
||||
public vp: ViewportService,
|
||||
private searchService: SearchService,
|
||||
private DS: DataStoreService,
|
||||
private router: Router,
|
||||
private store: StorageService,
|
||||
public dialogRef: MatDialogRef<SuperSearchComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
/**
|
||||
* OnInit-function.
|
||||
*
|
||||
* Initializes the collections and the translated ones.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.DS.modifiedObservable.pipe(auditTime(100)).subscribe(() => this.search());
|
||||
|
||||
this.registeredModels = this.searchService.getRegisteredModels();
|
||||
this.collectionStrings = this.registeredModels.map(rm => rm.collectionString);
|
||||
this.translatedCollectionStrings = this.searchService.getTranslatedCollectionStrings();
|
||||
|
||||
this.searchForm.valueChanges.pipe(debounceTime(250)).subscribe(value => {
|
||||
if (value.trim() === '') {
|
||||
this.clearResults();
|
||||
} else {
|
||||
this.specificCollectionString = this.searchSpecificCollection(value.trim());
|
||||
}
|
||||
this.search();
|
||||
});
|
||||
|
||||
this.restoreQueryFromStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function to search through all collections.
|
||||
*/
|
||||
private search(): void {
|
||||
if (this.searchString !== '') {
|
||||
// Local variable to check, if the user searches for a specific id.
|
||||
let dedicatedId: number;
|
||||
|
||||
const query = this.searchString;
|
||||
// Looks, if the query matches variations of 'nr.' followed by at least one digit.
|
||||
// If so, the user searches for a specific id in some collections.
|
||||
// Everything not case-sensitive.
|
||||
if (query.match(/n\w*r\.?\:?\s*\d+/gi)) {
|
||||
// If so, this expression finds the number.
|
||||
dedicatedId = +query.match(/\d+/g);
|
||||
}
|
||||
this.searchResults = this.searchService.search(
|
||||
query,
|
||||
this.specificCollectionString ? [this.specificCollectionString] : this.collectionStrings,
|
||||
'title',
|
||||
dedicatedId
|
||||
);
|
||||
this.selectFirstResult();
|
||||
} else {
|
||||
this.searchResults = [];
|
||||
}
|
||||
this.searchResultCount = this.searchResults
|
||||
.map(result => result.models.length)
|
||||
.reduce((acc, current) => acc + current, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function test, if the query matches some of the `collectionStrings`.
|
||||
*
|
||||
* That indicates, that the user looks for items in a specific collection.
|
||||
*
|
||||
* @returns { { collection: string, query: string[] } | null } Either an object containing the found collection and the query
|
||||
* or null, if there exists none.
|
||||
*/
|
||||
private searchSpecificCollection(query: string): string | null {
|
||||
// The query is splitted by the first whitespace or the first ':'.
|
||||
const splittedQuery = query.split(/\s*(?::|\s+)\s*/);
|
||||
const nextCollection = this.translatedCollectionStrings.find(item =>
|
||||
// The value of the item should match the query plus any further
|
||||
// characters (useful for splitted words in the query).
|
||||
// This will look, if the user searches in a specific collection.
|
||||
// Flag 'i' tells, that cases are ignored.
|
||||
new RegExp(item.value, 'i').test(splittedQuery[0])
|
||||
);
|
||||
if (!!nextCollection) {
|
||||
this.searchString = splittedQuery.slice(1).join(' ');
|
||||
this.searchCollection = splittedQuery[0];
|
||||
return nextCollection.collection;
|
||||
} else {
|
||||
this.searchString = query;
|
||||
this.searchCollection = '';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to search through the result-list and select the first valid result to display.
|
||||
*
|
||||
* Otherwise the model is set to 'null'.
|
||||
*/
|
||||
private selectFirstResult(): void {
|
||||
for (const result of this.searchResults) {
|
||||
if (result.models.length > 0) {
|
||||
this.changeModel(result.models[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If this code is reached, there are no results for the query!
|
||||
this.selectedModel = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to go through the whole list of results.
|
||||
*
|
||||
* @param up If the user presses the `ArrowUp`.
|
||||
*/
|
||||
private selectNextResult(up: boolean): void {
|
||||
const tmp = this.searchResults.flatMap((result: SearchResult) => result.models);
|
||||
this.changeModel(tmp[(tmp.indexOf(this.selectedModel) + (up ? -1 : 1)).modulo(tmp.length)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to set a specific collection and search through it.
|
||||
*
|
||||
* @param collectionName The `verboseName` of the selected collection.
|
||||
*/
|
||||
public setCollection(collectionName: string): void {
|
||||
this.searchCollection = this.searchCollection === collectionName ? '' : collectionName;
|
||||
if (this.searchCollection !== '') {
|
||||
this.searchForm.setValue(this.searchCollection + ': ' + this.searchString);
|
||||
} else {
|
||||
this.searchForm.setValue(this.searchString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to change the selected model.
|
||||
*
|
||||
* Ensures, that the preview-window's size is reset to the default one.
|
||||
*
|
||||
* @param model The model, the user selected. Typeof `BaseViewModel & Searchable`.
|
||||
*/
|
||||
public changeModel(model: BaseViewModel & Searchable): void {
|
||||
this.selectedModel = model;
|
||||
this.selectedCollection = model.collectionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to go to the detailed view of the model.
|
||||
*
|
||||
* @param model The model, the user selected.
|
||||
*/
|
||||
public viewResult(model: BaseViewModel & Searchable): void {
|
||||
if (model.collectionString === 'mediafiles/mediafile' && !(<ViewMediafile>model).is_directory) {
|
||||
window.open(model.getDetailStateURL(), '_blank');
|
||||
} else {
|
||||
this.router.navigateByUrl(model.getDetailStateURL());
|
||||
}
|
||||
this.hideOverlay();
|
||||
this.saveQueryToStorage(this.searchForm.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the overlay, so the search will disappear.
|
||||
*/
|
||||
public hideOverlay(): void {
|
||||
this.clearResults();
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the whole search with results and preview.
|
||||
*/
|
||||
private clearResults(): void {
|
||||
this.searchResults = [];
|
||||
this.selectedModel = null;
|
||||
this.searchCollection = '';
|
||||
this.searchString = '';
|
||||
this.saveQueryToStorage(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to save an entered query.
|
||||
*
|
||||
* @param query The query to store.
|
||||
*/
|
||||
private saveQueryToStorage(query: string | null): void {
|
||||
this.store.set(this.storageKey, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to restore a previous entered query.
|
||||
* Once loaded, the result is passed as value to the form-control.
|
||||
*/
|
||||
private restoreQueryFromStorage(): void {
|
||||
this.store.get<string>(this.storageKey).then(value => {
|
||||
if (value) {
|
||||
this.searchForm.setValue(value);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to open the global `super-search.component`.
|
||||
*
|
||||
* @param event KeyboardEvent to listen to keyboard-inputs.
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event']) public onKeyNavigation(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter') {
|
||||
this.viewResult(this.selectedModel);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.selectNextResult(true);
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.selectNextResult(false);
|
||||
}
|
||||
if (event.altKey && event.shiftKey && event.key === 'V') {
|
||||
this.showPreview = !this.showPreview;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,19 +6,11 @@ import { CountUsersComponent } from './components/count-users/count-users.compon
|
||||
import { ErrorComponent } from './components/error/error.component';
|
||||
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||
import { SearchComponent } from './components/search/search.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { StartComponent } from './components/start/start.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CommonRoutingModule, SharedModule],
|
||||
declarations: [
|
||||
PrivacyPolicyComponent,
|
||||
StartComponent,
|
||||
LegalNoticeComponent,
|
||||
SearchComponent,
|
||||
CountUsersComponent,
|
||||
ErrorComponent
|
||||
]
|
||||
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, CountUsersComponent, ErrorComponent]
|
||||
})
|
||||
export class OsCommonModule {}
|
||||
|
@ -10,7 +10,7 @@ import { Subscription } from 'rxjs';
|
||||
import { AuthService } from 'app/core/core-services/auth.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { LoginDataService } from 'app/core/ui-services/login-data.service';
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
|
||||
@ -62,7 +62,7 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
|
||||
* @param httpService used to get information before the login
|
||||
* @param OpenSlides The Service for OpenSlides
|
||||
* @param loginDataService provide information about the legal notice and privacy policy
|
||||
* @param spinnerService Service to show the spinner when the user is signing in
|
||||
* @param overlayService Service to show the spinner when the user is signing in
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
@ -74,11 +74,10 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private loginDataService: LoginDataService,
|
||||
private spinnerService: SpinnerService
|
||||
private overlayService: OverlayService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
// Hide the spinner if the user is at `login-mask`
|
||||
spinnerService.setVisibility(false);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
@ -138,7 +137,7 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
|
||||
this.loginErrorMsg = '';
|
||||
try {
|
||||
await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => {
|
||||
this.spinnerService.setVisibility(true, this.translate.instant('Loading data. Please wait ...'));
|
||||
this.overlayService.setSpinner(true, this.translate.instant('Loading data. Please wait...'));
|
||||
this.clearOperatorSubscription(); // We take control, not the subscription.
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -51,11 +51,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
||||
}
|
||||
|
||||
public get title(): string {
|
||||
if (this.is_directory) {
|
||||
return this.mediafile.path;
|
||||
} else {
|
||||
return this.mediafile.title;
|
||||
}
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
public get filename(): string {
|
||||
@ -78,7 +74,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
||||
return !this.is_directory;
|
||||
}
|
||||
|
||||
public get size(): string {
|
||||
public get size(): string | null {
|
||||
return this.mediafile.filesize;
|
||||
}
|
||||
|
||||
@ -90,7 +86,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
||||
return this.mediafile.url;
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
public get type(): string | null {
|
||||
return this.mediafile.mediafile ? this.mediafile.mediafile.type : '';
|
||||
}
|
||||
|
||||
@ -103,11 +99,23 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
||||
}
|
||||
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title, this.path];
|
||||
const type = this.is_directory ? 'directory' : this.type;
|
||||
const properties = [
|
||||
{ key: 'Title', value: this.getTitle() },
|
||||
{ key: 'Path', value: this.path },
|
||||
{ key: 'Type', value: type },
|
||||
{ key: 'Timestamp', value: this.timestamp },
|
||||
{ key: 'Size', value: this.size ? this.size : '0' }
|
||||
];
|
||||
return {
|
||||
properties,
|
||||
searchValue: properties.map(property => property.value),
|
||||
type: type
|
||||
};
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
return this.url;
|
||||
return this.is_directory ? ('/mediafiles/files/' + this.path).slice(0, -1) : this.url;
|
||||
}
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
|
@ -107,7 +107,10 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
|
||||
}
|
||||
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.name, this.prefix];
|
||||
return {
|
||||
properties: [{ key: 'Name', value: this.name }, { key: 'Prefix', value: this.prefix }],
|
||||
searchValue: [this.name, this.prefix]
|
||||
};
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -42,7 +42,7 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
|
||||
* @override
|
||||
*/
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title];
|
||||
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { _ } from 'app/core/translate/translation-marker';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { SearchRepresentation } from 'app/core/ui-services/search.service';
|
||||
import { SearchProperty, SearchRepresentation } from 'app/core/ui-services/search.service';
|
||||
import { Motion, MotionComment } from 'app/shared/models/motions/motion';
|
||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
|
||||
@ -351,24 +351,50 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
|
||||
* @override
|
||||
*/
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
let searchValues = [this.title, this.text, this.reason];
|
||||
const properties: SearchProperty[] = [];
|
||||
properties.push({ key: 'Title', value: this.getTitle() });
|
||||
properties.push({ key: 'Submitters', value: this.submittersAsUsers.map(user => user.full_name).join(', ') });
|
||||
properties.push({ key: 'Text', value: this.text, trusted: true });
|
||||
properties.push({ key: 'Reason', value: this.reason, trusted: true });
|
||||
if (this.amendment_paragraphs) {
|
||||
searchValues = searchValues.concat(this.amendment_paragraphs.filter(x => !!x));
|
||||
properties.push({
|
||||
key: 'Amendments',
|
||||
value: this.amendment_paragraphs.filter(x => !!x).join('\n'),
|
||||
trusted: true
|
||||
});
|
||||
}
|
||||
searchValues = searchValues.concat(this.submittersAsUsers.map(user => user.full_name));
|
||||
searchValues = searchValues.concat(this.supporters.map(user => user.full_name));
|
||||
searchValues = searchValues.concat(this.tags.map(tag => tag.getTitle()));
|
||||
searchValues = searchValues.concat(this.motion.comments.map(comment => comment.comment));
|
||||
properties.push({ key: 'Tags', value: this.tags.map(tag => tag.getTitle()).join(', ') });
|
||||
properties.push({
|
||||
key: 'Comments',
|
||||
value: this.motion.comments.map(comment => comment.comment).join('\n'),
|
||||
trusted: true
|
||||
});
|
||||
properties.push({ key: 'Supporters', value: this.supporters.map(user => user.full_name).join(', ') });
|
||||
|
||||
// A property with block-value to unify the meta-info.
|
||||
const metaData: SearchProperty = {
|
||||
key: null,
|
||||
value: null,
|
||||
blockProperties: []
|
||||
};
|
||||
if (this.motion_block) {
|
||||
searchValues.push(this.motion_block.getTitle());
|
||||
metaData.blockProperties.push({ key: 'Motion block', value: this.motion_block.getTitle() });
|
||||
}
|
||||
if (this.category) {
|
||||
searchValues.push(this.category.getTitle());
|
||||
metaData.blockProperties.push({ key: 'Category', value: this.category.getTitle() });
|
||||
}
|
||||
if (this.state) {
|
||||
searchValues.push(this.state.name);
|
||||
metaData.blockProperties.push({ key: 'State', value: this.state.name });
|
||||
}
|
||||
return searchValues;
|
||||
|
||||
properties.push(metaData);
|
||||
|
||||
return {
|
||||
properties,
|
||||
searchValue: properties.map(property =>
|
||||
property.key ? property.value : property.blockProperties.join(',')
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -36,7 +36,7 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
|
||||
}
|
||||
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title];
|
||||
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -15,7 +15,7 @@ import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflo
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { OsFilterOptionCondition } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
|
||||
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
import { BaseListViewComponent } from 'app/site/base/base-list-view';
|
||||
@ -214,7 +214,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
|
||||
public multiselectService: MotionMultiselectService,
|
||||
public perms: LocalPermissionsService,
|
||||
private motionExport: MotionExportService,
|
||||
private spinnerService: SpinnerService
|
||||
private overlayService: OverlayService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar, storage);
|
||||
this.canMultiSelect = true;
|
||||
@ -380,7 +380,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
} finally {
|
||||
this.spinnerService.setVisibility(false);
|
||||
this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,9 @@ import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflo
|
||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ChoiceService } from 'app/core/ui-services/choice.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { PersonalNoteService } from 'app/core/ui-services/personal-note.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
import { TreeService } from 'app/core/ui-services/tree.service';
|
||||
import { ChoiceDialogOptions } from 'app/shared/components/choice-dialog/choice-dialog.component';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
@ -45,7 +45,7 @@ export class MotionMultiselectService {
|
||||
* @param httpService
|
||||
* @param treeService
|
||||
* @param personalNoteService
|
||||
* @param spinnerService to show a spinner when http-requests are made.
|
||||
* @param overlayService to show a spinner when http-requests are made.
|
||||
*/
|
||||
public constructor(
|
||||
private repo: MotionRepositoryService,
|
||||
@ -61,7 +61,7 @@ export class MotionMultiselectService {
|
||||
private httpService: HttpService,
|
||||
private treeService: TreeService,
|
||||
private personalNoteService: PersonalNoteService,
|
||||
private spinnerService: SpinnerService
|
||||
private overlayService: OverlayService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -81,10 +81,10 @@ export class MotionMultiselectService {
|
||||
`\n${i} ` +
|
||||
this.translate.instant('of') +
|
||||
` ${motions.length}`;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.repo.delete(motion);
|
||||
}
|
||||
this.spinnerService.setVisibility(false);
|
||||
this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,8 +118,13 @@ export class MotionMultiselectService {
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice) {
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.repo.setMultiState(motions, selectedChoice.items as number);
|
||||
// .catch(error => {
|
||||
// this.overlayService.setSpinner(false);
|
||||
// throw error;
|
||||
// });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,10 +151,15 @@ export class MotionMultiselectService {
|
||||
}));
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
||||
motions: requestData
|
||||
});
|
||||
// .catch(error => {
|
||||
// this.overlayService.setSpinner(false);
|
||||
// throw error;
|
||||
// });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,8 +180,13 @@ export class MotionMultiselectService {
|
||||
);
|
||||
if (selectedChoice) {
|
||||
const message = this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.repo.setMultiCategory(motions, selectedChoice.items as number);
|
||||
// .catch(error => {
|
||||
// this.overlayService.setSpinner(false);
|
||||
// throw error;
|
||||
// });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,8 +225,9 @@ export class MotionMultiselectService {
|
||||
}
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,8 +276,9 @@ export class MotionMultiselectService {
|
||||
}
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,9 +299,14 @@ export class MotionMultiselectService {
|
||||
);
|
||||
if (selectedChoice) {
|
||||
const message = this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
const blockId = selectedChoice.action ? null : (selectedChoice.items as number);
|
||||
await this.repo.setMultiMotionBlock(motions, blockId);
|
||||
// .catch(error => {
|
||||
// this.overlayService.setSpinner(false);
|
||||
// throw error;
|
||||
// });
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,8 +369,9 @@ export class MotionMultiselectService {
|
||||
if (selectedChoice && motions.length) {
|
||||
const message = this.translate.instant(`I have ${motions.length} favorite motions. Please wait ...`);
|
||||
const star = (selectedChoice.items as number) === choices[0].id;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
this.overlayService.setSpinner(true, message);
|
||||
await this.personalNoteService.bulkSetStar(motions, star);
|
||||
// this.overlayService.setSpinner(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,13 +90,7 @@
|
||||
</a>
|
||||
</span>
|
||||
<mat-divider></mat-divider>
|
||||
<a
|
||||
[@navItemAnim]
|
||||
mat-list-item
|
||||
routerLink="/search"
|
||||
routerLinkActive="active"
|
||||
(click)="mobileAutoCloseNav()"
|
||||
>
|
||||
<a [@navItemAnim] *ngIf="vp.isMobile" mat-list-item routerLinkActive="active" (click)="toggleSearch()">
|
||||
<mat-icon>search</mat-icon>
|
||||
<span translate>Search</span>
|
||||
</a>
|
||||
@ -156,3 +150,5 @@
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
<os-global-spinner></os-global-spinner>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, HostListener, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
@ -12,6 +12,7 @@ import { filter } from 'rxjs/operators';
|
||||
import { navItemAnim, pageTransition } from '../shared/animations';
|
||||
import { OfflineService } from 'app/core/core-services/offline.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { OverlayService } from 'app/core/ui-services/overlay.service';
|
||||
import { UpdateService } from 'app/core/ui-services/update.service';
|
||||
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
||||
import { AuthService } from '../core/core-services/auth.service';
|
||||
@ -100,9 +101,11 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
public mainMenuService: MainMenuService,
|
||||
public OSStatus: OpenSlidesStatusService,
|
||||
public timeTravel: TimeTravelService,
|
||||
private matSnackBar: MatSnackBar
|
||||
private matSnackBar: MatSnackBar,
|
||||
private overlayService: OverlayService
|
||||
) {
|
||||
super(title, translate);
|
||||
overlayService.setSpinner(true, translate.instant('Loading data. Please wait...'));
|
||||
|
||||
this.operator.getViewUserObservable().subscribe(user => {
|
||||
if (user) {
|
||||
@ -208,6 +211,15 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
this.sideNav.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the `super-search.component`,
|
||||
* only if the user is on a mobile device.
|
||||
*/
|
||||
public toggleSearch(): void {
|
||||
this.overlayService.showSearch();
|
||||
this.mobileAutoCloseNav();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically close the navigation in while navigating in mobile mode
|
||||
*/
|
||||
@ -250,6 +262,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
public logout(): void {
|
||||
this.authService.logout();
|
||||
this.overlayService.logout();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -292,15 +305,6 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the search bar
|
||||
*/
|
||||
public search(): void {
|
||||
const query = this.searchform.get('query').value;
|
||||
this.searchform.reset();
|
||||
this.router.navigate(['/search'], { queryParams: { query: query } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp for the current point in history mode.
|
||||
* Tries to detect the ideal timestamp format using the translation service
|
||||
@ -310,4 +314,15 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
public getHistoryTimestamp(): string {
|
||||
return this.OSStatus.getHistoryTimeStamp(langToLocale(this.translate.currentLang));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to open the global `super-search.component`.
|
||||
*
|
||||
* @param event KeyboardEvent to listen to keyboard-inputs.
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event']) public onKeyNavigation(event: KeyboardEvent): void {
|
||||
if (event.altKey && event.shiftKey && event.code === 'KeyF') {
|
||||
this.overlayService.showSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export class ViewTag extends BaseViewModel<Tag> implements TagTitleInformation,
|
||||
}
|
||||
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.name];
|
||||
return { properties: [{ key: 'Name', value: this.name }], searchValue: [this.name] };
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -46,7 +46,10 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl
|
||||
* @override
|
||||
*/
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title, this.text];
|
||||
return {
|
||||
properties: [{ key: 'Title', value: this.getTitle() }, { key: 'Text', value: this.text }],
|
||||
searchValue: [this.getTitle(), this.text]
|
||||
};
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -127,7 +127,14 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
|
||||
* @override
|
||||
*/
|
||||
public formatForSearch(): SearchRepresentation {
|
||||
return [this.title, this.first_name, this.last_name, this.structure_level, this.number];
|
||||
const properties = [
|
||||
{ key: 'Title', value: this.getTitle() },
|
||||
{ key: 'First name', value: this.first_name },
|
||||
{ key: 'Last name', value: this.last_name },
|
||||
{ key: 'Structure level', value: this.structure_level },
|
||||
{ key: 'Number', value: this.number }
|
||||
];
|
||||
return { properties, searchValue: properties.map(property => property.value) };
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div *ngIf="data">
|
||||
<div *ngIf="isImage" [ngClass]="data.element.fullscreen ? 'fullscreen' : 'nofullscreen'" >
|
||||
<div *ngIf="isImage" [ngClass]="data.element.fullscreen ? 'fullscreen' : 'nofullscreen'">
|
||||
<img [src]="url" alt="" />
|
||||
</div>
|
||||
<div *ngIf="isPdf" class="fullscreen">
|
||||
@ -11,6 +11,7 @@
|
||||
[page]="data.element.page || 1"
|
||||
[zoom]="zoom"
|
||||
[src]="url"
|
||||
style="display: block;"></pdf-viewer>
|
||||
style="display: block;"
|
||||
></pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { PdfViewerModule } from 'ng2-pdf-viewer';
|
||||
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { SLIDE } from 'app/slides/slide-token';
|
||||
import { MediafileSlideComponent } from './mediafile-slide.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule, PdfViewerModule],
|
||||
imports: [CommonModule, SharedModule],
|
||||
declarations: [MediafileSlideComponent],
|
||||
providers: [{ provide: SLIDE, useValue: MediafileSlideComponent }],
|
||||
entryComponents: [MediafileSlideComponent]
|
||||
|
@ -21,6 +21,8 @@
|
||||
@import './app/shared/components/icon-container/icon-container.component.scss';
|
||||
@import './app/site/common/components/start/start.component.scss';
|
||||
@import './app/site/mediafiles/components/mediafile-list/mediafile-list.component.scss-theme.scss';
|
||||
@import './app/site/common/components/super-search/super-search.component.scss';
|
||||
@import './app/shared/components/rounded-input/rounded-input.component.scss';
|
||||
|
||||
/** fonts */
|
||||
@import './assets/styles/fonts.scss';
|
||||
@ -42,6 +44,8 @@ $narrow-spacing: (
|
||||
@include os-global-spinner-theme($theme);
|
||||
@include os-tile-style($theme);
|
||||
@include os-mediafile-list-theme($theme);
|
||||
@include os-super-search-style($theme);
|
||||
@include os-rounded-input-style($theme);
|
||||
}
|
||||
|
||||
/** Load projector specific SCSS values */
|
||||
|
Loading…
Reference in New Issue
Block a user