Pull request #134: File attributes

Merge in RED/ui from file-attributes to master

* commit '77fc8c1b4825fb5c07f01fd7a1efdb00e1c08491':
  Finished file attributes
  File workload separate component
  Display document info over workload
  Document info
  Delete file attribute
  Add file attribute
  Reorganize more files
  Quick fixes
  Move items outside of common directory
  Extract routing to module
  File attributes listing
This commit is contained in:
Timo Bejan 2021-03-26 08:34:54 +01:00
commit 0621aca7fc
130 changed files with 2188 additions and 1023 deletions

View File

@ -0,0 +1,212 @@
import { AuthErrorComponent } from './screens/auth-error/auth-error.component';
import { AuthGuard } from './auth/auth.guard';
import { PdfViewerScreenComponent } from './screens/pdf-viewer-screen/pdf-viewer-screen.component';
import { CompositeRouteGuard } from './utils/composite-route.guard';
import { RedRoleGuard } from './auth/red-role.guard';
import { HtmlDebugScreenComponent } from './screens/html-debug-screen/html-debug-screen.component';
import { BaseScreenComponent } from './screens/base-screen/base-screen.component';
import { ProjectListingScreenComponent } from './screens/project-listing-screen/project-listing-screen.component';
import { AppStateGuard } from './state/app-state.guard';
import { ProjectOverviewScreenComponent } from './screens/project-overview-screen/project-overview-screen.component';
import { FilePreviewScreenComponent } from './screens/file/file-preview-screen/file-preview-screen.component';
import { DownloadsListScreenComponent } from './screens/downloads-list-screen/downloads-list-screen.component';
import { RuleSetsListingScreenComponent } from './screens/admin/rule-sets-listing-screen/rule-sets-listing-screen.component';
import { DictionaryListingScreenComponent } from './screens/admin/dictionary-listing-screen/dictionary-listing-screen.component';
import { DictionaryOverviewScreenComponent } from './screens/admin/dictionary-overview-screen/dictionary-overview-screen.component';
import { PendingChangesGuard } from './utils/can-deactivate.guard';
import { RulesScreenComponent } from './screens/admin/rules-screen/rules-screen.component';
import { FileAttributesListingScreenComponent } from './screens/admin/file-attributes-listing-screen/file-attributes-listing-screen.component';
import { WatermarkScreenComponent } from './screens/admin/watermark-screen/watermark-screen.component';
import { DefaultColorsScreenComponent } from './screens/admin/default-colors-screen/default-colors-screen.component';
import { UserListingScreenComponent } from './screens/admin/user-listing-screen/user-listing-screen.component';
import { LicenseInformationScreenComponent } from './screens/admin/license-information-screen/license-information-screen.component';
import { DigitalSignatureScreenComponent } from './screens/admin/digital-signature-screen/digital-signature-screen.component';
import { AuditScreenComponent } from './screens/admin/audit-screen/audit-screen.component';
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
const routes = [
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'auth-error',
component: AuthErrorComponent,
canActivate: [AuthGuard]
},
{
path: 'pdf-preview/:projectId/:fileId',
component: PdfViewerScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard]
}
},
{
path: 'html-debug/:projectId/:fileId',
component: HtmlDebugScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard]
}
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'downloads',
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'admin',
children: [
{ path: '', redirectTo: 'project-templates', pathMatch: 'full' },
{
path: 'project-templates',
children: [
{
path: '',
component: RuleSetsListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: ':ruleSetId',
children: [
{
path: 'dictionaries',
children: [
{
path: '',
component: DictionaryListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: ':type',
component: DictionaryOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
},
{
path: 'rules',
component: RulesScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'file-attributes',
component: FileAttributesListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'watermark',
component: WatermarkScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'default-colors',
component: DefaultColorsScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{ path: '', redirectTo: 'dictionaries', pathMatch: 'full' }
]
}
]
},
{
path: 'users',
component: UserListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'license-info',
component: LicenseInformationScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'digital-signature',
component: DigitalSignatureScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'audit',
component: AuditScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
}
]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http';
@ -38,20 +38,16 @@ import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { AuthModule } from './auth/auth.module';
import { FileUploadDownloadModule } from './upload-download/file-upload-download.module';
import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
import { FullPageLoadingIndicatorComponent } from './components/full-page-loading-indicator/full-page-loading-indicator.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component';
import { InitialsAvatarComponent } from './components/initials-avatar/initials-avatar.component';
import { StatusBarComponent } from './components/status-bar/status-bar.component';
import { LogoComponent } from './logo/logo.component';
import { CompositeRouteGuard } from './utils/composite-route.guard';
import { AppStateGuard } from './state/app-state.guard';
import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
import { AuthGuard } from './auth/auth.guard';
import { AuthErrorComponent } from './screens/auth-error/auth-error.component';
import { RedRoleGuard } from './auth/red-role.guard';
import { MatListModule } from '@angular/material/list';
import { AssignOwnerDialogComponent } from './dialogs/assign-owner-dialog/assign-owner-dialog.component';
import { MatDatepickerModule } from '@angular/material/datepicker';
@ -61,28 +57,28 @@ import { HumanizePipe } from './utils/humanize.pipe';
import { CommentsComponent } from './components/comments/comments.component';
import { ManualAnnotationDialogComponent } from './dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
import { ToastComponent } from './components/toast/toast.component';
import { FilterComponent } from './common/filter/filter.component';
import { FilterComponent } from './components/filter/filter.component';
import { TableColNameComponent } from './components/table-col-name/table-col-name.component';
import { ProjectDetailsComponent } from './screens/project-overview-screen/project-details/project-details.component';
import { ProjectDetailsComponent } from './components/project-details/project-details.component';
import { PageIndicatorComponent } from './screens/file/page-indicator/page-indicator.component';
import { NeedsWorkBadgeComponent } from './components/needs-work-badge/needs-work-badge.component';
import { ProjectListingEmptyComponent } from './screens/empty-states/project-listing-empty/project-listing-empty.component';
import { ProjectListingEmptyComponent } from './components/empty-states/project-listing-empty/project-listing-empty.component';
import { AnnotationActionsComponent } from './screens/file/annotation-actions/annotation-actions.component';
import { ProjectListingDetailsComponent } from './screens/project-listing-screen/project-listing-details/project-listing-details.component';
import { FileActionsComponent } from './common/file-actions/file-actions.component';
import { ProjectListingDetailsComponent } from './components/project-listing-details/project-listing-details.component';
import { FileActionsComponent } from './components/file-actions/file-actions.component';
import { TypeAnnotationIconComponent } from './components/type-annotation-icon/type-annotation-icon.component';
import { TypeFilterComponent } from './components/type-filter/type-filter.component';
import { DictionaryAnnotationIconComponent } from './components/dictionary-annotation-icon/dictionary-annotation-icon.component';
import { BulkActionsComponent } from './screens/project-overview-screen/bulk-actions/bulk-actions.component';
import { ProjectOverviewBulkActionsComponent } from './components/bulk-actions/project-overview-bulk-actions.component';
import { HttpCacheInterceptor } from '@redaction/red-cache';
import { HiddenActionComponent } from './common/hidden-action/hidden-action.component';
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
import { IconButtonComponent } from './components/buttons/icon-button/icon-button.component';
import { UserButtonComponent } from './components/buttons/user-button/user-button.component';
import { CircleButtonComponent } from './components/buttons/circle-button/circle-button.component';
import { ChevronButtonComponent } from './components/buttons/chevron-button/chevron-button.component';
import { DictionaryListingScreenComponent } from './screens/admin/dictionary-listing-screen/dictionary-listing-screen.component';
import { SyncWidthDirective } from './utils/sync-width.directive';
import { AddEditDictionaryDialogComponent } from './screens/admin/dictionary-listing-screen/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
import { AddEditDictionaryDialogComponent } from './dialogs/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
import { DictionaryOverviewScreenComponent } from './screens/admin/dictionary-overview-screen/dictionary-overview-screen.component';
import { ColorPickerModule } from 'ngx-color-picker';
import { AceEditorModule } from 'ng2-ace-editor';
@ -94,208 +90,42 @@ import { RulesScreenComponent } from './screens/admin/rules-screen/rules-screen.
import { WatermarkScreenComponent } from './screens/admin/watermark-screen/watermark-screen.component';
import { PdfViewerScreenComponent } from './screens/pdf-viewer-screen/pdf-viewer-screen.component';
import { HtmlDebugScreenComponent } from './screens/html-debug-screen/html-debug-screen.component';
import { ProjectListingActionsComponent } from './screens/project-listing-screen/project-listing-actions/project-listing-actions.component';
import { ProjectListingActionsComponent } from './components/project-listing-actions/project-listing-actions.component';
import { HasScrollbarDirective } from './utils/has-scrollbar.directive';
import { RuleSetsListingScreenComponent } from './screens/admin/rule-sets-listing-screen/rule-sets-listing-screen.component';
import { AddEditRuleSetDialogComponent } from './screens/admin/rule-sets-listing-screen/add-edit-rule-set-dialog/add-edit-rule-set-dialog.component';
import { AddEditRuleSetDialogComponent } from './dialogs/add-edit-rule-set-dialog/add-edit-rule-set-dialog.component';
import { RuleSetActionsComponent } from './components/rule-set-actions/rule-set-actions.component';
import { TabsComponent } from './components/rule-set-view-switch/tabs.component';
import { MatSliderModule } from '@angular/material/slider';
import { PendingChangesGuard } from './utils/can-deactivate.guard';
import { OverwriteFilesDialogComponent } from './dialogs/overwrite-files-dialog/overwrite-files-dialog.component';
import { KeycloakService } from 'keycloak-angular';
import { FileDownloadBtnComponent } from './components/buttons/file-download-btn/file-download-btn.component';
import { LicenseInformationScreenComponent } from './screens/admin/license-information-screen/license-information-screen.component';
import { DefaultColorsScreenComponent } from './screens/admin/default-colors-screen/default-colors-screen.component';
import { EditColorDialogComponent } from './screens/admin/default-colors-screen/edit-color-dialog/edit-color-dialog.component';
import { EditColorDialogComponent } from './dialogs/edit-color-dialog/edit-color-dialog.component';
import { DownloadsListScreenComponent } from './screens/downloads-list-screen/downloads-list-screen.component';
import { DigitalSignatureScreenComponent } from './screens/admin/digital-signature-screen/digital-signature-screen.component';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { RemoveAnnotationsDialogComponent } from './dialogs/remove-annotations-dialog/remove-annotations-dialog.component';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { ComboChartComponent, ComboSeriesVerticalComponent } from './screens/admin/license-information-screen/combo-chart';
import { ComboChartComponent, ComboSeriesVerticalComponent } from './components/combo-chart';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { ForceRedactionDialogComponent } from './dialogs/force-redaction-dialog/force-redaction-dialog.component';
import { AuditScreenComponent } from './screens/admin/audit-screen/audit-screen.component';
import { PaginationComponent } from './components/pagination/pagination.component';
import { FileAttributesListingScreenComponent } from './screens/admin/file-attributes-listing-screen/file-attributes-listing-screen.component';
import { SearchInputComponent } from './components/search-input/search-input.component';
import { AppRoutingModule } from './app-routing.module';
import { AddEditFileAttributeDialogComponent } from './dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { ConfirmDeleteFileAttributeDialogComponent } from './dialogs/confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component';
import { DocumentInfoDialogComponent } from './dialogs/document-info-dialog/document-info-dialog.component';
import { DocumentInfoComponent } from './components/document-info/document-info.component';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
}
const routes = [
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'auth-error',
component: AuthErrorComponent,
canActivate: [AuthGuard]
},
{
path: 'pdf-preview/:projectId/:fileId',
component: PdfViewerScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard]
}
},
{
path: 'html-debug/:projectId/:fileId',
component: HtmlDebugScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard]
}
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'downloads',
component: DownloadsListScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'admin',
children: [
{ path: '', redirectTo: 'project-templates', pathMatch: 'full' },
{
path: 'project-templates',
children: [
{
path: '',
component: RuleSetsListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: ':ruleSetId',
children: [
{
path: 'dictionaries',
children: [
{
path: '',
component: DictionaryListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: ':type',
component: DictionaryOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
},
{
path: 'rules',
component: RulesScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'watermark',
component: WatermarkScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'default-colors',
component: DefaultColorsScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{ path: '', redirectTo: 'dictionaries', pathMatch: 'full' }
]
}
]
},
{
path: 'users',
component: UserListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'license-info',
component: LicenseInformationScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'digital-signature',
component: DigitalSignatureScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'audit',
component: AuditScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
}
]
}
];
const matImports = [
MatDialogModule,
MatNativeDateModule,
@ -356,7 +186,7 @@ const matImports = [
TypeAnnotationIconComponent,
TypeFilterComponent,
DictionaryAnnotationIconComponent,
BulkActionsComponent,
ProjectOverviewBulkActionsComponent,
FileActionsComponent,
HiddenActionComponent,
IconButtonComponent,
@ -392,7 +222,13 @@ const matImports = [
ComboSeriesVerticalComponent,
AuditScreenComponent,
PaginationComponent,
SearchInputComponent
FileAttributesListingScreenComponent,
SearchInputComponent,
AddEditFileAttributeDialogComponent,
ConfirmDeleteFileAttributeDialogComponent,
DocumentInfoDialogComponent,
DocumentInfoComponent,
FileWorkloadComponent
],
imports: [
BrowserModule,
@ -411,7 +247,7 @@ const matImports = [
deps: [HttpClient]
}
}),
RouterModule.forRoot(routes),
AppRoutingModule,
NgpSortModule,
...matImports,
ToastrModule.forRoot({

View File

@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core';
import { AppStateService } from '../../state/app-state.service';
import { UserPreferenceService } from '../../common/service/user-preference.service';
import { PermissionsService } from '../../common/service/permissions.service';
import { UserPreferenceService } from '../../utils/user-preference.service';
import { PermissionsService } from '../../utils/permissions.service';
@Component({
selector: 'redaction-admin-breadcrumbs',

View File

@ -1,20 +1,20 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { UserService } from '../../../user/user.service';
import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { FileManagementControllerService, ReanalysisControllerService } from '@redaction/red-ui-http';
import { DialogService } from '../../../dialogs/dialog.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { FileStatusWrapper } from '../../file/model/file-status.wrapper';
import { FileActionService } from '../../file/service/file-action.service';
import { DialogService } from '../../dialogs/dialog.service';
import { PermissionsService } from '../../utils/permissions.service';
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
import { FileActionService } from '../../screens/file/service/file-action.service';
import { Observable } from 'rxjs';
import { StatusOverlayService } from '../../../upload-download/status-overlay.service';
import { StatusOverlayService } from '../../upload-download/status-overlay.service';
@Component({
selector: 'redaction-bulk-actions',
templateUrl: './bulk-actions.component.html',
styleUrls: ['./bulk-actions.component.scss']
selector: 'redaction-project-overview-bulk-actions',
templateUrl: './project-overview-bulk-actions.component.html',
styleUrls: ['./project-overview-bulk-actions.component.scss']
})
export class BulkActionsComponent {
export class ProjectOverviewBulkActionsComponent {
@Input() selectedFileIds: string[];
@Output() private reload = new EventEmitter();
public loading = false;

View File

@ -7,7 +7,9 @@
[disabled]="disabled"
[class.small]="small"
[class.overlay]="showDot"
[class.dummy]="dummy"
mat-icon-button
[disableRipple]="dummy"
>
<mat-icon [svgIcon]="icon"></mat-icon>
</button>

View File

@ -36,4 +36,8 @@ button {
background-color: $yellow-2;
}
}
&.dummy {
cursor: default;
}
}

View File

@ -14,6 +14,7 @@ export class CircleButtonComponent implements OnInit {
@Input() disabled = false;
@Input() small = false;
@Input() type: 'default' | 'primary' | 'warn' | 'dark-bg' = 'default';
@Input() dummy = false;
@Output() action = new EventEmitter<any>();
constructor() {}

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { ProjectWrapper } from '../../../state/model/project.wrapper';
import { FileStatusWrapper } from '../../../screens/file/model/file-status.wrapper';
import { FileDownloadService } from '../../../upload-download/file-download.service';

View File

@ -0,0 +1,44 @@
<div class="right-title heading" translate="file-preview.tabs.document-info.label">
<div>
<redaction-circle-button
icon="red:edit"
(action)="edit()"
tooltipPosition="before"
tooltip="file-preview.tabs.document-info.edit"
></redaction-circle-button>
<redaction-circle-button
icon="red:close"
(action)="closeDocumentInfoView.emit()"
tooltipPosition="before"
tooltip="file-preview.tabs.document-info.close"
></redaction-circle-button>
</div>
</div>
<div class="right-content" redactionHasScrollbar>
<div class="section">
<div class="attribute" *ngFor="let attr of fileAttributesConfig">
<div class="small-label">{{ attr.label }}:</div>
<div>{{ file.fileAttributes.attributeIdToValue[attr.id] || '-' }}</div>
</div>
</div>
<div class="section small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:folder"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.project' | translate: { projectName: project.name } }}</span>
</div>
<div>
<mat-icon svgIcon="red:document"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.pages' | translate: { pages: file.numberOfPages } }}</span>
</div>
<div>
<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="project.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.due' | translate: { date: project.project.dueDate | date: 'mediumDate' } }}</span>
</div>
</div>
</div>

View File

@ -0,0 +1,58 @@
@import '../../../assets/styles/red-variables';
@import '../../../assets/styles/red-mixins';
:host {
display: block;
position: absolute;
height: 100%;
background: white;
z-index: 1;
width: 100%;
@include inset-shadow;
}
.right-content {
flex-direction: column;
@include scroll-bar;
overflow: hidden;
&:hover {
overflow: auto;
}
&.has-scrollbar .section {
padding-right: 13px;
}
}
.right-title > div {
display: flex;
> redaction-circle-button:not(:last-child) {
margin-right: 2px;
}
}
.section {
padding: 24px;
flex-direction: column;
> div {
justify-content: flex-start;
}
&:not(:last-child) {
border-bottom: 1px solid $separator;
}
}
.attribute {
> .small-label {
margin-bottom: 6px;
}
&:not(:last-child) {
margin-bottom: 16px;
}
}

View File

@ -0,0 +1,34 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FileAttributeConfig, FileAttributesControllerService, FileStatus } from '@redaction/red-ui-http';
import { AppStateService } from '../../state/app-state.service';
import { DialogService } from '../../dialogs/dialog.service';
@Component({
selector: 'redaction-document-info',
templateUrl: './document-info.component.html',
styleUrls: ['./document-info.component.scss']
})
export class DocumentInfoComponent implements OnInit {
@Input() file: FileStatus;
@Output() closeDocumentInfoView = new EventEmitter();
public fileAttributesConfig: FileAttributeConfig[];
constructor(
private readonly _appStateService: AppStateService,
private readonly _fileAttributesService: FileAttributesControllerService,
private readonly _dialogService: DialogService
) {
this.fileAttributesConfig = this._appStateService.fileAttributesConfig.filter((attr) => attr.visible);
}
ngOnInit(): void {}
public get project() {
return this._appStateService.getProjectById(this.file.projectId);
}
public edit() {
this._dialogService.openDocumentInfoDialog(this.file);
}
}

View File

@ -1,12 +1,6 @@
<div class="empty-state-container">
<div class="heading-xl" translate="project-listing.no-projects"></div>
<button
(click)="addProjectRequest.emit()"
*ngIf="userService.isManager()"
class="add-project-btn"
color="primary"
mat-flat-button
>
<button (click)="addProjectRequest.emit()" *ngIf="userService.isManager()" class="add-project-btn" color="primary" mat-flat-button>
<mat-icon svgIcon="red:plus"></mat-icon>
<span translate="project-listing.add-new"></span>
</button>

View File

@ -49,6 +49,15 @@
>
</redaction-file-download-btn>
<redaction-circle-button
*ngIf="screen === 'file-preview'"
(action)="toggleViewDocumentInfo()"
tooltip="file-preview.document-info"
tooltipPosition="before"
icon="red:status-info"
[attr.aria-expanded]="activeDocumentInfo"
></redaction-circle-button>
<!-- Ready for approval-->
<redaction-circle-button
(action)="setFileUnderApproval($event, fileStatus)"

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PermissionsService } from '../service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
import { DialogService } from '../../dialogs/dialog.service';
import { AppStateService } from '../../state/app-state.service';
@ -14,6 +14,7 @@ import { DEFAULT_RUL_SET_UUID } from '../../utils/rule-set-default';
})
export class FileActionsComponent implements OnInit {
@Input() fileStatus: FileStatusWrapper;
@Input() activeDocumentInfo: boolean;
@Output() actionPerformed = new EventEmitter<string>();
actionMenuOpen: boolean;
@ -40,6 +41,10 @@ export class FileActionsComponent implements OnInit {
}
}
public toggleViewDocumentInfo() {
this.actionPerformed.emit('view-document-info');
}
public get tooltipPosition() {
return this.screen === 'file-preview' ? 'below' : 'above';
}

View File

@ -0,0 +1,133 @@
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<redaction-filter
(filtersChanged)="filtersChanged($event)"
[chevron]="true"
[filterTemplate]="annotationFilterTemplate"
[actionsTemplate]="annotationFilterActionTemplate"
[filters]="annotationFilters"
></redaction-filter>
</div>
</div>
<div class="right-content">
<div
#quickNavigation
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="pagesPanelActive"
class="quick-navigation"
tabindex="0"
>
<div
class="jump"
[class.disabled]="!quickScrollFirstEnabled"
[matTooltip]="'file-preview.quick-nav.jump-first' | translate"
matTooltipPosition="above"
(click)="quickScrollFirstEnabled && scrollQuickNavFirst()"
>
<mat-icon svgIcon="red:nav-first"></mat-icon>
</div>
<div class="pages" (scroll)="computeQuickNavButtonsState()" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of displayedPages"
[active]="pageNumber === activeViewerPage"
[number]="pageNumber"
[viewedPages]="fileData.viewedPages"
>
</redaction-page-indicator>
</div>
<div
class="jump"
[class.disabled]="!quickScrollLastEnabled"
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
matTooltipPosition="above"
(click)="scrollQuickNavLast()"
>
<mat-icon svgIcon="red:nav-last"></mat-icon>
</div>
</div>
<div style="overflow: hidden; width: 100%;">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
<span [translate]="displayedAnnotations[activeViewerPage]?.annotations?.length === 1 ? 'annotation' : 'annotations'"></span
></span>
</div>
<div
#annotationsElement
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
redactionHasScrollbar
class="annotations"
tabindex="1"
>
<div *ngIf="!displayedAnnotations[activeViewerPage]" class="heading-l no-annotations">
{{ 'file-preview.no-annotations-for-page' | translate }}
</div>
<div
(click)="annotationClicked(annotation)"
class="annotation-wrapper"
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations"
attr.annotation-id="{{ annotation.id }}"
attr.annotation-page="{{ activeViewerPage }}"
[class.active]="annotationIsSelected(annotation)"
>
<div class="active-marker"></div>
<div class="annotation" [class.removed]="annotation.isChangeLogRemoved">
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2">
<div class="details">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
</div>
<div *ngIf="annotation.dictionary && annotation.dictionary !== 'manual'">
<strong
><span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.content && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.content }}
</div>
</div>
<ng-container [ngTemplateOutlet]="annotationActionsTemplate" [ngTemplateOutletContext]="{ annotation: annotation }"> </ng-container>
<!-- <redaction-annotation-actions-->
<!-- (annotationsChanged)="annotationsChangedByReviewAction($event)"-->
<!-- [annotation]="annotation"-->
<!-- [canPerformAnnotationActions]="canPerformAnnotationActions"-->
<!-- [viewer]="activeViewer"-->
<!-- ></redaction-annotation-actions>-->
</div>
</redaction-hidden-action>
<redaction-comments [annotation]="annotation"></redaction-comments>
</div>
</div>
</div>
</div>
</div>
<ng-template #annotationFilterTemplate let-filter="filter">
<redaction-type-filter *ngIf="filter.topLevelFilter" [filter]="filter"></redaction-type-filter>
<ng-container *ngIf="!filter.topLevelFilter">
<redaction-dictionary-annotation-icon [dictionaryKey]="filter.key"></redaction-dictionary-annotation-icon>
{{ filter.key | humanize: false }}
</ng-container>
</ng-template>
<ng-template #annotationFilterActionTemplate let-filter="filter">
<ng-container *ngIf="filter.key === 'skipped'">
<redaction-circle-button
[icon]="hideSkipped ? 'red:visibility-off' : 'red:visibility'"
(action)="toggleSkipped.emit($event)"
class="skipped-toggle-button"
>
</redaction-circle-button>
</ng-container>
</ng-template>

View File

@ -0,0 +1,134 @@
@import '../../../assets/styles/red-variables';
@import '../../../assets/styles/red-mixins';
.right-content {
.no-annotations {
padding: 24px;
text-align: center;
}
.quick-navigation,
.annotations {
overflow-y: scroll;
outline: none;
&.active-panel {
background-color: #fafafa;
}
}
.quick-navigation {
border-right: 1px solid $separator;
min-width: 61px;
overflow: hidden;
display: flex;
flex-direction: column;
.jump {
min-height: 32px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.25s;
&:not(.disabled):hover {
background-color: $grey-6;
}
mat-icon {
width: 16px;
height: 16px;
}
&.disabled {
cursor: default;
mat-icon {
opacity: 0.3;
}
}
}
.pages {
@include no-scroll-bar();
overflow: auto;
flex: 1;
}
}
.page-separator {
border-bottom: 1px solid $separator;
height: 32px;
box-sizing: border-box;
padding: 0 10px;
display: flex;
align-items: center;
background-color: $grey-6;
}
.annotations {
overflow: hidden;
width: 100%;
height: calc(100% - 32px);
.annotation-wrapper {
display: flex;
border-bottom: 1px solid $separator;
.active-marker {
min-width: 4px;
min-height: 100%;
}
&.active {
.active-marker {
background-color: $primary;
}
}
.annotation {
padding: 10px 21px 10px 6px;
font-size: 11px;
line-height: 14px;
cursor: pointer;
display: flex;
flex-direction: column;
&.removed {
text-decoration: line-through;
color: $grey-7;
}
.details {
display: flex;
position: relative;
}
redaction-type-annotation-icon {
margin-top: 6px;
margin-right: 10px;
}
}
&:hover {
background-color: #f9fafb;
::ng-deep .annotation-actions {
display: flex;
}
}
}
&:hover {
overflow-y: auto;
@include scroll-bar;
}
&.has-scrollbar:hover {
.annotation {
padding-right: 10px;
}
}
}
}

View File

@ -0,0 +1,296 @@
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core';
import { FilterModel } from '../filter/model/filter.model';
import { AnnotationWrapper } from '../../screens/file/model/annotation.wrapper';
import { AnnotationProcessingService } from '../../screens/file/service/annotation-processing.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import { debounce } from '../../utils/debounce';
import { FileDataModel } from '../../screens/file/model/file-data.model';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@Component({
selector: 'redaction-file-workload',
templateUrl: './file-workload.component.html',
styleUrls: ['./file-workload.component.scss']
})
export class FileWorkloadComponent {
public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
private _annotations: AnnotationWrapper[];
@Input()
set annotations(value: AnnotationWrapper[]) {
this._annotations = value;
// this.computeQuickNavButtonsState();
}
@Input() selectedAnnotations: AnnotationWrapper[];
@Input() activeViewerPage: number;
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
@Input() dialogRef: MatDialogRef<any>;
@Input() annotationFilters: FilterModel[];
@Input() fileData: FileDataModel;
@Input() hideSkipped: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>;
@Output() selectAnnotation = new EventEmitter<AnnotationWrapper>();
@Output() selectPage = new EventEmitter<number>();
@Output() toggleSkipped = new EventEmitter<any>();
public quickScrollFirstEnabled = false;
public quickScrollLastEnabled = false;
public displayedPages: number[] = [];
public pagesPanelActive = true;
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
constructor(private _changeDetectorRef: ChangeDetectorRef, private _annotationProcessingService: AnnotationProcessingService) {}
private get firstSelectedAnnotation() {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
}
private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
if (elements.length > 0) {
scrollIntoView(elements[0], {
behavior: 'smooth',
scrollMode: mode,
block: 'start',
inline: 'start'
});
}
}
public annotationIsSelected(annotation: AnnotationWrapper) {
return this.selectedAnnotations?.find((a) => a.id === annotation.id);
}
public logAnnotation(annotation: AnnotationWrapper) {
console.log(annotation);
}
@debounce(0)
public filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters);
this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
this.computeQuickNavButtonsState();
this._changeDetectorRef.markForCheck();
}
public computeQuickNavButtonsState() {
setTimeout(() => {
const element: HTMLElement = this._quickNavigationElement.nativeElement.querySelector(`#pages`);
const { scrollTop, scrollHeight, clientHeight } = element;
this.quickScrollFirstEnabled = scrollTop !== 0;
this.quickScrollLastEnabled = scrollHeight !== scrollTop + clientHeight;
}, 0);
}
public annotationClicked(annotation: AnnotationWrapper) {
this.pagesPanelActive = false;
this.selectAnnotation.emit(annotation);
}
@HostListener('window:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent) {
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN) {
return;
}
if ($event.key === 'ArrowLeft') {
this.pagesPanelActive = true;
return;
}
if ($event.key === 'ArrowRight') {
this.pagesPanelActive = false;
// if we activated annotationsPanel - select first annotation from this page in case there is no
// selected annotation on this page
if (!this.pagesPanelActive) {
this._selectFirstAnnotationOnCurrentPageIfNecessary();
}
return;
}
if (!this.pagesPanelActive) {
this._navigateAnnotations($event);
} else {
this._navigatePages($event);
}
this._changeDetectorRef.detectChanges();
}
public scrollAnnotations() {
if (this.firstSelectedAnnotation?.pageNumber === this.activeViewerPage) {
return;
}
this.scrollAnnotationsToPage(this.activeViewerPage, 'always');
}
public scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
@debounce()
public scrollToSelectedAnnotation() {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) {
return;
}
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.firstSelectedAnnotation.id}"].active`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
public scrollQuickNavigation() {
let quickNavPageIndex = this.displayedPages.findIndex((p) => p >= this.activeViewerPage);
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) {
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
}
this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]);
}
public scrollQuickNavFirst() {
if (this.displayedPages.length > 0) {
this._scrollQuickNavigationToPage(this.displayedPages[0]);
}
}
public scrollQuickNavLast() {
if (this.displayedPages.length > 0) {
this._scrollQuickNavigationToPage(this.displayedPages[this.displayedPages.length - 1]);
}
}
public pageSelectedByClick($event: number) {
this.pagesPanelActive = true;
this.selectPage.emit($event);
}
public preventKeyDefault($event: KeyboardEvent) {
if (COMMAND_KEY_ARRAY.includes($event.key)) {
$event.preventDefault();
}
}
private _selectFirstAnnotationOnCurrentPageIfNecessary() {
if (
(!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) &&
this.displayedPages.indexOf(this.activeViewerPage) >= 0
) {
this.selectAnnotation.emit(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
}
}
private _navigateAnnotations($event: KeyboardEvent) {
if (!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) {
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
if (pageIdx !== -1) {
// Displayed page has annotations
this.selectAnnotation.emit(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
} else {
// Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') {
const nextPage = this._nextPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation.emit(this.displayedAnnotations[nextPage].annotations[0]);
} else {
const prevPage = this._prevPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false;
const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
this.selectAnnotation.emit(prevPageAnnotations[prevPageAnnotations.length - 1]);
}
}
} else {
const page = this.firstSelectedAnnotation.pageNumber;
const pageIdx = this.displayedPages.indexOf(page);
const annotationsOnPage = this.displayedAnnotations[page].annotations;
const idx = annotationsOnPage.findIndex((a) => a.id === this.firstSelectedAnnotation.id);
if ($event.key === 'ArrowDown') {
if (idx + 1 !== annotationsOnPage.length) {
// If not last item in page
this.selectAnnotation.emit(annotationsOnPage[idx + 1]);
} else if (pageIdx + 1 < this.displayedPages.length) {
// If not last page
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation.emit(nextPageAnnotations[0]);
}
} else {
if (idx !== 0) {
// If not first item in page
this.selectAnnotation.emit(annotationsOnPage[idx - 1]);
} else if (pageIdx) {
// If not first page
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation.emit(prevPageAnnotations[prevPageAnnotations.length - 1]);
}
}
}
}
private _navigatePages($event: KeyboardEvent) {
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
if ($event.key === 'ArrowDown') {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== this.displayedPages.length - 1) {
this.selectPage.emit(this.displayedPages[pageIdx + 1]);
}
} else {
// If active page doesn't have annotations
const nextPage = this._nextPageWithAnnotations();
if (nextPage) {
this.selectPage.emit(nextPage);
}
}
} else {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== 0) {
this.selectPage.emit(this.displayedPages[pageIdx - 1]);
}
} else {
// If active page doesn't have annotations
const prevPage = this._prevPageWithAnnotations();
if (prevPage) {
this.selectPage.emit(prevPage);
}
}
}
}
private _nextPageWithAnnotations() {
let idx = 0;
for (const page of this.displayedPages) {
if (page > this.activeViewerPage) {
break;
}
++idx;
}
return idx < this.displayedPages.length ? this.displayedPages[idx] : null;
}
private _prevPageWithAnnotations() {
let idx = this.displayedPages.length - 1;
for (const page of this.displayedPages.reverse()) {
if (page < this.activeViewerPage) {
this.selectPage.emit(this.displayedPages[idx]);
this.scrollAnnotations();
break;
}
--idx;
}
return idx >= 0 ? this.displayedPages[idx] : null;
}
private _scrollQuickNavigationToPage(page: number) {
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
}

