Pull request #264: Common lib

Merge in RED/ui from common-lib to master

* commit 'd8e758ff147eb9331808ab51dc9cd0fee924974d':
  refactor file status
  update restore last scrolled position
  dossier wrapper refactor
  move status bar to common lib
  move table header to common lib
  move quick filters
  remove withSort, rename column to sortByKey
This commit is contained in:
Dan Percic 2021-08-11 19:06:06 +02:00 committed by Timo Bejan
commit 3f3ddb019c
81 changed files with 463 additions and 1036 deletions

View File

@ -12,7 +12,7 @@
[routerLink]="'/main/dossiers/' + appStateService.activeDossierId"
mat-menu-item
>
{{ appStateService.activeDossier.dossier.dossierName }}
{{ appStateService.activeDossier.dossierName }}
</button>
<button
*ngIf="appStateService.activeFile"
@ -44,7 +44,7 @@
class="breadcrumb"
routerLinkActive="active"
>
{{ appStateService.activeDossier.dossier.dossierName }}
{{ appStateService.activeDossier.dossierName }}
</a>
<mat-icon *ngIf="appStateService.activeFile" svgIcon="red:arrow-right"></mat-icon>
<a

View File

@ -3,13 +3,13 @@
<div class="red-content-inner">
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[hasEmptyColumn]="true"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"

View File

