From 5f405b69a043e26e0139f44b2b02b7f0c123b065 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 10 Apr 2026 10:08:17 +0200 Subject: [PATCH] solved problem on container import --- .../admin-create-user/admin-create-user.ts | 27 +-- .../pages/admin/admin-users/admin-users.ts | 27 +-- src/app/pages/auth/signup/signup.ts | 9 +- .../services/authorization/authorization.ts | 22 ++- .../containers/containers-call.fake.ts | 37 ++++ .../services/containers/containers-call.ts | 35 ++++ .../task-executions/task-executions.ts | 158 +++++++----------- .../shared/flow-assistant/flow-assistant.ts | 40 ++--- src/app/shared/ordering/ordering.ts | 11 +- src/app/shared/rete-editor/rete-editor.ts | 22 +-- .../task-execution-viewer.html | 2 +- .../task-execution-viewer.ts | 6 +- src/app/shared/title-toolbar/title-toolbar.ts | 11 +- src/app/utilities/password-validation.ts | 31 ++++ 14 files changed, 234 insertions(+), 204 deletions(-) create mode 100644 src/app/utilities/password-validation.ts diff --git a/src/app/pages/admin/admin-create-user/admin-create-user.ts b/src/app/pages/admin/admin-create-user/admin-create-user.ts index 1c08683..f6da9cd 100644 --- a/src/app/pages/admin/admin-create-user/admin-create-user.ts +++ b/src/app/pages/admin/admin-create-user/admin-create-user.ts @@ -12,14 +12,7 @@ import { AdminCreateUserRequest, UserRole } from '@models/user'; import { Router } from '@angular/router'; import { Authorization } from '@services/authorization/authorization'; import { FormUtility } from '@utilities/form-utility'; - -function hasValidPasswordComplexity(value: string): boolean { - return /^\S+$/.test(value) - && /[a-z]/.test(value) - && /[A-Z]/.test(value) - && /\d/.test(value) - && /[^A-Za-z0-9]/.test(value); -} +import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation'; @Component({ selector: 'app-admin-create-user-page', @@ -76,28 +69,14 @@ export class AdminCreateUserPage extends FormUtility { }); }); - readonly createPasswordChecks = signal([ - { label: 'At least 8 characters', satisfied: false }, - { label: 'At least one lowercase letter', satisfied: false }, - { label: 'At least one uppercase letter', satisfied: false }, - { label: 'At least one number', satisfied: false }, - { label: 'At least one special character', satisfied: false }, - { label: 'No spaces', satisfied: true } - ]); + readonly createPasswordChecks = signal(initialPasswordChecks()); constructor() { super(); effect(() => { const password = this.createModel().password; - this.createPasswordChecks.set([ - { label: 'At least 8 characters', satisfied: password.length >= 8 }, - { label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) }, - { label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) }, - { label: 'At least one number', satisfied: /\d/.test(password) }, - { label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) }, - { label: 'No spaces', satisfied: /^\S*$/.test(password) } - ]); + this.createPasswordChecks.set(evaluatePasswordChecks(password)); }); } diff --git a/src/app/pages/admin/admin-users/admin-users.ts b/src/app/pages/admin/admin-users/admin-users.ts index 4d5bbf3..f45c012 100644 --- a/src/app/pages/admin/admin-users/admin-users.ts +++ b/src/app/pages/admin/admin-users/admin-users.ts @@ -17,14 +17,7 @@ import { Authorization } from '@services/authorization/authorization'; import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-dialog/admin-reset-password-dialog'; import { FormUtility } from '@utilities/form-utility'; - -function hasValidPasswordComplexity(value: string): boolean { - return /^\S+$/.test(value) - && /[a-z]/.test(value) - && /[A-Z]/.test(value) - && /\d/.test(value) - && /[^A-Za-z0-9]/.test(value); -} +import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation'; @Component({ selector: 'app-admin-users', @@ -95,28 +88,14 @@ export class AdminUsersPage extends FormUtility { }); }); - readonly createPasswordChecks = signal([ - { label: 'At least 8 characters', satisfied: false }, - { label: 'At least one lowercase letter', satisfied: false }, - { label: 'At least one uppercase letter', satisfied: false }, - { label: 'At least one number', satisfied: false }, - { label: 'At least one special character', satisfied: false }, - { label: 'No spaces', satisfied: true } - ]); + readonly createPasswordChecks = signal(initialPasswordChecks()); constructor() { super(); effect(() => { const password = this.createModel().password; - this.createPasswordChecks.set([ - { label: 'At least 8 characters', satisfied: password.length >= 8 }, - { label: 'At least one lowercase letter', satisfied: /[a-z]/.test(password) }, - { label: 'At least one uppercase letter', satisfied: /[A-Z]/.test(password) }, - { label: 'At least one number', satisfied: /\d/.test(password) }, - { label: 'At least one special character', satisfied: /[^A-Za-z0-9]/.test(password) }, - { label: 'No spaces', satisfied: /^\S*$/.test(password) } - ]); + this.createPasswordChecks.set(evaluatePasswordChecks(password)); }); } diff --git a/src/app/pages/auth/signup/signup.ts b/src/app/pages/auth/signup/signup.ts index 90e3db7..ca67c85 100644 --- a/src/app/pages/auth/signup/signup.ts +++ b/src/app/pages/auth/signup/signup.ts @@ -10,14 +10,7 @@ import { environment } from '@environment'; import { UserRegistration } from '@models/user'; import { Authorization } from '@services/authorization/authorization'; import { FormUtility } from '@utilities/form-utility'; - -function hasValidPasswordComplexity(value: string): boolean { - return /^\S+$/.test(value) - && /[a-z]/.test(value) - && /[A-Z]/.test(value) - && /\d/.test(value) - && /[^A-Za-z0-9]/.test(value); -} +import { hasValidPasswordComplexity } from '@utilities/password-validation'; declare global { interface Window { diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts index 119a015..a76e250 100644 --- a/src/app/services/authorization/authorization.ts +++ b/src/app/services/authorization/authorization.ts @@ -11,7 +11,7 @@ import { } from '@models/user'; import { AuthorizationCallServiceBase } from './authorization-call.base'; import { environment } from '@environment'; -import { Observable, take, tap } from 'rxjs'; +import { Observable, catchError, take, tap, throwError } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -31,9 +31,17 @@ export class Authorization { tap(res => { const normalizedUser = this.normalizeUser(res); this.user.set(normalizedUser); - if (typeof localStorage !== 'undefined') { - localStorage.setItem(Authorization.USER_STORAGE_KEY, JSON.stringify(normalizedUser)); + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(Authorization.USER_STORAGE_KEY, JSON.stringify(normalizedUser)); + } + } catch { + // Ignore storage errors (e.g. quota exceeded, private browsing). } + }), + catchError((err) => { + console.error('Login failed', err); + return throwError(() => err); }) ); } @@ -102,8 +110,12 @@ export class Authorization { return this.authCall.logout().pipe( take(1), tap(() => { - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(Authorization.USER_STORAGE_KEY); + try { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(Authorization.USER_STORAGE_KEY); + } + } catch { + // Ignore storage errors. } this.user.set(null); }) diff --git a/src/app/services/containers/containers-call.fake.ts b/src/app/services/containers/containers-call.fake.ts index 1105c6e..c5fa5c9 100644 --- a/src/app/services/containers/containers-call.fake.ts +++ b/src/app/services/containers/containers-call.fake.ts @@ -74,8 +74,10 @@ export class ContainersCallServiceFake extends ContainersCallServiceBase { override createContainer(containerId: string, configuration: any): Observable { const typeName = String(configuration?.typeName ?? configuration?.type ?? 'GenericContainer'); + const configurationType = this.resolveConfigurationType(typeName, configuration); const specificConfiguration = { ...configuration, + type: configurationType, name: typeof configuration?.name === 'string' && configuration.name.length > 0 ? configuration.name : 'Container' @@ -128,4 +130,39 @@ export class ContainersCallServiceFake extends ContainersCallServiceBase { openOutputs: [] }); } + + private resolveConfigurationType(containerType: string, configuration: Record) { + const explicitType = configuration['type']; + if (typeof explicitType === 'string' && explicitType.length > 0) { + return explicitType; + } + + const descriptor = this.containerTypes.find((candidate) => candidate.type === containerType); + const schemaType = this.resolveConfigurationTypeFromSchema(descriptor?.schema); + if (schemaType) return schemaType; + + return descriptor?.configurationType ?? 'GenericContainerConfiguration'; + } + + private resolveConfigurationTypeFromSchema(schema: Record | null | undefined) { + if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return null; + + const properties = schema['properties']; + if (!properties || typeof properties !== 'object' || Array.isArray(properties)) return null; + + const typeProperty = (properties as Record)['type']; + if (!typeProperty || typeof typeProperty !== 'object' || Array.isArray(typeProperty)) return null; + + const typeSchema = typeProperty as Record; + if (typeof typeSchema['default'] === 'string' && typeSchema['default'].length > 0) { + return typeSchema['default']; + } + + const enumValues = Array.isArray(typeSchema['enum']) + ? typeSchema['enum'].filter((value): value is string => typeof value === 'string' && value.length > 0) + : []; + if (enumValues.length === 1) return enumValues[0]; + + return null; + } } diff --git a/src/app/services/containers/containers-call.ts b/src/app/services/containers/containers-call.ts index 5879736..2424441 100644 --- a/src/app/services/containers/containers-call.ts +++ b/src/app/services/containers/containers-call.ts @@ -225,11 +225,46 @@ export class ContainersCallService extends ContainersCallServiceBase { private buildContainerConfigurationPayload(containerType: string, configuration: Record) { const { typeName: _ignoreTypeName, ...sanitized } = configuration; + const configurationType = this.resolveConfigurationType(containerType, sanitized); return { ...sanitized, + type: configurationType, name: typeof sanitized["name"] === "string" && sanitized["name"].length > 0 ? sanitized["name"] : containerType }; } + + private resolveConfigurationType(containerType: string, configuration: Record) { + const explicitType = this.toNullableString(configuration["type"]); + if (explicitType) return explicitType; + + const descriptor = this.containerTypesCache + ?.find((candidate) => candidate.type === containerType); + const schemaType = this.resolveConfigurationTypeFromSchema(descriptor?.schema); + if (schemaType) return schemaType; + + return descriptor?.configurationType ?? "GenericContainerConfiguration"; + } + + private resolveConfigurationTypeFromSchema(schema: Record | null | undefined) { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) return null; + + const properties = schema["properties"]; + if (!properties || typeof properties !== "object" || Array.isArray(properties)) return null; + + const typeProperty = (properties as Record)["type"]; + if (!typeProperty || typeof typeProperty !== "object" || Array.isArray(typeProperty)) return null; + + const typeSchema = typeProperty as Record; + const defaultValue = this.toNullableString(typeSchema["default"]); + if (defaultValue) return defaultValue; + + const enumValues = Array.isArray(typeSchema["enum"]) + ? typeSchema["enum"].filter((value): value is string => typeof value === "string" && value.length > 0) + : []; + if (enumValues.length === 1) return enumValues[0]; + + return null; + } } diff --git a/src/app/services/task-executions/task-executions.ts b/src/app/services/task-executions/task-executions.ts index 9a22578..e85e966 100644 --- a/src/app/services/task-executions/task-executions.ts +++ b/src/app/services/task-executions/task-executions.ts @@ -2,7 +2,7 @@ import { DestroyRef, inject, Injectable, signal } from '@angular/core'; import { environment } from '@environment'; import { LLMDescriptor } from '@models/flow'; import { ExecutionEventLogEntry, getExecutionStatusGroup, TaskExecution } from '@models/task-execution'; -import { catchError, finalize, tap, throwError } from 'rxjs'; +import { catchError, finalize, Observable, tap, throwError } from 'rxjs'; import { TaskExecutionsCallServiceBase } from './task-executions-call.base'; @Injectable({ @@ -43,11 +43,10 @@ export class TaskExecutionsService { } retrieveExecutionEvents(executionId: string) { - return this.taskExecutionsCallService.retrieveExecutionEvents(executionId).pipe( - catchError((err) => { - console.error('Retrieve execution events failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.retrieveExecutionEvents(executionId), + 'Retrieve execution events failed', + false ); } @@ -64,22 +63,16 @@ export class TaskExecutionsService { } deleteExecution(executionId: string) { - return this.taskExecutionsCallService.deleteTaskExecution(executionId).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Delete execution failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.deleteTaskExecution(executionId), + 'Delete execution failed' ); } startExecution(executionId: string) { - return this.taskExecutionsCallService.startTaskExecution(executionId).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Start execution failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.startTaskExecution(executionId), + 'Start execution failed' ); } @@ -92,22 +85,16 @@ export class TaskExecutionsService { return throwError(() => new Error('A simulator descriptor is required to start simulation.')); } - return this.taskExecutionsCallService.simulateTaskExecution(executionId, simulator).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Simulate execution failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.simulateTaskExecution(executionId, simulator), + 'Simulate execution failed' ); } cancelExecution(executionId: string) { - return this.taskExecutionsCallService.cancelTaskExecution(executionId).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Cancel execution failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.cancelTaskExecution(executionId), + 'Cancel execution failed' ); } @@ -118,92 +105,65 @@ export class TaskExecutionsService { return throwError(() => new Error('Resume is only supported for suspended executions.')); } - return this.taskExecutionsCallService.resumeTaskExecution(executionId).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Resume execution failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.resumeTaskExecution(executionId), + 'Resume execution failed' ); } prepareStringInput(executionId: string, nodeId: string, inputName: string, value: string) { - return this.taskExecutionsCallService.prepareStringInput(executionId, nodeId, inputName, value).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare string input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareStringInput(executionId, nodeId, inputName, value), + 'Prepare string input failed' ); } prepareStringArrayInput(executionId: string, nodeId: string, inputName: string, values: string[]) { - return this.taskExecutionsCallService.prepareStringArrayInput(executionId, nodeId, inputName, values).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare string array input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareStringArrayInput(executionId, nodeId, inputName, values), + 'Prepare string array input failed' ); } prepareFileInput(executionId: string, nodeId: string, inputName: string, file: File) { - return this.taskExecutionsCallService.prepareFileInput(executionId, nodeId, inputName, file).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare file input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareFileInput(executionId, nodeId, inputName, file), + 'Prepare file input failed' ); } prepareFileArrayInput(executionId: string, nodeId: string, inputName: string, files: File[]) { - return this.taskExecutionsCallService.prepareFileArrayInput(executionId, nodeId, inputName, files).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare file array input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareFileArrayInput(executionId, nodeId, inputName, files), + 'Prepare file array input failed' ); } prepareGlobalStringInput(executionId: string, inputName: string, value: string) { - return this.taskExecutionsCallService.prepareGlobalStringInput(executionId, inputName, value).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare global string input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareGlobalStringInput(executionId, inputName, value), + 'Prepare global string input failed' ); } prepareGlobalStringArrayInput(executionId: string, inputName: string, values: string[]) { - return this.taskExecutionsCallService.prepareGlobalStringArrayInput(executionId, inputName, values).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare global string array input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareGlobalStringArrayInput(executionId, inputName, values), + 'Prepare global string array input failed' ); } prepareGlobalFileInput(executionId: string, inputName: string, file: File) { - return this.taskExecutionsCallService.prepareGlobalFileInput(executionId, inputName, file).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare global file input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareGlobalFileInput(executionId, inputName, file), + 'Prepare global file input failed' ); } prepareGlobalFileArrayInput(executionId: string, inputName: string, files: File[]) { - return this.taskExecutionsCallService.prepareGlobalFileArrayInput(executionId, inputName, files).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Prepare global file array input failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.prepareGlobalFileArrayInput(executionId, inputName, files), + 'Prepare global file array input failed' ); } @@ -213,22 +173,16 @@ export class TaskExecutionsService { return throwError(() => new Error('Manual interaction is disabled for simulated executions.')); } - return this.taskExecutionsCallService.submitInteractionText(executionId, nodeId, fieldName, value).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Submit interaction text failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.submitInteractionText(executionId, nodeId, fieldName, value), + 'Submit interaction text failed' ); } provideAuthorization(executionId: string, key: string, value: string) { - return this.taskExecutionsCallService.provideAuthorization(executionId, key, value).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Provide authorization failed', err); - return throwError(() => err); - }) + return this.withRefreshAndErrorHandling( + this.taskExecutionsCallService.provideAuthorization(executionId, key, value), + 'Provide authorization failed' ); } @@ -257,4 +211,16 @@ export class TaskExecutionsService { clearInterval(this.pollTimer); this.pollTimer = null; } + + private withRefreshAndErrorHandling(source: Observable, errorMessage: string, refresh = true): Observable { + const piped = refresh + ? source.pipe(tap(() => this.refresh())) + : source; + return piped.pipe( + catchError((err) => { + console.error(errorMessage, err); + return throwError(() => err); + }) + ); + } } diff --git a/src/app/shared/flow-assistant/flow-assistant.ts b/src/app/shared/flow-assistant/flow-assistant.ts index fd01686..c6898ef 100644 --- a/src/app/shared/flow-assistant/flow-assistant.ts +++ b/src/app/shared/flow-assistant/flow-assistant.ts @@ -18,7 +18,7 @@ import { Flow } from '@models/flow'; import { AssistantService } from '@services/assistant/assistant'; import { Authorization } from '@services/authorization/authorization'; import { EditorStateHolder } from '@stores/flow-editor'; -import { finalize, take } from 'rxjs'; +import { finalize, interval, Subscription, switchMap, take } from 'rxjs'; @Component({ selector: 'app-flow-assistant', @@ -31,7 +31,7 @@ export class FlowAssistant implements OnInit, OnDestroy { private readonly assistant = inject(AssistantService); private readonly editorState = inject(EditorStateHolder); private readonly authorization = inject(Authorization); - private pollTick: ReturnType | null = null; + private pollSubscription: Subscription | null = null; readonly assistantConfig = signal(null); readonly sessionState = signal(null); @@ -264,24 +264,22 @@ export class FlowAssistant implements OnInit, OnDestroy { private startPolling(callId: string, sessionId: string) { this.stopPolling(); - this.pollTick = setInterval(() => { - this.assistant.getCall(callId).pipe( - take(1) - ).subscribe({ - next: (callState) => { - this.currentCall.set(callState); - if (callState.status === 'COMPLETED' || callState.status === 'FAILED') { - this.stopPolling(); - void this.reloadSession(sessionId, callState.status === 'FAILED' ? callState.errorMessage : undefined); - } - }, - error: (err) => { - console.error('Assistant call polling failed', err); + this.pollSubscription = interval(500).pipe( + switchMap(() => this.assistant.getCall(callId)) + ).subscribe({ + next: (callState) => { + this.currentCall.set(callState); + if (callState.status === 'COMPLETED' || callState.status === 'FAILED') { this.stopPolling(); - this.pushLocalAssistantMessage('Polling the assistant call failed.'); + void this.reloadSession(sessionId, callState.status === 'FAILED' ? callState.errorMessage : undefined); } - }); - }, 500); + }, + error: (err) => { + console.error('Assistant call polling failed', err); + this.stopPolling(); + this.pushLocalAssistantMessage('Polling the assistant call failed.'); + } + }); } private async reloadSession(sessionId: string, failureMessage?: string) { @@ -378,10 +376,8 @@ export class FlowAssistant implements OnInit, OnDestroy { } private stopPolling() { - if (this.pollTick) { - clearInterval(this.pollTick); - this.pollTick = null; - } + this.pollSubscription?.unsubscribe(); + this.pollSubscription = null; } private phaseText(phase: AssistantCallPhase): string { diff --git a/src/app/shared/ordering/ordering.ts b/src/app/shared/ordering/ordering.ts index 4354322..09feb33 100644 --- a/src/app/shared/ordering/ordering.ts +++ b/src/app/shared/ordering/ordering.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, effect, Input, input, model, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, input, model, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -19,7 +19,7 @@ export type OrderField = { field: string; label?: string }; }) export class Ordering { - @Input({required : true}) orderView!: OrderViewState; + readonly orderView = input.required(); constructor() { effect(() => { @@ -34,9 +34,10 @@ export class Ordering { orderBy = model(null); ngOnInit() { - if (this.orderView.orderBy) { - this.orderBy.set(this.orderView.orderBy); - this.orderDir.set(this.orderView.orderDir); + const view = this.orderView(); + if (view.orderBy) { + this.orderBy.set(view.orderBy); + this.orderDir.set(view.orderDir); } } diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index ae3f7fb..d3876c0 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, effect, ElementRef, HostListener, Injector, input, OnChanges, OnDestroy, output, signal, SimpleChanges, untracked, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, ElementRef, HostListener, Injector, input, OnChanges, OnDestroy, output, signal, SimpleChanges, untracked, viewChild } from '@angular/core'; import { BlockType, FlowData, FlowNode } from '@models/flow'; import { Drag } from 'rete-area-plugin'; import { BlocksService } from '@services/blocks/blocks'; @@ -40,8 +40,8 @@ export class ReteEditor implements OnChanges, OnDestroy { }); } - @ViewChild("editor") container!: ElementRef; - @ViewChild("shell") shell!: ElementRef; + readonly container = viewChild.required('editor'); + readonly shell = viewChild.required>('shell'); private rete?: ReteEditorInstance; private viewReady = false; @@ -158,7 +158,7 @@ export class ReteEditor implements OnChanges, OnDestroy { this.selectionPointerId = event.pointerId; this.selectionStart = { x: event.clientX, y: event.clientY }; this.selectionBox.set({ left: 0, top: 0, width: 0, height: 0 }); - this.shell.nativeElement.setPointerCapture(event.pointerId); + this.shell().nativeElement.setPointerCapture(event.pointerId); } setEditorMode(mode: 'standard' | 'select', event?: Event) { @@ -189,7 +189,7 @@ export class ReteEditor implements OnChanges, OnDestroy { onShellPointerMove(event: PointerEvent) { if (this.selectionPointerId !== event.pointerId || !this.selectionStart) return; - const shellRect = this.shell.nativeElement.getBoundingClientRect(); + const shellRect = this.shell().nativeElement.getBoundingClientRect(); const left = Math.min(this.selectionStart.x, event.clientX) - shellRect.left; const top = Math.min(this.selectionStart.y, event.clientY) - shellRect.top; const width = Math.abs(event.clientX - this.selectionStart.x); @@ -201,7 +201,7 @@ export class ReteEditor implements OnChanges, OnDestroy { onShellPointerUp(event: PointerEvent) { if (this.selectionPointerId !== event.pointerId || !this.selectionStart) return; - const shell = this.shell.nativeElement; + const shell = this.shell().nativeElement; if (shell.hasPointerCapture(event.pointerId)) { shell.releasePointerCapture(event.pointerId); } @@ -278,7 +278,7 @@ export class ReteEditor implements OnChanges, OnDestroy { } private async reloadEditor() { - const host = this.container?.nativeElement as HTMLElement | undefined; + const host = this.container()?.nativeElement as HTMLElement | undefined; if (!host) return; await this.ensureNodeTypesLoaded(); @@ -495,7 +495,7 @@ export class ReteEditor implements OnChanges, OnDestroy { } private getDropPosition(event: DragEvent) { - const host = this.container.nativeElement as HTMLElement; + const host = this.container().nativeElement as HTMLElement; const rect = host.getBoundingClientRect(); const transform = this.rete!.area.area.transform; @@ -555,7 +555,7 @@ export class ReteEditor implements OnChanges, OnDestroy { const selection = this.selectionBox(); if (!selection) return []; - const shellRect = this.shell.nativeElement.getBoundingClientRect(); + const shellRect = this.shell().nativeElement.getBoundingClientRect(); const selectionRect = { left: shellRect.left + selection.left, top: shellRect.top + selection.top, @@ -564,7 +564,7 @@ export class ReteEditor implements OnChanges, OnDestroy { }; return Array.from( - this.shell.nativeElement.querySelectorAll('[data-testid="node"][data-block-id]') + this.shell().nativeElement.querySelectorAll('[data-testid="node"][data-block-id]') ) .filter((element) => this.isRectIntersecting(selectionRect, element.getBoundingClientRect())) .map((element) => element.dataset['blockId'] ?? '') @@ -585,7 +585,7 @@ export class ReteEditor implements OnChanges, OnDestroy { private async applyZoom(multiplier: number) { const area = this.rete?.area?.area; - const host = this.container?.nativeElement as HTMLElement | undefined; + const host = this.container()?.nativeElement as HTMLElement | undefined; if (!area || !host) return; const currentZoom = area.transform.k || 1; diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html index d8e6ed8..af2d899 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.html +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html @@ -111,7 +111,7 @@ @if (contextAsideOpen()) { -