Implements the global 'super-search.component'
- Moves the 'spinner.component' to 'site.component' - Replaces also the 'spinner.service' with an 'overlay.service', that handles all request to show an element on an overlay.
This commit is contained in:
parent
b1c02133ee
commit
5f29732e26
@ -1,4 +1,3 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<os-global-spinner></os-global-spinner>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,22 +11,30 @@ import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade
|
|||||||
import { LoadFontService } from './core/ui-services/load-font.service';
|
import { LoadFontService } from './core/ui-services/load-font.service';
|
||||||
import { LoginDataService } from './core/ui-services/login-data.service';
|
import { LoginDataService } from './core/ui-services/login-data.service';
|
||||||
import { OperatorService } from './core/core-services/operator.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 { PingService } from './core/core-services/ping.service';
|
||||||
import { PrioritizeService } from './core/core-services/prioritize.service';
|
import { PrioritizeService } from './core/core-services/prioritize.service';
|
||||||
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
||||||
import { ServertimeService } from './core/core-services/servertime.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 { ThemeService } from './core/ui-services/theme.service';
|
||||||
import { ViewUser } from './site/users/models/view-user';
|
import { ViewUser } from './site/users/models/view-user';
|
||||||
|
|
||||||
|
declare global {
|
||||||
/**
|
/**
|
||||||
* Enhance array with own functions
|
* Enhance array with own functions
|
||||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||||
*/
|
*/
|
||||||
declare global {
|
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
flatMap(o: any): any[];
|
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,
|
loginDataService: LoginDataService,
|
||||||
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
||||||
themeService: ThemeService,
|
themeService: ThemeService,
|
||||||
private spinnerService: SpinnerService,
|
private overlayService: OverlayService,
|
||||||
countUsersService: CountUsersService, // Needed to register itself.
|
countUsersService: CountUsersService, // Needed to register itself.
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
loadFontService: LoadFontService,
|
loadFontService: LoadFontService,
|
||||||
@ -95,8 +103,9 @@ export class AppComponent {
|
|||||||
// change default JS functions
|
// change default JS functions
|
||||||
this.overloadArrayToString();
|
this.overloadArrayToString();
|
||||||
this.overloadFlatMap();
|
this.overloadFlatMap();
|
||||||
|
this.overloadModulo();
|
||||||
|
|
||||||
// Show the spinner initial
|
// Show the spinner initial
|
||||||
spinnerService.setVisibility(true, translate.instant('Loading data. Please wait ...'));
|
|
||||||
|
|
||||||
appRef.isStable
|
appRef.isStable
|
||||||
.pipe(
|
.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.
|
* Function to check if the user is existing and the app is already stable.
|
||||||
* If both conditions true, hide the spinner.
|
* If both conditions true, hide the spinner.
|
||||||
*/
|
*/
|
||||||
private checkConnectionProgress(): void {
|
private checkConnectionProgress(): void {
|
||||||
if ((this.user || this.operator.isAnonymous) && this.isStable) {
|
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 { AppComponent } from './app.component';
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { GlobalSpinnerComponent } from './site/common/components/global-spinner/global-spinner.component';
|
|
||||||
import { LoginModule } from './site/login/login.module';
|
import { LoginModule } from './site/login/login.module';
|
||||||
import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module';
|
import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module';
|
||||||
import { SlidesModule } from './slides/slides.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.
|
* Global App Module. Keep it as clean as possible.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent, GlobalSpinnerComponent],
|
declarations: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
HttpClientModule,
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,51 @@ import { BaseRepository } from '../repositories/base-repository';
|
|||||||
import { Searchable } from '../../site/base/searchable';
|
import { Searchable } from '../../site/base/searchable';
|
||||||
import { ViewModelStoreService } from '../core-services/view-model-store.service';
|
import { ViewModelStoreService } from '../core-services/view-model-store.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines, how the properties look like
|
||||||
|
*/
|
||||||
|
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.
|
* The representation every searchable model should use to represent their data.
|
||||||
*/
|
*/
|
||||||
export type SearchRepresentation = string[];
|
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.
|
* Our representation of a searchable model for external use.
|
||||||
@ -47,7 +88,7 @@ export interface SearchResult {
|
|||||||
collectionString: string;
|
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.
|
* it should have a singular value, else a plural name.
|
||||||
*/
|
*/
|
||||||
verboseName: string;
|
verboseName: string;
|
||||||
@ -63,6 +104,21 @@ export interface SearchResult {
|
|||||||
models: (BaseViewModel & Searchable)[];
|
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.
|
* 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 query The search query
|
||||||
* @param inCollectionStrings All connection strings which should be used for searching.
|
* @param inCollectionStrings All connection strings which should be used for searching.
|
||||||
* @param sortingProperty Sorting by `id` or `title`.
|
* @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()`).
|
* @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();
|
query = query.toLowerCase();
|
||||||
return this.searchModels
|
return this.searchModels
|
||||||
.filter(s => inCollectionStrings.includes(s.collectionString))
|
.filter(s => inCollectionStrings.indexOf(s.collectionString) !== -1)
|
||||||
.map(searchModel => {
|
.map(searchModel => {
|
||||||
const results = this.viewModelStore
|
const results = this.viewModelStore
|
||||||
.getAll(searchModel.collectionString)
|
.getAll(searchModel.collectionString)
|
||||||
.map(x => x as (BaseViewModel & Searchable))
|
.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) => {
|
.sort((a, b) => {
|
||||||
switch (sortingProperty) {
|
switch (sortingProperty) {
|
||||||
case 'id':
|
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>
|
||||||
<div class="spacer"></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">
|
<div class="toolbar-right">
|
||||||
<!-- Extra controls slot -->
|
<!-- Extra controls slot -->
|
||||||
<ng-content select=".extra-controls-slot"></ng-content>
|
<ng-content select=".extra-controls-slot"></ng-content>
|
||||||
|
@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { MainMenuService } from 'app/core/core-services/main-menu.service';
|
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 { RoutingStateService } from 'app/core/ui-services/routing-state.service';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
|
|
||||||
@ -141,7 +142,8 @@ export class HeadBarComponent {
|
|||||||
private menu: MainMenuService,
|
private menu: MainMenuService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private routingState: RoutingStateService
|
private routingState: RoutingStateService,
|
||||||
|
private overlayService: OverlayService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -172,6 +174,13 @@ export class HeadBarComponent {
|
|||||||
this.saveEvent.next(true);
|
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
|
* Exits the view to return to the previous page or
|
||||||
* visit the parent view again.
|
* 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"
|
[autofocus]="autofocus"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
class="rounded-input"
|
class="rounded-input"
|
||||||
[ngClass]="[size]"
|
[ngClass]="[size, borderRadius, hasChildren ? 'children-bottom' : '']"
|
||||||
[formControl]="modelForm"
|
[formControl]="modelForm"
|
||||||
(keyup)="keyPressed($event)"
|
(keyup)="keyPressed($event)"
|
||||||
|
(blur)="blur()"
|
||||||
/>
|
/>
|
||||||
<div *ngIf="modelForm.value !== ''" class="input-suffix">
|
<div *ngIf="modelForm.value !== ''" class="input-suffix">
|
||||||
<mat-icon (mouseup)="clear()">close</mat-icon>
|
<mat-icon (click)="clear()">close</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,15 @@
|
|||||||
|
@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 {
|
.input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -14,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
&.input-suffix {
|
&.input-suffix {
|
||||||
right: 8px;
|
right: 8px;
|
||||||
color: #666;
|
color: $foreground-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +39,8 @@
|
|||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
color: #666;
|
background: mat-color($background, background);
|
||||||
|
color: $foreground-color;
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
@ -39,9 +52,23 @@
|
|||||||
width: 200px;
|
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 {
|
mat-icon {
|
||||||
cursor: pointer;
|
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 { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { debounceTime } from 'rxjs/operators';
|
import { debounceTime } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type declared to see, which values are possible for some inputs.
|
||||||
|
*/
|
||||||
|
export type Size = 'small' | 'medium' | 'large';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-rounded-input',
|
selector: 'os-rounded-input',
|
||||||
templateUrl: './rounded-input.component.html',
|
templateUrl: './rounded-input.component.html',
|
||||||
styleUrls: ['./rounded-input.component.scss']
|
styleUrls: ['./rounded-input.component.scss']
|
||||||
})
|
})
|
||||||
export class RoundedInputComponent implements OnInit, OnDestroy {
|
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.
|
* Reference to the `<input />`-element.
|
||||||
*/
|
*/
|
||||||
@ -45,7 +68,13 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
|||||||
* Defaults to `'medium'`.
|
* Defaults to `'medium'`.
|
||||||
*/
|
*/
|
||||||
@Input()
|
@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`.
|
* Custom `FormControl`.
|
||||||
@ -59,6 +88,12 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
public autofocus = false;
|
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.
|
* 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()
|
@Input()
|
||||||
public clearOnEscape = true;
|
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.
|
* EventHandler for the input-changes.
|
||||||
*/
|
*/
|
||||||
@ -89,11 +138,24 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
|||||||
@Output()
|
@Output()
|
||||||
public onkeyup: EventEmitter<KeyboardEvent> = new EventEmitter();
|
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.
|
* Subscription, that will handle the value-changes of the input.
|
||||||
*/
|
*/
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable for the border-radius as class.
|
||||||
|
*/
|
||||||
|
private _borderRadius = 'large-border-radius';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor
|
* Default constructor
|
||||||
*/
|
*/
|
||||||
@ -107,6 +169,9 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
|||||||
* Overwrites `OnInit` - initializes the subscription.
|
* Overwrites `OnInit` - initializes the subscription.
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
|
if (this.autofocus) {
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
this.subscription = this.modelForm.valueChanges
|
this.subscription = this.modelForm.valueChanges
|
||||||
.pipe(debounceTime(this.lazyInput ? 250 : 0))
|
.pipe(debounceTime(this.lazyInput ? 250 : 0))
|
||||||
.subscribe(nextValue => {
|
.subscribe(nextValue => {
|
||||||
@ -128,10 +193,26 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
|
|||||||
* Function to clear the input and refocus it.
|
* Function to clear the input and refocus it.
|
||||||
*/
|
*/
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this.osInput.nativeElement.focus();
|
this.focus();
|
||||||
this.modelForm.setValue('');
|
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.
|
* Function to handle typing.
|
||||||
* Useful to listen to special keys.
|
* Useful to listen to special keys.
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
os-rounded-input {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-filter {
|
.active-filter {
|
||||||
@ -74,7 +77,3 @@ span.right-with-margin {
|
|||||||
height: 0px;
|
height: 0px;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
os-rounded-input {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
@ -19,7 +19,7 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
|
|||||||
public title: string;
|
public title: string;
|
||||||
public mediafile?: FileMetadata;
|
public mediafile?: FileMetadata;
|
||||||
public media_url_prefix: string;
|
public media_url_prefix: string;
|
||||||
public filesize: string;
|
public filesize?: string;
|
||||||
public access_groups_id: number[];
|
public access_groups_id: number[];
|
||||||
public create_timestamp: string;
|
public create_timestamp: string;
|
||||||
public parent_id: number | null;
|
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 { AttachmentControlComponent } from './components/attachment-control/attachment-control.component';
|
||||||
import { RoundedInputComponent } from './components/rounded-input/rounded-input.component';
|
import { RoundedInputComponent } from './components/rounded-input/rounded-input.component';
|
||||||
import { ProgressSnackBarComponent } from './components/progress-snack-bar/progress-snack-bar.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.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -157,7 +162,8 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
|||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
PblNgridModule,
|
PblNgridModule,
|
||||||
PblNgridMaterialModule,
|
PblNgridMaterialModule,
|
||||||
PblNgridTargetEventsModule
|
PblNgridTargetEventsModule,
|
||||||
|
PdfViewerModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@ -197,6 +203,7 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
|||||||
NgxFileDropModule,
|
NgxFileDropModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
OpenSlidesTranslateModule,
|
OpenSlidesTranslateModule,
|
||||||
|
PdfViewerModule,
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
IsSuperAdminDirective,
|
IsSuperAdminDirective,
|
||||||
DomChangeDirective,
|
DomChangeDirective,
|
||||||
@ -236,7 +243,10 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
|||||||
ListViewTableComponent,
|
ListViewTableComponent,
|
||||||
AgendaContentObjectFormComponent,
|
AgendaContentObjectFormComponent,
|
||||||
ExtensionFieldComponent,
|
ExtensionFieldComponent,
|
||||||
RoundedInputComponent
|
RoundedInputComponent,
|
||||||
|
GlobalSpinnerComponent,
|
||||||
|
OverlayComponent,
|
||||||
|
PreviewComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -276,7 +286,11 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
|||||||
ExtensionFieldComponent,
|
ExtensionFieldComponent,
|
||||||
AttachmentControlComponent,
|
AttachmentControlComponent,
|
||||||
RoundedInputComponent,
|
RoundedInputComponent,
|
||||||
ProgressSnackBarComponent
|
ProgressSnackBarComponent,
|
||||||
|
GlobalSpinnerComponent,
|
||||||
|
SuperSearchComponent,
|
||||||
|
OverlayComponent,
|
||||||
|
PreviewComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||||
@ -294,7 +308,8 @@ import { ProgressSnackBarComponent } from './components/progress-snack-bar/progr
|
|||||||
PromptDialogComponent,
|
PromptDialogComponent,
|
||||||
ChoiceDialogComponent,
|
ChoiceDialogComponent,
|
||||||
ProjectionDialogComponent,
|
ProjectionDialogComponent,
|
||||||
ProgressSnackBarComponent
|
ProgressSnackBarComponent,
|
||||||
|
SuperSearchComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule {}
|
export class SharedModule {}
|
||||||
|
@ -127,7 +127,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
|
|||||||
}
|
}
|
||||||
|
|
||||||
public formatForSearch(): SearchRepresentation {
|
public formatForSearch(): SearchRepresentation {
|
||||||
return [this.title];
|
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDetailStateURL(): string {
|
public getDetailStateURL(): string {
|
||||||
|
@ -16,6 +16,14 @@ export function isSearchable(object: any): object is Searchable {
|
|||||||
export interface Searchable extends DetailNavigable {
|
export interface Searchable extends DetailNavigable {
|
||||||
/**
|
/**
|
||||||
* Should return strings that represents the object.
|
* 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;
|
formatForSearch: () => SearchRepresentation;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { RouterModule, Routes } from '@angular/router';
|
|||||||
import { ErrorComponent } from './components/error/error.component';
|
import { ErrorComponent } from './components/error/error.component';
|
||||||
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
||||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||||
import { SearchComponent } from './components/search/search.component';
|
|
||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -22,10 +21,6 @@ const routes: Routes = [
|
|||||||
path: 'privacypolicy',
|
path: 'privacypolicy',
|
||||||
component: PrivacyPolicyComponent
|
component: PrivacyPolicyComponent
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'search',
|
|
||||||
component: SearchComponent
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'error',
|
path: 'error',
|
||||||
component: ErrorComponent
|
component: ErrorComponent
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<div
|
<os-overlay *ngIf="isVisible">
|
||||||
*ngIf="isVisible"
|
|
||||||
class="global-spinner-component">
|
|
||||||
<div class="spinner-container">
|
|
||||||
<div>
|
<div>
|
||||||
<div class="spinner"></div>
|
<mat-progress-spinner
|
||||||
|
[mode]="mode"
|
||||||
|
[diameter]="diameter"
|
||||||
|
[strokeWidth]="stroke"
|
||||||
|
[value]="value"
|
||||||
|
class="spinner"
|
||||||
|
></mat-progress-spinner>
|
||||||
<div class="text">{{ text }}</div>
|
<div class="text">{{ text }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</os-overlay>
|
||||||
<div class="backdrop"></div>
|
|
||||||
</div>
|
|
||||||
|
@ -1,71 +1,8 @@
|
|||||||
@import '~@angular/material/theming';
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
@mixin os-global-spinner-theme($theme) {
|
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-spinner-component {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
.spinner-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
position: absolute;
|
display: inline-block;
|
||||||
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 {
|
||||||
@ -74,10 +11,3 @@
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backdrop {
|
|
||||||
z-index: 899;
|
|
||||||
background-color: #303030;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
// External imports
|
// External imports
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ProgressSpinnerMode } from '@angular/material';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
import { OverlayService, SpinnerConfig } from 'app/core/ui-services/overlay.service';
|
||||||
|
|
||||||
// Internal imports
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for the global spinner.
|
* Component for the global spinner.
|
||||||
@ -17,6 +16,26 @@ import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
|||||||
styleUrls: ['./global-spinner.component.scss']
|
styleUrls: ['./global-spinner.component.scss']
|
||||||
})
|
})
|
||||||
export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
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.
|
* 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 translate Service to get translations for the messages.
|
||||||
* @param cd Service to manual initiate a change of the UI.
|
* @param cd Service to manual initiate a change of the UI.
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private spinnerService: SpinnerService,
|
private overlayService: OverlayService,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
@ -53,14 +72,17 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
|||||||
* Init method
|
* Init method
|
||||||
*/
|
*/
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
this.spinnerSubscription = this.spinnerService // subscribe to the service.
|
this.spinnerSubscription = this.overlayService // subscribe to the service.
|
||||||
.getVisibility()
|
.getSpinner()
|
||||||
.subscribe((value: { isVisible: boolean; text: string }) => {
|
.subscribe((value: { isVisible: boolean; text: string; config?: SpinnerConfig }) => {
|
||||||
this.isVisible = value.isVisible;
|
this.isVisible = value.isVisible;
|
||||||
this.text = this.translate.instant(value.text);
|
this.text = this.translate.instant(value.text);
|
||||||
if (!this.text) {
|
if (!this.text) {
|
||||||
this.text = this.LOADING;
|
this.text = this.LOADING;
|
||||||
}
|
}
|
||||||
|
if (value.config) {
|
||||||
|
this.setConfig(value.config);
|
||||||
|
}
|
||||||
this.cd.detectChanges();
|
this.cd.detectChanges();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -77,4 +99,16 @@ export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.spinnerSubscription = null;
|
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 { ErrorComponent } from './components/error/error.component';
|
||||||
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
|
||||||
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
|
||||||
import { SearchComponent } from './components/search/search.component';
|
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, CommonRoutingModule, SharedModule],
|
imports: [CommonModule, CommonRoutingModule, SharedModule],
|
||||||
declarations: [
|
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, CountUsersComponent, ErrorComponent]
|
||||||
PrivacyPolicyComponent,
|
|
||||||
StartComponent,
|
|
||||||
LegalNoticeComponent,
|
|
||||||
SearchComponent,
|
|
||||||
CountUsersComponent,
|
|
||||||
ErrorComponent
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class OsCommonModule {}
|
export class OsCommonModule {}
|
||||||
|
@ -10,7 +10,7 @@ import { Subscription } from 'rxjs';
|
|||||||
import { AuthService } from 'app/core/core-services/auth.service';
|
import { AuthService } from 'app/core/core-services/auth.service';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { LoginDataService } from 'app/core/ui-services/login-data.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 { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
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 httpService used to get information before the login
|
||||||
* @param OpenSlides The Service for OpenSlides
|
* @param OpenSlides The Service for OpenSlides
|
||||||
* @param loginDataService provide information about the legal notice and privacy policy
|
* @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(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
@ -74,11 +74,10 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private loginDataService: LoginDataService,
|
private loginDataService: LoginDataService,
|
||||||
private spinnerService: SpinnerService
|
private overlayService: OverlayService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
// Hide the spinner if the user is at `login-mask`
|
// Hide the spinner if the user is at `login-mask`
|
||||||
spinnerService.setVisibility(false);
|
|
||||||
this.createForm();
|
this.createForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +137,7 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
|
|||||||
this.loginErrorMsg = '';
|
this.loginErrorMsg = '';
|
||||||
try {
|
try {
|
||||||
await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => {
|
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.
|
this.clearOperatorSubscription(); // We take control, not the subscription.
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -51,11 +51,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get title(): string {
|
public get title(): string {
|
||||||
if (this.is_directory) {
|
return this.filename;
|
||||||
return this.mediafile.path;
|
|
||||||
} else {
|
|
||||||
return this.mediafile.title;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get filename(): string {
|
public get filename(): string {
|
||||||
@ -78,7 +74,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
return !this.is_directory;
|
return !this.is_directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get size(): string {
|
public get size(): string | null {
|
||||||
return this.mediafile.filesize;
|
return this.mediafile.filesize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +86,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
return this.mediafile.url;
|
return this.mediafile.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get type(): string {
|
public get type(): string | null {
|
||||||
return this.mediafile.mediafile ? this.mediafile.mediafile.type : '';
|
return this.mediafile.mediafile ? this.mediafile.mediafile.type : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,11 +99,23 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public formatForSearch(): SearchRepresentation {
|
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 {
|
public getDetailStateURL(): string {
|
||||||
return this.url;
|
return this.is_directory ? ('/mediafiles/files/' + this.path).slice(0, -1) : this.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||||
|
@ -107,7 +107,10 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
|
|||||||
}
|
}
|
||||||
|
|
||||||
public formatForSearch(): SearchRepresentation {
|
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 {
|
public getDetailStateURL(): string {
|
||||||
|
@ -42,7 +42,7 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public formatForSearch(): SearchRepresentation {
|
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 { _ } from 'app/core/translate/translation-marker';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
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 { Motion, MotionComment } from 'app/shared/models/motions/motion';
|
||||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||||
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
|
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
@ -351,24 +351,50 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public formatForSearch(): SearchRepresentation {
|
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) {
|
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));
|
properties.push({ key: 'Tags', value: this.tags.map(tag => tag.getTitle()).join(', ') });
|
||||||
searchValues = searchValues.concat(this.supporters.map(user => user.full_name));
|
properties.push({
|
||||||
searchValues = searchValues.concat(this.tags.map(tag => tag.getTitle()));
|
key: 'Comments',
|
||||||
searchValues = searchValues.concat(this.motion.comments.map(comment => comment.comment));
|
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) {
|
if (this.motion_block) {
|
||||||
searchValues.push(this.motion_block.getTitle());
|
metaData.blockProperties.push({ key: 'Motion block', value: this.motion_block.getTitle() });
|
||||||
}
|
}
|
||||||
if (this.category) {
|
if (this.category) {
|
||||||
searchValues.push(this.category.getTitle());
|
metaData.blockProperties.push({ key: 'Category', value: this.category.getTitle() });
|
||||||
}
|
}
|
||||||
if (this.state) {
|
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 {
|
public getDetailStateURL(): string {
|
||||||
|
@ -36,7 +36,7 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public formatForSearch(): SearchRepresentation {
|
public formatForSearch(): SearchRepresentation {
|
||||||
return [this.title];
|
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDetailStateURL(): string {
|
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 { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||||
import { OsFilterOptionCondition } from 'app/core/ui-services/base-filter-list.service';
|
import { OsFilterOptionCondition } from 'app/core/ui-services/base-filter-list.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.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 { ColumnRestriction } from 'app/shared/components/list-view-table/list-view-table.component';
|
||||||
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
|
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
import { BaseListViewComponent } from 'app/site/base/base-list-view';
|
import { BaseListViewComponent } from 'app/site/base/base-list-view';
|
||||||
@ -206,7 +206,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
|
|||||||
public multiselectService: MotionMultiselectService,
|
public multiselectService: MotionMultiselectService,
|
||||||
public perms: LocalPermissionsService,
|
public perms: LocalPermissionsService,
|
||||||
private motionExport: MotionExportService,
|
private motionExport: MotionExportService,
|
||||||
private spinnerService: SpinnerService
|
private overlayService: OverlayService
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar, storage);
|
super(titleService, translate, matSnackBar, storage);
|
||||||
this.canMultiSelect = true;
|
this.canMultiSelect = true;
|
||||||
@ -372,7 +372,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.raiseError(e);
|
this.raiseError(e);
|
||||||
} finally {
|
} 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 { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ChoiceService } from 'app/core/ui-services/choice.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 { PersonalNoteService } from 'app/core/ui-services/personal-note.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.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 { TreeService } from 'app/core/ui-services/tree.service';
|
||||||
import { ChoiceDialogOptions } from 'app/shared/components/choice-dialog/choice-dialog.component';
|
import { ChoiceDialogOptions } from 'app/shared/components/choice-dialog/choice-dialog.component';
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
@ -45,7 +45,7 @@ export class MotionMultiselectService {
|
|||||||
* @param httpService
|
* @param httpService
|
||||||
* @param treeService
|
* @param treeService
|
||||||
* @param personalNoteService
|
* @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(
|
public constructor(
|
||||||
private repo: MotionRepositoryService,
|
private repo: MotionRepositoryService,
|
||||||
@ -61,7 +61,7 @@ export class MotionMultiselectService {
|
|||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
private treeService: TreeService,
|
private treeService: TreeService,
|
||||||
private personalNoteService: PersonalNoteService,
|
private personalNoteService: PersonalNoteService,
|
||||||
private spinnerService: SpinnerService
|
private overlayService: OverlayService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,10 +81,10 @@ export class MotionMultiselectService {
|
|||||||
`\n${i} ` +
|
`\n${i} ` +
|
||||||
this.translate.instant('of') +
|
this.translate.instant('of') +
|
||||||
` ${motions.length}`;
|
` ${motions.length}`;
|
||||||
this.spinnerService.setVisibility(true, message);
|
this.overlayService.setSpinner(true, message);
|
||||||
await this.repo.delete(motion);
|
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);
|
const selectedChoice = await this.choiceService.open(title, choices);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
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);
|
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);
|
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/', {
|
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
||||||
motions: requestData
|
motions: requestData
|
||||||
});
|
});
|
||||||
|
// .catch(error => {
|
||||||
|
// this.overlayService.setSpinner(false);
|
||||||
|
// throw error;
|
||||||
|
// });
|
||||||
|
// this.overlayService.setSpinner(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,8 +180,13 @@ export class MotionMultiselectService {
|
|||||||
);
|
);
|
||||||
if (selectedChoice) {
|
if (selectedChoice) {
|
||||||
const message = this.translate.instant(this.messageForSpinner);
|
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);
|
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);
|
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 });
|
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);
|
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 });
|
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) {
|
if (selectedChoice) {
|
||||||
const message = this.translate.instant(this.messageForSpinner);
|
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);
|
const blockId = selectedChoice.action ? null : (selectedChoice.items as number);
|
||||||
await this.repo.setMultiMotionBlock(motions, blockId);
|
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) {
|
if (selectedChoice && motions.length) {
|
||||||
const message = this.translate.instant(`I have ${motions.length} favorite motions. Please wait ...`);
|
const message = this.translate.instant(`I have ${motions.length} favorite motions. Please wait ...`);
|
||||||
const star = (selectedChoice.items as number) === choices[0].id;
|
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);
|
await this.personalNoteService.bulkSetStar(motions, star);
|
||||||
|
// this.overlayService.setSpinner(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,13 +90,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<a
|
<a [@navItemAnim] *ngIf="vp.isMobile" mat-list-item routerLinkActive="active" (click)="toggleSearch()">
|
||||||
[@navItemAnim]
|
|
||||||
mat-list-item
|
|
||||||
routerLink="/search"
|
|
||||||
routerLinkActive="active"
|
|
||||||
(click)="mobileAutoCloseNav()"
|
|
||||||
>
|
|
||||||
<mat-icon>search</mat-icon>
|
<mat-icon>search</mat-icon>
|
||||||
<span translate>Search</span>
|
<span translate>Search</span>
|
||||||
</a>
|
</a>
|
||||||
@ -156,3 +150,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</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 { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSidenav } from '@angular/material/sidenav';
|
import { MatSidenav } from '@angular/material/sidenav';
|
||||||
@ -12,6 +12,7 @@ import { filter } from 'rxjs/operators';
|
|||||||
import { navItemAnim, pageTransition } from '../shared/animations';
|
import { navItemAnim, pageTransition } from '../shared/animations';
|
||||||
import { OfflineService } from 'app/core/core-services/offline.service';
|
import { OfflineService } from 'app/core/core-services/offline.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.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 { UpdateService } from 'app/core/ui-services/update.service';
|
||||||
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
import { langToLocale } from 'app/shared/utils/lang-to-locale';
|
||||||
import { AuthService } from '../core/core-services/auth.service';
|
import { AuthService } from '../core/core-services/auth.service';
|
||||||
@ -100,9 +101,11 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
|||||||
public mainMenuService: MainMenuService,
|
public mainMenuService: MainMenuService,
|
||||||
public OSStatus: OpenSlidesStatusService,
|
public OSStatus: OpenSlidesStatusService,
|
||||||
public timeTravel: TimeTravelService,
|
public timeTravel: TimeTravelService,
|
||||||
private matSnackBar: MatSnackBar
|
private matSnackBar: MatSnackBar,
|
||||||
|
private overlayService: OverlayService
|
||||||
) {
|
) {
|
||||||
super(title, translate);
|
super(title, translate);
|
||||||
|
overlayService.setSpinner(true, translate.instant('Loading data. Please wait...'));
|
||||||
|
|
||||||
this.operator.getViewUserObservable().subscribe(user => {
|
this.operator.getViewUserObservable().subscribe(user => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -208,6 +211,15 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
|||||||
this.sideNav.toggle();
|
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
|
* Automatically close the navigation in while navigating in mobile mode
|
||||||
*/
|
*/
|
||||||
@ -250,6 +262,7 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public logout(): void {
|
public logout(): void {
|
||||||
this.authService.logout();
|
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.
|
* Get the timestamp for the current point in history mode.
|
||||||
* Tries to detect the ideal timestamp format using the translation service
|
* 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 {
|
public getHistoryTimestamp(): string {
|
||||||
return this.OSStatus.getHistoryTimeStamp(langToLocale(this.translate.currentLang));
|
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 {
|
public formatForSearch(): SearchRepresentation {
|
||||||
return [this.name];
|
return { properties: [{ key: 'Name', value: this.name }], searchValue: [this.name] };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDetailStateURL(): string {
|
public getDetailStateURL(): string {
|
||||||
|
@ -46,7 +46,10 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public formatForSearch(): SearchRepresentation {
|
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 {
|
public getDetailStateURL(): string {
|
||||||
|
@ -127,7 +127,14 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public formatForSearch(): SearchRepresentation {
|
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 {
|
public getDetailStateURL(): string {
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
[page]="data.element.page || 1"
|
[page]="data.element.page || 1"
|
||||||
[zoom]="zoom"
|
[zoom]="zoom"
|
||||||
[src]="url"
|
[src]="url"
|
||||||
style="display: block;"></pdf-viewer>
|
style="display: block;"
|
||||||
|
></pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer';
|
|
||||||
|
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
import { SLIDE } from 'app/slides/slide-token';
|
import { SLIDE } from 'app/slides/slide-token';
|
||||||
import { MediafileSlideComponent } from './mediafile-slide.component';
|
import { MediafileSlideComponent } from './mediafile-slide.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, SharedModule, PdfViewerModule],
|
imports: [CommonModule, SharedModule],
|
||||||
declarations: [MediafileSlideComponent],
|
declarations: [MediafileSlideComponent],
|
||||||
providers: [{ provide: SLIDE, useValue: MediafileSlideComponent }],
|
providers: [{ provide: SLIDE, useValue: MediafileSlideComponent }],
|
||||||
entryComponents: [MediafileSlideComponent]
|
entryComponents: [MediafileSlideComponent]
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
@import './app/shared/components/icon-container/icon-container.component.scss';
|
@import './app/shared/components/icon-container/icon-container.component.scss';
|
||||||
@import './app/site/common/components/start/start.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/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 */
|
/** fonts */
|
||||||
@import './assets/styles/fonts.scss';
|
@import './assets/styles/fonts.scss';
|
||||||
@ -42,6 +44,8 @@ $narrow-spacing: (
|
|||||||
@include os-global-spinner-theme($theme);
|
@include os-global-spinner-theme($theme);
|
||||||
@include os-tile-style($theme);
|
@include os-tile-style($theme);
|
||||||
@include os-mediafile-list-theme($theme);
|
@include os-mediafile-list-theme($theme);
|
||||||
|
@include os-super-search-style($theme);
|
||||||
|
@include os-rounded-input-style($theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load projector specific SCSS values */
|
/** Load projector specific SCSS values */
|
||||||
|
Loading…
Reference in New Issue
Block a user