filters rework and project overview

This commit is contained in:
Timo Bejan 2020-11-01 21:02:16 +02:00
parent 6ae16ae5e9
commit 5508f8c2d9
13 changed files with 329 additions and 181 deletions

View File

@ -2,29 +2,27 @@
<button
color="accent"
mat-button
class="arrow-button"
[class.arrow-button]="hasArrow"
[matMenuTriggerFor]="filterMenu"
[ngClass]="{ overlay: hasActiveFilters }"
>
<span translate="file-preview.filter-menu.label"></span>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
<mat-icon [svgIcon]="icon" *ngIf="icon"></mat-icon>
<span [translate]="filterLabel"></span>
<mat-icon svgIcon="red:arrow-down" *ngIf="hasArrow"></mat-icon>
</button>
<div class="dot" *ngIf="hasActiveFilters"></div>
<mat-menu #filterMenu="matMenu" xPosition="before" (closed)="applyFilters()">
<div class="filter-menu-header">
<div
class="all-caps-label"
translate="file-preview.filter-menu.filter-types.label"
></div>
<div class="all-caps-label" translate="filter-menu.filter-types.label"></div>
<div class="actions">
<div
class="all-caps-label primary pointer"
translate="file-preview.filter-menu.all.label"
translate="filter-menu.all.label"
(click)="activateAllFilters(); $event.stopPropagation()"
></div>
<div
class="all-caps-label primary pointer"
translate="file-preview.filter-menu.none.label"
translate="filter-menu.none.label"
(click)="deactivateAllFilters(); $event.stopPropagation()"
></div>
</div>
@ -80,5 +78,5 @@
</div>
<ng-template #defaultFilterTemplate let-filter="filter">
{{ filter?.key }}
{{ filter?.label }}
</ng-template>

View File

@ -19,9 +19,11 @@ import { handleCheckedValue } from './utils/filter-utils';
})
export class FilterComponent implements OnChanges {
@Input() filterTemplate: TemplateRef<any>;
@Input() manualRedactions: ManualRedactions;
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Input() filters: FilterModel[] = [];
@Input() filterLabel = 'filter-menu.label';
@Input() hasArrow = true;
@Input() icon: string;
constructor(public readonly appStateService: AppStateService) {}

View File

@ -37,7 +37,7 @@
{
length: val.value,
color: val.color,
label: val.value + ' ' + (val.label | translate | lowercase)
label: val.value + ' ' + (val.label | humanize)
}
]"
>

View File

@ -90,7 +90,6 @@
<redaction-filter
[filterTemplate]="annotationFilterTemplate"
[filters]="filters"
[manualRedactions]="fileData?.manualRedactions"
(filtersChanged)="filtersChanged($event)"
></redaction-filter>
</div>

View File

@ -1,24 +1,41 @@
<div class="page-header">
<div class="filters flex-row">
<div translate="filters.filter-by.label"></div>
<button mat-button translate="filters.status.label">
<mat-icon svgIcon="red:status"></mat-icon>
</button>
<button mat-button translate="filters.people.label">
<mat-icon svgIcon="red:user"></mat-icon>
</button>
<button mat-button translate="filters.due-date.label">
<mat-icon svgIcon="red:lightning"></mat-icon>
</button>
<button mat-button translate="filters.created-on.label">
<mat-icon svgIcon="red:calendar"></mat-icon>
</button>
<button mat-button translate="filters.project.label">
<mat-icon svgIcon="red:folder"></mat-icon>
</button>
<button mat-button translate="filters.document.label">
<mat-icon svgIcon="red:document"></mat-icon>
</button>
<redaction-filter
[filters]="statusFilters"
[filterLabel]="'filters.status.label'"
[hasArrow]="false"
[icon]="'red:status'"
(filtersChanged)="filtersChanged()"
></redaction-filter>
<redaction-filter
[filters]="peopleFilters"
[filterLabel]="'filters.people.label'"
[hasArrow]="false"
[icon]="'red:user'"
(filtersChanged)="filtersChanged()"
></redaction-filter>
<redaction-filter
[filters]="dueDateFilters"
[filterLabel]="'filters.due-date.label'"
[hasArrow]="false"
[icon]="'red:lightning'"
(filtersChanged)="filtersChanged()"
></redaction-filter>
<redaction-filter
[filters]="addedDateFilters"
[filterLabel]="'filters.created-on.label'"
[hasArrow]="false"
[icon]="'red:calendar'"
(filtersChanged)="filtersChanged()"
></redaction-filter>
<!-- <button mat-button translate="filters.project.label">-->
<!-- <mat-icon svgIcon="red:folder"></mat-icon>-->
<!-- </button>-->
<!-- <button mat-button translate="filters.document.label">-->
<!-- <mat-icon svgIcon="red:document"></mat-icon>-->
<!-- </button>-->
</div>
<button
(click)="openAddProjectDialog()"
@ -74,15 +91,14 @@
</div>
<div
*ngIf="appStateService.allProjects?.length === 0"
*ngIf="displayedProjects?.length === 0"
class="no-data"
translate="project-listing.no-projects.label"
></div>
<div
*ngFor="
let pw of appStateService.allProjects
| sortBy: sortingOption.order:sortingOption.column
let pw of displayedProjects | sortBy: sortingOption.order:sortingOption.column
"
[routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]"
class="table-item"
@ -97,6 +113,10 @@
<mat-icon svgIcon="red:document"></mat-icon>
{{ documentCount(pw) }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ pw.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ userCount(pw) }}

