Pull request #32: Filters

Merge in RED/ui from filters to master

* commit '5fa011822c2cf0b18e3390fce3ff29f7e12c3534':
  Fixed styling
  Link filters on project listing
  Project listing details component
  Doughnut chart filters
  Fixed filter linking
  Minor UI improvements
  Filename tooltip position
  Link needs work filters with project details
This commit is contained in:
Timo Bejan 2020-11-08 18:16:19 +01:00
commit 8cf51e6014
20 changed files with 340 additions and 227 deletions

View File

@ -73,6 +73,7 @@ import { PageIndicatorComponent } from './screens/file/page-indicator/page-indic
import { NeedsWorkBadgeComponent } from './screens/common/needs-work-badge/needs-work-badge.component';
import { ProjectOverviewEmptyComponent } from './screens/empty-states/project-overview-empty/project-overview-empty.component';
import { ProjectListingEmptyComponent } from './screens/empty-states/project-listing-empty/project-listing-empty.component';
import { ProjectListingDetailsComponent } from './screens/project-listing-screen/project-listing-details/project-listing-details.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -111,7 +112,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
PageIndicatorComponent,
NeedsWorkBadgeComponent,
ProjectOverviewEmptyComponent,
ProjectListingEmptyComponent
ProjectListingEmptyComponent,
ProjectListingDetailsComponent
],
imports: [
BrowserModule,

View File

@ -1,6 +1,5 @@
import { FilterModel } from '../model/filter.model';
import { FileStatusWrapper } from '../../../screens/file/model/file-status.wrapper';
import * as moment from 'moment';
import { ProjectWrapper } from '../../../state/app-state.service';
export const RedactionFilterSorter = {
@ -57,9 +56,6 @@ export const annotationFilterChecker = (f: FileStatusWrapper, filter: FilterMode
return f[getter];
};
export const fileAddedFilterChecker = (f: FileStatusWrapper, filter: FilterModel) =>
moment(f.added).format('DD/MM/YYYY') === filter.key;
export const projectStatusChecker = (pw: ProjectWrapper, filter: FilterModel) =>
pw.hasStatus(filter.key);

View File

@ -1,5 +1,11 @@
<div [matTooltipPosition]="'above'" [matTooltip]="username" class="flex-row">
<div [className]="colorClass + ' oval ' + size">{{ initials }}</div>
<div class="flex-row">
<div
[className]="colorClass + ' oval ' + size"
[matTooltipPosition]="'above'"
[matTooltip]="username"
>
{{ initials }}
</div>
<div *ngIf="withName" class="clamp-2">
{{ username || ('initials-avatar.unassigned' | translate) }}
</div>

View File

@ -30,7 +30,12 @@
<div class="breakdown-container">
<div>
<div *ngFor="let val of parsedConfig">
<div
*ngFor="let val of parsedConfig"
[class.active]="val.checked"
[class.filter-disabled]="!filter"
(click)="toggleFilter.emit(val.label)"
>
<redaction-status-bar
[small]="true"
[config]="[

View File

@ -30,11 +30,29 @@
flex-direction: column;
align-items: center;
gap: 8px;
margin-left: -8px;
div {
> div {
width: fit-content;
display: flex;
flex-direction: column;
gap: 8px;
> div {
border-radius: 4px;
padding: 3px 8px;
&:not(.filter-disabled) {
cursor: pointer;
}
&:hover:not(.active):not(.filter-disabled) {
background-color: $grey-6;
}
&.active {
background-color: rgba($primary, 0.1);
}
}
}
}

View File

@ -1,10 +1,12 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Color } from '../../utils/types';
import { FilterModel } from '../../common/filter/model/filter.model';
export class DoughnutChartConfig {
value: number;
color: Color;
label: string;
active?: boolean;
}
@Component({
@ -18,6 +20,8 @@ export class SimpleDoughnutChartComponent implements OnChanges {
@Input() radius = 85;
@Input() strokeWidth = 20;
@Input() direction: 'row' | 'column' = 'column';
@Input() filter: FilterModel[];
@Output() public toggleFilter = new EventEmitter<string>();
public chartData: any[] = [];
public perimeter: number;
@ -70,6 +74,11 @@ export class SimpleDoughnutChartComponent implements OnChanges {
// Eliminate items with value = 0
public get parsedConfig() {
return this.config.filter((el) => el.value);
return this.config
.filter((el) => el.value)
.map((el) => ({
...el,
checked: this.filter?.find((f) => f.key === el.label)?.checked
}));
}
}

View File

@ -0,0 +1,34 @@
<div>
<redaction-simple-doughnut-chart
[config]="projectsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.projects'"
></redaction-simple-doughnut-chart>
<div class="project-stats-container">
<div class="project-stats-item">
<mat-icon svgIcon="red:document"></mat-icon>
<div>
<div class="heading">{{ totalPages }}</div>
<div translate="project-listing.stats.analyzed-pages"></div>
</div>
</div>
<div class="project-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ totalPeople }}</div>
<div translate="project-listing.stats.total-people"></div>
</div>
</div>
</div>
</div>
<div>
<redaction-simple-doughnut-chart
[config]="documentsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.total-documents'"
[filter]="filters.statusFilters"
(toggleFilter)="toggleFilter('statusFilters', $event)"
></redaction-simple-doughnut-chart>
</div>

View File

@ -0,0 +1,32 @@
:host {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
> div {
display: flex;
flex-direction: column;
align-items: center;
.project-stats-container {
width: fit-content;
.project-stats-item {
display: flex;
width: fit-content;
gap: 5px;
margin-top: 25px;
&:first-of-type {
margin-top: 50px;
}
mat-icon {
height: 16px;
margin-top: 2px;
}
}
}
}
}

View File

@ -0,0 +1,34 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DoughnutChartConfig } from '../../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { AppStateService } from '../../../state/app-state.service';
import { FilterModel } from '../../../common/filter/model/filter.model';
@Component({
selector: 'redaction-project-listing-details',
templateUrl: './project-listing-details.component.html',
styleUrls: ['./project-listing-details.component.scss']
})
export class ProjectListingDetailsComponent implements OnInit {
@Input() public projectsChartData: DoughnutChartConfig[];
@Input() public documentsChartData: DoughnutChartConfig[];
@Input() public filters: { statusFilters: FilterModel[] };
@Output() public filtersChanged = new EventEmitter();
constructor(private readonly _appStateService: AppStateService) {}
ngOnInit(): void {}
public get totalPages() {
return this._appStateService.totalAnalysedPages;
}
public get totalPeople() {
return this._appStateService.totalPeople;
}
public toggleFilter(filterType: 'needsWorkFilters' | 'statusFilters', key: string): void {
const filter = this.filters[filterType].find((f) => f.key === key);
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
}

View File

@ -162,7 +162,7 @@
[config]="getProjectStatusConfig(pw)"
></redaction-status-bar>
<div class="action-buttons">
<div class="action-buttons" *ngIf="userService.isManager(user)">
<button
(click)="openDeleteProjectDialog($event, pw.project)"
[matTooltip]="'project-listing.delete.action' | translate"
@ -229,38 +229,12 @@
</div>
<div class="right-fixed-container">
<div>
<redaction-simple-doughnut-chart
[config]="projectsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.projects'"
></redaction-simple-doughnut-chart>
<div class="project-stats-container">
<div class="project-stats-item">
<mat-icon svgIcon="red:document"></mat-icon>
<div>
<div class="heading">{{ totalPages }}</div>
<div translate="project-listing.stats.analyzed-pages"></div>
</div>
</div>
<div class="project-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ totalPeople }}</div>
<div translate="project-listing.stats.total-people"></div>
</div>
</div>
</div>
</div>
<div>
<redaction-simple-doughnut-chart
[config]="documentsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.total-documents'"
></redaction-simple-doughnut-chart>
</div>
<redaction-project-listing-details
[projectsChartData]="projectsChartData"
[documentsChartData]="documentsChartData"
[filters]="detailsContainerFilters"
(filtersChanged)="filtersChanged($event)"
></redaction-project-listing-details>
</div>
</div>
</section>

View File

@ -38,31 +38,4 @@
display: flex;
width: 430px;
padding-top: 50px;
> div {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.project-stats-container {
width: fit-content;
.project-stats-item {
display: flex;
width: fit-content;
gap: 5px;
margin-top: 25px;
&:first-of-type {
margin-top: 50px;
}
mat-icon {
height: 16px;
margin-top: 2px;
}
}
}
}

View File

@ -34,8 +34,12 @@ export class ProjectListingScreenComponent implements OnInit {
public dueDateFilters: FilterModel[];
public peopleFilters: FilterModel[];
public needsWorkFilters: FilterModel[];
public displayedProjects: ProjectWrapper[] = [];
public detailsContainerFilters: {
statusFilters: FilterModel[];
};
public displayedProjects: ProjectWrapper[] = [];
public sortingOption: SortingOption = { column: 'projectDate', order: 'desc' };
constructor(
@ -72,14 +76,6 @@ export class ProjectListingScreenComponent implements OnInit {
return this.userService.user;
}
public get totalPages() {
return this.appStateService.totalAnalysedPages;
}
public get totalPeople() {
return this.appStateService.totalPeople;
}
public get activeProjects() {
return this.appStateService.allProjects.reduce(
(i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0),
@ -216,7 +212,14 @@ export class ProjectListingScreenComponent implements OnInit {
this.needsWorkFilters = needsWorkFilters;
}
public filtersChanged() {
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();
}
@ -227,7 +230,9 @@ export class ProjectListingScreenComponent implements OnInit {
{ values: this.dueDateFilters, checker: dueDateChecker },
{ values: this.needsWorkFilters, checker: annotationFilterChecker, matchAll: true }
];
this.detailsContainerFilters = {
statusFilters: this.statusFilters.map((f) => ({ ...f }))
};
this.displayedProjects = getFilteredEntities(this.appStateService.allProjects, filters);
this._changeDetectorRef.detectChanges();
}

View File

@ -64,29 +64,23 @@
[config]="documentsChartData"
[radius]="70"
[strokeWidth]="15"
[subtitle]="'project-overview.project-details.charts.total-documents'"
[subtitle]="'project-overview.project-details.charts.documents-in-project'"
direction="row"
[filter]="filters.statusFilters"
(toggleFilter)="toggleFilter('statusFilters', $event)"
></redaction-simple-doughnut-chart>
</div>
<div class="mt-24 legend" *ngIf="hasFiles">
<div>
<div
*ngFor="let filter of filters.needsWorkFilters"
[class.active]="filter.checked"
(click)="toggleFilter('needsWorkFilters', filter.key)"
>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue('hint')"
[typeValue]="appStateService.getDictionaryTypeValue(filter.key.slice(0, -1))"
></redaction-annotation-icon>
{{ 'project-overview.legend.contains-hints' | translate }}
</div>
<div>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue('redaction')"
></redaction-annotation-icon>
{{ 'project-overview.legend.contains-redactions' | translate }}
</div>
<div>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue('suggestion')"
></redaction-annotation-icon>
{{ 'project-overview.legend.contains-suggestions' | translate }}
{{ 'project-overview.legend.' + filter.key | translate }}
</div>
</div>

View File

@ -1,3 +1,5 @@
@import '../../../../assets/styles/red-variables';
.members-container {
gap: 5px;
}
@ -5,12 +7,24 @@
.legend {
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
margin-left: -8px;
> div {
display: flex;
gap: 8px;
align-items: center;
border-radius: 4px;
cursor: pointer;
padding: 3px 8px;
&:hover {
background-color: $grey-6;
}
&.active {
background-color: rgba($primary, 0.1);
}
}
}

View File

@ -1,10 +1,11 @@
import { Component, OnInit, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, Output, EventEmitter, Input, ChangeDetectorRef } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { UserService } from '../../../user/user.service';
import { groupBy } from '../../../utils/functions';
import { DoughnutChartConfig } from '../../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { DialogService } from '../../../dialogs/dialog.service';
import { Router } from '@angular/router';
import { FilterModel } from '../../../common/filter/model/filter.model';
@Component({
selector: 'redaction-project-details',
@ -13,7 +14,9 @@ import { Router } from '@angular/router';
})
export class ProjectDetailsComponent implements OnInit {
public documentsChartData: DoughnutChartConfig[] = [];
@Output() public reloadProjects = new EventEmitter<void>();
@Input() public filters: { needsWorkFilters: FilterModel[]; statusFilters: FilterModel[] };
@Output() public reloadProjects = new EventEmitter();
@Output() public filtersChanged = new EventEmitter();
constructor(
public readonly appStateService: AppStateService,
@ -61,7 +64,7 @@ export class ProjectDetailsComponent implements OnInit {
);
}
public openAssignProjectMembersDialog() {
public openAssignProjectMembersDialog(): void {
this._dialogService.openAssignProjectMembersAndOwnerDialog(
null,
this.appStateService.activeProject.project,
@ -71,12 +74,12 @@ export class ProjectDetailsComponent implements OnInit {
);
}
public downloadRedactionReport($event: MouseEvent) {
public downloadRedactionReport($event: MouseEvent): void {
$event.stopPropagation();
this.appStateService.downloadRedactionReport();
}
public calculateChartConfig() {
public calculateChartConfig(): void {
if (this.appStateService.activeProject) {
const groups = groupBy(this.appStateService.activeProject?.files, 'status');
this.documentsChartData = [];
@ -87,7 +90,13 @@ export class ProjectDetailsComponent implements OnInit {
}
}
get hasFiles() {
get hasFiles(): boolean {
return this.appStateService.activeProject.hasFiles;
}
public toggleFilter(filterType: 'needsWorkFilters' | 'statusFilters', key: string): void {
const filter = this.filters[filterType].find((f) => f.key === key);
filter.checked = !filter.checked;
this.filtersChanged.emit(this.filters);
}
}

View File

@ -159,24 +159,21 @@
></div>
</div>
<div
[matTooltipPosition]="'above'"
[matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename"
>
<div class="filename-wrapper">
<div
[class.disabled]="isPending(fileStatus) || isProcessing(fileStatus)"
[class.error]="isError(fileStatus)"
class="table-item-title"
>
{{ fileStatus.filename }}
</div>
<span
*ngIf="appStateService.fileNotUpToDateWithDictionary(fileStatus)"
class="pill"
translate="project-overview.new-rule.label"
></span>
<div>
<div
[class.disabled]="isPending(fileStatus) || isProcessing(fileStatus)"
[class.error]="isError(fileStatus)"
class="table-item-title"
[matTooltipPosition]="'above'"
[matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename"
>
{{ fileStatus.filename }}
</div>
<span
*ngIf="appStateService.fileNotUpToDateWithDictionary(fileStatus)"
class="pill"
translate="project-overview.new-rule.label"
></span>
</div>
<div>
@ -355,6 +352,8 @@
<redaction-project-details
#projectDetailsComponent
(reloadProjects)="reloadProjects()"
[filters]="detailsContainerFilters"
(filtersChanged)="filtersChanged($event)"
></redaction-project-details>
</div>
</div>

View File

@ -57,13 +57,6 @@
.status-container {
align-items: flex-end;
}
.filename-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
}
}

View File

@ -43,6 +43,11 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
public displayedFiles: FileStatusWrapper[] = [];
public detailsContainerFilters: {
needsWorkFilters: FilterModel[];
statusFilters: FilterModel[];
};
@ViewChild('projectDetailsComponent', { static: false })
private _projectDetailsComponent: ProjectDetailsComponent;
@ -313,7 +318,14 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.needsWorkFilters = needsWorkFilters;
}
filtersChanged() {
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();
}
@ -327,6 +339,10 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this.appStateService.activeProject.files,
filters
);
this.detailsContainerFilters = {
needsWorkFilters: this.needsWorkFilters.map((f) => ({ ...f })),
statusFilters: this.statusFilters.map((f) => ({ ...f }))
};
this._changeDetectorRef.detectChanges();
}

View File

@ -226,7 +226,7 @@
},
"project-details": {
"charts": {
"total-documents": "Total Documents"
"documents-in-project": "Documents in Project"
},
"stats": {
"documents": "{{count}} documents",
@ -241,9 +241,9 @@
"upload-document": "Upload Document",
"no-project": "Requested project: {{projectId}} does not exist! <a href='/ui/projects'>Back to Project Listing. <a/>",
"legend": {
"contains-hints": "Hints only ",
"contains-redactions": "Redacted ",
"contains-suggestions": "Suggested Redaction "
"hints": "Hints only",
"redactions": "Redacted",
"requests": "Redaction requests"
}
},
"file-preview": {

View File

@ -1,96 +1,96 @@
{
"name": "redaction",
"version": "0.0.123",
"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.123",
"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.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"
}
}