1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

implementing preview settings #80, #381

This commit is contained in:
Patrik J. Braun 2022-01-26 22:50:49 +01:00
parent b169fa67b3
commit 1255246b0f
24 changed files with 819 additions and 322 deletions

View File

@ -14,6 +14,7 @@ import {
ServerIndexingConfig,
ServerJobConfig,
ServerPhotoConfig,
ServerPreviewConfig,
ServerThumbnailConfig,
ServerVideoConfig
} from '../../../common/config/private/PrivateConfig';
@ -101,6 +102,31 @@ export class SettingsMWs {
}
}
public static async updatePreviewSettings(req: Request, res: Response, next: NextFunction): Promise<any> {
if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed'));
}
try {
await ConfigDiagnostics.testPreviewConfig(req.body.settings as ServerPreviewConfig);
Config.Server.Preview = (req.body.settings as ServerPreviewConfig);
// only updating explicitly set config (not saving config set by the diagnostics)
const original = await Config.original();
original.Server.Preview = (req.body.settings as ServerPreviewConfig);
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
return next();
} catch (err) {
if (err instanceof Error) {
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + err.toString(), err));
}
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err));
}
}
public static async updateVideoSettings(req: Request, res: Response, next: NextFunction): Promise<any> {
if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed'));

View File

@ -24,8 +24,12 @@ import {
ServerDataBaseConfig,
ServerJobConfig,
ServerPhotoConfig,
ServerPreviewConfig,
ServerVideoConfig
} from '../../../common/config/private/PrivateConfig';
import {SearchQueryParser} from '../../../common/SearchQueryParser';
import {SearchQueryTypes, TextSearch} from '../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../common/Utils';
const LOG_TAG = '[ConfigDiagnostics]';
@ -225,6 +229,13 @@ export class ConfigDiagnostics {
}
static async testPreviewConfig(settings: ServerPreviewConfig): Promise<void> {
const sp = new SearchQueryParser();
if (!Utils.equalsFilter(sp.parse(sp.stringify(settings.SearchQuery)), settings.SearchQuery)) {
throw new Error('SearchQuery is not valid');
}
}
static async runDiagnostics(): Promise<void> {
if (Config.Server.Database.type !== DatabaseType.memory) {
@ -315,6 +326,16 @@ export class ConfigDiagnostics {
Config.Client.Search.enabled = false;
}
try {
await ConfigDiagnostics.testPreviewConfig(Config.Server.Preview);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning('Preview settings are not valid, resetting search query', err.toString());
Logger.warn(LOG_TAG, 'Preview settings are not valid, resetting search query', err.toString());
Config.Server.Preview.SearchQuery = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
}
try {
await ConfigDiagnostics.testFacesConfig(Config.Client.Faces, Config);
} catch (ex) {

View File

@ -68,6 +68,12 @@ export class SettingsRouter {
SettingsMWs.updateSearchSettings,
RenderingMWs.renderOK
);
app.put('/api/settings/preview',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
SettingsMWs.updatePreviewSettings,
RenderingMWs.renderOK
);
app.put('/api/settings/faces',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),

View File

@ -5,7 +5,7 @@ import {ClientConfig} from '../public/ClientConfig';
import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
import {DefaultsJobs} from '../../entities/job/JobDTO';
import {SearchQueryDTO} from '../../entities/SearchQueryDTO';
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../entities/SearchQueryDTO';
import {SortingMethods} from '../../entities/SortingMethods';
import {UserRoles} from '../../entities/UserDTO';
@ -334,7 +334,7 @@ export class ServerPhotoConfig {
@SubConfigClass()
export class ServerPreviewConfig {
@ConfigProperty({type: 'object'})
SearchQuery: SearchQueryDTO = null;
SearchQuery: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
@ConfigProperty({arrayType: SortingMethods})
Sorting: SortingMethods[] = [
SortingMethods.descRating,

View File

@ -95,7 +95,7 @@ import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/
import {StringifySearchQuery} from './pipes/StringifySearchQuery';
import {AutoCompleteService} from './ui/gallery/search/autocomplete.service';
import {SearchQueryParserService} from './ui/gallery/search/search-query-parser.service';
import {GallerySearchFieldComponent} from './ui/gallery/search/search-field/search-field.gallery.component';
import {GallerySearchFieldBaseComponent} from './ui/gallery/search/search-field-base/search-field-base.gallery.component';
import {AppRoutingModule} from './app.routing';
import {CookieService} from 'ngx-cookie-service';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
@ -112,6 +112,8 @@ import {MDFilesFilterPipe} from './pipes/MDFilesFilterPipe';
import {FileDTOToPathPipe} from './pipes/FileDTOToPathPipe';
import {BlogService} from './ui/gallery/blog/blog.service';
import {PhotoFilterPipe} from './pipes/PhotoFilterPipe';
import {PreviewSettingsComponent} from './ui/settings/preview/preview.settings.component';
import {GallerySearchFieldComponent} from './ui/gallery/search/search-field/search-field.gallery.component';
@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
@ -206,6 +208,7 @@ Marker.prototype.options.icon = iconDefault;
FrameComponent,
GallerySearchComponent,
GallerySearchQueryEntryComponent,
GallerySearchFieldBaseComponent,
GallerySearchFieldComponent,
GallerySearchQueryBuilderComponent,
GalleryShareComponent,
@ -241,6 +244,7 @@ Marker.prototype.options.icon = iconDefault;
JobProgressComponent,
JobsSettingsComponent,
JobButtonComponent,
PreviewSettingsComponent,
// Pipes
StringifyRole,

View File

@ -25,7 +25,8 @@
{{notification.details | json}}
<ng-container *ngIf="notification.request">
<br/>
Request: "{{notification.request.method}}", url: "{{notification.request.url}}", status code: "{{notification.request.statusCode}}"
Request: "{{notification.request.method}}", url: "{{notification.request.url}}", status code:
"{{notification.request.statusCode}}"
</ng-container>
</div>
</ng-container>
@ -74,7 +75,8 @@
<nav class="nav flex-column sticky-top">
<div class="card">
<div class="card-body text-md-left text-center align-content-md-start align-content-center">
<h5 i18n="title of left card in settings page that contains settings contents" class="card-title">Menu</h5>
<h5 i18n="title of left card in settings page that contains settings contents" class="card-title">
Menu</h5>
<button class="btn btn-link nav-link text-md-left py-md-1 px-md-0"
*ngFor="let s of contents; let i=index;"
(click)="scrollTo(i)"
@ -104,6 +106,9 @@
<app-settings-thumbnail #setting #thumbnail
[hidden]="!thumbnail.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-thumbnail>
<app-settings-preview #setting #preview
[hidden]="!preview.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-preview>
<app-settings-search #setting #search
[hidden]="!search.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-search>
@ -126,8 +131,8 @@
[hidden]="!faces.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-faces>
<app-settings-albums #setting #albums
[hidden]="!albums.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-albums>
[hidden]="!albums.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-albums>
<app-settings-indexing #setting #indexing
[hidden]="!indexing.HasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-indexing>
@ -141,7 +146,7 @@
<div class="col-12">
<div class="text-right">
<ng-container i18n>Up time</ng-container><!--
-->: {{(settingsService.settings | async).Server.Environment.upTime | date:'medium'}}
-->: {{(settingsService.settings | async).Server.Environment.upTime | date:'medium'}}
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
import {SearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO';
export class AutoCompleteRenderItem {
public preText = '';
public highLightText = '';
public postText = '';
public type: SearchQueryTypes;
public queryHint: string;
public notSearchable: boolean;
constructor(public text: string, searchText: string, type: SearchQueryTypes, queryHint: string, notSearchable = false) {
const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase());
if (preIndex > -1) {
this.preText = text.substring(0, preIndex);
this.highLightText = text.substring(preIndex, preIndex + searchText.length);
this.postText = text.substring(preIndex + searchText.length);
} else {
this.postText = text;
}
this.type = type;
this.queryHint = queryHint;
this.notSearchable = notSearchable;
}
}

View File

@ -1,9 +1,10 @@
<app-gallery-search-field [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
(search)="search.emit()"
name="form-search-field">
<app-gallery-search-field-base [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
(search)="search.emit()"
[placeholder]="placeholder"
name="form-search-field">
</app-gallery-search-field>
</app-gallery-search-field-base>
<hr>
<app-gallery-search-query-entry
[(ngModel)]="searchQueryDTO"

View File

@ -1,4 +1,4 @@
import {Component, EventEmitter, forwardRef, Output} from '@angular/core';
import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import {SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../../common/entities/SearchQueryDTO';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
import {SearchQueryParserService} from '../search-query-parser.service';
@ -23,6 +23,7 @@ import {SearchQueryParserService} from '../search-query-parser.service';
export class GallerySearchQueryBuilderComponent implements ControlValueAccessor, Validator {
public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''} as TextSearch;
@Output() search = new EventEmitter<void>();
@Input() placeholder: string;
public rawSearchText = '';
@ -59,6 +60,7 @@ export class GallerySearchQueryBuilderComponent implements ControlValueAccessor,
public writeValue(obj: any): void {
this.searchQueryDTO = obj;
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
}
registerOnChange(fn: (_: any) => void): void {

View File

@ -0,0 +1,69 @@
.autocomplete-list {
position: absolute;
left: 0;
top: 34px;
background-color: white;
width: 100%;
border: 1px solid #ccc;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 5px 0;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
z-index: 7;
}
.insert-button {
margin-right: -15px;
display: none;
margin-top: 2px;
}
.autocomplete-item-selected .insert-button {
display: block;
}
@media (hover: none) {
.insert-button {
display: block;
}
}
.autocomplete-item-selected .insert-button:hover {
color: black;
}
.autocomplete-item {
cursor: pointer;
padding-top: 2px;
padding-bottom: 2px;
font-size: 17px;
}
.autocomplete-item {
color: #333;
padding: 0 20px;
line-height: 1.42857143;
font-weight: 400;
display: block;
}
.autocomplete-item-selected {
background-color: #007bff;
color: #FFF;
}
.search-text {
z-index: 6;
width: 100%;
background: transparent;
}
.search-hint {
left: 0;
z-index: 1;
width: 100%;
position: absolute;
margin-left: 0 !important;
}

View File

@ -0,0 +1,60 @@
<div class="input-group">
<input type="text"
class="form-control search-text"
[placeholder]="placeholder"
(keyup)="onSearchChange($event)"
(blur)="onFocusLost()"
[(ngModel)]="rawSearchText"
(ngModelChange)="onChange()"
(keydown.enter)="OnEnter($event)"
(keydown.arrowRight)="applyHint($event)"
(keydown.arrowUp)="selectAutocompleteUp()"
(keydown.arrowDown)="selectAutocompleteDown()"
(scroll)="Scrolled()"
(selectionchange)="Scrolled()"
#name="ngModel"
size="30"
ngControl="search"
name="srch-term"
id="srch-term"
#SearchField
autocomplete="off">
<input type="text"
class="form-control search-hint"
[ngModel]="SearchHint"
size="30"
name="srch-term-hint"
id="srch-term-hint"
#SearchHintField
autocomplete="off">
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item"
[ngClass]="{'autocomplete-item-selected': highlightedAutoCompleteItem === i}"
(mouseover)="setMouseOverAutoCompleteItem(i)"
(click)="searchAutoComplete(item)"
*ngFor="let item of autoCompleteRenders; let i = index">
<div>
<span [ngSwitch]="item.type">
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
<span *ngSwitchCase="SearchQueryTypes.distance" class="oi oi-map-marker"></span>
</span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
<span class="oi oi-chevron-right insert-button float-right" (click)="applyAutoComplete(item)"
title="Insert"
i18n-title>
</span>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,251 @@
import {Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Output, ViewChild} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {BehaviorSubject, Subscription} from 'rxjs';
import {AutoCompleteService, RenderableAutoCompleteItem} from '../autocomplete.service';
import {MetadataSearchQueryTypes, SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
import {Config} from '../../../../../../common/config/public/Config';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
import {AutoCompleteRenderItem} from '../AutoCompleteRenderItem';
@Component({
selector: 'app-gallery-search-field-base',
templateUrl: './search-field-base.gallery.component.html',
styleUrls: ['./search-field-base.gallery.component.css'],
providers: [
AutoCompleteService, RouterLink,
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GallerySearchFieldBaseComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GallerySearchFieldBaseComponent),
multi: true
}
]
})
export class GallerySearchFieldBaseComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() placeholder = 'Search';
@Output() search = new EventEmitter<void>();
@ViewChild('SearchField', {static: false}) searchField: ElementRef;
@ViewChild('SearchHintField', {static: false}) searchHintField: ElementRef;
autoCompleteRenders: AutoCompleteRenderItem[] = [];
public rawSearchText = '';
mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes;
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
public highlightedAutoCompleteItem = -1;
private cache = {
lastAutocomplete: '',
lastInstantSearch: ''
};
private autoCompleteItemsSubscription: Subscription = null;
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
constructor(private autoCompleteService: AutoCompleteService,
public router: Router) {
this.SearchQueryTypes = SearchQueryTypes;
this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map((v): { value: string; key: SearchQueryTypes } => ({
key: v,
value: SearchQueryTypes[v]
}));
}
get SearchHint(): string {
if (!this.rawSearchText) {
return '';
}
if (!this.autoCompleteItems ||
!this.autoCompleteItems.value || this.autoCompleteItems.value.length === 0) {
return this.rawSearchText;
}
const itemIndex = this.highlightedAutoCompleteItem < 0 ? 0 : this.highlightedAutoCompleteItem;
const searchText = this.getAutocompleteToken();
if (searchText.current === '') {
return this.rawSearchText + this.autoCompleteItems.value[itemIndex].queryHint;
}
if (this.autoCompleteItems.value[0].queryHint.startsWith(searchText.current)) {
return this.rawSearchText + this.autoCompleteItems
.value[itemIndex].queryHint.substr(searchText.current.length);
}
return this.rawSearchText;
}
ngOnDestroy(): void {
if (this.autoCompleteItemsSubscription) {
this.autoCompleteItemsSubscription.unsubscribe();
this.autoCompleteItemsSubscription = null;
}
}
getAutocompleteToken(): { current: string, prev: string } {
if (this.rawSearchText.trim().length === 0) {
return {current: '', prev: ''};
}
const tokens = this.rawSearchText.split(' ');
return {
current: tokens[tokens.length - 1],
prev: (tokens.length > 2 ? tokens[tokens.length - 2] : '')
};
}
onSearchChange(event: KeyboardEvent): void {
const searchText = this.getAutocompleteToken();
if (Config.Client.Search.AutoComplete.enabled &&
this.cache.lastAutocomplete !== searchText.current) {
this.cache.lastAutocomplete = searchText.current;
this.autocomplete(searchText).catch(console.error);
}
}
public setMouseOverAutoComplete(value: boolean): void {
this.mouseOverAutoComplete = value;
}
public onFocusLost(): void {
if (this.mouseOverAutoComplete === false) {
this.autoCompleteRenders = [];
}
}
applyHint($event: any): void {
if ($event.target.selectionStart !== this.rawSearchText.length) {
return;
}
// if no item selected, apply hint
if (this.highlightedAutoCompleteItem < 0) {
this.rawSearchText = this.SearchHint;
this.onChange();
return;
}
// force apply selected autocomplete item
this.applyAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
}
applyAutoComplete(item: AutoCompleteRenderItem): void {
const token = this.getAutocompleteToken();
this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.current.length)
+ item.queryHint;
this.onChange();
this.emptyAutoComplete();
}
searchAutoComplete(item: AutoCompleteRenderItem): void {
this.applyAutoComplete(item);
if (!item.notSearchable) {
this.search.emit();
}
}
setMouseOverAutoCompleteItem(i: number): void {
this.highlightedAutoCompleteItem = i;
}
selectAutocompleteUp(): void {
if (this.highlightedAutoCompleteItem > 0) {
this.highlightedAutoCompleteItem--;
}
}
selectAutocompleteDown(): void {
if (this.autoCompleteItems &&
this.highlightedAutoCompleteItem < this.autoCompleteItems.value.length - 1) {
this.highlightedAutoCompleteItem++;
}
}
OnEnter($event: any): boolean {
// no autocomplete shown, just search whatever is there.
if (this.autoCompleteRenders.length === 0 || this.highlightedAutoCompleteItem === -1) {
this.search.emit();
return false;
}
// search selected autocomplete
this.searchAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
return false;
}
public onTouched(): void {
}
public writeValue(obj: any): void {
this.rawSearchText = obj;
}
registerOnChange(fn: (_: any) => void): void {
this.propagateChange = fn;
}
registerOnTouched(fn: () => void): void {
this.propagateTouch = fn;
}
public onChange(): void {
this.propagateChange(this.rawSearchText);
}
validate(control: FormControl): ValidationErrors {
return {required: true};
}
Scrolled(): void {
this.searchHintField.nativeElement.scrollLeft = this.searchField.nativeElement.scrollLeft;
}
private emptyAutoComplete(): void {
this.highlightedAutoCompleteItem = -1;
this.autoCompleteRenders = [];
}
private async autocomplete(searchText: { current: string, prev: string }): Promise<void> {
if (!Config.Client.Search.AutoComplete.enabled) {
return;
}
if (this.rawSearchText.trim().length > 0) { // are we searching for anything?
try {
if (this.autoCompleteItemsSubscription) {
this.autoCompleteItemsSubscription.unsubscribe();
this.autoCompleteItemsSubscription = null;
}
this.autoCompleteItems = this.autoCompleteService.autoComplete(searchText);
this.autoCompleteItemsSubscription = this.autoCompleteItems.subscribe((): void => {
this.showSuggestions(this.autoCompleteItems.value, searchText.current);
});
} catch (error) {
console.error(error);
}
} else {
this.emptyAutoComplete();
}
}
private showSuggestions(suggestions: RenderableAutoCompleteItem[], searchText: string): void {
this.emptyAutoComplete();
suggestions.forEach((item: RenderableAutoCompleteItem): void => {
const renderItem = new AutoCompleteRenderItem(item.text, this.autoCompleteService.getPrefixLessSearchText(searchText),
item.type, item.queryHint, item.notSearchable);
this.autoCompleteRenders.push(renderItem);
});
}
private propagateChange = (_: any): void => {
};
private propagateTouch = (_: any): void => {
};
}

View File

@ -1,61 +1,44 @@
<div class="input-group">
<input type="text"
class="form-control search-text"
i18n-placeholder
placeholder="Search"
(keyup)="onSearchChange($event)"
(blur)="onFocusLost()"
[(ngModel)]="rawSearchText"
(ngModelChange)="onChange()"
(keydown.enter)="OnEnter($event)"
(keydown.arrowRight)="applyHint($event)"
(keydown.arrowUp)="selectAutocompleteUp()"
(keydown.arrowDown)="selectAutocompleteDown()"
(scroll)="Scrolled()"
(selectionchange)="Scrolled()"
#name="ngModel"
size="30"
ngControl="search"
name="srch-term"
id="srch-term"
#SearchField
autocomplete="off">
<input type="text"
class="form-control search-hint"
[ngModel]="SearchHint"
size="30"
name="srch-term-hint"
id="srch-term-hint"
#SearchHintField
autocomplete="off">
<app-gallery-search-field-base [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
class="search-field col p-0"
(search)="search.emit()"
[placeholder]="placeholder"
name="search-field">
<div class="autocomplete-list" *ngIf="autoCompleteRenders.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item"
[ngClass]="{'autocomplete-item-selected': highlightedAutoCompleteItem === i}"
(mouseover)="setMouseOverAutoCompleteItem(i)"
(click)="searchAutoComplete(item)"
*ngFor="let item of autoCompleteRenders; let i = index">
<div>
<span [ngSwitch]="item.type">
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
<span *ngSwitchCase="SearchQueryTypes.distance" class="oi oi-map-marker"></span>
</span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
<span class="oi oi-chevron-right insert-button float-right" (click)="applyAutoComplete(item)"
title="Insert"
i18n-title>
</span>
</div>
</div>
</app-gallery-search-field-base>
<div class="input-group-btn col-auto pr-0" style="display: block">
<button class="btn btn-light" type="button" (click)="openSearchModal(searchModal)">
<span class="oi oi-chevron-bottom"></span>
</button>
</div>
</div>
<ng-template #searchModal>
<!-- sharing Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>{{placeholder}}</h5>
<button type="button" class="close" (click)="hideSearchModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #searchPanelForm="ngForm" class="form-horizontal">
<app-gallery-search-query-builder
name="search-query-builder"
[(ngModel)]="searchQueryDTO"
[placeholder]="placeholder"
(change)="onQueryChange()"
(search)="search.emit()">
</app-gallery-search-query-builder>
</form>
</div>
</ng-template>

View File

@ -1,10 +1,11 @@
import {Component, ElementRef, EventEmitter, forwardRef, OnDestroy, Output, ViewChild} from '@angular/core';
import {Component, EventEmitter, forwardRef, Input, Output} from '@angular/core';
import {Router, RouterLink} from '@angular/router';
import {BehaviorSubject, Subscription} from 'rxjs';
import {AutoCompleteService, RenderableAutoCompleteItem} from '../autocomplete.service';
import {MetadataSearchQueryTypes, SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
import {Config} from '../../../../../../common/config/public/Config';
import {AutoCompleteService} from '../autocomplete.service';
import {SearchQueryDTO} from '../../../../../../common/entities/SearchQueryDTO';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
import {SearchQueryParserService} from '../search-query-parser.service';
import {TemplateRef} from '../../../../../../../node_modules/@angular/core';
import {BsModalRef, BsModalService} from '../../../../../../../node_modules/ngx-bootstrap/modal';
@Component({
selector: 'app-gallery-search-field',
@ -24,160 +25,40 @@ import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Val
}
]
})
export class GallerySearchFieldComponent implements ControlValueAccessor, Validator, OnDestroy {
export class GallerySearchFieldComponent implements ControlValueAccessor, Validator {
@ViewChild('SearchField', {static: false}) searchField: ElementRef;
@ViewChild('SearchHintField', {static: false}) searchHintField: ElementRef;
@Output() search = new EventEmitter<void>();
autoCompleteRenders: AutoCompleteRenderItem[] = [];
@Input() placeholder: string;
public rawSearchText = '';
mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes;
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
public highlightedAutoCompleteItem = -1;
private cache = {
lastAutocomplete: '',
lastInstantSearch: ''
};
private autoCompleteItemsSubscription: Subscription = null;
private autoCompleteItems: BehaviorSubject<RenderableAutoCompleteItem[]>;
public searchQueryDTO: SearchQueryDTO;
private searchModalRef: BsModalRef;
constructor(private autoCompleteService: AutoCompleteService,
private searchQueryParserService: SearchQueryParserService,
private modalService: BsModalService,
public router: Router) {
this.SearchQueryTypes = SearchQueryTypes;
this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map((v): { value: string; key: SearchQueryTypes } => ({
key: v,
value: SearchQueryTypes[v]
}));
}
get SearchHint(): string {
if (!this.rawSearchText) {
return '';
}
if (!this.autoCompleteItems ||
!this.autoCompleteItems.value || this.autoCompleteItems.value.length === 0) {
return this.rawSearchText;
}
const itemIndex = this.highlightedAutoCompleteItem < 0 ? 0 : this.highlightedAutoCompleteItem;
const searchText = this.getAutocompleteToken();
if (searchText.current === '') {
return this.rawSearchText + this.autoCompleteItems.value[itemIndex].queryHint;
}
if (this.autoCompleteItems.value[0].queryHint.startsWith(searchText.current)) {
return this.rawSearchText + this.autoCompleteItems
.value[itemIndex].queryHint.substr(searchText.current.length);
}
return this.rawSearchText;
}
ngOnDestroy(): void {
if (this.autoCompleteItemsSubscription) {
this.autoCompleteItemsSubscription.unsubscribe();
this.autoCompleteItemsSubscription = null;
}
}
getAutocompleteToken(): { current: string, prev: string } {
if (this.rawSearchText.trim().length === 0) {
return {current: '', prev: ''};
}
const tokens = this.rawSearchText.split(' ');
return {
current: tokens[tokens.length - 1],
prev: (tokens.length > 2 ? tokens[tokens.length - 2] : '')
};
}
onSearchChange(event: KeyboardEvent): void {
const searchText = this.getAutocompleteToken();
if (Config.Client.Search.AutoComplete.enabled &&
this.cache.lastAutocomplete !== searchText.current) {
this.cache.lastAutocomplete = searchText.current;
this.autocomplete(searchText).catch(console.error);
}
}
public setMouseOverAutoComplete(value: boolean): void {
this.mouseOverAutoComplete = value;
}
public onFocusLost(): void {
if (this.mouseOverAutoComplete === false) {
this.autoCompleteRenders = [];
}
public async openSearchModal(template: TemplateRef<any>): Promise<void> {
this.searchModalRef = this.modalService.show(template, {class: 'modal-lg'});
document.body.style.paddingRight = '0px';
}
applyHint($event: any): void {
if ($event.target.selectionStart !== this.rawSearchText.length) {
return;
}
// if no item selected, apply hint
if (this.highlightedAutoCompleteItem < 0) {
this.rawSearchText = this.SearchHint;
this.onChange();
return;
}
// force apply selected autocomplete item
this.applyAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
public hideSearchModal(): void {
this.searchModalRef.hide();
this.searchModalRef = null;
}
applyAutoComplete(item: AutoCompleteRenderItem): void {
const token = this.getAutocompleteToken();
this.rawSearchText = this.rawSearchText.substr(0, this.rawSearchText.length - token.current.length)
+ item.queryHint;
this.onChange();
this.emptyAutoComplete();
}
searchAutoComplete(item: AutoCompleteRenderItem): void {
this.applyAutoComplete(item);
if (!item.notSearchable) {
this.search.emit();
}
}
setMouseOverAutoCompleteItem(i: number): void {
this.highlightedAutoCompleteItem = i;
}
selectAutocompleteUp(): void {
if (this.highlightedAutoCompleteItem > 0) {
this.highlightedAutoCompleteItem--;
}
}
selectAutocompleteDown(): void {
if (this.autoCompleteItems &&
this.highlightedAutoCompleteItem < this.autoCompleteItems.value.length - 1) {
this.highlightedAutoCompleteItem++;
}
}
OnEnter($event: any): void {
// no autocomplete shown, just search whatever is there.
if (this.autoCompleteRenders.length === 0 || this.highlightedAutoCompleteItem === -1) {
this.search.emit();
return;
}
// search selected autocomplete
this.searchAutoComplete(this.autoCompleteRenders[this.highlightedAutoCompleteItem]);
}
public onTouched(): void {
}
public writeValue(obj: any): void {
this.rawSearchText = obj;
this.searchQueryDTO = obj;
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
}
registerOnChange(fn: (_: any) => void): void {
@ -188,55 +69,30 @@ export class GallerySearchFieldComponent implements ControlValueAccessor, Valida
this.propagateTouch = fn;
}
public onChange(): void {
this.propagateChange(this.rawSearchText);
this.propagateChange(this.searchQueryDTO);
}
validate(control: FormControl): ValidationErrors {
return {required: true};
}
Scrolled(): void {
this.searchHintField.nativeElement.scrollLeft = this.searchField.nativeElement.scrollLeft;
onQueryChange(): void {
this.rawSearchText = this.searchQueryParserService.stringify(this.searchQueryDTO);
this.onChange();
}
private emptyAutoComplete(): void {
this.highlightedAutoCompleteItem = -1;
this.autoCompleteRenders = [];
}
private async autocomplete(searchText: { current: string, prev: string }): Promise<void> {
if (!Config.Client.Search.AutoComplete.enabled) {
return;
}
if (this.rawSearchText.trim().length > 0) { // are we searching for anything?
try {
if (this.autoCompleteItemsSubscription) {
this.autoCompleteItemsSubscription.unsubscribe();
this.autoCompleteItemsSubscription = null;
}
this.autoCompleteItems = this.autoCompleteService.autoComplete(searchText);
this.autoCompleteItemsSubscription = this.autoCompleteItems.subscribe((): void => {
this.showSuggestions(this.autoCompleteItems.value, searchText.current);
});
} catch (error) {
console.error(error);
}
} else {
this.emptyAutoComplete();
validateRawSearchText(): void {
try {
this.searchQueryDTO = this.searchQueryParserService.parse(this.rawSearchText);
this.onChange();
} catch (e) {
console.error(e);
}
}
private showSuggestions(suggestions: RenderableAutoCompleteItem[], searchText: string): void {
this.emptyAutoComplete();
suggestions.forEach((item: RenderableAutoCompleteItem): void => {
const renderItem = new AutoCompleteRenderItem(item.text, this.autoCompleteService.getPrefixLessSearchText(searchText),
item.type, item.queryHint, item.notSearchable);
this.autoCompleteRenders.push(renderItem);
});
}
private propagateChange = (_: any): void => {
};
@ -245,26 +101,3 @@ export class GallerySearchFieldComponent implements ControlValueAccessor, Valida
};
}
class AutoCompleteRenderItem {
public preText = '';
public highLightText = '';
public postText = '';
public type: SearchQueryTypes;
public queryHint: string;
public notSearchable: boolean;
constructor(public text: string, searchText: string, type: SearchQueryTypes, queryHint: string, notSearchable = false) {
const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase());
if (preIndex > -1) {
this.preText = text.substring(0, preIndex);
this.highLightText = text.substring(preIndex, preIndex + searchText.length);
this.postText = text.substring(preIndex + searchText.length);
} else {
this.postText = text;
}
this.type = type;
this.queryHint = queryHint;
this.notSearchable = notSearchable;
}
}

View File

@ -1,13 +1,13 @@
<form class="navbar-form" role="search" #SearchForm="ngForm">
<div class="input-group">
<app-gallery-search-field [(ngModel)]="rawSearchText"
<app-gallery-search-field-base [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
class="search-field"
(search)="Search()"
name="search-field">
</app-gallery-search-field>
</app-gallery-search-field-base>
<div class="input-group-btn" style="display: block">
<button class="btn btn-light" type="button"

View File

@ -176,7 +176,7 @@ export abstract class SettingsComponentDirective<T extends { [key: string]: any
ngOnChanges(): void {
this.hasAvailableSettings = ((this.settingsService.isSupported() &&
this.settingsService.showInSimplifiedMode())
this.settingsService.showInSimplifiedMode())
|| !this.simplifiedMode);
}

View File

@ -13,47 +13,89 @@
<div class="col-md-10">
<div class="input-group">
<input
*ngIf="!state.isEnumType && state.type !== 'boolean'"
[type]="type" [min]="state.min" [max]="state.max" class="form-control" [placeholder]="PlaceHolder"
<app-gallery-search-field
*ngIf="!IsEnumType && Type === 'searchQuery'"
[(ngModel)]="state.value"
[id]="idName"
[name]="idName"
[title]="title"
[(ngModel)]="value"
[disabled]="state.readonly || disabled"
(change)="onChange($event)"
placeholder="Search Query">
</app-gallery-search-field>
<input
*ngIf="!IsEnumType && Type !== 'boolean' && Type !== 'searchQuery'"
[type]="type" [min]="state.min" [max]="state.max" class="form-control"
[placeholder]="PlaceHolder"
[title]="title"
[(ngModel)]="StringValue"
(ngModelChange)="onChange($event)"
[name]="idName"
[disabled]="state.readonly || disabled"
[id]="idName"
required="required">
<select
*ngIf="state.isEnumType === true"
[id]="idName"
[name]="idName"
[title]="title"
(ngModelChange)="onChange($event)"
[disabled]="state.readonly || disabled"
class="form-control" [(ngModel)]="state.value">
<option *ngFor="let opt of optionsView" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
<ng-container *ngIf="IsEnumType || Type === 'boolean'">
<ng-container *ngFor="let _ of Values; let i=index">
<div class="row col-12 mt-1 m-0 p-0">
<div class="col p-0">
<select
*ngIf="IsEnumType"
[id]="'list_'+idName+i"
[name]="'list_'+idName+i"
[title]="title"
(ngModelChange)="onChange($event)"
[disabled]="state.readonly || disabled"
class="form-control" [(ngModel)]="Values[i]">
<option *ngFor="let opt of optionsView" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
<bSwitch
*ngIf="state.type === 'boolean'"
class="switch"
[id]="idName"
[name]="idName"
[title]="title"
[disabled]="state.readonly || disabled"
switch-on-color="primary"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
(ngModelChange)="onChange($event)"
[(ngModel)]="state.value">
</bSwitch>
<bSwitch
*ngIf="Type === 'boolean'"
class="switch"
[id]="'list_'+idName+i"
[name]="'list_'+idName+i"
[title]="title"
[disabled]="state.readonly || disabled"
switch-on-color="primary"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
(ngModelChange)="onChange($event)"
[(ngModel)]="Values[i]">
</bSwitch>
</div>
<ng-container *ngIf="state.type === 'array'">
<div class="col-auto pr-0">
<button class="btn btn-secondary float-right"
[id]="'list_btn_'+idName+i"
[name]="'list_btn_'+idName+i" (click)="remove(i)"><span
class="oi oi-trash"></span>
</button>
</div>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="state.type === 'array'">
<div class="col-12 p-0">
<button class="btn btn-primary mt-1 float-right"
[id]="'btn_add_'+idName"
[name]="'btn_add_'+idName"
(click)="AddNew()">+Add
</button>
</div>
</ng-container>
</ng-container>
<div class="input-group-append">
<span
triggers="mouseenter:mouseleave"

View File

@ -2,6 +2,22 @@ import {Component, forwardRef, Input, OnChanges} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
import {Utils} from '../../../../../../common/Utils';
import {propertyTypes} from 'typeconfig/common';
import {SearchQueryParserService} from '../../../gallery/search/search-query-parser.service';
interface IState {
isEnumArrayType: boolean;
isEnumType: boolean;
isConfigType: boolean;
default: any;
value: any;
min?: number;
max?: number;
type: propertyTypes;
arrayType: propertyTypes;
original: any;
readonly?: boolean;
}
@Component({
selector: 'app-settings-entry',
@ -32,13 +48,8 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
@Input() simplifiedMode = false;
@Input() allowSpaces = false;
@Input() description: string;
state: {
isEnumType: boolean,
isConfigType: boolean,
default: any, value: any, min?: number, max?: number,
type: propertyTypes, arrayType: propertyTypes,
original: any, readonly?: boolean
};
@Input() typeOverride: 'searchQuery';
state: IState;
isNumberArray = false;
isNumber = false;
type = 'text';
@ -49,9 +60,7 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
private readonly GUID = Utils.GUID();
// value: { default: any, setting: any, original: any, readonly?: boolean, onChange: () => void };
constructor() {
constructor(private searchQueryParserService: SearchQueryParserService) {
}
get changed(): boolean {
@ -61,7 +70,7 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
if (this.state.type === 'array') {
return !Utils.equalsFilter(this.state.value, this.state.default);
}
return this.state.value !== this.state.default;
return !Utils.equalsFilter(this.state.value, this.state.default);
}
get shouldHide(): boolean {
@ -79,6 +88,11 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
get defaultStr(): string {
if (this.typeOverride === 'searchQuery') {
return '\'' + this.searchQueryParserService.stringify(this.state.default) + '\'';
}
if (this.state.type === 'array' && this.state.arrayType === 'string') {
return (this.state.default || []).join(';');
}
@ -86,7 +100,23 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
return this.state.default;
}
get value(): any {
get Values(): any[] {
if (Array.isArray(this.state.value)) {
return this.state.value;
}
return [this.state.value];
}
get Type(): any {
return this.typeOverride || this.state.type;
}
get IsEnumType(): boolean {
return this.state.isEnumType === true || this.state.isEnumArrayType === true;
}
get StringValue(): any {
if (this.state.type === 'array' &&
(this.state.arrayType === 'string' || this.isNumberArray)) {
return this.state.value.join(';');
@ -95,7 +125,7 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
return this.state.value;
}
set value(value: any) {
set StringValue(value: any) {
if (this.state.type === 'array' &&
(this.state.arrayType === 'string' || this.isNumberArray)) {
value = value.replace(new RegExp(',', 'g'), ';');
@ -137,14 +167,15 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
this.state.arrayType === 'integer' || this.state.arrayType === 'float' || this.state.arrayType === 'positiveFloat';
this.isNumber = this.state.type === 'unsignedInt' ||
this.state.type === 'integer' || this.state.type === 'float' || this.state.type === 'positiveFloat';
if (this.state.isEnumType) {
const eClass = this.state.isEnumType ? this.state.type : this.state.arrayType;
if (this.state.isEnumType || this.state.isEnumArrayType) {
if (this.options) {
this.optionsView = this.options;
} else {
if (this.optionMap) {
this.optionsView = Utils.enumToArray(this.state.type).map(this.optionMap);
this.optionsView = Utils.enumToArray(eClass).map(this.optionMap);
} else {
this.optionsView = Utils.enumToArray(this.state.type);
this.optionsView = Utils.enumToArray(eClass);
}
}
}
@ -187,6 +218,16 @@ export class SettingsEntryComponent implements ControlValueAccessor, Validator,
this.onTouched = fn;
}
AddNew(): void {
if (this.state.type === 'array') {
this.state.value.push(this.state.value[this.state.value.length - 1]);
}
}
remove(i: number): void {
(this.state.value as any[]).splice(i, 1);
}
}

View File

@ -1,4 +1,6 @@
app-gallery-search-field{
width: 100%;
}
.changed-settings input, .changed-settings select {
border-color: #007bff;
border-width: 1px;

View File

@ -0,0 +1,55 @@
<form #settingsForm="ngForm" class="form-horizontal">
<div class="card mb-4">
<h5 class="card-header">
{{Name}}
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Preview Filter query"
description="Filters the sub-folders with this search query"
i18n-description i18n-name
[ngModel]="states.SearchQuery"
[simplifiedMode]="simplifiedMode"
[typeOverride]="'searchQuery'"
required="true">
</app-settings-entry>
<app-settings-entry
name="Preview Sorting"
description="If multiple preview is available sorts them by these methods and selects the first one."
i18n-description i18n-name
[ngModel]="states.Sorting"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-right"
[disabled]=" !changed || inProgress"
(click)="reset()" i18n>Reset
</button>
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(jobError)="error=$event"
[jobName]="jobName"
[allowParallelRun]="false"
[config]="Config"></app-settings-job-button>
<ng-container *ngIf="Progress != null">
<br/>
<hr/>
<app-settings-job-progress [progress]="Progress"></app-settings-job-progress>
</ng-container>
</div>
</div>
</form>

View File

@ -0,0 +1,48 @@
import {Component, OnInit} from '@angular/core';
import {SettingsComponentDirective} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {DefaultsJobs, JobDTOUtils} from '../../../../../common/entities/job/JobDTO';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {JobProgressDTO, JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerPreviewConfig} from '../../../../../common/config/private/PrivateConfig';
import {PreviewSettingsService} from './preview.settings.service';
@Component({
selector: 'app-settings-preview',
templateUrl: './preview.settings.component.html',
styleUrls: ['./preview.settings.component.css',
'../_abstract/abstract.settings.component.css'],
providers: [PreviewSettingsService],
})
export class PreviewSettingsComponent
extends SettingsComponentDirective<ServerPreviewConfig>
implements OnInit {
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Preview Filling']];
constructor(authService: AuthenticationService,
navigation: NavigationService,
settingsService: PreviewSettingsService,
notification: NotificationService,
public jobsService: ScheduledJobsService) {
super($localize`Preview`, authService, navigation, settingsService, notification, s => s.Server.Preview);
}
get Config(): any {
return {};
}
get Progress(): JobProgressDTO {
return this.jobsService.progress.value[JobDTOUtils.getHashName(this.jobName, this.Config)];
}
ngOnInit(): void {
super.ngOnInit();
}
}

View File

@ -0,0 +1,24 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../../model/network/network.service';
import {AbstractSettingsService} from '../_abstract/abstract.settings.service';
import {SettingsService} from '../settings.service';
import {ServerPreviewConfig} from '../../../../../common/config/private/PrivateConfig';
@Injectable()
export class PreviewSettingsService
extends AbstractSettingsService<ServerPreviewConfig> {
constructor(private networkService: NetworkService,
settingsService: SettingsService) {
super(settingsService);
}
showInSimplifiedMode(): boolean {
return false;
}
public updateSettings(settings: ServerPreviewConfig): Promise<void> {
return this.networkService.putJson('/settings/preview', {settings});
}
}

View File

@ -4,7 +4,7 @@ import {AuthenticationService} from '../../../model/network/authentication.servi
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {SearchSettingsService} from './search.settings.service';
import {ClientConfig, ClientSearchConfig} from '../../../../../common/config/public/ClientConfig';
import {ClientSearchConfig} from '../../../../../common/config/public/ClientConfig';
@Component({
selector: 'app-settings-search',