added flow service

This commit is contained in:
Lucio Lelii 2026-01-12 17:23:44 +01:00
parent fbd34a0882
commit 0fbe49f0f3
28 changed files with 535 additions and 115 deletions

91
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@angular/common": "^21.0.4",
"@angular/compiler": "^21.0.4",
"@angular/core": "^21.0.4",
"@angular/elements": "^21.0.8",
"@angular/forms": "^21.0.4",
"@angular/platform-browser": "^21.0.4",
"@angular/platform-browser-dynamic": "^21.0.4",
@ -22,6 +23,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-angular": "^0.561.0",
"rete-angular-plugin": "^2.6.0",
"rete-render-utils": "^2.0.3",
"rxjs": "~7.8.0",
"tailwind-merge": "^3.4.0",
"tslib": "^2.3.0"
@ -597,6 +600,22 @@
}
}
},
"node_modules/@angular/elements": {
"version": "21.0.8",
"resolved": "https://registry.npmjs.org/@angular/elements/-/elements-21.0.8.tgz",
"integrity": "sha512-eOvxX5eQw5u5Dp1UT6hhv7OSH6P/xzWb4MwiKS8fZhAJs0CNY3WtJfKTJCFJ3vwIk0zIoqz52OV1lrT6fuozHQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.0.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/forms": {
"version": "21.0.5",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.5.tgz",
@ -1001,6 +1020,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@ -8207,6 +8235,62 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rete": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/rete/-/rete-2.0.6.tgz",
"integrity": "sha512-kPmlKCGFES2VWtY7Y7SCB8ZeXRMsgX5deza9cu4OwmfM/ZUimd461kC3hRyccoyVxE4POlHUx0gg2jcGfusHFg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
}
},
"node_modules/rete-angular-plugin": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/rete-angular-plugin/-/rete-angular-plugin-2.6.0.tgz",
"integrity": "sha512-0GdYJbuo2V6mmN1ylzW/WUOetncAPWmZPpj+l2yw9sPUPpr/QtfBnXxUXje4cnY1ws7foq2gYeWsNnoClutUSw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">= 12 < 22",
"@angular/core": ">= 12 < 22",
"@angular/elements": ">= 12 < 22",
"rete": "^2.0.1",
"rete-area-plugin": "^2.0.0",
"rete-render-utils": "^2.0.0",
"rxjs": ">=6.6.0",
"zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0 || ~0.14.0 || ~0.15.0 || ~0.16.0"
}
},
"node_modules/rete-area-plugin": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/rete-area-plugin/-/rete-area-plugin-2.1.5.tgz",
"integrity": "sha512-iquEvwkQlcsO4cmgM3Z37TG0AWaE536dfA+lCJAze5YJzVx4RBaViUCqdB4dUA/utSytpBCkiDC4D3ztM9akGQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"peerDependencies": {
"rete": "^2.0.0"
}
},
"node_modules/rete-render-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/rete-render-utils/-/rete-render-utils-2.0.3.tgz",
"integrity": "sha512-Oz4W2PNayHocRvlzadb5BCNf+tDzJ8RhTwB3ucBPCdCLKZ974wWDiTSCRfA287L2hmHVzRfBdyAwC03K9eP+4g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"peerDependencies": {
"rete": "^2.0.0",
"rete-area-plugin": "^2.0.0"
}
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -10155,6 +10239,13 @@
"peerDependencies": {
"zod": "^3.25 || ^4"
}
},
"node_modules/zone.js": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
"license": "MIT",
"peer": true
}
}
}

View File

@ -28,6 +28,7 @@
"@angular/common": "^21.0.4",
"@angular/compiler": "^21.0.4",
"@angular/core": "^21.0.4",
"@angular/elements": "^21.0.8",
"@angular/forms": "^21.0.4",
"@angular/platform-browser": "^21.0.4",
"@angular/platform-browser-dynamic": "^21.0.4",
@ -37,6 +38,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-angular": "^0.561.0",
"rete-angular-plugin": "^2.6.0",
"rete-render-utils": "^2.0.3",
"rxjs": "~7.8.0",
"tailwind-merge": "^3.4.0",
"tslib": "^2.3.0"

View File

