Pull request #26: Sorting

Merge in RED/ui from ui-updates to master

* commit '6c117f9f3e66d92aeef2039a09ff33732c01826b':
  More sorting
  Generic sorting
This commit is contained in:
Timo Bejan 2020-11-02 17:01:32 +01:00
commit 4f59a39b3d
15 changed files with 276 additions and 173 deletions

View File

@ -66,6 +66,8 @@ import { FileNotAvailableOverlayComponent } from './screens/file/file-not-availa
import { ToastComponent } from './components/toast/toast.component';
import { FilterComponent } from './common/filter/filter.component';
import { AppInfoComponent } from './screens/app-info/app-info.component';
import { SortingComponent } from './components/sorting/sorting.component';
import { TableColNameComponent } from './components/table-col-name/table-col-name.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -97,7 +99,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
ToastComponent,
FileNotAvailableOverlayComponent,
FilterComponent,
AppInfoComponent
AppInfoComponent,
SortingComponent,
TableColNameComponent
],
imports: [
BrowserModule,

View File

@ -1,4 +1,4 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Color } from '../../utils/types';
export class DoughnutChartConfig {

View File

@ -0,0 +1,11 @@
<mat-form-field appearance="none" class="red-select">
<mat-select
[(ngModel)]="activeOption"
panelClass="red-select-panel"
(selectionChange)="dropdownSelect()"
>
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>

View File

@ -0,0 +1,86 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
export class SortingOption {
label?: string;
order: 'asc' | 'desc';
column: string;
}
const SORTING_OPTIONS: { [key: string]: SortingOption[] } = {
'project-listing': [
{ label: 'sorting.recent.label', order: 'desc', column: 'projectDate' },
{ label: 'sorting.alphabetically.label', order: 'asc', column: 'project.projectName' }
],
'project-overview': [
{ label: 'sorting.recent.label', order: 'desc', column: 'added' },
{ label: 'sorting.alphabetically.label', order: 'asc', column: 'filename' },
{ label: 'sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages' },
{ label: 'sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' }
]
};
@Component({
selector: 'redaction-sorting',
templateUrl: './sorting.component.html',
styleUrls: ['./sorting.component.scss']
})
export class SortingComponent implements OnInit {
@Input()
private type: 'project-overview' | 'project-listing';
@Output()
private optionChanged = new EventEmitter<SortingOption>();
public sortingOptions: SortingOption[];
public activeOption: SortingOption;
constructor() {}
public ngOnInit(): void {}
private _addCustomOption(option: Partial<SortingOption>) {
const customOption = {
label: 'sorting.custom.label',
column: option.column,
order: option.order
};
this.sortingOptions.push(customOption);
this.activeOption = customOption;
}
private _resetOptions() {
if (this.activeOption?.label !== 'sorting.custom.label') {
this.sortingOptions = [...SORTING_OPTIONS[this.type]];
}
}
public dropdownSelect() {
this._resetOptions();
this.optionChanged.emit(this.activeOption);
}
public setOption(option: { column: string; order: 'asc' | 'desc' }) {
if (!this.sortingOptions) {
this._resetOptions();
}
const existingOption = this.sortingOptions.find(
(o) => o.column === option.column && o.order === option.order
);
if (existingOption) {
this.activeOption = existingOption;
this._resetOptions();
} else {
this._addCustomOption(option);
}
}
public toggleSort(column: string) {
if (this.activeOption.column === column) {
const currentOrder = this.activeOption.order;
this.setOption({ column, order: currentOrder === 'asc' ? 'desc' : 'asc' });
} else {
this.setOption({ column, order: 'asc' });
}
this.optionChanged.emit(this.activeOption);
}
}

View File

@ -0,0 +1,7 @@
<div (click)="withSort && toggleSort.emit(column)" [class.pointer]="withSort" [ngClass]="class">
<span [translate]="label" class="small-label"></span>
<div class="sort-arrows-container" *ngIf="withSort">
<mat-icon [color]="arrowColor.up" svgIcon="red:arrow-up"></mat-icon>
<mat-icon [color]="arrowColor.down" svgIcon="red:arrow-down"></mat-icon>
</div>
</div>

View File

@ -0,0 +1,23 @@
@import '../../../assets/styles/red-variables';
:host {
display: flex;
border-bottom: 1px solid $separator;
> div {
align-items: center;
text-transform: uppercase;
display: flex;
font-weight: 600;
gap: 8px;
padding: 8px 16px;
}
.sort-arrows-container {
mat-icon {
display: block;
width: 6px;
height: 11px;
}
}
}

View File

@ -0,0 +1,34 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { SortingOption } from '../sorting/sorting.component';
@Component({
selector: 'redaction-table-col-name',
templateUrl: './table-col-name.component.html',
styleUrls: ['./table-col-name.component.scss']
})
export class TableColNameComponent implements OnInit {
@Input() public activeSortingOption: SortingOption;
@Input() public column: string;
@Input() public label: string;
@Input() public withSort = false;
@Input() public class: string;
@Output() public toggleSort = new EventEmitter<string>();
constructor() {}
ngOnInit(): void {}
public get arrowColor(): { up: string; down: string } {
const up =
this.activeSortingOption.order === 'desc' &&
this.activeSortingOption.column === this.column
? 'primary'
: 'accent';
const down =
this.activeSortingOption.order === 'asc' &&
this.activeSortingOption.column === this.column
? 'primary'
: 'accent';
return { up, down };
}
}

View File

@ -74,37 +74,34 @@
[class.active]="bulkSelectActive"
(click)="toggleBulkSelect()"
></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<redaction-sorting
#sortingComponent
(optionChanged)="sortingOptionChanged($event)"
type="project-listing"
></redaction-sorting>
</div>
</div>
<div class="grid-container" [class.bulk-select]="bulkSelectActive">
<div class="table-col-name" *ngIf="bulkSelectActive"></div>
<div class="select-oval-placeholder" *ngIf="bulkSelectActive"></div>
<div class="table-col-name">
<span
class="small-label"
translate="project-listing.table-col-names.name.label"
></span>
</div>
<div class="table-col-name">
<span
class="small-label"
translate="project-listing.table-col-names.owner.label"
></span>
</div>
<div class="table-col-name flex-end">
<span
class="small-label"
translate="project-listing.table-col-names.status.label"
></span>
</div>
<redaction-table-col-name
label="project-listing.table-col-names.name.label"
column="project.projectName"
[activeSortingOption]="sortingOption"
[withSort]="true"
(toggleSort)="sortingComponent.toggleSort($event)"
></redaction-table-col-name>
<redaction-table-col-name
label="project-listing.table-col-names.owner.label"
></redaction-table-col-name>
<redaction-table-col-name
label="project-listing.table-col-names.status.label"
class="flex-end"
></redaction-table-col-name>
<div
*ngIf="displayedProjects?.length === 0"

View File

@ -1,13 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { FileStatus, Project } from '@redaction/red-ui-http';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Project } from '@redaction/red-ui-http';
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { SortingOption } from '../../utils/types';
import { groupBy, humanize } from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service';
import { FilterModel } from '../../common/filter/model/filter.model';
import * as moment from 'moment';
import { SortingComponent, SortingOption } from '../../components/sorting/sorting.component';
@Component({
selector: 'redaction-project-listing-screen',
@ -19,23 +19,18 @@ export class ProjectListingScreenComponent implements OnInit {
public projectsChartData: DoughnutChartConfig[] = [];
public documentsChartData: DoughnutChartConfig[] = [];
public sortingOptions: SortingOption[] = [
{ label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate' },
{
label: 'project-listing.sorting.alphabetically.label',
order: 'asc',
column: 'project.projectName'
}
];
public sortingOption: SortingOption = this.sortingOptions[0];
public bulkSelectActive = false;
statusFilters: FilterModel[];
dueDateFilters: FilterModel[];
peopleFilters: FilterModel[];
addedDateFilters: FilterModel[];
public statusFilters: FilterModel[];
public dueDateFilters: FilterModel[];
public peopleFilters: FilterModel[];
public addedDateFilters: FilterModel[];
displayedProjects: ProjectWrapper[] = [];
public displayedProjects: ProjectWrapper[] = [];
@ViewChild('sortingComponent', { static: true }) public sortingComponent: SortingComponent;
public sortingOption: SortingOption = { column: 'projectDate', order: 'desc' };
constructor(
public readonly appStateService: AppStateService,
@ -43,7 +38,8 @@ export class ProjectListingScreenComponent implements OnInit {
private readonly _dialogService: DialogService
) {}
ngOnInit(): void {
public ngOnInit(): void {
this.sortingComponent.setOption(this.sortingOption);
this.appStateService.reset();
this._calculateData();
this.appStateService.fileStatusChanged.subscribe(() => {
@ -112,7 +108,7 @@ export class ProjectListingScreenComponent implements OnInit {
});
}
downloadRedactionReport($event: MouseEvent, project: Project) {
public downloadRedactionReport($event: MouseEvent, project: Project) {
$event.preventDefault();
this.appStateService.downloadRedactionReport(project);
}
@ -125,6 +121,10 @@ export class ProjectListingScreenComponent implements OnInit {
this.bulkSelectActive = !this.bulkSelectActive;
}
public sortingOptionChanged(option: SortingOption) {
this.sortingOption = option;
}
public getProjectStatusConfig(pw: ProjectWrapper) {
const obj = pw.files.reduce((acc, file) => {
const status = file.status;
@ -141,7 +141,7 @@ export class ProjectListingScreenComponent implements OnInit {
.map((status) => ({ length: obj[status], color: status }));
}
reanalyseProject($event: MouseEvent, project: Project) {
public reanalyseProject($event: MouseEvent, project: Project) {
$event.preventDefault();
this.appStateService.reanalyseProject(project);
}
@ -199,7 +199,7 @@ export class ProjectListingScreenComponent implements OnInit {
});
}
filtersChanged() {
public filtersChanged() {
this._filterProjects();
}

View File

@ -80,64 +80,50 @@
[class.active]="bulkSelectActive"
(click)="toggleBulkSelect()"
></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<redaction-sorting
#sortingComponent
(optionChanged)="sortingOptionChanged($event)"
type="project-overview"
></redaction-sorting>
</div>
</div>
<div class="grid-container" [class.bulk-select]="bulkSelectActive">
<!-- Table column names-->
<div class="table-col-name" *ngIf="bulkSelectActive"></div>
<div class="select-oval-placeholder" *ngIf="bulkSelectActive"></div>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.name.label"
></span>
</div>
<redaction-table-col-name
label="project-overview.table-col-names.name.label"
column="filename"
[activeSortingOption]="sortingOption"
[withSort]="true"
(toggleSort)="sortingComponent.toggleSort($event)"
></redaction-table-col-name>
<div class="table-col-name pointer" (click)="toggleSortByAddedOn()">
<span
class="small-label"
translate="project-overview.table-col-names.added-on.label"
></span>
<div class="sort-arrows-container">
<mat-icon
svgIcon="red:arrow-up"
[color]="sortingOption === sortingOptions[0] ? 'primary' : 'currentColor'"
></mat-icon>
<mat-icon
svgIcon="red:arrow-down"
[color]="sortingOption === sortingOptions[1] ? 'primary' : 'currentColor'"
></mat-icon>
</div>
</div>
<redaction-table-col-name
label="project-overview.table-col-names.added-on.label"
column="added"
[activeSortingOption]="sortingOption"
[withSort]="true"
(toggleSort)="sortingComponent.toggleSort($event)"
></redaction-table-col-name>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.needs-work.label"
></span>
</div>
<redaction-table-col-name
label="project-overview.table-col-names.needs-work.label"
></redaction-table-col-name>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.assigned-to.label"
></span>
</div>
<redaction-table-col-name
label="project-overview.table-col-names.assigned-to.label"
></redaction-table-col-name>
<div class="table-col-name flex-end">
<span
class="small-label"
translate="project-overview.table-col-names.status.label"
></span>
</div>
<redaction-table-col-name
label="project-overview.table-col-names.status.label"
class="flex-end"
column="status"
[activeSortingOption]="sortingOption"
[withSort]="true"
(toggleSort)="sortingComponent.toggleSort($event)"
></redaction-table-col-name>
<div
*ngIf="displayedFiles?.length === 0"

View File

@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
FileStatus,
@ -13,7 +13,6 @@ import { FileUploadModel } from '../../upload/model/file-upload.model';
import { FileUploadService } from '../../upload/file-upload.service';
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import { UserService } from '../../user/user.service';
import { SortingOption } from '../../utils/types';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy, humanize } from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service';
@ -21,6 +20,7 @@ import { TranslateService } from '@ngx-translate/core';
import { FileActionService } from '../file/service/file-action.service';
import { FilterModel } from '../../common/filter/model/filter.model';
import * as moment from 'moment';
import { SortingComponent, SortingOption } from '../../components/sorting/sorting.component';
@Component({
selector: 'redaction-project-overview-screen',
@ -30,26 +30,6 @@ import * as moment from 'moment';
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private _selectedFileIds: string[] = [];
public sortingOptions: SortingOption[] = [
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
{ label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' },
{
label: 'project-overview.sorting.alphabetically.label',
order: 'asc',
column: 'filename'
},
{
label: 'project-overview.sorting.number-of-pages.label',
order: 'asc',
column: 'numberOfPages'
},
{
label: 'project-overview.sorting.number-of-analyses.label',
order: 'desc',
column: 'numberOfAnalyses'
}
];
public sortingOption: SortingOption = this.sortingOptions[0];
public documentsChartData: DoughnutChartConfig[] = [];
public bulkSelectActive = false;
@ -59,6 +39,9 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
public displayedFiles: FileStatus[] = [];
@ViewChild('sortingComponent', { static: true }) public sortingComponent: SortingComponent;
public sortingOption: SortingOption = { column: 'added', order: 'desc' };
constructor(
public readonly appStateService: AppStateService,
public readonly userService: UserService,
@ -85,6 +68,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.sortingComponent.setOption(this.sortingOption);
this._fileDropOverlayService.initFileDropHandling();
this._calculateData();
this._displayNewRuleToast();
@ -304,9 +288,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return !this.isError(fileStatus) && !this.isProcessing(fileStatus);
}
public toggleSortByAddedOn() {
const sortedByRecent: boolean = this.sortingOption === this.sortingOptions[0];
this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0];
public sortingOptionChanged(option: SortingOption) {
this.sortingOption = option;
}
private _computeAllFilters() {

View File

@ -1,9 +1,3 @@
import { FileStatus } from '@redaction/red-ui-http';
export type Color = FileStatus.StatusEnum | ProjectStatus.StatusEnum;
export class SortingOption {
label: string;
order: string;
column: string;
}

View File

@ -280,14 +280,6 @@
},
"no-projects": {
"label": "You currently have no projects. You can start your work by creating a new one!"
},
"sorting": {
"recent": {
"label": "Recent"
},
"alphabetically": {
"label": "Alphabetically"
}
}
},
"file-details": {
@ -355,9 +347,6 @@
},
"bulk-select": {
"label": "Bulk select"
},
"recent": {
"label": "Recent"
}
},
"table-col-names": {
@ -377,23 +366,6 @@
"label": "Status"
}
},
"sorting": {
"recent": {
"label": "Recent"
},
"oldest": {
"label": "Oldest"
},
"alphabetically": {
"label": "Alphabetically"
},
"number-of-pages": {
"label": "Number of pages"
},
"number-of-analyses": {
"label": "Number of analyses"
}
},
"upload-error": {
"label": "Failed to upload file: {{name}}"
},
@ -599,5 +571,25 @@
"filter-types": {
"label": "Filter types"
}
},
"sorting": {
"recent": {
"label": "Recent"
},
"oldest": {
"label": "Oldest"
},
"alphabetically": {
"label": "Alphabetically"
},
"number-of-pages": {
"label": "Number of pages"
},
"number-of-analyses": {
"label": "Number of analyses"
},
"custom": {
"label": "Custom"
}
}
}

View File

@ -24,22 +24,8 @@
grid-column: 1/-1;
}
.table-col-name {
font-weight: 600;
display: flex;
gap: 8px;
padding: 8px 16px;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
align-items: center;
text-transform: uppercase;
.sort-arrows-container {
mat-icon {
display: block;
width: 6px;
height: 11px;
}
}
.select-oval-placeholder {
border-bottom: 1px solid $separator;
}
.table-item {
@ -62,7 +48,7 @@
> div {
height: 80px;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
border-bottom: 1px solid $separator;
padding: 0 16px;
}