Align flow save API integration and add editor save/create feedback

This commit is contained in:
Lucio Lelii 2026-03-09 11:37:01 +01:00
parent a2f7152202
commit e13e06d858
10 changed files with 88 additions and 31 deletions

View File

@ -34,9 +34,15 @@
@switch (open) {
@case ('flows') {
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
@if (!creatingFlow()) {
<i ngProjectAs="header-button"
class="bi bi-plus-circle-fill text-blue-600 hover:cursor-pointer hover:text-blue-400 text-lg"
(click)="createNewFlow()"></i>
} @else {
<span ngProjectAs="header-button" class="spinner-border spinner-border-sm text-blue-600" role="status">
<span class="visually-hidden">Creating flow...</span>
</span>
}
<app-flows-list></app-flows-list>
</app-group-holder>
}

View File

@ -6,6 +6,7 @@ import { EditorStateHolder } from '@stores/flow-editor';
import { CommonModule } from '@angular/common';
import { FlowsService } from '@services/flows/flows';
import { ListState } from '@stores/list-state';
import { finalize } from 'rxjs';
type OpenedId = 'flows' | 'blocks';
@ -25,6 +26,7 @@ export class EditorSidebar {
blockDisabled = computed(() => !this.flowState.hasFlow());
collapsed = signal(true);
creatingFlow = signal(false);
open: OpenedId | null = null;
@ -40,16 +42,20 @@ export class EditorSidebar {
}
createNewFlow() {
if (this.creatingFlow()) return;
this.creatingFlow.set(true);
console.log('Creating new flow...');
this.flowService.createNewFlow().subscribe({
next: flow => {
console.log('New flow created:', flow);
this.flowState.openDocument(flow);
},
error: err => {
console.error('Error creating new flow:', err);
},
});
this.flowService.createNewFlow().pipe(
finalize(() => this.creatingFlow.set(false))
).subscribe({
next: flow => {
console.log('New flow created:', flow);
this.flowState.openDocument(flow);
},
error: err => {
console.error('Error creating new flow:', err);
},
});
}
createNewBlock() {

View File

@ -44,13 +44,3 @@ export function toFlowCreateRequest(name: string, description?: string, flow?: F
}
};
}
export function toFlowUpdateRequest(flow: Flow) {
return {
name: flow.name,
description: flow.description,
published: flow.published ?? flow.visibility === 'PUBLIC',
finalized: flow.finalized ?? false,
flow: flow.data
};
}

View File

@ -7,10 +7,10 @@ export abstract class FlowsCallServiceBase {
abstract retrieveAllFlows() : Observable<Flow[]>;
abstract updateFlow(flow: Flow) : Observable<void>;
abstract updateFlow(flow: Flow) : Observable<Flow>;
abstract createNewFlow(name?: string) : Observable<Flow>;
abstract deleteFlow(flowId: string) : Observable<void>;
}
}

View File

@ -28,7 +28,7 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
override updateFlow(flow: Flow) {
this.data[flow.id] = flow;
return of(void 0);
return of(flow);
}
override createNewFlow(name?: string): Observable<Flow> {

View File

@ -3,7 +3,7 @@ import { inject } from '@angular/core';
import { environment } from '@environment';
import { Flow } from '@models/flow';
import { map, Observable } from 'rxjs';
import { flowFromApi, toFlowCreateRequest, toFlowUpdateRequest } from './flow-mapper';
import { flowFromApi, toFlowCreateRequest } from './flow-mapper';
import { FlowsCallServiceBase } from './flows-call.base';
export class FlowsCallService extends FlowsCallServiceBase {
@ -33,8 +33,13 @@ export class FlowsCallService extends FlowsCallServiceBase {
.pipe(map((raw) => raw.map((flow) => flowFromApi(flow))));
}
override updateFlow(flow: Flow): Observable<void> {
override updateFlow(flow: Flow): Observable<Flow> {
const encodedId = encodeURIComponent(flow.id);
return this.http.put<void>(`${environment.apiUrl}/flows/${encodedId}`, toFlowUpdateRequest(flow));
return this.http
.put<unknown>(
`${environment.apiUrl}/flows/${encodedId}`,
toFlowCreateRequest(flow.name, flow.description, flow.data)
)
.pipe(map((raw) => flowFromApi(raw)));
}
}

View File

@ -52,7 +52,7 @@ export class FlowsService {
);
}
cloneFlow(flowId: string): Observable<void> {
cloneFlow(flowId: string): Observable<Flow> {
const originalFlow$ = this.flowsCallService.getFlowById(flowId);
const newFlow$ = this.flowsCallService.createNewFlow();

View File

@ -0,0 +1,17 @@
.save-snackbar {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 2000;
padding: 10px 14px;
border-radius: 8px;
background: #15803d;
color: #ffffff;
font-size: 13px;
font-weight: 500;
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.22);
}
.save-snackbar-error {
background: #b91c1c;
}

View File

@ -69,3 +69,9 @@
}
</div>
}
@if (snackbarMessage()) {
<div class="save-snackbar" [class.save-snackbar-error]="snackbarType() === 'error'">
{{ snackbarMessage() }}
</div>
}

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, computed, ElementRef, inject, ViewChild } from '@angular/core';
import { Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
import { take } from 'rxjs';
import { EditorStateHolder } from '@stores/flow-editor';
@Component({
@ -9,6 +10,7 @@ import { EditorStateHolder } from '@stores/flow-editor';
styleUrl: './title-toolbar.css',
})
export class TitleToolbar {
private snackTimeout: ReturnType<typeof setTimeout> | null = null;
@ViewChild('titleInput') myInputRef!: ElementRef;
@ -20,6 +22,8 @@ export class TitleToolbar {
});
notSaved = computed(() => this.editorState.isDirty());
snackbarMessage = signal<string | null>(null);
snackbarType = signal<'success' | 'error'>('success');
changeTitle(value: string) {
@ -35,9 +39,18 @@ export class TitleToolbar {
save() {
if (!this.notSaved()) return;
this.editorState.save().subscribe(
err => console.error('Save failed', err)
);
this.editorState.save().pipe(
take(1)
).subscribe({
next: () => {
console.log('Flow saved');
this.showSnackbar('Flow saved', 'success');
},
error: err => {
console.error('Save failed', err);
this.showSnackbar('Errore durante il salvataggio', 'error');
}
});
}
undo() {
@ -46,5 +59,19 @@ export class TitleToolbar {
redo() {
this.editorState.redo();
}
}
private showSnackbar(message: string, type: 'success' | 'error') {
this.snackbarMessage.set(message);
this.snackbarType.set(type);
if (this.snackTimeout) {
clearTimeout(this.snackTimeout);
}
this.snackTimeout = setTimeout(() => {
this.snackbarMessage.set(null);
this.snackTimeout = null;
}, 2500);
}
}