Fixed ui and template mapping issue
This commit is contained in:
@@ -81,15 +81,19 @@ export class TemplateService {
|
||||
);
|
||||
}
|
||||
|
||||
createTemplate(data: { template_name: string, fields: TemplateField[] }): Observable<Template> {
|
||||
createTemplate(data: { template_name: string, source_document_id?: string, fields: TemplateField[] }): Observable<Template> {
|
||||
return this.http.post<Template>(`${this.apiUrl}/templates`, data);
|
||||
}
|
||||
|
||||
saveMappings(templateId: number, mappings: any[]): Observable<any> {
|
||||
saveMappings(templateId: string, mappings: any[]): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/templates/${templateId}/mappings/save`, mappings);
|
||||
}
|
||||
|
||||
getTemplate(templateId: string): Observable<any> {
|
||||
return this.http.get(`${this.apiUrl}/templates/${templateId}`);
|
||||
}
|
||||
|
||||
recognizeTemplate(documentId: string): Observable<any> {
|
||||
return this.http.post(`${this.apiUrl}/documents/${documentId}/recognize`, {});
|
||||
recognizeTemplate(documentId: string): Observable<any[]> {
|
||||
return this.http.post<any[]>(`${this.apiUrl}/templates/match`, { document_id: documentId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
|
||||
<div *ngFor="let node of page.nodes; let j = index"
|
||||
cdkDrag
|
||||
[cdkDragData]="node"
|
||||
class="layout-node"
|
||||
(mouseup)="onNodeMouseUp(node, $event)"
|
||||
[ngClass]="node.block_type.toLowerCase()"
|
||||
[style.left.%]="(node.x_coordinate / node.page_width) * 100"
|
||||
[style.top.%]="(node.y_coordinate / node.page_height) * 100"
|
||||
@@ -71,9 +73,12 @@
|
||||
|
||||
<div class="field-list">
|
||||
<div *ngFor="let field of templateFields" class="template-field-container">
|
||||
<div class="field-header">
|
||||
<strong>{{ field.field_label }}</strong>
|
||||
<span class="badge">{{ field.field_type }}</span>
|
||||
<div class="field-header flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ field.field_label }}</strong>
|
||||
<span class="badge ml-2">{{ field.field_type }}</span>
|
||||
</div>
|
||||
<p-button icon="pi pi-trash" styleClass="p-button-rounded p-button-danger p-button-text p-button-sm" (onClick)="removeField(field.pk_template_field_id)"></p-button>
|
||||
</div>
|
||||
|
||||
<div class="drop-zone"
|
||||
@@ -87,8 +92,10 @@
|
||||
Drop value here...
|
||||
</div>
|
||||
|
||||
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]" cdkDrag class="mapped-node">
|
||||
<i class="pi pi-arrow-left text-xs mr-2"></i> {{ mappedNode.text_value }}
|
||||
<div *ngFor="let mappedNode of mappings[field.pk_template_field_id]; let k = index" cdkDrag [cdkDragData]="mappedNode" class="mapped-node">
|
||||
<i class="pi pi-arrow-left text-xs mr-2"></i>
|
||||
<span class="flex-1 overflow-hidden white-space-nowrap text-overflow-ellipsis">{{ mappedNode.text_value }}</span>
|
||||
<i class="pi pi-times cursor-pointer text-red-500 hover:text-red-700 ml-2" (click)="removeFromField(field.pk_template_field_id!, k)" title="Remove"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { DragDropModule, CdkDragDrop, moveItemInArray, transferArrayItem, copyArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { SplitterModule } from 'primeng/splitter';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
@@ -121,10 +121,71 @@ export class TemplatesComponent implements OnInit {
|
||||
autoRecognize() {
|
||||
if (!this.documentId) return;
|
||||
this.templateService.recognizeTemplate(this.documentId).subscribe({
|
||||
next: (res) => {
|
||||
if (res.templateMatched) {
|
||||
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${res.confidence}%` });
|
||||
// In a full implementation, we'd load the template fields and populate the mappings.
|
||||
next: (matches) => {
|
||||
if (matches && matches.length > 0 && matches[0].confidence_score > 0.5) {
|
||||
const match = matches[0];
|
||||
this.messageService.add({ severity: 'info', summary: 'Template Recognized', detail: `Confidence: ${(match.confidence_score * 100).toFixed(1)}%` });
|
||||
|
||||
this.templateService.getTemplate(match.format_id).subscribe({
|
||||
next: (templateData) => {
|
||||
this.templateName = templateData.name;
|
||||
this.templateFields = [];
|
||||
this.mappings = {};
|
||||
|
||||
if (templateData.cells) {
|
||||
templateData.cells.forEach((cell: any) => {
|
||||
if (cell.is_dynamic) {
|
||||
const fieldId = new Date().getTime() + Math.random();
|
||||
this.templateFields.push({
|
||||
pk_template_field_id: fieldId as any,
|
||||
field_label: cell.field_name,
|
||||
field_type: cell.data_type,
|
||||
display_order: cell.sequence,
|
||||
required_flag: false
|
||||
});
|
||||
this.mappings[fieldId] = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (templateData.regions) {
|
||||
templateData.regions.forEach((region: any) => {
|
||||
if (region.region_type === 'field_mapping' && region.content && region.content.field_name) {
|
||||
const field = this.templateFields.find(f => f.field_label === region.content.field_name);
|
||||
if (field && field.pk_template_field_id) {
|
||||
// Find the corresponding page in the current document
|
||||
const page = this.pages.find(p => p.page_number === region.page_number);
|
||||
if (page) {
|
||||
let bestMatchIdx = -1;
|
||||
let bestOverlap = 0;
|
||||
|
||||
// Find the text node on the canvas that overlaps most with this region's bounding box
|
||||
for (let i = 0; i < page.nodes.length; i++) {
|
||||
const node = page.nodes[i];
|
||||
const x_overlap = Math.max(0, Math.min(region.x + region.width, node.x_coordinate + node.width) - Math.max(region.x, node.x_coordinate));
|
||||
const y_overlap = Math.max(0, Math.min(region.y + region.height, node.y_coordinate + node.height) - Math.max(region.y, node.y_coordinate));
|
||||
const overlapArea = x_overlap * y_overlap;
|
||||
|
||||
if (overlapArea > bestOverlap) {
|
||||
bestOverlap = overlapArea;
|
||||
bestMatchIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a matching node (at least some overlap), move it from canvas to mappings
|
||||
if (bestMatchIdx !== -1) {
|
||||
const matchedNode = page.nodes.splice(bestMatchIdx, 1)[0];
|
||||
|
||||
// Because *ngFor tracks objects by reference, we clone it to force Angular to render it in the right panel cleanly
|
||||
this.mappings[field.pk_template_field_id].push({...matchedNode});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -143,6 +204,12 @@ export class TemplatesComponent implements OnInit {
|
||||
this.displayAddField = false;
|
||||
}
|
||||
|
||||
removeField(fieldId?: number) {
|
||||
if (!fieldId) return;
|
||||
this.templateFields = this.templateFields.filter(f => f.pk_template_field_id !== fieldId);
|
||||
delete this.mappings[fieldId];
|
||||
}
|
||||
|
||||
saveTemplateAndMappings() {
|
||||
if (!this.templateName || this.templateName.trim() === '') {
|
||||
this.messageService.add({ severity: 'warn', summary: 'Warning', detail: 'Please provide a template name.' });
|
||||
@@ -156,11 +223,39 @@ export class TemplatesComponent implements OnInit {
|
||||
|
||||
this.templateService.createTemplate({
|
||||
template_name: this.templateName,
|
||||
source_document_id: this.documentId || undefined,
|
||||
fields: this.templateFields
|
||||
}).subscribe({
|
||||
next: (res) => {
|
||||
// In a real scenario, we pass res.templateId and map the layoutNodes
|
||||
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template & Mappings saved successfully.' });
|
||||
next: (res: any) => {
|
||||
const templateId = res.pk_template_id;
|
||||
|
||||
// Construct the mappings payload
|
||||
const mappingsPayload: any[] = [];
|
||||
this.templateFields.forEach(field => {
|
||||
const fieldId = field.pk_template_field_id;
|
||||
if (fieldId && this.mappings[fieldId] && this.mappings[fieldId].length > 0) {
|
||||
mappingsPayload.push({
|
||||
field_name: field.field_label,
|
||||
mapped_nodes: this.mappings[fieldId]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (mappingsPayload.length > 0) {
|
||||
this.templateService.saveMappings(templateId, mappingsPayload).subscribe({
|
||||
next: () => {
|
||||
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template & Mappings saved successfully.' });
|
||||
},
|
||||
error: (err) => {
|
||||
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save mappings.' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Template saved successfully (No mappings).' });
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.messageService.add({ severity: 'error', summary: 'Error', detail: 'Failed to save template.' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -168,14 +263,58 @@ export class TemplatesComponent implements OnInit {
|
||||
// Drag and Drop Logic
|
||||
drop(event: CdkDragDrop<DocumentLayout[]>, fieldId?: number) {
|
||||
if (event.previousContainer === event.container) {
|
||||
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
|
||||
// Do nothing! This allows the item to naturally snap back to its original position
|
||||
// since it's a failed drop (didn't land in a mapping field).
|
||||
} else {
|
||||
transferArrayItem(
|
||||
event.previousContainer.data,
|
||||
event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex,
|
||||
);
|
||||
const isFromCanvas = event.previousContainer.id.startsWith('document-layout-list');
|
||||
const isToCanvas = event.container.id.startsWith('document-layout-list');
|
||||
|
||||
if (isFromCanvas && !isToCanvas) {
|
||||
// Drag from Canvas -> Field
|
||||
// We use event.item.data which perfectly tracks the dragged object regardless of DOM indexes
|
||||
const clonedNode = JSON.parse(JSON.stringify(event.item.data));
|
||||
|
||||
// Insert the clone into the destination mapping field
|
||||
event.container.data.splice(event.currentIndex, 0, clonedNode);
|
||||
|
||||
// Force Angular to completely recreate the DOM elements for this specific canvas page.
|
||||
// By using .map(node => ({...node})), we change every object's identity.
|
||||
// This forces Angular to destroy the corrupted DOM element (which CDK moved and left a translate3d on)
|
||||
// and recreate it fresh with its original absolute coordinates!
|
||||
const pageIndex = parseInt(event.previousContainer.id.split('-').pop() || '0');
|
||||
if (!isNaN(pageIndex) && this.pages[pageIndex]) {
|
||||
this.pages[pageIndex].nodes = this.pages[pageIndex].nodes.map(node => ({...node}));
|
||||
|
||||
// Also explicitly clear the transform on the dragged element just in case CDK holds a ref to it
|
||||
event.item.element.nativeElement.style.transform = '';
|
||||
}
|
||||
|
||||
} else if (!isFromCanvas && isToCanvas) {
|
||||
// Drag from Field -> Canvas (Delete from Field)
|
||||
event.previousContainer.data.splice(event.previousIndex, 1);
|
||||
} else {
|
||||
// Drag from Field -> Field (Move)
|
||||
transferArrayItem(
|
||||
event.previousContainer.data,
|
||||
event.container.data,
|
||||
event.previousIndex,
|
||||
event.currentIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onNodeMouseUp(node: DocumentLayout, event: MouseEvent) {
|
||||
// If the user resized the node using the native CSS resize handle, save the new size
|
||||
const el = event.currentTarget as HTMLElement;
|
||||
if (el.style.width && el.style.width.endsWith('px')) {
|
||||
const parentRect = el.parentElement!.getBoundingClientRect();
|
||||
node.width = (el.offsetWidth / parentRect.width) * node.page_width;
|
||||
node.height = (el.offsetHeight / parentRect.height) * node.page_height;
|
||||
|
||||
// Clear the inline pixel styles so Angular bindings take over smoothly
|
||||
el.style.width = '';
|
||||
el.style.height = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,4 +331,10 @@ export class TemplatesComponent implements OnInit {
|
||||
event.stopPropagation();
|
||||
this.pages[pageIndex].nodes.splice(nodeIndex, 1);
|
||||
}
|
||||
|
||||
removeFromField(fieldId: number, nodeIndex: number) {
|
||||
if (this.mappings[fieldId]) {
|
||||
this.mappings[fieldId].splice(nodeIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user