@ -1,23 +1,55 @@
import { FileAttributesConfig, FileStatus } from '@redaction/red-ui-http';
import { StatusSorter } from '@utils/sorters/status-sorter';
export class FileStatusWrapper {
const processingStatuses = [
FileStatus.StatusEnum.REPROCESS,
FileStatus.StatusEnum.FULLREPROCESS,
FileStatus.StatusEnum.OCRPROCESSING,
FileStatus.StatusEnum.INDEXING,
FileStatus.StatusEnum.PROCESSING
] as const;
export class FileStatusWrapper implements FileStatus {
readonly added = this.fileStatus.added;
readonly allManualRedactionsApplied = this.fileStatus.allManualRedactionsApplied;
readonly analysisDuration = this.fileStatus.analysisDuration;
readonly analysisRequired = this.fileStatus.analysisRequired && !this.fileStatus.excluded;
readonly approvalDate = this.fileStatus.approvalDate;
currentReviewer = this.fileStatus.currentReviewer;
readonly dictionaryVersion = this.fileStatus.dictionaryVersion;
readonly dossierDictionaryVersion = this.fileStatus.dossierDictionaryVersion;
readonly dossierId = this.fileStatus.dossierId;
readonly excluded = this.fileStatus.excluded;
readonly fileAttributes = this.fileStatus.fileAttributes;
readonly fileId = this.fileStatus.fileId;
readonly filename = this.fileStatus.filename;
readonly hasAnnotationComments = this.fileStatus.hasAnnotationComments;
readonly hasHints = this.fileStatus.hasHints;
readonly hasImages = this.fileStatus.hasImages;
readonly hasRedactions = this.fileStatus.hasRedactions;
readonly hasRequests = this.fileStatus.hasRequests;
readonly hasUpdates = this.fileStatus.hasUpdates;
readonly lastOCRTime = this.fileStatus.lastOCRTime;
readonly lastProcessed = this.fileStatus.lastProcessed;
readonly lastReviewer = this.fileStatus.lastReviewer;
readonly lastUpdated = this.fileStatus.lastUpdated;
readonly lastUploaded = this.fileStatus.lastUploaded;
readonly legalBasisVersion = this.fileStatus.legalBasisVersion;
readonly numberOfAnalyses = this.fileStatus.numberOfAnalyses;
readonly numberOfPages = this.fileStatus.numberOfPages;
readonly rulesVersion = this.fileStatus.rulesVersion;
readonly status = this._status;
readonly uploader = this.fileStatus.uploader;
readonly excludedPages = this.fileStatus.excludedPages;
primaryAttribute: string;
searchField: string;
constructor(
public fileStatus: FileStatus,
public reviewerName: string,
public dossierTemplateId: string,
fileAttributesConfig?: FileAttributesConfig
) {
this.searchField = fileStatus.filename;
constructor(readonly fileStatus: FileStatus, public reviewerName: string, fileAttributesConfig?: FileAttributesConfig) {
if (fileAttributesConfig) {
const primary = fileAttributesConfig.fileAttributeConfigs?.find(c => c.primaryAttribute);
if (primary && fileStatus.fileAttributes?.attributeIdToValue) {
this.primaryAttribute = fileStatus.fileAttributes?.attributeIdToValue[primary.id];
this.searchField += ' ' + this.primaryAttribute;
this.filename += ' ' + this.primaryAttribute;
}
if (!this.primaryAttribute) {
@ -27,178 +59,40 @@ export class FileStatusWrapper {
}
}
get analysisDuration() {
return this.fileStatus.analysisDuration;
readonly excludedPagesCount = this.excludedPages?.length ?? 0;
readonly statusSort = StatusSorter[this.status];
readonly pages = this._pages;
readonly cacheIdentifier = btoa(this.lastUploaded + this.lastOCRTime);
readonly hasUnappliedSuggestions = !this.allManualRedactionsApplied;
readonly hintsOnly = this.hasHints && !this.hasRedactions;
readonly hasNone = !this.hasRedactions && !this.hasHints && !this.hasRequests;
readonly isError = this.status === FileStatus.StatusEnum.ERROR;
readonly isProcessing = processingStatuses.includes(this.status);
readonly isApproved = this.status === FileStatus.StatusEnum.APPROVED;
readonly isPending = this.status === FileStatus.StatusEnum.UNPROCESSED;
readonly isUnderReview = this.status === FileStatus.StatusEnum.UNDERREVIEW;
readonly isUnderApproval = this.status === FileStatus.StatusEnum.UNDERAPPROVAL;
readonly canBeApproved = !this.analysisRequired && !this.hasRequests;
readonly canBeOpened = !this.isError && !this.isPending;
readonly isWorkable = !this.isProcessing && this.canBeOpened;
readonly canBeOCRed = !this.excluded && !this.lastOCRTime && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(this.status);
get isUnassigned() {
return !this.currentReviewer;
}
get lastProcessed() {
return this.fileStatus.lastProcessed;
}
get added() {
return this.fileStatus.added;
}
get lastUploaded() {
return this.fileStatus.lastUploaded;
}
get hasImages() {
return this.fileStatus.hasImages;
}
get hasUpdates() {
return this.fileStatus.hasUpdates && !this.hasRequests;
}
get hasUnappliedSuggestions() {
return !this.fileStatus.allManualRedactionsApplied;
}
get currentReviewer() {
return this.fileStatus.currentReviewer;
}
set currentReviewer(value: string) {
this.fileStatus.currentReviewer = value;
}
get fileId() {
return this.fileStatus.fileId;
}
get filename() {
return this.fileStatus.filename;
}
get hasAnnotationComments() {
// return this.fileStatus.hasAnnotationComments;
// TODO remove this once backend works properly
return false;
}
get ocrTime() {
return this.fileStatus.lastOCRTime;
}
get hasHints() {
return this.fileStatus.hasHints;
}
get hintsOnly() {
return this.fileStatus.hasHints && !this.fileStatus.hasRedactions;
}
get hasRedactions() {
return this.fileStatus.hasRedactions;
}
get hasRequests() {
return this.fileStatus.hasRequests || this.hasUnappliedSuggestions;
}
get hasNone() {
return !this.hasRedactions && !this.hasHints && !this.hasRequests;
}
get lastUpdated() {
return this.fileStatus.lastUpdated;
}
get numberOfAnalyses() {
return this.fileStatus.numberOfAnalyses;
}
get dossierId() {
return this.fileStatus.dossierId;
}
get isExcluded() {
return this.fileStatus.excluded;
}
get status() {
return this.fileStatus.status === 'REPROCESS' || this.fileStatus.status === 'FULLREPROCESS' ? 'PROCESSING' : this.fileStatus.status;
}
get numberOfPages() {
return this.fileStatus.numberOfPages;
}
get numberOfExcludedPages() {
return this.fileStatus.excludedPages?.length || 0;
}
get uploader() {
return this.fileStatus.uploader;
}
get isPending() {
return this.status === FileStatus.StatusEnum.UNPROCESSED;
}
get isProcessing() {
return [
FileStatus.StatusEnum.REPROCESS,
FileStatus.StatusEnum.FULLREPROCESS,
FileStatus.StatusEnum.OCRPROCESSING,
FileStatus.StatusEnum.INDEXING,
FileStatus.StatusEnum.PROCESSING
].includes(this.status);
}
get analysisRequired() {
return this.fileStatus.analysisRequired && !this.fileStatus.excluded;
}
get statusSort() {
return StatusSorter[this.status];
}
get isWorkable() {
return !this.isProcessing && !this.isPending && !this.isError;
}
get isApproved() {
return this.fileStatus.status === 'APPROVED';
}
get isError() {
return this.fileStatus.status === 'ERROR';
}
get pages() {
private get _pages() {
if (this.fileStatus.status === 'ERROR') {
return -1;
}
return this.fileStatus.numberOfPages ? this.fileStatus.numberOfPages : 0;
}
get isApprovedOrUnderApproval() {
return this.status === 'APPROVED' || this.status === 'UNDER_APPROVAL';
}
get isUnassigned() {
return !this.currentReviewer;
}
get isUnderReview() {
return this.fileStatus.status === 'UNDER_REVIEW';
}
get isUnderApproval() {
return this.fileStatus.status === 'UNDER_APPROVAL';
}
get canApprove() {
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
}
get cacheIdentifier() {
return btoa(this.fileStatus.lastUploaded + this.fileStatus.lastOCRTime);
}
get excludedPages(): number[] {
return this.fileStatus.excludedPages;
private get _status(): FileStatus.StatusEnum {
return this.fileStatus.status === FileStatus.StatusEnum.REPROCESS || this.fileStatus.status === FileStatus.StatusEnum.FULLREPROCESS
? FileStatus.StatusEnum.PROCESSING
: this.fileStatus.status;
}
}

View File

@ -1,7 +1,7 @@
<div class="action-buttons">
<iqser-circle-button
(action)="openDeleteDossierTemplateDialog($event)"
*ngIf="permissionsService.isAdmin()"
*ngIf="currentUser.isAdmin"
[tooltip]="'dossier-templates-listing.action.delete' | translate"
icon="red:trash"
[type]="circleButtonTypes.dark"
@ -9,7 +9,7 @@
<iqser-circle-button
(action)="openEditDossierTemplateDialog($event)"
*ngIf="permissionsService.isAdmin()"
*ngIf="currentUser.isAdmin"
[tooltip]="'dossier-templates-listing.action.edit' | translate"
icon="red:edit"
[type]="circleButtonTypes.dark"

View File

@ -1,34 +1,35 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { Router } from '@angular/router';
import { AdminDialogService } from '../../services/admin-dialog.service';
import { DossierTemplateControllerService } from '@redaction/red-ui-http';
import { LoadingService } from '@services/loading.service';
import { CircleButtonTypes } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
@Component({
selector: 'redaction-dossier-template-actions',
templateUrl: './dossier-template-actions.component.html',
styleUrls: ['./dossier-template-actions.component.scss']
})
export class DossierTemplateActionsComponent {
export class DossierTemplateActionsComponent implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@Input() dossierTemplateId: string;
@Output() loadDossierTemplatesData = new EventEmitter<any>();
constructor(
private readonly _dialogService: AdminDialogService,
private readonly _appStateService: AppStateService,
private readonly _dossierTemplateControllerService: DossierTemplateControllerService,
private readonly _loadingService: LoadingService,
private readonly _router: Router,
readonly permissionsService: PermissionsService
) {
if (!this.dossierTemplateId) {
this.dossierTemplateId = this._appStateService.activeDossierTemplateId;
}
private readonly _userService: UserService,
private readonly _loadingService: LoadingService,
private readonly _appStateService: AppStateService,
private readonly _dialogService: AdminDialogService,
private readonly _dossierTemplateControllerService: DossierTemplateControllerService
) {}
ngOnInit() {
this.dossierTemplateId ??= this._appStateService.activeDossierTemplateId;
}
get dossierTemplate() {
@ -48,9 +49,7 @@ export class DossierTemplateActionsComponent {
await this._appStateService.loadAllDossierTemplates();
await this._appStateService.loadDictionaryData();
await this._router.navigate(['main', 'admin']);
if (this.loadDossierTemplatesData) {
this.loadDossierTemplatesData.emit();
}
this.loadDossierTemplatesData?.emit();
});
}
}

View File

@ -1,10 +1,10 @@
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"

View File

@ -8,7 +8,7 @@ iqser-table-column-name::ng-deep > div {
}
}
redaction-table-header::ng-deep .header-item {
iqser-table-header::ng-deep .header-item {
padding: 0 24px 0 10px;
box-shadow: none;
border-top: 1px solid $separator;

View File

@ -86,7 +86,7 @@
</div>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<div class="table-header" iqserSyncWidth="table-item">
<iqser-table-column-name
[label]="'audit-screen.table-col-names.message' | translate"
column="message"

View File

@ -20,11 +20,11 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<redaction-table-header
<iqser-table-header
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<div *cdkVirtualFor="let color of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">

View File

@ -30,8 +30,7 @@ export class DefaultColorsScreenComponent extends ListingComponent<ListItem> imp
readonly tableColumnConfigs: TableColumnConfig<ListItem>[] = [
{
label: _('default-colors-screen.table-col-names.key'),
withSort: true,
column: 'key'
sortByKey: 'key'
},
{ label: _('default-colors-screen.table-col-names.color'), class: 'flex-center' }
];

View File

@ -20,13 +20,13 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
(action)="openAddEditDictionaryDialog()"

View File

@ -1,7 +1,7 @@
@import '../../../../../assets/styles/variables';
@import '../../../../../assets/styles/red-mixins';
redaction-table-header::ng-deep .header-item {
iqser-table-header::ng-deep .header-item {
padding-right: 16px;
}

View File

@ -34,13 +34,11 @@ export class DictionaryListingScreenComponent extends ListingComponent<TypeValue
readonly tableColumnConfigs: TableColumnConfig<TypeValueWrapper>[] = [
{
label: _('dictionary-listing.table-col-names.type'),
withSort: true,
column: 'label'
sortByKey: 'label'
},
{
label: _('dictionary-listing.table-col-names.order-of-importance'),
withSort: true,
column: 'rank',
sortByKey: 'rank',
class: 'flex-center'
},
{

View File

@ -5,7 +5,7 @@
<div class="actions">
<iqser-circle-button
(action)="openDeleteDictionaryDialog($event)"
*ngIf="permissionsService.isAdmin()"
*ngIf="currentUser.isAdmin"
[tooltip]="'dictionary-overview.action.delete' | translate"
icon="red:trash"
tooltipPosition="below"
@ -14,7 +14,7 @@
<iqser-circle-button
(action)="openEditDictionaryDialog($event)"
*ngIf="permissionsService.isAdmin()"
*ngIf="currentUser.isAdmin"
[tooltip]="'dictionary-overview.action.edit' | translate"
icon="red:edit"
tooltipPosition="below"
@ -30,7 +30,7 @@
<iqser-circle-button
(action)="fileInput.click()"
*ngIf="permissionsService.isAdmin()"
*ngIf="currentUser.isAdmin"
[tooltip]="'dictionary-overview.action.upload' | translate"
icon="red:upload"
tooltipPosition="below"
@ -56,7 +56,7 @@
<redaction-dictionary-manager
#dictionaryManager
(saveDictionary)="saveEntries($event)"
[canEdit]="permissionsService.isAdmin()"
[canEdit]="currentUser.isAdmin"
[initialEntries]="entries"
></redaction-dictionary-manager>

View File

@ -1,7 +1,6 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { saveAs } from 'file-saver';
@ -12,6 +11,7 @@ import { DictionarySaveService } from '@shared/services/dictionary-save.service'
import { TypeValueWrapper } from '@models/file/type-value.wrapper';
import { LoadingService } from '@services/loading.service';
import { CircleButtonTypes } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
@Component({
templateUrl: './dictionary-overview-screen.component.html',
@ -19,33 +19,32 @@ import { CircleButtonTypes } from '@iqser/common-ui';
})
export class DictionaryOverviewScreenComponent extends ComponentHasChanges implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
entries: string[] = [];
dictionary: TypeValueWrapper;
@ViewChild('dictionaryManager', { static: false })
private readonly _dictionaryManager: DictionaryManagerComponent;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
constructor(
readonly permissionsService: PermissionsService,
private readonly _router: Router,
private readonly _userService: UserService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _loadingService: LoadingService,
private readonly _appStateService: AppStateService,
private readonly _dialogService: AdminDialogService,
protected readonly _translateService: TranslateService,
private readonly _dictionarySaveService: DictionarySaveService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _dialogService: AdminDialogService,
private readonly _router: Router,
private readonly _activatedRoute: ActivatedRoute,
private readonly _appStateService: AppStateService,
private readonly _loadingService: LoadingService
private readonly _dictionaryControllerService: DictionaryControllerService
) {
super(_translateService);
this._appStateService.activateDictionary(
this._activatedRoute.snapshot.params.type,
this._activatedRoute.snapshot.params.dossierTemplateId
);
}
get dictionary(): TypeValueWrapper {
return this._appStateService.activeDictionary;
this.dictionary = this._appStateService.activeDictionary;
}
get hasChanges() {
@ -67,6 +66,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple
async () => {
this._loadingService.start();
await this._appStateService.loadDictionaryData();
this.dictionary = this._appStateService.activeDictionary;
this._loadingService.stop();
}
);

View File

@ -20,13 +20,13 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
(action)="openAddEditAttributeDialog($event)"

View File

@ -25,14 +25,12 @@ export class DossierAttributesListingScreenComponent extends ListingComponent<Do
readonly tableColumnConfigs: TableColumnConfig<DossierAttributeConfig>[] = [
{
label: _('dossier-attributes-listing.table-col-names.label'),
withSort: true,
column: 'label'
sortByKey: 'label'
},
{ label: _('dossier-attributes-listing.table-col-names.placeholder') },
{
label: _('dossier-attributes-listing.table-col-names.type'),
withSort: true,
column: 'type'
sortByKey: 'type'
}
];

View File

@ -8,7 +8,7 @@
<div class="red-content-inner">
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
@ -29,7 +29,7 @@
icon="red:plus"
></iqser-icon-button>
</div>
</redaction-table-header>
</iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"

View File

@ -24,19 +24,16 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
tableColumnConfigs: TableColumnConfig<DossierTemplateModelWrapper>[] = [
{
label: _('dossier-templates-listing.table-col-names.name'),
withSort: true,
column: 'name'
sortByKey: 'name'
},
{ label: _('dossier-templates-listing.table-col-names.created-by'), class: 'user-column' },
{
label: _('dossier-templates-listing.table-col-names.created-on'),
withSort: true,
column: 'dateAdded'
sortByKey: 'dateAdded'
},
{
label: _('dossier-templates-listing.table-col-names.modified-on'),
withSort: true,
column: 'dateModified'
sortByKey: 'dateModified'
}
];

View File

@ -20,13 +20,13 @@
<redaction-admin-side-nav type="dossierTemplates"></redaction-admin-side-nav>
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"

View File

@ -5,7 +5,7 @@
justify-content: flex-end;
}
redaction-table-header::ng-deep .header-item {
iqser-table-header::ng-deep .header-item {
padding: 0 24px 0 10px;
}

View File

@ -25,18 +25,15 @@ export class FileAttributesListingScreenComponent extends ListingComponent<FileA
readonly tableColumnConfigs: TableColumnConfig<FileAttributeConfig>[] = [
{
label: _('file-attributes-listing.table-col-names.name'),
withSort: true,
column: 'label'
sortByKey: 'label'
},
{
label: _('file-attributes-listing.table-col-names.type'),
withSort: true,
column: 'type'
sortByKey: 'type'
},
{
label: _('file-attributes-listing.table-col-names.read-only'),
withSort: true,
column: 'editable',
sortByKey: 'editable',
class: 'flex-center'
},
{ label: _('file-attributes-listing.table-col-names.csv-column') },

View File

@ -7,10 +7,9 @@ import { saveAs } from 'file-saver';
import { ComponentHasChanges } from '@guards/can-deactivate.guard';
import { ActivatedRoute } from '@angular/router';
import { AppStateService } from '@state/app-state.service';
import { Debounce } from '../../../../utils/debounce';
import { Debounce, IconButtonTypes } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { LoadingService } from '../../../../services/loading.service';
import { IconButtonTypes } from '@iqser/common-ui';
import { LoadingService } from '@services/loading.service';
import ICodeEditor = monaco.editor.ICodeEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;

View File

@ -5,12 +5,12 @@
<div class="red-content-inner">
<div class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
*ngIf="entitiesService.noData$ | async"

View File

@ -1,5 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Dossier } from '@redaction/red-ui-http';
import { LoadingService } from '@services/loading.service';
import { AppConfigKey, AppConfigService } from '@app-config/app-config.service';
@ -29,11 +28,10 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
readonly circleButtonTypes = CircleButtonTypes;
readonly tableHeaderLabel = _('trash.table-header.title');
readonly canRestoreSelected$ = this._canRestoreSelected$;
readonly tableColumnConfigs: TableColumnConfig<DossierListItem>[] = [
readonly tableColumnConfigs: readonly TableColumnConfig<DossierListItem>[] = [
{
label: _('trash.table-col-names.name'),
withSort: true,
column: 'dossierName'
sortByKey: 'dossierName'
},
{
label: _('trash.table-col-names.owner'),
@ -41,25 +39,22 @@ export class TrashScreenComponent extends ListingComponent<DossierListItem> impl
},
{
label: _('trash.table-col-names.deleted-on'),
withSort: true,
column: 'softDeletedTime'
sortByKey: 'softDeletedTime'
},
{
label: _('trash.table-col-names.time-to-restore'),
withSort: true,
column: 'softDeletedTime'
sortByKey: 'softDeletedTime'
}
];
protected readonly _primaryKey = 'dossierName';
private readonly _deleteRetentionHours = this._appConfigService.getConfig(AppConfigKey.DELETE_RETENTION_HOURS);
constructor(
readonly permissionsService: PermissionsService,
protected readonly _injector: Injector,
private readonly _dossiersService: DossiersService,
private readonly _loadingService: LoadingService,
private readonly _adminDialogService: AdminDialogService,
private readonly _appConfigService: AppConfigService
private readonly _dossiersService: DossiersService,
private readonly _appConfigService: AppConfigService,
private readonly _adminDialogService: AdminDialogService
) {
super(_injector);
}

View File

@ -33,13 +33,13 @@
<div class="red-content-inner">
<div [class.extended]="collapsedDetails" class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[hasEmptyColumn]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state *ngIf="noMatch$ | async" [text]="'user-listing.no-match.title' | translate"></redaction-empty-state>

View File

@ -5,15 +5,14 @@ import { AppStateService } from '@state/app-state.service';
import { environment } from '@environments/environment';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Debounce } from '@utils/debounce';
import { Debounce, IconButtonTypes } from '@iqser/common-ui';
import { WatermarkControllerService, WatermarkModelRes } from '@redaction/red-ui-http';
import { Toaster } from '@services/toaster.service';
import { ActivatedRoute } from '@angular/router';
import { BASE_HREF } from '../../../../tokens';
import { stampPDFPage } from '../../../../utils/page-stamper';
import { stampPDFPage } from '@utils/page-stamper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { LoadingService } from '../../../../services/loading.service';
import { IconButtonTypes } from '@iqser/common-ui';
import { LoadingService } from '@services/loading.service';
export const DEFAULT_WATERMARK: WatermarkModelRes = {
text: null,

View File

@ -89,7 +89,7 @@ export class DossierOverviewBulkActionsComponent {
}
get canOcr() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canOcrFile(file), true);
return this.selectedFiles.reduce((acc, file) => acc && file.canBeOCRed, true);
}
get fileStatuses() {
@ -112,7 +112,7 @@ export class DossierOverviewBulkActionsComponent {
}
get canApprove() {
return this.selectedFiles.reduce((acc, file) => acc && this._permissionsService.canApprove(file), true);
return this.selectedFiles.reduce((acc, file) => acc && file.canBeApproved, true);
}
// Undo approval
@ -163,7 +163,7 @@ export class DossierOverviewBulkActionsComponent {
}
async reanalyse() {
const fileIds = this.selectedFiles.filter(file => this._permissionsService.fileRequiresReanalysis(file)).map(file => file.fileId);
const fileIds = this.selectedFiles.filter(file => file.analysisRequired).map(file => file.fileId);
this._performBulkAction(
this._reanalysisControllerService.reanalyzeFilesForDossier(fileIds, this._appStateService.activeDossier.dossierId)
);

View File

@ -26,7 +26,7 @@
<div class="section small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:folder"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.dossier' | translate: { dossierName: dossier.name } }}</span>
<span>{{ 'file-preview.tabs.document-info.details.dossier' | translate: { dossierName: dossier.dossierName } }}</span>
</div>
<div>
<mat-icon svgIcon="red:document"></mat-icon>
@ -36,11 +36,9 @@
<mat-icon svgIcon="red:calendar"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.created-on' | translate: { date: file.added | date: 'mediumDate' } }}</span>
</div>
<div *ngIf="dossier.dossier.dueDate">
<div *ngIf="dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span>{{
'file-preview.tabs.document-info.details.due' | translate: { date: dossier.dossier.dueDate | date: 'mediumDate' }
}}</span>
<span>{{ 'file-preview.tabs.document-info.details.due' | translate: { date: dossier.dueDate | date: 'mediumDate' } }}</span>
</div>
<div>
<mat-icon svgIcon="red:template"></mat-icon>

View File

@ -19,18 +19,18 @@
'dossier-overview.dossier-details.stats.created-on'
| translate
: {
date: activeDossier.dossier.date | date: 'd MMM. yyyy'
date: activeDossier.date | date: 'd MMM. yyyy'
}
}}
</span>
</div>
<div *ngIf="activeDossier.dossier.dueDate">
<div *ngIf="activeDossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span>{{
'dossier-overview.dossier-details.stats.due-date'
| translate
: {
date: activeDossier.dossier.dueDate | date: 'd MMM. yyyy'
date: activeDossier.dueDate | date: 'd MMM. yyyy'
}
}}</span>
</div>

View File

@ -5,7 +5,7 @@
</div>
<div class="header-wrapper mt-8">
<div class="heading-xl flex-1">{{ appStateService.activeDossier.dossier.dossierName }}</div>
<div class="heading-xl flex-1">{{ appStateService.activeDossier.dossierName }}</div>
<ng-container
*ngTemplateOutlet="collapsible; context: { action: 'collapse', tooltip: (collapseTooltip | translate) }"
></ng-container>
@ -65,7 +65,7 @@
></redaction-dossier-details-stats>
</div>
<div *ngIf="appStateService.activeDossier.dossier.description as description" class="pb-32">
<div *ngIf="appStateService.activeDossier.description as description" class="pb-32">
<div class="heading" translate="dossier-overview.dossier-details.description"></div>
<div class="mt-8">{{ description }}</div>
</div>

View File

@ -40,7 +40,7 @@ export class DossierDetailsComponent implements OnInit {
) {}
get memberIds(): string[] {
return this.appStateService.activeDossier.dossier.memberIds;
return this.appStateService.activeDossier.memberIds;
}
get hasFiles(): boolean {
@ -54,7 +54,7 @@ export class DossierDetailsComponent implements OnInit {
ngOnInit(): void {
this.owner = this._userService.getRedUserById(this.appStateService.activeDossier.ownerId);
this.calculateChartConfig();
this.appStateService.fileChanged.subscribe(() => {
this.appStateService.fileChanged$.subscribe(() => {
this.calculateChartConfig();
});
}
@ -81,12 +81,11 @@ export class DossierDetailsComponent implements OnInit {
async assignOwner(user: UserWrapper | string) {
this.owner = typeof user === 'string' ? this._userService.getRedUserById(user) : user;
const dw = Object.assign({}, this.appStateService.activeDossier);
dw.dossier.ownerId = this.owner.id;
await this.appStateService.createOrUpdateDossier(dw.dossier);
const dw = { ...this.appStateService.activeDossier, ownerId: this.owner.id };
await this.appStateService.createOrUpdateDossier(dw);
const ownerName = this._userService.getNameForId(this.owner.id);
const dossierName = this.appStateService.activeDossier.name;
const dossierName = this.appStateService.activeDossier.dossierName;
this._toaster.info(_('assignment.owner'), { params: { ownerName, dossierName } });
}
}

View File

@ -1,4 +1,4 @@
<redaction-status-bar [config]="getDossierStatusConfig(dossier)"></redaction-status-bar>
<iqser-status-bar [configs]="statusBarConfig"></iqser-status-bar>
<div class="action-buttons">
<iqser-circle-button
(action)="openEditDossierDialog($event, dossier)"

View File

@ -1,24 +1,28 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { AppStateService } from '@state/app-state.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { CircleButtonTypes } from '@iqser/common-ui';
import { CircleButtonTypes, StatusBarConfig } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
@Component({
selector: 'redaction-dossier-listing-actions',
templateUrl: './dossier-listing-actions.component.html',
styleUrls: ['./dossier-listing-actions.component.scss']
styleUrls: ['./dossier-listing-actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingActionsComponent {
export class DossierListingActionsComponent implements OnInit {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@Input() dossier: DossierWrapper;
@Output() actionPerformed = new EventEmitter<DossierWrapper | undefined>();
statusBarConfig: readonly StatusBarConfig<string>[];
constructor(
readonly permissionsService: PermissionsService,
readonly appStateService: AppStateService,
@ -26,6 +30,10 @@ export class DossierListingActionsComponent {
private readonly _userService: UserService
) {}
ngOnInit() {
this.statusBarConfig = this._getStatusConfig(this.dossier.files);
}
openEditDossierDialog($event: MouseEvent, dossierWrapper: DossierWrapper) {
this._dialogService.openDialog('editDossier', $event, {
dossierWrapper,
@ -40,8 +48,8 @@ export class DossierListingActionsComponent {
});
}
getDossierStatusConfig(dw: DossierWrapper) {
const obj = dw.files.reduce((acc, file) => {
private _getStatusConfig(files: readonly FileStatusWrapper[]) {
const obj = files.reduce((acc, file) => {
const status = file.status;
if (!acc[status]) {
acc[status] = 1;

View File

@ -1,6 +1,6 @@
<div *ngIf="screen === 'dossier-overview'" class="action-buttons">
<ng-container *ngTemplateOutlet="actions"></ng-container>
<redaction-status-bar *ngIf="fileStatus?.isWorkable" [config]="statusBarConfig"></redaction-status-bar>
<iqser-status-bar *ngIf="isWorkable" [configs]="statusBarConfig"></iqser-status-bar>
</div>
<ng-container *ngIf="screen === 'file-preview'">
@ -9,10 +9,9 @@
<ng-template #actions>
<div class="file-actions" *ngIf="fileStatus">
<!-- delete-->
<iqser-circle-button
(action)="openDeleteFileDialog($event)"
*ngIf="permissionsService.canDeleteFile(fileStatus)"
*ngIf="canDelete"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.delete.action' | translate"
[type]="buttonType"
@ -23,7 +22,7 @@
(action)="assign($event)"
*ngIf="canAssign && screen === 'dossier-overview'"
[tooltipPosition]="tooltipPosition"
[tooltip]="assignTooltip"
[tooltip]="assignTooltip | translate"
[type]="buttonType"
icon="red:assign"
></iqser-circle-button>
@ -88,13 +87,11 @@
<!-- Approved-->
<iqser-circle-button
(action)="setFileApproved($event)"
*ngIf="permissionsService.isReadyForApproval(fileStatus)"
[disabled]="!permissionsService.canApprove(fileStatus)"
*ngIf="readyForApproval"
[disabled]="!fileStatus.canBeApproved"
[tooltipPosition]="tooltipPosition"
[tooltip]="
permissionsService.canApprove(fileStatus)
? ('dossier-overview.approve' | translate)
: ('dossier-overview.approve-disabled' | translate)
fileStatus.canBeApproved ? ('dossier-overview.approve' | translate) : ('dossier-overview.approve-disabled' | translate)
"
[type]="buttonType"
icon="red:approved"
@ -112,7 +109,7 @@
<iqser-circle-button
(action)="ocrFile($event)"
*ngIf="canOcr"
*ngIf="fileStatus.canBeOCRed"
[tooltipPosition]="tooltipPosition"
[tooltip]="'dossier-overview.ocr-file' | translate"
[type]="buttonType"
@ -145,9 +142,9 @@
<mat-slide-toggle
(change)="toggleAnalysis()"
(click)="$event.stopPropagation()"
[checked]="!fileStatus?.isExcluded"
[checked]="!fileStatus?.excluded"
[class.mr-24]="screen === 'dossier-overview'"
[disabled]="!permissionsService.canToggleAnalysis(fileStatus)"
[disabled]="!canToggleAnalysis"
[matTooltipPosition]="tooltipPosition"
[matTooltip]="toggleTooltip | translate"
color="primary"

View File

@ -17,7 +17,7 @@
font-weight: bold;
}
redaction-status-bar {
iqser-status-bar {
margin-left: 2px;
}

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { AppStateService } from '@state/app-state.service';
@ -6,18 +6,18 @@ import { FileActionService } from '../../services/file-action.service';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { ConfirmationDialogInput } from '@shared/dialogs/confirmation-dialog/confirmation-dialog.component';
import { LoadingService } from '@services/loading.service';
import { FileManagementControllerService } from '@redaction/red-ui-http';
import { TranslateService } from '@ngx-translate/core';
import { FileManagementControllerService, FileStatus } from '@redaction/red-ui-http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { CircleButtonTypes } from '@iqser/common-ui';
import { AutoUnsubscribe, CircleButtonType, CircleButtonTypes, StatusBarConfig } from '@iqser/common-ui';
import { UserService } from '@services/user.service';
import { filter } from 'rxjs/operators';
@Component({
selector: 'redaction-file-actions',
templateUrl: './file-actions.component.html',
styleUrls: ['./file-actions.component.scss']
})
export class FileActionsComponent implements OnInit {
export class FileActionsComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
@ -27,6 +27,22 @@ export class FileActionsComponent implements OnInit {
@Output() actionPerformed = new EventEmitter<string>();
screen: 'file-preview' | 'dossier-overview';
statusBarConfig?: readonly StatusBarConfig<FileStatus.StatusEnum>[];
tooltipPosition?: 'below' | 'above';
toggleTooltip?: string;
assignTooltip?: string;
buttonType?: CircleButtonType;
isWorkable: boolean;
canUndoApproval: boolean;
canAssignToSelf: boolean;
canAssign: boolean;
canDelete: boolean;
canReanalyse: boolean;
canSetToUnderReview: boolean;
canSetToUnderApproval: boolean;
readyForApproval: boolean;
canToggleAnalysis: boolean;
constructor(
readonly permissionsService: PermissionsService,
@ -35,81 +51,57 @@ export class FileActionsComponent implements OnInit {
private readonly _fileActionService: FileActionService,
private readonly _loadingService: LoadingService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _translateService: TranslateService,
private readonly _userService: UserService
) {}
get statusBarConfig() {
return [{ color: this.fileStatus.status, length: 1 }];
) {
super();
}
get tooltipPosition() {
return this.screen === 'file-preview' ? 'below' : 'above';
}
get buttonType() {
return this.screen === 'file-preview' ? CircleButtonTypes.default : CircleButtonTypes.dark;
}
get toggleTooltip(): string {
private get _toggleTooltip(): string {
if (!this.currentUser.isManager) {
return _('file-preview.toggle-analysis.only-managers');
}
return this.fileStatus?.isExcluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
return this.fileStatus?.excluded ? _('file-preview.toggle-analysis.enable') : _('file-preview.toggle-analysis.disable');
}
get canAssignToSelf() {
return this.permissionsService.canAssignToSelf(this.fileStatus);
}
private _setup() {
this.statusBarConfig = [{ color: this.fileStatus.status, length: 1 }];
this.tooltipPosition = this.screen === 'file-preview' ? 'below' : 'above';
this.assignTooltip = this.fileStatus.isUnderApproval
? _('dossier-overview.assign-approver')
: _('dossier-overview.assign-reviewer');
this.buttonType = this.screen === 'file-preview' ? CircleButtonTypes.default : CircleButtonTypes.dark;
this.isWorkable = this.fileStatus.isWorkable;
this.toggleTooltip = this._toggleTooltip;
get canAssign() {
return this.permissionsService.canAssignUser(this.fileStatus);
}
get canDelete() {
return this.permissionsService.canDeleteFile(this.fileStatus);
}
get canReanalyse() {
return this.permissionsService.canReanalyseFile(this.fileStatus);
}
get canOcr() {
return this.permissionsService.canOcrFile(this.fileStatus);
}
get canSetToUnderReview() {
return this.permissionsService.canSetUnderReview(this.fileStatus);
}
get canSetToUnderApproval() {
return this.permissionsService.canSetUnderApproval(this.fileStatus);
}
get canUndoApproval() {
return this.permissionsService.canUndoApproval(this.fileStatus);
}
get assignTooltip() {
return this.fileStatus.isUnderApproval
? this._translateService.instant('dossier-overview.assign-approver')
: this._translateService.instant('dossier-overview.assign-reviewer');
this.canUndoApproval = this.permissionsService.canUndoApproval(this.fileStatus);
this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.fileStatus);
this.canAssign = this.permissionsService.canAssignUser(this.fileStatus);
this.canDelete = this.permissionsService.canDeleteFile(this.fileStatus);
this.canReanalyse = this.permissionsService.canReanalyseFile(this.fileStatus);
this.canSetToUnderReview = this.permissionsService.canSetUnderReview(this.fileStatus);
this.canSetToUnderApproval = this.permissionsService.canSetUnderApproval(this.fileStatus);
this.readyForApproval = this.permissionsService.isReadyForApproval(this.fileStatus);
this.canToggleAnalysis = this.permissionsService.canToggleAnalysis(this.fileStatus);
}
ngOnInit(): void {
if (this.fileStatus) {
this.screen = 'dossier-overview';
this._setup();
return;
}
this.fileStatus = this.appStateService.activeFile;
this.screen = 'file-preview';
this.appStateService.fileChanged.subscribe(fileStatus => {
if (fileStatus.fileId === this.fileStatus?.fileId) {
this.fileStatus = this.appStateService.activeFile;
}
});
this._setup();
this.addSubscription = this.appStateService.fileChanged$
.pipe(filter(file => file.fileId === this.fileStatus?.fileId))
.subscribe(fileStatus => {
this.fileStatus = fileStatus;
this._setup();
});
}
toggleViewDocumentInfo() {
@ -156,7 +148,7 @@ export class FileActionsComponent implements OnInit {
reanalyseFile($event: MouseEvent) {
$event.stopPropagation();
this._fileActionService.reanalyseFile(this.fileStatus).subscribe(() => {
this.addSubscription = this._fileActionService.reanalyseFile(this.fileStatus).subscribe(() => {
this.reloadDossiers('reanalyse');
});
}
@ -171,7 +163,7 @@ export class FileActionsComponent implements OnInit {
true
);
} else {
this._fileActionService.setFileUnderApproval(this.fileStatus).subscribe(() => {
this.addSubscription = this._fileActionService.setFileUnderApproval(this.fileStatus).subscribe(() => {
this.reloadDossiers('set-under-approval');
});
}
@ -179,14 +171,14 @@ export class FileActionsComponent implements OnInit {
setFileApproved($event: MouseEvent) {
$event.stopPropagation();
this._fileActionService.setFileApproved(this.fileStatus).subscribe(() => {
this.addSubscription = this._fileActionService.setFileApproved(this.fileStatus).subscribe(() => {
this.reloadDossiers('set-approved');
});
}
ocrFile($event: MouseEvent) {
$event.stopPropagation();
this._fileActionService.ocrFile(this.fileStatus).subscribe(() => {
this.addSubscription = this._fileActionService.ocrFile(this.fileStatus).subscribe(() => {
this.reloadDossiers('ocr-file');
});
}
@ -210,6 +202,6 @@ export class FileActionsComponent implements OnInit {
async toggleAnalysis() {
await this._fileActionService.toggleAnalysis(this.fileStatus).toPromise();
await this.appStateService.getFiles();
this.actionPerformed.emit(this.fileStatus?.isExcluded ? 'enable-analysis' : 'disable-analysis');
this.actionPerformed.emit(this.fileStatus?.excluded ? 'enable-analysis' : 'disable-analysis');
}
}

View File

@ -15,12 +15,11 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import { Debounce } from '@utils/debounce';
import { CircleButtonTypes, Debounce, IconButtonTypes, NestedFilter } from '@iqser/common-ui';
import { FileDataModel } from '@models/file/file-data.model';
import { CommentsComponent } from '../comments/comments.component';
import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { CircleButtonTypes, IconButtonTypes, NestedFilter } from '@iqser/common-ui';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];

View File

@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@ -12,7 +11,7 @@ import { DossierWrapper } from '@state/model/dossier.wrapper';
export class NeedsWorkBadgeComponent {
@Input() needsWorkInput: FileStatusWrapper | DossierWrapper;
constructor(private readonly _appStateService: AppStateService, private readonly _permissionsService: PermissionsService) {}
constructor(private readonly _appStateService: AppStateService) {}
get suggestionColor() {
return this._getDictionaryColor('suggestion');
@ -52,9 +51,9 @@ export class NeedsWorkBadgeComponent {
reanalysisRequired() {
if (this.needsWorkInput instanceof DossierWrapper) {
return this._permissionsService.dossierReanalysisRequired(this.needsWorkInput);
return this.needsWorkInput.reanalysisRequired;
} else {
return this._permissionsService.fileRequiresReanalysis(this.needsWorkInput);
return this.needsWorkInput.analysisRequired;
}
}

View File

@ -34,7 +34,7 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy {
}
ngOnInit(): void {
this._subscription = this._appStateService.fileChanged.subscribe(() => {
this._subscription = this._appStateService.fileChanged$.subscribe(() => {
this.canMarkPagesAsViewed = this._permissionService.canMarkPagesAsViewed();
});
}

View File

@ -82,11 +82,13 @@ export class TeamMembersManagerComponent implements OnInit {
const ownerId = this.selectedOwnerId;
const memberIds = this.selectedMembersList;
const approverIds = this.selectedApproversList;
const dw = Object.assign({}, this.dossierWrapper);
dw.dossier.memberIds = memberIds;
dw.dossier.approverIds = approverIds;
dw.dossier.ownerId = ownerId;
result = await this._appStateService.createOrUpdateDossier(dw.dossier);
const dw = {
...this.dossierWrapper,
memberIds,
approverIds,
ownerId
};
result = await this._appStateService.createOrUpdateDossier(dw);
this.save.emit(result);
} catch (error) {
this._toaster.error('Failed: ' + error.error ? error.error.message : error);

View File

@ -8,7 +8,6 @@ import { downloadTypesTranslations } from '../../../../translations/download-typ
import { IconButtonTypes } from '@iqser/common-ui';
@Component({
selector: 'redaction-add-dossier-dialog',
templateUrl: './add-dossier-dialog.component.html',
styleUrls: ['./add-dossier-dialog.component.scss']
})
@ -71,7 +70,7 @@ export class AddDossierDialogComponent {
async saveDossier() {
const dossier: Dossier = this._formToObject();
const foundDossier = this._appStateService.allDossiers.find(p => p.dossier.dossierId === dossier.dossierId);
const foundDossier = this._appStateService.allDossiers.find(p => p.dossierId === dossier.dossierId);
if (foundDossier) {
dossier.memberIds = foundDossier.memberIds;
}

View File

@ -6,7 +6,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@Component({
selector: 'redaction-document-info-dialog',
templateUrl: './document-info-dialog.component.html',
styleUrls: ['./document-info-dialog.component.scss']
})

View File

@ -43,11 +43,11 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
get changed() {
if (this.dossierForm) {
for (const key of Object.keys(this.dossierForm.getRawValue())) {
if (this.dossierWrapper.dossier[key].length !== this.dossierForm.get(key).value.length) {
if (this.dossierWrapper[key].length !== this.dossierForm.get(key).value.length) {
return true;
}
const originalItems = [...this.dossierWrapper.dossier[key]].sort();
const originalItems = [...this.dossierWrapper[key]].sort();
const newItems = [...this.dossierForm.get(key).value].sort();
for (let idx = 0; idx < originalItems.length; ++idx) {
@ -71,8 +71,8 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
this.dossierForm = this._formBuilder.group(
{
reportTemplateIds: [this.dossierWrapper.dossier.reportTemplateIds],
downloadFileTypes: [this.dossierWrapper.dossier.downloadFileTypes]
reportTemplateIds: [this.dossierWrapper.reportTemplateIds],
downloadFileTypes: [this.dossierWrapper.downloadFileTypes]
},
{
validators: control =>
@ -85,7 +85,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
async save() {
const dossier = {
...this.dossierWrapper.dossier,
...this.dossierWrapper,
downloadFileTypes: this.dossierForm.get('downloadFileTypes').value,
reportTemplateIds: this.dossierForm.get('reportTemplateIds').value
};
@ -95,8 +95,8 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
revert() {
this.dossierForm.reset({
downloadFileTypes: this.dossierWrapper.dossier.downloadFileTypes,
reportTemplateIds: this.dossierWrapper.dossier.reportTemplateIds
downloadFileTypes: this.dossierWrapper.downloadFileTypes,
reportTemplateIds: this.dossierWrapper.reportTemplateIds
});
}
}

View File

@ -1,6 +1,6 @@
<section class="dialog">
<div class="dialog-header heading-l">
{{ 'edit-dossier-dialog.header' | translate: { dossierName: dossierWrapper.name } }}
{{ 'edit-dossier-dialog.header' | translate: { dossierName: dossierWrapper.dossierName } }}
</div>
<div class="dialog-content">

View File

@ -94,7 +94,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti
async save() {
const dossier = {
...this.dossierWrapper.dossier,
...this.dossierWrapper,
dossierName: this.dossierForm.get('dossierName').value,
description: this.dossierForm.get('description').value,
watermarkEnabled: this.dossierForm.get('watermarkEnabled').value,

View File

@ -8,10 +8,7 @@
<div class="red-content-inner">
<div class="content-container">
<redaction-table-header
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
<iqser-table-header [tableColumnConfigs]="tableColumnConfigs" [tableHeaderLabel]="tableHeaderLabel"></iqser-table-header>
<redaction-empty-state
(action)="openAddDossierDialog()"
@ -26,54 +23,54 @@
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let dw of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.pointer]="!!dw"
[routerLink]="[!!dw ? '/main/dossiers/' + dw.dossier.dossierId : []]"
*cdkVirtualFor="let item of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.pointer]="!!item"
[routerLink]="[item.routerLink]"
class="table-item"
>
<div class="filename">
<div [matTooltip]="dw.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ dw.dossierName }}
<div [matTooltip]="item.dossierName" class="table-item-title heading" matTooltipPosition="above">
{{ item.dossierName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ getDossierTemplate(dw).name }}
{{ item.dossierTemplateName }}
</div>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ dw.files.length }}
{{ item.dossier.filesLength }}
</div>
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ dw.totalNumberOfPages }}
{{ item.dossier.totalNumberOfPages }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ dw.numberOfMembers }}
{{ item.dossier.memberCount }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ dw.dossier.date | date: 'mediumDate' }}
{{ item.dossier.date | date: 'mediumDate' }}
</div>
<div *ngIf="dw.dossier.dueDate">
<div *ngIf="item.dossier.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ dw.dossier.dueDate | date: 'mediumDate' }}
{{ item.dossier.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div>
<redaction-needs-work-badge [needsWorkInput]="dw"></redaction-needs-work-badge>
<redaction-needs-work-badge [needsWorkInput]="item.dossier"></redaction-needs-work-badge>
</div>
<div class="user-column">
<redaction-initials-avatar [userId]="dw.dossier.ownerId" [withName]="true"></redaction-initials-avatar>
<redaction-initials-avatar [userId]="item.dossier.ownerId" [withName]="true"></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-dossier-listing-actions
(actionPerformed)="calculateData()"
[dossier]="dw"
[dossier]="item.dossier"
></redaction-dossier-listing-actions>
</div>
<div class="scrollbar-placeholder"></div>

View File

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectorRef, Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AfterViewInit, Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DossierTemplateModel } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
@ -7,11 +7,11 @@ import { groupBy } from '@utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { timer } from 'rxjs';
import { filter } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
import { TranslateChartService } from '@services/translate-chart.service';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { NavigationStart, Router } from '@angular/router';
import { Router } from '@angular/router';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { UserPreferenceService } from '@services/user-preference.service';
@ -29,7 +29,12 @@ import {
import { PermissionsService } from '@services/permissions.service';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
const isLeavingScreen = event => event instanceof NavigationStart && event.url !== '/main/dossiers';
interface ListItem {
readonly dossierName: string;
readonly routerLink: string;
readonly dossierTemplateName: string;
readonly dossier: DossierWrapper;
}
@Component({
templateUrl: './dossier-listing-screen.component.html',
@ -37,13 +42,14 @@ const isLeavingScreen = event => event instanceof NavigationStart && event.url !
providers: [...DefaultListingServices]
})
export class DossierListingScreenComponent
extends ListingComponent<DossierWrapper>
extends ListingComponent<ListItem>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly itemSize = 85;
protected readonly _primaryKey = 'dossierName';
readonly currentUser = this._userService.currentUser;
readonly tableHeaderLabel = _('dossier-listing.table-header.title');
readonly buttonConfigs: ButtonConfig[] = [
readonly buttonConfigs: readonly ButtonConfig[] = [
{
label: _('dossier-listing.add-new'),
action: () => this.openAddDossierDialog(),
@ -52,11 +58,10 @@ export class DossierListingScreenComponent
type: 'primary'
}
];
readonly tableColumnConfigs: TableColumnConfig<DossierWrapper>[] = [
readonly tableColumnConfigs: readonly TableColumnConfig<ListItem>[] = [
{
label: _('dossier-listing.table-col-names.name'),
withSort: true,
column: 'dossierName'
sortByKey: 'dossierName'
},
{
label: _('dossier-listing.table-col-names.needs-work')
@ -70,10 +75,11 @@ export class DossierListingScreenComponent
class: 'flex-end'
}
];
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
protected readonly _primaryKey = 'dossierName';
private _lastScrollPosition: number;
private _lastScrolledIndex: number;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
@ViewChild(CdkVirtualScrollViewport)
@ -83,7 +89,6 @@ export class DossierListingScreenComponent
private readonly _router: Router,
protected readonly _injector: Injector,
private readonly _userService: UserService,
readonly changeDetectorRef: ChangeDetectorRef,
readonly permissionsService: PermissionsService,
private readonly _appStateService: AppStateService,
private readonly _translateService: TranslateService,
@ -96,10 +101,6 @@ export class DossierListingScreenComponent
this._loadEntitiesFromState();
}
private get _userId() {
return this._userService.currentUser.id;
}
private get _activeDossiersCount(): number {
return this.entitiesService.all.filter(p => p.dossier.status === Dossier.StatusEnum.ACTIVE).length;
}
@ -117,32 +118,29 @@ export class DossierListingScreenComponent
this.calculateData();
});
this.addSubscription = this._appStateService.fileChanged.subscribe(() => {
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
this.addSubscription = this._router.events.pipe(filter(isLeavingScreen)).subscribe(() => {
this._lastScrollPosition = this._scrollViewport.measureScrollOffset('top');
});
}
ngAfterViewInit() {
this.changeDetectorRef.detectChanges();
this.addSubscription = this._scrollViewport.scrolledIndexChange.pipe(tap(index => (this._lastScrolledIndex = index))).subscribe();
}
ngOnAttach() {
this._scrollViewport.scrollTo({ top: this._lastScrollPosition });
this._appStateService.reset();
this._loadEntitiesFromState();
this.ngOnInit();
this.ngAfterViewInit();
this._scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach(): void {
this.ngOnDestroy();
}
getDossierTemplate(dw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId);
private _getDossierTemplate(dossierTemplateId: string): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dossierTemplateId);
}
openAddDossierDialog(): void {
@ -180,7 +178,17 @@ export class DossierListingScreenComponent
}
private _loadEntitiesFromState() {
this.entitiesService.setEntities(this._appStateService.allDossiers);
const entities = this._appStateService.allDossiers.map(dossier => this._toListItem(dossier));
this.entitiesService.setEntities(entities);
}
private _toListItem(dossier: DossierWrapper): ListItem {
return {
dossierName: dossier.dossierName,
routerLink: '/main/dossiers/' + dossier.dossierId.toString(),
dossierTemplateName: this._getDossierTemplate(dossier.dossierTemplateId).name,
dossier
};
}
private _computeAllFilters() {
@ -189,20 +197,20 @@ export class DossierListingScreenComponent
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
this.entitiesService?.all?.forEach(entry => {
this.entitiesService.all?.forEach(entry => {
// all people
entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f));
// Needs work
entry.files.forEach(file => {
entry.dossier.files.forEach(file => {
allDistinctFileStatus.add(file.status);
if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis');
if (entry.hintsOnly) allDistinctNeedsWork.add('hint');
if (entry.hasRedactions) allDistinctNeedsWork.add('redaction');
if (entry.hasRequests) allDistinctNeedsWork.add('suggestion');
if (entry.hasNone) allDistinctNeedsWork.add('none');
if (file.analysisRequired) allDistinctNeedsWork.add('analysis');
if (entry.dossier.hintsOnly) allDistinctNeedsWork.add('hint');
if (entry.dossier.hasRedactions) allDistinctNeedsWork.add('redaction');
if (entry.dossier.hasRequests) allDistinctNeedsWork.add('suggestion');
if (entry.dossier.hasNone) allDistinctNeedsWork.add('none');
});
allDistinctDossierTemplates.add(entry.dossierTemplateId);
allDistinctDossierTemplates.add(entry.dossier.dossierTemplateId);
});
const statusFilters = [...allDistinctFileStatus].map<NestedFilter>(status => ({
@ -243,8 +251,7 @@ export class DossierListingScreenComponent
filterTemplate: this._needsWorkTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: [this.permissionsService]
matchAll: true
});
const dossierTemplateFilters = [...allDistinctDossierTemplates].map<NestedFilter>(id => ({
@ -275,22 +282,22 @@ export class DossierListingScreenComponent
{
key: 'my-dossiers',
label: myDossiersLabel,
checker: (dw: DossierWrapper) => dw.ownerId === this._userId
checker: (dw: DossierWrapper) => dw.ownerId === this.currentUser.id
},
{
key: 'to-approve',
label: this._translateService.instant('dossier-listing.quick-filters.to-approve'),
checker: (dw: DossierWrapper) => dw.approverIds.includes(this._userId)
checker: (dw: DossierWrapper) => dw.approverIds.includes(this.currentUser.id)
},
{
key: 'to-review',
label: this._translateService.instant('dossier-listing.quick-filters.to-review'),
checker: (dw: DossierWrapper) => dw.memberIds.includes(this._userId)
checker: (dw: DossierWrapper) => dw.memberIds.includes(this.currentUser.id)
},
{
key: 'other',
label: this._translateService.instant('dossier-listing.quick-filters.other'),
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._userId)
checker: (dw: DossierWrapper) => !dw.memberIds.includes(this.currentUser.id)
}
];

View File

@ -1,4 +1,4 @@
<section *ngIf="!!activeDossier">
<section *ngIf="!!currentDossier">
<redaction-page-header
[actionConfigs]="actionConfigs"
[searchPlaceholder]="'dossier-overview.search' | translate"
@ -6,7 +6,7 @@
>
<redaction-file-download-btn
[disabled]="entitiesService.areSomeSelected$ | async"
[dossier]="activeDossier"
[dossier]="currentDossier"
[file]="entitiesService.all$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
@ -36,12 +36,12 @@
<div class="red-content-inner">
<div [class.extended]="collapsedDetails" class="content-container">
<redaction-table-header
<iqser-table-header
[bulkActions]="bulkActions"
[selectionEnabled]="true"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
></iqser-table-header>
<redaction-empty-state
(action)="fileInput.click()"
@ -57,9 +57,9 @@
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="let fileStatus of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
[class.disabled]="fileStatus.isExcluded"
[class.disabled]="fileStatus.excluded"
[class.last-opened]="isLastOpenedFile(fileStatus.fileId)"
[class.pointer]="permissionsService.canOpenFile(fileStatus)"
[class.pointer]="fileStatus.canBeOpened"
[routerLink]="fileLink(fileStatus)"
class="table-item"
>
@ -93,15 +93,15 @@
</div>
<div>
<mat-icon svgIcon="red:exclude-pages"></mat-icon>
{{ fileStatus.numberOfExcludedPages }}
{{ fileStatus.excludedPagesCount }}
</div>
<div
*ngIf="fileStatus.ocrTime"
*ngIf="fileStatus.lastOCRTime"
[matTooltipPosition]="'above'"
[matTooltip]="'dossier-overview.ocr-performed' | translate"
>
<mat-icon svgIcon="red:ocr"></mat-icon>
{{ fileStatus.ocrTime | date: 'mediumDate' }}
{{ fileStatus.lastOCRTime | date: 'mediumDate' }}
</div>
</div>
</div>
@ -147,15 +147,15 @@
class="small-label loading"
translate="dossier-overview.file-listing.file-entry.file-processing"
></div>
<redaction-status-bar
<iqser-status-bar
*ngIf="fileStatus.isWorkable"
[config]="[
[configs]="[
{
color: fileStatus.status,
length: 1
}
]"
></redaction-status-bar>
></iqser-status-bar>
<redaction-file-actions
(actionPerformed)="calculateData()"

View File

@ -1,5 +1,4 @@
import { ChangeDetectorRef, Component, ElementRef, HostListener, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { Toaster } from '@services/toaster.service';
import { AppStateService } from '@state/app-state.service';
import { FileDropOverlayService } from '@upload-download/services/file-drop-overlay.service';
@ -12,12 +11,11 @@ import { DossierDetailsComponent } from '../../components/dossier-details/dossie
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { UserService } from '@services/user.service';
import { timer } from 'rxjs';
import { filter } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { convertFiles, handleFileDrop } from '@utils/file-drop-utils';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { AppConfigKey, AppConfigService } from '@app-config/app-config.service';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
@ -42,8 +40,9 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
readonly itemSize = 80;
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
currentDossier = this._appStateService.activeDossier;
readonly tableHeaderLabel = _('dossier-overview.table-header.title');
readonly actionConfigs: ActionConfig[] = [
readonly actionConfigs: readonly ActionConfig[] = [
{
label: this._translateService.instant('dossier-overview.header-actions.edit'),
action: $event => this.openEditDossierDialog($event),
@ -51,45 +50,40 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
hide: !this.currentUser.isManager
}
];
readonly tableColumnConfigs: TableColumnConfig<FileStatusWrapper>[] = [
readonly tableColumnConfigs: readonly TableColumnConfig<FileStatusWrapper>[] = [
{
label: _('dossier-overview.table-col-names.name'),
withSort: true,
column: 'filename'
sortByKey: 'filename'
},
{
label: _('dossier-overview.table-col-names.added-on'),
withSort: true,
column: 'added'
sortByKey: 'added'
},
{
label: _('dossier-overview.table-col-names.needs-work')
},
{
label: _('dossier-overview.table-col-names.assigned-to'),
withSort: true,
class: 'user-column',
column: 'reviewerName'
sortByKey: 'reviewerName'
},
{
label: _('dossier-overview.table-col-names.pages'),
withSort: true,
column: 'pages'
sortByKey: 'pages'
},
{
label: _('dossier-overview.table-col-names.status'),
withSort: true,
class: 'flex-end',
column: 'statusSort'
sortByKey: 'statusSort'
}
];
collapsedDetails = false;
dossierAttributes: DossierAttributeWithValue[] = [];
protected readonly _primaryKey = 'filename';
private readonly _lastOpenedFileKey = 'Dossier-Recent-' + this.activeDossier.dossierId;
private readonly _lastOpenedFileKey = 'Dossier-Recent-' + this.currentDossier.dossierId;
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private _lastScrollPosition: number;
private _lastScrolledIndex: number;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
@ -97,7 +91,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
private readonly _scrollViewport: CdkVirtualScrollViewport;
constructor(
private readonly _router: Router,
private readonly _toaster: Toaster,
protected readonly _injector: Injector,
private readonly _userService: UserService,
@ -118,10 +111,6 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
this._loadEntitiesFromState();
}
get activeDossier(): DossierWrapper {
return this._appStateService.activeDossier;
}
get checkedRequiredFilters() {
return this.filterService.getGroup('quickFilters')?.filters.filter(f => f.required && f.checked);
}
@ -146,20 +135,16 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
this._loadEntitiesFromState();
});
this.addSubscription = this._appStateService.fileChanged.subscribe(() => {
this.addSubscription = this._appStateService.fileChanged$.subscribe(() => {
this.calculateData();
});
this.addSubscription = this._router.events
.pipe(filter(event => event instanceof NavigationStart))
.subscribe((event: NavigationStart) => {
if (!event.url.endsWith(this._appStateService.activeDossierId)) {
this._lastScrollPosition = this._scrollViewport.measureScrollOffset('top');
}
});
this.addSubscription = this._scrollViewport.scrolledIndexChange
.pipe(tap(index => (this._lastScrolledIndex = index)))
.subscribe();
this.dossierAttributes = await this._dossierAttributesService.getValues(this.activeDossier);
this.searchService.setSearchKey('searchField');
this.dossierAttributes = await this._dossierAttributesService.getValues(this.currentDossier);
this.searchService.setSearchKey('filename');
} catch (e) {
} finally {
this._loadingService.stop();
@ -175,7 +160,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
await this._appStateService.reloadActiveDossierFiles();
this._loadEntitiesFromState();
await this.ngOnInit();
this._scrollViewport.scrollTo({ top: this._lastScrollPosition });
this._scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
ngOnDetach() {
@ -193,7 +178,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
}
reloadDossiers() {
this._appStateService.getFiles(this.activeDossier, false).then(() => {
this._appStateService.getFiles(this.currentDossier, false).then(() => {
this.calculateData();
});
}
@ -210,7 +195,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
handleFileDrop(event, this.activeDossier, this._uploadFiles.bind(this));
handleFileDrop(event, this.currentDossier, this._uploadFiles.bind(this));
}
@HostListener('dragover', ['$event'])
@ -220,14 +205,12 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
}
async uploadFiles(files: File[] | FileList) {
await this._uploadFiles(convertFiles(files, this.activeDossier));
await this._uploadFiles(convertFiles(files, this.currentDossier));
this._fileInput.nativeElement.value = null;
}
fileLink(fileStatus: FileStatusWrapper) {
return this.permissionsService.canOpenFile(fileStatus)
? [`/main/dossiers/${this.activeDossier.dossierId}/file/${fileStatus.fileId}`]
: [];
return fileStatus.canBeOpened ? [`/main/dossiers/${this.currentDossier.dossierId}/file/${fileStatus.fileId}`] : [];
}
bulkActionPerformed() {
@ -237,7 +220,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
openEditDossierDialog($event: MouseEvent) {
this._dialogService.openDialog('editDossier', $event, {
dossierWrapper: this.activeDossier
dossierWrapper: this.currentDossier
});
}
@ -246,7 +229,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
'editDossier',
null,
{
dossierWrapper: this.activeDossier,
dossierWrapper: this.currentDossier,
section: 'members'
},
() => this.reloadDossiers()
@ -254,7 +237,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
}
openDossierDictionaryDialog() {
this._dialogService.openDialog('dossierDictionary', null, this.activeDossier, () => {
this._dialogService.openDialog('dossierDictionary', null, this.currentDossier, () => {
this.reloadDossiers();
});
}
@ -267,7 +250,8 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
moment(file.lastUpdated).add(this._appConfigService.getConfig(AppConfigKey.RECENT_PERIOD_IN_HOURS), 'hours').isAfter(moment());
private _loadEntitiesFromState() {
if (this.activeDossier) this.entitiesService.setEntities(this.activeDossier.files);
this.currentDossier = this._appStateService.activeDossier;
if (this.currentDossier) this.entitiesService.setEntities(this.currentDossier.files);
}
private async _uploadFiles(files: FileUploadModel[]) {
@ -276,7 +260,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
}
private _computeAllFilters() {
if (!this.activeDossier) return;
if (!this.currentDossier) return;
const allDistinctFileStatusWrapper = new Set<string>();
const allDistinctPeople = new Set<string>();
@ -288,7 +272,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
allDistinctFileStatusWrapper.add(file.status);
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'));
if (this.permissionsService.fileRequiresReanalysis(file)) allDistinctNeedsWork.add('analysis');
if (file.analysisRequired) allDistinctNeedsWork.add('analysis');
if (file.hintsOnly) allDistinctNeedsWork.add('hint');
if (file.hasRedactions) allDistinctNeedsWork.add('redaction');
if (file.hasRequests) allDistinctNeedsWork.add('suggestion');
@ -345,8 +329,7 @@ export class DossierOverviewScreenComponent extends ListingComponent<FileStatusW
filterTemplate: this._needsWorkTemplate,
filters: needsWorkFilters.sort(RedactionFilterSorter.byKey),
checker: annotationFilterChecker,
matchAll: true,
checkerArgs: [this.permissionsService]
matchAll: true
});
this.filterService.addFilterGroup({

View File

@ -30,9 +30,9 @@
</div>
<div class="flex-1 actions-container">
<ng-container *ngIf="!appStateService.activeFile.isExcluded">
<ng-container *ngIf="!appStateService.activeFile.excluded">
<ng-container *ngIf="!appStateService.activeFile.isProcessing">
<redaction-status-bar [config]="statusBarConfig" [small]="true"></redaction-status-bar>
<iqser-status-bar [configs]="statusBarConfig" [small]="true"></iqser-status-bar>
<div class="all-caps-label mr-16 ml-8">
{{ translations[status] | translate }}
@ -148,7 +148,7 @@
<div class="right-container">
<redaction-empty-state
*ngIf="appStateService.activeFile.isExcluded && !viewDocumentInfo && !excludePages"
*ngIf="appStateService.activeFile.excluded && !viewDocumentInfo && !excludePages"
[horizontalPadding]="40"
[text]="'file-preview.tabs.is-excluded' | translate"
icon="red:needs-work"
@ -168,7 +168,7 @@
(selectAnnotations)="selectAnnotations($event)"
(selectPage)="selectPage($event)"
(toggleSkipped)="toggleSkipped($event)"
*ngIf="!appStateService.activeFile.isExcluded"
*ngIf="!appStateService.activeFile.excluded"
[(shouldDeselectAnnotationsOnPageChange)]="shouldDeselectAnnotationsOnPageChange"
[activeViewerPage]="activeViewerPage"
[annotationActionsTemplate]="annotationActionsTemplate"

View File

@ -3,7 +3,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from
import { AppStateService } from '@state/app-state.service';
import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import { PdfViewerComponent } from '../../components/pdf-viewer/pdf-viewer.component';
import { Debounce } from '@utils/debounce';
import { AutoUnsubscribe, CircleButtonTypes, Debounce, NestedFilter, processFilters } from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@ -33,7 +33,6 @@ import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { LoadingService } from '@services/loading.service';
import { stampPDFPage } from '@utils/page-stamper';
import { TranslateService } from '@ngx-translate/core';
import { AutoUnsubscribe, CircleButtonTypes, NestedFilter, processFilters } from '@iqser/common-ui';
import { fileStatusTranslations } from '../../translations/file-status-translations';
import { handleFilterDelta } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -41,7 +40,6 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@Component({
selector: 'redaction-file-preview-screen',
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss']
})
@ -133,11 +131,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
get canSwitchToRedactedView() {
return this.fileData && !this.permissionsService.fileRequiresReanalysis() && !this.fileData.fileStatus.isExcluded;
return this.fileData && !this.fileData.fileStatus.analysisRequired && !this.fileData.fileStatus.excluded;
}
get canSwitchToDeltaView() {
return this.fileData?.redactionChangeLog?.redactionLogEntry?.length > 0 && !this.fileData.fileStatus.isExcluded;
return this.fileData?.redactionChangeLog?.redactionLogEntry?.length > 0 && !this.fileData.fileStatus.excluded;
}
get canAssign(): boolean {
@ -233,7 +231,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
if (!this.permissionsService.canOpenFile(this.appStateService.activeFile)) {
if (!this.appStateService.activeFile.canBeOpened) {
await this._router.navigate(['/main/dossiers/' + this.dossierId]);
return;
}
@ -549,7 +547,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private _subscribeToFileUpdates(): void {
this.addSubscription = timer(0, 5000).subscribe(async () => await this.appStateService.reloadActiveFile());
this.addSubscription = this.appStateService.fileReanalysed.subscribe(async (fileStatus: FileStatusWrapper) => {
this.addSubscription = this.appStateService.fileReanalysed$.subscribe(async (fileStatus: FileStatusWrapper) => {
if (fileStatus.fileId === this.fileId) {
await this._loadFileData(!this._reloadFileOnReanalysis);
this._reloadFileOnReanalysis = false;

View File

@ -10,10 +10,7 @@
<div class="red-content-inner">
<div class="content-container">
<redaction-table-header
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></redaction-table-header>
<iqser-table-header [tableColumnConfigs]="tableColumnConfigs" [tableHeaderLabel]="tableHeaderLabel"></iqser-table-header>
<redaction-empty-state
*ngIf="searchResult.length === 0"
@ -62,8 +59,8 @@
</div>
<div>
<redaction-status-bar
[config]="[
<iqser-status-bar
[configs]="[
{
color: item.status,
label: fileStatusTranslations[item.status] | translate,
@ -72,7 +69,7 @@
}
]"
[small]="true"
></redaction-status-bar>
></iqser-status-bar>
</div>
<div class="small-label">

View File

@ -25,7 +25,7 @@ export class FileActionService {
fileStatusWrapper = this._appStateService.activeFile;
}
return this._reanalysisControllerService.reanalyzeFile(
this._appStateService.activeDossier.dossier.dossierId,
this._appStateService.activeDossier.dossierId,
fileStatusWrapper.fileId,
true
);
@ -38,7 +38,7 @@ export class FileActionService {
return this._reanalysisControllerService.toggleAnalysis(
fileStatusWrapper.dossierId,
fileStatusWrapper.fileId,
!fileStatusWrapper.isExcluded
!fileStatusWrapper.excluded
);
}

View File

@ -31,9 +31,9 @@
</div>
<div class="red-input-group w-200 mr-8">
<mat-select [(ngModel)]="dossier" [disabled]="!compare">
<mat-option [value]="selectDossier">{{ selectDossier.name | translate }}</mat-option>
<mat-option [value]="selectDossier">{{ selectDossier.dossierName | translate }}</mat-option>
<mat-option *ngFor="let dossier of dossiers" [value]="dossier">
{{ dossier.name }}
{{ dossier.dossierName }}
</mat-option>
</mat-select>
</div>
@ -55,7 +55,7 @@
[original]="diffEditorText"
></ngx-monaco-diff-editor>
<div *ngIf="compare && dossier === selectDossier" class="no-dictionary-selected">
<div *ngIf="compare && dossier.dossierName === selectDossier.dossierName" class="no-dictionary-selected">
<mat-icon svgIcon="red:dictionary"></mat-icon>
<span class="heading-l" translate="dictionary-overview.select-dictionary"></span>
</div>

View File

@ -1,12 +1,11 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { Debounce } from '@utils/debounce';
import { Debounce, IconButtonTypes } from '@iqser/common-ui';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { IconButtonTypes } from '@iqser/common-ui';
import ICodeEditor = monaco.editor.ICodeEditor;
import IDiffEditor = monaco.editor.IDiffEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
@ -41,7 +40,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
showDiffEditor = false;
searchText = '';
selectDossier = { name: _('dictionary-overview.compare.select-dossier') };
selectDossier = { dossierName: _('dictionary-overview.compare.select-dossier') };
compare: false;
private _codeEditor: ICodeEditor;

View File

@ -1,6 +1,5 @@
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { PermissionsService } from '@services/permissions.service';
import { handleCheckedValue, NestedFilter } from '@iqser/common-ui';
export function handleFilterDelta(oldFilters: NestedFilter[], newFilters: NestedFilter[], allFilters: NestedFilter[]) {
@ -46,17 +45,13 @@ export function handleFilterDelta(oldFilters: NestedFilter[], newFilters: Nested
});
}
export const annotationFilterChecker = (
input: FileStatusWrapper | DossierWrapper,
filter: NestedFilter,
permissionsService: PermissionsService
) => {
export const annotationFilterChecker = (input: FileStatusWrapper | DossierWrapper, filter: NestedFilter) => {
switch (filter.key) {
case 'analysis': {
if (input instanceof DossierWrapper) {
return permissionsService.dossierReanalysisRequired(input);
return input.reanalysisRequired;
} else {
return permissionsService.fileRequiresReanalysis(input);
return input.analysisRequired;
}
}
case 'suggestion': {

View File

@ -1,8 +0,0 @@
<div
(click)="filterService.toggleFilter('quickFilters', filter.key)"
*ngFor="let filter of quickFilters$ | async"
[class.active]="filter.checked"
class="quick-filter"
>
{{ filter.label }}
</div>

View File

@ -1,34 +0,0 @@
@import '../../../../../../assets/styles/variables';
:host {
display: flex;
flex: 1;
justify-content: flex-end;
}
.quick-filter {
box-sizing: border-box;
border: 1px solid $grey-5;
border-radius: 17px;
background-color: $grey-6;
padding: 0 14px;
height: 34px;
display: flex;
align-items: center;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: $white;
}
&.active {
background-color: $white;
font-weight: 600;
border: none;
}
&:not(:last-child) {
margin-right: 8px;
}
}

View File

@ -1,14 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FilterService } from '@iqser/common-ui';
@Component({
selector: 'redaction-quick-filters',
templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
constructor(readonly filterService: FilterService) {}
}

View File

@ -16,11 +16,11 @@ export class PageHeaderComponent<T extends object> {
@Input() pageLabel: string;
@Input() showCloseButton: boolean;
@Input() actionConfigs: ActionConfig[];
@Input() buttonConfigs: ButtonConfig[];
@Input() actionConfigs: readonly ActionConfig[];
@Input() buttonConfigs: readonly ButtonConfig[];
@Input() searchPlaceholder: string;
@Input() searchWidth: number | 'full';
@Input() searchPosition: SearchPosition = this.searchPositions.afterFilters;
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
readonly showResetFilters$ = this._showResetFilters$;

View File

@ -30,8 +30,8 @@
[class.active]="filterChecked$(val.key) | async"
[class.filter-disabled]="(statusFilters$ | async)?.length === 0"
>
<redaction-status-bar
[config]="[
<iqser-status-bar
[configs]="[
{
length: val.value,
color: val.color,
@ -41,7 +41,7 @@
]"
[small]="true"
>
</redaction-status-bar>
</iqser-status-bar>
</div>
</div>
</div>

View File

@ -1,11 +0,0 @@
<div [ngClass]="{ small: small }" class="rectangle-container">
<div *ngFor="let rect of config" [style]="'flex: ' + (rect.length || 1) + ';'" class="section-wrapper">
<div
[className]="'rectangle ' + rect.color"
[ngStyle]="{
'background-color': rect.color.includes('#') ? rect.color : ''
}"
></div>
<div *ngIf="rect.label" [class]="rect.cssClass">{{ rect.label }}</div>
</div>
</div>

View File

@ -1,46 +0,0 @@
@import '../../../../../assets/styles/variables';
.rectangle-container {
flex: 1;
display: flex;
width: 100%;
min-width: 12px;
&.small {
.rectangle {
width: 12px;
min-width: 12px;
}
.section-wrapper {
display: flex;
align-items: center;
> *:not(:last-child) {
margin-right: 10px;
}
}
}
.section-wrapper:first-child {
.rectangle {
border-radius: 6px 0 0 6px;
}
}
.section-wrapper:last-child {
.rectangle {
border-radius: 0 6px 6px 0;
}
&:first-child {
.rectangle {
border-radius: 6px;
}
}
}
.rectangle {
height: 6px;
}
}

View File

@ -1,22 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { Color } from '@utils/types';
@Component({
selector: 'redaction-status-bar',
templateUrl: './status-bar.component.html',
styleUrls: ['./status-bar.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class StatusBarComponent {
@Input()
config: {
length: number;
color: Color;
label?: string;
cssClass?: string;
}[] = [];
@Input()
small = false;
}

View File

@ -1,43 +0,0 @@
<div [class.selection-enabled]="selectionEnabled" class="header-item">
<iqser-round-checkbox
(click)="entitiesService.selectAll()"
*ngIf="selectionEnabled"
[active]="entitiesService.areAllSelected$ | async"
[indeterminate]="entitiesService.notAllSelected$ | async"
></iqser-round-checkbox>
<span class="all-caps-label">
{{ tableHeaderLabel | translate: { length: (entitiesService.displayedLength$ | async) } }}
</span>
<ng-container [ngTemplateOutlet]="bulkActions"></ng-container>
<redaction-quick-filters *ngIf="hasQuickFilters$ | async"></redaction-quick-filters>
<!-- Custom content-->
<ng-content></ng-content>
</div>
<div
[class.no-data]="entitiesService.noData$ | async"
[class.selection-enabled]="selectionEnabled"
class="table-header"
redactionSyncWidth="table-item"
>
<div *ngIf="selectionEnabled" class="select-oval-placeholder"></div>
<iqser-table-column-name
*ngFor="let config of tableColumnConfigs"
[class]="config.class"
[column]="config.column"
[label]="config.label | translate"
[leftIcon]="config.leftIcon"
[rightIconTooltip]="config.rightIconTooltip"
[rightIcon]="config.rightIcon"
[withSort]="config.withSort"
></iqser-table-column-name>
<div *ngIf="hasEmptyColumn"></div>
<div class="scrollbar-placeholder"></div>
</div>

View File

@ -1,21 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core';
import { EntitiesService, FilterService, Required, TableColumnConfig } from '@iqser/common-ui';
import { map } from 'rxjs/operators';
@Component({
selector: 'redaction-table-header',
templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T extends object> {
@Input() @Required() tableHeaderLabel: string;
@Input() @Required() tableColumnConfigs: TableColumnConfig<T>[];
@Input() hasEmptyColumn = false;
@Input() selectionEnabled = false;
@Input() bulkActions: TemplateRef<any>;
readonly hasQuickFilters$ = this.filterService.getGroup$('quickFilters').pipe(map(filters => !!filters));
constructor(readonly entitiesService: EntitiesService<T>, readonly filterService: FilterService) {}
}

View File

@ -1,76 +0,0 @@
import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/core';
import { Debounce } from '@utils/debounce';
@Directive({
selector: '[redactionSyncWidth]',
exportAs: 'redactionSyncWidth'
})
export class SyncWidthDirective implements AfterViewInit, OnDestroy {
@Input()
redactionSyncWidth: string;
private _interval: number;
constructor(private readonly _elementRef: ElementRef) {}
ngAfterViewInit(): void {
this._interval = setInterval(() => {
this.matchWidth();
}, 1000);
}
ngOnDestroy(): void {
clearInterval(this._interval);
}
@Debounce(10)
matchWidth() {
const headerItems = this._elementRef.nativeElement.children;
const tableRows = this._elementRef.nativeElement.parentElement.parentElement.getElementsByClassName(this.redactionSyncWidth);
if (!tableRows || !tableRows.length) {
return;
}
this._elementRef.nativeElement.setAttribute('synced', true);
const { tableRow, length } = this._sampleRow(tableRows);
const hasExtraColumns = headerItems.length !== length ? 1 : 0;
for (let idx = 0; idx < length - hasExtraColumns - 1; ++idx) {
if (headerItems[idx]) {
headerItems[idx].style.width = `${tableRow.children[idx].getBoundingClientRect().width}px`;
headerItems[idx].style.minWidth = `${tableRow.children[idx].getBoundingClientRect().width}px`;
}
}
for (let idx = length - hasExtraColumns - 1; idx < headerItems.length; ++idx) {
if (headerItems[idx]) {
headerItems[idx].style.minWidth = `0`;
}
}
}
@HostListener('window:resize')
onResize() {
this.matchWidth();
}
private _sampleRow(tableRows: HTMLCollectionOf<Element>): {
tableRow: Element;
length: number;
} {
let length = 0;
let tableRow: Element;
for (let idx = 0; idx < tableRows.length; ++idx) {
const row = tableRows.item(idx);
if (row.children.length > length) {
length = row.children.length;
tableRow = row;
}
}
return { tableRow, length };
}
}

View File

@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FullPageLoadingIndicatorComponent } from './components/full-page-loading-indicator/full-page-loading-indicator.component';
import { TranslateModule } from '@ngx-translate/core';
import { InitialsAvatarComponent } from './components/initials-avatar/initials-avatar.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { PaginationComponent } from './components/pagination/pagination.component';
@ -12,8 +11,6 @@ import { IconsModule } from '../icons/icons.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component';
import { StatusBarComponent } from './components/status-bar/status-bar.component';
import { SyncWidthDirective } from './directives/sync-width.directive';
import { HasScrollbarDirective } from './directives/has-scrollbar.directive';
import { DictionaryAnnotationIconComponent } from './components/dictionary-annotation-icon/dictionary-annotation-icon.component';
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
@ -27,25 +24,21 @@ import { NavigateLastDossiersScreenDirective } from './directives/navigate-last-
import { DictionaryManagerComponent } from './components/dictionary-manager/dictionary-manager.component';
import { SideNavComponent } from '@shared/components/side-nav/side-nav.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { QuickFiltersComponent } from './components/filters/quick-filters/quick-filters.component';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { AssignUserDropdownComponent } from './components/assign-user-dropdown/assign-user-dropdown.component';
import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component';
import { PageHeaderComponent } from './components/page-header/page-header.component';
import { DatePipe } from '@shared/pipes/date.pipe';
import { TableHeaderComponent } from './components/table-header/table-header.component';
const buttons = [FileDownloadBtnComponent, UserButtonComponent];
const components = [
FullPageLoadingIndicatorComponent,
InitialsAvatarComponent,
TableHeaderComponent,
PaginationComponent,
InputWithActionComponent,
AnnotationIconComponent,
SimpleDoughnutChartComponent,
StatusBarComponent,
DictionaryAnnotationIconComponent,
HiddenActionComponent,
PopupFilterComponent,
@ -54,21 +47,20 @@ const components = [
SelectComponent,
SideNavComponent,
DictionaryManagerComponent,
QuickFiltersComponent,
AssignUserDropdownComponent,
PageHeaderComponent,
...buttons
];
const utils = [DatePipe, SyncWidthDirective, HasScrollbarDirective, NavigateLastDossiersScreenDirective];
const utils = [DatePipe, HasScrollbarDirective, NavigateLastDossiersScreenDirective];
const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, CommonUiModule];
const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, CommonUiModule];
@NgModule({
declarations: [...components, ...utils, TableHeaderComponent],
declarations: [...components, ...utils],
imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils, TableHeaderComponent],
exports: [...modules, ...components, ...utils],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { UserService, UserWrapper } from './user.service';
import { UserService } from './user.service';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { Comment } from '@redaction/red-ui-http';
import { DossierWrapper } from '@state/model/dossier.wrapper';
@ -23,16 +23,8 @@ export class PermissionsService {
return this.isFileReviewer(fileStatus) || this.isApprover();
}
dossierReanalysisRequired(dossier: DossierWrapper): boolean {
for (const file of dossier.files) if (this.fileRequiresReanalysis(file)) return true;
}
fileRequiresReanalysis(fileStatus = this._activeFile): boolean {
return fileStatus.analysisRequired;
}
displayReanalyseBtn(dossier = this._activeDossier): boolean {
return this.isApprover(dossier) && dossier.files.filter(file => this.fileRequiresReanalysis(file)).length > 0;
return this.isApprover(dossier) && dossier.files.filter(file => file.analysisRequired).length > 0;
}
canToggleAnalysis(fileStatus: FileStatusWrapper): boolean {
@ -41,7 +33,7 @@ export class PermissionsService {
canReanalyseFile(fileStatus = this._activeFile): boolean {
return (
(this.fileRequiresReanalysis(fileStatus) && (this.isReviewerOrApprover(fileStatus) || fileStatus.isUnassigned)) ||
(fileStatus.analysisRequired && (this.isReviewerOrApprover(fileStatus) || fileStatus.isUnassigned)) ||
(fileStatus.isError && fileStatus.isUnassigned)
);
}
@ -54,14 +46,10 @@ export class PermissionsService {
return this.isOwner(dossier) || fileStatus.isUnassigned;
}
isApprovedOrUnderApproval(fileStatus = this._activeFile): boolean {
return fileStatus.isApprovedOrUnderApproval;
}
canAssignToSelf(fileStatus = this._activeFile): boolean {
const precondition = this.isDossierMember() && !fileStatus.isProcessing && !fileStatus.isError && !fileStatus.isApproved;
const isTheOnlyReviewer = !this._appStateService.activeDossier.hasMoreThanOneReviewer;
const isTheOnlyReviewer = !this._appStateService.activeDossier?.hasMoreThanOneReviewer;
if (precondition) {
if (
@ -96,10 +84,6 @@ export class PermissionsService {
return this.canSetUnderReview(fileStatus);
}
canApprove(fileStatus = this._activeFile): boolean {
return !fileStatus?.analysisRequired && !fileStatus.hasRequests;
}
canSetUnderApproval(fileStatus = this._activeFile): boolean {
return fileStatus?.isUnderReview && this.isReviewerOrApprover(fileStatus);
}
@ -124,18 +108,10 @@ export class PermissionsService {
return ['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus?.status) && this.isFileReviewer(fileStatus);
}
canOpenFile(fileStatus = this._activeFile): boolean {
return !fileStatus?.isError && !fileStatus?.isPending;
}
canUndoApproval(fileStatus = this._activeFile): boolean {
return fileStatus?.isApproved && this.isApprover();
}
canUndoUnderApproval(fileStatus = this._activeFile): boolean {
return fileStatus?.isUnderApproval && this.isDossierMember();
}
canMarkPagesAsViewed(fileStatus = this._activeFile): boolean {
return ['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus?.status) && this.isFileReviewer(fileStatus);
}
@ -155,16 +131,6 @@ export class PermissionsService {
return user.isAdmin;
}
canOcrFile(fileStatus = this._activeFile): boolean {
return (
!fileStatus.isExcluded && !fileStatus.ocrTime && ['UNASSIGNED', 'UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus.status)
);
}
canManageUsers(user: UserWrapper = this._userService.currentUser): boolean {
return user.isUserAdmin;
}
canAddComment(fileStatus = this._activeFile): boolean {
return (this.isFileReviewer(fileStatus) || this.isApprover()) && !fileStatus.isApproved;
}

View File

@ -1,4 +1,4 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import {
DictionaryControllerService,
Dossier,
@ -13,7 +13,7 @@ import { Toaster } from '@services/toaster.service';
import { TranslateService } from '@ngx-translate/core';
import { Event, NavigationEnd, ResolveStart, Router } from '@angular/router';
import { UserService } from '@services/user.service';
import { forkJoin, Observable, of } from 'rxjs';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { FALLBACK_COLOR, hexToRgb } from '@utils/functions';
import { FileStatusWrapper } from '@models/file/file-status.wrapper';
@ -41,8 +41,8 @@ export interface AppState {
providedIn: 'root'
})
export class AppStateService {
fileChanged = new EventEmitter<FileStatusWrapper>();
fileReanalysed = new EventEmitter<FileStatusWrapper>();
readonly fileChanged$ = new Subject<FileStatusWrapper>();
readonly fileReanalysed$ = new Subject<FileStatusWrapper>();
private _appState: AppState;
constructor(
@ -208,7 +208,7 @@ export class AppStateService {
}
getDossierById(id: string) {
return this.allDossiers.find(dossier => dossier.dossier.dossierId === id);
return this.allDossiers.find(dossier => dossier.dossierId === id);
}
getFileById(dossierId: string, fileId: string) {
@ -243,7 +243,6 @@ export class AppStateService {
const activeFileWrapper = new FileStatusWrapper(
activeFile,
this._userService.getNameForId(activeFile.currentReviewer),
this.activeDossierTemplateId,
this._appState.fileAttributesConfig[this.activeDossierTemplateId]
);
this.activeDossier.files = this.activeDossier?.files.map(file =>
@ -252,9 +251,9 @@ export class AppStateService {
this._computeStats();
if (activeFileWrapper.lastProcessed !== oldProcessedDate) {
this.fileReanalysed.emit(activeFileWrapper);
this.fileReanalysed$.next(activeFileWrapper);
}
this.fileChanged.emit(activeFileWrapper);
this.fileChanged$.next(activeFileWrapper);
return activeFileWrapper;
}
@ -346,12 +345,13 @@ export class AppStateService {
const updatedDossier = await this._dossiersService.createOrUpdate(dossier);
let foundDossier = this.allDossiers.find(p => p.dossierId === updatedDossier.dossierId);
if (foundDossier) {
Object.assign((foundDossier.dossier = updatedDossier));
this._appState.dossiers.splice(this._appState.dossiers.indexOf(foundDossier), 1);
foundDossier = new DossierWrapper(updatedDossier, foundDossier.files);
} else {
foundDossier = new DossierWrapper(updatedDossier, []);
this._appState.dossiers.push(foundDossier);
}
this._appState.dossiers = [...this._appState.dossiers];
this._appState.dossiers.push(foundDossier);
return foundDossier;
} catch (error) {
this._toaster.error(
@ -668,7 +668,6 @@ export class AppStateService {
const fileStatusWrapper = new FileStatusWrapper(
file,
this._userService.getNameForId(file.currentReviewer),
dossier.dossierTemplateId,
this._appState.fileAttributesConfig[dossier.dossierTemplateId]
);
if (JSON.stringify(oldFile) !== JSON.stringify(fileStatusWrapper)) {
@ -686,7 +685,6 @@ export class AppStateService {
const fsw = new FileStatusWrapper(
file,
this._userService.getNameForId(file.currentReviewer),
dossier.dossierTemplateId,
this._appState.fileAttributesConfig[dossier.dossierTemplateId]
);
fileStatusChangedEvent.push(fsw);
@ -698,15 +696,14 @@ export class AppStateService {
new FileStatusWrapper(
f,
this._userService.getNameForId(f.currentReviewer),
dossier.dossierTemplateId,
this._appState.fileAttributesConfig[dossier.dossierTemplateId]
)
);
this._computeStats();
if (emitEvents) {
fileReanalysedEvent.forEach(file => this.fileReanalysed.emit(file));
fileStatusChangedEvent.forEach(file => this.fileChanged.emit(file));
fileReanalysedEvent.forEach(file => this.fileReanalysed$.next(file));
fileStatusChangedEvent.forEach(file => this.fileChanged$.next(file));
}
return files;
@ -716,12 +713,10 @@ export class AppStateService {
let totalAnalysedPages = 0;
let totalDocuments = 0;
const totalPeople = new Set<string>();
this.allDossiers.forEach(d => {
totalDocuments += d.files.length;
if (d.dossier.memberIds) {
d.dossier.memberIds.forEach(m => totalPeople.add(m));
}
d.memberIds?.forEach(m => totalPeople.add(m));
d.totalNumberOfPages = d.files.reduce((acc, file) => acc + file.numberOfPages, 0);
totalAnalysedPages += d.totalNumberOfPages;
});

View File

@ -2,9 +2,33 @@ import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import * as moment from 'moment';
import { Dictionary, Dossier } from '@redaction/red-ui-http';
export class DossierWrapper {
totalNumberOfPages?: number;
export class DossierWrapper implements Dossier {
readonly approverIds = this._dossier.approverIds;
readonly date = this._dossier.date;
readonly description = this._dossier.description;
readonly dossierId = this._dossier.dossierId;
readonly dossierName = this._dossier.dossierName;
readonly dossierTemplateId = this._dossier.dossierTemplateId;
readonly downloadFileTypes = this._dossier.downloadFileTypes;
readonly dueDate = this._dossier.dueDate;
readonly hardDeletedTime = this._dossier.hardDeletedTime;
readonly memberIds = this._dossier.memberIds;
readonly ownerId = this._dossier.ownerId;
readonly reportTemplateIds = this._dossier.reportTemplateIds;
readonly reportTypes = this._dossier.reportTypes;
readonly softDeletedTime = this._dossier.softDeletedTime;
readonly status = this._dossier.status;
readonly watermarkEnabled = this._dossier.watermarkEnabled;
readonly hasMoreThanOneApprover = this.approverIds.length > 1;
readonly hasMoreThanOneReviewer = this.memberIds.length > 1;
readonly memberCount = this.memberIds.length;
reanalysisRequired = this._files.some(file => file.analysisRequired);
hasFiles = this._files.length > 0;
filesLength = this._files.length;
totalNumberOfPages?: number;
hintsOnly?: boolean;
hasRedactions?: boolean;
hasRequests?: boolean;
@ -12,15 +36,12 @@ export class DossierWrapper {
hasPendingOrProcessing?: boolean;
allFilesApproved?: boolean;
type: Dictionary;
type?: Dictionary;
constructor(public dossier: Dossier, files: FileStatusWrapper[]) {
this._files = files ? files : [];
constructor(private readonly _dossier: Dossier, private _files: FileStatusWrapper[] = []) {
this._recomputeFileStatus();
}
private _files: FileStatusWrapper[];
get files() {
return this._files;
}
@ -30,84 +51,16 @@ export class DossierWrapper {
this._recomputeFileStatus();
}
get hasMoreThanOneApprover() {
return this.approverIds.length > 1;
}
get hasMoreThanOneReviewer() {
return this.memberIds.length > 1;
}
get dossierName() {
return this.dossier.dossierName;
}
get description() {
return this.dossier.description;
}
get dossierTemplateId() {
return this.dossier.dossierTemplateId;
}
get ownerId() {
return this.dossier.ownerId;
}
get dossierId() {
return this.dossier.dossierId;
}
get memberIds() {
return this.dossier.memberIds;
}
get approverIds() {
return this.dossier.approverIds;
}
get dossierDate() {
return this.dossier.date;
}
get numberOfMembers() {
return this.dossier.memberIds.length;
}
get dueDate() {
return this.dossier.dueDate;
}
get hasFiles() {
return this._files.length > 0;
}
get name() {
return this.dossier.dossierName;
}
get memberCount() {
return this.dossier.memberIds.length;
}
get watermarkEnabled(): boolean {
return this.dossier.watermarkEnabled;
}
hasStatus(status: string): boolean {
return !!this._files.find(f => f.status === status);
}
hasMember(key: string) {
return this.dossier.memberIds.indexOf(key) >= 0;
}
dueDateMatches(key: string) {
return moment(this.dueDate).format('DD/MM/YYYY') === key;
return this._dossier.memberIds.indexOf(key) >= 0;
}
addedDateMatches(key: string) {
return moment(this.dossierDate).format('DD/MM/YYYY') === key;
return moment(this.date).format('DD/MM/YYYY') === key;
}
private _recomputeFileStatus() {
@ -127,5 +80,8 @@ export class DossierWrapper {
this.hasPendingOrProcessing = this.hasPendingOrProcessing || f.isPending || f.isProcessing;
});
this.hasNone = !this.hasRequests && !this.hasRedactions && !this.hintsOnly;
this.hasFiles = this._files.length > 0;
this.filesLength = this._files.length;
this.reanalysisRequired = this._files.some(file => file.analysisRequired);
}
}

View File

@ -1,5 +1,5 @@
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
import { Debounce } from './debounce';
import { Debounce } from '@iqser/common-ui';
export interface OnAttach {
ngOnAttach(previousRoute: ActivatedRouteSnapshot);

View File

@ -1,14 +0,0 @@
export function Debounce(delay: number = 300): MethodDecorator {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let timeout = null;
const original = descriptor.value;
descriptor.value = function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => original.apply(this, args), delay);
};
return descriptor;
};
}

View File

@ -35,5 +35,6 @@ $dark: $black;
$btn-bg-hover: $grey-4;
$btn-bg: $grey-6;
$quick-filter-border: $grey-5;
$separator: rgba(226, 228, 233, 0.9);

View File

@ -1,31 +0,0 @@
@import 'variables';
@import 'red-mixins';
.header-item {
background-color: $grey-6;
height: 50px;
padding: 0 24px;
display: flex;
align-items: center;
z-index: 1;
border-bottom: 1px solid $separator;
box-sizing: border-box;
> *:not(:last-child) {
margin-right: 10px;
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 16px;
}
}
&.selection-enabled {
padding-left: 10px;
}
}

View File

@ -123,8 +123,3 @@ cdk-virtual-scroll-viewport {
}
}
}
.scrollbar-placeholder {
width: 11px;
padding: 0 !important;
}

View File

@ -18,7 +18,6 @@
@import 'red-controls';
@import 'red-toasts';
@import 'red-tooltips';
@import 'red-grid';
@import 'red-breadcrumbs';
@import 'red-editor';
@import 'red-slider';

@ -1 +1 @@
Subproject commit c552ed2e21d3df6fbcfe842ae85ac5a75006ab85
Subproject commit 1d46b21c74c07d6811a99b9de6c940c740bc8ceb

View File

@ -169,5 +169,5 @@ export namespace FileStatus {
UNDERREVIEW: 'UNDER_REVIEW' as StatusEnum,
UNPROCESSED: 'UNPROCESSED' as StatusEnum,
INDEXING: 'INDEXING' as StatusEnum
};
} as const;
}