Pull request #162: Refactored filtering, searching, selection and sorting for lists in a separate component

Merge in RED/ui from base-listing to master

* commit 'dfb3787d42555add7ad8ea37992606ce89ee7296':
  Refactor file attributes CSV import
  Refactor user listing
  Refactored file attributes listing
  Refactored default colors screen
  Refactor dictionary listing
  Sorting logic, added for rulesets listing
  Base listing (filter, search, select) for project listing & overview
This commit is contained in:
Timo Bejan 2021-04-20 08:56:44 +02:00
commit 9d9b7ee4f6
24 changed files with 744 additions and 922 deletions

View File

@ -32,6 +32,7 @@ import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-e
import { UsersStatsComponent } from './components/users-stats/users-stats.component'; import { UsersStatsComponent } from './components/users-stats/users-stats.component';
import { ConfirmDeleteUsersDialogComponent } from './dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component'; import { ConfirmDeleteUsersDialogComponent } from './dialogs/confirm-delete-users-dialog/confirm-delete-users-dialog.component';
import { FileAttributesCsvImportDialogComponent } from './dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component'; import { FileAttributesCsvImportDialogComponent } from './dialogs/file-attributes-csv-import-dialog/file-attributes-csv-import-dialog.component';
import { ActiveFieldsListingComponent } from './dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component';
const dialogs = [ const dialogs = [
AddEditRuleSetDialogComponent, AddEditRuleSetDialogComponent,
@ -41,7 +42,8 @@ const dialogs = [
EditColorDialogComponent, EditColorDialogComponent,
SmtpAuthDialogComponent, SmtpAuthDialogComponent,
AddEditUserDialogComponent, AddEditUserDialogComponent,
ConfirmDeleteUsersDialogComponent ConfirmDeleteUsersDialogComponent,
FileAttributesCsvImportDialogComponent
]; ];
const screens = [ const screens = [
@ -66,12 +68,14 @@ const components = [
ComboChartComponent, ComboChartComponent,
ComboSeriesVerticalComponent, ComboSeriesVerticalComponent,
UsersStatsComponent, UsersStatsComponent,
ActiveFieldsListingComponent,
...dialogs, ...dialogs,
...screens ...screens
]; ];
@NgModule({ @NgModule({
declarations: [...components, FileAttributesCsvImportDialogComponent], declarations: [...components],
providers: [AdminDialogService], providers: [AdminDialogService],
imports: [CommonModule, SharedModule, AdminRoutingModule, AceEditorModule, NgxChartsModule, ColorPickerModule] imports: [CommonModule, SharedModule, AdminRoutingModule, AceEditorModule, NgxChartsModule, ColorPickerModule]
}) })

View File

@ -0,0 +1,175 @@
<div class="header-item">
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div>
<mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
</div>
<span class="all-caps-label">
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: allEntities.length } }}
</span>
<ng-container *ngIf="areSomeEntitiesSelected">
<redaction-circle-button
[matMenuTriggerFor]="readOnlyMenu"
tooltip="file-attributes-csv-import.table-header.actions.read-only"
type="dark-bg"
icon="red:read-only"
>
</redaction-circle-button>
<redaction-circle-button
[matMenuTriggerFor]="displayMenu"
tooltip="file-attributes-csv-import.table-header.actions.display"
type="dark-bg"
icon="red:visibility"
>
</redaction-circle-button>
<redaction-circle-button
(action)="deactivateSelection()"
tooltip="file-attributes-csv-import.table-header.actions.remove-selected"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
<div class="separator"></div>
<redaction-chevron-button text="file-attributes-csv-import.table-header.actions.type" [matMenuTriggerFor]="typeMenu"></redaction-chevron-button>
<mat-menu #readOnlyMenu="matMenu" class="no-padding-bottom">
<button
mat-menu-item
(click)="setAttributeForSelection('readonly', true)"
translate="file-attributes-csv-import.table-header.actions.enable-read-only"
></button>
<button
mat-menu-item
(click)="setAttributeForSelection('readonly', false)"
translate="file-attributes-csv-import.table-header.actions.disable-read-only"
></button>
</mat-menu>
<mat-menu #displayMenu="matMenu" class="no-padding-bottom">
<button
mat-menu-item
(click)="setAttributeForSelection('display', true)"
translate="file-attributes-csv-import.table-header.actions.enable-display"
></button>
<button
mat-menu-item
(click)="setAttributeForSelection('display', false)"
translate="file-attributes-csv-import.table-header.actions.disable-display"
></button>
</mat-menu>
<mat-menu #typeMenu="matMenu" class="no-padding-bottom">
<button *ngFor="let type of ['Text', 'Number', 'Date']" mat-menu-item (click)="setAttributeForSelection('type', type)">
{{ 'file-attributes-csv-import.types.' + type | translate }}
</button>
</mat-menu>
</ng-container>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.name" class="name"></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.type"></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.read-only" class="flex-center" icon="red:read-only"></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.display" class="flex-center" icon="red:visibility"></redaction-table-col-name>
<div></div>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state *ngIf="!allEntities.length" icon="red:attribute" screen="file-attributes-csv-import"> </redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<!-- Table lines -->
<div
class="table-item"
*cdkVirtualFor="let field of displayedEntities"
(mouseenter)="setHoveredColumn.emit(field.csvColumn)"
(mouseleave)="setHoveredColumn.emit()"
>
<div class="pr-0" (click)="toggleEntitySelected($event, field)">
<div *ngIf="!isEntitySelected(field)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(field)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div class="name" [class.editing]="field.editingName">
<div *ngIf="!field.editingName">
{{ field.name }}
</div>
<form (submit)="field.editingName = false; field.name = field.temporaryName" *ngIf="field.editingName">
<div class="red-input-group w-200">
<input name="name" [(ngModel)]="field.temporaryName" />
</div>
</form>
<redaction-circle-button
class="edit-name-button"
*ngIf="!field.editingName"
(action)="field.editingName = true"
tooltip="file-attributes-csv-import.action.edit-name"
type="dark-bg"
icon="red:edit"
>
</redaction-circle-button>
<ng-container *ngIf="field.editingName">
<redaction-circle-button
(action)="field.editingName = false; field.name = field.temporaryName"
tooltip="file-attributes-csv-import.action.save-name"
type="dark-bg"
icon="red:check-alt"
>
</redaction-circle-button>
<redaction-circle-button
(action)="field.editingName = false; field.temporaryName = field.name"
tooltip="file-attributes-csv-import.action.cancel-edit-name"
type="dark-bg"
icon="red:close"
>
</redaction-circle-button>
</ng-container>
</div>
<div>
<div class="red-input-group">
<mat-form-field class="no-label">
<mat-select [(ngModel)]="field.type">
<mat-option *ngFor="let type of ['Text', 'Number', 'Date']" [value]="type">
{{ 'file-attributes-csv-import.types.' + type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="center">
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div>
<div class="center"><mat-slide-toggle [(ngModel)]="field.display" color="primary"></mat-slide-toggle></div>
<div class="actions-container">
<div class="action-buttons">
<redaction-circle-button
(action)="toggleFieldActive.emit(field)"
[removeTooltip]="true"
tooltip="file-attributes-csv-import.action.remove"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>

View File

@ -0,0 +1,84 @@
@import '../../../../../../assets/styles/red-variables';
redaction-table-col-name::ng-deep {
> div {
padding: 0 13px 0 10px !important;
&.name {
padding-left: 22px !important;
}
}
}
.header-item {
padding: 0 24px 0 10px;
box-shadow: none;
border-top: 1px solid $separator;
.all-caps-label {
margin-right: 10px;
}
redaction-circle-button {
margin-right: 2px;
}
.separator {
margin-left: 14px;
background-color: $separator;
width: 1px;
height: 30px;
margin-right: 16px;
}
}
cdk-virtual-scroll-viewport {
height: calc(100% - 80px);
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 25vw) 150px auto auto auto 11px;
.table-item {
> div {
height: 50px;
&:not(.scrollbar-placeholder) {
padding-left: 10px;
&.center {
align-items: center;
}
}
&.name {
flex-direction: row;
align-items: center;
justify-content: flex-start;
&:not(.editing) {
padding-left: 22px;
}
.edit-name-button {
display: none;
}
redaction-circle-button:first-of-type {
margin-left: 7px;
margin-right: 2px;
}
}
}
&:hover .name .edit-name-button {
display: block;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 25vw) 150px auto auto auto;
}
}
}

View File

@ -0,0 +1,40 @@
import { Component, EventEmitter, Injector, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { BaseListingComponent } from '../../../../shared/base/base-listing.component';
import { Field } from '../file-attributes-csv-import-dialog.component';
@Component({
selector: 'redaction-active-fields-listing',
templateUrl: './active-fields-listing.component.html',
styleUrls: ['./active-fields-listing.component.scss']
})
export class ActiveFieldsListingComponent extends BaseListingComponent<Field> implements OnChanges {
@Input() public allEntities: Field[];
@Output() public allEntitiesChange = new EventEmitter<Field[]>();
@Output() public setHoveredColumn = new EventEmitter<string>();
@Output() public toggleFieldActive = new EventEmitter<Field>();
protected readonly _selectionKey = 'csvColumn';
constructor(protected readonly _injector: Injector) {
super(_injector);
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.allEntities) {
this.displayedEntities = this.allEntities;
this._updateSelection();
}
}
public deactivateSelection() {
this.allEntities = [...this.allEntities.filter((field) => !this.isEntitySelected(field))];
this.allEntitiesChange.emit(this.allEntities);
this.selectedEntitiesIds = [];
}
public setAttributeForSelection(attribute: string, value: any) {
for (const csvColumn of this.selectedEntitiesIds) {
this.allEntities.find((f) => f.csvColumn === csvColumn)[attribute] = value;
}
}
}

View File

