From fbd34a0882a222347db8cc728f2e98054a19daed Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 9 Jan 2026 17:20:27 +0100 Subject: [PATCH] added store state for editor --- src/app/app.html | 1 + src/app/app.routes.ts | 2 +- src/app/app.ts | 3 +- src/app/layouts/flow-editor/flow-editor.html | 33 ++++++++++++- src/app/layouts/flow-editor/flow-editor.ts | 14 +++++- src/app/models/flow.ts | 6 +++ .../main/editor-sidebar/editor-sidebar.css} | 0 .../main/editor-sidebar/editor-sidebar.html | 13 +++++ .../editor-sidebar/editor-sidebar.spec.ts | 23 +++++++++ .../main/editor-sidebar/editor-sidebar.ts | 26 ++++++++++ src/app/pages/main/tasks/tasks.css | 0 .../{layouts => pages/main}/tasks/tasks.html | 0 .../main}/tasks/tasks.spec.ts | 0 .../{layouts => pages/main}/tasks/tasks.ts | 0 .../services/dialogs/confirm-dialog.spec.ts | 16 +++++++ src/app/services/dialogs/confirm-dialog.ts | 26 ++++++++++ src/app/shared/accordion/accordion.css | 0 src/app/shared/accordion/accordion.html | 38 +++++++++++++++ src/app/shared/accordion/accordion.spec.ts | 23 +++++++++ src/app/shared/accordion/accordion.ts | 14 ++++++ .../shared/confirm-dialog/confirm-dialog.css | 0 .../shared/confirm-dialog/confirm-dialog.html | 35 ++++++++++++++ .../confirm-dialog/confirm-dialog.spec.ts | 23 +++++++++ .../shared/confirm-dialog/confirm-dialog.ts | 18 +++++++ src/app/shared/flows-list/flows-list.css | 0 src/app/shared/flows-list/flows-list.html | 48 +++++++++++++++++++ src/app/shared/flows-list/flows-list.spec.ts | 23 +++++++++ src/app/shared/flows-list/flows-list.ts | 35 ++++++++++++++ src/app/stores/editor.spec.ts | 16 +++++++ src/app/stores/editor.ts | 46 ++++++++++++++++++ 30 files changed, 477 insertions(+), 5 deletions(-) create mode 100644 src/app/models/flow.ts rename src/app/{layouts/tasks/tasks.css => pages/main/editor-sidebar/editor-sidebar.css} (100%) create mode 100644 src/app/pages/main/editor-sidebar/editor-sidebar.html create mode 100644 src/app/pages/main/editor-sidebar/editor-sidebar.spec.ts create mode 100644 src/app/pages/main/editor-sidebar/editor-sidebar.ts create mode 100644 src/app/pages/main/tasks/tasks.css rename src/app/{layouts => pages/main}/tasks/tasks.html (100%) rename src/app/{layouts => pages/main}/tasks/tasks.spec.ts (100%) rename src/app/{layouts => pages/main}/tasks/tasks.ts (100%) create mode 100644 src/app/services/dialogs/confirm-dialog.spec.ts create mode 100644 src/app/services/dialogs/confirm-dialog.ts create mode 100644 src/app/shared/accordion/accordion.css create mode 100644 src/app/shared/accordion/accordion.html create mode 100644 src/app/shared/accordion/accordion.spec.ts create mode 100644 src/app/shared/accordion/accordion.ts create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.css create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.html create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.spec.ts create mode 100644 src/app/shared/confirm-dialog/confirm-dialog.ts create mode 100644 src/app/shared/flows-list/flows-list.css create mode 100644 src/app/shared/flows-list/flows-list.html create mode 100644 src/app/shared/flows-list/flows-list.spec.ts create mode 100644 src/app/shared/flows-list/flows-list.ts create mode 100644 src/app/stores/editor.spec.ts create mode 100644 src/app/stores/editor.ts diff --git a/src/app/app.html b/src/app/app.html index 67e7bd4..8c47805 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1,2 @@ + diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d536e16..f01e580 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,7 +4,7 @@ import { Signup } from '@pages/auth/signup/signup'; import { AppLayout } from '@layouts/app-layout/app-layout'; import { authGuard } from '@guards/auth-guard'; import { FlowEditor } from '@layouts/flow-editor/flow-editor'; -import { Tasks } from '@layouts/tasks/tasks'; +import { Tasks } from '@pages/main/tasks/tasks'; export const routes: Routes = [ { diff --git a/src/app/app.ts b/src/app/app.ts index 971a834..a46483b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,9 +1,10 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { ConfirmDialogHostComponent } from '@shared/confirm-dialog/confirm-dialog'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, ConfirmDialogHostComponent], templateUrl: './app.html', styleUrl: './app.css' }) diff --git a/src/app/layouts/flow-editor/flow-editor.html b/src/app/layouts/flow-editor/flow-editor.html index accabde..b7e28cf 100644 --- a/src/app/layouts/flow-editor/flow-editor.html +++ b/src/app/layouts/flow-editor/flow-editor.html @@ -1 +1,32 @@ -