@ -7,15 +7,13 @@
<div class="flex flex-1 overflow-hidden">
<!-- LEFT SIDEBAR -->
<div class="w-lg border-r bg-white shadow-sm p-2 overflow-auto">
<app-editor-sidebar></app-editor-sidebar>
</div>
<app-editor-sidebar></app-editor-sidebar>
<!-- CENTER EDITOR -->
<main class="flex-1 bg-gray-100 overflow-auto">
@if (flow()) {
<div>
<h2 class="text-sm font-semibold">{{ flow()?.name }}</h2>
<app-title-toolbar></app-title-toolbar>
<!-- editor canvas -->
</div>
} @else {

View File

@ -2,10 +2,11 @@ import { Component, inject } from '@angular/core';
import { Flow } from '@models/flow';
import { EditorSidebar } from "@pages/main/editor-sidebar/editor-sidebar";
import { EditorStateHolder } from 'app/stores/editor';
import { TitleToolbar } from "@shared/title-toolbar/title-toolbar";
@Component({
selector: 'app-flow-editor',
imports: [EditorSidebar],
imports: [EditorSidebar, TitleToolbar],
templateUrl: './flow-editor.html',
styleUrl: './flow-editor.css',
})

View File

@ -0,0 +1,3 @@
.enabled:hover {
background-color: var(--color-gray-100) ;
}

View File

@ -1,13 +1,55 @@
<app-accordion title="Flows" icon="bi-lightning-charge-fill" [open]="true" [open]="openAccordion === 'flows'"
(toggle)="toggle('flows')">
<app-flows-list></app-flows-list>
</app-accordion>
<aside
class="relative h-full
bg-white border-r border-gray-200
transition-all duration-300
overflow-hidden"
[class.w-72]="!collapsed()"
[class.w-14]="collapsed()">
@if (!collapsed()) {
<button
(click)="collapse()"
class="absolute top-3 right-3
w-6 h-6 rounded-full
bg-white border shadow
flex items-center justify-center
hover:bg-gray-50 z-10">
<i class="bi bi-chevron-left text-xs transition-transform"></i>
</button>
}
<!-- CONTENT -->
<div class="h-full p-4">
@if(!collapsed()) {
<div class="pt-6 space-y-6">
<!-- ACCORDION -->
@switch (open) {
@case ('flows') {
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
<app-flows-list></app-flows-list>
</app-group-holder>
}
@case ('blocks') {
<app-group-holder title="Blocks" icon="bi-boxes">
<div class="space-y-2">
<div class="block-item">Input</div>
<div class="block-item">Transform</div>
<div class="block-item">Output</div>
</div>
</app-group-holder>
}
}
</div>
} @else {
<!-- ICON MODE -->
<div class="flex flex-col gap-4 items-center pt-6">
<i class="bi bi-lightning-charge-fill text-indigo-500 text-xl cursor-pointer enabled p-2" (click)="openSide('flows')"></i>
<i [ngClass]="{'cursor-pointer text-gray-600 enabled' : !blockDisabled()}"
class="bi bi-boxes text-gray-200 text-xl p-2" (click)="!blockDisabled() && openSide('blocks')"></i>
</div>
}
<app-accordion title="Blocks" icon="bi-boxes" [open]="openAccordion === 'blocks'"
(toggle)="toggle('blocks')">
<div class="space-y-2">
<div class="block-item">Input</div>
<div class="block-item">Transform</div>
<div class="block-item">Output</div>
</div>
</app-accordion>
</aside>

View File

@ -1,26 +1,35 @@
import { Component } from '@angular/core';
import { Accordion } from '@shared/accordion/accordion';
import { Component, computed, inject, signal } from '@angular/core';
import { GroupHolder } from '@shared/group-holder/group-holder';
import { FlowsList } from '@shared/flows-list/flows-list';
import { EditorStateHolder } from 'app/stores/editor';
import { CommonModule } from '@angular/common';
type AccordionId = 'flows' | 'blocks';
type OpenedId = 'flows' | 'blocks';
@Component({
selector: 'app-editor-sidebar',
imports: [Accordion, FlowsList],
imports: [GroupHolder, FlowsList, CommonModule],
templateUrl: './editor-sidebar.html',
styleUrl: './editor-sidebar.css',
})
export class EditorSidebar {
collapsed = false;
flowState = inject(EditorStateHolder);
openAccordion: AccordionId | null = null;
blockDisabled = computed(() => !this.flowState.hasFlow());
collapsed = signal(true);
open: OpenedId | null = null;
openSide(id: OpenedId) {
this.open = id;
this.collapsed.set(false);
}
collapse() {
this.collapsed = !this.collapsed;
}
toggle(id: AccordionId) {
this.openAccordion = this.openAccordion === id ? null : id;
this.collapsed.set(true);
}
}

View File

@ -0,0 +1,10 @@
import { Flow } from "@models/flow";
import { Observable } from "rxjs";
export abstract class FlowsCallServiceBase {
abstract retrieveAllFlows() : Observable<Flow[]>;
abstract updateFlow(flow: Flow) : Observable<void>;
}

View File

@ -0,0 +1,20 @@
import { Flow } from "@models/flow";
import { FlowsCallServiceBase } from "./flows-call.base";
import { BehaviorSubject, of } from "rxjs";
export class FlowsCallServiceFake extends FlowsCallServiceBase {
private data: Record<string, Flow> = {
'1': { id: '1', name: 'Test Flow 1', data: {}, visibility: 'public' },
'2': { id: '2', name: 'Test Flow 2', data: {}, visibility: 'private' },
};
retrieveAllFlows() {
return of(Object.values(this.data));
}
updateFlow(flow: Flow) {
this.data[flow.id] = flow;
return of(void 0);
}
}

View File

@ -0,0 +1,13 @@
import { Flow } from "@models/flow";
import { FlowsCallServiceBase } from "./flows-call.base";
import { Observable } from "rxjs";
export class FlowsCallService extends FlowsCallServiceBase {
override retrieveAllFlows(): Observable<Flow[]> {
throw new Error("Method not implemented.");
}
override updateFlow(flow: Flow): Observable<void> {
throw new Error("Method not implemented.");
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { Flows } from './flows';
describe('Flows', () => {
let service: Flows;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(Flows);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,45 @@
import { Injectable, signal } from '@angular/core';
import { environment } from '@environment';
import { Flow } from '@models/flow';
import { FlowsCallServiceBase } from './flows-call.base';
import { BehaviorSubject, catchError, tap, throwError } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class FlowsService {
flowsCallService: FlowsCallServiceBase = new environment.flowsCallService();
toInit: boolean = true;
private _flows = signal<Flow[]>([]);
async getAllFlows() {
console.log('Flows signal accessed');
if (this.toInit) {
this.refresh();
this.toInit = false;
}
return this._flows.asReadonly();
}
refresh() {
this.flowsCallService.retrieveAllFlows().subscribe(flows => {
this._flows.set(flows);
});
}
updateFlow(flow: Flow) {
return this.flowsCallService.updateFlow(flow).pipe(
tap(() => this.refresh()),
catchError(err => {
console.error('Update flow failed', err);
return throwError(() => err);
})
);
}
}

View File

@ -1,38 +0,0 @@
<div
class="rounded-xl border border-gray-200
bg-white shadow-sm
transition">
<!-- Header -->
<button
type="button"
(click)="toggle.emit()"
class="w-full flex items-center justify-between
px-4 py-3
text-sm font-semibold text-gray-700
hover:bg-gray-50">
<div class="flex items-center gap-2">
<i class="bi {{ icon }} text-indigo-500"></i>
{{ title }}
</div>
<i
class="bi bi-chevron-down
transition-transform duration-200"
[class.rotate-180]="open()">
</i>
</button>
<!-- Body -->
<div
class="overflow-hidden transition-all duration-300"
[class.max-h-0]="!open()"
[class.max-h-96]="open()">
<div class="p-3 space-y-2">
<ng-content></ng-content>
</div>
</div>
</div>

View File

@ -1,14 +0,0 @@
import { Component, EventEmitter, input, Input, Output } from '@angular/core';
@Component({
selector: 'app-accordion',
imports: [],
templateUrl: './accordion.html',
styleUrl: './accordion.css',
})
export class Accordion {
@Input() title = '';
@Input() icon = '';
open = input<boolean>(false);
@Output() toggle = new EventEmitter<void>();
}

View File

@ -1,6 +1,6 @@
@if(!!this.state()) {
<div class="fixed inset-0 z-50 flex items-center justify-center">
<div class="fixed inset-0 z-9999 flex items-center justify-center">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/40" (click)="close(false)">

View File

@ -1,29 +1,36 @@
@if (loading()) {
<div class="flex items-center justify-center h-32">
<div class="spinner-border text-indigo-500" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
} @else if (filteredFlows.length === 0) {
<div class="btn-group w-full mb-3" role="group">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter === 'all'"
[class.btn-outline-primary]="filter !== 'all'" (click)="filter = 'all'">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'all'"
[class.btn-outline-primary]="filter() !== 'all'" (click)="filter.set('all')">
All
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter === 'public'"
[class.btn-outline-primary]="filter !== 'public'" (click)="filter = 'public'">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'public'"
[class.btn-outline-primary]="filter() !== 'public'" (click)="filter.set('public')">
Public
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter === 'private'"
[class.btn-outline-primary]="filter !== 'private'" (click)="filter = 'private'">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'private'"
[class.btn-outline-primary]="filter() !== 'private'" (click)="filter.set('private')">
Private
</button>
</div>
<div class="space-y-2">
@for (flow of filteredFlows; track flow.id) {
@for (flow of filteredFlows(); track flow.id) {
<div class="group rounded-lg border
px-3 py-2 cursor-pointer
transition
hover:shadow-[0_0_12px_rgba(99,102,241,0.6)]
bg-indigo-50 border-indigo-200" (click)="open(flow)">
bg-indigo-50 border-indigo-200 select-none" (dblclick)="open(flow)">
<div class="flex items-center justify-between text-sm">
@ -45,4 +52,5 @@
</div>
}
</div>
</div>
}

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
import { Flow } from '@models/flow';
import { FlowsService } from '@services/flows/flows';
import { EditorStateHolder } from 'app/stores/editor';
type FlowFilter = 'all' | 'public' | 'private';
@ -12,21 +13,27 @@ type FlowFilter = 'all' | 'public' | 'private';
styleUrl: './flows-list.css',
})
export class FlowsList {
filter: FlowFilter = 'all';
filter = signal<FlowFilter>('all');
private editorState = inject(EditorStateHolder);
private flowsService = inject(FlowsService);
flows : Flow[] = [
{ id: '1', name: 'Flow One', visibility: 'public', data: {} },
{ id: '2', name: 'Flow Two', visibility: 'private', data: {} },
{ id: '3', name: 'Flow Three', visibility: 'public', data: {} },
{ id: '4', name: 'Flow Four', visibility: 'private', data: {} },
]
loading: WritableSignal<boolean> = signal(true);
get filteredFlows() {
if (this.filter === 'all') return this.flows;
return this.flows.filter(f => f.visibility === this.filter);
flows: Signal<Flow[]> | undefined;
ngOnInit() {
this.flowsService.getAllFlows().then(flowsSignal => {
this.flows = flowsSignal;
this.loading.set(false);
});
}
filteredFlows = computed(() => {
if (!this.flows) return [];
if (this.filter() === 'all') return this.flows();
return this.flows().filter(f => f.visibility === this.filter());
});
open(flow: Flow) {
console.log('Opening flow:', flow);

View File

@ -0,0 +1,29 @@
<div
class="rounded-xl border border-gray-200
bg-white shadow-sm
transition overflow-hidden">
<!-- Header -->
<div
class="w-full flex items-center justify-between
px-4 py-3
text-sm font-semibold text-gray-700
bg-gray-50 "
>
<div class="flex items-center gap-2">
<i class="bi {{ icon }} text-lg"></i>
<span>{{ title }}</span>
</div>
</div>
<!-- Body -->
<div
class="overflow-hidden transition-all duration-300 max-h-96"
>
<div class="p-3 space-y-2">
<ng-content></ng-content>
</div>
</div>
</div>

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Accordion } from './accordion';
import { GroupHolder } from './group-holder';
describe('Accordion', () => {
let component: Accordion;
let fixture: ComponentFixture<Accordion>;
describe('GroupHolder', () => {
let component: GroupHolder;
let fixture: ComponentFixture<GroupHolder>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Accordion]
imports: [GroupHolder]
})
.compileComponents();
fixture = TestBed.createComponent(Accordion);
fixture = TestBed.createComponent(GroupHolder);
component = fixture.componentInstance;
await fixture.whenStable();
});

View File

@ -0,0 +1,12 @@
import { Component, EventEmitter, input, Input, Output } from '@angular/core';
@Component({
selector: 'app-group-holder',
imports: [],
templateUrl: './group-holder.html',
styleUrl: './group-holder.css',
})
export class GroupHolder {
@Input() title = '';
@Input() icon = '';
}

View File

@ -0,0 +1,67 @@
<div class="flex items-center justify-between
px-4 py-2
bg-white border-b border-gray-200
shadow-sm">
<!-- Title -->
<div class="flex items-center gap-2">
<i class="bi bi-pencil-square text-indigo-500"></i>
<h2 class="m-0 text-sm font-semibold text-gray-800">
<input type="text" class="border-0 text-sm font-semibold text-gray-800
focus:ring-0 p-0 m-0 box-border
min-w-[4ch] max-w-[16ch]
bg-transparent"
[value]="title()"
(blur)="changeTitle(titleInput.value)"
(keydown.enter)="changeTitle(titleInput.value); $event.target.blur()"
[style.width.ch]="titleWidth()"
maxlength="20"
size="20"
(input)="tempTitle.set(titleInput.value)"
#titleInput
/>
</h2>
@if (unsaved()) {
<span class="inline-flex items-center gap-x-1.5 py-1.5 px-3 rounded-full text-xs font-medium border border-yellow-500 text-yellow-500">not saved</span>
}
</div>
<!-- Actions -->
<div class="flex items-center gap-2" >
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-green-200 text-green-600
p-1"
title="Save" (click)="save()">
<i class="bi bi-save"></i>
</button>
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-gray-100 text-gray-600 p-1"
title="Undo">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-gray-100 text-gray-600 p-1"
title="Redo">
<i class="bi bi-arrow-clockwise"></i>
</button>
<div class="w-px h-5 bg-gray-200 mx-2"></div>
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-red-200 text-red-600 p-1"
title="Clear">
<i class="bi bi-trash"></i>
</button>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TitleToolbar } from './title-toolbar';
describe('TitleToolbar', () => {
let component: TitleToolbar;
let fixture: ComponentFixture<TitleToolbar>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TitleToolbar]
})
.compileComponents();
fixture = TestBed.createComponent(TitleToolbar);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,53 @@
import { Component, computed, ElementRef, inject, linkedSignal, ViewChild, WritableSignal } from '@angular/core';
import { EditorStateHolder } from 'app/stores/editor';
@Component({
selector: 'app-title-toolbar',
imports: [],
templateUrl: './title-toolbar.html',
styleUrl: './title-toolbar.css',
})
export class TitleToolbar {
@ViewChild('titleInput') myInputRef!: ElementRef;
private editorState: EditorStateHolder = inject(EditorStateHolder);
title = computed(() => {
const flow = this.editorState.currentFlow();
return flow ? flow.name : 'No Flow Opened';
});
tempTitle: WritableSignal<string | null> = linkedSignal(() =>null);
titleWidth = computed(() => {
const temp = this.tempTitle();
if (temp !== null) {
return Math.max(temp.length-1, 4)
}
const len = this.title().length || 4;
return Math.min(len, 20)-2;
});
unsaved = computed(() => this.editorState.isDirty());
changeTitle(value: string) {
this.tempTitle.set(null);
const trimmed = value.trim();
if (value === this.title()) return;
if (trimmed.length < 4) {
this.myInputRef.nativeElement.value = this.title();
return;
}
console.log('Updating flow title to:', trimmed);
const flow = this.editorState.currentFlow()!;
this.editorState.updateFlow({ ...flow, name: trimmed });
}
save() {
this.editorState.save().subscribe(
err => console.error('Save failed', err)
);
}
}

View File

@ -1,6 +1,9 @@
import { computed, Injectable, signal } from '@angular/core';
import { computed, inject, Injectable, signal } from '@angular/core';
import { Flow } from '@models/flow';
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
import { FlowsService } from '@services/flows/flows';
import { FlowsCallService } from '@services/flows/flows-call';
import { tap } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class EditorStateHolder {
@ -12,16 +15,20 @@ export class EditorStateHolder {
/** Derived state */
readonly hasFlow = computed(() => !!this.currentFlow());
flowsService: FlowsService = inject(FlowsService);
constructor(private confirm: ConfirmDialogService) { }
/** Intent: open document */
async openDocument(doc: Flow): Promise<boolean> {
const confirmed = await this.confirm.open(
'You have unsaved changes. Open another document?'
);
if (!confirmed) return false;
if (this.isDirty()) {
const confirmed = await this.confirm.open(
'You have unsaved changes. Open another document?'
);
if (!confirmed) return false;
}
this.currentFlow.set(doc);
this.isDirty.set(false);
@ -29,12 +36,12 @@ export class EditorStateHolder {
}
/** Intent: mark editor dirty */
markDirty() {
private markDirty() {
this.isDirty.set(true);
}
/** Intent: save document */
markSaved() {
private markSaved() {
this.isDirty.set(false);
}
@ -43,4 +50,15 @@ export class EditorStateHolder {
this.currentFlow.set(null);
this.isDirty.set(false);
}
updateFlow(updated: Flow) {
this.currentFlow.set(updated);
this.markDirty();
}
save() {
return this.flowsService.updateFlow(this.currentFlow()!).pipe(
tap(() => this.markSaved())
)
}
}

View File

@ -1,7 +1,9 @@
import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake";
import { FlowsCallServiceFake } from "@services/flows/flows-call.fake";
export const environment = {
production: false,
apiUrl: 'http://localhost:8080',
authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here
flowsCallService: FlowsCallServiceFake
};

View File

@ -1,7 +1,9 @@
import { AuthorizationCallService } from "@services/authorization/authorization-call";
import { FlowsCallService } from "@services/flows/flows-call";
export const environment = {
apiUrl: '/api',
production: true,
authorizationCallService: AuthorizationCallService,
flowsCallService: FlowsCallService
};