Base listing (filter, search, select) for project listing & overview

This commit is contained in:
Adina Țeudan 2021-04-19 00:18:22 +03:00
parent 45f7fe6e44
commit c7d76e3141
6 changed files with 239 additions and 205 deletions

View File

@ -48,7 +48,7 @@
<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>
@ -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,7 +7,6 @@ import { groupBy } from '../../../../utils/functions';
import { FilterModel } from '../../../shared/components/filter/model/filter.model';
import {
annotationFilterChecker,
getFilteredEntities,
processFilters,
projectMemberChecker,
projectStatusChecker,
@ -23,21 +22,19 @@ 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 {
public projectsChartData: DoughnutChartConfig[] = [];
public documentsChartData: DoughnutChartConfig[] = [];
public searchForm: FormGroup;
public actionMenuOpen: boolean;
public statusFilters: FilterModel[];
@ -51,7 +48,6 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
statusFilters: []
};
public displayedProjects: ProjectWrapper[] = [];
private projectAutoUpdateTimer: Subscription;
@ViewChild('statusFilter') private _statusFilterComponent: FilterComponent;
@ -60,36 +56,33 @@ 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 +91,30 @@ 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;
}
protected get _searchKey() {
return 'name';
}
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({
@ -150,12 +136,12 @@ export class ProjectListingScreenComponent implements OnInit, OnDestroy {
return this.sortingService.getSortingOption('project-listing');
}
public get activeProjects() {
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0);
public get activeProjectsCount() {
return this.allEntities.filter((p) => p.project.status === Project.StatusEnum.ACTIVE).length;
}
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 +157,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 +179,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 +234,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 +252,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,7 +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 { annotationFilterChecker, keyChecker, processFilters } from '../../../shared/components/filter/utils/filter-utils';
import { SortingOption, SortingService } from '../../../../services/sorting.service';
import { PermissionsService } from '../../../../services/permissions.service';
import { UserService } from '../../../../services/user.service';
@ -21,27 +21,25 @@ 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 {
public statusFilters: FilterModel[];
public peopleFilters: FilterModel[];
public needsWorkFilters: FilterModel[];
public collapsedDetails = false;
public searchForm: FormGroup;
displayedFiles: FileStatusWrapper[] = [];
detailsContainerFilters: {
needsWorkFilters: FilterModel[];
statusFilters: FilterModel[];
@ -56,7 +54,7 @@ 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,
@ -67,23 +65,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 +86,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 +100,20 @@ 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));
protected get _searchKey() {
return 'filename';
}
protected get _selectionKey() {
return 'fileId';
}
public get activeProject(): ProjectWrapper {
return this._appStateService.activeProject;
}
public reanalyseProject() {
return this.appStateService
return this._appStateService
.reanalyzeProject()
.then(() => {
this.reloadProjects();
@ -151,76 +152,38 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
this._sortingService.toggleSort('project-overview', $event);
}
public get hasActiveFilters() {
return (
this._statusFilterComponent?.hasActiveFilters ||
this._peopleFilterComponent?.hasActiveFilters ||
this._needsWorkFilterComponent?.hasActiveFilters ||
this.searchForm.get('query').value
);
protected get filterComponents(): FilterComponent[] {
return [this._statusFilterComponent, this._peopleFilterComponent, this._needsWorkFilterComponent];
}
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 +193,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 +205,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
private _computeAllFilters() {
if (!this.appStateService.activeProject) {
if (!this.activeProject) {
return;
}
@ -252,16 +215,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 +273,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 +288,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,132 @@
import { ChangeDetectorRef, Component, Inject, 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';
// Filter, search, select
@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;
// ----
// Overwrite in child class:
protected get _searchKey(): string {
throw new Error('Not implemented.');
}
protected get _selectionKey(): string {
throw new Error('Not implemented.');
}
protected get filters(): { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] {
throw new Error('Not implemented.');
}
protected preFilter() {
throw new Error('Not implemented.');
}
protected get filterComponents(): FilterComponent[] {
throw new Error('Not implemented.');
}
// ----
constructor(protected readonly _injector: Injector) {
this._formBuilder = this._injector.get<FormBuilder>(FormBuilder);
this._changeDetectorRef = this._injector.get<ChangeDetectorRef>(ChangeDetectorRef);
this._initSearch();
}
private _initSearch() {
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe(() => this._executeSearch());
}
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();
}
private get _filteredEntities(): T[] {
const filters = this.filters;
return getFilteredEntities(this.allEntities, filters);
}
protected _filterEntities() {
this.preFilter();
this.filteredEntities = this._filteredEntities;
this.selectedEntitiesIds = this.displayedEntities.map((entity) => entity[this._selectionKey]).filter((id) => this.selectedEntitiesIds.includes(id));
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;
}
@debounce(200)
protected _executeSearch() {
this.displayedEntities = this.filteredEntities.filter((entity) =>
entity[this._searchKey].toLowerCase().includes(this.searchForm.get('query').value.toLowerCase())
);
}
toggleEntitySelected($event: MouseEvent, entity: T) {
console.log({ entity, key: entity[this._selectionKey] });
$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;
}
}

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
];