Merge branch 'master' into VM/RED-4247
This commit is contained in:
commit
1264751a7c
@ -21,3 +21,5 @@ ij_typescript_spaces_within_imports = true
|
||||
|
||||
[{*.json, .prettierrc, .eslintrc}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_json_array_wrapping = off
|
||||
|
||||
@ -4,3 +4,4 @@
|
||||
/coverage
|
||||
/node_modules
|
||||
/bamboo-specs
|
||||
*.md
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -415,13 +415,24 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
|
||||
}
|
||||
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<string, unknown> {
|
||||
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<string, unknown> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -17,8 +17,8 @@ export class SystemPreferencesFormComponent extends BaseFormComponent {
|
||||
readonly translations = systemPreferencesTranslations;
|
||||
readonly keys: { name: KeysOf<SystemPreferences>; type: ValueType }[] = [
|
||||
{ name: 'softDeleteCleanupTime', type: 'number' },
|
||||
{ name: 'downloadCleanupDownloadFilesHours', type: 'number' },
|
||||
{ name: 'downloadCleanupNotDownloadFilesHours', type: 'number' },
|
||||
{ name: 'downloadCleanupDownloadFilesHours', type: 'number' },
|
||||
];
|
||||
private _initialConfiguration: SystemPreferences;
|
||||
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
<ngx-charts-chart
|
||||
(legendLabelActivate)="onActivate($event)"
|
||||
(legendLabelClick)="onClick($event)"
|
||||
(legendLabelDeactivate)="onDeactivate($event)"
|
||||
[activeEntries]="activeEntries"
|
||||
[animations]="animations"
|
||||
[legendOptions]="legendOptions"
|
||||
[showLegend]="legend"
|
||||
[view]="[width + legendSpacing, height]"
|
||||
>
|
||||
<svg:g [attr.transform]="transform" class="bar-chart chart">
|
||||
<svg:g
|
||||
(dimensionsChanged)="updateXAxisHeight($event)"
|
||||
*ngIf="xAxis"
|
||||
[dims]="dims"
|
||||
[labelText]="xAxisLabel"
|
||||
[rotateTicks]="true"
|
||||
[showLabel]="showXAxisLabel"
|
||||
[tickFormatting]="xAxisTickFormatting"
|
||||
[xScale]="xScale"
|
||||
ngx-charts-x-axis
|
||||
></svg:g>
|
||||
<svg:g
|
||||
(dimensionsChanged)="updateYAxisWidth($event)"
|
||||
*ngIf="yAxis"
|
||||
[dims]="dims"
|
||||
[labelText]="yAxisLabel"
|
||||
[showGridLines]="showGridLines"
|
||||
[showLabel]="showYAxisLabel"
|
||||
[tickFormatting]="yAxisTickFormatting"
|
||||
[yOrient]="yOrientLeft"
|
||||
[yScale]="yScale"
|
||||
ngx-charts-y-axis
|
||||
></svg:g>
|
||||
<svg:g
|
||||
(dimensionsChanged)="updateYAxisWidth($event)"
|
||||
*ngIf="yAxis"
|
||||
[dims]="dims"
|
||||
[labelText]="yAxisLabelRight"
|
||||
[showGridLines]="showGridLines"
|
||||
[showLabel]="showRightYAxisLabel"
|
||||
[tickFormatting]="yRightAxisTickFormatting"
|
||||
[yOrient]="yOrientRight"
|
||||
[yScale]="yScaleLine"
|
||||
ngx-charts-y-axis
|
||||
></svg:g>
|
||||
<svg:g
|
||||
(activate)="onActivate($event)"
|
||||
(bandwidth)="updateLineWidth($event)"
|
||||
(deactivate)="onDeactivate($event)"
|
||||
[activeEntries]="activeEntries"
|
||||
[animations]="animations"
|
||||
[colors]="colors"
|
||||
[dims]="dims"
|
||||
[gradient]="gradient"
|
||||
[noBarWhenZero]="noBarWhenZero"
|
||||
[seriesLine]="lineChart"
|
||||
[series]="results"
|
||||
[tooltipDisabled]="true"
|
||||
[xScale]="xScale"
|
||||
[yScale]="yScale"
|
||||
ngx-combo-charts-series-vertical
|
||||
></svg:g>
|
||||
</svg:g>
|
||||
<svg:g [attr.transform]="transform" class="line-chart chart">
|
||||
<svg:g>
|
||||
<svg:g *ngFor="let series of lineChart; trackBy: trackBy">
|
||||
<svg:g
|
||||
[activeEntries]="activeEntries"
|
||||
[animations]="animations"
|
||||
[colors]="colorsLine"
|
||||
[curve]="curve"
|
||||
[data]="series"
|
||||
[rangeFillOpacity]="rangeFillOpacity"
|
||||
[scaleType]="scaleType"
|
||||
[xScale]="xScaleLine"
|
||||
[yScale]="yScaleLine"
|
||||
ngx-charts-line-series
|
||||
/>
|
||||
</svg:g>
|
||||
|
||||
<svg:g
|
||||
(hover)="updateHoveredVertical($event)"
|
||||
*ngIf="!tooltipDisabled"
|
||||
[colors]="colorsLine"
|
||||
[dims]="dims"
|
||||
[results]="combinedSeries"
|
||||
[tooltipDisabled]="tooltipDisabled"
|
||||
[xScale]="xScaleLine"
|
||||
[xSet]="xSet"
|
||||
[yScale]="yScaleLine"
|
||||
ngx-charts-tooltip-area
|
||||
/>
|
||||
|
||||
<svg:g *ngFor="let series of lineChart">
|
||||
<svg:g
|
||||
(activate)="onActivate($event)"
|
||||
(deactivate)="onDeactivate($event)"
|
||||
[activeEntries]="activeEntries"
|
||||
[colors]="colorsLine"
|
||||
[data]="series"
|
||||
[scaleType]="scaleType"
|
||||
[tooltipDisabled]="tooltipDisabled"
|
||||
[visibleValue]="hoveredVertical"
|
||||
[xScale]="xScaleLine"
|
||||
[yScale]="yScaleLine"
|
||||
ngx-charts-circle-series
|
||||
/>
|
||||
</svg:g>
|
||||
</svg:g>
|
||||
</svg:g>
|
||||
</ngx-charts-chart>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<unknown>;
|
||||
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<unknown>;
|
||||
@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 });
|
||||
}
|
||||
}
|
||||
@ -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: `
|
||||
<svg:g
|
||||
ngx-charts-bar
|
||||
*ngFor="let bar of bars; trackBy: trackBy"
|
||||
[@animationState]="'active'"
|
||||
[width]="bar.width"
|
||||
[height]="bar.height"
|
||||
[x]="bar.x"
|
||||
[y]="bar.y"
|
||||
[fill]="bar.color"
|
||||
[stops]="bar.gradientStops"
|
||||
[data]="bar.data"
|
||||
[orientation]="orientations.Vertical"
|
||||
[roundEdges]="bar.roundEdges"
|
||||
[gradient]="gradient"
|
||||
[isActive]="isActive(bar.data)"
|
||||
[animations]="animations"
|
||||
[noBarWhenZero]="noBarWhenZero"
|
||||
(activate)="activate.emit($event)"
|
||||
(deactivate)="deactivate.emit($event)"
|
||||
ngx-tooltip
|
||||
[tooltipDisabled]="tooltipDisabled"
|
||||
[tooltipPlacement]="tooltipPlacements.Top"
|
||||
[tooltipType]="tooltipTypes.tooltip"
|
||||
[tooltipTitle]="bar.tooltipText"
|
||||
></svg:g>
|
||||
`,
|
||||
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 = `
|
||||
<span class="tooltip-label">${tooltipLabel}</span>
|
||||
<span class="tooltip-val">
|
||||
Y1 - ${value.toLocaleString()} • Y2 - ${lineValue.toLocaleString()}%
|
||||
</span>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './combo-chart.component';
|
||||
export * from './combo-series-vertical.component';
|
||||
@ -0,0 +1,11 @@
|
||||
export interface ISeries {
|
||||
name: number;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface ILineChartSeries {
|
||||
name: string;
|
||||
series: ISeries[];
|
||||
}
|
||||
@ -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: ` <google-chart
|
||||
[columns]="columns"
|
||||
[data]="data"
|
||||
[height]="300"
|
||||
[options]="options"
|
||||
[type]="type"
|
||||
[width]="1000"
|
||||
></google-chart>`,
|
||||
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];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<combo-chart-component
|
||||
*ngIf="lineChartSeries$ | async as lineChartSeries"
|
||||
[animations]="true"
|
||||
[colorSchemeLine]="lineChartScheme"
|
||||
[legendTitle]="'license-info-screen.chart.legend' | translate"
|
||||
[legend]="true"
|
||||
[lineChart]="lineChartSeries"
|
||||
[results]="barChart"
|
||||
[scheme]="comboBarScheme"
|
||||
[showGridLines]="true"
|
||||
[showRightYAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
[view]="[1000, 300]"
|
||||
[xAxis]="true"
|
||||
[yAxisLabelRight]="'license-info-screen.chart.total-pages' | translate"
|
||||
[yAxisLabel]="'license-info-screen.chart.pages-per-month' | translate"
|
||||
[yAxis]="true"
|
||||
></combo-chart-component>
|
||||
@ -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: '<redaction-google-chart [data]="chartData$ | async"></redaction-google-chart>',
|
||||
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<ILineChartSeries[]> {
|
||||
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<Row[]> {
|
||||
async #getLicenseData(license: ILicense): Promise<ILineChartSeries[]> {
|
||||
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<IDateRange>, reports: List<ILicenseReport>): Row[] {
|
||||
let cumulativePages = 0;
|
||||
const processingPages = this._licenseService.processingPages;
|
||||
#mapRangesToReports(month: number, year: number, dateRanges: List<IDateRange>, reports: List<ILicenseReport>): 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<ILicenseReport>) {
|
||||
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<IDateRange>, id: string) {
|
||||
@ -78,4 +118,11 @@ export class LicenseChartComponent {
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
#totalLicensedPagesSeries(dateRanges: List<IDateRange>) {
|
||||
return dateRanges.map(dateRange => ({
|
||||
name: verboseDate(dateRange),
|
||||
value: this._licenseService.processingPages,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +50,8 @@
|
||||
<div class="row">
|
||||
<div translate="license-info-screen.licensing-period"></div>
|
||||
<div>
|
||||
{{ (selectedLicense.validFrom | date: 'dd-MM-YYYY') || '-' }} /
|
||||
{{ (selectedLicense.validUntil | date: 'dd-MM-YYYY') || '-' }}
|
||||
{{ (selectedLicense.validFrom | date: 'dd-MM-yyyy') || '-' }} /
|
||||
{{ (selectedLicense.validUntil | date: 'dd-MM-yyyy') || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
:host {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.content-container,
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -10,4 +10,16 @@
|
||||
|
||||
<div class="cell">
|
||||
<redaction-dossier-state [dossier]="dossier"></redaction-dossier-state>
|
||||
|
||||
<div class="action-buttons">
|
||||
<iqser-circle-button
|
||||
(action)="openEditDossierDialog($event, dossier.id)"
|
||||
*ngIf="currentUser.isUser"
|
||||
[icon]="currentUser.isManager ? 'iqser:edit' : 'red:info'"
|
||||
[scrollableParentView]="scrollableParentView"
|
||||
[tooltip]="(currentUser.isManager ? 'dossier-listing.edit.action' : 'dossier-listing.dossier-info.action') | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
iqserHelpMode="edit_dossier_dossier_info"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<DossierStats>;
|
||||
readonly #ngOnChanges$ = new BehaviorSubject<string>(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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,11 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="statusConfig$ | async as statusConfig" class="mt-24">
|
||||
<iqser-progress-bar *ngFor="let config of statusConfig" [config]="config"></iqser-progress-bar>
|
||||
<iqser-progress-bar
|
||||
*ngFor="let config of statusConfig"
|
||||
[config]="config"
|
||||
filterKey="processingTypeFilters"
|
||||
></iqser-progress-bar>
|
||||
</div>
|
||||
|
||||
<div *ngIf="stats.hasFiles && needsWorkFilters$ | async as filters" class="mt-32 legend pb-32" iqserHelpMode="dashboard_in_dossier">
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<string>();
|
||||
const allDistinctAddedDates = new Set<string>();
|
||||
const allDistinctNeedsWork = new Set<string>();
|
||||
const allDistinctProcessingTypes = new Set<ProcessingType>();
|
||||
|
||||
const dynamicFilters = new Map<string, Set<string>>();
|
||||
|
||||
@ -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<string>, 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;
|
||||
|
||||
|
||||
@ -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<AnnotationWrapper>),
|
||||
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<AnnotationWrapper>) {
|
||||
if (!newAnnotations.length) {
|
||||
return;
|
||||
}
|
||||
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
|
||||
this.#rebuildFilters();
|
||||
|
||||
|
||||
@ -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<void> {
|
||||
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) {
|
||||
|
||||
@ -67,8 +67,8 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
|
||||
const recommendations: List<IAddRedactionRequest> = annotations.map(annotation => ({
|
||||
addToDictionary: true,
|
||||
sourceId: annotation.annotationId,
|
||||
reason: 'False Positive',
|
||||
value: annotation.value,
|
||||
reason: annotation.legalBasis,
|
||||
positions: annotation.positions,
|
||||
type: annotation.recommendationType,
|
||||
comment,
|
||||
|
||||
@ -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<ManualRedactionEntryWrapper>();
|
||||
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
|
||||
|
||||
@ -42,6 +42,9 @@ export class REDAnnotationManager {
|
||||
|
||||
delete(annotations?: List | List<AnnotationWrapper> | 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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -70,7 +70,7 @@ export class EditDossierDownloadPackageComponent implements OnInit, EditDossierS
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
return !this.form?.value?.downloadFileTypes?.length;
|
||||
return false;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
<ng-container *ngIf="dossierTemplate$ | async as dossierTemplate">
|
||||
<button
|
||||
[disabled]="dossierTemplate.numberOfActiveDossiers === 0"
|
||||
[routerLinkActive]="'active'"
|
||||
[routerLink]="['..', DOSSIERS_ROUTE]"
|
||||
class="red-tab"
|
||||
>
|
||||
<button [routerLinkActive]="'active'" [routerLink]="['..', DOSSIERS_ROUTE]" class="red-tab">
|
||||
{{ 'dossiers-type-switch.active' | translate }}
|
||||
</button>
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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<Dossier[]> {
|
||||
this.initializeRefresh();
|
||||
return super.loadAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<Dossier> {
|
||||
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<IDossierChanges> {
|
||||
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<IDossierChanges> {
|
||||
const body = { value: this._lastCheckedForChanges.get('root') ?? '0' };
|
||||
return this._post<IDossierChanges>(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<DossierStats[]> {
|
||||
const queryParams: List<QueryParam> = [{ 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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<Dossier, IDossier>
|
||||
);
|
||||
}
|
||||
|
||||
loadOnlyChanged(): Observable<IDossierChanges> {
|
||||
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<Dossier[]> {
|
||||
const dossierIds = (dossiers: Dossier[]) => dossiers.map(d => d.id);
|
||||
return this.getAll().pipe(
|
||||
@ -67,21 +47,12 @@ export abstract class DossiersService extends EntitiesService<Dossier, IDossier>
|
||||
);
|
||||
}
|
||||
|
||||
hasChangesDetails$(): Observable<IDossierChanges> {
|
||||
const body = { value: this._lastCheckedForChanges.get('root') ?? '0' };
|
||||
return this._post<IDossierChanges>(body, `${this._defaultModelPath}/changes/details`).pipe(filter(changes => changes.length > 0));
|
||||
updateDossier(dossier: Dossier): Observable<DossierStats[]> {
|
||||
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<DossierStats[]> {
|
||||
const queryParams: List<QueryParam> = [{ 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])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TrashItem> {
|
||||
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<TrashItem> {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -96,3 +96,7 @@ export function getLast<T>(list: List<T>) {
|
||||
}
|
||||
|
||||
export const dateWithoutTime = (date: Dayjs) => date.set('h', 0).set('m', 0).set('s', 0).set('ms', 0);
|
||||
|
||||
export function hasChanges<T>(left: T, right: T) {
|
||||
return JSON.stringify(left) !== JSON.stringify(right);
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"'",
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit f1934abc2b259a6e89303e95fe70a54471d2401d
|
||||
Subproject commit f9e24883381ddbf93df5074ec1c176973db44ed1
|
||||
@ -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<ProcessingType, number>;
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
@ -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<IFile> implements IFile {
|
||||
readonly added?: string;
|
||||
@ -73,6 +77,8 @@ export class File extends Entity<IFile> 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<IFile> 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<IFile> 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);
|
||||
}
|
||||
|
||||
@ -66,3 +66,24 @@ export interface StatusBarConfig {
|
||||
}
|
||||
|
||||
export type StatusBarConfigs = List<StatusBarConfig>;
|
||||
|
||||
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];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
Binary file not shown.
167
yarn.lock
167
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==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user