added details to flows

This commit is contained in:
Lucio Lelii 2026-01-13 12:21:32 +01:00
parent 0fbe49f0f3
commit 8e6084c32a
14 changed files with 109 additions and 139 deletions

View File

@ -1,7 +1,7 @@
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 { EditorStateHolder } from '@stores/flow-editor';
import { TitleToolbar } from "@shared/title-toolbar/title-toolbar";
@Component({
@ -14,9 +14,5 @@ export class FlowEditor {
private editorState: EditorStateHolder = inject(EditorStateHolder);
flow = this.editorState.currentFlow;
}

View File

@ -3,4 +3,7 @@ export type Flow = {
name: string;
visibility: 'public' | 'private';
data: any;
author: string;
createdAt: Date;
updatedAt: Date;
};

View File

@ -1,7 +1,7 @@
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 { EditorStateHolder } from '@stores/flow-editor';
import { CommonModule } from '@angular/common';
type OpenedId = 'flows' | 'blocks';

View File

@ -3,8 +3,14 @@ import { Observable } from "rxjs";
export abstract class FlowsCallServiceBase {
abstract getFlowById(flowId: string) : Observable<Flow>;
abstract retrieveAllFlows() : Observable<Flow[]>;
abstract updateFlow(flow: Flow) : Observable<void>;
abstract createNewFlow() : Observable<Flow>;
abstract deleteFlow(flowId: string) : Observable<void>;
}

View File

@ -1,20 +1,38 @@
import { Flow } from "@models/flow";
import { FlowsCallServiceBase } from "./flows-call.base";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject, Observable, of } from "rxjs";
export class FlowsCallServiceFake extends FlowsCallServiceBase {
override getFlowById(flowId: string): Observable<Flow> {
const flow = this.data[flowId];
if (!flow) {
throw new Error(`Flow with id ${flowId} not found`);
}
return of(flow);
}
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' },
'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() },
};
retrieveAllFlows() {
override retrieveAllFlows() {
return of(Object.values(this.data));
}
updateFlow(flow: Flow) {
override updateFlow(flow: Flow) {
this.data[flow.id] = flow;
return of(void 0);
}
override createNewFlow(): Observable<Flow> {
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() };
return of(this.data[newId]);
}
override deleteFlow(flowId: string): Observable<void> {
delete this.data[flowId];
return of(void 0);
}
}

View File

@ -3,6 +3,15 @@ import { FlowsCallServiceBase } from "./flows-call.base";
import { Observable } from "rxjs";
export class FlowsCallService extends FlowsCallServiceBase {
override getFlowById(flowId: string): Observable<Flow> {
throw new Error("Method not implemented.");
}
override createNewFlow(): Observable<Flow> {
throw new Error("Method not implemented.");
}
override deleteFlow(flowId: string): Observable<void> {
throw new Error("Method not implemented.");
}
override retrieveAllFlows(): Observable<Flow[]> {
throw new Error("Method not implemented.");
}

View File

@ -2,27 +2,27 @@ 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';
import { BehaviorSubject, catchError, combineLatest, Observable, switchMap, 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');
console.log('Flows signal accessed');
if (this.toInit) {
this.refresh();
this.toInit = false;
}
return this._flows.asReadonly();
}
@ -36,9 +36,49 @@ export class FlowsService {
return this.flowsCallService.updateFlow(flow).pipe(
tap(() => this.refresh()),
catchError(err => {
console.error('Update flow failed', err);
return throwError(() => err);
})
console.error('Update flow failed', err);
return throwError(() => err);
})
);
}
deleteFlow(flowId: string) {
return this.flowsCallService.deleteFlow(flowId).pipe(
tap(() => this.refresh()),
catchError(err => {
console.error('Delete flow failed', err);
return throwError(() => err);
})
);
}
cloneFlow(flowId: string): Observable<void> {
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 };
return this.flowsCallService.updateFlow(newFlow);
}),
tap(() => this.refresh()),
catchError(err => {
console.error('Cloning flow failed', err);
return throwError(() => err);
})
);
}
createNewFlow() {
return this.flowsCallService.createNewFlow().pipe(
tap(() => this.refresh()),
catchError(err => {
console.error('Create new flow failed', err);
return throwError(() => err);
})
);
}

