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 { 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 { ActiveFieldsListingComponent } from './dialogs/file-attributes-csv-import-dialog/active-fields-listing/active-fields-listing.component';
const dialogs = [
AddEditRuleSetDialogComponent,
@ -41,7 +42,8 @@ const dialogs = [
EditColorDialogComponent,
SmtpAuthDialogComponent,
AddEditUserDialogComponent,
ConfirmDeleteUsersDialogComponent
ConfirmDeleteUsersDialogComponent,
FileAttributesCsvImportDialogComponent
];
const screens = [
@ -66,12 +68,14 @@ const components = [
ComboChartComponent,
ComboSeriesVerticalComponent,
UsersStatsComponent,
ActiveFieldsListingComponent,
...dialogs,
...screens
];
@NgModule({
declarations: [...components, FileAttributesCsvImportDialogComponent],
declarations: [...components],
providers: [AdminDialogService],
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-wrapper"
*ngFor="let field of filteredFields"
*ngFor="let field of displayedEntities"
(mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()"
(click)="toggleFieldActive(field)"
@ -128,197 +128,11 @@
</div>
</div>
<div class="content-container">
<div class="header-item">
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllFieldsSelected"
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>
<redaction-active-fields-listing
[(allEntities)]="activeFields"
(setHoveredColumn)="setHoveredColumn($event)"
(toggleFieldActive)="toggleFieldActive($event)"
></redaction-active-fields-listing>
</div>
</div>
</div>

View File

@ -114,6 +114,7 @@
> .left {
width: 375px;
min-width: 375px;
background: $grey-2;
.csv-header-pill-content {
@ -198,88 +199,5 @@
> .content-container {
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 { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { Component, Inject, Injector, ViewChild } from '@angular/core';
import { AbstractControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AppStateService } from '../../../../state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as Papa from 'papaparse';
import { FileAttributesControllerService } from '@redaction/red-ui-http';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { debounce } from '../../../../utils/debounce';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
enum FieldType {
Text = 'Text',
@ -15,7 +15,7 @@ enum FieldType {
Date = 'Date'
}
interface Field {
export interface Field {
csvColumn: string;
name: string;
type: FieldType;
@ -30,17 +30,16 @@ interface Field {
templateUrl: './file-attributes-csv-import-dialog.component.html',
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 ruleSetId: string;
public parseResult: { data: any[]; errors: any[]; meta: any; fields: Field[] };
public hoveredColumn: string;
public activeFields: Field[] = [];
public selectedFields: string[] = [];
public baseConfigForm: FormGroup;
public isSearchOpen = false;
public searchForm: FormGroup;
public filteredFields: Field[];
public previewExpanded = true;
public filteredKeyOptions: Observable<string[]>;
public keepPreview = false;
@ -50,18 +49,15 @@ export class FileAttributesCsvImportDialogComponent {
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesControllerService: FileAttributesControllerService,
public dialogRef: MatDialogRef<FileAttributesCsvImportDialogComponent>,
protected readonly _injector: Injector,
@Inject(MAT_DIALOG_DATA) public data: { csv: File; ruleSetId: string }
) {
super(_injector);
this.csvFile = data.csv;
this.ruleSetId = data.ruleSetId;
this.searchForm = this._formBuilder.group({
query: ['']
});
this.baseConfigForm = this._formBuilder.group({
filenameMappingColumnHeaderName: ['', [Validators.required, this._autocompleteStringValidator()]],
delimiter: [undefined, Validators.required],
@ -69,13 +65,6 @@ export class FileAttributesCsvImportDialogComponent {
});
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 {
@ -98,12 +87,14 @@ export class FileAttributesCsvImportDialogComponent {
if (!this.baseConfigForm.get('delimiter').value) {
this.baseConfigForm.patchValue({ delimiter: this.parseResult.meta.delimiter });
}
this.parseResult.fields = this.parseResult.meta.fields.map((field) => this._buildAttribute(field));
this.filteredFields = [...this.parseResult.fields];
this.allEntities = this.parseResult.meta.fields.map((field) => this._buildAttribute(field));
this.displayedEntities = [...this.allEntities];
this.filteredKeyOptions = this.baseConfigForm.get('filenameMappingColumnHeaderName').valueChanges.pipe(
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);
@ -137,9 +128,6 @@ export class FileAttributesCsvImportDialogComponent {
} else {
this.activeFields.splice(this.activeFields.indexOf(field), 1);
this.activeFields = [...this.activeFields];
if (this.isFieldSelected(field.csvColumn)) {
this.toggleFieldSelected(field.csvColumn);
}
}
}
@ -157,52 +145,11 @@ export class FileAttributesCsvImportDialogComponent {
}
public activateAll() {
this.activeFields = [...this.parseResult.fields];
this.activeFields = [...this.allEntities];
}
public deactivateAll() {
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() {

View File

@ -15,7 +15,7 @@
<div class="content-container">
<div class="header-item">
<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>
</div>
@ -36,7 +36,7 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- 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 class="table-item-title heading" [translate]="'default-colors-screen.types.' + color.key"></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 { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-default-colors-screen',
templateUrl: './default-colors-screen.component.html',
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;
private _colorsObj: Colors;
public colors: { key: string; value: string }[] = [];
constructor(
private readonly _appStateService: AppStateService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _sortingService: SortingService,
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._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> {
await this._appStateService.loadAllRuleSets();
}
@ -46,7 +40,7 @@ export class DefaultColorsScreenComponent {
.toPromise()
.then((data) => {
this._colorsObj = data;
this.colors = Object.keys(data).map((key) => ({
this.allEntities = Object.keys(data).map((key) => ({
key,
value: data[key]
}));

View File

@ -17,13 +17,13 @@
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllDictsSelected"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllDictsSelected && !areSomeDictsSelected"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></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
*ngIf="areSomeDictsSelected && !areAllDictsSelected"
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
@ -31,7 +31,7 @@
</div>
<span class="all-caps-label">
{{ 'dictionary-listing.table-header.title' | translate: { length: displayedDictionaries.length } }}
{{ 'dictionary-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span>
<div class="attributes-actions-container">
@ -48,7 +48,7 @@
</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>
<redaction-table-col-name
@ -74,28 +74,24 @@
</div>
<redaction-empty-state
*ngIf="!dictionaries.length"
*ngIf="!allEntities.length"
icon="red:dictionary"
(action)="openAddEditDictionaryDialog()"
[showButton]="permissionsService.isAdmin()"
screen="dictionary-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="dictionaries.length && !displayedDictionaries.length"
screen="dictionary-listing"
type="no-match"
></redaction-empty-state>
<redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="dictionary-listing" type="no-match"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div
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]"
>
<div class="pr-0" (click)="toggleDictSelected($event, dict)">
<div *ngIf="!isDictSelected(dict)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isDictSelected(dict)" svgIcon="red:radio-selected"></mat-icon>
<div class="pr-0" (click)="toggleEntitySelected($event, dict)">
<div *ngIf="!isEntitySelected(dict)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(dict)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div>
@ -137,7 +133,7 @@
</redaction-circle-button>
<redaction-circle-button
(action)="openEditDictionaryDialog($event, dict)"
(action)="openAddEditDictionaryDialog($event, dict)"
*ngIf="permissionsService.isAdmin()"
tooltip="dictionary-listing.action.edit"
type="dark-bg"
@ -153,7 +149,7 @@
<div class="right-container" redactionHasScrollbar>
<redaction-simple-doughnut-chart
*ngIf="dictionaries.length"
*ngIf="allEntities.length"
[config]="chartData"
[strokeWidth]="15"
[radius]="82"
@ -164,3 +160,5 @@
</div>
</div>
</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 { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { AppStateService } from '../../../../state/app-state.service';
import { tap } from 'rxjs/operators';
import { defaultIfEmpty, tap } from 'rxjs/operators';
import { forkJoin } from 'rxjs';
import { PermissionsService } from '../../../../services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { ActivatedRoute } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-dictionary-listing-screen',
templateUrl: './dictionary-listing-screen.component.html',
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 dictionaries: TypeValue[];
public displayedDictionaries: TypeValue[];
public selectedDictKeys: string[] = [];
public searchForm: FormGroup;
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _activatedRoute: ActivatedRoute,
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.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
}
ngOnInit(): void {
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() {
const appStateDictionaryData = this._appStateService.dictionaryData[this._appStateService.activeRuleSetId];
this.dictionaries = Object.keys(appStateDictionaryData)
this.allEntities = Object.keys(appStateDictionaryData)
.map((key) => appStateDictionaryData[key])
.filter((d) => !d.virtual || d.type === 'false_positive');
this.displayedDictionaries = [...this.dictionaries];
const dataObs = [];
this.dictionaries.forEach((item) => {
const observable = this._dictionaryControllerService.getDictionaryForType(item.type, this._appStateService.activeRuleSetId).pipe(
this.displayedEntities = [...this.allEntities];
const dataObs = this.allEntities.map((dict) =>
this._dictionaryControllerService.getDictionaryForType(dict.type, this._appStateService.activeRuleSetId).pipe(
tap((values) => {
item.entries = values.entries ? values.entries : [];
dict.entries = values.entries ? values.entries : [];
})
);
dataObs.push(observable);
});
forkJoin(dataObs).subscribe(() => {
this._calculateData();
});
)
);
forkJoin(dataObs)
.pipe(defaultIfEmpty(null))
.subscribe(() => {
this._calculateData();
});
}
private _calculateData() {
this.chartData = [];
for (const dict of this.dictionaries) {
for (const dict of this.allEntities) {
this.chartData.push({
value: dict.entries ? dict.entries.length : 0,
color: dict.hexColor,
@ -82,47 +69,11 @@ export class DictionaryListingScreenComponent implements OnInit {
});
}
this.chartData.sort((a, b) => (a.label < b.label ? -1 : 1));
this.viewReady = true;
}
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption('dictionary-listing');
}
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) {
openAddEditDictionaryDialog($event?: MouseEvent, dict?: TypeValue) {
$event?.stopPropagation();
this._dialogService.openAddEditDictionaryDialog(dict, this._appStateService.activeRuleSetId, async (newDictionary) => {
if (newDictionary) {
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) {
this._dialogService.openDeleteDictionaryDialog($event, dict, this._appStateService.activeRuleSetId, async () => {
await this._appStateService.loadDictionaryData();

View File

@ -15,18 +15,13 @@
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllAttributesSelected"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllAttributesSelected && !areSomeAttributesSelected"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></div>
<mat-icon *ngIf="areAllEntitiesSelected" (click)="toggleSelectAll()" class="selection-icon active" svgIcon="red:radio-selected"></mat-icon>
<mat-icon
*ngIf="areAllAttributesSelected"
(click)="toggleSelectAll()"
class="selection-icon active"
svgIcon="red:radio-selected"
></mat-icon>
<mat-icon
*ngIf="areSomeAttributesSelected && !areAllAttributesSelected"
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
@ -34,11 +29,11 @@
</div>
<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>
<redaction-circle-button
*ngIf="areSomeAttributesSelected"
*ngIf="areSomeEntitiesSelected"
tooltip="file-attributes-listing.bulk-actions.delete"
type="dark-bg"
icon="red:trash"
@ -69,7 +64,7 @@
</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>
<redaction-table-col-name
@ -77,7 +72,7 @@
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="name"
column="label"
></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>
<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
*ngIf="attributes.length && !displayedAttributes.length"
*ngIf="allEntities.length && !displayedEntities.length"
screen="file-attributes-listing"
type="no-match"
></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let attribute of displayedAttributes | sortBy: sortingOption.order:sortingOption.column">
<div class="pr-0" (click)="toggleAttributeSelected($event, attribute)">
<div *ngIf="!isAttributeSelected(attribute)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isAttributeSelected(attribute)" svgIcon="red:radio-selected"></mat-icon>
<div class="table-item" *cdkVirtualFor="let attribute of displayedEntities | sortBy: sortingOption.order:sortingOption.column">
<div class="pr-0" (click)="toggleEntitySelected($event, attribute)">
<div *ngIf="!isEntitySelected(attribute)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(attribute)" svgIcon="red:radio-selected"></mat-icon>
</div>
<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 { FormBuilder, FormGroup } from '@angular/forms';
import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../../state/app-state.service';
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 { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-file-attributes-listing-screen',
templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss']
})
export class FileAttributesListingScreenComponent implements OnInit {
public searchForm: FormGroup;
public attributes: FileAttributeConfig[] = [];
public displayedAttributes: FileAttributeConfig[] = [];
public selectedFileAttributeIds: string[] = [];
export class FileAttributesListingScreenComponent extends BaseListingComponent<FileAttributeConfig> implements OnInit {
protected readonly _searchKey = 'label';
protected readonly _selectionKey = 'id';
protected readonly _sortKey = 'file-attributes-listing';
public viewReady = false;
public loading = false;
@ -25,20 +23,14 @@ export class FileAttributesListingScreenComponent implements OnInit {
constructor(
public readonly permissionsService: PermissionsService,
public readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesControllerService,
private readonly _appStateService: AppStateService,
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.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
}
async ngOnInit() {
@ -48,34 +40,15 @@ export class FileAttributesListingScreenComponent implements OnInit {
private async _loadData() {
try {
const response = await this._fileAttributesService.getFileAttributesConfiguration(this._appStateService.activeRuleSetId).toPromise();
this.attributes = response?.fileAttributeConfigs || [];
this.allEntities = response?.fileAttributeConfigs || [];
} catch (e) {
} finally {
// Remove potentially deleted items
this.selectedFileAttributeIds = this.selectedFileAttributeIds.filter((id) => !!this.attributes.find((attr) => attr.id === id));
this._executeSearch();
this.viewReady = true;
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) {
$event.stopPropagation();
this._dialogService.openAddEditFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async (newValue: FileAttributeConfig) => {
@ -92,42 +65,12 @@ export class FileAttributesListingScreenComponent implements OnInit {
if (!!fileAttribute) {
await this._fileAttributesService.deleteFileAttribute(this._appStateService.activeRuleSetId, fileAttribute.id).toPromise();
} else {
await this._fileAttributesService.deleteFileAttributes(this.selectedFileAttributeIds, this._appStateService.activeRuleSetId).toPromise();
await this._fileAttributesService.deleteFileAttributes(this.selectedEntitiesIds, this._appStateService.activeRuleSetId).toPromise();
}
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[]) {
const csvFile = files[0];
this._fileInput.nativeElement.value = null;

View File

@ -30,13 +30,13 @@
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllRuleSetsSelected"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllRuleSetsSelected && !areSomeRuleSetsSelected"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></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
*ngIf="areSomeRuleSetsSelected && !areAllRuleSetsSelected"
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
@ -44,11 +44,11 @@
</div>
<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>
</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>
<redaction-table-col-name
@ -76,10 +76,10 @@
<div class="scrollbar-placeholder"></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
*ngIf="ruleSets.length && !displayedRuleSets.length"
*ngIf="allEntities.length && !displayedEntities.length"
screen="project-templates-listing"
type="no-match"
></redaction-empty-state>
@ -87,12 +87,12 @@
<cdk-virtual-scroll-viewport [itemSize]="100" redactionHasScrollbar>
<div
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']"
>
<div class="pr-0" (click)="toggleTemplateSelected($event, ruleSet)">
<div *ngIf="!isRuleSetSelected(ruleSet)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isRuleSetSelected(ruleSet)" svgIcon="red:radio-selected"></mat-icon>
<div class="pr-0" (click)="toggleEntitySelected($event, ruleSet)">
<div *ngIf="!isEntitySelected(ruleSet)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(ruleSet)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div>

View File

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

View File

@ -28,23 +28,23 @@
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllUsersSelected"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllUsersSelected && !areSomeUsersSelected"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></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
*ngIf="areSomeUsersSelected && !areAllUsersSelected"
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
</div>
<span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }}
{{ 'user-listing.table-header.title' | translate: { length: displayedEntities.length } }}
</span>
<ng-container *ngIf="areSomeUsersSelected && !loading">
<ng-container *ngIf="areSomeEntitiesSelected && !loading">
<redaction-circle-button
(action)="bulkDelete()"
[disabled]="!canDeleteSelected"
@ -73,14 +73,14 @@
<div class="scrollbar-placeholder"></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>
<!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let user of displayedUsers">
<div class="pr-0" (click)="toggleUserSelected($event, user)">
<div *ngIf="!isUserSelected(user)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isUserSelected(user)" svgIcon="red:radio-selected"></mat-icon>
<div class="table-item" *cdkVirtualFor="let user of displayedEntities">
<div class="pr-0" (click)="toggleEntitySelected($event, user)">
<div *ngIf="!isEntitySelected(user)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(user)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div>
<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 { UserService } from '../../../../services/user.service';
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 { TranslateService } from '@ngx-translate/core';
import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { TranslateChartService } from '../../../../services/translate-chart.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-user-listing-screen',
templateUrl: './user-listing-screen.component.html',
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 loading = false;
public collapsedDetails = false;
public chartData: DoughnutChartConfig[] = [];
public users: User[];
public displayedUsers: User[] = [];
public searchForm: FormGroup;
public selectedUsersIds: string[] = [];
constructor(
public readonly permissionsService: PermissionsService,
public readonly userService: UserService,
private readonly _formBuilder: FormBuilder,
private readonly _translateService: TranslateService,
private readonly _adminDialogService: AdminDialogService,
private readonly _userControllerService: UserControllerService,
private readonly _translateChartService: TranslateChartService
private readonly _translateChartService: TranslateChartService,
protected readonly _injector: Injector
) {
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe(() => this._executeSearch());
super(_injector);
}
public async ngOnInit() {
await this._loadData();
}
@debounce(200)
private _executeSearch() {
const value = this.searchForm.get('query').value;
this.displayedUsers = this.users.filter((user) => this.userService.getName(user).toLowerCase().includes(value.toLowerCase()));
protected _searchField(user: any): string {
return this.userService.getName(user);
}
public openAddEditUserDialog($event: MouseEvent, user?: User) {
@ -77,7 +68,7 @@ export class UserListingScreenComponent implements OnInit {
}
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._computeStats();
this.viewReady = true;
@ -88,32 +79,32 @@ export class UserListingScreenComponent implements OnInit {
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',
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',
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',
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',
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',
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',
label: 'RED_ADMIN'
}
@ -136,41 +127,11 @@ export class UserListingScreenComponent implements OnInit {
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() {
this.openDeleteUserDialog(this.users.filter((u) => this.isUserSelected(u)));
this.openDeleteUserDialog(this.allEntities.filter((u) => this.isEntitySelected(u)));
}
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="header-item">
<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>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
(toggleSort)="sortingService.toggleSort('project-listing', $event)"
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="project.projectName"
@ -70,22 +70,18 @@
</div>
<redaction-empty-state
*ngIf="!appStateService.hasProjects"
*ngIf="!allEntities.length"
icon="red:folder"
screen="project-listing"
(action)="openAddProjectDialog()"
[showButton]="permissionsService.isManager()"
></redaction-empty-state>
<redaction-empty-state
*ngIf="appStateService.hasProjects && !displayedProjects.length"
screen="project-listing"
type="no-match"
></redaction-empty-state>
<redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="project-listing" type="no-match"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="85" redactionHasScrollbar>
<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)"
[routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]"
class="table-item"
@ -139,7 +135,7 @@
<div class="right-container" redactionHasScrollbar>
<redaction-project-listing-details
*ngIf="appStateService.hasProjects"
*ngIf="allEntities.length"
(filtersChanged)="filtersChanged($event)"
[documentsChartData]="documentsChartData"
[filters]="detailsContainerFilters"
@ -149,8 +145,6 @@
</div>
</section>
<!--<redaction-project-listing-empty (addProjectRequest)="openAddProjectDialog()" *ngIf="!appStateService.hasProjects"></redaction-project-listing-empty>-->
<ng-template #needsWorkTemplate let-filter="filter">
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</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 { AppStateService } from '../../../../state/app-state.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 {
annotationFilterChecker,
getFilteredEntities,
processFilters,
projectMemberChecker,
projectStatusChecker,
ruleSetChecker
} from '../../../shared/components/filter/utils/filter-utils';
import { TranslateService } from '@ngx-translate/core';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service';
import { ProjectWrapper } from '../../../../state/model/project.wrapper';
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 { StatusSorter } from '../../../../utils/sorters/status-sorter';
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 { ProjectsDialogService } from '../../services/projects-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
@Component({
selector: 'redaction-project-listing-screen',
templateUrl: './project-listing-screen.component.html',
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 documentsChartData: DoughnutChartConfig[] = [];
public searchForm: FormGroup;
public actionMenuOpen: boolean;
public statusFilters: FilterModel[];
@ -51,7 +50,6 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
statusFilters: []
};
public displayedProjects: ProjectWrapper[] = [];
private projectAutoUpdateTimer: Subscription;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ -60,36 +58,32 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
@ViewChild('ruleSetFilter') private _ruleSetFilterComponent: FilterComponent;
constructor(
public readonly appStateService: AppStateService,
private readonly _appStateService: AppStateService,
public readonly userService: UserService,
public readonly permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dialogService: ProjectsDialogService,
private readonly _translateService: TranslateService,
private readonly _router: Router,
public readonly sortingService: SortingService,
public readonly translateChartService: TranslateChartService,
private readonly _formBuilder: FormBuilder,
private readonly _fileManagementControllerService: FileManagementControllerService
private readonly _fileManagementControllerService: FileManagementControllerService,
protected readonly _injector: Injector
) {
this.appStateService.reset();
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
super(_injector);
this._appStateService.reset();
this._loadEntitiesFromState();
}
public ngOnInit(): void {
this.projectAutoUpdateTimer = timer(0, 10000)
.pipe(
tap(async () => {
await this.appStateService.loadAllProjects();
await this._appStateService.loadAllProjects();
this._loadEntitiesFromState();
})
)
.subscribe();
this._calculateData();
this.appStateService.fileChanged.subscribe(() => {
this._appStateService.fileChanged.subscribe(() => {
this._calculateData();
});
}
@ -98,37 +92,26 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
this.projectAutoUpdateTimer.unsubscribe();
}
public get hasActiveFilters() {
return (
this._statusFilterComponent?.hasActiveFilters ||
this._peopleFilterComponent?.hasActiveFilters ||
this._needsWorkFilterComponent?.hasActiveFilters ||
this._ruleSetFilterComponent?.hasActiveFilters ||
this.searchForm.get('query').value
);
private _loadEntitiesFromState() {
this.allEntities = this._appStateService.allProjects;
}
public get noData() {
return this.appStateService.allProjects?.length === 0;
return this.allEntities.length === 0;
}
public resetFilters() {
this._statusFilterComponent.deactivateAllFilters();
this._peopleFilterComponent.deactivateAllFilters();
this._needsWorkFilterComponent.deactivateAllFilters();
this._ruleSetFilterComponent.deactivateAllFilters();
this.filtersChanged();
this.searchForm.reset({ query: '' });
protected get filterComponents(): FilterComponent[] {
return [this._statusFilterComponent, this._peopleFilterComponent, this._needsWorkFilterComponent, this._ruleSetFilterComponent];
}
private _calculateData() {
this._computeAllFilters();
this._filterProjects();
this._filterEntities();
this.projectsChartData = [
{ value: this.activeProjects, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveProjects, color: 'DELETED', label: 'archived' }
{ value: this.activeProjectsCount, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveProjectsCount, color: 'DELETED', label: 'archived' }
];
const groups = groupBy(this.appStateService.aggregatedFiles, 'status');
const groups = groupBy(this._appStateService.aggregatedFiles, 'status');
this.documentsChartData = [];
for (const key of Object.keys(groups)) {
this.documentsChartData.push({
@ -146,16 +129,12 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
return this.userService.user;
}
public get sortingOption(): SortingOption {
return this.sortingService.getSortingOption('project-listing');
public get activeProjectsCount() {
return this.allEntities.filter((p) => p.project.status === Project.StatusEnum.ACTIVE).length;
}
public get activeProjects() {
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0);
}
public get inactiveProjects() {
return this.appStateService.allProjects.length - this.activeProjects;
public get inactiveProjectsCount() {
return this.allEntities.length - this.activeProjectsCount;
}
public documentCount(project: ProjectWrapper) {
@ -171,7 +150,7 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
}
public getRuleSet(pw: ProjectWrapper): RuleSetModel {
return this.appStateService.getRuleSetById(pw.project.ruleSetId);
return this._appStateService.getRuleSetById(pw.project.ruleSetId);
}
public openAddProjectDialog(): void {
@ -193,7 +172,7 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
const allDistinctPeople = new Set<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctRuleSets = new Set<string>();
this.appStateService.allProjects.forEach((entry) => {
this.allEntities.forEach((entry) => {
// all people
entry.project.memberIds.forEach((memberId) => allDistinctPeople.add(memberId));
// file statuses
@ -248,25 +227,14 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
allDistinctRuleSets.forEach((ruleSetId) => {
ruleSetFilters.push({
key: ruleSetId,
label: this.appStateService.getRuleSetById(ruleSetId).name
label: this._appStateService.getRuleSetById(ruleSetId).name
});
});
this.ruleSetFilters = processFilters(this.ruleSetFilters, ruleSetFilters);
}
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._filterProjects();
}
private get _filteredProjects(): ProjectWrapper[] {
const filters = [
protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
return [
{ values: this.statusFilters, checker: projectStatusChecker },
{ values: this.peopleFilters, checker: projectMemberChecker },
{
@ -277,25 +245,15 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
},
{ values: this.ruleSetFilters, checker: ruleSetChecker }
];
return getFilteredEntities(this.appStateService.allProjects, filters);
}
private _filterProjects() {
protected preFilter() {
this.detailsContainerFilters = {
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)
private _executeSearch(value: { query: string }) {
this.displayedProjects = this._filteredProjects.filter((project) => project.name.toLowerCase().includes(value.query.toLowerCase()));
}
actionPerformed(pw: ProjectWrapper) {
public actionPerformed(pw: ProjectWrapper) {
this._calculateData();
}
}

View File

@ -1,4 +1,4 @@
<section *ngIf="!!appStateService.activeProject">
<section *ngIf="!!activeProject">
<div class="page-header">
<div class="filters">
<div translate="filters.filter-by"></div>
@ -53,8 +53,7 @@
icon="red:assign"
></redaction-circle-button>
<redaction-file-download-btn [project]="appStateService.activeProject" tooltipPosition="below" [file]="appStateService.activeProject.files">
</redaction-file-download-btn>
<redaction-file-download-btn [project]="activeProject" tooltipPosition="below" [file]="allEntities"> </redaction-file-download-btn>
<redaction-circle-button
*ngIf="permissionsService.displayReanalyseBtn()"
@ -88,13 +87,13 @@
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllFilesSelected"
[class.active]="areAllEntitiesSelected"
class="select-oval always-visible"
*ngIf="!areAllFilesSelected && !areSomeFilesSelected"
*ngIf="!areAllEntitiesSelected && !areSomeEntitiesSelected"
></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
*ngIf="areSomeFilesSelected && !areAllFilesSelected"
*ngIf="areSomeEntitiesSelected && !areAllEntitiesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
@ -102,16 +101,16 @@
</div>
<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>
<redaction-project-overview-bulk-actions
[selectedFileIds]="selectedFileIds"
[selectedFileIds]="selectedEntitiesIds"
(reload)="bulkActionPerformed()"
></redaction-project-overview-bulk-actions>
</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-->
<div class="select-oval-placeholder"></div>
@ -162,30 +161,26 @@
</div>
<redaction-empty-state
*ngIf="!appStateService.activeProject?.hasFiles"
*ngIf="!allEntities.length"
icon="red:document"
screen="project-overview"
(action)="fileInput.click()"
buttonIcon="red:upload"
></redaction-empty-state>
<redaction-empty-state
*ngIf="appStateService.activeProject?.hasFiles && !displayedFiles.length"
screen="project-overview"
type="no-match"
></redaction-empty-state>
<redaction-empty-state *ngIf="allEntities.length && !displayedEntities.length" screen="project-overview" type="no-match"></redaction-empty-state>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<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)"
[routerLink]="fileLink(fileStatus)"
class="table-item"
[class.disabled]="fileStatus.isExcluded"
>
<div class="pr-0" (click)="toggleFileSelected($event, fileStatus)">
<div *ngIf="!isFileSelected(fileStatus)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isFileSelected(fileStatus)" svgIcon="red:radio-selected"></mat-icon>
<div class="pr-0" (click)="toggleEntitySelected($event, fileStatus)">
<div *ngIf="!isEntitySelected(fileStatus)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isEntitySelected(fileStatus)" svgIcon="red:radio-selected"></mat-icon>
</div>
<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 { NotificationService, NotificationType } from '../../../../services/notification.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 { ProjectDetailsComponent } from '../../components/project-details/project-details.component';
import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
import { annotationFilterChecker, getFilteredEntities, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { annotationFilterChecker, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils';
import { PermissionsService } from '../../../../services/permissions.service';
import { UserService } from '../../../../services/user.service';
import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http';
@ -21,27 +20,29 @@ import { Subscription, timer } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RedactionFilterSorter } from '../../../../utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '../../../../utils/sorters/status-sorter';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../../utils/debounce';
import { FormGroup } from '@angular/forms';
import { convertFiles, handleFileDrop } from '../../../../utils/file-drop-utils';
import { FilterComponent } from '../../../shared/components/filter/filter.component';
import { ProjectsDialogService } from '../../services/projects-dialog.service';
import { BaseListingComponent } from '../../../shared/base/base-listing.component';
import { ProjectWrapper } from '../../../../state/model/project.wrapper';
@Component({
selector: 'redaction-project-overview-screen',
templateUrl: './project-overview-screen.component.html',
styleUrls: ['./project-overview-screen.component.scss']
})
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
public selectedFileIds: string[] = [];
export class ProjectOverviewScreenComponent extends BaseListingComponent<FileStatusWrapper> implements OnInit, OnDestroy {
protected readonly _searchKey = 'filename';
protected readonly _selectionKey = 'fileId';
protected readonly _sortKey = 'project-overview';
public statusFilters: FilterModel[];
public peopleFilters: FilterModel[];
public needsWorkFilters: FilterModel[];
public collapsedDetails = false;
public searchForm: FormGroup;
displayedFiles: FileStatusWrapper[] = [];
detailsContainerFilters: {
needsWorkFilters: FilterModel[];
statusFilters: FilterModel[];
@ -56,9 +57,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
@ViewChild('needsWorkFilter') private _needsWorkFilterComponent: FilterComponent;
constructor(
public readonly appStateService: AppStateService,
private readonly _appStateService: AppStateService,
public readonly userService: UserService,
private readonly _sortingService: SortingService,
public readonly permissionsService: PermissionsService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _notificationService: NotificationService,
@ -67,23 +67,19 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _fileUploadService: FileUploadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _router: Router,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _translateService: TranslateService,
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({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
super(_injector);
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();
});
}
@ -92,7 +88,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.filesAutoUpdateTimer = timer(0, 7500)
.pipe(
tap(async () => {
await this.appStateService.reloadActiveProjectFilesIfNecessary();
await this._appStateService.reloadActiveProjectFilesIfNecessary();
this._loadEntitiesFromState();
})
)
.subscribe();
@ -105,14 +102,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.filesAutoUpdateTimer.unsubscribe();
}
@debounce(200)
private _executeSearch(value: { query: string }) {
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 get activeProject(): ProjectWrapper {
return this._appStateService.activeProject;
}
public reanalyseProject() {
return this.appStateService
return this._appStateService
.reanalyzeProject()
.then(() => {
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);
}
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption('project-overview');
protected get filterComponents(): FilterComponent[] {
return [this._statusFilterComponent, this._peopleFilterComponent, this._needsWorkFilterComponent];
}
public toggleSort($event) {
this._sortingService.toggleSort('project-overview', $event);
}
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: '' });
private _loadEntitiesFromState() {
this.allEntities = this._appStateService.activeProject.files;
}
reloadProjects() {
this.appStateService.getFiles(this.appStateService.activeProject, false).then(() => {
this._appStateService.getFiles(this._appStateService.activeProject, false).then(() => {
this.calculateData();
});
}
calculateData(): void {
if (!this.appStateService.activeProjectId) {
if (!this._appStateService.activeProjectId) {
return;
}
this._loadEntitiesFromState();
this._computeAllFilters();
this._filterFiles();
this._filterEntities();
this._projectDetailsComponent?.calculateChartConfig();
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) {
return item.fileId;
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
handleFileDrop(event, this.appStateService.activeProject, this._uploadFiles.bind(this));
handleFileDrop(event, this.activeProject, this._uploadFiles.bind(this));
}
@HostListener('dragover', ['$event'])
@ -230,7 +179,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
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[]) {
@ -242,7 +191,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
private _computeAllFilters() {
if (!this.appStateService.activeProject) {
if (!this.activeProject) {
return;
}
@ -252,16 +201,16 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
const allDistinctNeedsWork = new Set<string>();
// All people
this.appStateService.activeProject.files.forEach((file) => allDistinctPeople.add(file.currentReviewer));
this.allEntities.forEach((file) => allDistinctPeople.add(file.currentReviewer));
// File statuses
this.appStateService.activeProject.files.forEach((file) => allDistinctFileStatusWrapper.add(file.status));
this.allEntities.forEach((file) => allDistinctFileStatusWrapper.add(file.status));
// 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
this.appStateService.activeProject.files.forEach((file) => {
this.allEntities.forEach((file) => {
if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis');
if (file.hintsOnly) allDistinctNeedsWork.add('hint');
if (file.hasRedactions) allDistinctNeedsWork.add('redaction');
@ -310,25 +259,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
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) {
return this.permissionsService.canOpenFile(fileStatus)
? ['/ui/projects/' + this.appStateService.activeProject.project.projectId + '/file/' + fileStatus.fileId]
: [];
return this.permissionsService.canOpenFile(fileStatus) ? ['/ui/projects/' + this.activeProject.project.projectId + '/file/' + fileStatus.fileId] : [];
}
private get _filteredFiles(): FileStatusWrapper[] {
const filters = [
protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
return [
{ values: this.statusFilters, checker: keyChecker('status') },
{ values: this.peopleFilters, checker: keyChecker('currentReviewer') },
{
@ -338,36 +274,32 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
checkerArgs: this.permissionsService
}
];
return getFilteredEntities(this.appStateService.activeProject.files, filters);
}
private _filterFiles() {
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));
protected preFilter() {
this.detailsContainerFilters = {
needsWorkFilters: this.needsWorkFilters.map((f) => ({ ...f })),
statusFilters: this.statusFilters.map((f) => ({ ...f }))
};
this._changeDetectorRef.detectChanges();
}
bulkActionPerformed() {
this.selectedFileIds = [];
this.selectedEntitiesIds = [];
this.reloadProjects();
}
public openEditProjectDialog($event: MouseEvent) {
this._dialogService.openEditProjectDialog($event, this.appStateService.activeProject);
this._dialogService.openEditProjectDialog($event, this.activeProject);
}
public openDeleteProjectDialog($event: MouseEvent) {
this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject, () => {
this._dialogService.openDeleteProjectDialog($event, this.activeProject, () => {
this._router.navigate(['/ui/projects']);
});
}
public openAssignProjectMembersDialog(): void {
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.appStateService.activeProject, () => {
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => {
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 { FilterComponent } from './components/filter/filter.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { BaseListingComponent } from './base/base-listing.component';
const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent];
@ -44,6 +45,7 @@ const components = [
FilterComponent,
ConfirmationDialogComponent,
EmptyStateComponent,
BaseListingComponent,
...buttons
];

View File

@ -5,7 +5,7 @@ export class SortingOption {
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({
providedIn: 'root'
@ -17,12 +17,12 @@ export class SortingService {
'dictionary-listing': { column: 'label', order: 'asc' },
'rule-sets-listing': { column: 'name', order: 'asc' },
'default-colors': { column: 'key', order: 'asc' },
'file-attributes-listing': { column: 'name', order: 'asc' }
'file-attributes-listing': { column: 'label', order: 'asc' }
};
constructor() {}
public toggleSort(screen: Screen, column: string) {
public toggleSort(screen: ScreenName, column: string) {
if (this._options[screen].column === column) {
const currentOrder = this._options[screen].order;
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];
}
}