Add get tables features

This commit is contained in:
Dan Percic 2023-08-02 14:11:44 +03:00
parent 40d0dbd6c0
commit a2c4d68b80
14 changed files with 179 additions and 33 deletions

View File

@ -150,7 +150,7 @@ export const appModuleFactory = (config: AppConfig) => {
enabled: true,
},
CHANGES: {
enabled: true,
enabled: false,
},
STATS: {
enabled: false,
@ -164,6 +164,9 @@ export const appModuleFactory = (config: AppConfig) => {
PAGES: {
enabled: false,
},
DOSSIERS_CHANGES: {
enabled: false,
},
},
} as ILoggerConfig,
},

View File

@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { UserService } from '@users/user.service';
import { ConfigService } from '@services/config.service';
import { getConfig } from '@iqser/common-ui';
import { AppConfig } from '@red/domain';
@Component({
selector: 'redaction-auth-error',
@ -8,8 +9,9 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./auth-error.component.scss'],
})
export class AuthErrorComponent {
adminName = this._configService.values.ADMIN_CONTACT_NAME;
adminUrl = this._configService.values.ADMIN_CONTACT_URL;
readonly #config = getConfig<AppConfig>();
readonly adminName = this.#config.ADMIN_CONTACT_NAME;
readonly adminUrl = this.#config.ADMIN_CONTACT_URL;
constructor(readonly userService: UserService, private readonly _configService: ConfigService) {}
constructor(readonly userService: UserService) {}
}

View File

@ -40,6 +40,14 @@
type="file-preview"
></redaction-file-actions>
<iqser-circle-button
(action)="getTables()"
*allow="roles.getTables"
[icon]="'red:csv'"
[tooltip]="'file-preview.get-tables' | translate"
class="ml-2"
></iqser-circle-button>
<iqser-circle-button
(action)="toggleFullScreen()"
[icon]="fullScreen ? 'red:exit-fullscreen' : 'red:fullscreen'"

View File

