mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
parent
b169fa67b3
commit
1255246b0f
@ -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'));
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
24
src/frontend/app/ui/gallery/search/AutoCompleteRenderItem.ts
Normal file
24
src/frontend/app/ui/gallery/search/AutoCompleteRenderItem.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 => {
|
||||
};
|
||||
}
|
||||
|
@ -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">×</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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
app-gallery-search-field{
|
||||
width: 100%;
|
||||
}
|
||||
.changed-settings input, .changed-settings select {
|
||||
border-color: #007bff;
|
||||
border-width: 1px;
|
||||
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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});
|
||||
}
|
||||
|
||||
}
|
@ -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',
|
||||
|
Loading…
x
Reference in New Issue
Block a user