+
-
+
-
+
-
+
@if (flow()) {
- (null);
+ readonly isDirty = signal(false);
+
+ /** Derived state */
+ readonly hasFlow = computed(() => !!this.currentFlow());
+
+ flowsService: FlowsService = inject(FlowsService);
+
+ constructor(private confirm: ConfirmDialogService) { }
+
+
+ previousDataStack : FlowData[] = [];
+ nextDataStack : FlowData[] = [];
+
+ redoEnabled = computed(() => this.nextDataStack.length > 0);
+ undoEnabled = computed(() => this.previousDataStack.length > 1);
+
+ /** Intent: open document */
+ async openDocument(doc: Flow): Promise {
+ 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);
+ return true;
+ }
+
+ /** Intent: mark editor dirty */
+ private markDirty() {
+ this.isDirty.set(true);
+ }
+
+ /** Intent: save document */
+ private markSaved() {
+ this.isDirty.set(false);
+ }
+
+ /** Intent: close document */
+ closeDocument() {
+ this.currentFlow.set(null);
+ this.isDirty.set(false);
+ }
+
+ updateData(data: FlowData) {
+ this.currentFlow.update(flow => {
+ if (!flow) return flow;
+ return { ...flow, data };
+ });
+ this.previousDataStack.push(data);
+ this.markDirty();
+ }
+
+ updateFlowTitle(newTitle: string) {
+ const flow = this.currentFlow();
+ if (!flow) return;
+ this.currentFlow.update(flow => {
+ if (!flow) return flow;
+ return { ...flow, name: newTitle };
+ });
+ this.previousDataStack.push(flow.data);
+ this.markDirty();
+ }
+
+ save() {
+ return this.flowsService.updateFlow(this.currentFlow()!).pipe(
+ tap(() => this.markSaved())
+ )
+ }
+
+ undo() {
+ const stack = this.previousDataStack;
+ if (stack.length < 2) return; // Nothing to undo
+
+ // Remove the current state
+ this.nextDataStack.push(stack.pop()!);
+ // Get the previous state
+ const previousData = stack[stack.length - 1];
+ this.updateData(previousData);
+ }
+
+ redo() {
+ const stack = this.nextDataStack;
+ if (stack.length === 0) return; // Nothing to redo
+
+ const nextData = stack.pop()!;
+ this.previousDataStack.push(nextData);
+ this.updateData(nextData);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/app/utilities/edit-flow.ts b/src/app/utilities/edit-flow.ts
new file mode 100644
index 0000000..e69de29
+
+
} @else {
-
diff --git a/src/app/shared/title-toolbar/title-toolbar.ts b/src/app/shared/title-toolbar/title-toolbar.ts
index b417ded..34ee4bd 100644
--- a/src/app/shared/title-toolbar/title-toolbar.ts
+++ b/src/app/shared/title-toolbar/title-toolbar.ts
@@ -11,7 +11,7 @@ export class TitleToolbar {
@ViewChild('titleInput') myInputRef!: ElementRef;
- private editorState: EditorStateHolder = inject(EditorStateHolder);
+ editorState: EditorStateHolder = inject(EditorStateHolder);
title = computed(() => {
const flow = this.editorState.currentFlow();
return flow ? flow.name : 'No Flow Opened';
@@ -41,7 +41,7 @@ export class TitleToolbar {
}
console.log('Updating flow title to:', trimmed);
const flow = this.editorState.currentFlow()!;
- this.editorState.updateFlow({ ...flow, name: trimmed });
+ this.editorState.updateFlowTitle(trimmed );
}
save() {
@@ -51,4 +51,11 @@ export class TitleToolbar {
);
}
+ undo() {
+ this.editorState.undo();
+ }
+
+ redo() {
+ this.editorState.redo();
+ }
}
diff --git a/src/app/stores/flow-editor.spec.ts b/src/app/stores/flow-editor.spec.ts
new file mode 100644
index 0000000..d9d27ab
--- /dev/null
+++ b/src/app/stores/flow-editor.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { EditorStateHolder as Editor } from './editor';
+
+describe('Editor', () => {
+ let service: Editor;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(Editor);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/stores/flow-editor.ts b/src/app/stores/flow-editor.ts
new file mode 100644
index 0000000..b98b907
--- /dev/null
+++ b/src/app/stores/flow-editor.ts
@@ -0,0 +1,107 @@
+import { computed, inject, Injectable, signal } from '@angular/core';
+import { Flow, FlowData } 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 {
+
+ /** Stato */
+ readonly currentFlow = signal
+
+
+
+ @if (!collapsed()) {
-
Select a document
}
diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts
index 9c6bbc6..d23a9e1 100644
--- a/src/app/models/flow.ts
+++ b/src/app/models/flow.ts
@@ -1,9 +1,18 @@
+export type FlowVisibility = 'public' | 'private';
+
+
+
export type Flow = {
id: string;
name: string;
- visibility: 'public' | 'private';
- data: any;
+ visibility: FlowVisibility;
+ data: FlowData;
author: string;
createdAt: Date;
updatedAt: Date;
-};
\ No newline at end of file
+};
+
+export class FlowData {
+
+ constructor(public nodes: any[] = [], public edges: any[] = []) {}
+};
diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.html b/src/app/pages/main/editor-sidebar/editor-sidebar.html
index 15cc7b2..b6e3f2e 100644
--- a/src/app/pages/main/editor-sidebar/editor-sidebar.html
+++ b/src/app/pages/main/editor-sidebar/editor-sidebar.html
@@ -1,55 +1,54 @@
-
- @if(!collapsed()) {
-
-
+ ;
- abstract createNewFlow() : Observable;
+ abstract createNewFlow(name?: string) : Observable;
abstract deleteFlow(flowId: string) : Observable;
diff --git a/src/app/services/flows/flows-call.fake.ts b/src/app/services/flows/flows-call.fake.ts
index 01b5b4b..f105001 100644
--- a/src/app/services/flows/flows-call.fake.ts
+++ b/src/app/services/flows/flows-call.fake.ts
@@ -1,6 +1,8 @@
-import { Flow } from "@models/flow";
+import { Flow, FlowData } from "@models/flow";
import { FlowsCallServiceBase } from "./flows-call.base";
import { BehaviorSubject, Observable, of } from "rxjs";
+import { Authorization } from "@services/authorization/authorization";
+import { inject } from "@angular/core";
export class FlowsCallServiceFake extends FlowsCallServiceBase {
override getFlowById(flowId: string): Observable {
@@ -11,9 +13,11 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
return of(flow);
}
+ authorizationService = inject(Authorization);
+
private data: Record = {
- '1': { id: '1', name: 'Test Flow 1', data: {}, visibility: 'public', author: 'Alice', createdAt: new Date(), updatedAt: new Date() },
- '2': { id: '2', name: 'Test Flow 2', data: {}, visibility: 'private', author: 'Bob', createdAt: new Date(), updatedAt: new Date() },
+ '1': { id: '1', name: 'Test Flow 1', data: new FlowData(), visibility: 'public', author: 'Alice', createdAt: new Date(), updatedAt: new Date() },
+ '2': { id: '2', name: 'Test Flow 2', data: new FlowData(), visibility: 'private', author: 'Bob', createdAt: new Date(), updatedAt: new Date() },
};
override retrieveAllFlows() {
@@ -25,9 +29,9 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
return of(void 0);
}
- override createNewFlow(): Observable {
+ override createNewFlow(name?: string): Observable {
const newId = (Object.keys(this.data).length + 1).toString();
- this.data[newId] = { id: newId, name: 'New Flow', data: {}, visibility: 'private', author: 'Charlie', createdAt: new Date(), updatedAt: new Date() };
+ this.data[newId] = { id: newId, name: name || `New Flow`, data: new FlowData() , visibility: 'private', author: this.authorizationService.loggedInUser()!.username, createdAt: new Date(), updatedAt: new Date() };
return of(this.data[newId]);
}
diff --git a/src/app/services/flows/flows-call.ts b/src/app/services/flows/flows-call.ts
index 7e3df3c..c0497f1 100644
--- a/src/app/services/flows/flows-call.ts
+++ b/src/app/services/flows/flows-call.ts
@@ -6,7 +6,7 @@ export class FlowsCallService extends FlowsCallServiceBase {
override getFlowById(flowId: string): Observable {
throw new Error("Method not implemented.");
}
- override createNewFlow(): Observable {
+ override createNewFlow(name?: string): Observable {
throw new Error("Method not implemented.");
}
override deleteFlow(flowId: string): Observable {
diff --git a/src/app/services/flows/flows.ts b/src/app/services/flows/flows.ts
index 67e5cc4..501b2c0 100644
--- a/src/app/services/flows/flows.ts
+++ b/src/app/services/flows/flows.ts
@@ -54,12 +54,13 @@ export class FlowsService {
cloneFlow(flowId: string): Observable {
const originalFlow$ = this.flowsCallService.getFlowById(flowId);
+
const newFlow$ = this.flowsCallService.createNewFlow();
return combineLatest([originalFlow$, newFlow$]).pipe(
switchMap(([originalFlow, newFlow]) => {
- newFlow.name = originalFlow.name + ' (Clone)';
- newFlow.data = { ...originalFlow.data };
+ newFlow.name = this.nextFileName(originalFlow.name);
+ newFlow.data = originalFlow.data ;
return this.flowsCallService.updateFlow(newFlow);
}),
@@ -72,8 +73,8 @@ export class FlowsService {
}
- createNewFlow() {
- return this.flowsCallService.createNewFlow().pipe(
+ createNewFlow(name? : string) {
+ return this.flowsCallService.createNewFlow(name || this.nextFileName('New Flow')).pipe(
tap(() => this.refresh()),
catchError(err => {
console.error('Create new flow failed', err);
@@ -82,4 +83,19 @@ export class FlowsService {
);
}
+
+ private nextFileName(base: string): string {
+ let n = 0;
+
+ while (true) {
+ const name =
+ n === 0 ? `${base}` : `${base}(${n})`;
+ const exists = this._flows().some(flow => flow.name === name);
+ if (!exists) {
+ return name;
+ }
+ n++;
+ }
+ }
+
}
diff --git a/src/app/shared/flows-list/flow-item/flow-item.css b/src/app/shared/flows-list/flow-item/flow-item.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/shared/flows-list/flow-item/flow-item.html b/src/app/shared/flows-list/flow-item/flow-item.html
new file mode 100644
index 0000000..09c54d8
--- /dev/null
+++ b/src/app/shared/flows-list/flow-item/flow-item.html
@@ -0,0 +1,77 @@
+;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [FlowItem]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(FlowItem);
+ component = fixture.componentInstance;
+ await fixture.whenStable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/flows-list/flow-item/flow-item.ts b/src/app/shared/flows-list/flow-item/flow-item.ts
new file mode 100644
index 0000000..86c38c6
--- /dev/null
+++ b/src/app/shared/flows-list/flow-item/flow-item.ts
@@ -0,0 +1,72 @@
+import { CommonModule } from '@angular/common';
+import { Component, computed, effect, EventEmitter, inject, input, model, output, signal, untracked } from '@angular/core';
+import { Flow } from '@models/flow';
+import { FlowsService } from '@services/flows/flows';
+import { EditorStateHolder } from '@stores/flow-editor';
+
+@Component({
+ selector: 'app-flow-item',
+ imports: [CommonModule],
+ templateUrl: './flow-item.html',
+ styleUrl: './flow-item.css',
+})
+export class FlowItem {
+
+ private editorState = inject(EditorStateHolder);
+
+ private flowsService = inject(FlowsService);
+
+ flow = input.required();
+
+ detailOpenedId = model(null);
+
+ openedFlowId = computed(() => this.editorState.currentFlow()?.id);
+
+ expanded = signal(false);
+
+ constructor() {
+ effect(() => {
+ if (this.expanded()) {
+ if (this.detailOpenedId() !== this.flow().id) {
+ this.expanded.set(false);
+ }
+ }
+ });
+ }
+
+ open() {
+ console.log('Opening flow:', this.flow());
+ this.editorState.openDocument(this.flow());
+ }
+
+ clone() {
+ this.flowsService.cloneFlow(this.flow().id).subscribe({
+ next: clonedFlow => {
+ console.log('Flow cloned:', clonedFlow);
+ },
+ error: err => console.error('Error cloning flow:', err)
+ });
+ }
+
+ remove() {
+ this.flowsService.deleteFlow(this.flow().id).subscribe({
+ next: () => {
+ console.log('Flow deleted:', this.flow().id);
+ if (this.openedFlowId() === this.flow().id) {
+ this.editorState.closeDocument();
+ }
+ },
+ error: err => console.error('Error deleting flow:', err)
+ });
+ }
+
+ toggleDetails() {
+ this.expanded.update(v => !v);
+ if (this.expanded()) {
+ this.detailOpenedId.set(this.flow().id);
+ } else {
+ this.detailOpenedId.set(null);
+ }
+ }
+}
+
diff --git a/src/app/shared/flows-list/flows-list.html b/src/app/shared/flows-list/flows-list.html
index 35d46b6..2e3e1dc 100644
--- a/src/app/shared/flows-list/flows-list.html
+++ b/src/app/shared/flows-list/flows-list.html
@@ -1,33 +1,41 @@
+
@if (loading()) {
-} @else if (filteredFlows.length === 0) {
- | undefined;
+ detailOpenedId = signal(null);
+
ngOnInit() {
this.flowsService.getAllFlows().then(flowsSignal => {
this.flows = flowsSignal;
@@ -30,10 +32,11 @@ export class FlowsList {
}
filteredFlows = computed(() => {
- if (!this.flows) return [];
- if (this.filter() === 'all') return this.flows();
- return this.flows().filter(f => f.visibility === this.filter());
+ const flows = this.flows?.();
+ console.log('Filtering flows', flows?.map(f => f.name));
+ if (!flows) return [];
+ if (this.filter() === 'all') return flows;
+ return flows.filter(f => f.visibility === this.filter());
});
-
}
diff --git a/src/app/shared/group-holder/group-holder.html b/src/app/shared/group-holder/group-holder.html
index 89860c8..3fc9160 100644
--- a/src/app/shared/group-holder/group-holder.html
+++ b/src/app/shared/group-holder/group-holder.html
@@ -1,13 +1,13 @@
diff --git a/src/app/shared/title-toolbar/title-toolbar.html b/src/app/shared/title-toolbar/title-toolbar.html
index 7e54a2f..4b99101 100644
--- a/src/app/shared/title-toolbar/title-toolbar.html
+++ b/src/app/shared/title-toolbar/title-toolbar.html
@@ -32,36 +32,30 @@
+
+
-
-
-
+
+
+ }
\ No newline at end of file
diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.ts b/src/app/pages/main/editor-sidebar/editor-sidebar.ts
index c787013..f8589ed 100644
--- a/src/app/pages/main/editor-sidebar/editor-sidebar.ts
+++ b/src/app/pages/main/editor-sidebar/editor-sidebar.ts
@@ -3,6 +3,7 @@ import { GroupHolder } from '@shared/group-holder/group-holder';
import { FlowsList } from '@shared/flows-list/flows-list';
import { EditorStateHolder } from '@stores/flow-editor';
import { CommonModule } from '@angular/common';
+import { FlowsService } from '@services/flows/flows';
type OpenedId = 'flows' | 'blocks';
@@ -16,6 +17,8 @@ export class EditorSidebar {
flowState = inject(EditorStateHolder);
+ flowService = inject(FlowsService);
+
blockDisabled = computed(() => !this.flowState.hasFlow());
collapsed = signal(true);
@@ -31,5 +34,18 @@ export class EditorSidebar {
collapse() {
this.collapsed.set(true);
}
+
+ createNewFlow() {
+ 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);
+ },
+ });
+ }
}
diff --git a/src/app/services/flows/flows-call.base.ts b/src/app/services/flows/flows-call.base.ts
index 0c0d4ab..af35ac8 100644
--- a/src/app/services/flows/flows-call.base.ts
+++ b/src/app/services/flows/flows-call.base.ts
@@ -9,7 +9,7 @@ export abstract class FlowsCallServiceBase {
abstract updateFlow(flow: Flow) : Observable
@switch (open) {
- @case ('flows') {
-
-
-
- }
- @case ('blocks') {
-
-
- }
+ @case ('flows') {
+
+
+
+
+ }
+ @case ('blocks') {
+
+
+ }
}
- } @else {
-
-
-
- Input
- Transform
- Output
-
+
+ Input
+ Transform
+ Output
+
-
-
-
- }
-
+
+
+
\ No newline at end of file
diff --git a/src/app/shared/flows-list/flow-item/flow-item.spec.ts b/src/app/shared/flows-list/flow-item/flow-item.spec.ts
new file mode 100644
index 0000000..cb4c1bc
--- /dev/null
+++ b/src/app/shared/flows-list/flow-item/flow-item.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FlowItem } from './flow-item';
+
+describe('FlowItem', () => {
+ let component: FlowItem;
+ let fixture: ComponentFixture
+
+
+
+
+
+
+
+ {{ flow().name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Author:
+
+
+ {{ flow().author }}
+
+
+
+
+
+
+ Created:
+
+
+ {{ flow().createdAt | date:'dd/MM/yyyy' }}
+
+
+
+
+
+
+
+
+
+
+
+
Loading...
+} @else {
+
+}
diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts
index 165b271..8021903 100644
--- a/src/app/shared/flows-list/flows-list.ts
+++ b/src/app/shared/flows-list/flows-list.ts
@@ -22,6 +22,8 @@ export class FlowsList {
flows: Signal
+
-
-
+
-
+
-
+
-
+
- @for (flow of filteredFlows(); track flow.id) {
-
- }
-
-
-}
\ No newline at end of file
+ @if (filteredFlows().length === 0) {
+
+ No flows found
+
+ } @else {
+
+ @for (flow of filteredFlows(); track flow.id) {
+
+ }
+
+ }
+
+ transition flex-1 h-full">
+
@@ -15,15 +15,14 @@
{{ title }}
+
+
-
-
+ class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 overflow-hidden">
-
-
+