Merge branch 'master' into VM/RED-3982

This commit is contained in:
Valentin Mihai 2022-06-02 12:11:47 +03:00
commit be0dbbba77
63 changed files with 486 additions and 549 deletions

View File

@ -95,9 +95,9 @@ const routes: Routes = [
},
{
path: `:${DOSSIER_ID}/file/:${FILE_ID}`,
canActivate: [CompositeRouteGuard, WebViewerLoadedGuard],
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [DossierFilesGuard],
routeGuards: [DossierFilesGuard, WebViewerLoadedGuard],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier, BreadcrumbTypes.file],
dossiersService: ACTIVE_DOSSIERS_SERVICE,
},

View File

@ -53,6 +53,7 @@ import { LoggerRulesService } from '@services/logger-rules.service';
import { ILoggerConfig } from '@red/domain';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
import { LicenseService } from '@services/license.service';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -181,6 +182,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
LanguageService,
UserService,
UserPreferenceService,
LicenseService,
],
},
{

View File

@ -11,7 +11,7 @@ import { UserListingScreenComponent } from './screens/user-listing/user-listing-
import { DossierTemplateBreadcrumbsComponent } from './components/dossier-template-breadcrumbs/dossier-template-breadcrumbs.component';
import { ColorPickerModule } from 'ngx-color-picker';
import { AddEditFileAttributeDialogComponent } from './dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { AddEditDossierTemplateDialogComponent } from './dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component';
import { AddEditCloneDossierTemplateDialogComponent } from './dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component';
import { AddEntityDialogComponent } from './dialogs/add-entity-dialog/add-entity-dialog.component';
import { EditColorDialogComponent } from './dialogs/edit-color-dialog/edit-color-dialog.component';
import { AdminDialogService } from './services/admin-dialog.service';
@ -41,7 +41,6 @@ import { AddEditDossierStateDialogComponent } from './dialogs/add-edit-dossier-s
import { A11yModule } from '@angular/cdk/a11y';
import { ConfirmDeleteDossierStateDialogComponent } from './dialogs/confirm-delete-dossier-state-dialog/confirm-delete-dossier-state-dialog.component';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { CloneDossierTemplateDialogComponent } from './dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component';
import { AdminSideNavComponent } from './admin-side-nav/admin-side-nav.component';
import { SystemPreferencesFormComponent } from './screens/general-config/system-preferences-form/system-preferences-form.component';
import { ConfigureCertificateDialogComponent } from './dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
@ -49,10 +48,9 @@ import { PkcsSignatureConfigurationComponent } from './dialogs/configure-digital
import { KmsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
const dialogs = [
AddEditDossierTemplateDialogComponent,
AddEditCloneDossierTemplateDialogComponent,
AddEntityDialogComponent,
AddEditFileAttributeDialogComponent,
CloneDossierTemplateDialogComponent,
EditColorDialogComponent,
SmtpAuthDialogComponent,
AddEditUserDialogComponent,

View File

@ -1,19 +1,19 @@
<section class="dialog">
<div
[translateParams]="{
type: dossierTemplate ? 'edit' : 'create',
type: dossierTemplate ? (data.clone ? 'clone' : 'edit') : 'create',
name: dossierTemplate?.name
}"
[translate]="'add-edit-dossier-template.title'"
[translate]="'add-edit-clone-dossier-template.title'"
class="dialog-header heading-l"
></div>
<form [formGroup]="form">
<div class="dialog-content">
<div class="iqser-input-group required w-300">
<label translate="add-edit-dossier-template.form.name"></label>
<label translate="add-edit-clone-dossier-template.form.name"></label>
<input
[placeholder]="'add-edit-dossier-template.form.name-placeholder' | translate"
[placeholder]="'add-edit-clone-dossier-template.form.name-placeholder' | translate"
formControlName="name"
name="name"
type="text"
@ -21,9 +21,9 @@
</div>
<div class="iqser-input-group w-400">
<label translate="add-edit-dossier-template.form.description"></label>
<label translate="add-edit-clone-dossier-template.form.description"></label>
<textarea
[placeholder]="'add-edit-dossier-template.form.description-placeholder' | translate"
[placeholder]="'add-edit-clone-dossier-template.form.description-placeholder' | translate"
formControlName="description"
name="description"
rows="4"
@ -34,11 +34,11 @@
<div class="validity">
<div>
<mat-checkbox (change)="toggleHasValid('from')" [checked]="hasValidFrom" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-from' | translate }}
{{ 'add-edit-clone-dossier-template.form.valid-from' | translate }}
</mat-checkbox>
<mat-checkbox (change)="toggleHasValid('to')" [checked]="hasValidTo" class="filter-menu-checkbox" color="primary">
{{ 'add-edit-dossier-template.form.valid-to' | translate }}
{{ 'add-edit-clone-dossier-template.form.valid-to' | translate }}
</mat-checkbox>
</div>
@ -83,7 +83,7 @@
<div class="dialog-actions">
<button (click)="save()" [disabled]="disabled" color="primary" mat-flat-button type="button">
{{ 'add-edit-dossier-template.save' | translate }}
{{ 'add-edit-clone-dossier-template.save' | translate }}
</button>
</div>
</form>

View File

@ -12,11 +12,16 @@ import { DictionaryService } from '@services/entity-services/dictionary.service'
import { firstValueFrom } from 'rxjs';
import dayjs, { Dayjs } from 'dayjs';
interface EditCloneTemplateData {
dossierTemplateId: string;
clone?: boolean;
}
@Component({
templateUrl: './add-edit-dossier-template-dialog.component.html',
styleUrls: ['./add-edit-dossier-template-dialog.component.scss'],
templateUrl: './add-edit-clone-dossier-template-dialog.component.html',
styleUrls: ['./add-edit-clone-dossier-template-dialog.component.scss'],
})
export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogComponent {
hasValidFrom: boolean;
hasValidTo: boolean;
downloadTypesEnum: DownloadFileType[] = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'];
@ -36,12 +41,12 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
private readonly _dictionaryService: DictionaryService,
private readonly _formBuilder: FormBuilder,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditDossierTemplateDialogComponent>,
protected readonly _dialogRef: MatDialogRef<AddEditCloneDossierTemplateDialogComponent>,
private readonly _loadingService: LoadingService,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplateId: string,
@Inject(MAT_DIALOG_DATA) readonly data: EditCloneTemplateData,
) {
super(_injector, _dialogRef, !!dossierTemplateId);
this.dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
super(_injector, _dialogRef, !!data && !data.clone);
this.dossierTemplate = this._dossierTemplatesService.find(this.data?.dossierTemplateId);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
this.hasValidFrom = !!this.dossierTemplate?.validFrom;
@ -81,13 +86,17 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
} as IDossierTemplate;
await firstValueFrom(this._dossierTemplatesService.createOrUpdate(dossierTemplate));
if (this.data?.clone) {
await firstValueFrom(this._dossierTemplatesService.clone(this.dossierTemplate.id, dossierTemplate));
} else {
await firstValueFrom(this._dossierTemplatesService.createOrUpdate(dossierTemplate));
}
this._dialogRef.close(true);
} catch (error: any) {
const message =
error.status === HttpStatusCode.Conflict
? _('add-edit-dossier-template.error.conflict')
: _('add-edit-dossier-template.error.generic');
? _('add-edit-clone-dossier-template.error.conflict')
: _('add-edit-clone-dossier-template.error.generic');
this._toaster.error(message, { error });
}
this._loadingService.stop();
@ -95,7 +104,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
private _getForm(): FormGroup {
return this._formBuilder.group({
name: [this.dossierTemplate?.name, Validators.required],
name: [this._getCloneName(), Validators.required],
description: [this.dossierTemplate?.description],
validFrom: [
this.dossierTemplate?.validFrom ? dayjs(this.dossierTemplate?.validFrom) : null,
@ -109,6 +118,32 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
});
}
private _getCloneName(): string {
if (this.data?.clone) {
const templateName = this.dossierTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Copy of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Copy of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Copy of ${nameOfClonedTemplate} ${clonesCount === 1 ? '(1)' : `(${clonesCount})`}`;
}
return `Copy of ${nameOfClonedTemplate}`;
}
return this.dossierTemplate?.name;
}
private _applyValidityIntervalConstraints(value): boolean {
if (applyIntervalConstraints(value, this._previousValidFrom, this._previousValidTo, this.form, 'validFrom', 'validTo')) {
return true;
@ -130,4 +165,11 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
return null;
};
}
get disabled(): boolean {
if (!this.data?.clone) {
return super.disabled;
}
return !this.valid;
}
}

View File

@ -1,23 +0,0 @@
<section class="dialog">
<div translate="clone-dossier-template.title" class="dialog-header heading-l"></div>
<div class="dialog-content">
<div class="iqser-input-group required w-full">
<label translate="clone-dossier-template.content.name"></label>
<input
[placeholder]="'clone-dossier-template.content.name-placeholder' | translate"
[(ngModel)]="nameOfClonedDossierTemplate"
name="name"
type="text"
/>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit" (click)="save()" [disabled]="!nameOfClonedDossierTemplate">
{{ 'clone-dossier-template.actions.save' | translate }}
</button>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" mat-dialog-close></iqser-circle-button>
</section>

View File

@ -1,66 +0,0 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossierTemplate } from '@red/domain';
import { LoadingService, Toaster } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
templateUrl: './clone-dossier-template-dialog.component.html',
styleUrls: ['./clone-dossier-template-dialog.component.scss'],
})
export class CloneDossierTemplateDialogComponent {
nameOfClonedDossierTemplate: string;
private readonly _dossierTemplate: DossierTemplate;
constructor(
private readonly _toaster: Toaster,
private readonly _loadingService: LoadingService,
private readonly _dossierTemplatesService: DossierTemplatesService,
protected readonly _dialogRef: MatDialogRef<CloneDossierTemplateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplateId: string,
) {
this._dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
this.nameOfClonedDossierTemplate = this._getCloneName();
}
async save() {
this._loadingService.start();
try {
await firstValueFrom(
this._dossierTemplatesService.clone(this.dossierTemplateId, {
...this._dossierTemplate,
name: this.nameOfClonedDossierTemplate,
}),
);
this._dialogRef.close(true);
} catch (error: any) {
this._toaster.error(_('clone-dossier-template.error.generic'), { params: error });
}
this._loadingService.stop();
}
private _getCloneName(): string | null {
const templateName = this._dossierTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Clone of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Clone of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Clone of ${nameOfClonedTemplate} ${clonesCount === 1 ? '(1)' : `(${clonesCount})`}`;
}
return `Clone of ${nameOfClonedTemplate}`;
}
}

View File

@ -61,7 +61,7 @@ export class DossierTemplatesListingScreenComponent extends ListingComponent<Dos
}
openAddDossierTemplateDialog() {
this._dialogService.openDialog('addEditDossierTemplate', null, null);
this._dialogService.openDialog('addEditCloneDossierTemplate', null, null);
}
private async _deleteTemplates(templateIds = this.listingService.selected.map(d => d.dossierTemplateId)) {

View File

@ -31,6 +31,6 @@ export class DossierTemplateInfoScreenComponent {
}
openEditDossierTemplateDialog($event: MouseEvent, dossierTemplate: DossierTemplate) {
this._dialogService.openDialog('addEditDossierTemplate', $event, { dossierTemplateId: dossierTemplate.id });
this._dialogService.openDialog('addEditCloneDossierTemplate', $event, { dossierTemplateId: dossierTemplate.id });
}
}