@ -12,6 +12,7 @@ import {
TemplateRef,
ViewChild,
} from '@angular/core';
import { saveAs } from 'file-saver';
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import {
CircleButtonTypes,
@ -75,6 +76,8 @@ import { TenantsService } from '@iqser/common-ui/lib/tenants';
import { AddHintDialogComponent } from './dialogs/add-hint-dialog/add-hint-dialog.component';
import { AddAnnotationDialogComponent } from './dialogs/docu-mine/add-annotation-dialog/add-annotation-dialog.component';
import { RedactTextData } from './utils/dialog-types';
import { TablesService } from './services/tables.service';
import JSZip from 'jszip';
const textActions = [TextPopups.REDACT_TEXT, TextPopups.ADD_HINT, TextPopups.ADD_FALSE_POSITIVE];
@ -93,13 +96,13 @@ export class FilePreviewScreenComponent
})
private readonly _filterTemplate: TemplateRef<unknown>;
@ViewChild('actionsWrapper', { static: false }) private readonly _actionsWrapper: ElementRef;
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
fullScreen = false;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly lastAssignee = computed(() => this.getLastAssignee());
readonly #isDocumine;
width: number;
constructor(
@ -143,6 +146,7 @@ export class FilePreviewScreenComponent
private readonly _suggestionsService: SuggestionsService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialog: MatDialog,
private readonly _tablesService: TablesService,
) {
super();
effect(() => {
@ -174,7 +178,6 @@ export class FilePreviewScreenComponent
this._stampService.stampPDF().then();
}
});
this.#isDocumine = getConfig().IS_DOCUMINE;
}
get changed() {
@ -369,29 +372,6 @@ export class FilePreviewScreenComponent
);
}
async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
const hint = manualRedactionEntryWrapper.type === ManualRedactionEntryTypes.HINT;
const data = this.#getRedactTextDialogData(manualRedactionEntryWrapper, file);
const result = await this.#getRedactTextDialog(hint, data).result();
if (!result) {
return;
}
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, {
hint,
dictionaryLabel: result.dictionary?.label,
});
const addAndReload$ = add$.pipe(
tap(() => this._documentViewer.clearSelection()),
switchMap(() => this._filesService.reload(this.dossierId, file)),
);
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
}
toggleFullScreen() {
this.fullScreen = !this.fullScreen;
if (this.fullScreen) {
@ -521,6 +501,47 @@ export class FilePreviewScreenComponent
return this.#cleanupAndRedrawAnnotations(annotationsToDraw);
}
async getTables() {
const currentPage = this.pdf.currentPage();
const tables = await this._tablesService.get(this.state.dossierId, this.state.fileId, this.pdf.currentPage());
await this._annotationDrawService.drawTables(tables, currentPage, this.state.dossierTemplateId);
const filename = this.state.file().filename;
const zip = new JSZip();
tables.forEach((t, index) => {
const blob = new Blob([atob(t.csvAsBytes)], {
type: 'text/csv;charset=utf-8',
});
zip.file(filename + '_page' + currentPage + '_table' + (index + 1) + '.csv', blob);
});
saveAs(await zip.generateAsync({ type: 'blob' }), filename + '_tables.zip');
}
async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
const hint = manualRedactionEntryWrapper.type === ManualRedactionEntryTypes.HINT;
const data = this.#getRedactTextDialogData(manualRedactionEntryWrapper, file);
const result = await this.#getRedactTextDialog(hint, data).result();
if (!result) {
return;
}
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, {
hint,
dictionaryLabel: result.dictionary?.label,
});
const addAndReload$ = add$.pipe(
tap(() => this._documentViewer.clearSelection()),
switchMap(() => this._filesService.reload(this.dossierId, file)),
);
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
}
@Debounce(30)
private _updateItemWidth(entry: ResizeObserverEntry): void {
this.width = entry.contentRect.width;

View File

@ -72,6 +72,7 @@ import { RemoveAnnotationDialogComponent } from './dialogs/docu-mine/remove-anno
import { ResizeAnnotationDialogComponent } from './dialogs/docu-mine/resize-annotation-dialog/resize-annotation-dialog.component';
import { EditAnnotationDialogComponent } from './dialogs/docu-mine/edit-annotation-dialog/edit-annotation-dialog.component';
import { EditRedactionDialogComponent } from './dialogs/edit-redaction-dialog/edit-redaction-dialog.component';
import { TablesService } from './services/tables.service';
const routes: IqserRoutes = [
{
@ -157,6 +158,6 @@ const components = [
TenantPipe,
LogPipe,
],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, SuggestionsService],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, SuggestionsService, TablesService],
})
export class FilePreviewModule {}

View File

@ -0,0 +1,46 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { log } from '@common-ui/utils';
export interface BoundingBox {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
}
interface CellPerRow {
readonly boundingBox: BoundingBox;
readonly value: string;
}
export interface Table {
readonly boundingBox: BoundingBox;
readonly cellsPerRow: CellPerRow[][];
readonly experimental: boolean;
readonly csvAsBytes: string;
}
interface Response {
readonly tables: Table[];
}
@Injectable()
export class TablesService {
readonly #http = inject(HttpClient);
readonly #serviceName = 'table-provider';
readonly #defaultUrl = 'tables';
get(dossierId: string, fileId: string, pageNumber: number) {
const url = `/${this.#serviceName}/${encodeURI(this.#defaultUrl)}/${dossierId}/${fileId}`;
const request$ = this.#http.post<Response>(url, { pageNumber, tableExtractionType: 'STABLE' });
return firstValueFrom(
request$.pipe(
map(response => response.tables),
log('TablesService.get'),
),
);
}
}

View File

