From 71c864e71f4301558f5ef2c97f98e7d0060dec75 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 26 Nov 2020 15:39:20 +0100 Subject: [PATCH] Feature/contributors in search (#600) * Fix for rerun rules. * Tests fixed * Contributors in search. --- .../pages/contents/contents-page.component.ts | 8 +- .../contributor-add-form.component.html | 4 +- .../contributor-add-form.component.scss | 2 +- .../forms/editors/dropdown.component.html | 20 ++-- .../forms/editors/dropdown.component.scss | 19 ++-- .../forms/editors/dropdown.component.ts | 102 ++++++++++++------ frontend/app/framework/utils/keys.ts | 36 +++++++ .../queries/filter-comparison.component.html | 34 +++++- .../queries/filter-comparison.component.scss | 10 ++ .../queries/filter-comparison.component.ts | 16 ++- .../components/search/query-list.component.ts | 2 +- .../search/search-form.component.ts | 15 ++- .../shared/services/contributors.service.ts | 4 + .../shared/state/contributors.state.spec.ts | 7 ++ .../app/shared/state/contributors.state.ts | 10 +- frontend/app/shared/state/query.ts | 12 ++- 16 files changed, 223 insertions(+), 78 deletions(-) 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' },