solved problem on container import

This commit is contained in:
Lucio Lelii 2026-04-10 10:08:17 +02:00
parent f57f5be073
commit 5f405b69a0
14 changed files with 234 additions and 204 deletions

View File

@ -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));
});
}

View File

@ -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));
});
}

View File

@ -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 {

View File

@ -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);
})

View File

@ -74,8 +74,10 @@ export class ContainersCallServiceFake extends ContainersCallServiceBase {
override createContainer(containerId: string, configuration: any): Observable<FlowContainer> {
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<string, unknown>) {
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<string, unknown> | 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<string, unknown>)['type'];
if (!typeProperty || typeof typeProperty !== 'object' || Array.isArray(typeProperty)) return null;
const typeSchema = typeProperty as Record<string, unknown>;
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;
}
}

View File

@ -225,11 +225,46 @@ export class ContainersCallService extends ContainersCallServiceBase {
private buildContainerConfigurationPayload(containerType: string, configuration: Record<string, unknown>) {
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<string, unknown>) {
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<string, unknown> | 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<string, unknown>)["type"];
if (!typeProperty || typeof typeProperty !== "object" || Array.isArray(typeProperty)) return null;
const typeSchema = typeProperty as Record<string, unknown>;
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;
}
}

View File

@ -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<T>(source: Observable<T>, errorMessage: string, refresh = true): Observable<T> {
const piped = refresh
? source.pipe(tap(() => this.refresh()))
: source;
return piped.pipe(
catchError((err) => {
console.error(errorMessage, err);
return throwError(() => err);
})
);
}
}

View File

@ -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<typeof setInterval> | null = null;
private pollSubscription: Subscription | null = null;
readonly assistantConfig = signal<AssistantConfig | null>(null);
readonly sessionState = signal<AssistantSessionState | null>(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 {

View File

@ -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<OrderViewState>();
constructor() {
effect(() => {
@ -34,9 +34,10 @@ export class Ordering {
orderBy = model<string | null>(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);
}
}

View File

@ -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<HTMLElement>;
readonly container = viewChild.required<ElementRef>('editor');
readonly shell = viewChild.required<ElementRef<HTMLElement>>('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<HTMLElement>('[data-testid="node"][data-block-id]')
this.shell().nativeElement.querySelectorAll<HTMLElement>('[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;

View File

@ -111,7 +111,7 @@
</button>
@if (contextAsideOpen()) {
<aside class="w-[360px] border border-slate-200 rounded-md bg-white min-h-0 overflow-hidden flex flex-col">
<aside class="w-90 border border-slate-200 rounded-md bg-white min-h-0 overflow-hidden flex flex-col">
<div class="flex border-b border-slate-200 bg-slate-50 px-2 pt-2">
<button
type="button"

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, input, OnDestroy, signal, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, input, OnDestroy, signal, viewChild } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
@ -96,7 +96,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
readonly executionLogs = signal<ExecutionEventLogEntry[]>([]);
readonly logsLoading = signal(false);
readonly logsError = signal<string | null>(null);
@ViewChild('logsScrollViewport') private logsScrollViewport?: ElementRef<HTMLDivElement>;
private readonly logsScrollViewport = viewChild<ElementRef<HTMLDivElement>>('logsScrollViewport');
constructor() {
effect(() => {
@ -821,7 +821,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
}
private scrollLogsToBottom() {
const element = this.logsScrollViewport?.nativeElement;
const element = this.logsScrollViewport()?.nativeElement;
if (!element || this.activeAsideTab() !== 'logs') return;
element.scrollTop = element.scrollHeight;
}

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
@ -26,7 +26,7 @@ import { EditorStateHolder } from '@stores/flow-editor';
export class TitleToolbar {
private snackTimeout: ReturnType<typeof setTimeout> | null = null;
@ViewChild('titleInput') myInputRef!: ElementRef;
readonly titleInputRef = viewChild<ElementRef>('titleInput');
editorState: EditorStateHolder = inject(EditorStateHolder);
private router = inject(Router);
@ -98,8 +98,8 @@ export class TitleToolbar {
this.draftTitle.set(flow.name);
this.editingTitle.set(true);
queueMicrotask(() => {
this.myInputRef?.nativeElement?.focus();
this.myInputRef?.nativeElement?.select();
this.titleInputRef()?.nativeElement?.focus();
this.titleInputRef()?.nativeElement?.select();
});
}
@ -117,7 +117,8 @@ export class TitleToolbar {
}
if (trimmed.length < 4) {
this.draftTitle.set(this.title());
this.myInputRef.nativeElement.value = this.title();
const inputEl = this.titleInputRef()?.nativeElement;
if (inputEl) inputEl.value = this.title();
this.editingTitle.set(false);
return;
}

View File

@ -0,0 +1,31 @@
export 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);
}
export type PasswordCheck = { label: string; satisfied: boolean };
export function evaluatePasswordChecks(password: string): PasswordCheck[] {
return [
{ 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) }
];
}
export function initialPasswordChecks(): PasswordCheck[] {
return [
{ 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 }
];
}