View File

@ -1,7 +1,7 @@
import { FilterModel } from '../model/filter.model';
import { FileStatusWrapper } from '../../../screens/file/model/file-status.wrapper';
import { ProjectWrapper } from '../../../state/model/project.wrapper';
import { PermissionsService } from '../../service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters);

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { AppStateService } from '../../state/app-state.service';
import { PermissionsService } from '../../common/service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
import { ProjectWrapper } from '../../state/model/project.wrapper';

View File

@ -58,7 +58,7 @@
</div>
</div>
<div class="pb-32 small-label stats-subtitle">
<div class="pb-32 small-label stats-subtitle" [class.mt-24]="!hasFiles">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
<span>{{ 'project-overview.project-details.stats.documents' | translate: { count: appStateService.activeProject.files.length } }}</span>

View File

@ -1,4 +1,4 @@
@import '../../../../assets/styles/red-variables';
@import '../../../assets/styles/red-variables';
.header-wrapper {
display: flex;

View File

@ -1,13 +1,13 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { groupBy } from '../../../utils/functions';
import { DoughnutChartConfig } from '../../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { DialogService } from '../../../dialogs/dialog.service';
import { AppStateService } from '../../state/app-state.service';
import { groupBy } from '../../utils/functions';
import { DoughnutChartConfig } from '../simple-doughnut-chart/simple-doughnut-chart.component';
import { DialogService } from '../../dialogs/dialog.service';
import { Router } from '@angular/router';
import { FilterModel } from '../../../common/filter/model/filter.model';
import { PermissionsService } from '../../../common/service/permissions.service';
import { TranslateChartService } from '../../../utils/translate-chart.service';
import { StatusSorter } from '../../../common/sorters/status-sorter';
import { FilterModel } from '../filter/model/filter.model';
import { PermissionsService } from '../../utils/permissions.service';
import { TranslateChartService } from '../../utils/translate-chart.service';
import { StatusSorter } from '../../utils/sorters/status-sorter';
@Component({
selector: 'redaction-project-details',

View File

@ -36,5 +36,5 @@
>
</redaction-circle-button>
<redaction-file-download-btn [file]="project.files" [project]="project"> </redaction-file-download-btn>
<redaction-file-download-btn [file]="project.files" [project]="project" type="dark-bg"> </redaction-file-download-btn>
</div>

View File

@ -1,12 +1,12 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { ProjectWrapper } from '../../../state/model/project.wrapper';
import { StatusSorter } from '../../../common/sorters/status-sorter';
import { download } from '../../../utils/file-download-utils';
import { computerize } from '../../../utils/functions';
import { PermissionsService } from '../../utils/permissions.service';
import { ProjectWrapper } from '../../state/model/project.wrapper';
import { StatusSorter } from '../../utils/sorters/status-sorter';
import { download } from '../../utils/file-download-utils';
import { computerize } from '../../utils/functions';
import { FileManagementControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../state/app-state.service';
import { DialogService } from '../../../dialogs/dialog.service';
import { AppStateService } from '../../state/app-state.service';
import { DialogService } from '../../dialogs/dialog.service';
@Component({
selector: 'redaction-project-listing-actions',

View File

@ -1,4 +1,4 @@
@import '../../../../assets/styles/red-variables';
@import '../../../assets/styles/red-variables';
:host {
flex: 1;

View File

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DoughnutChartConfig } from '../../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { AppStateService } from '../../../state/app-state.service';
import { FilterModel } from '../../../common/filter/model/filter.model';
import { DoughnutChartConfig } from '../simple-doughnut-chart/simple-doughnut-chart.component';
import { AppStateService } from '../../state/app-state.service';
import { FilterModel } from '../filter/model/filter.model';
@Component({
selector: 'redaction-project-listing-details',

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DialogService } from '../../dialogs/dialog.service';
import { PermissionsService } from '../../common/service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
import { AppStateService } from '../../state/app-state.service';
import { Router } from '@angular/router';

View File

@ -1,7 +1,7 @@
<ng-container *ngFor="let tab of tabs">
<div
class="red-tab"
*ngIf="!tab.onlyDevMode || userPreferenceService.areDevFeaturesEnabled"
*ngIf="(!tab.onlyAdmin || permissionsService.isAdmin()) && (!tab.onlyDevMode || userPreferenceService.areDevFeaturesEnabled)"
(click)="switchView(tab.screen)"
[class.active]="tab.screen === screen"
>

View File

@ -1,7 +1,8 @@
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AppStateService } from '../../state/app-state.service';
import { UserPreferenceService } from '../../common/service/user-preference.service';
import { UserPreferenceService } from '../../utils/user-preference.service';
import { PermissionsService } from '../../utils/permissions.service';
@Component({
selector: 'redaction-tabs',
@ -9,17 +10,19 @@ import { UserPreferenceService } from '../../common/service/user-preference.serv
styleUrls: ['./tabs.component.scss']
})
export class TabsComponent implements OnInit {
@Input() public screen: 'rules' | 'dictionaries' | 'watermark' | 'default-colors';
@Input() public screen: 'rules' | 'dictionaries' | 'watermark' | 'default-colors' | 'file-attributes';
public tabs: { screen: string; onlyDevMode?: boolean; label?: string }[] = [
public tabs: { screen: string; onlyDevMode?: boolean; onlyAdmin?: boolean; label?: string }[] = [
{ screen: 'dictionaries' },
{ screen: 'rules', onlyDevMode: true, label: 'rule-editor' },
{ screen: 'default-colors' },
{ screen: 'watermark' }
{ screen: 'watermark' },
{ screen: 'file-attributes', onlyAdmin: true, onlyDevMode: true }
];
constructor(
public readonly userPreferenceService: UserPreferenceService,
public readonly permissionsService: PermissionsService,
private readonly _router: Router,
private readonly _appStateService: AppStateService
) {}

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Color } from '../../utils/types';
import { FilterModel } from '../../common/filter/model/filter.model';
import { FilterModel } from '../filter/model/filter.model';
export class DoughnutChartConfig {
value: number;

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { PermissionsService } from '../../common/service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
@Component({
selector: 'redaction-team-members',

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { FilterModel } from '../../common/filter/model/filter.model';
import { FilterModel } from '../filter/model/filter.model';
import { AppStateService } from '../../state/app-state.service';
import { DEFAULT_RUL_SET_UUID } from '../../utils/rule-set-default';

View File

@ -1,4 +1,4 @@
@import '../../../../../assets/styles/red-variables';
@import '../../../assets/styles/red-variables';
.first-row {
display: flex;

View File

@ -1,10 +1,10 @@
import { Component, Inject } from '@angular/core';
import { AppStateService } from '../../../../state/app-state.service';
import { AppStateService } from '../../state/app-state.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { Observable } from 'rxjs';
import { NotificationService, NotificationType } from '../../../../notification/notification.service';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
@Component({

View File

@ -0,0 +1,39 @@
<section class="dialog">
<div class="dialog-header heading-l">
{{ (fileAttribute ? 'add-edit-file-attribute.title.edit' : 'add-edit-file-attribute.title.new') | translate: { name: fileAttribute?.label } }}
</div>
<form (submit)="saveFileAttribute()" [formGroup]="fileAttributeForm">
<div class="dialog-content">
<div class="red-input-group required w-300">
<label translate="add-edit-file-attribute.form.name"></label>
<input formControlName="label" name="label" type="text" placeholder="{{ 'add-edit-file-attribute.form.name-placeholder' | translate }}" />
</div>
<div class="red-input-group required w-300">
<label translate="add-edit-file-attribute.form.column-header"></label>
<input
formControlName="csvColumnHeader"
name="csvColumnHeader"
type="text"
placeholder="{{ 'add-edit-file-attribute.form.column-header-placeholder' | translate }}"
/>
</div>
<div class="red-input-group ignore-label-styles">
<mat-slide-toggle formControlName="readonly" color="primary">{{ 'add-edit-file-attribute.form.read-only' | translate }}</mat-slide-toggle>
</div>
<div class="red-input-group ignore-label-styles">
<mat-slide-toggle formControlName="visible" color="primary">{{ 'add-edit-file-attribute.form.visible' | translate }}</mat-slide-toggle>
</div>
</div>
<div class="dialog-actions">
<button [disabled]="fileAttributeForm.invalid || !changed" color="primary" mat-flat-button type="submit">
{{ 'add-edit-file-attribute.save' | translate }}
</button>
</div>
</form>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -0,0 +1,60 @@
import { Component, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '../../state/app-state.service';
import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'redaction-add-edit-file-attribute-dialog',
templateUrl: './add-edit-file-attribute-dialog.component.html',
styleUrls: ['./add-edit-file-attribute-dialog.component.scss']
})
export class AddEditFileAttributeDialogComponent {
public fileAttributeForm: FormGroup;
public fileAttribute: FileAttributeConfig;
public ruleSetId: string;
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesControllerService,
public dialogRef: MatDialogRef<AddEditFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { fileAttribute: FileAttributeConfig; ruleSetId: string }
) {
this.fileAttribute = data.fileAttribute;
this.ruleSetId = data.ruleSetId;
this.fileAttributeForm = this._formBuilder.group({
label: [this.fileAttribute?.label, Validators.required],
csvColumnHeader: [this.fileAttribute?.csvColumnHeader, Validators.required],
readonly: [this.fileAttribute ? !this.fileAttribute.editable : false],
visible: [this.fileAttribute?.visible]
});
}
public get changed(): boolean {
if (!this.fileAttribute) return true;
for (const key of Object.keys(this.fileAttributeForm.getRawValue())) {
if (key === 'readonly') {
if (this.fileAttribute.editable === this.fileAttributeForm.get(key).value) {
return true;
}
} else if (this.fileAttribute[key] !== this.fileAttributeForm.get(key).value) {
return true;
}
}
return false;
}
async saveFileAttribute() {
const fileAttribute: FileAttributeConfig = {
id: this.fileAttribute?.id,
editable: !this.fileAttributeForm.get('readonly').value,
...this.fileAttributeForm.getRawValue()
};
await this._fileAttributesService.setFileAttributesConfiguration(fileAttribute, this.ruleSetId).toPromise();
this.dialogRef.close(fileAttribute);
}
}

View File

@ -1,5 +1,5 @@
import { Component, Inject } from '@angular/core';
import { AppStateService } from '../../../../state/app-state.service';
import { AppStateService } from '../../state/app-state.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as moment from 'moment';

View File

@ -0,0 +1,21 @@
<section class="dialog">
<div class="dialog-header heading-l">
{{ 'confirm-delete-file-attribute.title' | translate: { name: fileAttribute.label } }}
</div>
<div class="dialog-content">
<div class="heading" translate="confirm-delete-file-attribute.warning"></div>
<mat-checkbox *ngFor="let checkbox of checkboxes; let idx = index" [(ngModel)]="checkbox.value" color="primary">
{{ 'confirm-delete-file-attribute.checkbox-' + (idx + 1) | translate }}
</mat-checkbox>
</div>
<div class="dialog-actions">
<button [disabled]="!valid" color="primary" mat-flat-button (click)="deleteFileAttribute()">
{{ 'confirm-delete-file-attribute.delete' | translate }}
</button>
<div class="all-caps-label cancel" (click)="cancel()" translate="confirm-delete-file-attribute.cancel"></div>
</div>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -0,0 +1,13 @@
@import '../../../assets/styles/red-variables';
.dialog-header {
color: $primary;
}
.heading {
margin-bottom: 24px;
}
mat-checkbox:not(:last-of-type) {
margin-bottom: 6px;
}

View File

@ -0,0 +1,38 @@
import { Component, Inject } from '@angular/core';
import { AppStateService } from '../../state/app-state.service';
import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'redaction-confirm-delete-file-attribute-dialog',
templateUrl: './confirm-delete-file-attribute-dialog.component.html',
styleUrls: ['./confirm-delete-file-attribute-dialog.component.scss']
})
export class ConfirmDeleteFileAttributeDialogComponent {
public fileAttribute: FileAttributeConfig;
public ruleSetId: string;
public checkboxes = [{ value: false }, { value: false }];
constructor(
private readonly _appStateService: AppStateService,
private readonly _fileAttributesService: FileAttributesControllerService,
public dialogRef: MatDialogRef<ConfirmDeleteFileAttributeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { fileAttribute: FileAttributeConfig; ruleSetId: string }
) {
this.fileAttribute = data.fileAttribute;
this.ruleSetId = data.ruleSetId;
}
public get valid() {
return this.checkboxes[0].value && this.checkboxes[1].value;
}
async deleteFileAttribute() {
await this._fileAttributesService.deleteFileAttributesConfiguration(this.ruleSetId, this.fileAttribute.id).toPromise();
this.dialogRef.close(true);
}
public cancel() {
this.dialogRef.close();
}
}

View File

@ -3,6 +3,7 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import {
Colors,
DictionaryControllerService,
FileAttributeConfig,
FileManagementControllerService,
FileStatus,
ManualRedactionControllerService,
@ -21,12 +22,15 @@ import { AnnotationWrapper } from '../screens/file/model/annotation.wrapper';
import { ManualAnnotationDialogComponent } from './manual-redaction-dialog/manual-annotation-dialog.component';
import { ManualAnnotationService } from '../screens/file/service/manual-annotation.service';
import { ProjectWrapper } from '../state/model/project.wrapper';
import { AddEditDictionaryDialogComponent } from '../screens/admin/dictionary-listing-screen/add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
import { AddEditRuleSetDialogComponent } from '../screens/admin/rule-sets-listing-screen/add-edit-rule-set-dialog/add-edit-rule-set-dialog.component';
import { AddEditDictionaryDialogComponent } from './add-edit-dictionary-dialog/add-edit-dictionary-dialog.component';
import { AddEditRuleSetDialogComponent } from './add-edit-rule-set-dialog/add-edit-rule-set-dialog.component';
import { OverwriteFilesDialogComponent } from './overwrite-files-dialog/overwrite-files-dialog.component';
import { EditColorDialogComponent } from '../screens/admin/default-colors-screen/edit-color-dialog/edit-color-dialog.component';
import { EditColorDialogComponent } from './edit-color-dialog/edit-color-dialog.component';
import { RemoveAnnotationsDialogComponent } from './remove-annotations-dialog/remove-annotations-dialog.component';
import { ForceRedactionDialogComponent } from './force-redaction-dialog/force-redaction-dialog.component';
import { AddEditFileAttributeDialogComponent } from './add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { ConfirmDeleteFileAttributeDialogComponent } from './confirm-delete-file-attribute-dialog/confirm-delete-file-attribute-dialog.component';
import { DocumentInfoDialogComponent } from './document-info-dialog/document-info-dialog.component';
const dialogConfig = {
width: '662px',
@ -336,6 +340,62 @@ export class DialogService {
return ref;
}
public openAddEditFileAttributeDialog(
fileAttribute: FileAttributeConfig,
ruleSetId: string,
cb?: Function
): MatDialogRef<AddEditFileAttributeDialogComponent> {
const ref = this._dialog.open(AddEditFileAttributeDialogComponent, {
...dialogConfig,
data: { fileAttribute, ruleSetId },
autoFocus: true
});
ref.afterClosed().subscribe((result) => {
if (result && cb) {
cb(result);
}
});
return ref;
}
public openConfirmDeleteFileAttributeDialog(
fileAttribute: FileAttributeConfig,
ruleSetId: string,
cb?: Function
): MatDialogRef<ConfirmDeleteFileAttributeDialogComponent> {
const ref = this._dialog.open(ConfirmDeleteFileAttributeDialogComponent, {
...dialogConfig,
data: { fileAttribute, ruleSetId },
autoFocus: true
});
ref.afterClosed().subscribe((result) => {
if (result && cb) {
cb(result);
}
});
return ref;
}
public openDocumentInfoDialog(file: FileStatus, cb?: Function): MatDialogRef<DocumentInfoDialogComponent> {
const ref = this._dialog.open(DocumentInfoDialogComponent, {
...dialogConfig,
data: file,
autoFocus: true
});
ref.afterClosed().subscribe((result) => {
if (result && cb) {
cb(result);
}
});
return ref;
}
openRemoveAnnotationModal($event: MouseEvent, annotation: AnnotationWrapper, callback: () => void) {
$event?.stopPropagation();

View File

@ -0,0 +1,19 @@
<section *ngIf="!!documentInfoForm" class="dialog">
<div class="dialog-header heading-l" translate="document-info.title"></div>
<form (submit)="saveDocumentInfo()" [formGroup]="documentInfoForm">
<div class="dialog-content">
<div class="red-input-group w-300" *ngFor="let attr of attributes">
<label>{{ attr.label }}</label>
<input [formControlName]="attr.id" [name]="attr.id" type="text" />
</div>
</div>
<div class="dialog-actions">
<button [disabled]="documentInfoForm.invalid" color="primary" mat-flat-button type="submit">
{{ 'document-info.save' | translate }}
</button>
</div>
</form>
<redaction-circle-button icon="red:close" mat-dialog-close class="dialog-close"></redaction-circle-button>
</section>

View File

@ -0,0 +1,45 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FileAttributeConfig, FileAttributesControllerService, FileStatus } from '@redaction/red-ui-http';
import { AppStateService } from '../../state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ProjectWrapper } from '../../state/model/project.wrapper';
@Component({
selector: 'redaction-document-info-dialog',
templateUrl: './document-info-dialog.component.html',
styleUrls: ['./document-info-dialog.component.scss']
})
export class DocumentInfoDialogComponent implements OnInit {
public documentInfoForm: FormGroup;
public file: FileStatus;
public attributes: FileAttributeConfig[];
private _project: ProjectWrapper;
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesControllerService,
public dialogRef: MatDialogRef<DocumentInfoDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: FileStatus
) {
this.file = this.data;
this._project = this._appStateService.getProjectById(this.file.projectId);
}
async ngOnInit() {
this.attributes = (await this._fileAttributesService.getFileAttributesConfiguration(this._project.ruleSetId).toPromise()).fileAttributeConfigs.filter(
(attr) => attr.visible && attr.editable
);
const formConfig = this.attributes.reduce((acc, attr) => ({ ...acc, [attr.id]: [this.file.fileAttributes.attributeIdToValue[attr.id]] }), {});
this.documentInfoForm = this._formBuilder.group(formConfig);
}
public async saveDocumentInfo() {
const attributeIdToValue = { ...this.file.fileAttributes.attributeIdToValue, ...this.documentInfoForm.getRawValue() };
await this._fileAttributesService.setFileAttributes({ attributeIdToValue }, this.file.projectId, this.file.fileId).toPromise();
this.file.fileAttributes = { attributeIdToValue };
this.dialogRef.close(true);
}
}

View File

@ -1,7 +1,7 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Colors, DictionaryControllerService } from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '../../../../notification/notification.service';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';

View File

@ -7,7 +7,7 @@ import { NotificationService } from '../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '../../user/user.service';
import { ManualAnnotationService } from '../../screens/file/service/manual-annotation.service';
import { PermissionsService } from '../../common/service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
export interface LegalBasisOption {
label?: string;

View File

@ -9,7 +9,7 @@ import { UserService } from '../../user/user.service';
import { ManualRedactionEntryWrapper } from '../../screens/file/model/manual-redaction-entry.wrapper';
import { ManualAnnotationService } from '../../screens/file/service/manual-annotation.service';
import { ManualAnnotationResponse } from '../../screens/file/model/manual-annotation-response';
import { PermissionsService } from '../../common/service/permissions.service';
import { PermissionsService } from '../../utils/permissions.service';
export interface LegalBasisOption {
label?: string;

View File

@ -55,6 +55,7 @@ export class IconsModule {
'preview',
'radio-indeterminate',
'radio-selected',
'read-only',
'ready-for-approval',
'refresh',
'report',
@ -63,8 +64,9 @@ export class IconsModule {
'sort-asc',
'sort-desc',
'status',
'status-expand',
'status-collapse',
'status-expand',
'status-info',
'template',
'thumb-down',
'thumb-up',

View File

@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { AuditControllerService, AuditResponse, AuditSearchRequest } from '@redaction/red-ui-http';
import { TranslateService } from '@ngx-translate/core';

View File

@ -3,7 +3,7 @@ import { AppStateService } from '../../../state/app-state.service';
import { Colors, DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { ActivatedRoute } from '@angular/router';
import { SortingOption, SortingService } from '../../../utils/sorting.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { DialogService } from '../../../dialogs/dialog.service';
@Component({

View File

@ -42,7 +42,7 @@
{{ 'dictionary-listing.table-header.title' | translate: { length: displayedDictionaries.length } }}
</span>
<div class="dictionary-actions-container">
<div class="attributes-actions-container">
<redaction-search-input [form]="searchForm" [placeholder]="'dictionary-listing.search'"></redaction-search-input>
<div class="actions">
<redaction-icon-button
@ -111,11 +111,11 @@
</div>
</div>
<div class="rank small-label">
<div class="center small-label">
{{ dict.rank }}
</div>
<div class="analyzed">
<div class="center">
<redaction-annotation-icon [dictType]="dict" [type]="dict.hint ? 'circle' : 'square'"></redaction-annotation-icon>
</div>

View File

@ -4,7 +4,7 @@
.header-item {
padding: 0 16px 0 10px;
.dictionary-actions-container {
.attributes-actions-container {
display: flex;
flex: 1;
justify-content: flex-end;
@ -36,8 +36,7 @@ redaction-table-col-name::ng-deep {
align-items: center;
justify-content: flex-start;
&.analyzed,
&.rank {
&.center {
justify-content: center;
}

View File

@ -6,7 +6,7 @@ import { DialogService } from '../../../dialogs/dialog.service';
import { AppStateService } from '../../../state/app-state.service';
import { tap } from 'rxjs/operators';
import { forkJoin } from 'rxjs';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../utils/debounce';
import { ActivatedRoute } from '@angular/router';

View File

@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
import { DictionaryControllerService, TypeValue } from '@redaction/red-ui-http';
import { DialogService } from '../../../dialogs/dialog.service';
import { AppStateService } from '../../../state/app-state.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { ActivatedRoute, Router } from '@angular/router';
import { AceEditorComponent } from 'ng2-ace-editor';
import { debounce } from '../../../utils/debounce';

View File

@ -3,7 +3,7 @@ import { DigitalSignature, DigitalSignatureControllerService } from '@redaction/
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService, NotificationType } from '../../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { lastIndexOfEnd } from '../../../utils/functions';
@Component({

View File

@ -0,0 +1,128 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs class="flex-1"></redaction-admin-breadcrumbs>
<redaction-tabs [screen]="'file-attributes'"></redaction-tabs>
<div class="actions flex-1">
<redaction-circle-button [routerLink]="['../..']" tooltip="common.close" tooltipPosition="before" icon="red:close"></redaction-circle-button>
</div>
</div>
<div class="red-content-inner">
<div class="left-container">
<div class="header-item">
<div class="select-all-container">
<div
(click)="toggleSelectAll()"
[class.active]="areAllAttributesSelected"
class="select-oval always-visible"
*ngIf="!areAllAttributesSelected && !areSomeAttributesSelected"
></div>
<mat-icon
*ngIf="areAllAttributesSelected"
(click)="toggleSelectAll()"
class="selection-icon active"
svgIcon="red:radio-selected"
></mat-icon>
<mat-icon
*ngIf="areSomeAttributesSelected && !areAllAttributesSelected"
(click)="toggleSelectAll()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
</div>
<span class="all-caps-label">
{{ 'file-attributes-listing.table-header.title' | translate: { length: displayedAttributes.length } }}
</span>
<div class="attributes-actions-container">
<redaction-search-input [form]="searchForm" [placeholder]="'file-attributes-listing.search'"></redaction-search-input>
<div class="actions">
<redaction-icon-button
icon="red:plus"
(action)="openAddEditAttributeDialog($event)"
text="file-attributes-listing.add-new"
type="primary"
></redaction-icon-button>
</div>
</div>
</div>
<div [class.no-data]="noData" class="table-header" redactionSyncWidth="table-item">
<div class="select-oval-placeholder"></div>
<redaction-table-col-name
label="file-attributes-listing.table-col-names.name"
(toggleSort)="toggleSort($event)"
[activeSortingOption]="sortingOption"
[withSort]="true"
column="name"
></redaction-table-col-name>
<!-- <redaction-table-col-name label="file-attributes-listing.table-col-names.created-by" class="flex-center"></redaction-table-col-name>-->
<redaction-table-col-name label="file-attributes-listing.table-col-names.read-only" class="flex-center"></redaction-table-col-name>
<div></div>
<div class="scrollbar-placeholder"></div>
</div>
<div *ngIf="noData" class="no-data heading-l" translate="file-attributes-listing.no-data"></div>
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines -->
<div class="table-item" *cdkVirtualFor="let attribute of displayedAttributes | sortBy: sortingOption.order:sortingOption.column">
<div class="pr-0" (click)="toggleAttributeSelected($event, attribute)">
<div *ngIf="!isAttributeSelected(attribute)" class="select-oval"></div>
<mat-icon class="selection-icon active" *ngIf="isAttributeSelected(attribute)" svgIcon="red:radio-selected"></mat-icon>
</div>
<div>
{{ attribute.label }}
</div>
<!-- TODO-->
<!-- <div class="center">-->
<!-- <redaction-initials-avatar [userId]="attribute.userId" [withName]="true" size="large"></redaction-initials-avatar>-->
<!-- </div>-->
<div class="center">
<redaction-circle-button
*ngIf="!attribute.editable"
type="dark-bg"
icon="red:read-only"
tooltip="file-attributes-listing.read-only"
[dummy]="true"
></redaction-circle-button>
</div>
<div class="actions-container">
<div class="action-buttons">
<redaction-circle-button
(action)="openAddEditAttributeDialog($event, attribute)"
tooltip="file-attributes-listing.action.edit"
tooltipPosition="before"
type="dark-bg"
icon="red:edit"
>
</redaction-circle-button>
<redaction-circle-button
(action)="openConfirmDeleteAttributeDialog($event, attribute)"
tooltip="file-attributes-listing.action.delete"
tooltipPosition="before"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
</div>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
</div>
<div class="right-container"></div>
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -0,0 +1,54 @@
.page-header .actions {
display: flex;
justify-content: flex-end;
}
.header-item {
padding: 0 24px 0 10px;
}
redaction-table-col-name::ng-deep {
> div {
padding-left: 10px !important;
}
}
.left-container {
width: 100vw;
.header-item {
.attributes-actions-container {
display: flex;
flex: 1;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 16px;
}
}
}
cdk-virtual-scroll-viewport {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 3fr 11px;
.table-item {
> div:not(.scrollbar-placeholder) {
padding-left: 10px;
}
> div {
&.center {
align-items: center;
}
}
}
}
&.has-scrollbar:hover {
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: auto 1fr 1fr 3fr;
}
}
}
}

View File

@ -0,0 +1,120 @@
import { Component, OnInit } from '@angular/core';
import { PermissionsService } from '../../../utils/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FileAttributeConfig, FileAttributesControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../state/app-state.service';
import { ActivatedRoute } from '@angular/router';
import { debounce } from '../../../utils/debounce';
import { SortingOption, SortingService } from '../../../utils/sorting.service';
import { DialogService } from '../../../dialogs/dialog.service';
@Component({
selector: 'redaction-file-attributes-listing-screen',
templateUrl: './file-attributes-listing-screen.component.html',
styleUrls: ['./file-attributes-listing-screen.component.scss']
})
export class FileAttributesListingScreenComponent implements OnInit {
public searchForm: FormGroup;
public attributes: FileAttributeConfig[] = [];
public displayedAttributes: FileAttributeConfig[] = [];
public selectedFileAttributeIds: string[] = [];
public viewReady = false;
constructor(
public readonly permissionsService: PermissionsService,
public readonly _sortingService: SortingService,
private readonly _formBuilder: FormBuilder,
private readonly _fileAttributesService: FileAttributesControllerService,
private readonly _appStateService: AppStateService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _dialogService: DialogService
) {
this._appStateService.activateRuleSet(_activatedRoute.snapshot.params.ruleSetId);
this.searchForm = this._formBuilder.group({
query: ['']
});
this.searchForm.valueChanges.subscribe((value) => this._executeSearch(value));
}
async ngOnInit() {
await this._loadData();
}
private async _loadData() {
this.viewReady = false;
try {
const response = await this._fileAttributesService.getFileAttributesConfiguration(this._appStateService.activeRuleSetId).toPromise();
this.attributes = response?.fileAttributeConfigs || [];
} catch (e) {
} finally {
this._executeSearch();
this.viewReady = true;
}
}
public get noData(): boolean {
return this.displayedAttributes.length === 0;
}
public get sortingOption(): SortingOption {
return this._sortingService.getSortingOption('file-attributes-listing');
}
public toggleSort($event) {
this._sortingService.toggleSort('file-attributes-listing', $event);
}
@debounce(200)
private _executeSearch(value?: { query: string }) {
if (!value) {
value = { query: this.searchForm.get('query').value };
}
this.displayedAttributes = this.attributes.filter((attribute) => attribute.label.toLowerCase().includes(value.query.toLowerCase()));
}
public openAddEditAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
$event.stopPropagation();
this._dialogService.openAddEditFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async () => {
await this._loadData();
});
}
public openConfirmDeleteAttributeDialog($event: MouseEvent, fileAttribute?: FileAttributeConfig) {
$event.stopPropagation();
this._dialogService.openConfirmDeleteFileAttributeDialog(fileAttribute, this._appStateService.activeRuleSetId, async () => {
await this._loadData();
});
}
public toggleAttributeSelected($event: MouseEvent, attribute: FileAttributeConfig) {
$event.stopPropagation();
const idx = this.selectedFileAttributeIds.indexOf(attribute.id);
if (idx === -1) {
this.selectedFileAttributeIds.push(attribute.id);
} else {
this.selectedFileAttributeIds.splice(idx, 1);
}
}
public toggleSelectAll() {
if (this.areSomeAttributesSelected) {
this.selectedFileAttributeIds = [];
} else {
this.selectedFileAttributeIds = this.displayedAttributes.map((a) => a.id);
}
}
public get areAllAttributesSelected() {
return this.displayedAttributes.length !== 0 && this.selectedFileAttributeIds.length === this.displayedAttributes.length;
}
public get areSomeAttributesSelected() {
return this.selectedFileAttributeIds.length > 0;
}
public isAttributeSelected(attribute: FileAttributeConfig) {
return this.selectedFileAttributeIds.indexOf(attribute.id) !== -1;
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { LicenseReport, LicenseReportControllerService } from '@redaction/red-ui-http';
import { AppConfigService } from '../../../app-config/app-config.service';
import * as moment from 'moment';

View File

@ -2,11 +2,11 @@ import { Component, OnInit } from '@angular/core';
import { SortingOption, SortingService } from '../../../utils/sorting.service';
import { DialogService } from '../../../dialogs/dialog.service';
import { AppStateService } from '../../../state/app-state.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounce } from '../../../utils/debounce';
import { RuleSetModel } from '@redaction/red-ui-http';
import { UserPreferenceService } from '../../../common/service/user-preference.service';
import { UserPreferenceService } from '../../../utils/user-preference.service';
@Component({
selector: 'redaction-rule-sets-listing-screen',

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { AceEditorComponent } from 'ng2-ace-editor';
import { RulesControllerService, RuleSetModel } from '@redaction/red-ui-http';
import { NotificationService, NotificationType } from '../../../notification/notification.service';

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { UserService } from '../../../user/user.service';
import { User } from '@redaction/red-ui-http';
import { FormBuilder, FormGroup } from '@angular/forms';

View File

@ -5,14 +5,7 @@
<redaction-tabs [screen]="'watermark'"></redaction-tabs>
<div class="actions flex-1">
<redaction-circle-button
class="ml-6"
*ngIf="permissionsService.isUser()"
[routerLink]="['/ui/projects/']"
tooltip="common.close"
tooltipPosition="before"
icon="red:close"
></redaction-circle-button>
<redaction-circle-button [routerLink]="['../..']" tooltip="common.close" tooltipPosition="before" icon="red:close"></redaction-circle-button>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { AppStateService } from '../../../state/app-state.service';
import { environment } from '../../../../environments/environment';

View File

@ -2,8 +2,8 @@ import { Component } from '@angular/core';
import { UserService } from '../../user/user.service';
import { AppStateService } from '../../state/app-state.service';
import { LanguageService } from '../../i18n/language.service';
import { PermissionsService } from '../../common/service/permissions.service';
import { UserPreferenceService } from '../../common/service/user-preference.service';
import { PermissionsService } from '../../utils/permissions.service';
import { UserPreferenceService } from '../../utils/user-preference.service';
import { Router } from '@angular/router';
import { AppConfigService } from '../../app-config/app-config.service';
import { Title } from '@angular/platform-browser';

View File

@ -1,9 +1,9 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AnnotationWrapper } from '../model/annotation.wrapper';
import { AppStateService } from '../../../state/app-state.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { AnnotationPermissions } from '../model/annotation.permissions';
import { AnnotationActionsService } from '../../../common/service/annotation-actions.service';
import { AnnotationActionsService } from '../../../utils/annotation-actions.service';
import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
@Component({

View File

@ -129,7 +129,11 @@
</ng-container>
<div class="vertical-line"></div>
<redaction-file-actions (actionPerformed)="fileActionPerformed($event)" *ngIf="viewReady"></redaction-file-actions>
<redaction-file-actions
(actionPerformed)="fileActionPerformed($event)"
[activeDocumentInfo]="viewDocumentInfo"
*ngIf="viewReady"
></redaction-file-actions>
<redaction-circle-button
(action)="toggleFullScreen()"
@ -200,118 +204,28 @@
</div>
<div class="right-container">
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<redaction-filter
(filtersChanged)="filtersChanged($event)"
[chevron]="true"
[filterTemplate]="annotationFilterTemplate"
[actionsTemplate]="annotationFilterActionTemplate"
[filters]="annotationFilters"
></redaction-filter>
</div>
</div>
<div class="right-content">
<div
#quickNavigation
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="pagesPanelActive"
class="quick-navigation"
tabindex="0"
>
<div
class="jump"
[class.disabled]="!quickScrollFirstEnabled"
[matTooltip]="'file-preview.quick-nav.jump-first' | translate"
matTooltipPosition="above"
(click)="quickScrollFirstEnabled && scrollQuickNavFirst()"
>
<mat-icon svgIcon="red:nav-first"></mat-icon>
</div>
<div class="pages" (scroll)="computeQuickNavButtonsState()" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of displayedPages"
[active]="pageNumber === activeViewerPage"
[number]="pageNumber"
[viewedPages]="fileData.viewedPages"
>
</redaction-page-indicator>
</div>
<div
class="jump"
[class.disabled]="!quickScrollLastEnabled"
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
matTooltipPosition="above"
(click)="scrollQuickNavLast()"
>
<mat-icon svgIcon="red:nav-last"></mat-icon>
</div>
</div>
<redaction-document-info
*ngIf="viewReady && viewDocumentInfo"
[file]="fileData.fileStatus.fileStatus"
(closeDocumentInfoView)="viewDocumentInfo = false"
>
</redaction-document-info>
<div style="overflow: hidden; width: 100%;">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
<span [translate]="displayedAnnotations[activeViewerPage]?.annotations?.length === 1 ? 'annotation' : 'annotations'"></span
></span>
</div>
<div
#annotationsElement
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
redactionHasScrollbar
class="annotations"
tabindex="1"
>
<div *ngIf="!displayedAnnotations[activeViewerPage]" class="heading-l no-annotations">
{{ 'file-preview.no-annotations-for-page' | translate }}
</div>
<div
(click)="annotationClicked(annotation)"
class="annotation-wrapper"
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations"
attr.annotation-id="{{ annotation.id }}"
attr.annotation-page="{{ activeViewerPage }}"
[class.active]="annotationIsSelected(annotation)"
>
<div class="active-marker"></div>
<div class="annotation" [class.removed]="annotation.isChangeLogRemoved">
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2">
<div class="details">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
</div>
<div *ngIf="annotation.dictionary && annotation.dictionary !== 'manual'">
<strong
><span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.content && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.content }}
</div>
</div>
<redaction-annotation-actions
(annotationsChanged)="annotationsChangedByReviewAction($event)"
[annotation]="annotation"
[canPerformAnnotationActions]="canPerformAnnotationActions"
[viewer]="activeViewer"
></redaction-annotation-actions>
</div>
</redaction-hidden-action>
<redaction-comments [annotation]="annotation"></redaction-comments>
</div>
</div>
</div>
</div>
</div>
<redaction-file-workload
#fileWorkloadComponent
[annotations]="annotations"
[selectedAnnotations]="selectedAnnotations"
[activeViewerPage]="activeViewerPage"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
[dialogRef]="dialogRef"
[annotationFilters]="annotationFilters"
[fileData]="fileData"
[hideSkipped]="hideSkipped"
[annotationActionsTemplate]="annotationActionsTemplate"
(selectAnnotation)="selectAnnotation($event)"
(selectPage)="selectPage($event)"
(toggleSkipped)="toggleSkipped($event)"
></redaction-file-workload>
</div>
</div>
</section>
@ -323,17 +237,11 @@
</div>
</redaction-full-page-loading-indicator>
<ng-template #annotationFilterTemplate let-filter="filter">
<redaction-type-filter *ngIf="filter.topLevelFilter" [filter]="filter"></redaction-type-filter>
<ng-container *ngIf="!filter.topLevelFilter">
<redaction-dictionary-annotation-icon [dictionaryKey]="filter.key"></redaction-dictionary-annotation-icon>
{{ filter.key | humanize: false }}
</ng-container>
</ng-template>
<ng-template #annotationFilterActionTemplate let-filter="filter">
<ng-container *ngIf="filter.key === 'skipped'">
<redaction-circle-button [icon]="hideSkipped ? 'red:visibility-off' : 'red:visibility'" (action)="toggleSkipped($event)" class="skipped-toggle-button">
</redaction-circle-button>
</ng-container>
<ng-template #annotationActionsTemplate let-annotation="annotation">
<redaction-annotation-actions
(annotationsChanged)="annotationsChangedByReviewAction($event)"
[annotation]="annotation"
[canPerformAnnotationActions]="canPerformAnnotationActions"
[viewer]="activeViewer"
></redaction-annotation-actions>
</ng-template>

View File

@ -29,166 +29,154 @@
padding: 0;
width: 350px;
min-width: 350px;
position: relative;
.right-title {
::ng-deep .right-title {
height: 70px;
display: flex;
border-bottom: 1px solid $separator;
align-items: center;
justify-content: space-between;
padding: 0 24px;
.close-icon {
height: 14px;
width: 14px;
cursor: pointer;
}
> div {
display: flex;
redaction-circle-button {
margin-left: 2px;
}
}
}
.right-content {
::ng-deep .right-content {
height: calc(100% - 72px);
box-sizing: border-box;
display: flex;
.quick-navigation,
.annotations {
overflow-y: scroll;
outline: none;
&.active-panel {
background-color: #fafafa;
}
}
.quick-navigation {
border-right: 1px solid $separator;
min-width: 61px;
overflow: hidden;
display: flex;
flex-direction: column;
.jump {
min-height: 32px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.25s;
&:not(.disabled):hover {
background-color: $grey-6;
}
mat-icon {
width: 16px;
height: 16px;
}
&.disabled {
cursor: default;
mat-icon {
opacity: 0.3;
}
}
}
.pages {
@include no-scroll-bar();
overflow: auto;
flex: 1;
}
}
.page-separator {
border-bottom: 1px solid $separator;
height: 32px;
box-sizing: border-box;
padding: 0 10px;
display: flex;
align-items: center;
background-color: $grey-6;
}
.annotations {
overflow: hidden;
width: 100%;
height: calc(100% - 32px);
.annotation-wrapper {
display: flex;
border-bottom: 1px solid $separator;
.active-marker {
min-width: 4px;
min-height: 100%;
}
&.active {
.active-marker {
background-color: $primary;
}
}
.annotation {
padding: 10px 21px 10px 6px;
font-size: 11px;
line-height: 14px;
cursor: pointer;
display: flex;
flex-direction: column;
&.removed {
text-decoration: line-through;
color: $grey-7;
}
.details {
display: flex;
position: relative;
}
redaction-type-annotation-icon {
margin-top: 6px;
margin-right: 10px;
}
}
&:hover {
background-color: #f9fafb;
::ng-deep .annotation-actions {
display: flex;
}
}
}
&:hover {
overflow-y: auto;
@include scroll-bar;
}
&.has-scrollbar:hover {
.annotation {
padding-right: 10px;
}
}
}
}
//
// .quick-navigation,
// .annotations {
// overflow-y: scroll;
// outline: none;
//
// &.active-panel {
// background-color: #fafafa;
// }
// }
//
// .quick-navigation {
// border-right: 1px solid $separator;
// min-width: 61px;
// overflow: hidden;
// display: flex;
// flex-direction: column;
//
// .jump {
// min-height: 32px;
// display: flex;
// justify-content: center;
// align-items: center;
// cursor: pointer;
// transition: background-color 0.25s;
//
// &:not(.disabled):hover {
// background-color: $grey-6;
// }
//
// mat-icon {
// width: 16px;
// height: 16px;
// }
//
// &.disabled {
// cursor: default;
//
// mat-icon {
// opacity: 0.3;
// }
// }
// }
//
// .pages {
// @include no-scroll-bar();
// overflow: auto;
// flex: 1;
// }
// }
//
// .page-separator {
// border-bottom: 1px solid $separator;
// height: 32px;
// box-sizing: border-box;
// padding: 0 10px;
// display: flex;
// align-items: center;
// background-color: $grey-6;
// }
//
// .annotations {
// overflow: hidden;
// width: 100%;
// height: calc(100% - 32px);
//
// .annotation-wrapper {
// display: flex;
// border-bottom: 1px solid $separator;
//
// .active-marker {
// min-width: 4px;
// min-height: 100%;
// }
//
// &.active {
// .active-marker {
// background-color: $primary;
// }
// }
//
// .annotation {
// padding: 10px 21px 10px 6px;
// font-size: 11px;
// line-height: 14px;
// cursor: pointer;
// display: flex;
// flex-direction: column;
//
// &.removed {
// text-decoration: line-through;
// color: $grey-7;
// }
//
// .details {
// display: flex;
// position: relative;
// }
//
// redaction-type-annotation-icon {
// margin-top: 6px;
// margin-right: 10px;
// }
// }
//
// &:hover {
// background-color: #f9fafb;
//
// ::ng-deep .annotation-actions {
// display: flex;
// }
// }
// }
//
// &:hover {
// overflow-y: auto;
// @include scroll-bar;
// }
//
// &.has-scrollbar:hover {
// .annotation {
// padding-right: 10px;
// }
// }
// }
//}
}
.no-annotations {
padding: 24px;
text-align: center;
}
//.no-annotations {
// padding: 24px;
// text-align: center;
//}
.assign-actions-wrapper {
display: flex;

View File

@ -15,24 +15,24 @@ import { AnnotationData, FileDataModel } from '../model/file-data.model';
import { FileActionService } from '../service/file-action.service';
import { AnnotationDrawService } from '../service/annotation-draw.service';
import { AnnotationProcessingService } from '../service/annotation-processing.service';
import { FilterModel } from '../../../common/filter/model/filter.model';
import { FilterModel } from '../../../components/filter/model/filter.model';
import { tap } from 'rxjs/operators';
import { NotificationService } from '../../../notification/notification.service';
import { TranslateService } from '@ngx-translate/core';
import { FileStatusWrapper } from '../model/file-status.wrapper';
import { PermissionsService } from '../../../common/service/permissions.service';
import { PermissionsService } from '../../../utils/permissions.service';
import { Subscription, timer } from 'rxjs';
import { handleFilterDelta, processFilters } from '../../../common/filter/utils/filter-utils';
import { UserPreferenceService } from '../../../common/service/user-preference.service';
import { handleFilterDelta, processFilters } from '../../../components/filter/utils/filter-utils';
import { UserPreferenceService } from '../../../utils/user-preference.service';
import { UserService } from '../../../user/user.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FileManagementControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { PdfViewerDataService } from '../service/pdf-viewer-data.service';
import { download } from '../../../utils/file-download-utils';
import { ViewMode } from '../model/view-mode';
import { FileWorkloadComponent } from '../../../components/file-workload/file-workload.component';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape', 'F', 'f'];
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@Component({
selector: 'redaction-file-preview-screen',
@ -40,6 +40,33 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Es
styleUrls: ['./file-preview-screen.component.scss']
})
export class FilePreviewScreenComponent implements OnInit, OnDestroy {
public dialogRef: MatDialogRef<any>;
public viewMode: ViewMode = 'STANDARD';
public fullScreen = false;
public editingReviewer = false;
public reviewerForm: FormGroup;
public shouldDeselectAnnotationsOnPageChange = true;
public analysisProgressInSeconds = 0;
public analysisProgress: number;
public analysisInterval: number;
fileData: FileDataModel;
fileId: string;
annotationData: AnnotationData;
selectedAnnotations: AnnotationWrapper[];
viewReady = false;
annotationFilters: FilterModel[];
loadingMessage: string;
canPerformAnnotationActions: boolean;
filesAutoUpdateTimer: Subscription;
fileReanalysedSubscription: Subscription;
hideSkipped = false;
public viewDocumentInfo = false;
private projectId: string;
private _instance: WebViewerInstance;
@ViewChild('fileWorkloadComponent') private _workloadComponent: FileWorkloadComponent;
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
constructor(
public readonly appStateService: AppStateService,
public readonly permissionsService: PermissionsService,
@ -72,7 +99,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
});
}
get annotations() {
get annotations(): AnnotationWrapper[] {
return this.annotationData ? this.annotationData.visibleAnnotations : [];
}
@ -92,60 +119,16 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
return this.fileData?.redactionChangeLog?.redactionLogEntry?.length > 0;
}
get ignoreColor() {
return this.appStateService.getDictionaryColor('skipped');
}
get displayData() {
return this.fileData?.fileData;
}
private projectId: string;
private _instance: WebViewerInstance;
private _dialogRef: MatDialogRef<any>;
public viewMode: ViewMode = 'STANDARD';
public fullScreen = false;
public editingReviewer = false;
public reviewerForm: FormGroup;
public shouldDeselectAnnotationsOnPageChange = true;
public analysisProgressInSeconds = 0;
public analysisProgress: number;
public analysisInterval: number;
public quickScrollFirstEnabled = false;
public quickScrollLastEnabled = false;
public displayedPages: number[] = [];
get indeterminateMode() {
return (
this.analysisProgress > 100 || this.appStateService.activeFile.analysisDuration < 3 * 1000 // it takes longer than usual - switch to indeterminate
); // on less than 3 seconds show indeterminate
}
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
fileData: FileDataModel;
fileId: string;
annotationData: AnnotationData;
displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
selectedAnnotations: AnnotationWrapper[];
pagesPanelActive = true;
viewReady = false;
annotationFilters: FilterModel[];
loadingMessage: string;
canPerformAnnotationActions: boolean;
filesAutoUpdateTimer: Subscription;
fileReanalysedSubscription: Subscription;
hideSkipped = false;
updateViewMode() {
const allAnnotations = this._instance.annotManager.getAnnotationsList();
@ -181,15 +164,11 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
break;
}
this._rebuildFilters();
this.rebuildFilters();
this._updateCanPerformActions();
}
private _updateCanPerformActions() {
this.canPerformAnnotationActions = this.permissionsService.canPerformAnnotationActions() && this.viewMode === 'STANDARD';
}
ngOnInit(): void {
document.documentElement.addEventListener('fullscreenchange', (event) => {
if (!document.fullscreenElement) {
@ -225,32 +204,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
this.fileReanalysedSubscription.unsubscribe();
}
private _loadFileData(performUpdate: boolean = false) {
return this._fileDownloadService.loadActiveFileData().pipe(
tap((fileDataModel) => {
if (fileDataModel.fileStatus.isWorkable) {
if (performUpdate) {
this.fileData.redactionLog = fileDataModel.redactionLog;
this.fileData.redactionChangeLog = fileDataModel.redactionChangeLog;
this.fileData.fileStatus = fileDataModel.fileStatus;
this.fileData.manualRedactions = fileDataModel.manualRedactions;
this._rebuildFilters(true);
} else {
this.fileData = fileDataModel;
this._rebuildFilters();
}
} else {
if (fileDataModel.fileStatus.isError) {
this._router.navigate(['/ui/projects/' + this.appStateService.activeProjectId]);
} else {
this.loadingMessage = 'file-preview.reanalyse-file';
}
}
})
);
}
private _rebuildFilters(deletePreviousAnnotations: boolean = false) {
public rebuildFilters(deletePreviousAnnotations: boolean = false) {
const startTime = new Date().getTime();
if (deletePreviousAnnotations) {
this.activeViewer.annotManager.deleteAnnotations(this.activeViewer.annotManager.getAnnotationsList(), {
@ -268,7 +222,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
);
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations);
this.annotationFilters = processFilters(this.annotationFilters, annotationFilters);
this.filtersChanged(this.annotationFilters);
this._workloadComponent.filtersChanged(this.annotationFilters);
console.log('[REDACTION] Process time: ' + (new Date().getTime() - processStartTime) + 'ms');
console.log(
'[REDACTION] Annotation Redraw and filter rebuild time: ' +
@ -281,27 +235,22 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
handleAnnotationSelected(annotationIds: string[]) {
this.selectedAnnotations = annotationIds.map((annotationId) => this.annotations.find((annotationWrapper) => annotationWrapper.id === annotationId));
this.scrollToSelectedAnnotation();
this._workloadComponent.scrollToSelectedAnnotation();
this._changeDetectorRef.detectChanges();
}
annotationClicked(annotation: AnnotationWrapper) {
this.pagesPanelActive = false;
this.selectAnnotation(annotation);
}
selectAnnotation(annotation: AnnotationWrapper) {
this._viewerComponent.selectAnnotation(annotation);
}
selectPage(pageNumber: number) {
this._viewerComponent.navigateToPage(pageNumber);
this._scrollAnnotationsToPage(pageNumber, 'always');
this._workloadComponent.scrollAnnotationsToPage(pageNumber, 'always');
}
openManualAnnotationDialog($event: ManualRedactionEntryWrapper) {
this.ngZone.run(() => {
this._dialogRef = this._dialogService.openManualAnnotationDialog($event, async (response: ManualAnnotationResponse) => {
this.dialogRef = this._dialogService.openManualAnnotationDialog($event, async (response: ManualAnnotationResponse) => {
if (response?.annotationId) {
const annotation = this.activeViewer.annotManager.getAnnotationById(response.manualRedactionEntryWrapper.rectId);
this.activeViewer.annotManager.deleteAnnotation(annotation);
@ -315,81 +264,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
});
}
annotationIsSelected(annotation: AnnotationWrapper) {
return this.selectedAnnotations?.find((a) => a.id === annotation.id);
}
private get firstSelectedAnnotation() {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
}
private get lastSelectedAnnotation() {
return this.selectedAnnotations?.length ? this.selectedAnnotations[this.selectedAnnotations.length - 1] : null;
}
@debounce()
private _scrollViews() {
this._scrollQuickNavigation();
this._scrollAnnotations();
}
@debounce()
private scrollToSelectedAnnotation() {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) {
return;
}
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.firstSelectedAnnotation.id}"].active`);
this._scrollToFirstElement(elements);
}
private _scrollQuickNavigationToPage(page: number) {
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
this._scrollToFirstElement(elements);
}
private _scrollQuickNavigation() {
let quickNavPageIndex = this.displayedPages.findIndex((p) => p >= this.activeViewerPage);
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) {
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
}
this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]);
}
public scrollQuickNavFirst() {
if (this.displayedPages.length > 0) {
this._scrollQuickNavigationToPage(this.displayedPages[0]);
}
}
public scrollQuickNavLast() {
if (this.displayedPages.length > 0) {
this._scrollQuickNavigationToPage(this.displayedPages[this.displayedPages.length - 1]);
}
}
private _scrollAnnotations() {
if (this.firstSelectedAnnotation?.pageNumber === this.activeViewerPage) {
return;
}
this._scrollAnnotationsToPage(this.activeViewerPage, 'always');
}
private _scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
this._scrollToFirstElement(elements, mode);
}
private _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
if (elements.length > 0) {
scrollIntoView(elements[0], {
behavior: 'smooth',
scrollMode: mode,
block: 'start',
inline: 'start'
});
}
}
public toggleFullScreen() {
this.fullScreen = !this.fullScreen;
if (this.fullScreen) {
@ -401,7 +275,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
@HostListener('window:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent) {
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) {
if (!ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN) {
return;
}
@ -419,145 +293,9 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
return;
}
if ($event.key === 'ArrowLeft') {
this.pagesPanelActive = true;
}
if ($event.key === 'ArrowRight') {
this.pagesPanelActive = false;
// if we activated annotationsPanel - select first annotation from this page in case there is no
// selected annotation on this page
if (!this.pagesPanelActive) {
this._selectFirstAnnotationOnCurrentPageIfNecessary();
}
}
if ($event.key === 'ArrowLeft' || $event.key === 'ArrowRight') {
return;
}
if (!this.pagesPanelActive) {
this._navigateAnnotations($event);
} else {
this._navigatePages($event);
}
this._changeDetectorRef.detectChanges();
}
private _selectFirstAnnotationOnCurrentPageIfNecessary() {
if (
(!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) &&
this.displayedPages.indexOf(this.activeViewerPage) >= 0
) {
this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
}
}
private _navigateAnnotations($event: KeyboardEvent) {
if (!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) {
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
if (pageIdx !== -1) {
// Displayed page has annotations
this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
} else {
// Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') {
const nextPage = this._nextPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation(this.displayedAnnotations[nextPage].annotations[0]);
} else {
const prevPage = this._prevPageWithAnnotations();
this.shouldDeselectAnnotationsOnPageChange = false;
const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
}
}
} else {
const page = this.firstSelectedAnnotation.pageNumber;
const pageIdx = this.displayedPages.indexOf(page);
const annotationsOnPage = this.displayedAnnotations[page].annotations;
const idx = annotationsOnPage.findIndex((a) => a.id === this.firstSelectedAnnotation.id);
if ($event.key === 'ArrowDown') {
if (idx + 1 !== annotationsOnPage.length) {
// If not last item in page
this.selectAnnotation(annotationsOnPage[idx + 1]);
} else if (pageIdx + 1 < this.displayedPages.length) {
// If not last page
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation(nextPageAnnotations[0]);
}
} else {
if (idx !== 0) {
// If not first item in page
this.selectAnnotation(annotationsOnPage[idx - 1]);
} else if (pageIdx) {
// If not first page
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
}
}
}
}
private _navigatePages($event: KeyboardEvent) {
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
if ($event.key === 'ArrowDown') {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== this.displayedPages.length - 1) {
this.selectPage(this.displayedPages[pageIdx + 1]);
}
} else {
// If active page doesn't have annotations
const nextPage = this._nextPageWithAnnotations();
if (nextPage) {
this.selectPage(nextPage);
}
}
} else {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== 0) {
this.selectPage(this.displayedPages[pageIdx - 1]);
}
} else {
// If active page doesn't have annotations
const prevPage = this._prevPageWithAnnotations();
if (prevPage) {
this.selectPage(prevPage);
}
}
}
}
private _nextPageWithAnnotations() {
let idx = 0;
for (const page of this.displayedPages) {
if (page > this.activeViewerPage) {
break;
}
++idx;
}
return idx < this.displayedPages.length ? this.displayedPages[idx] : null;
}
private _prevPageWithAnnotations() {
let idx = this.displayedPages.length - 1;
for (const page of this.displayedPages.reverse()) {
if (page < this.activeViewerPage) {
this.selectPage(this.displayedPages[idx]);
this._scrollAnnotations();
break;
}
--idx;
}
return idx >= 0 ? this.displayedPages[idx] : null;
}
viewerPageChanged($event: any) {
if (typeof $event === 'number') {
this._scrollViews();
@ -588,74 +326,10 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
}
}
filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this.annotations, filters);
this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
this.computeQuickNavButtonsState();
this._changeDetectorRef.markForCheck();
}
preventKeyDefault($event: KeyboardEvent) {
if (COMMAND_KEY_ARRAY.includes($event.key)) {
$event.preventDefault();
}
}
public computeQuickNavButtonsState() {
const element: HTMLElement = this._quickNavigationElement.nativeElement.querySelector(`#pages`);
const { scrollTop, scrollHeight, clientHeight } = element;
this.quickScrollFirstEnabled = scrollTop !== 0;
this.quickScrollLastEnabled = scrollHeight !== scrollTop + clientHeight;
}
private _cleanupAndRedrawManualAnnotations() {
this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => {
this.fileData.manualRedactions = manualRedactions;
this._rebuildFilters();
this._annotationDrawService.drawAnnotations(this._instance, this.annotationData.allAnnotations, this.hideSkipped);
});
}
private async _cleanupAndRedrawManualAnnotationsForEntirePage(page: number) {
const currentPageAnnotations = this.annotations.filter((a) => a.pageNumber === page);
const currentPageAnnotationIds = currentPageAnnotations.map((a) => a.id);
this.fileData.fileStatus = await this.appStateService.reloadActiveFile();
this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => {
this.fileData.manualRedactions = manualRedactions;
this._rebuildFilters();
if (this.viewMode === 'STANDARD') {
currentPageAnnotationIds.forEach((id) => {
this._findAndDeleteAnnotation(id);
});
const newPageAnnotations = this.annotations.filter((item) => item.pageNumber === page);
this._handleDeltaAnnotationFilters(currentPageAnnotations, newPageAnnotations);
this._annotationDrawService.drawAnnotations(this._instance, newPageAnnotations, this.hideSkipped);
}
});
}
private _handleDeltaAnnotationFilters(currentPageAnnotations: AnnotationWrapper[], newPageAnnotations: AnnotationWrapper[]) {
const hasAnyFilterSet = this.annotationFilters.find((f) => f.checked || f.indeterminate);
if (hasAnyFilterSet) {
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters);
this.filtersChanged(this.annotationFilters);
}
}
async annotationsChangedByReviewAction(annotation: AnnotationWrapper) {
await this._cleanupAndRedrawManualAnnotationsForEntirePage(annotation.pageNumber);
}
private _findAndDeleteAnnotation(id: string) {
const viewerAnnotation = this.activeViewer.annotManager.getAnnotationById(id);
if (viewerAnnotation) {
this.activeViewer.annotManager.deleteAnnotation(viewerAnnotation, { imported: true, force: true });
}
}
async fileActionPerformed(action: string) {
switch (action) {
case 'delete':
@ -670,36 +344,16 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
this._updateCanPerformActions();
await this.appStateService.reloadActiveProjectFiles();
return;
case 'view-document-info':
this.viewDocumentInfo = !this.viewDocumentInfo;
return;
default:
this._updateCanPerformActions();
}
}
private _startAnalysisTimer() {
this._stopAnalysisTimer();
if (this.appStateService.activeFile.analysisDuration > 0) {
this.analysisProgress = 0;
this.analysisProgressInSeconds = 0;
this.analysisInterval = setInterval(() => {
this.analysisProgressInSeconds += 1;
this.analysisProgress = (this.analysisProgressInSeconds * 100) / (this.appStateService.activeFile.analysisDuration / 1000);
}, 1000);
} else {
this.analysisInterval = 0;
this.analysisProgress = 0;
this.analysisProgressInSeconds = 0;
}
}
private _stopAnalysisTimer() {
if (this.analysisInterval) {
clearInterval(this.analysisInterval);
this.analysisInterval = 0;
}
}
public async assignToMe() {
await this._fileActionService.assignToMe(this.fileData.fileStatus, async () => {
await this.appStateService.reloadActiveFile();
@ -725,21 +379,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
this.reviewerForm.setValue({ reviewer: this.appStateService.activeFile.currentReviewer });
}
pageSelectedByClick($event: number) {
this.pagesPanelActive = true;
this.selectPage($event);
}
/* Get the documentElement (<html>) to display the page in fullscreen */
/* View in fullscreen */
private _openFullScreen() {
const documentElement = document.documentElement;
if (documentElement.requestFullscreen) {
documentElement.requestFullscreen();
}
}
/* Close fullscreen */
closeFullScreen() {
if (document.exitFullscreen) {
@ -780,8 +419,118 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy {
window.open(`/html-debug/${this.projectId}/${this.fileId}`, '_blank');
}
logAnnotation(annotation: AnnotationWrapper) {
console.log(annotation);
private _updateCanPerformActions() {
this.canPerformAnnotationActions = this.permissionsService.canPerformAnnotationActions() && this.viewMode === 'STANDARD';
}
private _loadFileData(performUpdate: boolean = false) {
return this._fileDownloadService.loadActiveFileData().pipe(
tap((fileDataModel) => {
if (fileDataModel.fileStatus.isWorkable) {
if (performUpdate) {
this.fileData.redactionLog = fileDataModel.redactionLog;
this.fileData.redactionChangeLog = fileDataModel.redactionChangeLog;
this.fileData.fileStatus = fileDataModel.fileStatus;
this.fileData.manualRedactions = fileDataModel.manualRedactions;
this.rebuildFilters(true);
} else {
this.fileData = fileDataModel;
this.rebuildFilters();
}
} else {
if (fileDataModel.fileStatus.isError) {
this._router.navigate(['/ui/projects/' + this.appStateService.activeProjectId]);
} else {
this.loadingMessage = 'file-preview.reanalyse-file';
}
}
})
);
}
@debounce()
private _scrollViews() {
this._workloadComponent.scrollQuickNavigation();
this._workloadComponent.scrollAnnotations();
}
/* Get the documentElement (<html>) to display the page in fullscreen */
private _cleanupAndRedrawManualAnnotations() {
this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => {
this.fileData.manualRedactions = manualRedactions;
this.rebuildFilters();
this._annotationDrawService.drawAnnotations(this._instance, this.annotationData.allAnnotations, this.hideSkipped);
});
}
private async _cleanupAndRedrawManualAnnotationsForEntirePage(page: number) {
const currentPageAnnotations = this.annotations.filter((a) => a.pageNumber === page);
const currentPageAnnotationIds = currentPageAnnotations.map((a) => a.id);
this.fileData.fileStatus = await this.appStateService.reloadActiveFile();
this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => {
this.fileData.manualRedactions = manualRedactions;
this.rebuildFilters();
if (this.viewMode === 'STANDARD') {
currentPageAnnotationIds.forEach((id) => {
this._findAndDeleteAnnotation(id);
});
const newPageAnnotations = this.annotations.filter((item) => item.pageNumber === page);
this._handleDeltaAnnotationFilters(currentPageAnnotations, newPageAnnotations);
this._annotationDrawService.drawAnnotations(this._instance, newPageAnnotations, this.hideSkipped);
}
});
}
private _handleDeltaAnnotationFilters(currentPageAnnotations: AnnotationWrapper[], newPageAnnotations: AnnotationWrapper[]) {
const hasAnyFilterSet = this.annotationFilters.find((f) => f.checked || f.indeterminate);
if (hasAnyFilterSet) {
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters);
this._workloadComponent.filtersChanged(this.annotationFilters);
}
}
private _findAndDeleteAnnotation(id: string) {
const viewerAnnotation = this.activeViewer.annotManager.getAnnotationById(id);
if (viewerAnnotation) {
this.activeViewer.annotManager.deleteAnnotation(viewerAnnotation, { imported: true, force: true });
}
}
private _startAnalysisTimer() {
this._stopAnalysisTimer();
if (this.appStateService.activeFile.analysisDuration > 0) {
this.analysisProgress = 0;
this.analysisProgressInSeconds = 0;
this.analysisInterval = setInterval(() => {
this.analysisProgressInSeconds += 1;
this.analysisProgress = (this.analysisProgressInSeconds * 100) / (this.appStateService.activeFile.analysisDuration / 1000);
}, 1000);
} else {
this.analysisInterval = 0;
this.analysisProgress = 0;
this.analysisProgressInSeconds = 0;
}
}
private _stopAnalysisTimer() {
if (this.analysisInterval) {
clearInterval(this.analysisInterval);
this.analysisInterval = 0;
}
}
/* View in fullscreen */
private _openFullScreen() {
const documentElement = document.documentElement;
if (documentElement.requestFullscreen) {
documentElement.requestFullscreen();
}
}
private _handleIgnoreAnnotationsDrawing() {

Some files were not shown because too many files have changed in this diff Show More