flow-editor works!

+
+ + + + + +
+ + +
+ +
+ + +
+ @if (flow()) { +
+

{{ flow()?.name }}

+ +
+ } @else { +
+ Select a document +
+ } +
+ + + + +
+
\ No newline at end of file diff --git a/src/app/layouts/flow-editor/flow-editor.ts b/src/app/layouts/flow-editor/flow-editor.ts index 801d544..bab1f96 100644 --- a/src/app/layouts/flow-editor/flow-editor.ts +++ b/src/app/layouts/flow-editor/flow-editor.ts @@ -1,11 +1,21 @@ -import { Component } from '@angular/core'; +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'; @Component({ selector: 'app-flow-editor', - imports: [], + imports: [EditorSidebar], templateUrl: './flow-editor.html', styleUrl: './flow-editor.css', }) export class FlowEditor { + private editorState: EditorStateHolder = inject(EditorStateHolder); + + + flow = this.editorState.currentFlow; + + + } diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts new file mode 100644 index 0000000..6bd9708 --- /dev/null +++ b/src/app/models/flow.ts @@ -0,0 +1,6 @@ +export type Flow = { + id: string; + name: string; + visibility: 'public' | 'private'; + data: any; +}; \ No newline at end of file diff --git a/src/app/layouts/tasks/tasks.css b/src/app/pages/main/editor-sidebar/editor-sidebar.css similarity index 100% rename from src/app/layouts/tasks/tasks.css rename to src/app/pages/main/editor-sidebar/editor-sidebar.css diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.html b/src/app/pages/main/editor-sidebar/editor-sidebar.html new file mode 100644 index 0000000..7dc558d --- /dev/null +++ b/src/app/pages/main/editor-sidebar/editor-sidebar.html @@ -0,0 +1,13 @@ + + + + + +
+
Input
+
Transform
+
Output
+
+
\ No newline at end of file diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.spec.ts b/src/app/pages/main/editor-sidebar/editor-sidebar.spec.ts new file mode 100644 index 0000000..39d3ea6 --- /dev/null +++ b/src/app/pages/main/editor-sidebar/editor-sidebar.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditorSidebar } from './editor-sidebar'; + +describe('EditorSidebar', () => { + let component: EditorSidebar; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditorSidebar] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EditorSidebar); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.ts b/src/app/pages/main/editor-sidebar/editor-sidebar.ts new file mode 100644 index 0000000..0b6bea4 --- /dev/null +++ b/src/app/pages/main/editor-sidebar/editor-sidebar.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { Accordion } from '@shared/accordion/accordion'; +import { FlowsList } from '@shared/flows-list/flows-list'; + +type AccordionId = 'flows' | 'blocks'; + +@Component({ + selector: 'app-editor-sidebar', + imports: [Accordion, FlowsList], + templateUrl: './editor-sidebar.html', + styleUrl: './editor-sidebar.css', +}) +export class EditorSidebar { + + collapsed = false; + + openAccordion: AccordionId | null = null; + + collapse() { + this.collapsed = !this.collapsed; + } + + toggle(id: AccordionId) { + this.openAccordion = this.openAccordion === id ? null : id; + } +} diff --git a/src/app/pages/main/tasks/tasks.css b/src/app/pages/main/tasks/tasks.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/layouts/tasks/tasks.html b/src/app/pages/main/tasks/tasks.html similarity index 100% rename from src/app/layouts/tasks/tasks.html rename to src/app/pages/main/tasks/tasks.html diff --git a/src/app/layouts/tasks/tasks.spec.ts b/src/app/pages/main/tasks/tasks.spec.ts similarity index 100% rename from src/app/layouts/tasks/tasks.spec.ts rename to src/app/pages/main/tasks/tasks.spec.ts diff --git a/src/app/layouts/tasks/tasks.ts b/src/app/pages/main/tasks/tasks.ts similarity index 100% rename from src/app/layouts/tasks/tasks.ts rename to src/app/pages/main/tasks/tasks.ts diff --git a/src/app/services/dialogs/confirm-dialog.spec.ts b/src/app/services/dialogs/confirm-dialog.spec.ts new file mode 100644 index 0000000..5cf3c85 --- /dev/null +++ b/src/app/services/dialogs/confirm-dialog.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ConfirmDialog } from './confirm-dialog'; + +describe('ConfirmDialog', () => { + let service: ConfirmDialog; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ConfirmDialog); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/dialogs/confirm-dialog.ts b/src/app/services/dialogs/confirm-dialog.ts new file mode 100644 index 0000000..21f8bf6 --- /dev/null +++ b/src/app/services/dialogs/confirm-dialog.ts @@ -0,0 +1,26 @@ +import { Injectable, signal } from "@angular/core"; + +@Injectable({ providedIn: 'root' }) +export class ConfirmDialogService { + + private _state = signal<{ + message: string; + resolve: (v: boolean) => void; + } | null>(null); + + readonly state = this._state.asReadonly(); + + open(message: string): Promise { + return new Promise(resolve => { + this._state.set({ message, resolve }); + }); + } + + confirm(result: boolean) { + const s = this._state(); + if (!s) return; + + s.resolve(result); + this._state.set(null); + } +} diff --git a/src/app/shared/accordion/accordion.css b/src/app/shared/accordion/accordion.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/accordion/accordion.html b/src/app/shared/accordion/accordion.html new file mode 100644 index 0000000..5887c3c --- /dev/null +++ b/src/app/shared/accordion/accordion.html @@ -0,0 +1,38 @@ +
+ + + + + +
+ +
+ +
+ +
+
diff --git a/src/app/shared/accordion/accordion.spec.ts b/src/app/shared/accordion/accordion.spec.ts new file mode 100644 index 0000000..fa15d4c --- /dev/null +++ b/src/app/shared/accordion/accordion.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Accordion } from './accordion'; + +describe('Accordion', () => { + let component: Accordion; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Accordion] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Accordion); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/accordion/accordion.ts b/src/app/shared/accordion/accordion.ts new file mode 100644 index 0000000..ca6faa7 --- /dev/null +++ b/src/app/shared/accordion/accordion.ts @@ -0,0 +1,14 @@ +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(false); + @Output() toggle = new EventEmitter(); +} diff --git a/src/app/shared/confirm-dialog/confirm-dialog.css b/src/app/shared/confirm-dialog/confirm-dialog.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/confirm-dialog/confirm-dialog.html b/src/app/shared/confirm-dialog/confirm-dialog.html new file mode 100644 index 0000000..7b33132 --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.html @@ -0,0 +1,35 @@ +@if(!!this.state()) { + +
+ + +
+
+ + +
+ +

+ Confirm +

+ +

+ {{ this.state()!.message }} +

+ +
+ + + +
+
+ +
+ +} \ No newline at end of file diff --git a/src/app/shared/confirm-dialog/confirm-dialog.spec.ts b/src/app/shared/confirm-dialog/confirm-dialog.spec.ts new file mode 100644 index 0000000..85ecf02 --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmDialog } from './confirm-dialog'; + +describe('ConfirmDialog', () => { + let component: ConfirmDialog; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfirmDialog] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfirmDialog); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/confirm-dialog/confirm-dialog.ts b/src/app/shared/confirm-dialog/confirm-dialog.ts new file mode 100644 index 0000000..305a068 --- /dev/null +++ b/src/app/shared/confirm-dialog/confirm-dialog.ts @@ -0,0 +1,18 @@ +import { Component, inject, Input } from '@angular/core'; +import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; + +@Component({ + selector: 'app-confirm-dialog-host', + standalone: true, + templateUrl: './confirm-dialog.html', +}) +export class ConfirmDialogHostComponent { + + private confirm: ConfirmDialogService = inject(ConfirmDialogService); + + state = this.confirm.state; + + close(result: boolean) { + this.confirm.confirm(result); + } +} diff --git a/src/app/shared/flows-list/flows-list.css b/src/app/shared/flows-list/flows-list.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/flows-list/flows-list.html b/src/app/shared/flows-list/flows-list.html new file mode 100644 index 0000000..d1b4666 --- /dev/null +++ b/src/app/shared/flows-list/flows-list.html @@ -0,0 +1,48 @@ +
+ + + + + + + +
+ +
+ @for (flow of filteredFlows; track flow.id) { +
+ +
+ + + +
+ + + {{ flow.name }} +
+ + + +
+
+ } + +
\ No newline at end of file diff --git a/src/app/shared/flows-list/flows-list.spec.ts b/src/app/shared/flows-list/flows-list.spec.ts new file mode 100644 index 0000000..1470b75 --- /dev/null +++ b/src/app/shared/flows-list/flows-list.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FlowsList } from './flows-list'; + +describe('FlowsList', () => { + let component: FlowsList; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FlowsList] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FlowsList); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts new file mode 100644 index 0000000..fcae93c --- /dev/null +++ b/src/app/shared/flows-list/flows-list.ts @@ -0,0 +1,35 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { Flow } from '@models/flow'; +import { EditorStateHolder } from 'app/stores/editor'; + +type FlowFilter = 'all' | 'public' | 'private'; + +@Component({ + selector: 'app-flows-list', + imports: [CommonModule], + templateUrl: './flows-list.html', + styleUrl: './flows-list.css', +}) +export class FlowsList { + filter: FlowFilter = 'all'; + + private editorState = inject(EditorStateHolder); + + 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: {} }, + ] + + get filteredFlows() { + if (this.filter === 'all') return this.flows; + return this.flows.filter(f => f.visibility === this.filter); + } + + open(flow: Flow) { + console.log('Opening flow:', flow); + this.editorState.openDocument(flow); + } +} diff --git a/src/app/stores/editor.spec.ts b/src/app/stores/editor.spec.ts new file mode 100644 index 0000000..57a1234 --- /dev/null +++ b/src/app/stores/editor.spec.ts @@ -0,0 +1,16 @@ +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(); + }); +}); diff --git a/src/app/stores/editor.ts b/src/app/stores/editor.ts new file mode 100644 index 0000000..b8fa857 --- /dev/null +++ b/src/app/stores/editor.ts @@ -0,0 +1,46 @@ +import { computed, Injectable, signal } from '@angular/core'; +import { Flow } from '@models/flow'; +import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; + +@Injectable({ providedIn: 'root' }) +export class EditorStateHolder { + + /** Stato */ + readonly currentFlow = signal(null); + readonly isDirty = signal(false); + + /** Derived state */ + readonly hasFlow = computed(() => !!this.currentFlow()); + + constructor(private confirm: ConfirmDialogService) { } + + /** Intent: open document */ + async openDocument(doc: Flow): Promise { + 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 */ + markDirty() { + this.isDirty.set(true); + } + + /** Intent: save document */ + markSaved() { + this.isDirty.set(false); + } + + /** Intent: close document */ + closeDocument() { + this.currentFlow.set(null); + this.isDirty.set(false); + } +}