diff --git a/.editorconfig b/.editorconfig index 347f00f8a..73ad2e311 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,5 @@ ij_typescript_spaces_within_imports = true [{*.json, .prettierrc, .eslintrc}] indent_size = 2 +tab_width = 2 +ij_json_array_wrapping = off diff --git a/.prettierignore b/.prettierignore index 62d06f966..81d117d52 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ /coverage /node_modules /bamboo-specs +*.md diff --git a/apps/red-ui/.eslintrc.json b/apps/red-ui/.eslintrc.json index a4c08896a..3261c1447 100644 --- a/apps/red-ui/.eslintrc.json +++ b/apps/red-ui/.eslintrc.json @@ -12,9 +12,13 @@ "rules": { "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/unbound-method": "off" + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-floating-promises": "off" } } ] diff --git a/apps/red-ui/src/app/app.component.ts b/apps/red-ui/src/app/app.component.ts index 39033dde8..10eefc848 100644 --- a/apps/red-ui/src/app/app.component.ts +++ b/apps/red-ui/src/app/app.component.ts @@ -2,6 +2,7 @@ import { Component, ViewContainerRef } from '@angular/core'; import { RouterHistoryService } from '@services/router-history.service'; import { UserService } from '@services/user.service'; import { REDDocumentViewer } from './modules/pdf-viewer/services/document-viewer.service'; +import { DossiersChangesService } from '@services/dossiers/dossier-changes.service'; @Component({ selector: 'redaction-root', @@ -12,9 +13,15 @@ export class AppComponent { // ViewContainerRef needs to be injected for the color picker to work // RouterHistoryService needs to be injected for last dossiers screen to be updated on first app load constructor( - public viewContainerRef: ViewContainerRef, + readonly viewContainerRef: ViewContainerRef, private readonly _routerHistoryService: RouterHistoryService, private readonly _userService: UserService, readonly documentViewer: REDDocumentViewer, - ) {} + private readonly _dossierChangesService: DossiersChangesService, + ) { + // TODO: Find a better place to initialize dossiers refresh + if (_userService.currentUser?.isUser) { + _dossierChangesService.initializeRefresh(); + } + } } diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index f40766fdc..c1ef70792 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -65,9 +65,8 @@ function cleanupBaseUrl(baseUrl: string) { return ''; } else if (baseUrl[baseUrl.length - 1] === '/') { return baseUrl.substring(0, baseUrl.length - 1); - } else { - return baseUrl; } + return baseUrl; } const screens = [BaseScreenComponent, DownloadsListScreenComponent]; @@ -128,7 +127,13 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp enabled: false, }, PDF: { - enabled: true, + enabled: false, + }, + FILE: { + enabled: false, + }, + CHANGES: { + enabled: false, }, STATS: { enabled: false, diff --git a/apps/red-ui/src/app/guards/dossiers.guard.ts b/apps/red-ui/src/app/guards/dossiers.guard.ts index 1476110b7..9d419ce78 100644 --- a/apps/red-ui/src/app/guards/dossiers.guard.ts +++ b/apps/red-ui/src/app/guards/dossiers.guard.ts @@ -37,11 +37,6 @@ export class DossiersGuard implements CanActivate { return false; } - if (!isArchive && dossierTemplateStats?.numberOfActiveDossiers === 0) { - await this._router.navigate(['main', dossierTemplateId, 'archive']); - return false; - } - await firstValueFrom(dossiersService.loadAll()); return true; } diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index 2bde17f99..25196da0e 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -415,13 +415,24 @@ export class AnnotationWrapper implements IListable, Record { } break; case ManualRedactionType.REMOVE_FROM_DICTIONARY: - switch (lastManualChange.annotationStatus) { - case LogEntryStatus.APPROVED: - return SuperTypes.Skipped; - case LogEntryStatus.DECLINED: - return SuperTypes.Redaction; - case LogEntryStatus.REQUESTED: - return SuperTypes.SuggestionRemoveDictionary; + if (redactionLogEntry.redacted) { + switch (lastManualChange.annotationStatus) { + case LogEntryStatus.APPROVED: + return SuperTypes.Skipped; + case LogEntryStatus.DECLINED: + return SuperTypes.Redaction; + case LogEntryStatus.REQUESTED: + return SuperTypes.SuggestionRemoveDictionary; + } + } else { + switch (lastManualChange.annotationStatus) { + case LogEntryStatus.APPROVED: + return SuperTypes.Redaction; + case LogEntryStatus.DECLINED: + return SuperTypes.Skipped; + case LogEntryStatus.REQUESTED: + return SuperTypes.SuggestionRemoveDictionary; + } } break; case ManualRedactionType.FORCE_REDACT: @@ -454,9 +465,8 @@ export class AnnotationWrapper implements IListable, Record { return SuperTypes.Redaction; } else if (redactionLogEntry.hint) { return SuperTypes.Hint; - } else { - return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped; } + return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped; } case LogEntryStatus.REQUESTED: return SuperTypes.SuggestionRecategorizeImage; @@ -481,9 +491,9 @@ export class AnnotationWrapper implements IListable, Record { return redactionLogEntry.type === 'manual' ? SuperTypes.ManualRedaction : SuperTypes.Redaction; } else if (redactionLogEntry.hint) { return SuperTypes.Hint; - } else { - return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped; } + return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped; + case LogEntryStatus.REQUESTED: return SuperTypes.SuggestionResize; } diff --git a/apps/red-ui/src/app/modules/admin/screens/general-config/system-preferences-form/system-preferences-form.component.ts b/apps/red-ui/src/app/modules/admin/screens/general-config/system-preferences-form/system-preferences-form.component.ts index 399199ad0..f14eac539 100644 --- a/apps/red-ui/src/app/modules/admin/screens/general-config/system-preferences-form/system-preferences-form.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/general-config/system-preferences-form/system-preferences-form.component.ts @@ -17,8 +17,8 @@ export class SystemPreferencesFormComponent extends BaseFormComponent { readonly translations = systemPreferencesTranslations; readonly keys: { name: KeysOf; type: ValueType }[] = [ { name: 'softDeleteCleanupTime', type: 'number' }, - { name: 'downloadCleanupDownloadFilesHours', type: 'number' }, { name: 'downloadCleanupNotDownloadFilesHours', type: 'number' }, + { name: 'downloadCleanupDownloadFilesHours', type: 'number' }, ]; private _initialConfiguration: SystemPreferences; diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html new file mode 100644 index 000000000..f72ec50ed --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss new file mode 100644 index 000000000..7ee754487 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.scss @@ -0,0 +1,89 @@ +.ngx-charts { + float: left; + overflow: visible; + + .circle, + .bar, + .arc { + cursor: pointer; + } + + .bar, + .cell, + .arc, + .card { + &.active, + &:hover { + opacity: 0.8; + transition: opacity 100ms ease-in-out; + } + + &:focus { + outline: none; + } + + &.hidden { + display: none; + } + } + + g { + &:focus { + outline: none; + } + } + + .line-series, + .line-series-range, + .area-series { + &.inactive { + transition: opacity 100ms ease-in-out; + opacity: 0.2; + } + } + + .line-highlight { + display: none; + + &.active { + display: block; + } + } + + .area { + opacity: 0.6; + } + + .circle { + &:hover { + cursor: pointer; + } + } + + .label { + font-size: 12px; + font-weight: normal; + } + + .tooltip-anchor { + fill: rgb(0, 0, 0); + } + + .gridline-path { + stroke: #ddd; + stroke-width: 1; + fill: none; + } + + .grid-panel { + rect { + fill: none; + } + + &.odd { + rect { + fill: rgba(0, 0, 0, 0.05); + } + } + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts new file mode 100644 index 000000000..7c5d8cfac --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-chart.component.ts @@ -0,0 +1,393 @@ +import { + Component, + ContentChild, + EventEmitter, + HostListener, + Input, + Output, + TemplateRef, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; + +import { curveLinear } from 'd3-shape'; +import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale'; +import { + BaseChartComponent, + calculateViewDimensions, + Color, + ColorHelper, + LegendPosition, + LineSeriesComponent, + Orientation, + ScaleType, + ViewDimensions, +} from '@swimlane/ngx-charts'; +import { ILineChartSeries } from './models'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'combo-chart-component', + templateUrl: './combo-chart.component.html', + styleUrls: ['./combo-chart.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class ComboChartComponent extends BaseChartComponent { + @Input() curve: any = curveLinear; + @Input() legend = false; + @Input() legendTitle = 'Legend'; + @Input() legendPosition: LegendPosition = LegendPosition.Right; + @Input() xAxis; + @Input() yAxis; + @Input() showXAxisLabel; + @Input() showYAxisLabel; + @Input() showRightYAxisLabel; + @Input() xAxisLabel; + @Input() yAxisLabel; + @Input() yAxisLabelRight; + @Input() tooltipDisabled = false; + @Input() gradient: boolean; + @Input() showGridLines = true; + @Input() activeEntries: any[] = []; + @Input() schemeType: ScaleType; + @Input() yAxisTickFormatting: any; + @Input() yRightAxisTickFormatting: any; + @Input() roundDomains = false; + @Input() colorSchemeLine: Color; + @Input() autoScale; + @Input() lineChart: ILineChartSeries[]; + @Input() yLeftAxisScaleFactor: any; + @Input() yRightAxisScaleFactor: any; + @Input() rangeFillOpacity: number; + @Input() animations = true; + @Input() noBarWhenZero = true; + @Output() activate = new EventEmitter<{ value; entries: unknown[] }>(); + @Output() deactivate = new EventEmitter<{ value; entries: unknown[] }>(); + @ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef; + @ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef; + @ViewChild(LineSeriesComponent) lineSeriesComponent: LineSeriesComponent; + dims: ViewDimensions; + xScale: any; + yScale: any; + xDomain: string[] | number[]; + yDomain: string[] | number[]; + transform: string; + colors: ColorHelper; + colorsLine: ColorHelper; + margin = [10, 20, 10, 20]; + xAxisHeight = 0; + yAxisWidth = 0; + legendOptions: any; + scaleType: ScaleType = ScaleType.Linear; + xScaleLine; + yScaleLine; + xDomainLine; + yDomainLine; + seriesDomain; + combinedSeries: ILineChartSeries[]; + xSet; + filteredDomain; + hoveredVertical; + yOrientLeft: Orientation = Orientation.Left; + yOrientRight: Orientation = Orientation.Right; + legendSpacing = 0; + bandwidth: number; + barPadding = 8; + + @Input() xAxisTickFormatting: any; + + trackBy(index, item): string { + return item.name; + } + + update(): void { + super.update(); + this.dims = calculateViewDimensions({ + width: this.width, + height: this.height, + margins: this.margin, + showXAxis: this.xAxis, + showYAxis: this.yAxis, + xAxisHeight: this.xAxisHeight, + yAxisWidth: this.yAxisWidth, + showXLabel: this.showXAxisLabel, + showYLabel: this.showYAxisLabel, + showLegend: this.legend, + legendType: this.schemeType, + legendPosition: this.legendPosition, + }); + + if (!this.yAxis) { + this.legendSpacing = 0; + } else if (this.showYAxisLabel && this.yAxis) { + this.legendSpacing = 100; + } else { + this.legendSpacing = 40; + } + this.xScale = this.getXScale(); + this.yScale = this.getYScale(); + + // line chart + this.xDomainLine = this.getXDomainLine(); + if (this.filteredDomain) { + this.xDomainLine = this.filteredDomain; + } + + this.yDomainLine = this.getYDomainLine(); + this.seriesDomain = this.getSeriesDomain(); + + this.scaleLines(); + + this.setColors(); + this.legendOptions = this.getLegendOptions(); + + this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`; + } + + deactivateAll() { + this.activeEntries = [...this.activeEntries]; + for (const entry of this.activeEntries) { + this.deactivate.emit({ value: entry, entries: [] }); + } + this.activeEntries = []; + } + + @HostListener('mouseleave') + hideCircles(): void { + this.hoveredVertical = null; + this.deactivateAll(); + } + + updateHoveredVertical(item): void { + this.hoveredVertical = item.value; + this.deactivateAll(); + } + + scaleLines() { + this.xScaleLine = this.getXScaleLine(this.xDomainLine, this.dims.width); + this.yScaleLine = this.getYScaleLine(this.yDomainLine, this.dims.height); + } + + getSeriesDomain(): any[] { + this.combinedSeries = this.lineChart.slice(0); + this.combinedSeries.push({ + name: this.yAxisLabel, + series: this.results, + }); + return this.combinedSeries.map(d => d.name); + } + + isDate(value): value is Date { + return value instanceof Date; + } + + getScaleType(values): ScaleType { + let date = true; + let num = true; + + for (const value of values) { + if (!this.isDate(value)) { + date = false; + } + + if (typeof value !== 'number') { + num = false; + } + } + + if (date) { + return ScaleType.Time; + } + if (num) { + return ScaleType.Linear; + } + return ScaleType.Ordinal; + } + + getXDomainLine(): any[] { + let values: number[] = []; + + for (const results of this.lineChart) { + for (const d of results.series) { + if (!values.includes(d.name)) { + values.push(d.name); + } + } + } + + this.scaleType = this.getScaleType(values); + let domain = []; + + if (this.scaleType === 'time') { + const min = Math.min(...values); + const max = Math.max(...values); + domain = [min, max]; + } else if (this.scaleType === 'linear') { + values = values.map(v => Number(v)); + const min = Math.min(...values); + const max = Math.max(...values); + domain = [min, max]; + } else { + domain = values; + } + + this.xSet = values; + return domain; + } + + getYDomainLine(): any[] { + const domain: number[] = []; + + for (const results of this.lineChart) { + for (const d of results.series) { + if (domain.indexOf(d.value) < 0) { + domain.push(d.value); + } + if (d.min !== undefined) { + if (domain.indexOf(d.min) < 0) { + domain.push(d.min); + } + } + if (d.max !== undefined) { + if (domain.indexOf(d.max) < 0) { + domain.push(d.max); + } + } + } + } + + let min = Math.min(...domain); + const max = Math.max(...domain); + if (this.yRightAxisScaleFactor) { + const minMax = this.yRightAxisScaleFactor(min, max); + return [Math.min(0, minMax.min as number), minMax.max]; + } + min = Math.min(0, min); + return [min, max]; + } + + getXScaleLine(domain, width: number): any { + let scale; + if (this.bandwidth === undefined) { + this.bandwidth = width - this.barPadding; + } + const offset = Math.floor((width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2); + + if (this.scaleType === 'time') { + scale = scaleTime().range([0, width]).domain(domain); + } else if (this.scaleType === 'linear') { + scale = scaleLinear().range([0, width]).domain(domain); + + if (this.roundDomains) { + scale = scale.nice(); + } + } else if (this.scaleType === 'ordinal') { + scale = scalePoint() + .range([offset + this.bandwidth / 2, width - offset - this.bandwidth / 2]) + .domain(domain); + } + + return scale; + } + + getYScaleLine(domain, height): any { + const scale = scaleLinear().range([height, 0]).domain(domain); + + return this.roundDomains ? scale.nice() : scale; + } + + getXScale(): any { + this.xDomain = this.getXDomain(); + const spacing = this.xDomain.length / (this.dims.width / this.barPadding + 1); + return scaleBand().range([0, this.dims.width]).paddingInner(spacing).domain(this.xDomain); + } + + getYScale(): any { + this.yDomain = this.getYDomain(); + const scale = scaleLinear().range([this.dims.height, 0]).domain(this.yDomain); + return this.roundDomains ? scale.nice() : scale; + } + + getXDomain(): any[] { + return this.results.map(d => d.name); + } + + getYDomain() { + const values: number[] = this.results.map(d => d.value); + const min = Math.min(0, ...values); + const max = Math.max(...values); + if (this.yLeftAxisScaleFactor) { + const minMax = this.yLeftAxisScaleFactor(min, max); + return [Math.min(0, minMax.min as number), minMax.max]; + } + return [min, max]; + } + + onClick(data) { + this.select.emit(data); + } + + setColors(): void { + let domain: number[] | string[]; + if (this.schemeType === 'ordinal') { + domain = this.xDomain; + } else { + domain = this.yDomain; + } + this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors); + this.colorsLine = new ColorHelper(this.colorSchemeLine, this.schemeType, domain, this.customColors); + } + + getLegendOptions() { + const opts = { + scaleType: this.schemeType, + colors: undefined, + domain: [], + title: undefined, + position: this.legendPosition, + }; + if (opts.scaleType === 'ordinal') { + opts.domain = this.seriesDomain; + opts.colors = this.colorsLine; + opts.title = this.legendTitle; + } else { + opts.domain = this.seriesDomain; + opts.colors = this.colors.scale; + } + return opts; + } + + updateLineWidth(width): void { + this.bandwidth = width; + this.scaleLines(); + } + + updateYAxisWidth({ width }: { width: number }): void { + this.yAxisWidth = width + 20; + this.update(); + } + + updateXAxisHeight({ height }): void { + this.xAxisHeight = height; + this.update(); + } + + onActivate(item) { + const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series); + if (idx > -1) { + return; + } + + this.activeEntries = [item, ...this.activeEntries]; + this.activate.emit({ value: item, entries: this.activeEntries }); + } + + onDeactivate(item) { + const idx = this.activeEntries.findIndex(d => d.name === item.name && d.value === item.value && d.series === item.series); + + this.activeEntries.splice(idx, 1); + this.activeEntries = [...this.activeEntries]; + + this.deactivate.emit({ value: item, entries: this.activeEntries }); + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts new file mode 100644 index 000000000..ee6c92cb5 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/combo-series-vertical.component.ts @@ -0,0 +1,199 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; +import { animate, style, transition, trigger } from '@angular/animations'; +import { Bar, BarOrientation, formatLabel, PlacementTypes, StyleTypes } from '@swimlane/ngx-charts'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'g[ngx-combo-charts-series-vertical]', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('animationState', [ + transition('* => void', [ + style({ + opacity: 1, + transform: '*', + }), + animate(500, style({ opacity: 0, transform: 'scale(0)' })), + ]), + ]), + ], +}) +export class ComboSeriesVerticalComponent implements OnChanges { + @Input() dims; + @Input() type = 'standard'; + @Input() series; + @Input() seriesLine; + @Input() xScale; + @Input() yScale; + @Input() colors; + @Input() tooltipDisabled = false; + @Input() gradient: boolean; + @Input() activeEntries: any[]; + @Input() seriesName: string; + @Input() animations = true; + @Input() noBarWhenZero = true; + + @Output() activate = new EventEmitter(); + @Output() deactivate = new EventEmitter(); + @Output() bandwidth = new EventEmitter(); + + bars: any; + x: any; + y: any; + readonly tooltipTypes = StyleTypes; + readonly tooltipPlacements = PlacementTypes; + readonly orientations = BarOrientation; + + ngOnChanges(): void { + this.update(); + } + + update(): void { + let width; + if (this.series.length) { + width = this.xScale.bandwidth(); + this.bandwidth.emit(width); + } + + let d0 = 0; + let total; + if (this.type === 'normalized') { + total = this.series.map(d => d.value).reduce((sum: number, d: number) => sum + d, 0); + } + + this.bars = this.series.map((d, index) => { + let value: number = d.value; + const label = d.name; + const formattedLabel = formatLabel(label); + const roundEdges = this.type === 'standard'; + + const bar: Bar = { + value, + label, + roundEdges, + data: d, + width, + formattedLabel, + height: 0, + x: 0, + y: 0, + ariaLabel: label, + tooltipText: label, + color: undefined, + gradientStops: undefined, + }; + + let offset0 = d0; + let offset1 = offset0 + value; + + if (this.type === 'standard') { + bar.height = Math.abs(this.yScale(value) - this.yScale(0)); + bar.x = this.xScale(label); + + if (value < 0) { + bar.y = this.yScale(0); + } else { + bar.y = this.yScale(value); + } + } else if (this.type === 'stacked') { + d0 += value; + + bar.height = this.yScale(offset0) - this.yScale(offset1); + bar.x = 0; + bar.y = this.yScale(offset1); + // bar.offset0 = offset0; + // bar.offset1 = offset1; + } else if (this.type === 'normalized') { + d0 += value; + + if (total > 0) { + offset0 = (offset0 * 100) / total; + offset1 = (offset1 * 100) / total; + } else { + offset0 = 0; + offset1 = 0; + } + + bar.height = this.yScale(offset0) - this.yScale(offset1); + bar.x = 0; + bar.y = this.yScale(offset1); + // bar.offset0 = offset0; + // bar.offset1 = offset1; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + value = (offset1 - offset0).toFixed(2) + '%'; + } + + if (this.colors.scaleType === 'ordinal') { + bar.color = this.colors.getColor(label); + } else { + if (this.type === 'standard') { + bar.color = this.colors.getColor(value); + bar.gradientStops = this.colors.getLinearGradientStops(value); + } else { + bar.color = this.colors.getColor(offset1); + bar.gradientStops = this.colors.getLinearGradientStops(offset1, offset0); + } + } + + let tooltipLabel = formattedLabel; + if (this.seriesName) { + tooltipLabel = `${this.seriesName} • ${formattedLabel}`; + } + + this.getSeriesTooltips(this.seriesLine, index); + const lineValue: string = this.seriesLine[0].series[index].value; + bar.tooltipText = ` + ${tooltipLabel} + + Y1 - ${value.toLocaleString()} • Y2 - ${lineValue.toLocaleString()}% + + `; + + return bar; + }); + } + + getSeriesTooltips(seriesLine, index) { + return seriesLine.map(d => d.series[index]); + } + + isActive(entry): boolean { + if (!this.activeEntries) { + return false; + } + const item = this.activeEntries.find(d => entry.name === d.name && entry.series === d.series); + return item !== undefined; + } + + trackBy(index, bar): string { + return bar.label; + } +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts new file mode 100644 index 000000000..2a0ab4f22 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/index.ts @@ -0,0 +1,2 @@ +export * from './combo-chart.component'; +export * from './combo-series-vertical.component'; diff --git a/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/models.ts b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/models.ts new file mode 100644 index 000000000..7358f8e55 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/combo-chart/models.ts @@ -0,0 +1,11 @@ +export interface ISeries { + name: number; + value: number; + min: number; + max: number; +} + +export interface ILineChartSeries { + name: string; + series: ISeries[]; +} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/google-chart/google-chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/google-chart/google-chart.component.ts deleted file mode 100644 index 5aa4f2d24..000000000 --- a/apps/red-ui/src/app/modules/admin/screens/license/google-chart/google-chart.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { ChartType, Column, Row } from 'angular-google-charts'; -import { TranslateService } from '@ngx-translate/core'; -import ComboChartOptions = google.visualization.ComboChartOptions; - -@Component({ - selector: 'redaction-google-chart', - template: ` `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class GoogleChartComponent { - @Input() data: Row[]; - - readonly options: ComboChartOptions; - readonly columns: Column[]; - readonly type = ChartType.ComboChart; - - constructor(translateService: TranslateService) { - const pagesPerMonth = translateService.instant('license-info-screen.chart.pages-per-month'); - const totalPages = translateService.instant('license-info-screen.chart.total-pages'); - const cumulative = translateService.instant('license-info-screen.chart.cumulative'); - - this.options = { - fontName: 'Inter', - fontSize: 13, - vAxis: { title: pagesPerMonth }, - seriesType: 'bars', - vAxes: { 1: { title: totalPages } }, - series: { 1: { type: 'line', targetAxisIndex: 1 }, 2: { type: 'line', targetAxisIndex: 1 } }, - colors: ['#0389ec', '#dd4d50', '#5ce594'], - legend: { position: 'top' }, - }; - - this.columns = ['abc', pagesPerMonth, totalPages, cumulative]; - } -} diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html new file mode 100644 index 000000000..8da1b9488 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.html @@ -0,0 +1,18 @@ + diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts index fcf188256..0cafbb004 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-chart/license-chart.component.ts @@ -1,24 +1,33 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { LICENSE_STORAGE_KEY } from '../utils/constants'; +import { Component } from '@angular/core'; +import { ComboBarScheme, LICENSE_STORAGE_KEY, LineChartScheme } from '../utils/constants'; import dayjs from 'dayjs'; 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'; -import { Row } from 'angular-google-charts'; +import { TranslateService } from '@ngx-translate/core'; +import { ILineChartSeries } from '../combo-chart/models'; +import { Observable } from 'rxjs'; @Component({ selector: 'redaction-license-chart', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './license-chart.component.html', }) export class LicenseChartComponent { - readonly chartData$ = this.#chartData$; + readonly lineChartScheme = LineChartScheme; + readonly comboBarScheme = ComboBarScheme; - constructor(private readonly _licenseService: LicenseService, private readonly _loadingService: LoadingService) {} + lineChartSeries$ = this.#licenseChartSeries$; + barChart = []; - get #chartData$() { + constructor( + private readonly _translateService: TranslateService, + private readonly _licenseService: LicenseService, + private readonly _loadingService: LoadingService, + ) {} + + get #licenseChartSeries$(): Observable { return this._licenseService.selectedLicense$.pipe( tap(() => this._loadingService.start()), switchMap(license => this.#getLicenseData(license)), @@ -26,28 +35,59 @@ export class LicenseChartComponent { ); } - async #getLicenseData(license: ILicense): Promise { + async #getLicenseData(license: ILicense): Promise { const startDate = dayjs(license.validFrom); const endDate = dayjs(license.validUntil); const startMonth: number = startDate.month(); const startYear: number = startDate.year(); - const dateRanges = generateDateRanges(startMonth, startYear, endDate.month() as number, endDate.year() as number); + const dateRanges = generateDateRanges(startMonth, startYear, endDate.month(), endDate.year()); const reports = await this.#getReports(dateRanges, license.id); - return this.#mapRangesToReports(dateRanges, reports); + return this.#mapRangesToReports(startMonth, startYear, dateRanges, reports); } - #mapRangesToReports(dateRanges: List, reports: List): Row[] { - let cumulativePages = 0; - const processingPages = this._licenseService.processingPages; + #mapRangesToReports(month: number, year: number, dateRanges: List, reports: List): ILineChartSeries[] { + return [ + { + name: this._translateService.instant('license-info-screen.chart.total-pages'), + series: this.#totalLicensedPagesSeries(dateRanges), + }, + { + name: this._translateService.instant('license-info-screen.chart.cumulative'), + series: this.#setBar(month, year, reports), + }, + ]; + } - return dateRanges.map((range, index) => [ - verboseDate(range), - reports[index].numberOfAnalyzedPages, - processingPages, - (cumulativePages += reports[index].numberOfAnalyzedPages), - ]); + #setBar(month: number, year: number, reports: List) { + let cumulativePages = 0; + const cumulativePagesSeries = []; + this.barChart = []; + const monthNames = dayjs.monthsShort(); + + for (const report of reports) { + cumulativePages += report.numberOfAnalyzedPages; + + const name = `${monthNames[month]} ${year}`; + this.barChart.push({ + name, + value: report.numberOfAnalyzedPages, + }); + + cumulativePagesSeries.push({ + name, + value: cumulativePages, + }); + + month++; + if (month === 12) { + month = 0; + year++; + } + } + + return cumulativePagesSeries; } #getReports(dateRanges: List, id: string) { @@ -78,4 +118,11 @@ export class LicenseChartComponent { return report; } + + #totalLicensedPagesSeries(dateRanges: List) { + return dateRanges.map(dateRange => ({ + name: verboseDate(dateRange), + value: this._licenseService.processingPages, + })); + } } diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html index 046e5e489..860edd4d3 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.html @@ -50,8 +50,8 @@
- {{ (selectedLicense.validFrom | date: 'dd-MM-YYYY') || '-' }} / - {{ (selectedLicense.validUntil | date: 'dd-MM-YYYY') || '-' }} + {{ (selectedLicense.validFrom | date: 'dd-MM-yyyy') || '-' }} / + {{ (selectedLicense.validUntil | date: 'dd-MM-yyyy') || '-' }}
diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts index 68ab95b66..128915d7e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/license-screen/license-screen.component.ts @@ -76,7 +76,7 @@ export class LicenseScreenComponent implements OnInit { const licenseCustomer = this.licenseService.selectedLicense.licensedTo; const subject = this._translateService.instant('license-info-screen.email.title', { licenseCustomer, - }); + }) as string; const lineBreak = '%0D%0A'; const body = [ this._translateService.instant('license-info-screen.email.body.analyzed', { diff --git a/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts b/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts index 998a40851..d1dcd8fe9 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/license.module.ts @@ -7,9 +7,9 @@ import { RouterModule, Routes } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { MatSelectModule } from '@angular/material/select'; import { IqserListingModule } from '@iqser/common-ui'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; +import { ComboChartComponent, ComboSeriesVerticalComponent } from './combo-chart'; import { FormsModule } from '@angular/forms'; -import { GoogleChartComponent } from './google-chart/google-chart.component'; -import { GoogleChartsModule } from 'angular-google-charts'; import { CommonModule } from '@angular/common'; const routes: Routes = [ @@ -20,15 +20,21 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [LicenseScreenComponent, LicenseSelectComponent, LicenseChartComponent, GoogleChartComponent], + declarations: [ + LicenseScreenComponent, + LicenseSelectComponent, + LicenseChartComponent, + ComboChartComponent, + ComboSeriesVerticalComponent, + ], imports: [ - CommonModule, RouterModule.forChild(routes), + CommonModule, TranslateModule, MatSelectModule, FormsModule, + NgxChartsModule, IqserListingModule, - GoogleChartsModule, ], providers: [LicenseService], }) diff --git a/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts b/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts index aaffc8d71..a139c9732 100644 --- a/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts +++ b/apps/red-ui/src/app/modules/admin/screens/license/utils/constants.ts @@ -1 +1,17 @@ +import { Color, ScaleType } from '@swimlane/ngx-charts'; + +export const ComboBarScheme: Color = { + name: 'Combo bar scheme', + selectable: true, + group: ScaleType.Ordinal, + domain: ['#0389ec'], +}; + +export const LineChartScheme: Color = { + name: 'Line chart scheme', + selectable: true, + group: ScaleType.Ordinal, + domain: ['#dd4d50', '#5ce594', '#0389ec'], +}; + export const LICENSE_STORAGE_KEY = 'redaction-license-reports'; diff --git a/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.scss index e2b5ad16b..f73496ca2 100644 --- a/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/reports/reports-screen/reports-screen.component.scss @@ -1,9 +1,7 @@ @use 'common-mixins'; :host { - flex-grow: 1; - overflow: hidden; - display: flex; + flex-direction: row; } .content-container, diff --git a/apps/red-ui/src/app/modules/archive/archive.module.ts b/apps/red-ui/src/app/modules/archive/archive.module.ts index 72a80db27..5eb8741ac 100644 --- a/apps/red-ui/src/app/modules/archive/archive.module.ts +++ b/apps/red-ui/src/app/modules/archive/archive.module.ts @@ -5,13 +5,14 @@ import { ArchiveRoutingModule } from './archive-routing.module'; import { TableItemComponent } from './components/table-item/table-item.component'; import { SharedModule } from '@shared/shared.module'; import { ConfigService } from './services/config.service'; +import { SharedDossiersModule } from '../shared-dossiers/shared-dossiers.module'; const components = [TableItemComponent]; const screens = [ArchivedDossiersScreenComponent]; @NgModule({ declarations: [...components, ...screens], - imports: [CommonModule, ArchiveRoutingModule, SharedModule], + imports: [CommonModule, ArchiveRoutingModule, SharedModule, SharedDossiersModule], providers: [ConfigService], }) export class ArchiveModule {} diff --git a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.html b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.html index 47c99ac14..d94dbc95f 100644 --- a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.html +++ b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.html @@ -10,4 +10,16 @@
+ +
+ +
diff --git a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts index b978de713..2b9f28e4c 100644 --- a/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts +++ b/apps/red-ui/src/app/modules/archive/components/table-item/table-item.component.ts @@ -3,6 +3,9 @@ import { Dossier, DossierStats } from '@red/domain'; import { BehaviorSubject, Observable } from 'rxjs'; import { DossierStatsService } from '@services/dossiers/dossier-stats.service'; import { switchMap } from 'rxjs/operators'; +import { CircleButtonTypes, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui'; +import { UserService } from '@services/user.service'; +import { DossiersDialogService } from '../../../shared-dossiers/services/dossiers-dialog.service'; @Component({ selector: 'redaction-table-item [dossier]', @@ -11,18 +14,32 @@ import { switchMap } from 'rxjs/operators'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableItemComponent implements OnChanges { - @Input() dossier!: Dossier; + readonly circleButtonTypes = CircleButtonTypes; + readonly currentUser = this._userService.currentUser; + @Input() dossier!: Dossier; readonly stats$: Observable; readonly #ngOnChanges$ = new BehaviorSubject(undefined); - constructor(readonly dossierStatsService: DossierStatsService) { + constructor( + readonly dossierStatsService: DossierStatsService, + private readonly _dialogService: DossiersDialogService, + private readonly _userService: UserService, + ) { this.stats$ = this.#ngOnChanges$.pipe(switchMap(dossierId => this.dossierStatsService.watch$(dossierId))); } + get scrollableParentView(): ScrollableParentView { + return ScrollableParentViews.VIRTUAL_SCROLL; + } + ngOnChanges() { if (this.dossier) { this.#ngOnChanges$.next(this.dossier.id); } } + + openEditDossierDialog($event: MouseEvent, dossierId: string): void { + this._dialogService.openDialog('editDossier', $event, { dossierId }); + } } diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html index 500e7f372..259d1af20 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.html @@ -48,7 +48,11 @@
- +
diff --git a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts index 85f3bebd8..3539b3133 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/components/dossier-details/dossier-details.component.ts @@ -6,6 +6,7 @@ import { DossierAttributeWithValue, DossierStats, IDossierRequest, + ProcessingTypes, StatusSorter, User, } from '@red/domain'; @@ -85,27 +86,31 @@ export class DossierDetailsComponent { #calculateStatusConfig(stats: DossierStats): ProgressBarConfigModel[] { return [ { + id: ProcessingTypes.pending, label: _('processing-status.pending'), total: stats.numberOfFiles, count: stats.processingStats.pending, icon: 'red:reanalyse', }, { + id: ProcessingTypes.ocr, label: _('processing-status.ocr'), total: stats.numberOfFiles, count: stats.processingStats.ocr, icon: 'iqser:ocr', }, { + id: ProcessingTypes.processing, label: _('processing-status.processing'), total: stats.numberOfFiles, count: stats.processingStats.processing, icon: 'red:reanalyse', }, { + id: ProcessingTypes.processed, label: _('processing-status.processed'), total: stats.numberOfFiles, - count: stats.processingStats.proccesed, + count: stats.processingStats.processed, icon: 'red:ready-for-approval', }, ].filter(config => config.count > 0); diff --git a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts index 7ea4cf40a..832b3e914 100644 --- a/apps/red-ui/src/app/modules/dossier-overview/config.service.ts +++ b/apps/red-ui/src/app/modules/dossier-overview/config.service.ts @@ -11,7 +11,7 @@ import { TableColumnConfig, WorkflowConfig, } from '@iqser/common-ui'; -import { Dossier, File, IFileAttributeConfig, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; +import { Dossier, File, IFileAttributeConfig, ProcessingType, StatusSorter, WorkflowFileStatus, WorkflowFileStatuses } from '@red/domain'; import { workflowFileStatusTranslations } from '@translations/file-status-translations'; import { PermissionsService } from '@services/permissions.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; @@ -170,6 +170,7 @@ export class ConfigService { const allDistinctPeople = new Set(); const allDistinctAddedDates = new Set(); const allDistinctNeedsWork = new Set(); + const allDistinctProcessingTypes = new Set(); const dynamicFilters = new Map>(); @@ -205,6 +206,8 @@ export class ConfigService { allDistinctNeedsWork.add('comment'); } + allDistinctProcessingTypes.add(file.processingType); + // extract values for dynamic filters fileAttributeConfigs.forEach(config => { if (config.filterable) { @@ -287,6 +290,14 @@ export class ConfigService { matchAll: true, }); + const processingTypesFilters = [...allDistinctProcessingTypes].map(item => new NestedFilter({ id: item, label: item })); + filterGroups.push({ + slug: 'processingTypeFilters', + filters: processingTypesFilters, + checker: (file: File, filter: INestedFilter) => file.processingType === filter.id, + hide: true, + }); + dynamicFilters.forEach((filterValue: Set, filterKey: string) => { const id = filterKey.split(':')[0]; const key = filterKey.split(':')[1]; @@ -334,9 +345,7 @@ export class ConfigService { } _recentlyModifiedChecker = (file: File) => - dayjs(file.lastUpdated) - .add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS as number, 'hours') - .isAfter(dayjs()); + dayjs(file.lastUpdated).add(this._appConfigService.values.RECENT_PERIOD_IN_HOURS, 'hours').isAfter(dayjs()); _assignedToMeChecker = (file: File) => file.assignee === this._userService.currentUser.id; diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 5124d2521..cf8b02fd6 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -27,7 +27,7 @@ import { File, ViewMode, ViewModes } from '@red/domain'; import { PermissionsService } from '@services/permissions.service'; import { combineLatest, firstValueFrom, from, of, pairwise } from 'rxjs'; import { UserPreferenceService } from '@services/user-preference.service'; -import { byId, byPage, download, handleFilterDelta } from '../../utils'; +import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils'; import { FilesService } from '@services/files/files.service'; import { FileManagementService } from '@services/files/file-management.service'; import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators'; @@ -363,14 +363,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni }), ); - const currentPageAnnotations$ = combineLatest([this.pdf.currentPage$, this._fileDataService.annotations$]).pipe( - map(([, annotations]) => annotations), - startWith([] as List), + const annotations$ = this._fileDataService.annotations$.pipe( + startWith([] as AnnotationWrapper[]), pairwise(), - map(([oldAnnotations, newAnnotations]) => { - const page = this.pdf.currentPage; - return [oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const; - }), + tap(annotations => this.deleteAnnotations(...annotations)), + ); + const currentPageAnnotations$ = combineLatest([this.pdf.currentPage$, annotations$]).pipe( + map( + ([page, [oldAnnotations, newAnnotations]]) => + [oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const, + ), ); let start; @@ -378,7 +380,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni filter(([, loaded]) => loaded), tap(() => (start = new Date().getTime())), map(([annotations]) => annotations), - tap(annotations => this.deleteAnnotations(...annotations)), switchMap(annotations => this.drawChangedAnnotations(...annotations)), tap(([, newAnnotations]) => this.#highlightSelectedAnnotations(newAnnotations)), tap(() => this._logger.info(`[ANNOTATIONS] Processing time: ${new Date().getTime() - start}`)), @@ -387,25 +388,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { - const annotationsToDelete = oldAnnotations.filter(oldAnnotation => !newAnnotations.some(byId(oldAnnotation.id))); + const annotationsToDelete = oldAnnotations.filter(oldAnnotation => { + const newAnnotation = newAnnotations.find(byId(oldAnnotation.id)); + return newAnnotation ? hasChanges(oldAnnotation, newAnnotation) : true; + }); - if (annotationsToDelete.length === 0) { - return; - } - - const toDelete = annotationsToDelete.filter(byPage(this.pdf.currentPage)); - - this._logger.info('[ANNOTATIONS] To delete: ', toDelete); - this._annotationManager.delete(toDelete); + this._logger.info('[ANNOTATIONS] To delete: ', annotationsToDelete); + this._annotationManager.delete(annotationsToDelete); } async drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { const annotationsToDraw = this.#getAnnotationsToDraw(oldAnnotations, newAnnotations); - - if (annotationsToDraw.length === 0) { - return [oldAnnotations, newAnnotations]; - } - this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw); this._annotationManager.delete(annotationsToDraw); await this._cleanupAndRedrawAnnotations(annotationsToDraw); @@ -422,15 +415,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } #getAnnotationsToDraw(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { - const annotations = this._annotationManager.annotations; - const ann = annotations.map(a => oldAnnotations.some(byId(a.Id))); - const hasAnnotations = ann.filter(a => !!a).length > 0; + const currentPage = this.pdf.currentPage; + const currentPageAnnotations = this._annotationManager.get(a => a.getPageNumber() === currentPage); + const existingAnnotations = currentPageAnnotations + .map(a => oldAnnotations.find(byId(a.Id)) || newAnnotations.find(byId(a.Id))) + .filter(a => !!a); - if (hasAnnotations) { - return this.#findAnnotationsToDraw(newAnnotations, oldAnnotations); - } else { - return newAnnotations; + if (existingAnnotations.length > 0) { + return this.#findAnnotationsToDraw(newAnnotations, oldAnnotations, existingAnnotations); } + return newAnnotations; } #rebuildFilters() { @@ -467,19 +461,26 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._changeDetectorRef.markForCheck(); } - #findAnnotationsToDraw(newAnnotations: AnnotationWrapper[], oldAnnotations: AnnotationWrapper[]) { + #findAnnotationsToDraw( + newAnnotations: AnnotationWrapper[], + oldAnnotations: AnnotationWrapper[], + existingAnnotations: AnnotationWrapper[], + ) { + function selectToDrawIfDoesNotExist(newAnnotation: AnnotationWrapper) { + return !existingAnnotations.some(byId(newAnnotation.id)); + } + return newAnnotations.filter(newAnnotation => { - const oldAnnotation = oldAnnotations.find(annotation => annotation.id === newAnnotation.id); - if (!oldAnnotation) { - return true; + const oldAnnotation = oldAnnotations.find(byId(newAnnotation.id)); + if (!oldAnnotation || !hasChanges(oldAnnotation, newAnnotation)) { + return selectToDrawIfDoesNotExist(newAnnotation); } - const changed = JSON.stringify(oldAnnotation) !== JSON.stringify(newAnnotation); - if (changed && this.userPreferenceService.areDevFeaturesEnabled) { + if (this.userPreferenceService.areDevFeaturesEnabled) { this.#logDiff(oldAnnotation, newAnnotation); } - return changed; + return true; }); } @@ -582,7 +583,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._errorService.set(error); } - private async _cleanupAndRedrawAnnotations(newAnnotations: readonly AnnotationWrapper[]) { + private async _cleanupAndRedrawAnnotations(newAnnotations: List) { + if (!newAnnotations.length) { + return; + } const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || []; this.#rebuildFilters(); diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 2a0f691f4..0e2e842ee 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -370,10 +370,7 @@ export class AnnotationActionsService { type: 'actionButton', img: this._convertPath('/assets/icons/general/close.svg'), title: this._translateService.instant('annotation-actions.reject-suggestion'), - onClick: () => - this._ngZone.run(() => { - this.rejectSuggestion(null, annotations); - }), + onClick: () => this._ngZone.run(() => this.rejectSuggestion(null, annotations)), }); } @@ -420,7 +417,7 @@ export class AnnotationActionsService { async acceptResize($event: MouseEvent, annotation: AnnotationWrapper): Promise { const fileId = this._state.fileId; const textAndPositions = await this._extractTextAndPositions(annotation.id); - const text = annotation.value === 'Rectangle' ? 'Rectangle' : annotation.isImage ? 'Image' : textAndPositions.text; + const text = annotation.rectangle ? annotation.value : annotation.isImage ? 'Image' : textAndPositions.text; const data = { annotation, text }; this._dialogService.openDialog('resizeAnnotation', $event, data, (result: { comment: string; updateDictionary: boolean }) => { const resizeRequest: IResizeRequest = { @@ -572,17 +569,18 @@ export class AnnotationActionsService { text: words.join(' '), positions: rectangles, }; - } else { - const rect = toPosition( - viewerAnnotation.getPageNumber(), - this._documentViewer.getHeight(viewerAnnotation.getPageNumber()), - this._annotationDrawService.annotationToQuads(viewerAnnotation), - ); - return { - positions: [rect], - text: null, - }; } + + const position = toPosition( + viewerAnnotation.getPageNumber(), + this._documentViewer.getHeight(viewerAnnotation.getPageNumber()), + this._annotationDrawService.annotationToQuads(viewerAnnotation), + ); + + return { + positions: [position], + text: null, + }; } private async _extractTextFromRect(page: Core.PDFNet.Page, rect: Core.PDFNet.Rect) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts index 46ac67e14..033cc3591 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts @@ -67,8 +67,8 @@ export class ManualRedactionService extends GenericService { const recommendations: List = annotations.map(annotation => ({ addToDictionary: true, sourceId: annotation.annotationId, - reason: 'False Positive', value: annotation.value, + reason: annotation.legalBasis, positions: annotation.positions, type: annotation.recommendationType, comment, diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index 8857208d6..437200340 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -7,7 +7,6 @@ import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper, } from '../../../models/file/manual-redaction-entry.wrapper'; -import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service'; import { AnnotationActionsService } from './annotation-actions.service'; import { UserPreferenceService } from '../../../services/user-preference.service'; @@ -38,6 +37,7 @@ export class PdfProxyService { readonly manualAnnotationRequested$ = new Subject(); readonly pageChanged$ = this._pdf.pageChanged$.pipe( tap(() => this._handleCustomActions()), + tap(() => this._pdf.resetAnnotationActions()), shareDistinctLast(), ); canPerformActions = true; @@ -107,20 +107,26 @@ export class PdfProxyService { #deactivateMultiSelect() { this._multiSelectService.deactivate(); this._annotationManager.deselect(); - console.log('deactivated multi select'); this.handleAnnotationSelected([]); } #processSelectedAnnotations(annotations: Annotation[], action) { - console.log('processSelectedAnnotations', annotations, action); let nextAnnotations: Annotation[]; if (action === 'deselected') { // Remove deselected annotations from selected list nextAnnotations = this._annotationManager.selected.filter(ann => !annotations.some(a => a.Id === ann.Id)); this._pdf.disable(TextPopups.ADD_RECTANGLE); + const currentPage = this._pdf.currentPage; + if (nextAnnotations.some(a => a.getPageNumber() === currentPage)) { + this.#configureAnnotationSpecificActions(nextAnnotations); + } else { + this._pdf.resetAnnotationActions(); + } return nextAnnotations.map(ann => ann.Id); - } else if (!this._multiSelectService.isEnabled) { + } + + if (!this._multiSelectService.isEnabled) { // Only choose the last selected annotation, to bypass viewer multi select nextAnnotations = annotations; const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id)); @@ -130,7 +136,7 @@ export class PdfProxyService { nextAnnotations = this._annotationManager.selected; } - this.#configureAnnotationSpecificActions(annotations); + this.#configureAnnotationSpecificActions(nextAnnotations); if (!(annotations.length === 1 && annotations[0].ReadOnly)) { this._pdf.enable(TextPopups.ADD_RECTANGLE); @@ -149,18 +155,14 @@ export class PdfProxyService { #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { if (!this.canPerformActions) { - if (this.instance.UI.annotationPopup.getItems().length) { - this.instance.UI.annotationPopup.update([]); - } - return; + return this._pdf.resetAnnotationActions(); } - const annotationWrappers: AnnotationWrapper[] = viewerAnnotations.map(va => this._fileDataService.find(va.Id)).filter(va => !!va); - this.instance.UI.annotationPopup.update([]); + const annotationWrappers = viewerAnnotations.map(va => this._fileDataService.find(va.Id)).filter(va => !!va); + this._pdf.resetAnnotationActions(); if (annotationWrappers.length === 0) { - this._configureRectangleAnnotationPopup(viewerAnnotations[0]); - return; + return this._configureRectangleAnnotationPopup(viewerAnnotations[0]); } // Add hide action as last item diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts index 2934f5f4c..7cd273b14 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts @@ -42,6 +42,9 @@ export class REDAnnotationManager { delete(annotations?: List | List | string | AnnotationWrapper) { const items = isStringOrWrapper(annotations) ? [this.get(annotations)] : this.get(annotations); + if (!items.length) { + return; + } const options: DeleteAnnotationsOptions = { force: true }; this.#manager.deleteAnnotations(items, options); } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts index b021c4f8e..8f4e2d850 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/pdf-viewer.service.ts @@ -104,6 +104,12 @@ export class PdfViewer { return page$.pipe(map(page => this.#adjustPage(page))); } + resetAnnotationActions() { + if (this.#instance.UI.annotationPopup.getItems().length) { + this.#instance.UI.annotationPopup.update([]); + } + } + navigateTo(page: string | number) { const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page; const paginationOffset = this.#paginationOffset; diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts index 582eb9528..7672f211e 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts @@ -183,7 +183,7 @@ export class ViewerHeaderService { } updateElements(): void { - this._pdf.instance.UI.setHeaderItems(header => { + this._pdf.instance?.UI.setHeaderItems(header => { const enabledItems: IHeaderElement[] = []; const groups: HeaderElementType[][] = [ [HeaderElements.COMPARE_BUTTON, HeaderElements.CLOSE_COMPARE_BUTTON], diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index 3fe6a9d90..de8bee50e 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -113,7 +113,7 @@ export class FileActionsComponent implements OnChanges { } private get _toggleTooltip(): string { - if (!this.currentUser.isManager) { + if (!this.canToggleAnalysis) { return _('file-preview.toggle-analysis.only-managers'); } @@ -405,7 +405,6 @@ export class FileActionsComponent implements OnChanges { this.assignTooltip = this.file.isUnderApproval ? _('dossier-overview.assign-approver') : _('dossier-overview.assign-reviewer'); this.buttonType = this.isFilePreview ? CircleButtonTypes.default : CircleButtonTypes.dark; - this.toggleTooltip = this._toggleTooltip; this.showSetToNew = this._permissionsService.canSetToNew(this.file, this.dossier) && !this.isDossierOverviewWorkflow; this.showUndoApproval = this._permissionsService.canUndoApproval(this.file, this.dossier) && !this.isDossierOverviewWorkflow; @@ -415,6 +414,8 @@ export class FileActionsComponent implements OnChanges { this.canToggleAnalysis = this._permissionsService.canToggleAnalysis(this.file, this.dossier); this.showToggleAnalysis = this._permissionsService.showToggleAnalysis(this.dossier); + this.toggleTooltip = this._toggleTooltip; + this.showDelete = this._permissionsService.canSoftDeleteFile(this.file, this.dossier); this.showOCR = this._permissionsService.canOcrFile(this.file, this.dossier); this.canReanalyse = this._permissionsService.canReanalyseFile(this.file, this.dossier); diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts index aded170fb..865099cd7 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/download-package/edit-dossier-download-package.component.ts @@ -70,7 +70,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS } get disabled() { - return !this.form?.value?.downloadFileTypes?.length; + return false; } get valid(): boolean { diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts index 620b88105..443f074bc 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component.ts @@ -84,7 +84,7 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti this.states.length === 1 ? 'edit-dossier-dialog.general-info.form.dossier-state.no-state-placeholder' : 'edit-dossier-dialog.general-info.form.dossier-state.placeholder', - ); + ) as string; } ngOnInit() { @@ -165,7 +165,6 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti if (result === ConfirmOptions.CONFIRM) { this._loadingService.start(); await firstValueFrom(this._archivedDossiersService.archive([this.dossier])); - await this._router.navigate([this.dossier.dossiersListRouterLink]); this._toaster.success(_('dossier-listing.archive.archive-succeeded'), { params: this.dossier }); this._editDossierDialogRef.close(); this._loadingService.stop(); @@ -174,10 +173,8 @@ export class EditDossierGeneralInfoComponent implements OnInit, EditDossierSecti } getStateName(stateId: string): string { - return ( - this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId)?.name || - this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder') - ); + return (this._dossierStatesMapService.get(this.dossier.dossierTemplateId, stateId)?.name || + this._translateService.instant('edit-dossier-dialog.general-info.form.dossier-state.placeholder')) as string; } getStateColor(stateId: string): string { diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts index 1c9a9bca7..a951b9321 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.ts @@ -62,7 +62,7 @@ export class DictionaryManagerComponent implements OnChanges { return this._dossierTemplate; } - set dossierTemplate(value) { + set dossierTemplate(value: DossierTemplate) { this._dossierTemplate = value; this.dictionaries = this._dictionaries; this._compareDictionary = this.selectDictionary; diff --git a/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html index ee5f38690..c92940f16 100644 --- a/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dossiers-type-switch/dossiers-type-switch.component.html @@ -1,10 +1,5 @@ - diff --git a/apps/red-ui/src/app/services/config.service.ts b/apps/red-ui/src/app/services/config.service.ts index ff1381bbd..ffb9ccae1 100644 --- a/apps/red-ui/src/app/services/config.service.ts +++ b/apps/red-ui/src/app/services/config.service.ts @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Title } from '@angular/platform-browser'; import packageInfo from '../../../../../package.json'; import envConfig from '../../assets/config/config.json'; -import { CacheApiService, wipeAllCaches, wipeCaches } from '@red/cache'; +import { CacheApiService, wipeAllCaches } from '@red/cache'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { AppConfig } from '@red/domain'; diff --git a/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts b/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts index 75c248f89..e5b57103a 100644 --- a/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts +++ b/apps/red-ui/src/app/services/dossier-templates/dashboard-stats.service.ts @@ -11,9 +11,8 @@ const templatesSorter = (a: DashboardStats, b: DashboardStats) => { return -1; } else if (a.isEmpty && !b.isEmpty) { return 1; - } else { - return 0; } + return 0; }; @Injectable({ diff --git a/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts index bd85ab539..b3cc9ab59 100644 --- a/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/active-dossiers.service.ts @@ -1,35 +1,12 @@ import { Injectable, Injector } from '@angular/core'; -import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; import { DossiersService } from './dossiers.service'; -import { Observable, timer } from 'rxjs'; -import { switchMap, tap } from 'rxjs/operators'; -import { Dossier, DOSSIERS_ROUTE } from '@red/domain'; +import { DOSSIERS_ROUTE } from '@red/domain'; @Injectable({ providedIn: 'root', }) export class ActiveDossiersService extends DossiersService { - private _initializedRefresh = false; - constructor(protected readonly _injector: Injector) { super(_injector, 'dossier', DOSSIERS_ROUTE); } - - initializeRefresh() { - if (this._initializedRefresh) { - return; - } - this._initializedRefresh = true; - timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) - .pipe( - switchMap(() => this.loadOnlyChanged()), - tap(changes => this._emitFileChanges(changes)), - ) - .subscribe(); - } - - loadAll(): Observable { - this.initializeRefresh(); - return super.loadAll(); - } } diff --git a/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts index 9a572140b..733f47d70 100644 --- a/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/archived-dossiers.service.ts @@ -1,12 +1,14 @@ import { Injectable, Injector } from '@angular/core'; -import { ARCHIVE_ROUTE, Dossier, DOSSIERS_ARCHIVE } from '@red/domain'; -import { catchError, tap } from 'rxjs/operators'; +import { ARCHIVE_ROUTE, Dossier, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE } from '@red/domain'; +import { catchError, switchMap, tap } from 'rxjs/operators'; import { Observable, of } from 'rxjs'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ActiveDossiersService } from './active-dossiers.service'; import { DossiersService } from './dossiers.service'; import { FilesMapService } from '../files/files-map.service'; import { FeaturesService } from '../features.service'; +import { DashboardStatsService } from '@services/dossier-templates/dashboard-stats.service'; +import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class ArchivedDossiersService extends DossiersService { @@ -15,6 +17,8 @@ export class ArchivedDossiersService extends DossiersService { private readonly _activeDossiersService: ActiveDossiersService, private readonly _filesMapService: FilesMapService, private readonly _featuresService: FeaturesService, + private readonly _dashboardStats: DashboardStatsService, + private readonly _router: Router, ) { super(_injector, 'archived-dossiers', ARCHIVE_ROUTE); } @@ -26,9 +30,18 @@ export class ArchivedDossiersService extends DossiersService { }; const archivedDossiersIds = dossiers.map(d => d.dossierId); + const dossierTemplateId = dossiers[0].dossierTemplateId; return this._post(archivedDossiersIds, `${this._defaultModelPath}/archive`).pipe( + switchMap(() => this._dashboardStats.loadAll()), tap(() => this.#removeFromActiveDossiers(archivedDossiersIds)), + switchMap(async () => { + let route = dossiers[0].dossiersListRouterLink; + if (!this._activeDossiersService.all.find(d => d.dossierTemplateId === dossierTemplateId)) { + route = route.replace(DOSSIERS_ROUTE, ARCHIVE_ROUTE); + } + await this._router.navigate([route]); + }), catchError(showArchiveFailedToast), ); } diff --git a/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts b/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts new file mode 100644 index 000000000..bdaab839d --- /dev/null +++ b/apps/red-ui/src/app/services/dossiers/dossier-changes.service.ts @@ -0,0 +1,88 @@ +import { GenericService, List, QueryParam } from '@iqser/common-ui'; +import { Dossier, DossierStats, IDossierChanges } from '@red/domain'; +import { forkJoin, Observable, of, throwError, timer } from 'rxjs'; +import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators'; +import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { NGXLogger } from 'ngx-logger'; +import { ActiveDossiersService } from './active-dossiers.service'; +import { ArchivedDossiersService } from './archived-dossiers.service'; +import { Injectable, Injector } from '@angular/core'; +import { DossierStatsService } from './dossier-stats.service'; +import { DashboardStatsService } from '../dossier-templates/dashboard-stats.service'; +import { CHANGED_CHECK_INTERVAL } from '@utils/constants'; + +@Injectable({ providedIn: 'root' }) +export class DossiersChangesService extends GenericService { + private _initializedRefresh = false; + + private readonly _activeDossiersService: ActiveDossiersService = this._injector.get(ActiveDossiersService); + private readonly _archivedDossiersService: ArchivedDossiersService = this._injector.get(ArchivedDossiersService); + + protected constructor( + protected readonly _injector: Injector, + private readonly _dossierStatsService: DossierStatsService, + private readonly _dashboardStatsService: DashboardStatsService, + private readonly _logger: NGXLogger, + ) { + super(_injector, 'dossier'); + } + + loadOnlyChanged(): Observable { + const removeIfNotFound = (id: string) => + catchError((error: HttpErrorResponse) => { + if (error.status === HttpStatusCode.NotFound) { + this._activeDossiersService.remove(id); + this._archivedDossiersService.remove(id); + return of([]); + } + return throwError(() => error); + }); + + const load = (changes: IDossierChanges) => + changes.map(change => this._load(change.dossierId).pipe(removeIfNotFound(change.dossierId))); + + return this.hasChangesDetails$().pipe( + tap(changes => this._logger.info('[CHANGES] ', changes)), + switchMap(dossierChanges => + forkJoin([...load(dossierChanges), this._dashboardStatsService.loadAll().pipe(take(1))]).pipe(map(() => dossierChanges)), + ), + tap(() => this._updateLastChanged()), + ); + } + + hasChangesDetails$(): Observable { + const body = { value: this._lastCheckedForChanges.get('root') ?? '0' }; + return this._post(body, `${this._defaultModelPath}/changes/details`).pipe(filter(changes => changes.length > 0)); + } + + initializeRefresh() { + if (this._initializedRefresh) { + return; + } + this._initializedRefresh = true; + timer(CHANGED_CHECK_INTERVAL, CHANGED_CHECK_INTERVAL) + .pipe( + switchMap(() => this.loadOnlyChanged()), + tap(changes => { + this._activeDossiersService.emitFileChanges(changes); + this._archivedDossiersService.emitFileChanges(changes); + }), + ) + .subscribe(); + } + + private _load(id: string): Observable { + const queryParams: List = [{ key: 'includeArchived', value: true }]; + return super._getOne([id], this._defaultModelPath, queryParams).pipe( + map(entity => new Dossier(entity)), + switchMap((dossier: Dossier) => { + if (dossier.isArchived) { + this._activeDossiersService.remove(dossier.id); + return this._archivedDossiersService.updateDossier(dossier); + } + this._archivedDossiersService.remove(dossier.id); + return this._activeDossiersService.updateDossier(dossier); + }), + ); + } +} diff --git a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts index e7d2809ab..5bb5fcbf6 100644 --- a/apps/red-ui/src/app/services/dossiers/dossiers.service.ts +++ b/apps/red-ui/src/app/services/dossiers/dossiers.service.ts @@ -1,7 +1,7 @@ -import { EntitiesService, List, mapEach, QueryParam, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; +import { EntitiesService, mapEach, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; import { Dossier, DossierStats, IDossier, IDossierChanges, IDossierRequest } from '@red/domain'; -import { forkJoin, Observable, of, Subject, throwError } from 'rxjs'; -import { catchError, filter, map, switchMap, tap } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { Injector } from '@angular/core'; import { DossierStatsService } from './dossier-stats.service'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; @@ -36,26 +36,6 @@ export abstract class DossiersService extends EntitiesService ); } - loadOnlyChanged(): Observable { - const removeIfNotFound = (id: string) => - catchError((error: HttpErrorResponse) => { - if (error.status === HttpStatusCode.NotFound) { - this.remove(id); - return of([]); - } - return throwError(() => error); - }); - - const load = (changes: IDossierChanges) => - changes.map(change => this._load(change.dossierId).pipe(removeIfNotFound(change.dossierId))); - - return this.hasChangesDetails$().pipe( - tap(changes => this._logger.info('[CHANGES] ', changes)), - switchMap(dossierChanges => forkJoin(load(dossierChanges)).pipe(map(() => dossierChanges))), - tap(() => this._updateLastChanged()), - ); - } - loadAll(): Observable { const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id); return this.getAll().pipe( @@ -67,21 +47,12 @@ export abstract class DossiersService extends EntitiesService ); } - hasChangesDetails$(): Observable { - const body = { value: this._lastCheckedForChanges.get('root') ?? '0' }; - return this._post(body, `${this._defaultModelPath}/changes/details`).pipe(filter(changes => changes.length > 0)); + updateDossier(dossier: Dossier): Observable { + this.replace(dossier); + return this._dossierStatsService.getFor([dossier.id]); } - protected _emitFileChanges(dossierChanges: IDossierChanges): void { + emitFileChanges(dossierChanges: IDossierChanges): void { dossierChanges.filter(change => change.fileChanges).forEach(change => this.dossierFileChanges$.next(change.dossierId)); } - - private _load(id: string): Observable { - const queryParams: List = [{ key: 'includeArchived', value: this._path === 'archived-dossiers' }]; - return super._getOne([id], this._defaultModelPath, queryParams).pipe( - map(entity => new Dossier(entity)), - tap(dossier => this.replace(dossier)), - switchMap(dossier => this._dossierStatsService.getFor([dossier.dossierId])), - ); - } } diff --git a/apps/red-ui/src/app/services/entity-services/trash.service.ts b/apps/red-ui/src/app/services/entity-services/trash.service.ts index 8fbf43c5f..94931ee73 100644 --- a/apps/red-ui/src/app/services/entity-services/trash.service.ts +++ b/apps/red-ui/src/app/services/entity-services/trash.service.ts @@ -11,6 +11,7 @@ import { flatMap } from 'lodash-es'; import { DossierStatsService } from '../dossiers/dossier-stats.service'; import { FilesService } from '../files/files.service'; import { SystemPreferencesService } from '@services/system-preferences.service'; +import { ArchivedDossiersService } from '@services/dossiers/archived-dossiers.service'; @Injectable({ providedIn: 'root', @@ -22,6 +23,7 @@ export class TrashService extends EntitiesService { private readonly _systemPreferencesService: SystemPreferencesService, private readonly _permissionsService: PermissionsService, private readonly _activeDossiersService: ActiveDossiersService, + private readonly _archivedDossiersService: ArchivedDossiersService, private readonly _userService: UserService, private readonly _dossierStatsService: DossierStatsService, private readonly _filesService: FilesService, @@ -34,8 +36,11 @@ export class TrashService extends EntitiesService { this._toaster.error(_('dossier-listing.delete.delete-failed'), { params: dossier }); return of({}); }; + + const reloadDossiers$ = dossier.isActive ? this._activeDossiersService.loadAll() : this._archivedDossiersService.loadAll(); + return this.delete(dossier.id, 'dossier').pipe( - switchMap(() => this._activeDossiersService.loadAll()), + switchMap(() => reloadDossiers$), catchError(showToast), ); } diff --git a/apps/red-ui/src/app/services/permissions.service.ts b/apps/red-ui/src/app/services/permissions.service.ts index f0d138521..fceea66d7 100644 --- a/apps/red-ui/src/app/services/permissions.service.ts +++ b/apps/red-ui/src/app/services/permissions.service.ts @@ -299,12 +299,12 @@ export class PermissionsService { /** UNDER_REVIEW => NEW */ private _canSetToNew(file: File, dossier: Dossier): boolean { - return dossier.isActive && file.isUnderReview && this.isDossierMember(dossier); + return dossier.isActive && file.isUnderReview && this.isAssigneeOrApprover(file, dossier); } /** UNDER_REVIEW => UNDER_APPROVAL */ private _canSetUnderApproval(file: File, dossier: Dossier): boolean { - return dossier.isActive && file.isUnderReview && this.isDossierMember(dossier); + return dossier.isActive && file.isUnderReview && this.isAssigneeOrApprover(file, dossier); } /** UNDER_APPROVAL => UNDER_REVIEW OR NEW => UNDER_REVIEW */ diff --git a/apps/red-ui/src/app/translations/placeholders-descriptions-translations.ts b/apps/red-ui/src/app/translations/placeholders-descriptions-translations.ts index 366aaa667..876acfb02 100644 --- a/apps/red-ui/src/app/translations/placeholders-descriptions-translations.ts +++ b/apps/red-ui/src/app/translations/placeholders-descriptions-translations.ts @@ -18,4 +18,8 @@ export const generalPlaceholdersDescriptionsTranslations = { 'date.MM/dd/yyyy': _('reports-screen.descriptions.general.date.m-d-y'), 'time.HH:mm': _('reports-screen.descriptions.general.time.h-m'), 'dossier.name': _('reports-screen.descriptions.general.dossier.name'), + 'redaction.value': _('reports-screen.descriptions.general.redaction.value'), + 'redaction.justificationLegalBasis': _('reports-screen.descriptions.general.redaction.justification-legal-basis'), + 'redaction.justificationText': _('reports-screen.descriptions.general.redaction.justification-text'), + 'redaction.entity.displayName': _('reports-screen.descriptions.general.redaction.entity.display-name'), } as const; diff --git a/apps/red-ui/src/app/utils/configuration.initializer.ts b/apps/red-ui/src/app/utils/configuration.initializer.ts index c50088029..d39a23ebd 100644 --- a/apps/red-ui/src/app/utils/configuration.initializer.ts +++ b/apps/red-ui/src/app/utils/configuration.initializer.ts @@ -53,7 +53,7 @@ export function configurationInitializer( }), switchMap(() => languageService.chooseAndSetInitialLanguage()), tap(() => userService.initialize()), - tap(() => firstValueFrom(licenseService.loadLicense())), + switchMap(() => licenseService.loadLicense()), take(1), ); return () => firstValueFrom(setup); diff --git a/apps/red-ui/src/app/utils/functions.ts b/apps/red-ui/src/app/utils/functions.ts index e7b7f1651..918227208 100644 --- a/apps/red-ui/src/app/utils/functions.ts +++ b/apps/red-ui/src/app/utils/functions.ts @@ -96,3 +96,7 @@ export function getLast(list: List) { } export const dateWithoutTime = (date: Dayjs) => date.set('h', 0).set('m', 0).set('s', 0).set('ms', 0); + +export function hasChanges(left: T, right: T) { + return JSON.stringify(left) !== JSON.stringify(right); +} diff --git a/apps/red-ui/src/assets/config/config.json b/apps/red-ui/src/assets/config/config.json index 72d288759..0c71adeeb 100644 --- a/apps/red-ui/src/assets/config/config.json +++ b/apps/red-ui/src/assets/config/config.json @@ -7,11 +7,6 @@ "BACKEND_APP_VERSION": "4.4.40", "EULA_URL": "EULA_URL", "FRONTEND_APP_VERSION": "1.1", - "LICENSE_CUSTOMER": "Development License", - "LICENSE_EMAIL": "todo-license@email.com", - "LICENSE_END": "31-12-2022", - "LICENSE_PAGE_COUNT": 10000, - "LICENSE_START": "01-01-2022", "MAX_FILE_SIZE_MB": 100, "MAX_RETRIES_ON_SERVER_ERROR": 3, "OAUTH_CLIENT_ID": "redaction", diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index cd49f2b30..fdeafd33b 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -1565,6 +1565,7 @@ "backend-version": "Backend-Version der Anwendung", "chart": { "cumulative": "Seiten insgesamt", + "legend": "", "pages-per-month": "Seiten pro Monat", "total-pages": "Gesamtzahl der Seiten" }, @@ -1821,12 +1822,18 @@ "name": "Dieser Platzhalter wird durch den Dateinamen ersetzt." }, "redaction": { + "entity": { + "display-name": "" + }, "excerpt": "Dieser Platzhalter wird durch einen Textausschnitt ersetzt, der die Schwärzung enthält.", "justification": "Dieser Platzhalter wird durch die Begründung der Schwärzung ersetzt. Es ist eine Kombination aus dem Rechtsverweis (justificationParagraph) und dem Begründungstext (justificationReason).", + "justification-legal-basis": "", "justification-paragraph": "Dieser Platzhalter wird durch den Rechtshinweis der Begründung der Redaktion ersetzt.", "justification-reason": "Dieser Platzhalter wird durch den Begründungstext der Schwärzung ersetzt.", + "justification-text": "", "page": "Dieser Platzhalter wird durch die Seitenzahl der Redaktion ersetzt.", - "paragraph": "Dieser Platzhalter wird durch den Absatz ersetzt, der die Schwärzung enthält." + "paragraph": "Dieser Platzhalter wird durch den Absatz ersetzt, der die Schwärzung enthält.", + "value": "" }, "time": { "h-m": "Dieser Platzhalter wird durch den Zeitpunkt ersetzt, zu dem der Bericht erstellt wurde." diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 1e56baa82..8d247ac06 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -343,7 +343,7 @@ "redaction": "Redaction", "skipped": "Skipped", "suggestion-add": "Suggested redaction", - "suggestion-add-dictionary": "Suggested dictionary add", + "suggestion-add-dictionary": "Suggested add to Dictionary", "suggestion-add-false-positive": "Suggested add to false positive", "suggestion-change-legal-basis": "Suggested change legal basis", "suggestion-force-hint": "Suggestion force hint", @@ -1458,9 +1458,9 @@ "subtitle": "SMTP (Simple Mail Transfer Protocol) enables you to send your emails through the specified server settings.", "system-preferences": { "labels": { - "download-cleanup-download-files-hours": "Deletion time (hours) for download packages that have been generated and downloaded", - "download-cleanup-not-download-files-hours": "Deletion time (hours) for download packages that have been generated but not yet downloaded", - "soft-delete-cleanup-time": "Deletion time (hours) for deleted files in Trash" + "download-cleanup-download-files-hours": "Delete downloaded packages automatically after X hours", + "download-cleanup-not-download-files-hours": "Keep the generated download package for X hours", + "soft-delete-cleanup-time": "Keep deleted files for X hours in trash" }, "placeholders": { "download-cleanup-download-files-hours": "(hours)", @@ -1565,6 +1565,7 @@ "backend-version": "Backend Application Version", "chart": { "cumulative": "Cumulative Pages", + "legend": "Legend", "pages-per-month": "Pages per Month", "total-pages": "Total Pages" }, @@ -1821,12 +1822,18 @@ "name": "This placeholder is replaced by the file name." }, "redaction": { + "entity": { + "display-name": "This placeholder is replaced by the name of the entity the redaction is based on." + }, "excerpt": "This placeholder is replaced by a text snippet that contains the redaction.", "justification": "This placeholder is replaced by the justification of the redaction. It is a combination of the legal reference (justificationParagraph) and the justification text (justificationReason).", + "justification-legal-basis": "This placeholder is replaced by the legal basis for the redaction.", "justification-paragraph": "This placeholder is replaced by the legal reference of the justification of the redaction.", "justification-reason": "This placeholder is replaced by the justification text of the redaction.", + "justification-text": "This placeholder is replaced by the justification text.", "page": "This placeholder is replaced by the page number of the redaction.", - "paragraph": "This placeholder is replaced by the paragraph that contains the redaction." + "paragraph": "This placeholder is replaced by the paragraph that contains the redaction.", + "value": "This placeholder is replaced by the value that was redacted." }, "time": { "h-m": "This placeholder is replaced by the time the report was created." @@ -1944,8 +1951,8 @@ "top-bar": { "navigation-items": { "back": "Back", - "back-to-dashboard": "Back to Dashboard", - "dashboard": "Dashboard", + "back-to-dashboard": "Back to Home", + "dashboard": "Home", "my-account": { "children": { "account": "Account", diff --git a/docker/red-ui/docker-entrypoint.sh b/docker/red-ui/docker-entrypoint.sh index 90063fae3..54e57d714 100755 --- a/docker/red-ui/docker-entrypoint.sh +++ b/docker/red-ui/docker-entrypoint.sh @@ -9,12 +9,6 @@ BACKEND_APP_VERSION="${BACKEND_APP_VERSION:-4.7.0}" EULA_URL="${EULA_URL:-}" FRONTEND_APP_VERSION="${FRONTEND_APP_VERSION:-}" -LICENSE_CUSTOMER="${LICENSE_CUSTOMER:-Developement License}" -LICENSE_EMAIL="${LICENSE_EMAIL:-license@iqser.com}" -LICENSE_END="${LICENSE_END:-31-12-2022}" -LICENSE_PAGE_COUNT="${LICENSE_PAGE_COUNT:-1000000}" -LICENSE_START="${LICENSE_START:-01-01-2021}" - MAX_FILE_SIZE_MB="${MAX_FILE_SIZE_MB:-50}" MAX_RETRIES_ON_SERVER_ERROR="${MAX_RETRIES_ON_SERVER_ERROR:-3}" OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID:-gin-client}" @@ -35,11 +29,6 @@ echo '{ "BACKEND_APP_VERSION":"'"$BACKEND_APP_VERSION"'", "EULA_URL":"'"$EULA_URL:"'", "FRONTEND_APP_VERSION":"'"$FRONTEND_APP_VERSION:"'", - "LICENSE_EMAIL":"'"$LICENSE_EMAIL"'", - "LICENSE_CUSTOMER":"'"$LICENSE_CUSTOMER"'", - "LICENSE_END":"'"$LICENSE_END"'", - "LICENSE_PAGE_COUNT":'"$LICENSE_PAGE_COUNT"', - "LICENSE_START":"'"$LICENSE_START"'", "MAX_FILE_SIZE_MB":"'"$MAX_FILE_SIZE_MB"'", "MAX_RETRIES_ON_SERVER_ERROR":"'"$MAX_RETRIES_ON_SERVER_ERROR"'", "OAUTH_CLIENT_ID":"'"$OAUTH_CLIENT_ID"'", diff --git a/libs/common-ui b/libs/common-ui index f1934abc2..f9e248833 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit f1934abc2b259a6e89303e95fe70a54471d2401d +Subproject commit f9e24883381ddbf93df5074ec1c176973db44ed1 diff --git a/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts index 1aa89a44e..f6821b4c4 100644 --- a/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts +++ b/libs/red-domain/src/lib/dossier-stats/dossier-stats.model.ts @@ -1,34 +1,17 @@ import { IDossierStats } from './dossier-stats'; import { FileCountPerProcessingStatus, FileCountPerWorkflowStatus } from './types'; -import { isProcessingStatuses, ProcessingFileStatus, ProcessingFileStatuses } from '../files'; +import { isProcessingStatuses, OCR_STATES, PENDING_STATES, PROCESSED_STATES, PROCESSING_STATES, ProcessingFileStatus } from '../files'; -const PENDING_STATES: ProcessingFileStatus[] = [ - ProcessingFileStatuses.ANALYSE, - ProcessingFileStatuses.ERROR, - ProcessingFileStatuses.FULLREPROCESS, - ProcessingFileStatuses.REPROCESS, - ProcessingFileStatuses.UNPROCESSED, -]; +export const ProcessingTypes = { + pending: 'pending', + ocr: 'ocr', + processing: 'processing', + processed: 'processed', +} as const; -const PROCESSING_STATES: ProcessingFileStatus[] = [ - ProcessingFileStatuses.IMAGE_ANALYZING, - ProcessingFileStatuses.INDEXING, - ProcessingFileStatuses.NER_ANALYZING, - ProcessingFileStatuses.PROCESSING, - ProcessingFileStatuses.SURROUNDING_TEXT_PROCESSING, - ProcessingFileStatuses.FULL_PROCESSING, -]; +export type ProcessingType = keyof typeof ProcessingTypes; -const PROCESSED_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.PROCESSED]; - -const OCR_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.OCR_PROCESSING]; - -interface ProcessingStats { - pending: number; - ocr: number; - processing: number; - proccesed: number; -} +export type ProcessingStats = Record; export class DossierStats implements IDossierStats { readonly dossierId: string; @@ -71,7 +54,7 @@ export class DossierStats implements IDossierStats { get #processingStats(): ProcessingStats { return { pending: this.#getTotal(PENDING_STATES), - proccesed: this.#getTotal(PROCESSED_STATES), + processed: this.#getTotal(PROCESSED_STATES), processing: this.#getTotal(PROCESSING_STATES), ocr: this.#getTotal(OCR_STATES), }; diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts index 30d02bb63..54f4ad208 100644 --- a/libs/red-domain/src/lib/files/file.model.ts +++ b/libs/red-domain/src/lib/files/file.model.ts @@ -3,6 +3,9 @@ import { StatusSorter } from '../shared'; import { isFullProcessingStatuses, isProcessingStatuses, + OCR_STATES, + PENDING_STATES, + PROCESSING_STATES, ProcessingFileStatus, ProcessingFileStatuses, WorkflowFileStatus, @@ -11,6 +14,7 @@ import { import { IFile } from './file'; import { FileAttributes } from '../file-attributes'; import { ARCHIVE_ROUTE, DOSSIERS_ROUTE } from '../dossiers'; +import { ProcessingType, ProcessingTypes } from '../dossier-stats'; export class File extends Entity implements IFile { readonly added?: string; @@ -73,6 +77,8 @@ export class File extends Entity implements IFile { readonly canBeOpened: boolean; readonly canBeOCRed: boolean; + readonly processingType: ProcessingType; + constructor(file: IFile, readonly reviewerName: string) { super(file); this.added = file.added; @@ -138,6 +144,8 @@ export class File extends Entity implements IFile { this.fileAttributes = file.fileAttributes && file.fileAttributes.attributeIdToValue ? file.fileAttributes : { attributeIdToValue: {} }; + + this.processingType = this.#processingType; } get deleted(): boolean { @@ -157,6 +165,19 @@ export class File extends Entity implements IFile { return this.canBeOpened ? `/main/${this.dossierTemplateId}/${routerPath}/${this.dossierId}/file/${this.fileId}` : undefined; } + get #processingType(): ProcessingType { + if (PENDING_STATES.includes(this.processingStatus)) { + return ProcessingTypes.pending; + } + if (PROCESSING_STATES.includes(this.processingStatus)) { + return ProcessingTypes.processing; + } + if (OCR_STATES.includes(this.processingStatus)) { + return ProcessingTypes.ocr; + } + return ProcessingTypes.processed; + } + isPageExcluded(page: number): boolean { return this.excludedPages.includes(page); } diff --git a/libs/red-domain/src/lib/files/types.ts b/libs/red-domain/src/lib/files/types.ts index 11b758caa..ab6879e37 100644 --- a/libs/red-domain/src/lib/files/types.ts +++ b/libs/red-domain/src/lib/files/types.ts @@ -66,3 +66,24 @@ export interface StatusBarConfig { } export type StatusBarConfigs = List; + +export const PENDING_STATES: ProcessingFileStatus[] = [ + ProcessingFileStatuses.ANALYSE, + ProcessingFileStatuses.ERROR, + ProcessingFileStatuses.FULLREPROCESS, + ProcessingFileStatuses.REPROCESS, + ProcessingFileStatuses.UNPROCESSED, +]; + +export const PROCESSING_STATES: ProcessingFileStatus[] = [ + ProcessingFileStatuses.IMAGE_ANALYZING, + ProcessingFileStatuses.INDEXING, + ProcessingFileStatuses.NER_ANALYZING, + ProcessingFileStatuses.PROCESSING, + ProcessingFileStatuses.SURROUNDING_TEXT_PROCESSING, + ProcessingFileStatuses.FULL_PROCESSING, +]; + +export const PROCESSED_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.PROCESSED]; + +export const OCR_STATES: ProcessingFileStatus[] = [ProcessingFileStatuses.OCR_PROCESSING]; diff --git a/libs/red-domain/src/lib/shared/app-config.ts b/libs/red-domain/src/lib/shared/app-config.ts index 32b81a77d..ba047ec67 100644 --- a/libs/red-domain/src/lib/shared/app-config.ts +++ b/libs/red-domain/src/lib/shared/app-config.ts @@ -7,11 +7,6 @@ export interface AppConfig { BACKEND_APP_VERSION: string; EULA_URL: string; FRONTEND_APP_VERSION: string; - LICENSE_CUSTOMER: string; - LICENSE_EMAIL: string; - LICENSE_END: string; - LICENSE_PAGE_COUNT: number; - LICENSE_START: string; MAX_FILE_SIZE_MB: number; MAX_RETRIES_ON_SERVER_ERROR: number; OAUTH_CLIENT_ID: string; diff --git a/package.json b/package.json index 56f21f348..08b5253a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redaction", - "version": "3.546.0", + "version": "3.570.0", "private": true, "license": "MIT", "scripts": { @@ -40,8 +40,8 @@ "@ngx-translate/http-loader": "^7.0.0", "@nrwl/angular": "14.3.2", "@pdftron/webviewer": "8.6.0", + "@swimlane/ngx-charts": "^20.0.1", "@tabuckner/material-dayjs-adapter": "2.0.0", - "angular-google-charts": "^2.2.2", "dayjs": "^1.11.3", "file-saver": "^2.0.5", "jwt-decode": "^3.1.2", diff --git a/paligo-theme.tar.gz b/paligo-theme.tar.gz index 174aed440..89a9974b1 100644 Binary files a/paligo-theme.tar.gz and b/paligo-theme.tar.gz differ diff --git a/yarn.lock b/yarn.lock index d8c6d861e..ffd095625 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2526,6 +2526,25 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@swimlane/ngx-charts@^20.0.1": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@swimlane/ngx-charts/-/ngx-charts-20.1.0.tgz#c1377adacc835fa35ed0c6cb32a8ec5b43ccfd69" + integrity sha512-PY/X+eW+ZEvF3N1kuUVV5H3NHoFXlIWOvNnCKAs874yye//ttgfL/Qf9haHQpki5WIHQtpwn8xM1ylVEQT98bg== + dependencies: + "@types/d3-shape" "^2.0.0" + d3-array "^2.9.1" + d3-brush "^2.1.0" + d3-color "^2.0.0" + d3-format "^2.0.0" + d3-hierarchy "^2.0.0" + d3-interpolate "^2.0.1" + d3-scale "^3.2.3" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-time-format "^3.0.0" + d3-transition "^2.0.0" + tslib "^2.0.0" + "@tabuckner/material-dayjs-adapter@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@tabuckner/material-dayjs-adapter/-/material-dayjs-adapter-2.0.0.tgz#e79207232363fca391820c7992f7ed97576d7199" @@ -2624,6 +2643,18 @@ dependencies: "@types/node" "*" +"@types/d3-path@^2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-2.0.2.tgz#6052f38f6186319769dfabab61b5514b0e02c75c" + integrity sha512-3YHpvDw9LzONaJzejXLOwZ3LqwwkoXb9LI2YN7Hbd6pkGo5nIlJ09ul4bQhBN4hQZJKmUpX8HkVqbzgUKY48cg== + +"@types/d3-shape@^2.0.0": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" + integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== + dependencies: + "@types/d3-path" "^2" + "@types/eslint-scope@^3.7.3": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" @@ -2664,11 +2695,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/google.visualization@0.0.58": - version "0.0.58" - resolved "https://registry.yarnpkg.com/@types/google.visualization/-/google.visualization-0.0.58.tgz#eb2aa3cf05d63a9b42ff5cfcab1fc0594fb45a0e" - integrity sha512-ldwrhRvqSlrCYjHELGZNNMP8m5oPLBb6iU7CfKF2C/YpgRTlHihqsrL5M293u0GL7mKfjm0AjSIgEE2LU8fuTw== - "@types/google.visualization@^0.0.68": version "0.0.68" resolved "https://registry.yarnpkg.com/@types/google.visualization/-/google.visualization-0.0.68.tgz#773e908c02e08dffe689844f0972dd481516e704" @@ -3241,14 +3267,6 @@ ajv@^8.0.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -angular-google-charts@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/angular-google-charts/-/angular-google-charts-2.2.2.tgz#fd26a78a0c44f8bc686832116e7ced9751b46987" - integrity sha512-dbNDqhSfqxv1rMMph2xV55+nsXSREClm03bTRpq4nRjpVghC1hVwsqfeg6p/cngB7WgamQmWj5ExTwN6vBzVAg== - dependencies: - "@types/google.visualization" "0.0.58" - tslib "^2.2.0" - ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -4427,6 +4445,122 @@ cuint@^0.2.2: resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= +d3-array@2, d3-array@^2.3.0, d3-array@^2.9.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +d3-brush@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-2.1.0.tgz#adadfbb104e8937af142e9a6e2028326f0471065" + integrity sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ== + dependencies: + d3-dispatch "1 - 2" + d3-drag "2" + d3-interpolate "1 - 2" + d3-selection "2" + d3-transition "2" + +"d3-color@1 - 2", d3-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-dispatch@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" + integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== + +d3-drag@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" + integrity sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w== + dependencies: + d3-dispatch "1 - 2" + d3-selection "2" + +"d3-ease@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" + integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== + +"d3-format@1 - 2", d3-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +d3-hierarchy@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218" + integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw== + +"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + +d3-selection@2, d3-selection@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" + integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== + +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3", d3-time-format@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + +"d3-timer@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" + integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== + +d3-transition@2, d3-transition@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" + integrity sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog== + dependencies: + d3-color "1 - 2" + d3-dispatch "1 - 2" + d3-ease "1 - 2" + d3-interpolate "1 - 2" + d3-timer "1 - 2" + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -6351,6 +6485,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -10342,7 +10481,7 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@2.4.0, tslib@^2.2.0, tslib@^2.4.0: +tslib@2.4.0, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==