From 0773a7c88e32775c5b69391dd0385e66e6fa2a0f Mon Sep 17 00:00:00 2001 From: aorin Date: Tue, 30 Sep 2025 17:00:36 +0300 Subject: [PATCH 1/3] fix import error messages not showing --- .../error-list/error-list.component.html | 2 +- .../error-list/error-list.component.ts | 82 ++++++++----------- .../spreadsheet/service/import.service.ts | 6 +- 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.html b/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.html index a9bc923eab..5c165b4995 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.html +++ b/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.html @@ -1,5 +1,5 @@
- {{ fields[data.field].fullLabel }} + {{ data.title }} diff --git a/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.ts b/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.ts index 07ff03f51b..27b516059e 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/importer/status-cell/error-list/error-list.component.ts @@ -1,97 +1,81 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; import { IFormField } from '../../../model/excel'; import { TranslateService } from '@ngx-translate/core'; import { Util } from '../../../../../shared/service/util.service'; +interface ErrorGroup { + title: string; + errors: string[]; +} + @Component({ selector: 'laji-error-list', templateUrl: './error-list.component.html', styleUrls: ['./error-list.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ErrorListComponent { +export class ErrorListComponent implements OnChanges { @Input({ required: true }) fields!: {[key: string]: IFormField}; + @Input() errors: unknown; - _errors: {field: string; errors: string[]}[] = []; + _errors: ErrorGroup[] = []; constructor(private translateService: TranslateService) { } - @Input() - set errors(data: unknown) { - const errors = []; + ngOnChanges() { + this._errors = this.processErrors(this.errors, this.fields); + } + + processErrors(data: unknown, fields: {[key: string]: IFormField}): ErrorGroup[] { + const errors: ErrorGroup[] = []; + if (Util.isObject(data)) { if (Util.hasOwnProperty(data, 'status')) { switch (data.status) { case 403: errors.push({ - field: 'id', + title: this.translateService.instant('error'), errors: [this.translateService.instant('form.permission.no-access')] }); break; case 422: default: errors.push({ - field: 'id', - errors: [Util.hasOwnProperty(data, 'statusText') ? data.statusText : this.translateService.instant('haseka.form.genericError')] + title: this.translateService.instant('error'), + errors: [this.translateService.instant('haseka.form.genericError')] }); } } else { Object.keys(data).forEach(field => { errors.push({ - field: this.pathToKey(field), - errors: Array.isArray(data[field]) ? data[field] : this.pickErrors(data[field]) + title: this.getFieldName(field, fields), + errors: data[field] as string[] }); }); } - } else if (Array.isArray(data)) { - data.forEach(err => { - if (typeof err !== 'object' || !Util.hasOwnProperty(err, 'dataPath')) { - return; - } - errors.push({ - field: err.dataPath - .substring(err.dataPath.substring(0, 1) === '/' ? 1 : 0) - .replace(/\/[0-9]+/g, '[*]') - .replace(/\//g, '.'), - errors: [this.getMessage(err)] - }); - }); } - this._errors = errors; + + return errors; } - private pathToKey(path: string) { + private getFieldName(path: string, fields: {[key: string]: IFormField}): string { if (path.substring(0, 1) === '.') { path = path.substring(1); } - return path.replace(/\[[0-9]+]/g, '[*]'); - } - private pickErrors(value: any): any[] { - if (typeof value === 'string') { - return [value]; - } else if (Array.isArray(value)) { - return value; - } else if (typeof value === 'object') { - return Object.keys((value)).reduce((prev, current) => [...prev, ...this.pickErrors(current)] as any, []); - } - return [value]; - } + const key = path.replace(/\[[0-9]+]/g, '[*]'); - private getMessage(err: unknown): string { - if (!Util.isObject(err) || !Util.hasOwnProperty(err, 'message') || typeof err.message !== 'string') { - return this.translateService.instant('haseka.form.genericError'); + if (fields[key]) { + return fields[key].fullLabel; } - let base = err.message; - if (Util.hasOwnProperty(err, 'params') && typeof err.params === 'object' && err.params && !Array.isArray(err.params)) { - const info = Object.values(err.params); - if (info.length) { - base += ` '${info.join('\', \'')}'`; - } + + const arrayKey = key + '[*]'; + + if (fields[arrayKey]) { + return fields[arrayKey].fullLabel; } - return base; + return ''; } - } diff --git a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts index 9c13b9d2b3..5508d450f9 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts @@ -94,9 +94,9 @@ export class ImportService { waitToComplete(type: keyof Pick, jobPayload: DocumentJobPayload, processCB: (status: JobStatus) => void): Observable { const personToken = this.userService.getToken(); - const source$ = type === 'validate' ? - this.documentApi.validate(jobPayload, {personToken}) : - this.documentApi.create(jobPayload, personToken); + const source$ = type === 'validate' + ? this.documentApi.validate(jobPayload, { personToken, lang: this.translateService.currentLang, validationErrorFormat: 'jsonPath' }) + : this.documentApi.create(jobPayload, personToken); return source$.pipe( switchMap(response => { processCB(response.status); From 41491f80c527a320d673f8473884cc99d3dec2e1 Mon Sep 17 00:00:00 2001 From: Olli Raitio Date: Thu, 6 Nov 2025 16:23:48 +0200 Subject: [PATCH 2/3] use new API's batch job endpoints for vihko spreadsheet --- .../importer/importer.component.ts | 32 ++++++++------ .../spreadsheet/service/augment.service.ts | 4 +- .../spreadsheet/service/import.service.ts | 44 +++++++------------ 3 files changed, 37 insertions(+), 43 deletions(-) diff --git a/projects/laji/src/app/shared-modules/spreadsheet/importer/importer.component.ts b/projects/laji/src/app/shared-modules/spreadsheet/importer/importer.component.ts index 37b3b6ab23..3e2c3ff6f4 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/importer/importer.component.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/importer/importer.component.ts @@ -12,7 +12,6 @@ import { } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { forkJoin, from as ObservableFrom, Observable, of } from 'rxjs'; -import { Document } from '../../../shared/model/Document'; import { FormService } from '../../../shared/service/form.service'; import { IFormField, VALUE_IGNORE } from '../model/excel'; import { CombineToDocument, IDocumentData, ImportService } from '../service/import.service'; @@ -32,9 +31,14 @@ import { FileService, instanceOfFileLoad } from '../service/file.service'; import { IUserMappingFile, MappingFileService } from '../service/mapping-file.service'; import { Form } from '../../../shared/model/Form'; import { Logger } from '../../../shared/logger'; -import { DocumentJobPayload } from '../../../shared/api/DocumentApi'; import { toHtmlSelectElement } from '../../../shared/service/html-element.service'; -import {ModalRef, ModalService} from 'projects/laji-ui/src/lib/modal/modal.service'; +import { ModalRef, ModalService } from 'projects/laji-ui/src/lib/modal/modal.service'; + +import type { components } from 'projects/laji-api-client-b/generated/api'; + +type Document = components['schemas']['document']; +type PublicityRestrictions = Document['publicityRestrictions']; +type BatchJob = components['schemas']['BatchJobValidationStatusResponse']; @Component({ selector: 'laji-importer', @@ -72,7 +76,7 @@ export class ImporterComponent implements OnInit, OnDestroy { header?: {[key: string]: string}; fields?: {[key: string]: IFormField}; dataColumns?: ImportTableColumn[]; - jobPayload?: DocumentJobPayload; + job?: BatchJob; docCnt = 0; origColMap?: {[key: string]: string}; colMap?: {[key: string]: string}; @@ -83,8 +87,8 @@ export class ImporterComponent implements OnInit, OnDestroy { mimeType?: string; errors: any; valid = false; - priv = Document.PublicityRestrictionsEnum.publicityRestrictionsPrivate; - publ = Document.PublicityRestrictionsEnum.publicityRestrictionsPublic; + priv: PublicityRestrictions = 'MZ.publicityRestrictionsPrivate'; + publ: PublicityRestrictions = 'MZ.publicityRestrictionsPublic'; excludedFromCopy: string[] = []; userMappings: any; separator = MappingService.valueSplitter; @@ -456,9 +460,9 @@ export class ImporterComponent implements OnInit, OnDestroy { ObservableFrom(rowData).pipe( concatMap(data => this.augmentService.augmentDocument(data.document, this.excludedFromCopy)), toArray(), - switchMap(documents => this.importService.validateData(documents)), - tap(job => this.jobPayload = job), - switchMap(job => this.importService.waitToComplete('validate', job, (status) => { + switchMap(documents => this.importService.startBatchJob(documents)), + tap(job => this.job = job), + switchMap(job => this.importService.waitToComplete(job, (status) => { this.current = status.processed; this.cdr.markForCheck(); })), @@ -508,7 +512,7 @@ export class ImporterComponent implements OnInit, OnDestroy { ); } - save(publicityRestrictions: Document.PublicityRestrictionsEnum) { + save(publicityRestrictions: PublicityRestrictions) { this.spreadsheetFacade.goToStep(Step.importing); this.showOnlyErroneous = false; let success = true; @@ -522,14 +526,14 @@ export class ImporterComponent implements OnInit, OnDestroy { const rowData = this.parsedData!.filter(data => data.document !== null); this.importService.sendData({ - ...this.jobPayload, - dataOrigin: [Document.DataOriginEnum.dataOriginSpreadsheetFile], + ...this.job, + dataOrigin: 'MY.dataOriginSpreadsheetFile', publicityRestrictions } as any).pipe( - switchMap(() => this.importService.waitToComplete('create', this.jobPayload as any, (status) => { + switchMap(() => this.importService.waitToComplete(this.job!, (status) => { ticker += add; this.current = status.processed === this.total ? - status.processed : + status.processed: Math.min(Math.max(this.total - 1, 0), ticker); this.cdr.markForCheck(); })), diff --git a/projects/laji/src/app/shared-modules/spreadsheet/service/augment.service.ts b/projects/laji/src/app/shared-modules/spreadsheet/service/augment.service.ts index 568a4ea71a..4ec0d750d4 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/service/augment.service.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/service/augment.service.ts @@ -4,9 +4,11 @@ import { from as ObservableFrom, Observable, of as ObservableOf } from 'rxjs'; import { NamedPlace } from '../../../shared/model/NamedPlace'; import { NamedPlaceApi } from '../../../shared/api/NamedPlaceApi'; import { UserService } from '../../../shared/service/user.service'; -import { Document } from '../../../shared/model/Document'; import { DocumentService } from '../../own-submissions/service/document.service'; import { MappingService } from './mapping.service'; +import type { components } from 'projects/laji-api-client-b/generated/api'; + +type Document = components['schemas']['document']; @Injectable() export class AugmentService { diff --git a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts index 9c13b9d2b3..9f5180da8c 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts @@ -1,9 +1,5 @@ import { Injectable } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { Observable, of } from 'rxjs'; -import { DocumentApi, DocumentJobPayload } from '../../../shared/api/DocumentApi'; -import { Document } from '../../../shared/model/Document'; -import { UserService } from '../../../shared/service/user.service'; import { IFormField, LEVEL_DOCUMENT, @@ -15,7 +11,11 @@ import { import { MappingService } from './mapping.service'; import * as Hash from 'object-hash'; import { catchError, delay, switchMap } from 'rxjs/operators'; -import { ArrayType } from '@angular/compiler'; +import { LajiApiClientBService } from 'projects/laji-api-client-b/src/laji-api-client-b.service'; +import type { components } from 'projects/laji-api-client-b/generated/api'; + +type Document = components['schemas']['document']; +type BatchJob = components['schemas']['BatchJobValidationStatusResponse']; export interface IData { rowIdx: number; @@ -74,9 +74,7 @@ export class ImportService { constructor( private mappingService: MappingService, - private documentApi: DocumentApi, - private userService: UserService, - private translateService: TranslateService + private api: LajiApiClientBService ) { } hasInvalidValue(value: unknown, field: IFormField) { @@ -84,46 +82,36 @@ export class ImportService { return Array.isArray(mappedValue) ? mappedValue.indexOf(null) > -1 : mappedValue === null; } - validateData(document: Document|Document[]): Observable { - return this.documentApi.validate(document, { - personToken: this.userService.getToken(), - lang: this.translateService.currentLang, - validationErrorFormat: 'jsonPath' - }); + startBatchJob(documents: Document[]) { + return this.api.post('/documents/batch', undefined, documents); } - waitToComplete(type: keyof Pick, jobPayload: DocumentJobPayload, processCB: (status: JobStatus) => void): Observable { - const personToken = this.userService.getToken(); - const source$ = type === 'validate' ? - this.documentApi.validate(jobPayload, {personToken}) : - this.documentApi.create(jobPayload, personToken); - return source$.pipe( + waitToComplete(job: BatchJob, processCB: (status: BatchJob['status']) => void): Observable { + return this.api.get('/documents/batch/{jobID}', { path: { jobID: job.id }, query: { validationErrorFormat: 'dotNotation' } }).pipe( switchMap(response => { processCB(response.status); - if (response.status.percentage === 100) { + if (response.phase === 'READY_TO_COMPLETE' || 'COMPLETED') { return of(response); } return of(response).pipe( delay(1000), - switchMap(() => this.waitToComplete(type, jobPayload, processCB)) + switchMap(() => this.waitToComplete(job, processCB)) ); }), catchError((e) => { console.log('ERROR', e); return of(e).pipe( delay(1000), - switchMap(() => this.waitToComplete(type, jobPayload, processCB)) + switchMap(() => this.waitToComplete(job, processCB)) ); }) ); } sendData( - job: DocumentJobPayload + job: BatchJob ): Observable { - return this.documentApi.create(job, this.userService.getToken(), { - lang: this.translateService.currentLang - }); + return this.api.post('/documents/batch/{jobID}', { path: { jobID: job.id } }); } flatFieldsToDocuments( @@ -287,7 +275,7 @@ export class ImportService { const docs: {[hash: string]: IDocumentData} = {}; Object.keys(documents).forEach(hash => { if (!docs[hash]) { - docs[hash] = {document: {formID}, ref: {[hash]: {}}, rows: {}, skipped: []}; + docs[hash] = {document: {formID} as Document, ref: {[hash]: {}}, rows: {}, skipped: []}; const docData = this.findDocumentData(documents[hash]); docs[hash].rows[(docData as any).rowIdx] = true; if (ignoreRowsWithNoCount && !this.hasCountValue((docData as any).data) && combineBy === CombineToDocument.none) { From 70f3c52b80310dd8c9bc3ec650f620a2d7fb22f5 Mon Sep 17 00:00:00 2001 From: Olli Raitio Date: Tue, 13 Jan 2026 15:23:55 +0200 Subject: [PATCH 3/3] fix import service checking import job phase --- .../app/shared-modules/spreadsheet/service/import.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts index 9f5180da8c..0d680ab74b 100644 --- a/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts +++ b/projects/laji/src/app/shared-modules/spreadsheet/service/import.service.ts @@ -90,7 +90,7 @@ export class ImportService { return this.api.get('/documents/batch/{jobID}', { path: { jobID: job.id }, query: { validationErrorFormat: 'dotNotation' } }).pipe( switchMap(response => { processCB(response.status); - if (response.phase === 'READY_TO_COMPLETE' || 'COMPLETED') { + if (!['VALIDATING', 'COMPLETING'].includes(response.phase)) { return of(response); } return of(response).pipe(