diff --git a/frontend/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts
index 49a293096..c94159365 100644
--- a/frontend/app/features/content/pages/contents/contents-page.component.ts
+++ b/frontend/app/features/content/pages/contents/contents-page.component.ts
@@ -9,7 +9,7 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
-import { AppLanguageDto, ContentDto, ContentsState, fadeAnimation, LanguagesState, ModalModel, Queries, Query, QueryModel, queryModelFromSchema, ResourceOwner, Router2State, SchemaDetailsDto, SchemasState, TableFields, TempService, UIState } from '@app/shared';
+import { AppLanguageDto, AppsState, ContentDto, ContentsState, ContributorsState, fadeAnimation, LanguagesState, ModalModel, Queries, Query, QueryModel, queryModelFromSchema, ResourceOwner, Router2State, SchemaDetailsDto, SchemasState, TableFields, TempService, UIState } from '@app/shared';
import { combineLatest } from 'rxjs';
import { distinctUntilChanged, onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { DueTimeSelectorComponent } from './../../shared/due-time-selector.component';
@@ -53,6 +53,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
constructor(
public readonly contentsRoute: Router2State,
public readonly contentsState: ContentsState,
+ private readonly appsState: AppsState,
+ private readonly contributorsState: ContributorsState,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly languagesState: LanguagesState,
@@ -64,6 +66,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
}
public ngOnInit() {
+ if (this.appsState.snapshot.selectedApp?.canReadContributors) {
+ this.contributorsState.loadIfNotLoaded();
+ }
+
this.own(
combineLatest([
this.schemasState.selectedSchema,
diff --git a/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html
index 82fcae249..889073653 100644
--- a/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html
+++ b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.html
@@ -5,9 +5,9 @@
-
+
- {{user.displayName}}
+ {{user.displayName}}
diff --git a/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss
index 72a42a133..5253b7e60 100644
--- a/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss
+++ b/frontend/app/features/settings/pages/contributors/contributor-add-form.component.scss
@@ -3,7 +3,7 @@
@include truncate;
}
- &-name {
+ .user-name {
margin-left: .25rem;
}
}
diff --git a/frontend/app/framework/angular/forms/editors/dropdown.component.html b/frontend/app/framework/angular/forms/editors/dropdown.component.html
index 678a9f677..c8ce3fe5d 100644
--- a/frontend/app/framework/angular/forms/editors/dropdown.component.html
+++ b/frontend/app/framework/angular/forms/editors/dropdown.component.html
@@ -1,13 +1,14 @@
-
+
-
-
{{snapshot.selectedItem}}
+
+ {{selectedItem}}
-
+
-
-
@@ -18,7 +19,12 @@
-
+
{{item}}
diff --git a/frontend/app/framework/angular/forms/editors/dropdown.component.scss b/frontend/app/framework/angular/forms/editors/dropdown.component.scss
index 14b32416f..7daa20767 100644
--- a/frontend/app/framework/angular/forms/editors/dropdown.component.scss
+++ b/frontend/app/framework/angular/forms/editors/dropdown.component.scss
@@ -1,16 +1,16 @@
$color-input-disabled: #eef1f4;
-.form-control {
+.custom-select {
& {
- width: 100%;
+ cursor: default;
}
&[readonly] {
- background: $color-input-background;
+ background-color: $color-input-background;
}
&:disabled {
- background: $color-input-disabled;
+ background-color: $color-input-disabled;
}
}
@@ -32,25 +32,18 @@ $color-input-disabled: #eef1f4;
.selection {
& {
- overflow: hidden;
position: relative;
}
.control-dropdown-item {
- @include absolute(0, 1rem, 0, 0);
+ @include absolute(0, 1.75rem, 0, 0);
line-height: 1.2rem;
+ overflow: hidden;
padding-bottom: 0;
pointer-events: none;
position: absolute;
}
- .icon-caret-down {
- @include absolute(30%, 5px, null, null);
- font-size: .9rem;
- font-weight: normal;
- pointer-events: none;
- }
-
.truncate {
min-height: 1.2rem;
}
diff --git a/frontend/app/framework/angular/forms/editors/dropdown.component.ts b/frontend/app/framework/angular/forms/editors/dropdown.component.ts
index 585935b56..a3f305787 100644
--- a/frontend/app/framework/angular/forms/editors/dropdown.component.ts
+++ b/frontend/app/framework/angular/forms/editors/dropdown.component.ts
@@ -5,6 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
+// tslint:disable: prefer-for-of
+
import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, OnChanges, OnInit, QueryList, SimpleChanges, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Keys, ModalModel, StatefulControlComponent, Types } from '@app/framework/internal';
@@ -18,9 +20,6 @@ interface State {
// The suggested item.
suggestedItems: ReadonlyArray
;
- // The selected suggested item.
- selectedItem: any;
-
// The selected suggested index.
selectedIndex: number;
@@ -40,12 +39,17 @@ const NO_EMIT = { emitEvent: false };
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DropdownComponent extends StatefulControlComponent> implements AfterContentInit, ControlValueAccessor, OnChanges, OnInit {
+ private value: any;
+
@Input()
public items: ReadonlyArray = [];
@Input()
public searchProperty = 'name';
+ @Input()
+ public valueProperty?: string;
+
@Input()
public canSearch = true;
@@ -62,9 +66,12 @@ export class DropdownComponent extends StatefulControlComponent ({
...s,
- suggestedIndex: 0,
+ suggestedIndex: this.getSelectedIndex(this.value),
suggestedItems: this.items || []
}));
}
@@ -127,7 +136,9 @@ export class DropdownComponent extends StatefulControlComponent= items.length && fromUserAction) {
- selectedIndex = items.length - 1;
- }
+ if (selectedIndex >= items.length) {
+ selectedIndex = items.length - 1;
+ }
- const value = items[selectedIndex];
+ const selectedItem = items[selectedIndex];
- if (value !== this.snapshot.selectedItem) {
- if (fromUserAction) {
- this.callChange(value);
+ let selectedValue = selectedItem;
+
+ if (this.valueProperty && this.valueProperty.length > 0 && selectedValue) {
+ selectedValue = selectedValue[this.valueProperty];
+ }
+
+ if (this.value !== selectedValue) {
+ this.value = selectedValue;
+
+ this.callChange(selectedValue);
this.callTouched();
}
+ }
+
+ this.next(s => ({ ...s, selectedIndex }));
+ }
- this.next(s => ({ ...s, selectedIndex, selectedItem: value }));
+ private getSelectedIndex(value: any) {
+ if (!value) {
+ return -1;
}
+
+ if (this.valueProperty && this.valueProperty.length > 0) {
+ for (let i = 0; i < this.items.length; i++) {
+ const item = this.items[i];
+
+ if (item && item[this.valueProperty] === value) {
+ return i;
+ }
+ }
+ } else {
+ return this.items.indexOf(value);
+ }
+
+ return -1;
}
}
\ No newline at end of file
diff --git a/frontend/app/framework/utils/keys.ts b/frontend/app/framework/utils/keys.ts
index 955703108..3ef1f339b 100644
--- a/frontend/app/framework/utils/keys.ts
+++ b/frontend/app/framework/utils/keys.ts
@@ -12,4 +12,40 @@ export module Keys {
export const ESCAPE = 27;
export const DOWN = 40;
export const UP = 38;
+
+ export function isComma(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === ',' || key === COMMA;
+ }
+
+ export function isDelete(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === 'Delete' || key === DELETE;
+ }
+
+ export function isEnter(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === 'ENTER' || key === ENTER;
+ }
+
+ export function isDown(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === 'ArrowDown' || key === DOWN;
+ }
+
+ export function isUp(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === 'ArrowUp' || key === UP;
+ }
+
+ export function isEscape(event: KeyboardEvent) {
+ const key = event.key || event.keyCode;
+
+ return key === 'Escape' || key === 'Esc' || key === UP;
+ }
}
\ No newline at end of file
diff --git a/frontend/app/shared/components/search/queries/filter-comparison.component.html b/frontend/app/shared/components/search/queries/filter-comparison.component.html
index cc72d5a7d..4cf71b455 100644
--- a/frontend/app/shared/components/search/queries/filter-comparison.component.html
+++ b/frontend/app/shared/components/search/queries/filter-comparison.component.html
@@ -49,14 +49,40 @@
-
- {{target.status}}
+
+ {{status.status}}
+
+
+
+
+
+
+
+ {{user.contributorName}}
+
+
+
+ {{user.contributorName}}
+
+
+
+
+
+
+
) {
- return statuses.find(x => x.status === this.filter.value);
- }
-
- public changeStatus(status: StatusInfo) {
- this.changeValue(status.status);
- }
-
public changeValue(value: any) {
this.filter.value = value;
diff --git a/frontend/app/shared/components/search/query-list.component.ts b/frontend/app/shared/components/search/query-list.component.ts
index f20ea4f07..e2a57890f 100644
--- a/frontend/app/shared/components/search/query-list.component.ts
+++ b/frontend/app/shared/components/search/query-list.component.ts
@@ -22,7 +22,7 @@ export class QueryListComponent {
public remove = new EventEmitter();
@Input()
- public queryUsed: Query | undefined;
+ public queryUsed?: Query;
@Input()
public queries: ReadonlyArray;
diff --git a/frontend/app/shared/components/search/search-form.component.ts b/frontend/app/shared/components/search/search-form.component.ts
index 301bed967..65a5eb1a5 100644
--- a/frontend/app/shared/components/search/search-form.component.ts
+++ b/frontend/app/shared/components/search/search-form.component.ts
@@ -7,7 +7,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms';
-import { DialogModel, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm } from '@app/shared/internal';
+import { DialogModel, equalsQuery, hasFilter, LanguageDto, Queries, Query, QueryModel, SaveQueryForm, Types } from '@app/shared/internal';
import { Observable } from 'rxjs';
@Component({
@@ -18,6 +18,7 @@ import { Observable } from 'rxjs';
})
export class SearchFormComponent implements OnChanges {
public readonly standalone = { standalone: true };
+ private previousQuery?: Query;
@Output()
public queryChange = new EventEmitter();
@@ -32,7 +33,7 @@ export class SearchFormComponent implements OnChanges {
public queryModel: QueryModel;
@Input()
- public query: Query | undefined;
+ public query?: Query;
@Input()
public queries: Queries;
@@ -62,6 +63,8 @@ export class SearchFormComponent implements OnChanges {
}
if (changes['query']) {
+ this.previousQuery = Types.clone(this.query);
+
this.hasFilter = hasFilter(this.query);
}
}
@@ -69,7 +72,13 @@ export class SearchFormComponent implements OnChanges {
public search(close = false) {
this.hasFilter = hasFilter(this.query);
- this.queryChange.emit(this.query);
+ if (!equalsQuery(this.query, this.previousQuery)) {
+ const clone = Types.clone(this.query);
+
+ this.queryChange.emit(clone);
+
+ this.previousQuery = this.query;
+ }
if (close) {
this.searchDialog.hide();
diff --git a/frontend/app/shared/services/contributors.service.ts b/frontend/app/shared/services/contributors.service.ts
index 92beb7281..e631718f2 100644
--- a/frontend/app/shared/services/contributors.service.ts
+++ b/frontend/app/shared/services/contributors.service.ts
@@ -26,6 +26,10 @@ export class ContributorDto {
public readonly canUpdate: boolean;
public readonly canRevoke: boolean;
+ public get token() {
+ return `subject:${this.contributorId}`;
+ }
+
constructor(
links: ResourceLinks,
public readonly contributorId: string,
diff --git a/frontend/app/shared/state/contributors.state.spec.ts b/frontend/app/shared/state/contributors.state.spec.ts
index 5953a99c5..794962a54 100644
--- a/frontend/app/shared/state/contributors.state.spec.ts
+++ b/frontend/app/shared/state/contributors.state.spec.ts
@@ -71,6 +71,13 @@ describe('ContributorsState', () => {
expect(contributorsState.snapshot.isLoading).toBeFalsy();
});
+ it('should not load if already loaded', () => {
+ contributorsState.load(true).subscribe();
+ contributorsState.loadIfNotLoaded().subscribe();
+
+ expect().nothing();
+ });
+
it('should only show current page of contributors', () => {
contributorsState.load().subscribe();
diff --git a/frontend/app/shared/state/contributors.state.ts b/frontend/app/shared/state/contributors.state.ts
index ead519108..5155bde44 100644
--- a/frontend/app/shared/state/contributors.state.ts
+++ b/frontend/app/shared/state/contributors.state.ts
@@ -7,7 +7,7 @@
import { Injectable } from '@angular/core';
import { DialogService, ErrorDto, Pager, shareMapSubscribed, shareSubscribed, State, StateSynchronizer, Types, Version } from '@app/framework';
-import { Observable, throwError } from 'rxjs';
+import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { AssignContributorDto, ContributorDto, ContributorsPayload, ContributorsService } from './../services/contributors.service';
import { AppsState } from './apps.state';
@@ -97,6 +97,14 @@ export class ContributorsState extends State {
.build();
}
+ public loadIfNotLoaded(): Observable {
+ if (this.snapshot.isLoaded) {
+ return EMPTY;
+ }
+
+ return this.loadInternal(false);
+ }
+
public load(isReload = false): Observable {
if (!isReload) {
const contributorsPager = this.snapshot.contributorsPager.reset();
diff --git a/frontend/app/shared/state/query.ts b/frontend/app/shared/state/query.ts
index e131e83dc..a01bfe322 100644
--- a/frontend/app/shared/state/query.ts
+++ b/frontend/app/shared/state/query.ts
@@ -21,7 +21,8 @@ export type QueryValueType =
'reference' |
'status' |
'string' |
- 'tags';
+ 'tags' |
+ 'user';
export interface FilterOperator {
// The optional display value.
@@ -255,6 +256,11 @@ const TypeStatus: QueryFieldModel = {
operators: EqualOperators
};
+const TypeUser: QueryFieldModel = {
+ type: 'user',
+ operators: EqualOperators
+};
+
const TypeString: QueryFieldModel = {
type: 'string',
operators: [...EqualOperators, ...CompareOperator, ...StringOperators, ...ArrayOperators]
@@ -272,7 +278,7 @@ const DEFAULT_FIELDS: QueryModelFields = {
description: 'i18n:contents.createFieldDescription'
},
createdBy: {
- ...TypeString,
+ ...TypeUser,
displayName: 'meta.createdBy',
description: 'i18n:contents.createdByFieldDescription'
},
@@ -282,7 +288,7 @@ const DEFAULT_FIELDS: QueryModelFields = {
description: 'i18n:contents.lastModifiedFieldDescription'
},
lastModifiedBy: {
- ...TypeString,
+ ...TypeUser,
displayName: 'meta.lastModifiedBy',
description: 'i18n:contents.lastModifiedByFieldDescription'
},