refactor input with action

This commit is contained in:
Dan Percic 2021-08-21 00:42:58 +03:00
parent 56aea20a87
commit 5bbe8ce80b
19 changed files with 103 additions and 151 deletions

View File

@ -90,9 +90,8 @@
</div>
<div *ngIf="isSearchOpen" class="search-input-container">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'file-attributes-csv-import.search.placeholder' | translate"
type="search"
width="full"
></redaction-input-with-action>
</div>

View File

@ -126,9 +126,8 @@
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'dictionary-listing.search' | translate"
type="search"
></redaction-input-with-action>
<div class="actions">
<iqser-icon-button

View File

@ -97,9 +97,8 @@
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'dossier-attributes-listing.search' | translate"
type="search"
></redaction-input-with-action>
<iqser-icon-button

View File

@ -16,9 +16,8 @@
>
<div class="actions flex-1">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'dossier-templates-listing.search' | translate"
type="search"
></redaction-input-with-action>
<iqser-icon-button

View File

@ -107,9 +107,8 @@
<div class="attributes-actions-container">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'file-attributes-listing.search' | translate"
type="search"
></redaction-input-with-action>
<input #fileInput (change)="importCSV($event.target['files'])" accept=".csv" class="csv-input" type="file" />

View File

@ -9,9 +9,8 @@
<div class="actions">
<redaction-input-with-action
[form]="searchService.searchForm"
[(value)]="searchService.searchValue"
[placeholder]="'user-listing.search' | translate"
type="search"
></redaction-input-with-action>
<iqser-icon-button
(action)="openAddEditUserDialog($event)"

View File

@ -19,13 +19,11 @@
</div>
<redaction-input-with-action
(action)="addComment()"
(action)="addComment($event)"
*ngIf="permissionsService.canAddComment()"
[form]="commentForm"
[placeholder]="'comments.add-comment' | translate"
autocomplete="off"
icon="red:collapse"
type="action"
width="full"
></redaction-input-with-action>

View File