View File

@ -1,11 +1,13 @@
import {Component, OnInit} 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} from '../../utils/functions';
import {DialogService} from '../../dialogs/dialog.service';
import { Component, OnInit } 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';
@Component({
selector: 'redaction-project-listing-screen',
@ -25,6 +27,13 @@ export class ProjectListingScreenComponent implements OnInit {
];
public sortingOption: SortingOption = this.sortingOptions[0];
statusFilters: FilterModel[];
dueDateFilters: FilterModel[];
peopleFilters: FilterModel[];
addedDateFilters: FilterModel[];
displayedProjects: ProjectWrapper[] = [];
constructor(
public readonly appStateService: AppStateService,
public readonly userService: UserService,
@ -40,6 +49,8 @@ export class ProjectListingScreenComponent implements OnInit {
}
private _calculateData() {
this._computeAllFilters();
this._filterProjects();
this.projectsChartData = [
{ value: this.activeProjects, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveProjects, color: 'DELETED', label: 'archived' }
@ -127,4 +138,114 @@ export class ProjectListingScreenComponent implements OnInit {
$event.preventDefault();
this.appStateService.reanalyseProject(project);
}
private _computeAllFilters() {
const allDistinctFileStatus = new Set<string>();
const allDistinctPeople = new Set<string>();
const allDistinctDueDates = new Set<string>();
const allDistinctAddedDates = new Set<string>();
this.appStateService.allProjects.forEach((entry) => {
// all people
entry.project.memberIds.forEach((memberId) => allDistinctPeople.add(memberId));
// due date
if (entry.dueDate) {
allDistinctDueDates.add(moment(entry.dueDate).format('DD/MM/YYYY'));
}
// added date
allDistinctAddedDates.add(moment(entry.projectDate).format('DD/MM/YYYY'));
// file statuses
entry.files.forEach((file) => {
allDistinctFileStatus.add(file.status);
});
});
this.statusFilters = [];
allDistinctFileStatus.forEach((status) => {
this.statusFilters.push({
key: status,
label: humanize(status)
});
});
this.peopleFilters = [];
allDistinctPeople.forEach((userId) => {
this.peopleFilters.push({
key: userId,
label: this.userService.getNameForId(userId)
});
});
this.dueDateFilters = [];
allDistinctDueDates.forEach((date) => {
this.dueDateFilters.push({
key: date,
label: date
});
});
this.addedDateFilters = [];
allDistinctAddedDates.forEach((date) => {
this.addedDateFilters.push({
key: date,
label: date
});
});
}
filtersChanged() {
this._filterProjects();
}
private _filterProjects() {
const filteredProjects = [];
for (const project of this.appStateService.allProjects) {
const statusFilterMatched = this._checkFilter(
project,
this.statusFilters,
(pw: ProjectWrapper, filter: FilterModel) => pw.hasStatus(filter.key)
);
const peopleFilterMatched = this._checkFilter(
project,
this.peopleFilters,
(pw: ProjectWrapper, filter: FilterModel) => pw.hasMember(filter.key)
);
const dueDateFilterMatched = this._checkFilter(
project,
this.dueDateFilters,
(pw: ProjectWrapper, filter: FilterModel) => pw.dueDateMatches(filter.key)
);
const addedFilterMatched = this._checkFilter(
project,
this.addedDateFilters,
(pw: ProjectWrapper, filter: FilterModel) => pw.addedDateMatches(filter.key)
);
if (
statusFilterMatched &&
peopleFilterMatched &&
addedFilterMatched &&
dueDateFilterMatched
) {
filteredProjects.push(project);
}
}
this.displayedProjects = filteredProjects;
}
private _checkFilter(project: ProjectWrapper, filters: FilterModel[], validate: Function) {
const hasChecked = filters.find((f) => f.checked);
if (!hasChecked) {
return true;
}
let filterMatched = false;
for (const filter of filters) {
if (filter.checked && validate(project, filter)) {
filterMatched = true;
break;
}
}
return filterMatched;
}
}

View File

@ -290,9 +290,13 @@
<div class="small-label stats-subtitle mt-20">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
<mat-icon svgIcon="red:document"></mat-icon>
{{ appStateService.activeProject.files.length }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ appStateService.activeProject.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ appStateService.activeProject.project.memberIds.length }}

View File

@ -18,6 +18,7 @@ import { tap } from 'rxjs/operators';
import { download } from '../utils/file-download-utils';
import { humanize } from '../utils/functions';
import { AnnotationWrapper } from '../screens/file/model/annotation.wrapper';
import * as moment from 'moment';
export interface AppState {
projects: ProjectWrapper[];
@ -29,11 +30,33 @@ export interface AppState {
}
export class ProjectWrapper {
totalNumberOfPages?: number;
hasStatus(status: string) {
return this.files.find((f) => f.status === status);
}
constructor(public project: Project, public files: FileStatus[]) {}
get projectDate() {
return this.project.date;
}
get dueDate() {
return this.project.dueDate;
}
hasMember(key: string) {
return this.project.memberIds.indexOf(key) >= 0;
}
dueDateMatches(key: string) {
return moment(this.dueDate).format('DD/MM/YYYY') === key;
}
addedDateMatches(key: string) {
return moment(this.projectDate).format('DD/MM/YYYY') === key;
}
}
@Injectable({
@ -313,9 +336,12 @@ export class AppStateService {
if (p.project.memberIds) {
p.project.memberIds.forEach((m) => totalPeople.add(m));
}
let numberOfPages = 0;
p.files.forEach((f) => {
totalAnalysedPages += f.numberOfPages;
numberOfPages += f.numberOfPages;
});
p.totalNumberOfPages = numberOfPages;
totalAnalysedPages += numberOfPages;
});
this._appState.totalPeople = totalPeople.size;

View File

@ -6,7 +6,10 @@ export function groupBy(xs: any[], key: string) {
}
export function humanize(str: string) {
const frags = str.split(/[ \-_]+/);
if (!str) {
return undefined;
}
const frags = str.toLowerCase().split(/[ \-_]+/);
for (let i = 0; i < frags.length; i++) {
frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
}

View File

@ -473,18 +473,6 @@
"view-toggle": {
"label": "Redacted View"
},
"filter-menu": {
"label": "Filter",
"all": {
"label": "All"
},
"none": {
"label": "None"
},
"filter-types": {
"label": "Filter types"
}
},
"tabs": {
"quick-navigation": {
"label": "Quick Navigation"
@ -587,5 +575,17 @@
"request": "Redaction Request",
"ignore": "Ignored redaction"
}
},
"filter-menu": {
"label": "Filter",
"all": {
"label": "All"
},
"none": {
"label": "None"
},
"filter-types": {
"label": "Filter types"
}
}
}

View File

@ -30,34 +30,4 @@
<path d="M117.765,288.341c0-9.341-6.478-14.915-17.911-14.915c-4.682,0-7.848,0.451-9.503,0.903v29.985
c1.95,0.451,4.37,0.592,7.677,0.592C110.246,304.907,117.765,298.74,117.765,288.341z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,96 +1,96 @@
{
"name": "redaction",
"version": "0.0.97",
"license": "MIT",
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged && ng lint --project=red-ui-http && ng lint --project=red-ui --fix"
"name": "redaction",
"version": "0.0.97",
"license": "MIT",
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged && ng lint --project=red-ui-http && ng lint --project=red-ui --fix"
}
},
"scripts": {
"build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod",
"nx": "nx",
"start": "nx serve",
"build": "nx build",
"test": "nx test",
"lint": "nx workspace-lint && nx lint",
"e2e": "nx e2e",
"affected:apps": "nx affected:apps",
"affected:libs": "nx affected:libs",
"affected:build": "nx affected:build",
"affected:e2e": "nx affected:e2e",
"affected:test": "nx affected:test",
"affected:lint": "nx affected:lint",
"affected:dep-graph": "nx affected:dep-graph",
"affected": "nx affected",
"format": "nx format:write",
"format:write": "nx format:write",
"format:check": "nx format:check",
"update": "nx migrate latest",
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"private": true,
"dependencies": {
"@angular/animations": "^10.0.0",
"@angular/cdk": "^10.2.3",
"@angular/common": "^10.0.0",
"@angular/core": "^10.0.0",
"@angular/forms": "^10.0.0",
"@angular/material": "^10.2.1",
"@angular/platform-browser": "^10.0.0",
"@angular/platform-browser-dynamic": "^10.0.0",
"@angular/router": "^10.0.0",
"@angular/service-worker": "^10.0.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@nrwl/angular": "^10.2.0",
"@pdftron/webviewer": "^7.0.1",
"file-saver": "^2.0.2",
"jwt-decode": "^3.0.0",
"keycloak-angular": "^8.0.1",
"keycloak-js": "10.0.2",
"lint-staged": "^10.5.0",
"ng2-file-upload": "^1.4.0",
"ngp-sort-pipe": "^0.0.4",
"ngx-dropzone": "^2.2.2",
"ngx-toastr": "^13.0.0",
"rxjs": "~6.5.5",
"scroll-into-view-if-needed": "^2.2.26",
"zone.js": "^0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.0",
"@angular-devkit/build-ng-packagr": "^0.1001.3",
"@angular/cli": "^10.1.2",
"@angular/compiler": "^10.0.0",
"@angular/compiler-cli": "^10.0.0",
"@angular/language-service": "^10.0.0",
"@nrwl/cypress": "10.2.0",
"@nrwl/jest": "10.2.0",
"@nrwl/workspace": "10.2.0",
"@types/jest": "26.0.8",
"@types/node": "~8.9.4",
"codelyzer": "~5.0.1",
"cypress": "^4.1.0",
"dotenv": "6.2.0",
"eslint": "6.8.0",
"google-translate-api-browser": "^1.1.71",
"husky": "^4.3.0",
"jest": "26.2.2",
"jest-preset-angular": "8.2.1",
"lodash": "^4.17.20",
"moment": "^2.29.1",
"ng-packagr": "^10.1.2",
"prettier": "2.0.4",
"pretty-quick": "^3.1.0",
"superagent": "^6.1.0",
"superagent-promise": "^1.1.0",
"ts-jest": "26.1.4",
"ts-node": "~7.0.0",
"tslint": "~6.0.0",
"typescript": "~3.9.3"
}
},
"scripts": {
"build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod",
"nx": "nx",
"start": "nx serve",
"build": "nx build",
"test": "nx test",
"lint": "nx workspace-lint && nx lint",
"e2e": "nx e2e",
"affected:apps": "nx affected:apps",
"affected:libs": "nx affected:libs",
"affected:build": "nx affected:build",
"affected:e2e": "nx affected:e2e",
"affected:test": "nx affected:test",
"affected:lint": "nx affected:lint",
"affected:dep-graph": "nx affected:dep-graph",
"affected": "nx affected",
"format": "nx format:write",
"format:write": "nx format:write",
"format:check": "nx format:check",
"update": "nx migrate latest",
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"private": true,
"dependencies": {
"@angular/animations": "^10.0.0",
"@angular/cdk": "^10.2.3",
"@angular/common": "^10.0.0",
"@angular/core": "^10.0.0",
"@angular/forms": "^10.0.0",
"@angular/material": "^10.2.1",
"@angular/platform-browser": "^10.0.0",
"@angular/platform-browser-dynamic": "^10.0.0",
"@angular/router": "^10.0.0",
"@angular/service-worker": "^10.0.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@nrwl/angular": "^10.2.0",
"@pdftron/webviewer": "^7.0.1",
"file-saver": "^2.0.2",
"jwt-decode": "^3.0.0",
"keycloak-angular": "^8.0.1",
"keycloak-js": "10.0.2",
"lint-staged": "^10.5.0",
"ng2-file-upload": "^1.4.0",
"ngp-sort-pipe": "^0.0.4",
"ngx-dropzone": "^2.2.2",
"ngx-toastr": "^13.0.0",
"rxjs": "~6.5.5",
"scroll-into-view-if-needed": "^2.2.26",
"zone.js": "^0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.0",
"@angular-devkit/build-ng-packagr": "^0.1001.3",
"@angular/cli": "^10.1.2",
"@angular/compiler": "^10.0.0",
"@angular/compiler-cli": "^10.0.0",
"@angular/language-service": "^10.0.0",
"@nrwl/cypress": "10.2.0",
"@nrwl/jest": "10.2.0",
"@nrwl/workspace": "10.2.0",
"@types/jest": "26.0.8",
"@types/node": "~8.9.4",
"codelyzer": "~5.0.1",
"cypress": "^4.1.0",
"dotenv": "6.2.0",
"eslint": "6.8.0",
"google-translate-api-browser": "^1.1.71",
"husky": "^4.3.0",
"jest": "26.2.2",
"jest-preset-angular": "8.2.1",
"lodash": "^4.17.20",
"moment": "^2.28.0",
"ng-packagr": "^10.1.2",
"prettier": "2.0.4",
"pretty-quick": "^3.1.0",
"superagent": "^6.1.0",
"superagent-promise": "^1.1.0",
"ts-jest": "26.1.4",
"ts-node": "~7.0.0",
"tslint": "~6.0.0",
"typescript": "~3.9.3"
}
}

View File

@ -7704,11 +7704,16 @@ mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdir
dependencies:
minimist "^1.2.5"
moment@^2.27.0, moment@^2.28.0:
moment@^2.27.0:
version "2.28.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.28.0.tgz#cdfe73ce01327cee6537b0fafac2e0f21a237d75"
integrity sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"