@ -12,12 +12,24 @@ import { REDAnnotationManager } from './annotation-manager.service';
import { List } from '@iqser/common-ui/lib/utils';
import { REDDocumentViewer } from './document-viewer.service';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { BoundingBox, Table } from '../../file-preview/services/tables.service';
import Annotation = Core.Annotations.Annotation;
import Quad = Core.Math.Quad;
const DEFAULT_TEXT_ANNOTATION_OPACITY = 1;
const DEFAULT_REMOVED_ANNOTATION_OPACITY = 0.2;
export function getSectionRectangle(box: BoundingBox): ISectionRectangle {
return {
topLeft: {
x: box.x,
y: box.y,
},
width: box.width,
height: box.height,
};
}
@Injectable()
export class AnnotationDrawService {
constructor(
@ -57,6 +69,20 @@ export class AnnotationDrawService {
return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4);
}
async drawTables(tables: Table[], page: number, dossierTemplateId: string) {
const sections: Core.Annotations.RectangleAnnotation[] = [];
tables.forEach(table => {
sections.push(this._computeSection(page, getSectionRectangle(table.boundingBox), dossierTemplateId));
table.cellsPerRow
.flatMap(row => row)
.forEach(row => {
sections.push(this._computeSection(page, getSectionRectangle(row.boundingBox), dossierTemplateId));
});
});
await this._annotationManager.add(sections);
}
private async _draw(annotationWrappers: List<AnnotationWrapper>, hideSkipped: boolean, dossierTemplateId: string) {
const totalPages = this._pdf.totalPages();
const annotations = annotationWrappers

View File

@ -16,6 +16,7 @@ export const Roles = {
search: 'red-search',
searchAudit: 'red-search-audit-log',
manageAclPermissions: 'red-manage-acl-permissions',
getTables: 'red-get-tables',
rules: {
read: 'red-read-rules',
write: 'red-write-rules',

View File

@ -1437,6 +1437,7 @@
"exclude-pages": "Seiten von Schwärzung ausschließen",
"excluded-from-redaction": "Von Schwärzung ausgeschlossen",
"fullscreen": "Vollbildmodus",
"get-tables": "",
"highlights": {
"convert": "",
"remove": ""

View File

@ -1437,6 +1437,7 @@
"exclude-pages": "Exclude pages from redaction",
"excluded-from-redaction": "excluded",
"fullscreen": "Full Screen (F)",
"get-tables": "Draw tables",
"highlights": {
"convert": "Convert earmarks",
"remove": "Remove earmarks"

View File

@ -1437,6 +1437,7 @@
"exclude-pages": "Seiten von Schwärzung ausschließen",
"excluded-from-redaction": "Von Schwärzung ausgeschlossen",
"fullscreen": "Vollbildmodus",
"get-tables": "",
"highlights": {
"convert": "",
"remove": ""

View File

@ -1437,6 +1437,7 @@
"exclude-pages": "Exclude pages from component",
"excluded-from-redaction": "excluded",
"fullscreen": "Full Screen (F)",
"get-tables": "Draw tables",
"highlights": {
"convert": "Convert earmarks",
"remove": "Remove earmarks"

View File

@ -42,6 +42,7 @@
"chart.js": "^4.3.0",
"dayjs": "^1.11.5",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
"keycloak-angular": "14.0.0",
"keycloak-js": "21.1.1",
@ -79,6 +80,7 @@
"@nx/linter": "16.3.2",
"@nx/workspace": "16.3.2",
"@schematics/angular": "16.1.0",
"@types/file-saver": "^2.0.5",
"@types/jest": "29.5.2",
"@types/lodash-es": "^4.17.6",
"@types/node": "20.3.1",

View File

@ -3720,6 +3720,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/file-saver@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
"@types/graceful-fs@^4.1.3":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae"
@ -7282,6 +7287,11 @@ image-size@~0.5.0:
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immutable@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
@ -8233,6 +8243,16 @@ jsonparse@^1.3.1:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jszip@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"
jwt-decode@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
@ -8335,6 +8355,13 @@ license-webpack-plugin@4.0.2, license-webpack-plugin@^4.0.2:
dependencies:
webpack-sources "^3.0.0"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lilconfig@2.1.0, lilconfig@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@ -9448,7 +9475,7 @@ pacote@15.2.0:
ssri "^10.0.0"
tar "^6.1.11"
pako@^1.0.3:
pako@^1.0.3, pako@~1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@ -10370,7 +10397,7 @@ read-package-json@^6.0.0:
normalize-package-data "^5.0.0"
npm-normalize-package-bin "^3.0.0"
readable-stream@^2.0.1, readable-stream@^2.3.0, readable-stream@^2.3.5:
readable-stream@^2.0.1, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
@ -10819,6 +10846,11 @@ set-blocking@^2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"