diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index d36f551f0..e8aee083d 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps "added pattern {[Name]}"); AddEventMessage( - "deleted pattern {[Name]}"); + "deleted pattern {[PatternId]}"); AddEventMessage( "updated pattern {[Name]}"); @@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps return Task.FromResult( ForEvent(@event, channel) - .AddParameter("Name", @event.Name)); + .AddParameter("PatternId", @event.PatternId)); } protected override Task CreateEventCoreAsync(Envelope @event) diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs index 0759e3fb3..3ae54ab42 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs @@ -14,7 +14,5 @@ namespace Squidex.Domain.Apps.Events.Apps public sealed class AppPatternDeleted : AppEvent { public Guid PatternId { get; set; } - - public string Name { get; set; } } } diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html index 14c598818..540d8fde1 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -60,11 +60,11 @@ - + Error - + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index b35a6ca17..882956d72 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -5,8 +5,13 @@ autocorrect="off" autocapitalize="off"> -
-
+
+
{{item}} diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 5d4592462..a8666a9cf 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -49,9 +49,8 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O @ContentChild(TemplateRef) public itemTemplate: TemplateRef; - public items: any[] = []; - - public selectedIndex = -1; + public suggestedItems: any[] = []; + public suggestedIndex = -1; public queryInput = new FormControl(); @@ -72,14 +71,13 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); } }), - distinctUntilChanged(), debounceTime(200), + distinctUntilChanged(), filter(query => !!query && !!this.source), - switchMap(query => this.source.find(query)), - catchError(error => of([]))) + switchMap(query => this.source.find(query)), catchError(() => of([]))) .subscribe(items => { - this.reset(); - this.items = items || []; + this.suggestedIndex = -1; + this.suggestedItems = items || []; }); } @@ -96,7 +94,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); return false; case KEY_ENTER: - if (this.items.length > 0) { + if (this.suggestedItems.length > 0) { this.selectItem(); return false; } @@ -110,7 +108,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O if (!obj) { this.resetForm(); } else { - const item = this.items.find(i => i === obj); + const item = this.suggestedItems.find(i => i === obj); if (item) { this.queryInput.setValue(obj.title || ''); @@ -144,11 +142,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public selectItem(selection: any | null = null) { if (!selection) { - selection = this.items[this.selectedIndex]; + selection = this.suggestedItems[this.suggestedIndex]; } - if (!selection && this.items.length === 1) { - selection = this.items[0]; + if (!selection && this.suggestedItems.length === 1) { + selection = this.suggestedItems[0]; } if (selection) { @@ -170,19 +168,19 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O selection = 0; } - if (selection >= this.items.length) { - selection = this.items.length - 1; + if (selection >= this.suggestedItems.length) { + selection = this.suggestedItems.length - 1; } - this.selectedIndex = selection; + this.suggestedIndex = selection; } private up() { - this.selectIndex(this.selectedIndex - 1); + this.selectIndex(this.suggestedIndex - 1); } private down() { - this.selectIndex(this.selectedIndex + 1); + this.selectIndex(this.suggestedIndex + 1); } private resetForm() { @@ -190,7 +188,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } private reset() { - this.items = []; - this.selectedIndex = -1; + this.suggestedItems = []; + this.suggestedIndex = -1; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index b55d1e014..737f2201a 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,18 +1,30 @@ -
- - {{item}} - + +
+ + {{item}} + - -
\ No newline at end of file + +
+ +
+
+ {{item}} +
+
+ \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss b/src/Squidex/app/framework/angular/forms/tag-editor.component.scss index f92d6a67f..db5a4bd4a 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.scss +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.scss @@ -43,19 +43,14 @@ .item { & { @include border-radius(10px); - @include truncate; - display: inline-block; color: $color-dark-foreground; cursor: default; - height: 20px; padding: 0 .6rem; background: $color-theme-blue; border: 0; font-size: .8rem; font-weight: normal; - line-height: 20px; - margin: 2px 2px 2px 0; - vertical-align: middle; + margin: 0px 2px 2px 0; } &, diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 398016b00..ba7808850 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,14 +5,18 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { Types } from '@app/framework/internal'; const KEY_COMMA = 188; const KEY_DELETE = 8; const KEY_ENTER = 13; +const KEY_UP = 38; +const KEY_DOWN = 40; export interface Converter { convert(input: string): any; @@ -73,7 +77,8 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { templateUrl: './tag-editor.component.html', providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] }) -export class TagEditorComponent implements ControlValueAccessor { +export class TagEditorComponent implements ControlValueAccessor, OnDestroy, OnInit { + private subscription: Subscription; private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; @@ -81,11 +86,17 @@ export class TagEditorComponent implements ControlValueAccessor { public converter: Converter = new StringConverter(); @Input() - public useDefaultValue = true; + public undefinedWhenEmpty = true; @Input() public acceptEnter = false; + @Input() + public allowDuplicates = true; + + @Input() + public suggestions: string[] = []; + @Input() public class: string; @@ -100,10 +111,44 @@ export class TagEditorComponent implements ControlValueAccessor { public hasFocus = false; + public suggestedItems: string[] = []; + public suggestedIndex = 0; + public items: any[] = []; public addInput = new FormControl(); + public ngOnDestroy() { + this.subscription.unsubscribe(); + } + + public ngOnInit() { + this.subscription = + this.addInput.valueChanges.pipe( + tap(() => { + this.adjustSize(); + }), + map(query => query), + map(query => query ? query.trim() : query), + tap(query => { + if (!query) { + this.resetAutocompletion(); + } + }), + distinctUntilChanged(), + map(query => { + if (Types.isArray(this.suggestions) && query && query.length > 0) { + return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.items.indexOf(s) < 0); + } else { + return []; + } + })) + .subscribe(items => { + this.suggestedIndex = -1; + this.suggestedItems = items || []; + }); + } + public writeValue(obj: any) { this.resetForm(); @@ -136,12 +181,6 @@ export class TagEditorComponent implements ControlValueAccessor { } } - private resetForm() { - this.addInput.reset(); - - this.adjustSize(); - } - public markTouched() { this.callTouched(); @@ -171,17 +210,13 @@ export class TagEditorComponent implements ControlValueAccessor { } public onKeyDown(event: KeyboardEvent) { - if (event.keyCode === KEY_COMMA || (event.keyCode === KEY_ENTER && this.acceptEnter)) { - const value = this.addInput.value; - - if (value && this.converter.isValidInput(value)) { - const converted = this.converter.convert(value); + const key = event.keyCode; - this.updateItems([...this.items, converted]); - this.resetForm(); + if (key === KEY_COMMA) { + if (this.selectValue(this.addInput.value)) { return false; } - } else if (event.keyCode === KEY_DELETE) { + } else if (key === KEY_DELETE) { const value = this.addInput.value; if (!value || value.length === 0) { @@ -189,15 +224,74 @@ export class TagEditorComponent implements ControlValueAccessor { return false; } + } else if (key === KEY_UP) { + this.up(); + return false; + } else if (key === KEY_DOWN) { + this.down(); + return false; + } else if (key === KEY_ENTER) { + if (this.suggestedIndex >= 0) { + if (this.selectValue(this.suggestedItems[this.suggestedIndex])) { + return false; + } + } else if (this.acceptEnter) { + if (this.selectValue(this.addInput.value)) { + return false; + } + } } return true; } - private updateItems(items: string[]) { + public selectValue(value: string) { + if (value && this.converter.isValidInput(value)) { + const converted = this.converter.convert(value); + + if (this.allowDuplicates || this.items.indexOf(converted) < 0) { + this.updateItems([...this.items, converted]); + } + + this.resetForm(); + this.resetAutocompletion(); + return true; + } + } + + private resetAutocompletion() { + this.suggestedItems = []; + this.suggestedIndex = -1; + } + + public selectIndex(selection: number) { + if (selection < 0) { + selection = 0; + } + + if (selection >= this.items.length) { + selection = this.items.length - 1; + } + + this.suggestedIndex = selection; + } + + private resetForm() { + this.addInput.reset(); + } + + private up() { + this.selectIndex(this.suggestedIndex - 1); + } + + private down() { + this.selectIndex(this.suggestedIndex + 1); + } + + private updateItems(items: any[]) { this.items = items; - if (items.length === 0 && this.useDefaultValue) { + if (items.length === 0 && this.undefinedWhenEmpty) { this.callChange(undefined); } else { this.callChange(this.items); diff --git a/src/Squidex/app/framework/angular/http/http-extensions.ts b/src/Squidex/app/framework/angular/http/http-extensions.ts index b215d1ce9..fd5b7e29a 100644 --- a/src/Squidex/app/framework/angular/http/http-extensions.ts +++ b/src/Squidex/app/framework/angular/http/http-extensions.ts @@ -67,7 +67,11 @@ export const pretifyError = (message: string) => (source: Observable) => if (!Types.is(response.error, Error)) { try { - const errorDto = Types.isObject(response.error) ? response.error : JSON.parse(response.error); + let errorDto = Types.isObject(response.error) ? response.error : JSON.parse(response.error); + + if (!errorDto) { + errorDto = { message: 'Failed to make the request.', details: [] }; + } if (response.status === 412) { result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.'); diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 243cc67f2..89b3a18a6 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -68,7 +68,13 @@
- + +
{{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index e69f17f6e..58bb509aa 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -54,6 +54,9 @@ export class AssetComponent implements OnDestroy, OnInit { @Input() public isSelectable = false; + @Input() + public allTags: string[]; + @Output() public loaded = new EventEmitter(); diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/src/Squidex/app/shared/components/assets-list.component.html index 1f9899e42..7b504e6ff 100644 --- a/src/Squidex/app/shared/components/assets-list.component.html +++ b/src/Squidex/app/shared/components/assets-list.component.html @@ -14,7 +14,7 @@
Drop file on existing item to replace the asset with a newer version.
-
+
@@ -25,6 +25,7 @@ [isDisabled]="isDisabled" [isSelectable]="selectedIds" [isSelected]="isSelected(asset)" + [allTags]="tags" (updated)="update($event)" (selected)="select($event)" (deleting)="delete($event)"> diff --git a/src/Squidex/app/shared/state/assets.state.ts b/src/Squidex/app/shared/state/assets.state.ts index 933924581..45457abc0 100644 --- a/src/Squidex/app/shared/state/assets.state.ts +++ b/src/Squidex/app/shared/state/assets.state.ts @@ -37,6 +37,10 @@ export class AssetsState extends State { this.changes.pipe(map(x => x.tags), distinctUntilChanged(), map(x => sort(x))); + public tagsNames = + this.tags.pipe( + distinctUntilChanged(), map(x => x.map(t => t.name))); + public assets = this.changes.pipe(map(x => x.assets), distinctUntilChanged()); diff --git a/src/Squidex/app/shared/state/contributors.state.ts b/src/Squidex/app/shared/state/contributors.state.ts index 2b218bcd8..4b1ed31f4 100644 --- a/src/Squidex/app/shared/state/contributors.state.ts +++ b/src/Squidex/app/shared/state/contributors.state.ts @@ -6,14 +6,16 @@ */ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { Observable, throwError } from 'rxjs'; +import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { DialogService, + ErrorDto, ImmutableArray, notify, State, + Types, Version } from '@app/framework'; @@ -100,6 +102,13 @@ export class ContributorsState extends State { this.replaceContributors(contributors, dto.version); }), + catchError(error => { + if (Types.is(error, ErrorDto) && error.statusCode === 404) { + return throwError(new ErrorDto(404, 'The user does not exist.')); + } else { + return throwError(error); + } + }), notify(this.dialogs)); } diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 55c6b4665..5ab170f23 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -34,6 +34,7 @@ "moment": "2.22.2", "mousetrap": "1.6.2", "ng2-dnd": "5.0.2", + "npm": "^6.2.0", "oidc-client": "1.4.1", "pikaday": "1.7.0", "progressbar.js": "1.0.1",