@ -1,11 +1,8 @@
import { ChangeDetectorRef, Component, HostBinding, Input } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Component, HostBinding, Input } from '@angular/core';
import { Comment } from '@redaction/red-ui-http';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserService } from '@services/user.service';
import { AppStateService } from '@state/app-state.service';
import { TranslateService } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
@Component({
@ -15,49 +12,43 @@ import { PermissionsService } from '@services/permissions.service';
})
export class CommentsComponent {
@Input() annotation: AnnotationWrapper;
commentForm: FormGroup;
@HostBinding('class.hidden') private _hidden = true;
constructor(
readonly translateService: TranslateService,
readonly permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _userService: UserService,
private readonly _manualAnnotationService: ManualAnnotationService
) {
this.commentForm = this._formBuilder.group({
value: ['']
});
}
) {}
addComment(): void {
const value = this.commentForm.value.value;
if (value) {
this._manualAnnotationService.addComment(value, this.annotation.id).subscribe(commentResponse => {
addComment(value: string): void {
if (!value) return;
this._manualAnnotationService
.addComment(value, this.annotation.id)
.toPromise()
.then(commentResponse => {
this.annotation.comments.push({
text: value,
id: commentResponse.commentId,
user: this._userService.currentUser.id
});
});
this.commentForm.reset();
}
}
toggleExpandComments($event?: MouseEvent) {
toggleExpandComments($event?: MouseEvent): void {
$event?.stopPropagation();
this._hidden = !this._hidden;
}
deleteComment(comment: Comment): void {
this._manualAnnotationService.deleteComment(comment.id, this.annotation.id).subscribe(() => {
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
if (!this.annotation.comments.length) {
this._hidden = true;
}
});
this._manualAnnotationService
.deleteComment(comment.id, this.annotation.id)
.toPromise()
.then(() => {
this.annotation.comments.splice(this.annotation.comments.indexOf(comment), 1);
if (!this.annotation.comments.length) {
this._hidden = true;
}
});
}
getOwnerName(comment: Comment): string {

View File

@ -26,6 +26,10 @@ import { map } from 'rxjs/operators';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
interface Target extends EventTarget {
localName: string;
}
@Component({
selector: 'redaction-file-workload',
templateUrl: './file-workload.component.html',
@ -155,6 +159,7 @@ export class FileWorkloadComponent {
}
annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void {
if (($event.target as Target).localName === 'input') return;
this.pagesPanelActive = false;
this.logAnnotation(annotation);
if (this.isSelected(annotation)) {
@ -175,7 +180,7 @@ export class FileWorkloadComponent {
if (
!ALL_HOTKEY_ARRAY.includes($event.key) ||
this.dialogRef?.getState() === MatDialogState.OPEN ||
($event.target as any).localName === 'input'
($event.target as Target).localName === 'input'
) {
return;
}

View File

@ -1,12 +1,10 @@
<div *ngIf="permissionsService.canExcludePages()" class="exclude-pages-input-container">
<redaction-input-with-action
(action)="excludePagesRange()"
[form]="excludePagesForm"
(action)="excludePagesRange($event)"
[hint]="'file-preview.tabs.exclude-pages.hint' | translate"
[placeholder]="'file-preview.tabs.exclude-pages.input-placeholder' | translate"
autocomplete="off"
icon="red:check"
type="action"
width="full"
></redaction-input-with-action>
</div>

View File

@ -1,11 +1,11 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { PageRange, ReanalysisControllerService } from '@redaction/red-ui-http';
import { Toaster } from '@iqser/common-ui';
import { LoadingService } from '@services/loading.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component';
@Component({
selector: 'redaction-page-exclusion',
@ -14,24 +14,19 @@ import { FileStatusWrapper } from '@models/file/file-status.wrapper';
})
export class PageExclusionComponent implements OnChanges {
@Input() fileStatus: FileStatusWrapper;
@Output() actionPerformed = new EventEmitter<string>();
@Output() readonly actionPerformed = new EventEmitter<string>();
excludePagesForm: FormGroup;
excludedPagesRanges: PageRange[] = [];
@ViewChild(InputWithActionComponent) private readonly _inputComponent: InputWithActionComponent;
constructor(
readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService
) {
this.excludePagesForm = this._formBuilder.group({
value: ['']
});
}
) {}
ngOnChanges() {
ngOnChanges(): void {
const excludedPages = (this.fileStatus?.excludedPages || []).sort((p1, p2) => p1 - p2);
this.excludedPagesRanges = excludedPages.reduce((ranges, page) => {
if (!ranges.length) {
@ -47,24 +42,21 @@ export class PageExclusionComponent implements OnChanges {
}, []);
}
async excludePagesRange() {
async excludePagesRange(value: string): Promise<void> {
this._loadingService.start();
try {
const pageRanges = this.excludePagesForm
.get('value')
.value.split(',')
.map(range => {
const splitted = range.split('-');
const startPage = parseInt(splitted[0], 10);
const endPage = splitted.length > 1 ? parseInt(splitted[1], 10) : startPage;
if (!startPage || !endPage) {
throw new Error();
}
return {
startPage,
endPage
};
});
const pageRanges = value.split(',').map(range => {
const splitted = range.split('-');
const startPage = parseInt(splitted[0], 10);
const endPage = splitted.length > 1 ? parseInt(splitted[1], 10) : startPage;
if (!startPage || !endPage) {
throw new Error();
}
return {
startPage,
endPage
};
});
await this._reanalysisControllerService
.excludePages(
{
@ -74,7 +66,7 @@ export class PageExclusionComponent implements OnChanges {
this.fileStatus.fileId
)
.toPromise();
this.excludePagesForm.reset();
this._inputComponent.reset();
this.actionPerformed.emit('exclude-pages');
} catch (e) {
this._toaster.error(_('file-preview.tabs.exclude-pages.error'));
@ -82,7 +74,7 @@ export class PageExclusionComponent implements OnChanges {
}
}
async includePagesRange(range: PageRange) {
async includePagesRange(range: PageRange): Promise<void> {
this._loadingService.start();
await this._reanalysisControllerService
.includePages(
@ -93,7 +85,7 @@ export class PageExclusionComponent implements OnChanges {
this.fileStatus.fileId
)
.toPromise();
this.excludePagesForm.reset();
this._inputComponent.reset();
this.actionPerformed.emit('exclude-pages');
}
}

View File

@ -36,11 +36,11 @@
<pre *ngIf="selectedReviewersList.length === 0" [innerHTML]="'assign-dossier-owner.dialog.no-reviewers' | translate" class="info"></pre>
<redaction-input-with-action
[form]="searchForm"
[(value)]="searchQuery"
(valueChange)="setMembersSelectOptions()"
[placeholder]="'assign-dossier-owner.dialog.search' | translate"
[width]="560"
class="search-container"
type="search"
></redaction-input-with-action>
<div class="members-list">

View File

@ -13,12 +13,12 @@ import { DossierWrapper } from '@state/model/dossier.wrapper';
})
export class TeamMembersManagerComponent implements OnInit {
teamForm: FormGroup;
searchForm: FormGroup;
searchQuery = '';
@Input() dossierWrapper: DossierWrapper;
@Output() save = new EventEmitter<Dossier>();
ownersSelectOptions: string[] = this.userService.managerUsers.map(m => m.id);
@Output() readonly save = new EventEmitter<Dossier>();
readonly ownersSelectOptions = this.userService.managerUsers.map(m => m.id);
selectedReviewersList: string[] = [];
membersSelectOptions: string[] = [];
changed = false;
@ -137,10 +137,9 @@ export class TeamMembersManagerComponent implements OnInit {
this.selectedReviewersList = this.selectedMembersList.filter(m => this.selectedApproversList.indexOf(m) === -1);
}
private _setMembersSelectOptions() {
const searchQuery = this.searchForm.get('query').value;
setMembersSelectOptions(): void {
this.membersSelectOptions = this.userService.eligibleUsers
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(searchQuery.toLowerCase()))
.filter(user => this.userService.getNameForId(user.id).toLowerCase().includes(this.searchQuery.toLowerCase()))
.filter(user => this.selectedOwnerId !== user.id)
.map(user => user.id);
}
@ -151,9 +150,6 @@ export class TeamMembersManagerComponent implements OnInit {
approvers: [[...this.dossierWrapper?.approverIds]],
members: [[...this.dossierWrapper?.memberIds]]
});
this.searchForm = this._formBuilder.group({
query: ['']
});
this.teamForm.get('owner').valueChanges.subscribe(owner => {
if (!this.isApprover(owner)) {
this.toggleApprover(owner);
@ -161,15 +157,12 @@ export class TeamMembersManagerComponent implements OnInit {
// If it is an approver, it is already a member, no need to check
this._updateLists();
});
this.searchForm.get('query').valueChanges.subscribe(() => {
this._setMembersSelectOptions();
});
this._updateLists();
}
private _updateLists() {
this._setSelectedReviewersList();
this._setMembersSelectOptions();
this.setMembersSelectOptions();
this._updateChanged();
}

View File

@ -85,10 +85,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
)
.subscribe(mappedValue => this._updateValues(mappedValue));
this.addSubscription = this.searchService.searchForm
.get('query')
.valueChanges.pipe(debounceTime(300))
.subscribe(value => this.updateNavigation(value));
this.addSubscription = this.searchService.valueChanges$.pipe(debounceTime(300)).subscribe(value => this.updateNavigation(value));
this.addSubscription = this.filterService.filterGroups$.pipe(skip(1)).subscribe(group => {
const dossierIds = group[0].filters.filter(v => v.checked).map(v => v.key);

View File

@ -14,8 +14,6 @@ export class AnnotationIconComponent implements OnChanges {
@ViewChild('icon', { static: true }) icon: ElementRef;
constructor() {}
get isHint() {
return this.type === 'circle' || this.dictType?.type === 'hint';
}

View File

@ -1,34 +1,25 @@
<form [autocomplete]="autocomplete" [formGroup]="form">
<div [style.max-width]="computedWidth" [style.width]="computedWidth" class="red-input-group">
<input
[formControlName]="formControlName"
[name]="formControlName"
[placeholder]="placeholder"
class="with-icon mt-0"
type="text"
/>
<div [style.max-width]="computedWidth" [style.width]="computedWidth" class="red-input-group">
<input
[(ngModel)]="value"
(ngModelChange)="valueChange.emit($event)"
[autocomplete]="autocomplete"
[placeholder]="placeholder"
class="with-icon mt-0"
type="text"
/>
<span *ngIf="hint" class="hint">{{ hint }}</span>
<span *ngIf="hint" class="hint">{{ hint }}</span>
<!-- Search-->
<mat-icon *ngIf="type === 'search' && !hasContent" class="icon-right" svgIcon="red:search"></mat-icon>
<mat-icon *ngIf="isSearch && !hasContent" class="icon-right" svgIcon="red:search"></mat-icon>
<iqser-circle-button
(action)="clearContent()"
*ngIf="type === 'search' && hasContent"
[disabled]="form.invalid"
[size]="25"
icon="red:close"
></iqser-circle-button>
<iqser-circle-button (action)="reset()" *ngIf="isSearch && hasContent" [size]="25" icon="red:close"></iqser-circle-button>
<!-- Submit-->
<iqser-circle-button
(action)="executeAction($event)"
*ngIf="type === 'action'"
[disabled]="!hasContent"
[icon]="icon"
[isSubmit]="true"
[size]="25"
></iqser-circle-button>
</div>
</form>
<iqser-circle-button
(action)="executeAction($event)"
*ngIf="!isSearch"
[disabled]="!hasContent"
[icon]="icon"
[isSubmit]="true"
[size]="25"
></iqser-circle-button>
</div>

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'redaction-input-with-action',
@ -7,35 +6,36 @@ import { FormGroup } from '@angular/forms';
styleUrls: ['./input-with-action.component.scss']
})
export class InputWithActionComponent {
@Input() form: FormGroup;
@Input() placeholder: string;
@Input() hint: string;
@Input() width: number | 'full' = 250;
@Input() type: 'search' | 'action';
@Input() icon: string;
@Input() autocomplete: 'on' | 'off' = 'on';
@Output() action = new EventEmitter<any>();
@Input() value = '';
@Output() readonly action = new EventEmitter<string>();
@Output() readonly valueChange = new EventEmitter<string>();
get formControlName(): 'query' | 'value' {
return this.type === 'search' ? 'query' : 'value';
get hasContent(): boolean {
return !!this.value.length;
}
get hasContent() {
return !!this.form.get(this.formControlName).value?.length;
}
get computedWidth() {
get computedWidth(): string {
return this.width === 'full' ? '100%' : `${this.width}px`;
}
clearContent() {
this.form.patchValue({ query: '' }, { emitEvent: true });
reset(): void {
this.value = '';
}
executeAction($event?: MouseEvent) {
$event?.stopPropagation();
get isSearch(): boolean {
return this.action.observers.length === 0;
}
executeAction($event: MouseEvent): void {
$event.stopPropagation();
if (this.hasContent) {
this.action.emit();
this.action.emit(this.value);
}
}
}

View File

@ -53,11 +53,10 @@
<ng-template #searchBar>
<redaction-input-with-action
[(value)]="searchService.searchValue"
*ngIf="searchPlaceholder && searchService"
[form]="searchService.searchForm"
[placeholder]="searchPlaceholder"
[width]="searchWidth"
[class.mr-8]="searchPosition === searchPositions.beforeFilters"
type="search"
></redaction-input-with-action>
</ng-template>

View File

@ -16,7 +16,7 @@ export interface ProfileModel {
}
export class UserWrapper {
constructor(private readonly _user: KeycloakProfile | User, public roles: string[], public id: string) {}
constructor(private readonly _user: KeycloakProfile | User, public roles: string[], readonly id: string) {}
email = this._user.email;
username = this._user.username || this.email;
@ -106,11 +106,7 @@ export class UserService {
getNameForId(userId: string): string | undefined {
const user = this.getUserById(userId);
return user ? this.getName(user) : undefined;
}
getName({ firstName, lastName, username }: UserWrapper) {
return firstName && lastName ? `${firstName} ${lastName}` : username;
return user ? user.name : undefined;
}
isManager(user: UserWrapper = this._currentUser): boolean {