@ -90,7 +90,7 @@
<div class="csv-header-pill-content" [class.search-open]="isSearchOpen"> <div class="csv-header-pill-content" [class.search-open]="isSearchOpen">
<div <div
class="csv-header-pill-wrapper" class="csv-header-pill-wrapper"
*ngFor="let field of filteredFields" *ngFor="let field of displayedEntities"
(mouseenter)="setHoveredColumn(field.csvColumn)" (mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()" (mouseleave)="setHoveredColumn()"
(click)="toggleFieldActive(field)" (click)="toggleFieldActive(field)"
@ -128,197 +128,11 @@
</div> </div>
</div> </div>
<div class="content-container"> <div class="content-container">
<div class="header-item"> <redaction-active-fields-listing
<div class="select-all-container"> [(allEntities)]="activeFields"
<div (setHoveredColumn)="setHoveredColumn($event)"
(click)="toggleSelectAll()" (toggleFieldActive)="toggleFieldActive($event)"
[class.active]="areAllFieldsSelected" ></redaction-active-fields-listing>
class="select-oval always-visible"
*ngIf="!areAllFieldsSelected && !areSomeFieldsSelected"
></div>
<mat-icon
*ngIf="areAllFieldsSelected"
(click)="toggleSelectAll()"
class="selection-icon active"
svgIcon="red:radio-selected"
></mat-icon>
<mat-icon
*ngIf="areSomeFieldsSelected && !areAllFieldsSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
</div>
<span class="all-caps-label">
{{ 'file-attributes-csv-import.table-header.title' | translate: { length: activeFields.length } }}
</span>
<ng-container *ngIf="areSomeFieldsSelected">
<redaction-circle-button
[matMenuTriggerFor]="readOnlyMenu"
tooltip="file-attributes-csv-import.table-header.actions.read-only"
type="dark-bg"
icon="red:read-only"
>
</redaction-circle-button>
<redaction-circle-button
[matMenuTriggerFor]="displayMenu"
tooltip="file-attributes-csv-import.table-header.actions.display"
type="dark-bg"
icon="red:visibility"
>
</redaction-circle-button>
<redaction-circle-button
(action)="deactivateSelection()"
tooltip="file-attributes-csv-import.table-header.actions.remove-selected"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
<div class="separator"></div>
<redaction-chevron-button
text="file-attributes-csv-import.table-header.actions.type"
[matMenuTriggerFor]="typeMenu"
></redaction-chevron-button>
<mat-menu #readOnlyMenu="matMenu" class="no-padding-bottom">
<button
mat-menu-item
(click)="setAttributeForSelection('readonly', true)"
translate="file-attributes-csv-import.table-header.actions.enable-read-only"
></button>
<button
mat-menu-item
(click)="setAttributeForSelection('readonly', false)"
translate="file-attributes-csv-import.table-header.actions.disable-read-only"
></button>
</mat-menu>
<mat-menu #displayMenu="matMenu" class="no-padding-bottom">
<button
mat-menu-item
(click)="setAttributeForSelection('display', true)"
translate="file-attributes-csv-import.table-header.actions.enable-display"
></button>
<button
mat-menu-item
(click)="setAttributeForSelection('display', false)"
translate="file-attributes-csv-import.table-header.actions.disable-display"
></button>
</mat-menu>
<mat-menu #typeMenu="matMenu" class="no-padding-bottom">
<button *ngFor="let type of ['Text', 'Number', 'Date']" mat-menu-item (click)="setAttributeForSelection('type', type)">
{{ 'file-attributes-csv-import.types.' + type | translate }}
</button>
</mat-menu>
</ng-container>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.name" class="name"></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-csv-import.table-col-names.type"></redaction-table-col-name>
<redaction-table-col-name
label="file-attributes-csv-import.table-col-names.read-only"
class="flex-center"
icon="red:read-only"
></redaction-table-col-name>
<redaction-table-col-name
label="file-attributes-csv-import.table-col-names.display"
class="flex-center"
icon="red:visibility"
></redaction-table-col-name>
<div></div>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state *ngIf="!activeFields.length" icon="red:attribute" screen="file-attributes-csv-import"> </redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="50" redactionHasScrollbar>
<!-- Table lines -->
<div
class="table-item"
*cdkVirtualFor="let field of activeFields"
(mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()"
>
<div class="pr-0" (click)="toggleFieldSelected(field.csvColumn)">
<div *ngIf="!isFieldSelected(field.csvColumn)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isFieldSelected(field.csvColumn)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div class="name" [class.editing]="field.editingName">
<div *ngIf="!field.editingName">
{{ field.name }}
</div>
<form (submit)="field.editingName = false; field.name = field.temporaryName" *ngIf="field.editingName">
<div class="red-input-group w-200">
<input name="name" [(ngModel)]="field.temporaryName" />
</div>
</form>
<redaction-circle-button
class="edit-name-button"
*ngIf="!field.editingName"
(action)="field.editingName = true"
tooltip="file-attributes-csv-import.action.edit-name"
type="dark-bg"
icon="red:edit"
>
</redaction-circle-button>
<ng-container *ngIf="field.editingName">
<redaction-circle-button
(action)="field.editingName = false; field.name = field.temporaryName"
tooltip="file-attributes-csv-import.action.save-name"
type="dark-bg"
icon="red:check-alt"
>
</redaction-circle-button>
<redaction-circle-button
(action)="field.editingName = false; field.temporaryName = field.name"
tooltip="file-attributes-csv-import.action.cancel-edit-name"
type="dark-bg"
icon="red:close"
>
</redaction-circle-button>
</ng-container>
</div>
<div>
<div class="red-input-group">
<mat-form-field class="no-label">
<mat-select [(ngModel)]="field.type">
<mat-option *ngFor="let type of ['Text', 'Number', 'Date']" [value]="type">
{{ 'file-attributes-csv-import.types.' + type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="center">
<mat-slide-toggle [(ngModel)]="field.readonly" color="primary"></mat-slide-toggle>
</div>
<div class="center"><mat-slide-toggle [(ngModel)]="field.display" color="primary"></mat-slide-toggle></div>
<div class="actions-container">
<div class="action-buttons">
<redaction-circle-button
(action)="toggleFieldActive(field)"
[removeTooltip]="true"
tooltip="file-attributes-csv-import.action.remove"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
</div> </div>
</div> </div>
</div> </div>

View File

@ -114,6 +114,7 @@
> .left { > .left {
width: 375px; width: 375px;
min-width: 375px;
background: $grey-2; background: $grey-2;
.csv-header-pill-content { .csv-header-pill-content {
@ -198,88 +199,5 @@
> .content-container { > .content-container {
width: 100%; width: 100%;
redaction-table-col-name::ng-deep {
> div {
padding: 0 13px 0 10px !important;
&.name {
padding-left: 22px !important;
}
}
}
.header-item {
padding: 0 24px 0 10px;
box-shadow: none;
border-top: 1px solid $separator;
.all-caps-label {
margin-right: 10px;
}
redaction-circle-button {
margin-right: 2px;
}
.separator {
margin-left: 14px;
background-color: $separator;
width: 1px;
height: 30px;
margin-right: 16px;
}
}
cdk-virtual-scroll-viewport {
height: calc(100% - 80px);
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 25vw) 150px auto auto auto 11px;
.table-item {
> div {
height: 50px;
&:not(.scrollbar-placeholder) {
padding-left: 10px;
&.center {
align-items: center;
}
}
&.name {
flex-direction: row;
align-items: center;
justify-content: flex-start;
&:not(.editing) {
padding-left: 22px;
}
.edit-name-button {
display: none;
}
redaction-circle-button:first-of-type {
margin-left: 7px;
margin-right: 2px;
}
}
}
&:hover .name .edit-name-button {
display: block;
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: 30px minmax(0, 25vw) 150px auto auto auto;
}
}
}
} }
} }

View File

@ -1,13 +1,13 @@
import { Component, Inject, ViewChild } from '@angular/core'; import { Component, Inject, Injector, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse'; import * as Papa from 'papaparse';
import { FileAttributesControllerService } from '@redaction/red-ui-http'; import { FileAttributesControllerService } from '@redaction/red-ui-http';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { debounce } from '../../../../utils/debounce';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
enum FieldType { enum FieldType {
Text = 'Text', Text = 'Text',
@ -15,7 +15,7 @@ enum FieldType {
Date = 'Date' Date = 'Date'
} }
interface Field { export interface Field {
csvColumn: string; csvColumn: string;
name: string; name: string;
type: FieldType; type: FieldType;
@ -30,17 +30,16 @@ interface Field {
templateUrl: './file-attributes-csv-import-dialog.component.html', templateUrl: './file-attributes-csv-import-dialog.component.html',
styleUrls: ['./file-attributes-csv-import-dialog.component.scss'] styleUrls: ['./file-attributes-csv-import-dialog.component.scss']
}) })
export class FileAttributesCsvImportDialogComponent { export class FileAttributesCsvImportDialogComponent extends BaseListingComponent<Field> {
protected readonly _searchKey = 'csvColumn';
public csvFile: File; public csvFile: File;
public ruleSetId: string; public ruleSetId: string;
public parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] }; public parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
public hoveredColumn: string; public hoveredColumn: string;
public activeFields: Field[] = []; public activeFields: Field[] = [];
public selectedFields: string[] = [];
public baseConfigForm: FormGroup; public baseConfigForm: FormGroup;
public isSearchOpen = false; public isSearchOpen = false;
public searchForm: FormGroup;
public filteredFields: Field[];
public previewExpanded = true; public previewExpanded = true;
public filteredKeyOptions: Observable<string[]>; public filteredKeyOptions: Observable<string[]>;
public keepPreview = false; public keepPreview = false;
@ -50,18 +49,15 @@ export class FileAttributesCsvImportDialogComponent {
constructor( constructor(
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesControllerService: FileAttributesControllerService, private readonly _fileAttributesControllerService: FileAttributesControllerService,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>, public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
protected readonly _injector: Injector,
@Inject(MAT_DIALOG_DATA) public data: { csv: File; ruleSetId: string } @Inject(MAT_DIALOG_DATA) public data: { csv: File; ruleSetId: string }
) { ) {
super(_injector);
this.csvFile = data.csv; this.csvFile = data.csv;
this.ruleSetId = data.ruleSetId; this.ruleSetId = data.ruleSetId;
this.searchForm = this._formBuilder.group({
query: ['']
});
this.baseConfigForm = this._formBuilder.group({ this.baseConfigForm = this._formBuilder.group({
filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]], filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]],
delimiter: [undefined, Validators.required], delimiter: [undefined, Validators.required],
@ -69,13 +65,6 @@ export class FileAttributesCsvImportDialogComponent {
}); });
this._readFile(); this._readFile();
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
}
@debounce(200)
private _executeSearch(value: { query: string }) {
this.filteredFields = this.parseResult.fields.filter((f) => f.csvColumn.toLowerCase().includes(value.query.toLowerCase()));
} }
private _autocompleteStringValidator(): ValidatorFn { private _autocompleteStringValidator(): ValidatorFn {
@ -98,12 +87,14 @@ export class FileAttributesCsvImportDialogComponent {
if (!this.baseConfigForm.get('delimiter').value) { if (!this.baseConfigForm.get('delimiter').value) {
this.baseConfigForm.patchValue({ delimiter: this.parseResult.meta.delimiter }); this.baseConfigForm.patchValue({ delimiter: this.parseResult.meta.delimiter });
} }
this.parseResult.fields = this.parseResult.meta.fields.map((field) => this._buildAttribute(field)); this.allEntities = this.parseResult.meta.fields.map((field) => this._buildAttribute(field));
this.filteredFields = [...this.parseResult.fields]; this.displayedEntities = [...this.allEntities];
this.filteredKeyOptions = this.baseConfigForm.get('filenameMappingColumnHeaderName').valueChanges.pipe( this.filteredKeyOptions = this.baseConfigForm.get('filenameMappingColumnHeaderName').valueChanges.pipe(
startWith(''), startWith(''),
map((value: string) => this.parseResult.meta.fields.filter((field) => field.toLowerCase().indexOf(value.toLowerCase()) !== -1)) map((value: string) =>
this.allEntities.filter((field) => field.csvColumn.toLowerCase().indexOf(value.toLowerCase()) !== -1).map((field) => field.csvColumn)
)
); );
}); });
reader.readAsText(this.csvFile, this.baseConfigForm.get('encoding').value); reader.readAsText(this.csvFile, this.baseConfigForm.get('encoding').value);
@ -137,9 +128,6 @@ export class FileAttributesCsvImportDialogComponent {
} else { } else {
this.activeFields.splice(this.activeFields.indexOf(field), 1); this.activeFields.splice(this.activeFields.indexOf(field), 1);
this.activeFields = [...this.activeFields]; this.activeFields = [...this.activeFields];
if (this.isFieldSelected(field.csvColumn)) {
this.toggleFieldSelected(field.csvColumn);
}
} }
} }
@ -157,52 +145,11 @@ export class FileAttributesCsvImportDialogComponent {
} }
public activateAll() { public activateAll() {
this.activeFields = [...this.parseResult.fields]; this.activeFields = [...this.allEntities];
} }
public deactivateAll() { public deactivateAll() {
this.activeFields = []; this.activeFields = [];
this.selectedFields = [];
}
public toggleFieldSelected(field: string) {
const idx = this.selectedFields.indexOf(field);
if (idx === -1) {
this.selectedFields.push(field);
} else {
this.selectedFields.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeFieldsSelected) {
this.selectedFields = [];
} else {
this.selectedFields = this.activeFields.map((field) => field.csvColumn);
}
}
public get areAllFieldsSelected() {
return this.activeFields.length !== 0 && this.selectedFields.length === this.activeFields.length;
}
public get areSomeFieldsSelected() {
return this.selectedFields.length > 0;
}
public isFieldSelected(field: string) {
return this.selectedFields.indexOf(field) !== -1;
}
public deactivateSelection() {
this.activeFields = [...this.activeFields.filter((field) => !this.isFieldSelected(field.csvColumn))];
this.selectedFields = [];
}
public setAttributeForSelection(attribute: string, value: any) {
for (const csvColumn of this.selectedFields) {
this.activeFields.find((f) => f.csvColumn === csvColumn)[attribute] = value;
}
} }
public async save() { public async save() {

View File

@ -15,7 +15,7 @@
<div class="content-container"> <div class="content-container">
<div class="header-item"> <div class="header-item">
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'default-colors-screen.table-header.title' | translate: { length: colors.length } }} {{ 'default-colors-screen.table-header.title' | translate: { length: allEntities.length } }}
</span> </span>
</div> </div>
@ -36,7 +36,7 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let color of colors | sortBy: sortingOption.order:sortingOption.column"> <div class="table-item" *cdkVirtualFor="let color of allEntities | sortBy: sortingOption.order:sortingOption.column">
<div> <div>
<div class="table-item-title heading" [translate]="'default-colors-screen.types.' + color.key"></div> <div class="table-item-title heading" [translate]="'default-colors-screen.types.' + color.key"></div>
</div> </div>

