From 838a94bd9187c3c314462e16d12fc9af2fd860d3 Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Wed, 10 Jun 2026 15:19:44 +0200 Subject: [PATCH 01/34] add EDC Contract Definition step to offer --- src/app/models/paginated-list.ts | 49 +++ .../edc-contract-definition.component.ts | 171 ++++++++ .../edc-contract-definition.component.css | 0 .../edc-contract-definition.component.html | 30 ++ .../edc-contract-definition.component.spec.ts | 27 ++ .../edc-contract-definition.component.ts | 193 +++++++++ .../shared/forms/offer/offer.component.html | 309 +++++++------- src/app/shared/forms/offer/offer.component.ts | 403 ++++++++++-------- src/app/validators/validators.ts | 11 + src/environments/environment.development.ts | 3 +- src/environments/environment.production.ts | 3 +- src/environments/environment.ts | 3 +- 12 files changed, 854 insertions(+), 348 deletions(-) create mode 100644 src/app/models/paginated-list.ts create mode 100644 src/app/shared/forms/offer/edc-contract-definition.component/edc-contract-definition.component.ts create mode 100644 src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.css create mode 100644 src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.html create mode 100644 src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.spec.ts create mode 100644 src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.ts diff --git a/src/app/models/paginated-list.ts b/src/app/models/paginated-list.ts new file mode 100644 index 00000000..de59c6e6 --- /dev/null +++ b/src/app/models/paginated-list.ts @@ -0,0 +1,49 @@ +import { signal } from '@angular/core'; + +export class PaginatedList { + readonly items = signal([]); + readonly loading = signal(false); + readonly loadingMore = signal(false); + readonly hasMore = signal(false); + + private page = 0; + private prefetched: T[] = []; + private readonly fetcher: (page: number) => Promise; + private readonly pageSize: number; + + constructor(fetcher: (page: number) => Promise, pageSize: number) { + this.fetcher = fetcher; + this.pageSize = pageSize; + } + + async load(): Promise { + this.loading.set(true); + this.page = 0; + this.items.set([]); + this.prefetched = []; + + try { + this.items.set(await this.fetcher(0)); + this.page = this.pageSize; + this.prefetched = await this.fetcher(this.page); + this.page += this.pageSize; + this.hasMore.set(this.prefetched.some(item => item != null)); + } finally { + this.loading.set(false); + } + } + + async loadMore(): Promise { + if (!this.hasMore()) return; + this.loadingMore.set(true); + + try { + this.items.set([...this.items(), ...this.prefetched]); + this.prefetched = await this.fetcher(this.page); + this.page += this.pageSize; + this.hasMore.set(this.prefetched.some(item => item != null)); + } finally { + this.loadingMore.set(false); + } + } +} diff --git a/src/app/shared/forms/offer/edc-contract-definition.component/edc-contract-definition.component.ts b/src/app/shared/forms/offer/edc-contract-definition.component/edc-contract-definition.component.ts new file mode 100644 index 00000000..2a1520f3 --- /dev/null +++ b/src/app/shared/forms/offer/edc-contract-definition.component/edc-contract-definition.component.ts @@ -0,0 +1,171 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; +import { Subject } from "rxjs"; +import { takeUntil } from 'rxjs/operators'; +import { EventMessageService } from "src/app/services/event-message.service"; +import { FormChangeState } from "../../../../models/interfaces"; +import { MarkdownTextareaComponent } from "../../markdown-textarea/markdown-textarea.component"; + +interface License { + treatment: string; + description: string; +} + +@Component({ + selector: 'app-license-form', + standalone: true, + imports: [ + MarkdownTextareaComponent, + ReactiveFormsModule, + TranslateModule + ], + templateUrl: './license.component.html', + styleUrl: './license.component.css' +}) +export class LicenseComponent implements OnInit, OnDestroy { + @Input() form!: AbstractControl; + @Input() formType!: string; + @Input() data: any; + @Output() formChange = new EventEmitter(); + private destroy$ = new Subject(); + + constructor( + private eventMessage: EventMessageService) { + this.eventMessage.messages$ + .pipe(takeUntil(this.destroy$)) + .subscribe(ev => { + if (ev.type === 'UpdateOffer') { + if (this.isEditMode && this.hasBeenModified && this.originalValue) { + const currentValue = { + treatment: 'License', + description: this.descControl?.value || '' + }; + + const dirtyFields = this.getDirtyFields(currentValue); + + if (dirtyFields.length > 0) { + const changeState: FormChangeState = { + subformType: 'license', + isDirty: true, + dirtyFields, + originalValue: this.originalValue, + currentValue + }; + + console.log('🚀 Emitting final change state:', changeState); + this.formChange.emit(changeState); + } else { + console.log('📝 No real changes detected, skipping emission'); + } + } + } + }) + } + + freeLicenseSelected: boolean = false; + private originalValue: License | null = null; + private hasBeenModified: boolean = false; + private isEditMode: boolean = false; + + get formGroup(): FormGroup { + return this.form as FormGroup; // Lo convierte en FormGroup + } + + get descControl(): FormControl | null { + const control = this.formGroup.get('description'); + return control instanceof FormControl ? control : null; + } + + ngOnInit() { + console.log('🔄 Initializing LicenseComponent'); + console.log('📝 Initializing form in', this.formType, 'mode'); + this.isEditMode = this.formType === 'update'; + + if (this.isEditMode && this.data) { + console.log('📝 Data received:', this.data); + //LICENSE + if (this.data.productOfferingTerm && Array.isArray(this.data.productOfferingTerm)) { + let license = this.data.productOfferingTerm?.find((element: { name: any; }) => element.name == 'License') + if (license) { + this.formGroup.addControl('name', new FormControl('License')); + this.formGroup.addControl('description', new FormControl(license.description)); + + // Store original value only in edit mode + this.originalValue = { + treatment: license.name, + description: license.description + }; + console.log('📝 Original value stored:', this.originalValue); + } else { + this.formGroup.addControl('name', new FormControl('License')); + this.formGroup.addControl('description', new FormControl('')); + } + } else { + this.formGroup.addControl('name', new FormControl('License')); + this.formGroup.addControl('description', new FormControl('')); + } + } else { + this.formGroup.addControl('name', new FormControl('License')); + this.formGroup.addControl('description', new FormControl('')); + } + + // Subscribe to form changes only in edit mode + if (this.isEditMode) { + this.formGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.hasBeenModified = true; + }); + } + } + + ngOnDestroy() { + console.log('🗑️ Destroying LicenseComponent'); + + // Solo emitir cambios si estamos en modo edición y hay cambios reales + if (this.isEditMode && this.hasBeenModified && this.originalValue) { + const currentValue = { + treatment: 'License', + description: this.descControl?.value || '' + }; + + const dirtyFields = this.getDirtyFields(currentValue); + + if (dirtyFields.length > 0) { + const changeState: FormChangeState = { + subformType: 'license', + isDirty: true, + dirtyFields, + originalValue: this.originalValue, + currentValue + }; + + console.log('🚀 Emitting final change state:', changeState); + this.formChange.emit(changeState); + } else { + console.log('📝 No real changes detected, skipping emission'); + } + } else if (!this.isEditMode) { + console.log('📝 Not in edit mode, skipping change detection'); + } + this.destroy$.next(); + this.destroy$.complete(); + } + + private getDirtyFields(currentValue: License): string[] { + const dirtyFields: string[] = []; + + if (!this.originalValue) return dirtyFields; + + if (currentValue.treatment !== this.originalValue.treatment) { + dirtyFields.push('treatment'); + } + + if (currentValue.description !== this.originalValue.description) { + dirtyFields.push('description'); + } + + return dirtyFields; + } +} diff --git a/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.css b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.html b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.html new file mode 100644 index 00000000..6ed1a4e6 --- /dev/null +++ b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.html @@ -0,0 +1,30 @@ + +@if (accessControl) { + + +@if (accessControl.hasError('invalidJson') && accessControl.touched) { +

{{ 'Invalid JSON' | translate }}

+} +} + +@if (contractControl) { + + +@if (contractControl.hasError('invalidJson') && contractControl.touched) { +

{{ 'Invalid JSON' | translate }}

+} +} + +@if (formGroup.hasError('policiesRequired') && (accessControl?.touched || contractControl?.touched)) { +

{{ 'Both Access Policy and Contract Policy are required if either is filled' | translate }}

+} diff --git a/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.spec.ts b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.spec.ts new file mode 100644 index 00000000..48718a6f --- /dev/null +++ b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.spec.ts @@ -0,0 +1,27 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +import { EdcContractDefinitionComponent } from './edc-contract-definition.component'; + +describe('LicenseComponent', () => { + let component: EdcContractDefinitionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + imports: [EdcContractDefinitionComponent, HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot()] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EdcContractDefinitionComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.ts b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.ts new file mode 100644 index 00000000..74880645 --- /dev/null +++ b/src/app/shared/forms/offer/edc-contract-definition/edc-contract-definition.component.ts @@ -0,0 +1,193 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, ValidatorFn } from "@angular/forms"; +import { TranslateModule } from "@ngx-translate/core"; +import { Subject } from "rxjs"; +import { takeUntil } from 'rxjs/operators'; +import { EventMessageService } from "src/app/services/event-message.service"; +import { jsonValidator } from "src/app/validators/validators"; +import { FormChangeState } from "../../../../models/interfaces"; + +interface EdcContractDefinition { + name: 'edc:contractDefinition'; + accessPolicy: string; + contractPolicy: string; +} + +@Component({ + selector: 'app-edc-contract-definition-form', + standalone: true, + imports: [ + ReactiveFormsModule, + TranslateModule + ], + templateUrl: './edc-contract-definition.component.html', + styleUrl: './edc-contract-definition.component.css' +}) +export class EdcContractDefinitionComponent implements OnInit, OnDestroy { + @Input() form!: AbstractControl; + @Input() formType!: string; + @Input() data: any; + @Output() formChange = new EventEmitter(); + private destroy$ = new Subject(); + + constructor( + private eventMessage: EventMessageService) { + this.eventMessage.messages$ + .pipe(takeUntil(this.destroy$)) + .subscribe(ev => { + if (ev.type === 'UpdateOffer') { + if (this.isEditMode && this.hasBeenModified && this.originalValue) { + const currentValue: EdcContractDefinition = { + name: 'edc:contractDefinition', + accessPolicy: this.accessControl?.value || '', + contractPolicy: this.contractControl?.value || '', + }; + + const dirtyFields = this.getDirtyFields(currentValue); + + if (dirtyFields.length > 0) { + const changeState: FormChangeState = { + subformType: 'contractDefinition', + isDirty: true, + dirtyFields, + originalValue: this.originalValue, + currentValue + }; + + console.log('🚀 Emitting final change state:', changeState); + this.formChange.emit(changeState); + } else { + console.log('📝 No real changes detected, skipping emission'); + } + } + } + }) + } + + freeLicenseSelected: boolean = false; + private originalValue: EdcContractDefinition | null = null; + private hasBeenModified: boolean = false; + private isEditMode: boolean = false; + + get formGroup(): FormGroup { + return this.form as FormGroup; + } + + get accessControl(): FormControl | null { + + const control = this.formGroup.get('accessPolicy'); + return control instanceof FormControl ? control : null; + } + + get contractControl(): FormControl | null { + const control = this.formGroup.get('contractPolicy'); + return control instanceof FormControl ? control : null; + } + + ngOnInit() { + console.log('🔄 Initializing LicenseComponent'); + console.log('📝 Initializing form in', this.formType, 'mode'); + this.isEditMode = this.formType === 'update'; + let contractDefinition = null; + if (this.isEditMode && this.data) { + console.log('📝 Data received:', this.data); + + if (this.data.productOfferingTerm && Array.isArray(this.data.productOfferingTerm)) { + contractDefinition = this.data.productOfferingTerm?.find((element: { name: any; }) => element.name == 'edc:contractDefinition') + if (contractDefinition) { + this.formGroup.addControl('name', new FormControl('edc:contractDefinition')); + this.formGroup.addControl('accessPolicy', new FormControl(contractDefinition.accessPolicy, jsonValidator)); + this.formGroup.addControl('contractPolicy', new FormControl(contractDefinition.contractPolicy, jsonValidator)); + this.formGroup.addValidators(this.edcPoliciesRequiredValidator); + + // Store original value only in edit mode + this.originalValue = { + name: contractDefinition.name, + accessPolicy: contractDefinition.accessPolicy, + contractPolicy: contractDefinition.contractPolicy + }; + console.log('📝 Original value stored:', this.originalValue); + } + } + } + if (!contractDefinition) { + this.formGroup.addControl('name', new FormControl('edc:contractDefinition')); + this.formGroup.addControl('accessPolicy', new FormControl('', jsonValidator)); + this.formGroup.addControl('contractPolicy', new FormControl('', jsonValidator)); + this.formGroup.addValidators(this.edcPoliciesRequiredValidator); + } + + // Subscribe to form changes only in edit mode + if (this.isEditMode) { + this.formGroup.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.hasBeenModified = true; + }); + } + } + + ngOnDestroy() { + console.log('🗑️ Destroying LicenseComponent'); + + if (this.isEditMode && this.hasBeenModified && this.originalValue) { + const currentValue: EdcContractDefinition = { + name: 'edc:contractDefinition', + accessPolicy: this.accessControl?.value || '', + contractPolicy: this.contractControl?.value || '' + }; + + const dirtyFields = this.getDirtyFields(currentValue); + + if (dirtyFields.length > 0) { + const changeState: FormChangeState = { + subformType: 'license', + isDirty: true, + dirtyFields, + originalValue: this.originalValue, + currentValue + }; + + console.log('🚀 Emitting final change state:', changeState); + this.formChange.emit(changeState); + } else { + console.log('📝 No real changes detected, skipping emission'); + } + } else if (!this.isEditMode) { + console.log('📝 Not in edit mode, skipping change detection'); + } + this.destroy$.next(); + this.destroy$.complete(); + } + + private getDirtyFields(currentValue: EdcContractDefinition): string[] { + const dirtyFields: string[] = []; + + if (!this.originalValue) return dirtyFields; + + if (currentValue.name !== this.originalValue.name) { + dirtyFields.push('name'); + } + + if (currentValue.accessPolicy !== this.originalValue.accessPolicy) { + dirtyFields.push('accessPolicy'); + } + + if (currentValue.contractPolicy !== this.originalValue.contractPolicy) { + dirtyFields.push('contractPolicy'); + } + + return dirtyFields; + } + private edcPoliciesRequiredValidator: ValidatorFn = (form: AbstractControl): ValidationErrors | null => { + const access = form.get('accessPolicy')?.value?.trim() || ''; + const contract = form.get('contractPolicy')?.value?.trim() || ''; + const eitherFilled = access !== '' || contract !== ''; + if (eitherFilled && (access === '' || contract === '')) { + return { policiesRequired: true }; + } + return null; + }; + + +} diff --git a/src/app/shared/forms/offer/offer.component.html b/src/app/shared/forms/offer/offer.component.html index 7f253113..6cc8eade 100644 --- a/src/app/shared/forms/offer/offer.component.html +++ b/src/app/shared/forms/offer/offer.component.html @@ -1,203 +1,198 @@ -
+
@if(formType === 'update'){ -

- {{ 'UPDATE_OFFER._update' | translate }} -

+

+ {{ 'UPDATE_OFFER._update' | translate }} +

} @else { -

- {{ 'CREATE_OFFER._create' | translate }} -

+

+ {{ 'CREATE_OFFER._create' | translate }} +

} -@if(loadingData){ -
- - Loading... -
-} @else { + @if(loadingData){ +
+ + Loading... +
+ } @else {
-
    +
      @for (step of steps; track i; let i = $index) { -
    1. - - {{ i + 1 }} - - -
    2. +
    3. + + {{ i + 1 }} + + +
    4. }
    -

    {{ this.steps[currentStep] }}

    +

    {{ + this.steps[currentStep] }}

    @if(loading){ -
    - - Loading... -
    +
    + + Loading... +
    } @else {
    - - @if (currentStep === 0) { - - - } @else if (currentStep === 1) { - - - } @else if (currentStep === 2) { - @if(this.formType=='update'){ - - - }@else{ - - } - } @else if (currentStep === 3){ - @if(this.formType=='update'){ - - - }@else{ - - - } - } @else if (currentStep === 4) { - @if(this.formType=='update'){ - - - }@else{ - - - } - } @else if (currentStep === 5) { - @if(this.formType=='update'){ - - - }@else{ - - - } - } @else if (currentStep === 6) { - @if(this.formType=='update'){ - - - }@else{ - - - - } - } @else if(currentStep === 7) { - + @if (currentStep === 0) { + + + } @else if (currentStep === 1) { + + + } @else if (currentStep === 2) { + @if(this.formType=='update'){ + + + }@else{ + + } + } @else if (currentStep === 3){ + @if(this.formType=='update'){ + + + }@else{ + + + } + } @else if (currentStep === 4){ + @if(this.formType=='update'){ + + + }@else{ + + + } + } @else if (currentStep === 5) { + @if(this.formType=='update'){ + + + }@else{ + + + } + } @else if (currentStep === 6) { + @if(this.formType=='update'){ + + + }@else{ + + + } + } @else if (currentStep === 7) { + @if(this.formType=='update'){ + + + }@else{ + + + + } + } @else if(currentStep === 8) { + - - } + + }
    - } + }
    - @if(formType === 'create' && currentStep === 7 || formType === 'update'){ - + {{ formType === 'create' ? 'Create Offer' : 'Update Offer' }} + }
    -} + }
@if(showError){ -
-