View File

@ -26,30 +26,7 @@
<div class="space-y-2">
@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 select-none" (dblclick)="open(flow)">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2 text-indigo-700 font-medium">
<i class="bi" [ngClass]="{
'bi-globe': flow.visibility === 'public',
'bi-lock-fill': flow.visibility === 'private'
}" [class.text-indigo-500]="flow.visibility === 'public'"
[class.text-gray-500]="flow.visibility === 'private'">
</i>
{{ flow.name }}
</div>
</div>
</div>
<app-flow-item [flow]="flow"></app-flow-item>
}
</div>

View File

@ -2,20 +2,20 @@ import { CommonModule } from '@angular/common';
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';
import { FlowItem } from './flow-item/flow-item';
type FlowFilter = 'all' | 'public' | 'private';
@Component({
selector: 'app-flows-list',
imports: [CommonModule],
imports: [FlowItem],
templateUrl: './flows-list.html',
styleUrl: './flows-list.css',
})
export class FlowsList {
filter = signal<FlowFilter>('all');
private editorState = inject(EditorStateHolder);
private flowsService = inject(FlowsService);
loading: WritableSignal<boolean> = signal(true);
@ -35,8 +35,5 @@ export class FlowsList {
return this.flows().filter(f => f.visibility === this.filter());
});
open(flow: Flow) {
console.log('Opening flow:', flow);
this.editorState.openDocument(flow);
}
}

View File

@ -21,7 +21,7 @@
#titleInput
/>
</h2>
@if (unsaved()) {
@if (notSaved()) {
<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>
@ -32,16 +32,16 @@
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-green-200 text-green-600
enabled:hover:bg-green-200 text-green-600
p-1"
title="Save" (click)="save()">
title="Save" [disabled]="!notSaved()" (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"
enabled:hover:bg-gray-100 text-gray-600 p-1"
title="Undo">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
@ -49,7 +49,7 @@
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-gray-100 text-gray-600 p-1"
enabled:hover:bg-gray-100 text-gray-600 p-1"
title="Redo">
<i class="bi bi-arrow-clockwise"></i>
</button>
@ -59,7 +59,7 @@
<button tabindex="-1"
class="border-0
flex items-center gap-1
hover:bg-red-200 text-red-600 p-1"
enabled:hover:bg-red-200 text-red-600 p-1"
title="Clear">
<i class="bi bi-trash"></i>
</button>

View File

@ -1,5 +1,5 @@
import { Component, computed, ElementRef, inject, linkedSignal, ViewChild, WritableSignal } from '@angular/core';
import { EditorStateHolder } from 'app/stores/editor';
import { EditorStateHolder } from '@stores/flow-editor';
@Component({
selector: 'app-title-toolbar',
@ -28,7 +28,7 @@ export class TitleToolbar {
return Math.min(len, 20)-2;
});
unsaved = computed(() => this.editorState.isDirty());
notSaved = computed(() => this.editorState.isDirty());
changeTitle(value: string) {
@ -45,6 +45,7 @@ export class TitleToolbar {
}
save() {
if (!this.notSaved()) return;
this.editorState.save().subscribe(
err => console.error('Save failed', err)
);

View File

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

View File

@ -1,64 +0,0 @@
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 {
/** Stato */
readonly currentFlow = signal<Flow | null>(null);
readonly isDirty = signal(false);
/** Derived state */
readonly hasFlow = computed(() => !!this.currentFlow());
flowsService: FlowsService = inject(FlowsService);
constructor(private confirm: ConfirmDialogService) { }
/** Intent: open document */
async openDocument(doc: Flow): Promise<boolean> {
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);
}
updateFlow(updated: Flow) {
this.currentFlow.set(updated);
this.markDirty();
}
save() {
return this.flowsService.updateFlow(this.currentFlow()!).pipe(
tap(() => this.markSaved())
)
}
}

View File

@ -31,6 +31,9 @@
],
"@assets/*": [
"/assets/*"
],
"@stores/*": [
"app/stores/*"
]
},
"strict": true,