View File

@ -1,41 +1,35 @@
import { Component } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { Colors, DictionaryControllerService } from '@redaction/red-ui-http'; import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({ @Component({
selector: 'redaction-default-colors-screen', selector: 'redaction-default-colors-screen',
templateUrl: './default-colors-screen.component.html', templateUrl: './default-colors-screen.component.html',
styleUrls: ['./default-colors-screen.component.scss'] styleUrls: ['./default-colors-screen.component.scss']
}) })
export class DefaultColorsScreenComponent { export class DefaultColorsScreenComponent extends BaseListingComponent<{ key: string; value: string }> {
protected readonly _sortKey = 'default-colors';
public viewReady = false; public viewReady = false;
private _colorsObj: Colors; private _colorsObj: Colors;
public colors: { key: string; value: string }[] = [];
constructor( constructor(
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _dictionaryControllerService: DictionaryControllerService, private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _sortingService: SortingService,
private readonly _dialogService: AdminDialogService, private readonly _dialogService: AdminDialogService,
public readonly permissionsService: PermissionsService public readonly permissionsService: PermissionsService,
protected readonly _injector: Injector
) { ) {
super(_injector);
this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId); this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId);
this._loadColors(); this._loadColors();
} }
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption('default-colors');
}
public toggleSort($event) {
this._sortingService.toggleSort('default-colors', $event);
}
public async loadRuleSetsData(): Promise<void> { public async loadRuleSetsData(): Promise<void> {
await this._appStateService.loadAllRuleSets(); await this._appStateService.loadAllRuleSets();
} }
@ -46,7 +40,7 @@ export class DefaultColorsScreenComponent {
.toPromise() .toPromise()
.then((data) => { .then((data) => {
this._colorsObj = data; this._colorsObj = data;
this.colors = Object.keys(data).map((key) => ({ this.allEntities = Object.keys(data).map((key) => ({
key, key,
value: data[key] value: data[key]
})); }));

View File

@ -17,13 +17,13 @@
<div class="select-all-container"> <div class="select-all-container">
<div <div
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
[class.active]="areAllDictsSelected" [class.active]="areAllEntitiesSelected"
class="select-oval always-visible" class="select-oval always-visible"
*ngIf="!areAllDictsSelected && !areSomeDictsSelected" *ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div> ></div>
<mat-icon *ngIf="areAllDictsSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon> <mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon <mat-icon
*ngIf="areSomeDictsSelected && !areAllDictsSelected" *ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
class="selection-icon" class="selection-icon"
svgIcon="red:radio-indeterminate" svgIcon="red:radio-indeterminate"
@ -31,7 +31,7 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'dictionary-listing.table-header.title' | translate: { length: displayedDictionaries.length } }} {{ 'dictionary-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span> </span>
<div class="attributes-actions-container"> <div class="attributes-actions-container">
@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!dictionaries.length"> <div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!allEntities.length">
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -74,28 +74,24 @@
</div> </div>
<redaction-empty-state <redaction-empty-state
*ngIf="!dictionaries.length" *ngIf="!allEntities.length"
icon="red:dictionary" icon="red:dictionary"
(action)="openAddEditDictionaryDialog()" (action)="openAddEditDictionaryDialog()"
[showButton]="permissionsService.isAdmin()" [showButton]="permissionsService.isAdmin()"
screen="dictionary-listing" screen="dictionary-listing"
></redaction-empty-state> ></redaction-empty-state>
<redaction-empty-state <redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="dictionary-listing" type="no-match"></redaction-empty-state>
*ngIf="dictionaries.length && !displayedDictionaries.length"
screen="dictionary-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div <div
class="table-item pointer" class="table-item pointer"
*cdkVirtualFor="let dict of displayedDictionaries | sortBy: sortingOption.order:sortingOption.column" *cdkVirtualFor="let dict of displayedEntities | sortBy: sortingOption.order:sortingOption.column"
[routerLink]="[dict.type]" [routerLink]="[dict.type]"
> >
<div class="pr-0" (click)="toggleDictSelected($event, dict)"> <div class="pr-0" (click)="toggleEntitySelected($event, dict)">
<div *ngIf="!isDictSelected(dict)" class="select-oval"></div> <div *ngIf="!isEntitySelected(dict)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isDictSelected(dict)" svgIcon="red:radio-selected"></mat-icon> <mat-icon class="selection-icon active" *ngIf="isEntitySelected(dict)" svgIcon="red:radio-selected"></mat-icon>
</div> </div>
<div> <div>
@ -137,7 +133,7 @@
</redaction-circle-button> </redaction-circle-button>
<redaction-circle-button <redaction-circle-button
(action)="openEditDictionaryDialog($event, dict)" (action)="openAddEditDictionaryDialog($event, dict)"
*ngIf="permissionsService.isAdmin()" *ngIf="permissionsService.isAdmin()"
tooltip="dictionary-listing.action.edit" tooltip="dictionary-listing.action.edit"
type="dark-bg" type="dark-bg"
@ -153,7 +149,7 @@
<div class="right-container" redactionHasScrollbar> <div class="right-container" redactionHasScrollbar>
<redaction-simple-doughnut-chart <redaction-simple-doughnut-chart
*ngIf="dictionaries.length" *ngIf="allEntities.length"
[config]="chartData" [config]="chartData"
[strokeWidth]="15" [strokeWidth]="15"
[radius]="82" [radius]="82"
@ -164,3 +160,5 @@
</div> </div>
</div> </div>
</section> </section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,79 +1,66 @@
import { Component, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http'; import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { tap } from 'rxjs/operators'; import { defaultIfEmpty, tap } from 'rxjs/operators';
import { forkJoin } from 'rxjs'; import { forkJoin } from 'rxjs';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({ @Component({
selector: 'redaction-dictionary-listing-screen', selector: 'redaction-dictionary-listing-screen',
templateUrl: './dictionary-listing-screen.component.html', templateUrl: './dictionary-listing-screen.component.html',
styleUrls: ['./dictionary-listing-screen.component.scss'] styleUrls: ['./dictionary-listing-screen.component.scss']
}) })
export class DictionaryListingScreenComponent implements OnInit { export class DictionaryListingScreenComponent extends BaseListingComponent<TypeValue> implements OnInit {
protected readonly _searchKey = 'label';
protected readonly _selectionKey = 'type';
protected readonly _sortKey = 'dictionary-listing';
public viewReady = false;
public chartData: DoughnutChartConfig[] = []; public chartData: DoughnutChartConfig[] = [];
public dictionaries: TypeValue[];
public displayedDictionaries: TypeValue[];
public selectedDictKeys: string[] = [];
public searchForm: FormGroup;
constructor( constructor(
private readonly _dialogService: AdminDialogService, private readonly _dialogService: AdminDialogService,
private readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _dictionaryControllerService: DictionaryControllerService, private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
public readonly permissionsService: PermissionsService public readonly permissionsService: PermissionsService,
protected readonly _injector: Injector
) { ) {
super(_injector);
this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId); this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId);
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
} }
ngOnInit(): void { ngOnInit(): void {
this._loadDictionaryData(); this._loadDictionaryData();
this._calculateData();
}
@debounce(200)
private _executeSearch(value: { query: string }) {
this.displayedDictionaries = this.dictionaries.filter((dict) => dict.label.toLowerCase().includes(value.query.toLowerCase()));
} }
private _loadDictionaryData() { private _loadDictionaryData() {
const appStateDictionaryData = this._appStateService.dictionaryData[this._appStateService.activeRuleSetId]; const appStateDictionaryData = this._appStateService.dictionaryData[this._appStateService.activeRuleSetId];
this.dictionaries = Object.keys(appStateDictionaryData) this.allEntities = Object.keys(appStateDictionaryData)
.map((key) => appStateDictionaryData[key]) .map((key) => appStateDictionaryData[key])
.filter((d) => !d.virtual || d.type === 'false_positive'); .filter((d) => !d.virtual || d.type === 'false_positive');
this.displayedDictionaries = [...this.dictionaries]; this.displayedEntities = [...this.allEntities];
const dataObs = []; const dataObs = this.allEntities.map((dict) =>
this.dictionaries.forEach((item) => { this._dictionaryControllerService.getDictionaryForType(dict.type, this._appStateService.activeRuleSetId).pipe(
const observable = this._dictionaryControllerService.getDictionaryForType(item.type, this._appStateService.activeRuleSetId).pipe(
tap((values) => { tap((values) => {
item.entries = values.entries ? values.entries : []; dict.entries = values.entries ? values.entries : [];
}) })
); )
dataObs.push(observable); );
}); forkJoin(dataObs)
forkJoin(dataObs).subscribe(() => { .pipe(defaultIfEmpty(null))
this._calculateData(); .subscribe(() => {
}); this._calculateData();
});
} }
private _calculateData() { private _calculateData() {
this.chartData = []; this.chartData = [];
for (const dict of this.dictionaries) { for (const dict of this.allEntities) {
this.chartData.push({ this.chartData.push({
value: dict.entries ? dict.entries.length : 0, value: dict.entries ? dict.entries.length : 0,
color: dict.hexColor, color: dict.hexColor,
@ -82,47 +69,11 @@ export class DictionaryListingScreenComponent implements OnInit {
}); });
} }
this.chartData.sort((a, b) => (a.label < b.label ? -1 : 1)); this.chartData.sort((a, b) => (a.label < b.label ? -1 : 1));
this.viewReady = true;
} }
public get sortingOption(): SortingOption { openAddEditDictionaryDialog($event?: MouseEvent, dict?: TypeValue) {
return this._sortingService.getSortingOption('dictionary-listing'); $event?.stopPropagation();
}
public toggleSort($event) {
this._sortingService.toggleSort('dictionary-listing', $event);
}
toggleDictSelected($event: MouseEvent, dict: TypeValue) {
$event.stopPropagation();
const idx = this.selectedDictKeys.indexOf(dict.type);
if (idx === -1) {
this.selectedDictKeys.push(dict.type);
} else {
this.selectedDictKeys.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeDictsSelected) {
this.selectedDictKeys = [];
} else {
this.selectedDictKeys = this.displayedDictionaries.map((dict) => dict.type);
}
}
public get areAllDictsSelected() {
return this.displayedDictionaries.length !== 0 && this.selectedDictKeys.length === this.displayedDictionaries.length;
}
public get areSomeDictsSelected() {
return this.selectedDictKeys.length > 0;
}
public isDictSelected(dict: TypeValue) {
return this.selectedDictKeys.indexOf(dict.type) !== -1;
}
openAddEditDictionaryDialog(dict?: TypeValue) {
this._dialogService.openAddEditDictionaryDialog(dict, this._appStateService.activeRuleSetId, async (newDictionary) => { this._dialogService.openAddEditDictionaryDialog(dict, this._appStateService.activeRuleSetId, async (newDictionary) => {
if (newDictionary) { if (newDictionary) {
await this._appStateService.loadDictionaryData(); await this._appStateService.loadDictionaryData();
@ -131,11 +82,6 @@ export class DictionaryListingScreenComponent implements OnInit {
}); });
} }
openEditDictionaryDialog($event: any, dict: TypeValue) {
$event.stopPropagation();
this.openAddEditDictionaryDialog(dict);
}
openDeleteDictionaryDialog($event: any, dict: TypeValue) { openDeleteDictionaryDialog($event: any, dict: TypeValue) {
this._dialogService.openDeleteDictionaryDialog($event, dict, this._appStateService.activeRuleSetId, async () => { this._dialogService.openDeleteDictionaryDialog($event, dict, this._appStateService.activeRuleSetId, async () => {
await this._appStateService.loadDictionaryData(); await this._appStateService.loadDictionaryData();

View File

@ -15,18 +15,13 @@
<div class="select-all-container"> <div class="select-all-container">
<div <div
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
[class.active]="areAllAttributesSelected" [class.active]="areAllEntitiesSelected"
class="select-oval always-visible" class="select-oval always-visible"
*ngIf="!areAllAttributesSelected && !areSomeAttributesSelected" *ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div> ></div>
<mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon <mat-icon
*ngIf="areAllAttributesSelected" *ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon active"
svgIcon="red:radio-selected"
></mat-icon>
<mat-icon
*ngIf="areSomeAttributesSelected && !areAllAttributesSelected"
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
class="selection-icon" class="selection-icon"
svgIcon="red:radio-indeterminate" svgIcon="red:radio-indeterminate"
@ -34,11 +29,11 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'file-attributes-listing.table-header.title' | translate: { length: displayedAttributes.length } }} {{ 'file-attributes-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span> </span>
<redaction-circle-button <redaction-circle-button
*ngIf="areSomeAttributesSelected" *ngIf="areSomeEntitiesSelected"
tooltip="file-attributes-listing.bulk-actions.delete" tooltip="file-attributes-listing.bulk-actions.delete"
type="dark-bg" type="dark-bg"
icon="red:trash" icon="red:trash"
@ -69,7 +64,7 @@
</div> </div>
</div> </div>
<div [class.no-data]="!attributes.length" class="table-header" redactionSyncWidth="table-item"> <div [class.no-data]="!allEntities.length" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -77,7 +72,7 @@
(toggleSort)="toggleSort($event)" (toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption" [activeSortingOption]="sortingOption"
[withSort]="true" [withSort]="true"
column="name" column="label"
></redaction-table-col-name> ></redaction-table-col-name>
<redaction-table-col-name label="file-attributes-listing.table-col-names.read-only" class="flex-center"></redaction-table-col-name> <redaction-table-col-name label="file-attributes-listing.table-col-names.read-only" class="flex-center"></redaction-table-col-name>
@ -89,20 +84,20 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!attributes.length" screen="file-attributes-listing" icon="red:attribute"></redaction-empty-state> <redaction-empty-state *ngIf="!allEntities.length" screen="file-attributes-listing" icon="red:attribute"></redaction-empty-state>
<redaction-empty-state <redaction-empty-state
*ngIf="attributes.length && !displayedAttributes.length" *ngIf="allEntities.length && !displayedEntities.length"
screen="file-attributes-listing" screen="file-attributes-listing"
type="no-match" type="no-match"
></redaction-empty-state> ></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let attribute of displayedAttributes | sortBy: sortingOption.order:sortingOption.column"> <div class="table-item" *cdkVirtualFor="let attribute of displayedEntities | sortBy: sortingOption.order:sortingOption.column">
<div class="pr-0" (click)="toggleAttributeSelected($event, attribute)"> <div class="pr-0" (click)="toggleEntitySelected($event, attribute)">
<div *ngIf="!isAttributeSelected(attribute)" class="select-oval"></div> <div *ngIf="!isEntitySelected(attribute)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isAttributeSelected(attribute)" svgIcon="red:radio-selected"></mat-icon> <mat-icon class="selection-icon active" *ngIf="isEntitySelected(attribute)" svgIcon="red:radio-selected"></mat-icon>
</div> </div>
<div class="label"> <div class="label">

View File

@ -1,23 +1,21 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Injector, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http'; import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { debounce } from '../../../../utils/debounce';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({ @Component({
selector: 'redaction-file-attributes-listing-screen', selector: 'redaction-file-attributes-listing-screen',
templateUrl: './file-attributes-listing-screen.component.html', templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss'] styleUrls: ['./file-attributes-listing-screen.component.scss']
}) })
export class FileAttributesListingScreenComponent implements OnInit { export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit {
public searchForm: FormGroup; protected readonly _searchKey = 'label';
public attributes: FileAttributeConfig[] = []; protected readonly _selectionKey = 'id';
public displayedAttributes: FileAttributeConfig[] = []; protected readonly _sortKey = 'file-attributes-listing';
public selectedFileAttributeIds: string[] = [];
public viewReady = false; public viewReady = false;
public loading = false; public loading = false;
@ -25,20 +23,14 @@ export class FileAttributesListingScreenComponent implements OnInit {
constructor( constructor(
public readonly permissionsService: PermissionsService, public readonly permissionsService: PermissionsService,
public readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesControllerService, private readonly _fileAttributesService: FileAttributesControllerService,
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _dialogService: AdminDialogService private readonly _dialogService: AdminDialogService,
protected readonly _injector: Injector
) { ) {
super(_injector);
this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId); this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId);
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
} }
async ngOnInit() { async ngOnInit() {
@ -48,34 +40,15 @@ export class FileAttributesListingScreenComponent implements OnInit {
private async _loadData() { private async _loadData() {
try { try {
const response = await this._fileAttributesService.getFileAttributesConfiguration(this._appStateService.activeRuleSetId).toPromise(); const response = await this._fileAttributesService.getFileAttributesConfiguration(this._appStateService.activeRuleSetId).toPromise();
this.attributes = response?.fileAttributeConfigs || []; this.allEntities = response?.fileAttributeConfigs || [];
} catch (e) { } catch (e) {
} finally { } finally {
// Remove potentially deleted items
this.selectedFileAttributeIds = this.selectedFileAttributeIds.filter((id) => !!this.attributes.find((attr) => attr.id === id));
this._executeSearch(); this._executeSearch();
this.viewReady = true; this.viewReady = true;
this.loading = false; this.loading = false;
} }
} }
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption('file-attributes-listing');
}
public toggleSort($event) {
this._sortingService.toggleSort('file-attributes-listing', $event);
}
@debounce(200)
private _executeSearch(value?: { query: string }) {
if (!value) {
value = { query: this.searchForm.get('query').value };
}
this.displayedAttributes = this.attributes.filter((attribute) => attribute.label.toLowerCase().includes(value.query.toLowerCase()));
}
public openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) { public openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
$event.stopPropagation(); $event.stopPropagation();
this._dialogService.openAddEditFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async (newValue: FileAttributeConfig) => { this._dialogService.openAddEditFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async (newValue: FileAttributeConfig) => {
@ -92,42 +65,12 @@ export class FileAttributesListingScreenComponent implements OnInit {
if (!!fileAttribute) { if (!!fileAttribute) {
await this._fileAttributesService.deleteFileAttribute(this._appStateService.activeRuleSetId, fileAttribute.id).toPromise(); await this._fileAttributesService.deleteFileAttribute(this._appStateService.activeRuleSetId, fileAttribute.id).toPromise();
} else { } else {
await this._fileAttributesService.deleteFileAttributes(this.selectedFileAttributeIds, this._appStateService.activeRuleSetId).toPromise(); await this._fileAttributesService.deleteFileAttributes(this.selectedEntitiesIds, this._appStateService.activeRuleSetId).toPromise();
} }
await this._loadData(); await this._loadData();
}); });
} }
public toggleAttributeSelected($event: MouseEvent, attribute: FileAttributeConfig) {
$event.stopPropagation();
const idx = this.selectedFileAttributeIds.indexOf(attribute.id);
if (idx === -1) {
this.selectedFileAttributeIds.push(attribute.id);
} else {
this.selectedFileAttributeIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeAttributesSelected) {
this.selectedFileAttributeIds = [];
} else {
this.selectedFileAttributeIds = this.displayedAttributes.map((a) => a.id);
}
}
public get areAllAttributesSelected() {
return this.displayedAttributes.length !== 0 && this.selectedFileAttributeIds.length === this.displayedAttributes.length;
}
public get areSomeAttributesSelected() {
return this.selectedFileAttributeIds.length > 0;
}
public isAttributeSelected(attribute: FileAttributeConfig) {
return this.selectedFileAttributeIds.indexOf(attribute.id) !== -1;
}
public importCSV(files: FileList | File[]) { public importCSV(files: FileList | File[]) {
const csvFile = files[0]; const csvFile = files[0];
this._fileInput.nativeElement.value = null; this._fileInput.nativeElement.value = null;

View File

@ -30,13 +30,13 @@
<div class="select-all-container"> <div class="select-all-container">
<div <div
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
[class.active]="areAllRuleSetsSelected" [class.active]="areAllEntitiesSelected"
class="select-oval always-visible" class="select-oval always-visible"
*ngIf="!areAllRuleSetsSelected && !areSomeRuleSetsSelected" *ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div> ></div>
<mat-icon *ngIf="areAllRuleSetsSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon> <mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon <mat-icon
*ngIf="areSomeRuleSetsSelected && !areAllRuleSetsSelected" *ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
class="selection-icon" class="selection-icon"
svgIcon="red:radio-indeterminate" svgIcon="red:radio-indeterminate"
@ -44,11 +44,11 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'project-templates-listing.table-header.title' | translate: { length: displayedRuleSets.length } }} {{ 'project-templates-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span> </span>
</div> </div>
<div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!ruleSets.length"> <div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!allEntities.length">
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
<redaction-table-col-name <redaction-table-col-name
@ -76,10 +76,10 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!ruleSets.length" icon="red:template" screen="project-templates-listing"></redaction-empty-state> <redaction-empty-state *ngIf="!allEntities.length" icon="red:template" screen="project-templates-listing"></redaction-empty-state>
<redaction-empty-state <redaction-empty-state
*ngIf="ruleSets.length && !displayedRuleSets.length" *ngIf="allEntities.length && !displayedEntities.length"
screen="project-templates-listing" screen="project-templates-listing"
type="no-match" type="no-match"
></redaction-empty-state> ></redaction-empty-state>
@ -87,12 +87,12 @@
<cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar>
<div <div
class="table-item pointer" class="table-item pointer"
*cdkVirtualFor="let ruleSet of displayedRuleSets | sortBy: sortingOption.order:sortingOption.column" *cdkVirtualFor="let ruleSet of displayedEntities | sortBy: sortingOption.order:sortingOption.column"
[routerLink]="[ruleSet.ruleSetId, 'dictionaries']" [routerLink]="[ruleSet.ruleSetId, 'dictionaries']"
> >
<div class="pr-0" (click)="toggleTemplateSelected($event, ruleSet)"> <div class="pr-0" (click)="toggleEntitySelected($event, ruleSet)">
<div *ngIf="!isRuleSetSelected(ruleSet)" class="select-oval"></div> <div *ngIf="!isEntitySelected(ruleSet)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isRuleSetSelected(ruleSet)" svgIcon="red:radio-selected"></mat-icon> <mat-icon class="selection-icon active" *ngIf="isEntitySelected(ruleSet)" svgIcon="red:radio-selected"></mat-icon>
</div> </div>
<div> <div>

View File

@ -1,57 +1,44 @@
import { Component, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { RuleSetModel } from '@redaction/red-ui-http';
import { UserPreferenceService } from '../../../../services/user-preference.service'; import { UserPreferenceService } from '../../../../services/user-preference.service';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { RuleSetModel } from '@redaction/red-ui-http';
@Component({ @Component({
selector: 'redaction-rule-sets-listing-screen', selector: 'redaction-rule-sets-listing-screen',
templateUrl: './rule-sets-listing-screen.component.html', templateUrl: './rule-sets-listing-screen.component.html',
styleUrls: ['./rule-sets-listing-screen.component.scss'] styleUrls: ['./rule-sets-listing-screen.component.scss']
}) })
export class RuleSetsListingScreenComponent implements OnInit { export class RuleSetsListingScreenComponent extends BaseListingComponent<RuleSetModel> implements OnInit {
public ruleSets: RuleSetModel[]; protected readonly _searchKey = 'name';
public displayedRuleSets: RuleSetModel[]; protected readonly _selectionKey = 'ruleSetId';
public selectedRuleSetIds: string[] = []; protected readonly _sortKey = 'rule-sets-listing';
public searchForm: FormGroup;
constructor( constructor(
private readonly _dialogService: AdminDialogService, private readonly _dialogService: AdminDialogService,
private readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
public readonly permissionsService: PermissionsService, public readonly permissionsService: PermissionsService,
public readonly userPreferenceService: UserPreferenceService public readonly userPreferenceService: UserPreferenceService,
protected readonly _injector: Injector
) { ) {
this.searchForm = this._formBuilder.group({ super(_injector);
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
} }
ngOnInit(): void { ngOnInit(): void {
this.loadRuleSetsData(); this.loadRuleSetsData();
} }
@debounce(200)
private _executeSearch(value: { query: string }) {
this.displayedRuleSets = this.ruleSets.filter((pt) => pt.name.toLowerCase().includes(value.query.toLowerCase()));
}
public loadRuleSetsData() { public loadRuleSetsData() {
this._appStateService.reset(); this._appStateService.reset();
this.ruleSets = this._appStateService.ruleSets; this.allEntities = this._appStateService.ruleSets;
this.displayedRuleSets = [...this.ruleSets]; this._executeSearch();
this._loadRuleSetStats(); this._loadRuleSetStats();
} }
private _loadRuleSetStats() { private _loadRuleSetStats() {
this.ruleSets.forEach((rs) => { this.allEntities.forEach((rs) => {
const dictionaries = this._appStateService.dictionaryData[rs.ruleSetId]; const dictionaries = this._appStateService.dictionaryData[rs.ruleSetId];
if (dictionaries) { if (dictionaries) {
rs.dictionariesCount = Object.keys(dictionaries) rs.dictionariesCount = Object.keys(dictionaries)
@ -64,45 +51,7 @@ export class RuleSetsListingScreenComponent implements OnInit {
}); });
} }
public get sortingOption(): SortingOption { public openAddRuleSetDialog() {
return this._sortingService.getSortingOption('rule-sets-listing');
}
public toggleSort($event) {
this._sortingService.toggleSort('rule-sets-listing', $event);
}
toggleTemplateSelected($event: MouseEvent, ruleSet: RuleSetModel) {
$event.stopPropagation();
const idx = this.selectedRuleSetIds.indexOf(ruleSet.ruleSetId);
if (idx === -1) {
this.selectedRuleSetIds.push(ruleSet.ruleSetId);
} else {
this.selectedRuleSetIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeRuleSetsSelected) {
this.selectedRuleSetIds = [];
} else {
this.selectedRuleSetIds = this.displayedRuleSets.map((rs) => rs.ruleSetId);
}
}
public get areAllRuleSetsSelected() {
return this.displayedRuleSets.length !== 0 && this.selectedRuleSetIds.length === this.displayedRuleSets.length;
}
public get areSomeRuleSetsSelected() {
return this.selectedRuleSetIds.length > 0;
}
public isRuleSetSelected(ruleSet: RuleSetModel) {
return this.selectedRuleSetIds.indexOf(ruleSet.ruleSetId) !== -1;
}
openAddRuleSetDialog() {
this._dialogService.openAddEditRuleSetDialog(null, async (newRuleSet) => { this._dialogService.openAddEditRuleSetDialog(null, async (newRuleSet) => {
if (newRuleSet) { if (newRuleSet) {
this.loadRuleSetsData(); this.loadRuleSetsData();

View File

@ -28,23 +28,23 @@
<div class="select-all-container"> <div class="select-all-container">
<div <div
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
[class.active]="areAllUsersSelected" [class.active]="areAllEntitiesSelected"
class="select-oval always-visible" class="select-oval always-visible"
*ngIf="!areAllUsersSelected && !areSomeUsersSelected" *ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div> ></div>
<mat-icon *ngIf="areAllUsersSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon> <mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon <mat-icon
*ngIf="areSomeUsersSelected && !areAllUsersSelected" *ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
class="selection-icon" class="selection-icon"
svgIcon="red:radio-indeterminate" svgIcon="red:radio-indeterminate"
></mat-icon> ></mat-icon>
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }} {{ 'user-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span> </span>
<ng-container *ngIf="areSomeUsersSelected && !loading"> <ng-container *ngIf="areSomeEntitiesSelected && !loading">
<redaction-circle-button <redaction-circle-button
(action)="bulkDelete()" (action)="bulkDelete()"
[disabled]="!canDeleteSelected" [disabled]="!canDeleteSelected"
@ -73,14 +73,14 @@
<div class="scrollbar-placeholder"></div> <div class="scrollbar-placeholder"></div>
</div> </div>
<redaction-empty-state *ngIf="!displayedUsers.length" screen="user-listing" type="no-match"></redaction-empty-state> <redaction-empty-state *ngIf="!displayedEntities.length" screen="user-listing" type="no-match"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let user of displayedUsers"> <div class="table-item" *cdkVirtualFor="let user of displayedEntities">
<div class="pr-0" (click)="toggleUserSelected($event, user)"> <div class="pr-0" (click)="toggleEntitySelected($event, user)">
<div *ngIf="!isUserSelected(user)" class="select-oval"></div> <div *ngIf="!isEntitySelected(user)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isUserSelected(user)" svgIcon="red:radio-selected"></mat-icon> <mat-icon class="selection-icon active" *ngIf="isEntitySelected(user)" svgIcon="red:radio-selected"></mat-icon>
</div> </div>
<div> <div>
<redaction-initials-avatar [user]="user" [withName]="true" [showYou]="true"></redaction-initials-avatar> <redaction-initials-avatar [user]="user" [withName]="true" [showYou]="true"></redaction-initials-avatar>

View File

@ -1,53 +1,44 @@
import { Component, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { UserService } from '../../../../services/user.service'; import { UserService } from '../../../../services/user.service';
import { User, UserControllerService } from '@redaction/red-ui-http'; import { User, UserControllerService } from '@redaction/red-ui-http';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { AdminDialogService } from '../../services/admin-dialog.service'; import { AdminDialogService } from '../../services/admin-dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { TranslateChartService } from '../../../../services/translate-chart.service'; import { TranslateChartService } from '../../../../services/translate-chart.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({ @Component({
selector: 'redaction-user-listing-screen', selector: 'redaction-user-listing-screen',
templateUrl: './user-listing-screen.component.html', templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss'] styleUrls: ['./user-listing-screen.component.scss']
}) })
export class UserListingScreenComponent implements OnInit { export class UserListingScreenComponent extends BaseListingComponent<User> implements OnInit {
protected readonly _selectionKey = 'userId';
public viewReady = false; public viewReady = false;
public loading = false; public loading = false;
public collapsedDetails = false; public collapsedDetails = false;
public chartData: DoughnutChartConfig[] = []; public chartData: DoughnutChartConfig[] = [];
public users: User[];
public displayedUsers: User[] = [];
public searchForm: FormGroup;
public selectedUsersIds: string[] = [];
constructor( constructor(
public readonly permissionsService: PermissionsService, public readonly permissionsService: PermissionsService,
public readonly userService: UserService, public readonly userService: UserService,
private readonly _formBuilder: FormBuilder,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _adminDialogService: AdminDialogService, private readonly _adminDialogService: AdminDialogService,
private readonly _userControllerService: UserControllerService, private readonly _userControllerService: UserControllerService,
private readonly _translateChartService: TranslateChartService private readonly _translateChartService: TranslateChartService,
protected readonly _injector: Injector
) { ) {
this.searchForm = this._formBuilder.group({ super(_injector);
query: ['']
});
this.searchForm.valueChanges.subscribe(() => this._executeSearch());
} }
public async ngOnInit() { public async ngOnInit() {
await this._loadData(); await this._loadData();
} }
@debounce(200) protected _searchField(user: any): string {
private _executeSearch() { return this.userService.getName(user);
const value = this.searchForm.get('query').value;
this.displayedUsers = this.users.filter((user) => this.userService.getName(user).toLowerCase().includes(value.toLowerCase()));
} }
public openAddEditUserDialog($event: MouseEvent, user?: User) { public openAddEditUserDialog($event: MouseEvent, user?: User) {
@ -77,7 +68,7 @@ export class UserListingScreenComponent implements OnInit {
} }
private async _loadData() { private async _loadData() {
this.users = (await this._userControllerService.getAllUsers({ requestId: new Date().toISOString() }).toPromise()).users; this.allEntities = (await this._userControllerService.getAllUsers({ requestId: new Date().toISOString() }).toPromise()).users;
this._executeSearch(); this._executeSearch();
this._computeStats(); this._computeStats();
this.viewReady = true; this.viewReady = true;
@ -88,32 +79,32 @@ export class UserListingScreenComponent implements OnInit {
this.chartData = this._translateChartService.translateRoles( this.chartData = this._translateChartService.translateRoles(
[ [
{ {
value: this.users.filter((user) => !this.userService.isActive(user)).length, value: this.allEntities.filter((user) => !this.userService.isActive(user)).length,
color: 'INACTIVE', color: 'INACTIVE',
label: 'INACTIVE' label: 'INACTIVE'
}, },
{ {
value: this.users.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length, value: this.allEntities.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length,
color: 'REGULAR', color: 'REGULAR',
label: 'REGULAR' label: 'REGULAR'
}, },
{ {
value: this.users.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length, value: this.allEntities.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length,
color: 'MANAGER', color: 'MANAGER',
label: 'RED_MANAGER' label: 'RED_MANAGER'
}, },
{ {
value: this.users.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length, value: this.allEntities.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length,
color: 'MANAGER_ADMIN', color: 'MANAGER_ADMIN',
label: 'MANAGER_ADMIN' label: 'MANAGER_ADMIN'
}, },
{ {
value: this.users.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length, value: this.allEntities.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length,
color: 'USER_ADMIN', color: 'USER_ADMIN',
label: 'RED_USER_ADMIN' label: 'RED_USER_ADMIN'
}, },
{ {
value: this.users.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length, value: this.allEntities.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length,
color: 'ADMIN', color: 'ADMIN',
label: 'RED_ADMIN' label: 'RED_ADMIN'
} }
@ -136,41 +127,11 @@ export class UserListingScreenComponent implements OnInit {
this.collapsedDetails = !this.collapsedDetails; this.collapsedDetails = !this.collapsedDetails;
} }
toggleUserSelected($event: MouseEvent, user: User) {
$event.stopPropagation();
const idx = this.selectedUsersIds.indexOf(user.userId);
if (idx === -1) {
this.selectedUsersIds.push(user.userId);
} else {
this.selectedUsersIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeUsersSelected) {
this.selectedUsersIds = [];
} else {
this.selectedUsersIds = this.displayedUsers.map((user) => user.userId);
}
}
public get areAllUsersSelected() {
return this.displayedUsers.length !== 0 && this.selectedUsersIds.length === this.displayedUsers.length;
}
public get areSomeUsersSelected() {
return this.selectedUsersIds.length > 0;
}
public isUserSelected(user: User) {
return this.selectedUsersIds.indexOf(user.userId) !== -1;
}
public async bulkDelete() { public async bulkDelete() {
this.openDeleteUserDialog(this.users.filter((u) => this.isUserSelected(u))); this.openDeleteUserDialog(this.allEntities.filter((u) => this.isEntitySelected(u)));
} }
public get canDeleteSelected(): boolean { public get canDeleteSelected(): boolean {
return this.selectedUsersIds.indexOf(this.userService.userId) === -1; return this.selectedEntitiesIds.indexOf(this.userService.userId) === -1;
} }
} }

View File

@ -48,13 +48,13 @@
<div class="content-container"> <div class="content-container">
<div class="header-item"> <div class="header-item">
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'project-listing.table-header.title' | translate: { length: displayedProjects.length || 0 } }} {{ 'project-listing.table-header.title' | translate: { length: displayedEntities.length || 0 } }}
</span> </span>
</div> </div>
<div class="table-header" redactionSyncWidth="table-item"> <div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name <redaction-table-col-name
(toggleSort)="sortingService.toggleSort('project-listing', $event)" (toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption" [activeSortingOption]="sortingOption"
[withSort]="true" [withSort]="true"
column="project.projectName" column="project.projectName"
@ -70,22 +70,18 @@
</div> </div>
<redaction-empty-state <redaction-empty-state
*ngIf="!appStateService.hasProjects" *ngIf="!allEntities.length"
icon="red:folder" icon="red:folder"
screen="project-listing" screen="project-listing"
(action)="openAddProjectDialog()" (action)="openAddProjectDialog()"
[showButton]="permissionsService.isManager()" [showButton]="permissionsService.isManager()"
></redaction-empty-state> ></redaction-empty-state>
<redaction-empty-state <redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="project-listing" type="no-match"></redaction-empty-state>
*ngIf="appStateService.hasProjects && !displayedProjects.length"
screen="project-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="85" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="85" redactionHasScrollbar>
<div <div
*cdkVirtualFor="let pw of displayedProjects | sortBy: sortingOption.order:sortingOption.column" *cdkVirtualFor="let pw of displayedEntities | sortBy: sortingOption.order:sortingOption.column"
[class.pointer]="canOpenProject(pw)" [class.pointer]="canOpenProject(pw)"
[routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]" [routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]"
class="table-item" class="table-item"
@ -139,7 +135,7 @@
<div class="right-container" redactionHasScrollbar> <div class="right-container" redactionHasScrollbar>
<redaction-project-listing-details <redaction-project-listing-details
*ngIf="appStateService.hasProjects" *ngIf="allEntities.length"
(filtersChanged)="filtersChanged($event)" (filtersChanged)="filtersChanged($event)"
[documentsChartData]="documentsChartData" [documentsChartData]="documentsChartData"
[filters]="detailsContainerFilters" [filters]="detailsContainerFilters"
@ -149,8 +145,6 @@
</div> </div>
</section> </section>
<!--<redaction-project-listing-empty (addProjectRequest)="openAddProjectDialog()" *ngIf="!appStateService.hasProjects"></redaction-project-listing-empty>-->
<ng-template #needsWorkTemplate let-filter="filter"> <ng-template #needsWorkTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter> <redaction-type-filter [filter]="filter"></redaction-type-filter>
</ng-template> </ng-template>

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FileManagementControllerService, Project, RuleSetModel } from '@redaction/red-ui-http'; import { FileManagementControllerService, Project, RuleSetModel } from '@redaction/red-ui-http';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { UserService } from '../../../../services/user.service'; import { UserService } from '../../../../services/user.service';
@ -7,14 +7,12 @@ import { groupBy } from '../../../../utils/functions';
import { FilterModel } from '../../../shared/components/filter/model/filter.model'; import { FilterModel } from '../../../shared/components/filter/model/filter.model';
import { import {
annotationFilterChecker, annotationFilterChecker,
getFilteredEntities,
processFilters, processFilters,
projectMemberChecker, projectMemberChecker,
projectStatusChecker, projectStatusChecker,
ruleSetChecker ruleSetChecker
} from '../../../shared/components/filter/utils/filter-utils'; } from '../../../shared/components/filter/utils/filter-utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { ProjectWrapper } from '../../../../state/model/project.wrapper'; import { ProjectWrapper } from '../../../../state/model/project.wrapper';
import { Subscription, timer } from 'rxjs'; import { Subscription, timer } from 'rxjs';
@ -23,21 +21,22 @@ import { TranslateChartService } from '../../../../services/translate-chart.serv
import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter'; import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '../../../../utils/sorters/status-sorter'; import { StatusSorter } from '../../../../utils/sorters/status-sorter';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { FilterComponent } from '../../../shared/components/filter/filter.component'; import { FilterComponent } from '../../../shared/components/filter/filter.component';
import { ProjectsDialogService } from '../../services/projects-dialog.service'; import { ProjectsDialogService } from '../../services/projects-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({ @Component({
selector: 'redaction-project-listing-screen', selector: 'redaction-project-listing-screen',
templateUrl: './project-listing-screen.component.html', templateUrl: './project-listing-screen.component.html',
styleUrls: ['./project-listing-screen.component.scss'] styleUrls: ['./project-listing-screen.component.scss']
}) })
export class ProjectListingScreenComponent implements OnInit, OnDestroy { export class ProjectListingScreenComponent extends BaseListingComponent<ProjectWrapper> implements OnInit, OnDestroy {
protected readonly _searchKey = 'name';
protected readonly _sortKey = 'project-listing';
public projectsChartData: DoughnutChartConfig[] = []; public projectsChartData: DoughnutChartConfig[] = [];
public documentsChartData: DoughnutChartConfig[] = []; public documentsChartData: DoughnutChartConfig[] = [];
public searchForm: FormGroup;
public actionMenuOpen: boolean; public actionMenuOpen: boolean;
public statusFilters: FilterModel[]; public statusFilters: FilterModel[];
@ -51,7 +50,6 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
statusFilters: [] statusFilters: []
}; };
public displayedProjects: ProjectWrapper[] = [];
private projectAutoUpdateTimer: Subscription; private projectAutoUpdateTimer: Subscription;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent; @ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ -60,36 +58,32 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
@ViewChild('ruleSetFilter') private _ruleSetFilterComponent: FilterComponent; @ViewChild('ruleSetFilter') private _ruleSetFilterComponent: FilterComponent;
constructor( constructor(
public readonly appStateService: AppStateService, private readonly _appStateService: AppStateService,
public readonly userService: UserService, public readonly userService: UserService,
public readonly permissionsService: PermissionsService, public readonly permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dialogService: ProjectsDialogService, private readonly _dialogService: ProjectsDialogService,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _router: Router, private readonly _router: Router,
public readonly sortingService: SortingService,
public readonly translateChartService: TranslateChartService, public readonly translateChartService: TranslateChartService,
private readonly _formBuilder: FormBuilder, private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _fileManagementControllerService: FileManagementControllerService protected readonly _injector: Injector
) { ) {
this.appStateService.reset(); super(_injector);
this.searchForm = this._formBuilder.group({ this._appStateService.reset();
query: [''] this._loadEntitiesFromState();
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
} }
public ngOnInit(): void { public ngOnInit(): void {
this.projectAutoUpdateTimer = timer(0, 10000) this.projectAutoUpdateTimer = timer(0, 10000)
.pipe( .pipe(
tap(async () => { tap(async () => {
await this.appStateService.loadAllProjects(); await this._appStateService.loadAllProjects();
this._loadEntitiesFromState();
}) })
) )
.subscribe(); .subscribe();
this._calculateData(); this._calculateData();
this.appStateService.fileChanged.subscribe(() => { this._appStateService.fileChanged.subscribe(() => {
this._calculateData(); this._calculateData();
}); });
} }
@ -98,37 +92,26 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
this.projectAutoUpdateTimer.unsubscribe(); this.projectAutoUpdateTimer.unsubscribe();
} }
public get hasActiveFilters() { private _loadEntitiesFromState() {
return ( this.allEntities = this._appStateService.allProjects;
this._statusFilterComponent?.hasActiveFilters ||
this._peopleFilterComponent?.hasActiveFilters ||
this._needsWorkFilterComponent?.hasActiveFilters ||
this._ruleSetFilterComponent?.hasActiveFilters ||
this.searchForm.get('query').value
);
} }
public get noData() { public get noData() {
return this.appStateService.allProjects?.length === 0; return this.allEntities.length === 0;
} }
public resetFilters() { protected get filterComponents(): FilterComponent[] {
this._statusFilterComponent.deactivateAllFilters(); return [this._statusFilterComponent, this._peopleFilterComponent, this._needsWorkFilterComponent, this._ruleSetFilterComponent];
this._peopleFilterComponent.deactivateAllFilters();
this._needsWorkFilterComponent.deactivateAllFilters();
this._ruleSetFilterComponent.deactivateAllFilters();
this.filtersChanged();
this.searchForm.reset({ query: '' });
} }
private _calculateData() { private _calculateData() {
this._computeAllFilters(); this._computeAllFilters();
this._filterProjects(); this._filterEntities();
this.projectsChartData = [ this.projectsChartData = [
{ value: this.activeProjects, color: 'ACTIVE', label: 'active' }, { value: this.activeProjectsCount, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveProjects, color: 'DELETED', label: 'archived' } { value: this.inactiveProjectsCount, color: 'DELETED', label: 'archived' }
]; ];
const groups = groupBy(this.appStateService.aggregatedFiles, 'status'); const groups = groupBy(this._appStateService.aggregatedFiles, 'status');
this.documentsChartData = []; this.documentsChartData = [];
for (const key of Object.keys(groups)) { for (const key of Object.keys(groups)) {
this.documentsChartData.push({ this.documentsChartData.push({
@ -146,16 +129,12 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
return this.userService.user; return this.userService.user;
} }
public get sortingOption(): SortingOption { public get activeProjectsCount() {
return this.sortingService.getSortingOption('project-listing'); return this.allEntities.filter((p) => p.project.status === Project.StatusEnum.ACTIVE).length;
} }
public get activeProjects() { public get inactiveProjectsCount() {
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0); return this.allEntities.length - this.activeProjectsCount;
}
public get inactiveProjects() {
return this.appStateService.allProjects.length - this.activeProjects;
} }
public documentCount(project: ProjectWrapper) { public documentCount(project: ProjectWrapper) {
@ -171,7 +150,7 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
} }
public getRuleSet(pw: ProjectWrapper): RuleSetModel { public getRuleSet(pw: ProjectWrapper): RuleSetModel {
return this.appStateService.getRuleSetById(pw.project.ruleSetId); return this._appStateService.getRuleSetById(pw.project.ruleSetId);
} }
public openAddProjectDialog(): void { public openAddProjectDialog(): void {
@ -193,7 +172,7 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
const allDistinctPeople = new Set<string>(); const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>(); const allDistinctNeedsWork = new Set<string>();
const allDistinctRuleSets = new Set<string>(); const allDistinctRuleSets = new Set<string>();
this.appStateService.allProjects.forEach((entry) => { this.allEntities.forEach((entry) => {
// all people // all people
entry.project.memberIds.forEach((memberId) => allDistinctPeople.add(memberId)); entry.project.memberIds.forEach((memberId) => allDistinctPeople.add(memberId));
// file statuses // file statuses
@ -248,25 +227,14 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
allDistinctRuleSets.forEach((ruleSetId) => { allDistinctRuleSets.forEach((ruleSetId) => {
ruleSetFilters.push({ ruleSetFilters.push({
key: ruleSetId, key: ruleSetId,
label: this.appStateService.getRuleSetById(ruleSetId).name label: this._appStateService.getRuleSetById(ruleSetId).name
}); });
}); });
this.ruleSetFilters = processFilters(this.ruleSetFilters, ruleSetFilters); this.ruleSetFilters = processFilters(this.ruleSetFilters, ruleSetFilters);
} }
filtersChanged(filters?: { [key: string]: FilterModel[] }): void { protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
if (filters) { return [
for (const key of Object.keys(filters)) {
for (let idx = 0; idx < this[key].length; ++idx) {
this[key][idx] = filters[key][idx];
}
}
}
this._filterProjects();
}
private get _filteredProjects(): ProjectWrapper[] {
const filters = [
{ values: this.statusFilters, checker: projectStatusChecker }, { values: this.statusFilters, checker: projectStatusChecker },
{ values: this.peopleFilters, checker: projectMemberChecker }, { values: this.peopleFilters, checker: projectMemberChecker },
{ {
@ -277,25 +245,15 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
}, },
{ values: this.ruleSetFilters, checker: ruleSetChecker } { values: this.ruleSetFilters, checker: ruleSetChecker }
]; ];
return getFilteredEntities(this.appStateService.allProjects, filters);
} }
private _filterProjects() { protected preFilter() {
this.detailsContainerFilters = { this.detailsContainerFilters = {
statusFilters: this.statusFilters.map((f) => ({ ...f })) statusFilters: this.statusFilters.map((f) => ({ ...f }))
}; };
this.displayedProjects = this._filteredProjects.filter((project) =>
project.name.toLowerCase().includes(this.searchForm.get('query').value.toLowerCase())
);
this._changeDetectorRef.detectChanges();
} }
@debounce(200) public actionPerformed(pw: ProjectWrapper) {
private _executeSearch(value: { query: string }) {
this.displayedProjects = this._filteredProjects.filter((project) => project.name.toLowerCase().includes(value.query.toLowerCase()));
}
actionPerformed(pw: ProjectWrapper) {
this._calculateData(); this._calculateData();
} }
} }

View File

@ -1,4 +1,4 @@
<section *ngIf="!!appStateService.activeProject"> <section *ngIf="!!activeProject">
<div class="page-header"> <div class="page-header">
<div class="filters"> <div class="filters">
<div translate="filters.filter-by"></div> <div translate="filters.filter-by"></div>
@ -53,8 +53,7 @@
icon="red:assign" icon="red:assign"
></redaction-circle-button> ></redaction-circle-button>
<redaction-file-download-btn [project]="appStateService.activeProject" tooltipPosition="below" [file]="appStateService.activeProject.files"> <redaction-file-download-btn [project]="activeProject" tooltipPosition="below" [file]="allEntities"> </redaction-file-download-btn>
</redaction-file-download-btn>
<redaction-circle-button <redaction-circle-button
*ngIf="permissionsService.displayReanalyseBtn()" *ngIf="permissionsService.displayReanalyseBtn()"
@ -88,13 +87,13 @@
<div class="select-all-container"> <div class="select-all-container">
<div <div
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
[class.active]="areAllFilesSelected" [class.active]="areAllEntitiesSelected"
class="select-oval always-visible" class="select-oval always-visible"
*ngIf="!areAllFilesSelected && !areSomeFilesSelected" *ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div> ></div>
<mat-icon *ngIf="areAllFilesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon> <mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon <mat-icon
*ngIf="areSomeFilesSelected && !areAllFilesSelected" *ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()" (click)="toggleSelectAll()"
class="selection-icon" class="selection-icon"
svgIcon="red:radio-indeterminate" svgIcon="red:radio-indeterminate"
@ -102,16 +101,16 @@
</div> </div>
<span class="all-caps-label"> <span class="all-caps-label">
{{ 'project-overview.table-header.title' | translate: { length: displayedFiles.length || 0 } }} {{ 'project-overview.table-header.title' | translate: { length: displayedEntities.length || 0 } }}
</span> </span>
<redaction-project-overview-bulk-actions <redaction-project-overview-bulk-actions
[selectedFileIds]="selectedFileIds" [selectedFileIds]="selectedEntitiesIds"
(reload)="bulkActionPerformed()" (reload)="bulkActionPerformed()"
></redaction-project-overview-bulk-actions> ></redaction-project-overview-bulk-actions>
</div> </div>
<div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!appStateService.activeProject?.hasFiles"> <div class="table-header" redactionSyncWidth="table-item" [class.no-data]="!allEntities.length">
<!-- Table column names--> <!-- Table column names-->
<div class="select-oval-placeholder"></div> <div class="select-oval-placeholder"></div>
@ -162,30 +161,26 @@
</div> </div>
<redaction-empty-state <redaction-empty-state
*ngIf="!appStateService.activeProject?.hasFiles" *ngIf="!allEntities.length"
icon="red:document" icon="red:document"
screen="project-overview" screen="project-overview"
(action)="fileInput.click()" (action)="fileInput.click()"
buttonIcon="red:upload" buttonIcon="red:upload"
></redaction-empty-state> ></redaction-empty-state>
<redaction-empty-state <redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="project-overview" type="no-match"></redaction-empty-state>
*ngIf="appStateService.activeProject?.hasFiles && !displayedFiles.length"
screen="project-overview"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div <div
*cdkVirtualFor="let fileStatus of displayedFiles | sortBy: sortingOption.order:sortingOption.column; trackBy: fileId" *cdkVirtualFor="let fileStatus of displayedEntities | sortBy: sortingOption.order:sortingOption.column; trackBy: fileId"
[class.pointer]="permissionsService.canOpenFile(fileStatus)" [class.pointer]="permissionsService.canOpenFile(fileStatus)"
[routerLink]="fileLink(fileStatus)" [routerLink]="fileLink(fileStatus)"
class="table-item" class="table-item"
[class.disabled]="fileStatus.isExcluded" [class.disabled]="fileStatus.isExcluded"
> >
<div class="pr-0" (click)="toggleFileSelected($event, fileStatus)"> <div class="pr-0" (click)="toggleEntitySelected($event, fileStatus)">
<div *ngIf="!isFileSelected(fileStatus)" class="select-oval"></div> <div *ngIf="!isEntitySelected(fileStatus)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isFileSelected(fileStatus)" svgIcon="red:radio-selected"></mat-icon> <mat-icon class="selection-icon active" *ngIf="isEntitySelected(fileStatus)" svgIcon="red:radio-selected"></mat-icon>
</div> </div>
<div [title]="'[' + fileStatus.status + '] ' + fileStatus.filename"> <div [title]="'[' + fileStatus.status + '] ' + fileStatus.filename">

View File

@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, HostListener, Injector, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NotificationService, NotificationType } from '../../../../services/notification.service'; import { NotificationService, NotificationType } from '../../../../services/notification.service';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
@ -12,8 +12,7 @@ import { FilterModel } from '../../../shared/components/filter/model/filter.mode
import * as moment from 'moment'; import * as moment from 'moment';
import { ProjectDetailsComponent } from '../../components/project-details/project-details.component'; import { ProjectDetailsComponent } from '../../components/project-details/project-details.component';
import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper'; import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
import { annotationFilterChecker, getFilteredEntities, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils'; import { annotationFilterChecker, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service'; import { PermissionsService } from '../../../../services/permissions.service';
import { UserService } from '../../../../services/user.service'; import { UserService } from '../../../../services/user.service';
import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http'; import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http';
@ -21,27 +20,29 @@ import { Subscription, timer } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter'; import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '../../../../utils/sorters/status-sorter'; import { StatusSorter } from '../../../../utils/sorters/status-sorter';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { convertFiles, handleFileDrop } from '../../../../utils/file-drop-utils'; import { convertFiles, handleFileDrop } from '../../../../utils/file-drop-utils';
import { FilterComponent } from '../../../shared/components/filter/filter.component'; import { FilterComponent } from '../../../shared/components/filter/filter.component';
import { ProjectsDialogService } from '../../services/projects-dialog.service'; import { ProjectsDialogService } from '../../services/projects-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { ProjectWrapper } from '../../../../state/model/project.wrapper';
@Component({ @Component({
selector: 'redaction-project-overview-screen', selector: 'redaction-project-overview-screen',
templateUrl: './project-overview-screen.component.html', templateUrl: './project-overview-screen.component.html',
styleUrls: ['./project-overview-screen.component.scss'] styleUrls: ['./project-overview-screen.component.scss']
}) })
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy { export class ProjectOverviewScreenComponent extends BaseListingComponent<FileStatusWrapper> implements OnInit, OnDestroy {
public selectedFileIds: string[] = []; protected readonly _searchKey = 'filename';
protected readonly _selectionKey = 'fileId';
protected readonly _sortKey = 'project-overview';
public statusFilters: FilterModel[]; public statusFilters: FilterModel[];
public peopleFilters: FilterModel[]; public peopleFilters: FilterModel[];
public needsWorkFilters: FilterModel[]; public needsWorkFilters: FilterModel[];
public collapsedDetails = false; public collapsedDetails = false;
public searchForm: FormGroup; public searchForm: FormGroup;
displayedFiles: FileStatusWrapper[] = [];
detailsContainerFilters: { detailsContainerFilters: {
needsWorkFilters: FilterModel[]; needsWorkFilters: FilterModel[];
statusFilters: FilterModel[]; statusFilters: FilterModel[];
@ -56,9 +57,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent; @ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent;
constructor( constructor(
public readonly appStateService: AppStateService, private readonly _appStateService: AppStateService,
public readonly userService: UserService, public readonly userService: UserService,
private readonly _sortingService: SortingService,
public readonly permissionsService: PermissionsService, public readonly permissionsService: PermissionsService,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
private readonly _notificationService: NotificationService, private readonly _notificationService: NotificationService,
@ -67,23 +67,19 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _fileUploadService: FileUploadService, private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService, private readonly _statusOverlayService: StatusOverlayService,
private readonly _router: Router, private readonly _router: Router,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _translateService: TranslateService, private readonly _translateService: TranslateService,
private readonly _fileDropOverlayService: FileDropOverlayService, private readonly _fileDropOverlayService: FileDropOverlayService,
private readonly _formBuilder: FormBuilder, private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _fileManagementControllerService: FileManagementControllerService protected readonly _injector: Injector
) { ) {
this.searchForm = this._formBuilder.group({ super(_injector);
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
this._activatedRoute.params.subscribe((params) => { this._activatedRoute.params.subscribe((params) => {
this.appStateService.activateProject(params.projectId); this._appStateService.activateProject(params.projectId);
this._loadEntitiesFromState();
}); });
this.appStateService.fileChanged.subscribe(() => { this._appStateService.fileChanged.subscribe(() => {
this.calculateData(); this.calculateData();
}); });
} }
@ -92,7 +88,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.filesAutoUpdateTimer = timer(0, 7500) this.filesAutoUpdateTimer = timer(0, 7500)
.pipe( .pipe(
tap(async () => { tap(async () => {
await this.appStateService.reloadActiveProjectFilesIfNecessary(); await this._appStateService.reloadActiveProjectFilesIfNecessary();
this._loadEntitiesFromState();
}) })
) )
.subscribe(); .subscribe();
@ -105,14 +102,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.filesAutoUpdateTimer.unsubscribe(); this.filesAutoUpdateTimer.unsubscribe();
} }
@debounce(200) public get activeProject(): ProjectWrapper {
private _executeSearch(value: { query: string }) { return this._appStateService.activeProject;
this.displayedFiles = this._filteredFiles.filter((file) => file.filename.toLowerCase().includes(value.query.toLowerCase()));
this.selectedFileIds = this.displayedFiles.map((d) => d.fileId).filter((x) => this.selectedFileIds.includes(x));
} }
public reanalyseProject() { public reanalyseProject() {
return this.appStateService return this._appStateService
.reanalyzeProject() .reanalyzeProject()
.then(() => { .then(() => {
this.reloadProjects(); this.reloadProjects();
@ -143,84 +138,38 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.FULLREPROCESS, FileStatus.StatusEnum.PROCESSING].includes(fileStatusWrapper.status); return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.FULLREPROCESS, FileStatus.StatusEnum.PROCESSING].includes(fileStatusWrapper.status);
} }
public get sortingOption(): SortingOption { protected get filterComponents(): FilterComponent[] {
return this._sortingService.getSortingOption('project-overview'); return [this._statusFilterComponent, this._peopleFilterComponent, this._needsWorkFilterComponent];
} }
public toggleSort($event) { private _loadEntitiesFromState() {
this._sortingService.toggleSort('project-overview', $event); this.allEntities = this._appStateService.activeProject.files;
}
public get hasActiveFilters() {
return (
this._statusFilterComponent?.hasActiveFilters ||
this._peopleFilterComponent?.hasActiveFilters ||
this._needsWorkFilterComponent?.hasActiveFilters ||
this.searchForm.get('query').value
);
}
public resetFilters() {
this._statusFilterComponent.deactivateAllFilters();
this._peopleFilterComponent.deactivateAllFilters();
this._needsWorkFilterComponent.deactivateAllFilters();
this.filtersChanged();
this.searchForm.reset({ query: '' });
} }
reloadProjects() { reloadProjects() {
this.appStateService.getFiles(this.appStateService.activeProject, false).then(() => { this._appStateService.getFiles(this._appStateService.activeProject, false).then(() => {
this.calculateData(); this.calculateData();
}); });
} }
calculateData(): void { calculateData(): void {
if (!this.appStateService.activeProjectId) { if (!this._appStateService.activeProjectId) {
return; return;
} }
this._loadEntitiesFromState();
this._computeAllFilters(); this._computeAllFilters();
this._filterFiles(); this._filterEntities();
this._projectDetailsComponent?.calculateChartConfig(); this._projectDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges(); this._changeDetectorRef.detectChanges();
} }
toggleFileSelected($event: MouseEvent, file: FileStatusWrapper) {
$event.stopPropagation();
const idx = this.selectedFileIds.indexOf(file.fileId);
if (idx === -1) {
this.selectedFileIds.push(file.fileId);
} else {
this.selectedFileIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeFilesSelected) {
this.selectedFileIds = [];
} else {
this.selectedFileIds = this.displayedFiles.map((file) => file.fileId);
}
}
public get areAllFilesSelected() {
return this.displayedFiles.length !== 0 && this.selectedFileIds.length === this.displayedFiles.length;
}
public get areSomeFilesSelected() {
return this.selectedFileIds.length > 0;
}
public isFileSelected(file: FileStatusWrapper) {
return this.selectedFileIds.indexOf(file.fileId) !== -1;
}
public fileId(index, item) { public fileId(index, item) {
return item.fileId; return item.fileId;
} }
@HostListener('drop', ['$event']) @HostListener('drop', ['$event'])
onDrop(event: DragEvent) { onDrop(event: DragEvent) {
handleFileDrop(event, this.appStateService.activeProject, this._uploadFiles.bind(this)); handleFileDrop(event, this.activeProject, this._uploadFiles.bind(this));
} }
@HostListener('dragover', ['$event']) @HostListener('dragover', ['$event'])
@ -230,7 +179,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
} }
async uploadFiles(files: File[] | FileList) { async uploadFiles(files: File[] | FileList) {
await this._uploadFiles(convertFiles(files, this.appStateService.activeProject)); await this._uploadFiles(convertFiles(files, this.activeProject));
} }
private async _uploadFiles(files: FileUploadModel[]) { private async _uploadFiles(files: FileUploadModel[]) {
@ -242,7 +191,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
} }
private _computeAllFilters() { private _computeAllFilters() {
if (!this.appStateService.activeProject) { if (!this.activeProject) {
return; return;
} }
@ -252,16 +201,16 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
const allDistinctNeedsWork = new Set<string>(); const allDistinctNeedsWork = new Set<string>();
// All people // All people
this.appStateService.activeProject.files.forEach((file) => allDistinctPeople.add(file.currentReviewer)); this.allEntities.forEach((file) => allDistinctPeople.add(file.currentReviewer));
// File statuses // File statuses
this.appStateService.activeProject.files.forEach((file) => allDistinctFileStatusWrapper.add(file.status)); this.allEntities.forEach((file) => allDistinctFileStatusWrapper.add(file.status));
// Added dates // Added dates
this.appStateService.activeProject.files.forEach((file) => allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'))); this.allEntities.forEach((file) => allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY')));
// Needs work // Needs work
this.appStateService.activeProject.files.forEach((file) => { this.allEntities.forEach((file) => {
if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis'); if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis');
if (file.hintsOnly) allDistinctNeedsWork.add('hint'); if (file.hintsOnly) allDistinctNeedsWork.add('hint');
if (file.hasRedactions) allDistinctNeedsWork.add('redaction'); if (file.hasRedactions) allDistinctNeedsWork.add('redaction');
@ -310,25 +259,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters); this.needsWorkFilters = processFilters(this.needsWorkFilters, needsWorkFilters);
} }
filtersChanged(filters?: { [key: string]: FilterModel[] }): void {
if (filters) {
for (const key of Object.keys(filters)) {
for (let idx = 0; idx < this[key].length; ++idx) {
this[key][idx] = filters[key][idx];
}
}
}
this._filterFiles();
}
fileLink(fileStatus: FileStatusWrapper) { fileLink(fileStatus: FileStatusWrapper) {
return this.permissionsService.canOpenFile(fileStatus) return this.permissionsService.canOpenFile(fileStatus) ? ['/ui/projects/' + this.activeProject.project.projectId + '/file/' + fileStatus.fileId] : [];
? ['/ui/projects/' + this.appStateService.activeProject.project.projectId + '/file/' + fileStatus.fileId]
: [];
} }
private get _filteredFiles(): FileStatusWrapper[] { protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
const filters = [ return [
{ values: this.statusFilters, checker: keyChecker('status') }, { values: this.statusFilters, checker: keyChecker('status') },
{ values: this.peopleFilters, checker: keyChecker('currentReviewer') }, { values: this.peopleFilters, checker: keyChecker('currentReviewer') },
{ {
@ -338,36 +274,32 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
checkerArgs: this.permissionsService checkerArgs: this.permissionsService
} }
]; ];
return getFilteredEntities(this.appStateService.activeProject.files, filters);
} }
private _filterFiles() { protected preFilter() {
this.displayedFiles = this._filteredFiles.filter((file) => file.filename.toLowerCase().includes(this.searchForm.get('query').value.toLowerCase()));
this.selectedFileIds = this.displayedFiles.map((d) => d.fileId).filter((x) => this.selectedFileIds.includes(x));
this.detailsContainerFilters = { this.detailsContainerFilters = {
needsWorkFilters: this.needsWorkFilters.map((f) => ({ ...f })), needsWorkFilters: this.needsWorkFilters.map((f) => ({ ...f })),
statusFilters: this.statusFilters.map((f) => ({ ...f })) statusFilters: this.statusFilters.map((f) => ({ ...f }))
}; };
this._changeDetectorRef.detectChanges();
} }
bulkActionPerformed() { bulkActionPerformed() {
this.selectedFileIds = []; this.selectedEntitiesIds = [];
this.reloadProjects(); this.reloadProjects();
} }
public openEditProjectDialog($event: MouseEvent) { public openEditProjectDialog($event: MouseEvent) {
this._dialogService.openEditProjectDialog($event, this.appStateService.activeProject); this._dialogService.openEditProjectDialog($event, this.activeProject);
} }
public openDeleteProjectDialog($event: MouseEvent) { public openDeleteProjectDialog($event: MouseEvent) {
this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject, () => { this._dialogService.openDeleteProjectDialog($event, this.activeProject, () => {
this._router.navigate(['/ui/projects']); this._router.navigate(['/ui/projects']);
}); });
} }
public openAssignProjectMembersDialog(): void { public openAssignProjectMembersDialog(): void {
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.appStateService.activeProject, () => { this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => {
this.reloadProjects(); this.reloadProjects();
}); });
} }

View File

@ -0,0 +1,173 @@
import { ChangeDetectorRef, Component, Injector } from '@angular/core';
import { FilterModel } from '../components/filter/model/filter.model';
import { getFilteredEntities } from '../components/filter/utils/filter-utils';
import { FilterComponent } from '../components/filter/filter.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../utils/debounce';
import { ScreenName, SortingOption, SortingService } from '../../../services/sorting.service';
// Functionalities: Filter, search, select, sort
// Usage: overwrite necessary methods/members in your component
@Component({ template: '' })
export class BaseListingComponent<T = any> {
public allEntities: T[] = [];
public filteredEntities: T[] = [];
public displayedEntities: T[] = [];
public selectedEntitiesIds: string[] = [];
public searchForm: FormGroup;
protected readonly _formBuilder: FormBuilder;
protected readonly _changeDetectorRef: ChangeDetectorRef;
protected readonly _sortingService: SortingService;
// ----
// Overwrite in child class:
protected readonly _searchKey: string;
protected readonly _selectionKey: string;
protected readonly _sortKey: ScreenName;
protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
return [];
}
protected preFilter() {
return;
}
protected get filterComponents(): FilterComponent[] {
return [];
}
protected _searchField(entity: T): string {
return entity[this.searchKey];
}
// ----
constructor(protected readonly _injector: Injector) {
this._formBuilder = this._injector.get<FormBuilder>(FormBuilder);
this._changeDetectorRef = this._injector.get<ChangeDetectorRef>(ChangeDetectorRef);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._initSearch();
}
private get searchKey(): string {
if (!this._searchKey) {
throw new Error('Not implemented.');
}
return this._searchKey;
}
private get selectionKey(): string {
if (!this._selectionKey) {
throw new Error('Not implemented.');
}
return this._selectionKey;
}
private get sortKey(): ScreenName {
if (!this._sortKey) {
throw new Error('Not implemented.');
}
return this._sortKey;
}
// Search
private _initSearch() {
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe(() => this._executeSearch());
}
@debounce(200)
protected _executeSearch() {
this.displayedEntities = (this.filters.length ? this.filteredEntities : this.allEntities).filter((entity) =>
this._searchField(entity).toLowerCase().includes(this.searchForm.get('query').value.toLowerCase())
);
this._updateSelection();
}
protected _updateSelection() {
if (this._selectionKey) {
this.selectedEntitiesIds = this.displayedEntities.map((entity) => entity[this.selectionKey]).filter((id) => this.selectedEntitiesIds.includes(id));
}
}
// Filter
public filtersChanged(filters?: { [key: string]: FilterModel[] }): void {
if (filters) {
for (const key of Object.keys(filters)) {
for (let idx = 0; idx < this[key].length; ++idx) {
this[key][idx] = filters[key][idx];
}
}
}
this._filterEntities();
}
protected _filterEntities() {
this.preFilter();
this.filteredEntities = getFilteredEntities(this.allEntities, this.filters);
this._executeSearch();
this._changeDetectorRef.detectChanges();
}
public resetFilters() {
for (const filterComponent of this.filterComponents) {
filterComponent.deactivateAllFilters();
}
this.filtersChanged();
this.searchForm.reset({ query: '' });
}
public get hasActiveFilters() {
return this.filterComponents.reduce((prev, component) => prev || component?.hasActiveFilters, false) || this.searchForm.get('query').value;
}
// Selection
toggleEntitySelected($event: MouseEvent, entity: T) {
$event.stopPropagation();
const idx = this.selectedEntitiesIds.indexOf(entity[this.selectionKey]);
if (idx === -1) {
this.selectedEntitiesIds.push(entity[this.selectionKey]);
} else {
this.selectedEntitiesIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeEntitiesSelected) {
this.selectedEntitiesIds = [];
} else {
this.selectedEntitiesIds = this.displayedEntities.map((entity) => entity[this.selectionKey]);
}
}
public get areAllEntitiesSelected() {
return this.displayedEntities.length !== 0 && this.selectedEntitiesIds.length === this.displayedEntities.length;
}
public get areSomeEntitiesSelected() {
return this.selectedEntitiesIds.length > 0;
}
public isEntitySelected(entity: T) {
return this.selectedEntitiesIds.indexOf(entity[this.selectionKey]) !== -1;
}
// Sort
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption(this.sortKey);
}
public toggleSort($event) {
this._sortingService.toggleSort(this.sortKey, $event);
}
}

View File

@ -27,6 +27,7 @@ import { HiddenActionComponent } from './components/hidden-action/hidden-action.
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { FilterComponent } from './components/filter/filter.component'; import { FilterComponent } from './components/filter/filter.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component'; import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { BaseListingComponent } from './base/base-listing.component';
const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent]; const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent];
@ -44,6 +45,7 @@ const components = [
FilterComponent, FilterComponent,
ConfirmationDialogComponent, ConfirmationDialogComponent,
EmptyStateComponent, EmptyStateComponent,
BaseListingComponent,
...buttons ...buttons
]; ];

View File

@ -5,7 +5,7 @@ export class SortingOption {
column: string; column: string;
} }
type Screen = 'project-listing' | 'project-overview' | 'dictionary-listing' | 'rule-sets-listing' | 'default-colors' | 'file-attributes-listing'; export type ScreenName = 'project-listing' | 'project-overview' | 'dictionary-listing' | 'rule-sets-listing' | 'default-colors' | 'file-attributes-listing';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -17,12 +17,12 @@ export class SortingService {
'dictionary-listing': { column: 'label', order: 'asc' }, 'dictionary-listing': { column: 'label', order: 'asc' },
'rule-sets-listing': { column: 'name', order: 'asc' }, 'rule-sets-listing': { column: 'name', order: 'asc' },
'default-colors': { column: 'key', order: 'asc' }, 'default-colors': { column: 'key', order: 'asc' },
'file-attributes-listing': { column: 'name', order: 'asc' } 'file-attributes-listing': { column: 'label', order: 'asc' }
}; };
constructor() {} constructor() {}
public toggleSort(screen: Screen, column: string) { public toggleSort(screen: ScreenName, column: string) {
if (this._options[screen].column === column) { if (this._options[screen].column === column) {
const currentOrder = this._options[screen].order; const currentOrder = this._options[screen].order;
this._options[screen].order = currentOrder === 'asc' ? 'desc' : 'asc'; this._options[screen].order = currentOrder === 'asc' ? 'desc' : 'asc';
@ -31,7 +31,7 @@ export class SortingService {
} }
} }
public getSortingOption(screen: Screen) { public getSortingOption(screen: ScreenName) {
return this._options[screen]; return this._options[screen];
} }
} }