Base listing (filter, search, select) for project listing & overview
This commit is contained in:
parent
45f7fe6e44
commit
c7d76e3141
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user