View File

@ -1,10 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LICENSE_STORAGE_KEY } from '../utils/constants';
import dayjs from 'dayjs';
import { ILicenseReport } from '@red/domain';
import { LicenseService } from '../services/license.service';
import { IDateRange } from '../utils/date-range';
import { ILicense } from '../utils/license';
import { IDateRange, ILicense, ILicenseReport } from '@red/domain';
import { LicenseService } from '../../../../../services/license.service';
import { switchMap, tap } from 'rxjs/operators';
import { List, LoadingService } from '@iqser/common-ui';
import { generateDateRanges, isCurrentMonth, toDate, verboseDate } from '../utils/functions';
@ -35,7 +33,7 @@ export class LicenseChartComponent {
const startYear: number = startDate.year();
const dateRanges = generateDateRanges(startMonth, startYear, endDate.month() as number, endDate.year() as number);
const reports = await this.#getReports(dateRanges);
const reports = await this.#getReports(dateRanges, license.id);
return this.#mapRangesToReports(dateRanges, reports);
}
@ -52,12 +50,12 @@ export class LicenseChartComponent {
]);
}
#getReports(dateRanges: List<IDateRange>) {
#getReports(dateRanges: List<IDateRange>, id: string) {
const reports = dateRanges.map(range => {
const startMonth = range.startMonth + 1;
const endMonth = range.endMonth + 1;
const key = `${startMonth}.${range.startYear}-${endMonth}.${range.endYear}`;
const key = `${id}-${startMonth}.${range.startYear}-${endMonth}.${range.endYear}`;
const existingReport = this._licenseService.storedReports[key];
if (existingReport) {
return existingReport;

View File

@ -9,7 +9,7 @@
<div class="content-inner">
<div class="content-container">
<div class="grid-container">
<div *ngIf="licenseService.licenseData$ | async" class="grid-container">
<div class="row">
<div translate="license-info-screen.backend-version"></div>
<div>{{ configService.values.BACKEND_APP_VERSION || '-' }}</div>
@ -34,7 +34,7 @@
<div class="section-title all-caps-label" translate="license-info-screen.licensing-details"></div>
<div *ngIf="userPreferenceService.areDevFeaturesEnabled" class="row">
<div class="row">
<div class="flex align-center" translate="license-info-screen.license-title"></div>
<div>
<redaction-license-select (valueChanges)="licenseChanged($event)"></redaction-license-select>

View File

@ -5,11 +5,11 @@ import { ButtonConfig, IconButtonTypes, LoadingService } from '@iqser/common-ui'
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
import { LicenseService } from '../services/license.service';
import { ILicenseReport } from '@red/domain';
import { LicenseService } from '../../../../../services/license.service';
import { ILicense, ILicenseReport } from '@red/domain';
import dayjs from 'dayjs';
import { ILicense } from '../utils/license';
import { UserPreferenceService } from '@services/user-preference.service';
import { firstValueFrom } from 'rxjs';
@Component({
templateUrl: './license-screen.component.html',
@ -30,7 +30,7 @@ export class LicenseScreenComponent implements OnInit {
totalInfo: ILicenseReport = {};
unlicensedInfo: ILicenseReport = {};
analysisPercentageOfLicense = 100;
totalLicensedNumberOfPages = this.licenseService.processingPages;
totalLicensedNumberOfPages: number;
constructor(
readonly configService: ConfigService,
@ -44,8 +44,10 @@ export class LicenseScreenComponent implements OnInit {
_loadingService.start();
}
ngOnInit() {
return this.loadLicenseData(this.licenseService.selectedLicense);
async ngOnInit() {
await firstValueFrom(this.licenseService.loadLicense());
this.totalLicensedNumberOfPages = this.licenseService.processingPages;
await this.loadLicenseData(this.licenseService.selectedLicense);
}
async loadLicenseData(license: ILicense) {

View File

@ -1,10 +1,10 @@
<div class="iqser-input-group w-400">
<mat-select (valueChange)="licenseChanged($event)" [(ngModel)]="value">
<div *ngIf="licenses$ | async as licenses" class="iqser-input-group w-400">
<mat-select (valueChange)="licenseChanged($event)" *ngIf="value" [(ngModel)]="value">
<mat-select-trigger>
<ng-container *ngTemplateOutlet="licenseInfo; context: { license: value }"></ng-container>
</mat-select-trigger>
<mat-option *ngFor="let license of licenseService.licenseData.licenses" [value]="license">
<mat-option *ngFor="let license of licenses" [value]="license">
<ng-container *ngTemplateOutlet="licenseInfo; context: { license: this.license }"></ng-container>
</mat-option>
</mat-select>

View File

@ -1,7 +1,8 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { LicenseService } from '../services/license.service';
import { ILicense } from '../utils/license';
import { LicenseService } from '../../../../../services/license.service';
import { ILicense } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { map, tap } from 'rxjs/operators';
const translations = {
active: _('license-info-screen.status.active'),
@ -15,8 +16,15 @@ const translations = {
})
export class LicenseSelectComponent {
@Output() readonly valueChanges = new EventEmitter<ILicense>();
value = this.licenseService.getActiveLicense();
value: ILicense;
licenses$ = this.licenseService.licenseData$.pipe(
map(data => data.licenses),
tap(() => {
if (!this.value) {
this.value = this.licenseService.activeLicense;
}
}),
);
constructor(readonly licenseService: LicenseService) {}
@ -26,6 +34,6 @@ export class LicenseSelectComponent {
licenseChanged($event: ILicense) {
this.valueChanges.emit($event);
this.licenseService.selectedLicense$.next($event);
this.licenseService.setSelectedLicense($event);
}
}

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { LicenseScreenComponent } from './license-screen/license-screen.component';
import { LicenseSelectComponent } from './license-select/license-select.component';
import { LicenseChartComponent } from './license-chart/license-chart.component';
import { LicenseService } from './services/license.service';
import { LicenseService } from '../../../../services/license.service';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatSelectModule } from '@angular/material/select';

View File

@ -1,81 +0,0 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { ILicenseReport, ILicenseReportRequest } from '@red/domain';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { ILicense, ILicenses, LICENSE_DATA } from '../utils/license';
import { ConfigService } from '../../../../../services/config.service';
import dayjs from 'dayjs';
import { getStoredReports } from '../utils/functions';
@Injectable()
export class LicenseService extends GenericService<ILicenseReport> {
storedReports = getStoredReports();
readonly licenseData = this.#licenseData;
readonly activeLicenseId = this.licenseData.activeLicense;
readonly selectedLicense$ = new BehaviorSubject<ILicense>(this.getActiveLicense());
constructor(protected readonly _injector: Injector, private readonly _configService: ConfigService) {
super(_injector, 'report');
}
get selectedLicense() {
return this.selectedLicense$.value;
}
get processingPages() {
const processingPagesFeature = this.selectedLicense$.value.features.find(f => f.name === 'processingPages');
return Number(processingPagesFeature.value ?? '0');
}
get #licenseData(): ILicenses {
return {
...LICENSE_DATA,
licenses: [
...LICENSE_DATA.licenses,
{
id: 'guid-0',
name: this._configService.values.LICENSE_CUSTOMER,
product: 'RedactManager',
licensedTo: this._configService.values.LICENSE_CUSTOMER,
licensedToEmail: this._configService.values.LICENSE_EMAIL,
validFrom: dayjs(this._configService.values.LICENSE_START, 'DD-MM-YYYY').toISOString(),
validUntil: dayjs(this._configService.values.LICENSE_END, 'DD-MM-YYYY').toISOString(),
features: [
{
name: 'pdftron',
type: 'STRING',
value: 'base64 encoded pdftron webviewer license key',
},
{
name: 'processingPages',
type: 'NUMBER',
value: this._configService.values.LICENSE_PAGE_COUNT.toString(),
},
],
},
],
};
}
getActiveLicense() {
return this.licenseData.licenses.find(license => license.id === this.activeLicenseId);
}
@Validate()
getReport$(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = [];
if (limit) {
queryParams.push({ key: 'limit', value: limit });
}
if (offset) {
queryParams.push({ key: 'offset', value: offset });
}
return this._post(body, `${this._defaultModelPath}/license`, queryParams);
}
getReport(body: ILicenseReportRequest, limit?: number, offset?: number) {
return firstValueFrom(this.getReport$(body, limit, offset));
}
}

View File

@ -1,7 +1,5 @@
import dayjs from 'dayjs';
import { IDateRange } from './date-range';
import { LICENSE_STORAGE_KEY } from './constants';
import { ILicenseReport } from '@red/domain';
import { IDateRange } from '@red/domain';
export function toDate(month: number, year: number) {
return dayjs(`01-${month}-${year}`, 'DD-M-YYYY').toDate();
@ -27,11 +25,6 @@ export function generateDateRanges(month: number, year: number, endMonth: number
return dates;
}
export function getStoredReports() {
const rawStoredReports = localStorage.getItem(LICENSE_STORAGE_KEY);
return JSON.parse(rawStoredReports ?? '{}') as Record<string, ILicenseReport>;
}
export function isCurrentMonth(month: number, year: number) {
const now = dayjs();
const currentMonth = now.month() + 1;

View File

@ -1,69 +0,0 @@
export interface ILicenseFeature {
readonly name: string;
readonly type: string;
readonly value: string;
}
export interface ILicense {
readonly id: string;
readonly name: string;
readonly product: string;
readonly licensedTo: string;
readonly licensedToEmail: string;
readonly validFrom: string;
readonly validUntil: string;
readonly features: readonly ILicenseFeature[];
}
export interface ILicenses {
readonly activeLicense: string;
readonly licenses: readonly ILicense[];
}
export const LICENSE_DATA: ILicenses = {
activeLicense: 'guid-0',
licenses: [
{
id: 'guid-1',
name: '1 Year cumulative (2022)',
product: 'RedactManager',
licensedTo: 'Customer company name 1',
licensedToEmail: 'customer@example.com',
validFrom: '2022-01-01T00:00:00.000Z',
validUntil: '2022-12-31T23:59:59.999Z',
features: [
{
name: 'pdftron',
type: 'STRING',
value: 'base64 encoded pdftron webviewer license key',
},
{
name: 'processingPages',
type: 'NUMBER',
value: '200000',
},
],
},
{
id: 'guid-2',
name: '2 Year cumulative (2021)',
product: 'RedactManager',
licensedTo: 'Customer company name 2',
licensedToEmail: 'customer@example.com',
validFrom: '2021-01-01T00:00:00.000Z',
validUntil: '2021-12-31T23:59:59.999Z',
features: [
{
name: 'pdftron',
type: 'STRING',
value: 'base64 encoded pdftron webviewer license key',
},
{
name: 'processingPages',
type: 'NUMBER',
value: '100000',
},
],
},
],
};

View File

@ -1,18 +1,18 @@
import { ChangeDetectorRef, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { environment } from '@environments/environment';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { DOSSIER_TEMPLATE_ID, IWatermark, WatermarkOrientation, WatermarkOrientations } from '@red/domain';
import { BASE_HREF, BASE_HREF_FN, BaseHrefFn } from '../../../../../tokens';
import { BASE_HREF_FN, BaseHrefFn } from '../../../../../tokens';
import { stampPDFPage } from '@utils/page-stamper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { firstValueFrom, Observable, of, switchMap } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { LicenseService } from '../../../../../services/license.service';
export const DEFAULT_WATERMARK: IWatermark = {
text: null,
@ -44,6 +44,7 @@ export class WatermarkScreenComponent implements OnInit {
private readonly _formBuilder: FormBuilder,
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _licenseService: LicenseService,
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef,
@ -125,7 +126,7 @@ export class WatermarkScreenComponent implements OnInit {
if (!this._instance) {
WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
licenseKey: this._licenseService.activeLicenseKey,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
@ -169,7 +170,18 @@ export class WatermarkScreenComponent implements OnInit {
const opacity: number = this.form.get('opacity').value;
const color: string = this.form.get('hexColor').value;
await stampPDFPage(document, pdfNet, text, fontSize, fontType, orientation, opacity, color, [1]);
await stampPDFPage(
document,
pdfNet,
text,
fontSize,
fontType,
orientation,
opacity,
color,
[1],
this._licenseService.activeLicenseKey,
);
this._instance.Core.documentViewer.refreshAll();
this._instance.Core.documentViewer.updateView([0], 0);
this._changeDetectorRef.detectChanges();

View File

@ -2,7 +2,7 @@ import { Injectable, TemplateRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddEditFileAttributeDialogComponent } from '../dialogs/add-edit-file-attribute-dialog/add-edit-file-attribute-dialog.component';
import { AddEntityDialogComponent } from '../dialogs/add-entity-dialog/add-entity-dialog.component';
import { AddEditDossierTemplateDialogComponent } from '../dialogs/add-edit-dossier-template-dialog/add-edit-dossier-template-dialog.component';
import { AddEditCloneDossierTemplateDialogComponent } from '../dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component';
import { EditColorDialogComponent } from '../dialogs/edit-color-dialog/edit-color-dialog.component';
import { SmtpAuthDialogComponent } from '../dialogs/smtp-auth-dialog/smtp-auth-dialog.component';
import { AddEditUserDialogComponent } from '../dialogs/add-edit-user-dialog/add-edit-user-dialog.component';
@ -28,7 +28,6 @@ import { ActiveDossiersService } from '@services/dossiers/active-dossiers.servic
import { UserService } from '@services/user.service';
import { IDossierAttributeConfig, IFileAttributeConfig, IReportTemplate } from '@red/domain';
import { ReportTemplateService } from '@services/report-template.service';
import { CloneDossierTemplateDialogComponent } from '../dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component';
import { ConfigureCertificateDialogComponent } from '../dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
type DialogType =
@ -40,8 +39,7 @@ type DialogType =
| 'fileAttributesConfigurations'
| 'addEditUser'
| 'smtpAuthConfig'
| 'addEditDossierTemplate'
| 'cloneDossierTemplate'
| 'addEditCloneDossierTemplate'
| 'addEditDossierAttribute'
| 'uploadDictionary'
| 'addEditDossierState'
@ -82,14 +80,10 @@ export class AdminDialogService extends DialogService<DialogType> {
component: SmtpAuthDialogComponent,
dialogConfig: { autoFocus: true },
},
addEditDossierTemplate: {
component: AddEditDossierTemplateDialogComponent,
addEditCloneDossierTemplate: {
component: AddEditCloneDossierTemplateDialogComponent,
dialogConfig: { width: '900px', autoFocus: true },
},
cloneDossierTemplate: {
component: CloneDossierTemplateDialogComponent,
dialogConfig: { disableClose: false },
},
addEditDossierAttribute: {
component: AddEditDossierAttributeDialogComponent,
dialogConfig: { autoFocus: true },

View File

@ -7,14 +7,14 @@
></iqser-circle-button>
<iqser-circle-button
(action)="openCloneDossierTemplateDialog($event)"
(action)="openEditCloneDossierTemplateDialog($event, true)"
[tooltip]="'dossier-templates-listing.action.clone' | translate"
icon="iqser:copy"
[type]="circleButtonTypes.dark"
></iqser-circle-button>
<iqser-circle-button
(action)="openEditDossierTemplateDialog($event)"
(action)="openEditCloneDossierTemplateDialog($event)"
[tooltip]="'dossier-templates-listing.action.edit' | translate"
icon="iqser:edit"
[type]="circleButtonTypes.dark"

View File

@ -31,12 +31,8 @@ export class DossierTemplateActionsComponent implements OnInit {
this.dossierTemplateId ??= this._route.snapshot.paramMap.get(DOSSIER_TEMPLATE_ID);
}
openEditDossierTemplateDialog($event: MouseEvent) {
this._dialogService.openDialog('addEditDossierTemplate', $event, this.dossierTemplateId);
}
openCloneDossierTemplateDialog($event: MouseEvent) {
this._dialogService.openDialog('cloneDossierTemplate', $event, this.dossierTemplateId);
openEditCloneDossierTemplateDialog($event: MouseEvent, clone: boolean = false) {
this._dialogService.openDialog('addEditCloneDossierTemplate', $event, { dossierTemplateId: this.dossierTemplateId, clone });
}
openDeleteDossierTemplateDialog($event?: MouseEvent) {

View File

@ -14,6 +14,8 @@
[noDataText]="'archived-dossiers-listing.no-data.title' | translate"
[noMatchText]="'archived-dossiers-listing.no-match.title' | translate"
[tableColumnConfigs]="tableColumnConfigs"
[hasScrollButton]="true"
[helpModeKey]="'dossier'"
noDataIcon="red:folder"
></iqser-table>
</div>

View File

@ -1,21 +0,0 @@
<div>
<div
[class.error]="file.isError"
[class.initial-processing]="file.isInitialProcessing"
[matTooltip]="file.filename"
class="table-item-title"
matTooltipPosition="above"
>
{{ file.filename }}
</div>
</div>
<div *ngIf="primaryAttribute" class="small-label">
<div class="primary-attribute">
<span [matTooltip]="primaryAttribute" matTooltipPosition="above">
{{ primaryAttribute }}
</span>
</div>
</div>
<redaction-file-stats [file]="file"></redaction-file-stats>

View File

@ -1,18 +0,0 @@
@use 'common-mixins';
.table-item-title {
max-width: 25vw;
&.error {
color: var(--iqser-red-1);
}
&.initial-processing {
color: var(--iqser-disabled);
}
}
.primary-attribute {
padding-top: 6px;
@include common-mixins.line-clamp(1);
}

View File

@ -1,21 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { File } from '@red/domain';
import { PrimaryFileAttributeService } from '@services/primary-file-attribute.service';
@Component({
selector: 'redaction-file-name-column',
templateUrl: './file-name-column.component.html',
styleUrls: ['./file-name-column.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileNameColumnComponent implements OnChanges {
@Input() file: File;
@Input() dossierTemplateId: string;
primaryAttribute: string;
constructor(private readonly _primaryFileAttributeService: PrimaryFileAttributeService) {}
ngOnChanges() {
this.primaryAttribute = this._primaryFileAttributeService.getPrimaryFileAttributeValue(this.file, this.dossierTemplateId);
}
}

View File

@ -24,7 +24,7 @@
[showNoDataButton]="permissionsService.canUploadFiles(dossier)"
[tableColumnConfigs]="tableColumnConfigs"
[tableItemClasses]="{ disabled: disabledFn, 'last-opened': lastOpenedFn }"
iqserHelpMode="document-list"
[helpModeKey]="'documents'"
></iqser-table>
<iqser-workflow

View File

@ -18,6 +18,7 @@
[noMatchText]="'dossier-listing.no-match.title' | translate"
[showNoDataButton]="permissionsService.canCreateDossier()"
[tableColumnConfigs]="tableColumnConfigs"
[helpModeKey]="'dossier'"
noDataIcon="red:folder"
></iqser-table>
</div>

View File

@ -9,6 +9,7 @@ import { firstValueFrom } from 'rxjs';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import { LicenseService } from '../../../services/license.service';
import PDFNet = Core.PDFNet;
@Injectable()
@ -21,6 +22,7 @@ export class StampService {
private readonly _viewModeService: ViewModeService,
private readonly _translateService: TranslateService,
private readonly _watermarkService: WatermarkService,
private readonly _licenseService: LicenseService,
) {}
async stampPDF(): Promise<void> {
@ -33,7 +35,7 @@ export class StampService {
const allPages = [...Array(file.numberOfPages).keys()].map(page => page + 1);
try {
await clearStamps(pdfDoc, this._pdf.PDFNet, allPages);
await clearStamps(pdfDoc, this._pdf.PDFNet, allPages, this._licenseService.activeLicenseKey);
} catch (e) {
console.error('Error clearing stamps: ', e);
return;
@ -63,6 +65,7 @@ export class StampService {
50,
'#dd4d50',
excludedPages,
this._licenseService.activeLicenseKey,
);
}
}
@ -79,6 +82,7 @@ export class StampService {
watermark.opacity,
watermark.hexColor,
Array.from({ length: await document.getPageCount() }, (x, i) => i + 1),
this._licenseService.activeLicenseKey,
);
}
}

View File

@ -7,6 +7,7 @@ export const processPage = async (
document2: Core.PDFNet.PDFDoc,
mergedDocument: Core.PDFNet.PDFDoc,
pdfNet: typeof Core.PDFNet,
licenseKey: string,
) => {
const document1PageCount = await document1.getPageCount();
@ -19,7 +20,16 @@ export const processPage = async (
const blankPage = await mergedDocument.pageCreate(await pageToCopy.getCropBox());
await blankPage.setRotation(await pageToCopy.getRotation());
await mergedDocument.pagePushBack(blankPage);
await stampPDFPage(mergedDocument, pdfNet, '<< Compare Placeholder Page >>', 20, 'courier', 'DIAGONAL', 33, '#ffb83b', [
await mergedDocument.getPageCount(),
]);
await stampPDFPage(
mergedDocument,
pdfNet,
'<< Compare Placeholder Page >>',
20,
'courier',
'DIAGONAL',
33,
'#ffb83b',
[await mergedDocument.getPageCount()],
licenseKey,
);
};

View File

@ -12,6 +12,7 @@ import { firstValueFrom } from 'rxjs';
import { MatDialogRef } from '@angular/material/dialog';
import { processPage } from '../../../file-preview/utils/compare-mode.utils';
import { NGXLogger } from 'ngx-logger';
import { LicenseService } from '@services/license.service';
import PDFDoc = Core.PDFNet.PDFDoc;
@Component({
@ -28,6 +29,7 @@ export class CompareFileInputComponent {
private readonly _loadingService: LoadingService,
private readonly _documentViewer: REDDocumentViewer,
private readonly _filesMapService: FilesMapService,
private readonly _licenseService: LicenseService,
private readonly _dialogService: SharedDialogService,
private readonly _viewerHeaderService: ViewerHeaderService,
) {}
@ -113,8 +115,8 @@ export class CompareFileInputComponent {
const maxPageCount = Math.max(await current.getPageCount(), await compare.getPageCount());
for (let idx = 1; idx <= maxPageCount; idx++) {
await processPage(idx, current, compare, merged, pdfNet);
await processPage(idx, compare, current, merged, pdfNet);
await processPage(idx, current, compare, merged, pdfNet, this._licenseService.activeLicenseKey);
await processPage(idx, compare, current, merged, pdfNet, this._licenseService.activeLicenseKey);
}
const buffer = await merged.saveMemoryBuffer(pdfNet.SDFDoc.SaveOptions.e_linearized);

View File

@ -1,6 +1,5 @@
import { Inject, Injectable, Injector } from '@angular/core';
import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer';
import { environment } from '@environments/environment';
import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens';
import { File, IHeaderElement } from '@red/domain';
import { ErrorService, shareDistinctLast } from '@iqser/common-ui';
@ -14,6 +13,7 @@ import { Rgb } from '../utils/types';
import { asList } from '../utils/functions';
import { REDAnnotationManager } from './annotation-manager.service';
import { TranslateService } from '@ngx-translate/core';
import { LicenseService } from '@services/license.service';
import TextTool = Core.Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
@ -52,6 +52,7 @@ export class PdfViewer {
private readonly _logger: NGXLogger,
private readonly _injector: Injector,
private readonly _activatedRoute: ActivatedRoute,
private readonly _licenseService: LicenseService,
private readonly _translateService: TranslateService,
private readonly _annotationManager: REDAnnotationManager,
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
@ -127,7 +128,8 @@ export class PdfViewer {
async init(htmlElement: HTMLElement) {
this.#instance = await this.#getInstance(htmlElement);
await this.PDFNet.initialize(environment.licenseKey ? window.atob(environment.licenseKey) : null);
await this.PDFNet.initialize(this._licenseService.activeLicenseKey);
this._logger.info('[PDF] Initialized');
this.documentViewer = this.#instance.Core.documentViewer;
@ -271,7 +273,7 @@ export class PdfViewer {
#getInstance(htmlElement: HTMLElement) {
const options: WebViewerOptions = {
licenseKey: environment.licenseKey ? window.atob(environment.licenseKey) : null,
licenseKey: this._licenseService.activeLicenseKey,
fullAPI: true,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),

View File

@ -76,7 +76,7 @@
<div class="cell small-label stats-subtitle">
<div>
<mat-icon *ngIf="item.isArchived" svgIcon="red:archive"></mat-icon>
<mat-icon *ngIf="item.archived" svgIcon="red:archive"></mat-icon>
{{ item.dossierName }}
</div>
</div>

View File

@ -10,6 +10,7 @@ interface PartialFile {
readonly excludedPages: number[];
readonly lastOCRTime?: string;
readonly fileAttributes: FileAttributes;
readonly lastManualChangeDate?: string;
}
@Component({

View File

@ -2,23 +2,24 @@
<div class="dialog-header heading-l" translate="overwrite-files-dialog.title"></div>
<div class="dialog-content">
<p [innerHTML]="'overwrite-files-dialog.question' | translate: { filename: filename }"></p>
<p [innerHTML]="'overwrite-files-dialog.question' | translate: { filename: filename }" class="mb-24"></p>
<mat-checkbox (change)="remember = !remember" [checked]="remember" class="flex-1" color="primary">
{{ 'overwrite-files-dialog.options.remember' | translate }}
</mat-checkbox>
<form [formGroup]="form">
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>
</form>
</div>
<div class="dialog-actions">
<button
(click)="selectOption('overwrite')"
color="primary"
mat-flat-button
translate="overwrite-files-dialog.options.overwrite"
></button>
<iqser-icon-button
(action)="selectOption('skip')"
[label]="'overwrite-files-dialog.options.skip' | translate"
(action)="selectOption(false)"
[disabled]="!form.valid"
[label]="'overwrite-files-dialog.options.current-files' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<iqser-icon-button
(action)="selectOption(true)"
[disabled]="!form.valid"
[label]="'overwrite-files-dialog.options.all-files' | translate"
[type]="iconButtonTypes.dark"
></iqser-icon-button>
<div (click)="cancel()" class="all-caps-label cancel" translate="overwrite-files-dialog.options.cancel"></div>

View File

@ -1,7 +1,10 @@
import { Component, Inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { IconButtonTypes } from '@iqser/common-ui';
import { DetailsRadioOption, IconButtonTypes } from '@iqser/common-ui';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
@Component({
selector: 'redaction-overwrite-files-dialog',
@ -10,19 +13,41 @@ import { IconButtonTypes } from '@iqser/common-ui';
})
export class OverwriteFilesDialogComponent {
readonly iconButtonTypes = IconButtonTypes;
remember = false;
readonly form: FormGroup;
readonly options: DetailsRadioOption<OverwriteFileOption>[] = [
{
label: _('overwrite-files-dialog.options.full-overwrite.label'),
value: OverwriteFileOptions.FULL_OVERWRITE,
description: _('overwrite-files-dialog.options.full-overwrite.description'),
},
{
label: _('overwrite-files-dialog.options.partial-overwrite.label'),
value: OverwriteFileOptions.PARTIAL_OVERWRITE,
description: _('overwrite-files-dialog.options.partial-overwrite.description'),
},
{
label: _('overwrite-files-dialog.options.skip.label'),
value: OverwriteFileOptions.SKIP,
description: _('overwrite-files-dialog.options.skip.description'),
},
];
constructor(
private readonly _translateService: TranslateService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<OverwriteFilesDialogComponent>,
@Inject(MAT_DIALOG_DATA) public filename: string,
) {}
) {
this.form = this._formBuilder.group({
option: [this.options[0], Validators.required],
});
}
cancel() {
this.dialogRef.close();
}
selectOption(option: 'overwrite' | 'skip') {
this.dialogRef.close({ option, remember: this.remember });
selectOption(remember: boolean) {
this.dialogRef.close({ option: this.form.get('option').value.value, remember });
}
}

View File

@ -9,4 +9,5 @@ export interface FileUploadModel {
typeError: boolean;
dossierId?: string;
dossierName?: string;
keepManualRedactions?: boolean;
}

View File

@ -4,7 +4,7 @@ import { HttpErrorResponse, HttpEventType, HttpStatusCode } from '@angular/commo
import { interval, Subject, Subscription } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { TranslateService } from '@ngx-translate/core';
import { IFileUploadResult } from '@red/domain';
import { IFileUploadResult, OverwriteFileOption, OverwriteFileOptions } from '@red/domain';
import { isAcceptedFileType, isCsv } from '@utils/file-drop-utils';
import { ErrorMessageService, GenericService, HeadersConfiguration, RequiredParam, Validate } from '@iqser/common-ui';
import { FilesMapService } from '@services/files/files-map.service';
@ -70,7 +70,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
const maxSizeMB = this._configService.values.MAX_FILE_SIZE_MB;
const maxSizeBytes = maxSizeMB * 1024 * 1024;
const dossierFiles = this._filesMapService.get(dossierId);
let option: 'overwrite' | 'skip';
let option: OverwriteFileOption;
for (let idx = 0; idx < files.length; ++idx) {
const file = files[idx];
let currentOption = option;
@ -84,7 +84,11 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
option = res.remember ? currentOption : undefined;
}
if (currentOption === 'skip') {
if (currentOption === OverwriteFileOptions.PARTIAL_OVERWRITE) {
file.keepManualRedactions = true;
}
if (currentOption === OverwriteFileOptions.SKIP) {
files.splice(idx, 1);
--idx;
continue;
@ -106,6 +110,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
file.sizeError = true;
}
}
this.files.push(...files);
files.forEach(newFile => {
this._addFileToGroup(newFile);
@ -139,9 +144,11 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
}
@Validate()
uploadFileForm(@RequiredParam() dossierId: string, file?: Blob) {
uploadFileForm(@RequiredParam() dossierId: string, keepManualRedactions = false, file?: Blob) {
const formParams = new FormData();
formParams.append('keepManualRedactions', keepManualRedactions.toString());
if (file !== undefined) {
formParams.append('file', file);
}
@ -190,7 +197,7 @@ export class FileUploadService extends GenericService<IFileUploadResult> impleme
private _createSubscription(uploadFile: FileUploadModel) {
this.activeUploads++;
const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.file);
const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.keepManualRedactions, uploadFile.file);
return obs.subscribe(
event => {
if (event.type === HttpEventType.UploadProgress) {

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { OverwriteFilesDialogComponent } from '../dialogs/overwrite-files-dialog/overwrite-files-dialog.component';
import { firstValueFrom } from 'rxjs';
import { OverwriteFileOption } from '@red/domain';
const dialogConfig = {
width: '662px',
@ -13,7 +14,7 @@ const dialogConfig = {
export class UploadDownloadDialogService {
constructor(private readonly _dialog: MatDialog) {}
openOverwriteFileDialog(filename: string): Promise<{ option?: 'overwrite' | 'skip'; remember?: boolean; cancel?: boolean }> {
openOverwriteFileDialog(filename: string): Promise<{ option?: OverwriteFileOption; remember?: boolean; cancel?: boolean }> {
const ref = this._dialog.open(OverwriteFilesDialogComponent, {
...dialogConfig,
data: filename,

View File

@ -0,0 +1,88 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService, QueryParam, RequiredParam, Validate } from '@iqser/common-ui';
import { ILicense, ILicenseReport, ILicenseReportRequest, ILicenses } from '@red/domain';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { ConfigService } from './config.service';
import { filter, tap } from 'rxjs/operators';
import { LICENSE_STORAGE_KEY } from '../modules/admin/screens/license/utils/constants';
export function getStoredReports() {
const rawStoredReports = localStorage.getItem(LICENSE_STORAGE_KEY);
return JSON.parse(rawStoredReports ?? '{}') as Record<string, ILicenseReport>;
}
@Injectable({
providedIn: 'root',
})
export class LicenseService extends GenericService<ILicenseReport> {
storedReports = getStoredReports();
readonly licenseData$: Observable<ILicenses>;
readonly selectedLicense$: Observable<ILicense>;
activeLicenseId: string;
readonly #licenseData$ = new BehaviorSubject<ILicenses | undefined>(undefined);
readonly #selectedLicense$ = new BehaviorSubject<ILicense | undefined>(undefined);
constructor(protected readonly _injector: Injector, private readonly _configService: ConfigService) {
super(_injector, 'report');
this.selectedLicense$ = this.#selectedLicense$.pipe(filter(license => !!license));
this.licenseData$ = this.#licenseData$.pipe(
filter(licenses => !!licenses),
tap(data => (this.activeLicenseId = data.activeLicense)),
);
}
get selectedLicense() {
return this.#selectedLicense$.value;
}
get processingPages() {
const processingPagesFeature = this.#selectedLicense$.value?.features?.find(f => f.name === 'processingPages');
return Number(processingPagesFeature?.value ?? '0');
}
get activeLicense() {
if (!this.#licenseData$.value) {
return undefined;
}
const { licenses, activeLicense: activeLicenseId } = this.#licenseData$.value;
return licenses.find(license => license.id === activeLicenseId);
}
get activeLicenseKey(): string {
return this.activeLicense.features.find(f => f.name === 'pdftron').value;
}
setDefaultSelectedLicense() {
this.setSelectedLicense(this.activeLicense);
}
@Validate()
getReport$(@RequiredParam() body: ILicenseReportRequest, limit?: number, offset?: number) {
const queryParams: QueryParam[] = [];
if (limit) {
queryParams.push({ key: 'limit', value: limit });
}
if (offset) {
queryParams.push({ key: 'offset', value: offset });
}
return this._post(body, `${this._defaultModelPath}/license`, queryParams);
}
getReport(body: ILicenseReportRequest, limit?: number, offset?: number) {
return firstValueFrom(this.getReport$(body, limit, offset));
}
loadLicense() {
return this._http.get<ILicenses>('/license').pipe(
tap(licenses => this.#licenseData$.next(licenses)),
tap(() => this.setDefaultSelectedLicense()),
);
}
setSelectedLicense($event: ILicense) {
this.#selectedLicense$.next($event);
}
}

View File

@ -2,16 +2,19 @@ import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
const LAST_DOSSIERS_SCREEN = 'routerHistory_lastDossiersScreen';
@Injectable({
providedIn: 'root',
})
export class RouterHistoryService {
private _lastDossiersScreen = '/';
private _lastDossiersScreen = localStorage.getItem(LAST_DOSSIERS_SCREEN);
constructor(private readonly _router: Router) {
this._router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => {
if (event.url.includes('/dossiers') || event.url.includes('/archive')) {
this._lastDossiersScreen = event.url;
localStorage.setItem(LAST_DOSSIERS_SCREEN, this._lastDossiersScreen);
}
});
}

View File

@ -24,4 +24,7 @@ export const processingFileStatusTranslations: { [key in ProcessingFileStatus]:
REPROCESS: _('file-status.reprocess'),
SURROUNDING_TEXT_PROCESSING: _('file-status.processing'),
UNPROCESSED: _('file-status.unprocessed'),
PRE_PROCESSING: _('file-status.full-processing'),
PRE_PROCESSED: _('file-status.processed'),
PRE_PROCESSING_FAILED: _('file-status.error'),
};

View File

@ -9,6 +9,7 @@ import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { FeaturesService } from '@services/features.service';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { LicenseService } from '@services/license.service';
function lastDossierTemplateRedirect(baseHref: string, userPreferenceService: UserPreferenceService) {
const url = window.location.href.split('/').filter(s => s.length > 0);
@ -29,6 +30,7 @@ export function configurationInitializer(
languageService: LanguageService,
userService: UserService,
userPreferenceService: UserPreferenceService,
licenseService: LicenseService,
) {
return () =>
firstValueFrom(
@ -53,6 +55,7 @@ export function configurationInitializer(
}),
switchMap(() => languageService.chooseAndSetInitialLanguage()),
tap(() => userService.initialize()),
tap(() => firstValueFrom(licenseService.loadLicense())),
take(1),
),
);

View File

@ -1,5 +1,4 @@
import { hexToRgb } from './functions';
import { environment } from '@environments/environment';
import { Core } from '@pdftron/webviewer';
import PDFDoc = Core.PDFNet.PDFDoc;
@ -25,15 +24,12 @@ function convertFont(type: string): number {
return 4;
}
export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet, pages: number[]) {
await pdfNet.runWithCleanup(
async () => {
await document.lock();
const pageSet = await createPageSet(pdfNet, pages);
await pdfNet.Stamper.deleteStamps(document, pageSet);
},
environment.licenseKey ? atob(environment.licenseKey) : null,
);
export async function clearStamps(document: PDFDoc, pdfNet: typeof Core.PDFNet, pages: number[], licenseKey: string) {
await pdfNet.runWithCleanup(async () => {
await document.lock();
const pageSet = await createPageSet(pdfNet, pages);
await pdfNet.Stamper.deleteStamps(document, pageSet);
}, licenseKey);
}
export async function stampPDFPage(
@ -46,43 +42,41 @@ export async function stampPDFPage(
opacity: number,
color: string,
pages: number[],
licenseKey: string,
) {
await pdfNet.runWithCleanup(
async () => {
await document.lock();
await pdfNet.runWithCleanup(async () => {
await document.lock();
const pageSet = await createPageSet(pdfNet, pages);
await pdfNet.Stamper.deleteStamps(document, pageSet);
const pageSet = await createPageSet(pdfNet, pages);
await pdfNet.Stamper.deleteStamps(document, pageSet);
const rgbColor = hexToRgb(color);
const rgbColor = hexToRgb(color);
const stamper = await pdfNet.Stamper.create(3, fontSize, 0);
await stamper.setFontColor(await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255));
await stamper.setOpacity(opacity / 100);
const stamper = await pdfNet.Stamper.create(3, fontSize, 0);
await stamper.setFontColor(await pdfNet.ColorPt.init(rgbColor.r / 255, rgbColor.g / 255, rgbColor.b / 255));
await stamper.setOpacity(opacity / 100);
switch (orientation) {
case 'VERTICAL':
await stamper.setAlignment(0, 0);
await stamper.setRotation(-90);
break;
case 'HORIZONTAL':
break;
case 'TOP_LEFT':
await stamper.setAlignment(-1, 1);
await stamper.setRotation(90);
await stamper.setPosition(20, 20);
break;
case 'DIAGONAL':
default:
await stamper.setAlignment(0, 0);
await stamper.setRotation(-45);
}
switch (orientation) {
case 'VERTICAL':
await stamper.setAlignment(0, 0);
await stamper.setRotation(-90);
break;
case 'HORIZONTAL':
break;
case 'TOP_LEFT':
await stamper.setAlignment(-1, 1);
await stamper.setRotation(90);
await stamper.setPosition(20, 20);
break;
case 'DIAGONAL':
default:
await stamper.setAlignment(0, 0);
await stamper.setRotation(-45);
}
const font = await pdfNet.Font.createAndEmbed(document, convertFont(fontType));
await stamper.setFont(font);
await stamper.setTextAlignment(0);
await stamper.stampText(document, text, pageSet);
},
environment.licenseKey ? atob(environment.licenseKey) : null,
);
const font = await pdfNet.Font.createAndEmbed(document, convertFont(fontType));
await stamper.setFont(font);
await stamper.setTextAlignment(0);
await stamper.stampText(document, text, pageSet);
}, licenseKey);
}

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-05.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://dev-08.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -16,7 +16,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-05.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://dev-08.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview"

View File

@ -125,14 +125,8 @@
"it": "",
"fr": ""
},
"documents_scroll_up_button": {
"en": "/en/index-en.html?contextId=documents_scroll_up_and_down",
"de": "",
"it": "",
"fr": ""
},
"documents_scroll_down_button": {
"en": "/en/index-en.html?contextId=documents_scroll_up_and_down",
"scroll_documents_list": {
"en": "/en/index-en.html?contextId=scroll_documents_list",
"de": "",
"it": "",
"fr": ""

View File

@ -36,6 +36,22 @@
},
"header-new": "Dossier erstellen"
},
"add-edit-clone-dossier-template": {
"error": {
"conflict": "Dossiervorlage konnte nicht erstellt werden: Es existiert bereits eine Dossiervorlage mit demselben Namen.",
"generic": "Fehler beim Erstellen der Dossiervorlage."
},
"form": {
"description": "Beschreibung",
"description-placeholder": "Beschreibung eingeben",
"name": "Name der Dossier-Vorlage",
"name-placeholder": "Namen eingeben",
"valid-from": "Gültig ab",
"valid-to": "Gültig bis"
},
"save": "Dossier-Vorlage speichern",
"title": "{type, select, edit{Dossier-Vorlage {name} bearbeiten} create{Dossier-Vorlage erstellen} clone{} other{}}"
},
"add-edit-dossier-attribute": {
"error": {
"generic": "Attribut konnte nicht gespeichert werden!"
@ -61,22 +77,6 @@
"success": "",
"title": ""
},
"add-edit-dossier-template": {
"error": {
"conflict": "Dossiervorlage konnte nicht erstellt werden: Es existiert bereits eine Dossiervorlage mit demselben Namen.",
"generic": "Fehler beim Erstellen der Dossiervorlage."
},
"form": {
"description": "Beschreibung",
"description-placeholder": "Beschreibung eingeben",
"name": "Name der Dossier-Vorlage",
"name-placeholder": "Namen eingeben",
"valid-from": "Gültig ab",
"valid-to": "Gültig bis"
},
"save": "Dossier-Vorlage speichern",
"title": "{type, select, edit{Dossier-Vorlage {name} bearbeiten} create{Dossier-Vorlage erstellen} other{}}"
},
"add-edit-entity": {
"error": {
"entity-already-exists": "",
@ -455,19 +455,6 @@
},
"header": "Begründung für die Schwärzung bearbeiten"
},
"clone-dossier-template": {
"actions": {
"save": ""
},
"content": {
"name": "",
"name-placeholder": ""
},
"error": {
"generic": ""
},
"title": ""
},
"color": "",
"comments": {
"add-comment": "Kommentar eingeben",
@ -577,6 +564,12 @@
"title": "{count, plural, one{{justificationName}} other{ausgewählte Begründungen}} löschen"
},
"input-label": "Bitte geben Sie unten Folgendes ein, um fortzufahren",
"keep-manual-redactions": {
"confirmation-text": "",
"deny-text": "",
"question": "",
"title": ""
},
"report-template-same-name": {
"confirmation-text": "Ja. Hochladen fortsetzen",
"deny-text": "Nein. Hochladen abbrechen",
@ -1472,11 +1465,13 @@
"labels": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"remove-digital-signature-on-upload": "",
"soft-delete-cleanup-time": ""
},
"placeholders": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"remove-digital-signature-on-upload": "",
"soft-delete-cleanup-time": ""
},
"title": ""
@ -1700,10 +1695,21 @@
},
"overwrite-files-dialog": {
"options": {
"all-files": "",
"cancel": "Alle Uploads abbrechen",
"overwrite": "Vorhandenes Dokument ersetzen",
"remember": "Auswahl speichern und nicht noch einmal fragen",
"skip": "Vorhandenes Dokument behalten"
"current-files": "",
"full-overwrite": {
"description": "",
"label": ""
},
"partial-overwrite": {
"description": "",
"label": ""
},
"skip": {
"description": "",
"label": ""
}
},
"question": "<b>{filename}</b> ist bereits vorhanden. Wie möchten Sie fortfahren?",
"title": "Das Dokument existiert bereits!"

View File

@ -36,6 +36,22 @@
},
"header-new": "Create Dossier"
},
"add-edit-clone-dossier-template": {
"error": {
"conflict": "Failed to create dossier template: a dossier template with the same name already exists.",
"generic": "Failed to create dossier template."
},
"form": {
"description": "Description",
"description-placeholder": "Enter Description",
"name": "Dossier Template Name",
"name-placeholder": "Enter Name",
"valid-from": "Valid from",
"valid-to": "Valid to"
},
"save": "Save Dossier Template",
"title": "{type, select, edit{Edit {name}} create{Create} clone{Clone} other{}} Dossier Template"
},
"add-edit-dossier-attribute": {
"error": {
"generic": "Failed to save attribute!"
@ -61,22 +77,6 @@
"success": "Successfully {type, select, edit{updated} create{created} other{}} the dossier state!",
"title": "{type, select, edit{Edit {name}} create{Create} other{}} Dossier State"
},
"add-edit-dossier-template": {
"error": {
"conflict": "Failed to create dossier template: a dossier template with the same name already exists.",
"generic": "Failed to create dossier template."
},
"form": {
"description": "Description",
"description-placeholder": "Enter Description",
"name": "Dossier Template Name",
"name-placeholder": "Enter Name",
"valid-from": "Valid from",
"valid-to": "Valid to"
},
"save": "Save Dossier Template",
"title": "{type, select, edit{Edit {name}} create{Create} other{}} Dossier Template"
},
"add-edit-entity": {
"error": {
"entity-already-exists": "Entity with this name already exists!",
@ -455,19 +455,6 @@
},
"header": "Edit Redaction Reason"
},
"clone-dossier-template": {
"actions": {
"save": "Save Dossier Template"
},
"content": {
"name": "Dossier Template Name",
"name-placeholder": "Enter Name"
},
"error": {
"generic": "Failed to clone dossier template."
},
"title": "Clone Dossier Template"
},
"color": "Color",
"comments": {
"add-comment": "Enter comment",
@ -577,6 +564,12 @@
"title": "Delete {count, plural, one{{justificationName}} other{Selected Justifications}}"
},
"input-label": "To proceed please type below",
"keep-manual-redactions": {
"confirmation-text": "Keep",
"deny-text": "Clear",
"question": "At least one of the files you're overwriting has manual changes, do you wish to keep them?",
"title": "Keep Manual Redactions?"
},
"report-template-same-name": {
"confirmation-text": "Yes. Continue upload",
"deny-text": "No. Cancel Upload",
@ -1471,14 +1464,14 @@
"labels": {
"download-cleanup-download-files-hours": "Cleanup time for downloaded files (hours)",
"download-cleanup-not-download-files-hours": "Cleanup time for not downloaded files (hours)",
"soft-delete-cleanup-time": "Soft delete cleanup time (hours)",
"remove-digital-signature-on-upload": "Remove Digital Signature on Upload"
"remove-digital-signature-on-upload": "Remove Digital Signature on Upload",
"soft-delete-cleanup-time": "Soft delete cleanup time (hours)"
},
"placeholders": {
"download-cleanup-download-files-hours": "(hours)",
"download-cleanup-not-download-files-hours": "(hours)",
"soft-delete-cleanup-time": "(hours)",
"remove-digital-signature-on-upload": "True / False"
"remove-digital-signature-on-upload": "True / False",
"soft-delete-cleanup-time": "(hours)"
},
"title": "System Preferences"
},
@ -1701,10 +1694,21 @@
},
"overwrite-files-dialog": {
"options": {
"cancel": "Cancel all uploads",
"overwrite": "Replace existing document",
"remember": "Remember choice and don't ask me again",
"skip": "Keep existing document"
"all-files": "Apply to all remaining files in current upload",
"cancel": "Cancel upload",
"current-files": "Apply to current file",
"full-overwrite": {
"description": "Manual changes done to the existing file will be removed and you are able to start over with redactions.",
"label": "Overwrite and start over"
},
"partial-overwrite": {
"description": "Manual changes are kept only if the affected redactions are still at the same position in the file. Some redactions could be misplaced if the content of the file changed.",
"label": "Overwrite and keep manual changes"
},
"skip": {
"description": "The upload will be skipped and the existing file will not be replaced.",
"label": "Keep the existing file and do not overwrite"
}
},
"question": "<b>{filename}</b> already exists. Choose how to proceed:",
"title": "Document already exists!"

@ -1 +1 @@
Subproject commit ea3c8b7f32be0e23c0b47a50bfda524d0b58107d
Subproject commit 09dfcac064c6c496bb67cf13e3c5cd6d7819764c

View File

@ -24,3 +24,4 @@ export * from './lib/dossier-state';
export * from './lib/trash';
export * from './lib/text-highlight';
export * from './lib/permissions';
export * from './lib/license';

View File

@ -104,7 +104,9 @@ export class File extends Entity<IFile> implements IFile {
this.numberOfAnalyses = file.numberOfAnalyses;
this.processingStatus = file.processingStatus;
this.workflowStatus = file.workflowStatus;
this.isError = this.processingStatus === ProcessingFileStatuses.ERROR;
this.isError =
this.processingStatus === ProcessingFileStatuses.ERROR ||
this.processingStatus === ProcessingFileStatuses.PRE_PROCESSING_FAILED;
this.isUnprocessed = this.processingStatus === ProcessingFileStatuses.UNPROCESSED;
this.numberOfPages = this.isError ? 0 : file.numberOfPages ?? 0;
this.rulesVersion = file.rulesVersion;

View File

@ -2,3 +2,4 @@ export * from './file';
export * from './file.model';
export * from './types';
export * from './file-upload-result';
export * from './overwrite-file-options';

View File

@ -0,0 +1,7 @@
export const OverwriteFileOptions = {
FULL_OVERWRITE: 'FULL_OVERWRITE',
PARTIAL_OVERWRITE: 'PARTIAL_OVERWRITE',
SKIP: 'SKIP',
} as const;
export type OverwriteFileOption = keyof typeof OverwriteFileOptions;

View File

@ -27,6 +27,9 @@ export const ProcessingFileStatuses = {
REPROCESS: 'REPROCESS',
SURROUNDING_TEXT_PROCESSING: 'SURROUNDING_TEXT_PROCESSING',
UNPROCESSED: 'UNPROCESSED',
PRE_PROCESSING: 'PRE_PROCESSING',
PRE_PROCESSED: 'PRE_PROCESSED',
PRE_PROCESSING_FAILED: 'PRE_PROCESSING_FAILED',
} as const;
export type ProcessingFileStatus = keyof typeof ProcessingFileStatuses;
@ -42,6 +45,7 @@ export const isProcessingStatuses: List<ProcessingFileStatus> = [
ProcessingFileStatuses.PROCESSING,
ProcessingFileStatuses.ANALYSE,
ProcessingFileStatuses.FULL_PROCESSING,
ProcessingFileStatuses.PRE_PROCESSING,
] as const;
export const isFullProcessingStatuses: List<ProcessingFileStatus> = [
@ -53,6 +57,7 @@ export const isFullProcessingStatuses: List<ProcessingFileStatus> = [
ProcessingFileStatuses.NER_ANALYZING,
ProcessingFileStatuses.OCR_PROCESSING,
ProcessingFileStatuses.FULL_PROCESSING,
ProcessingFileStatuses.PRE_PROCESSING,
] as const;
export interface StatusBarConfig {

View File

@ -0,0 +1,2 @@
export * from './date-range';
export * from './license';

View File

@ -0,0 +1,21 @@
export interface ILicenseFeature {
readonly name: string;
readonly type: string;
readonly value: string;
}
export interface ILicense {
readonly id: string;
readonly name: string;
readonly product: string;
readonly licensedTo: string;
readonly licensedToEmail: string;
readonly validFrom: string;
readonly validUntil: string;
readonly features: readonly ILicenseFeature[];
}
export interface ILicenses {
readonly activeLicense: string;
readonly licenses: readonly ILicense[];
}

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.506.0",
"version": "3.514.0",
"private": true,
"license": "MIT",
"scripts": {

View File

@ -49,12 +49,12 @@
/* Einleitung */
.cat-panel-1:before {
content: '\f277';
content: '\f007';
}
/* Workflow */
.cat-panel-2:before {
content: '\f0c1';
content: '\f085';
}
/* Voraussetzungen */

Binary file not shown.

View File

@ -144,12 +144,12 @@
/* Einleitung */
.cat-panel-1:before {
content: '\f277';
content: '\f007';
}
/* Workflow */
.cat-panel-2:before {
content: '\f0c1';
content: '\f085';
}
/* Voraussetzungen */
@ -440,7 +440,7 @@ main article {
}
section {
position: relative;
position: relative;
}
section > .titlepage .title {
@ -484,9 +484,9 @@ section > .titlepage .title {
}
.checklist-reset-wrapper {
position: absolute;
right: 0;
top: 0;
position: absolute;
right: 0;
top: 0;
}
.warning,