added store state for editor

This commit is contained in:
Lucio Lelii 2026-01-09 17:20:27 +01:00
parent f8cae973ff
commit fbd34a0882
30 changed files with 477 additions and 5 deletions

View File

@ -1 +1,2 @@
<router-outlet />
<app-confirm-dialog-host></app-confirm-dialog-host>

View File

@ -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 = [
{

View File

@ -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'
})

View File

@ -1 +1,32 @@
<p>flow-editor works!</p>
<div class="app-layout min-h-screen flex flex-col">
<!-- HEADER -->
<!--<app-header></app-header> -->
<!-- BODY -->
<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>
<!-- CENTER EDITOR -->
<main class="flex-1 bg-gray-100 overflow-auto">
@if (flow()) {
<div>
<h2 class="text-sm font-semibold">{{ flow()?.name }}</h2>
<!-- editor canvas -->
</div>
} @else {
<div class="text-gray-400 text-center mt-10">
Select a document
</div>
}
</main>
<!-- RIGHT PANEL -->
<!-- <app-right-panel></app-right-panel> -->
</div>
</div>

View File

@ -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;
}

6
src/app/models/flow.ts Normal file
View File

@ -0,0 +1,6 @@
export type Flow = {
id: string;
name: string;
visibility: 'public' | 'private';
data: any;
};

View File

@ -0,0 +1,13 @@
<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>
<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>

View File

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

View File

@ -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;
}
}

View File

View File

@ -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();
});
});

View File

@ -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<boolean> {
return new Promise<boolean>(resolve => {
this._state.set({ message, resolve });
});
}
confirm(result: boolean) {
const s = this._state();
if (!s) return;
s.resolve(result);
this._state.set(null);
}
}

View File

View File

@ -0,0 +1,38 @@
<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

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

View File

@ -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<boolean>(false);
@Output() toggle = new EventEmitter<void>();
}

View File

@ -0,0 +1,35 @@
@if(!!this.state()) {
<div class="fixed inset-0 z-50 flex items-center justify-center">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/40" (click)="close(false)">
</div>
<!-- Modal -->
<div class="relative z-10 w-full max-w-md
rounded-lg bg-white shadow-xl
p-5 animate-fade-in">
<h3 class="text-lg font-semibold mb-3">
Confirm
</h3>
<p class="text-sm text-gray-700 mb-5">
{{ this.state()!.message }}
</p>
<div class="flex justify-end gap-2">
<button class="btn btn-outline-secondary" (click)="close(false)">
Cancel
</button>
<button class="btn btn-primary" (click)="close(true)">
OK
</button>
</div>
</div>
</div>
}

View File

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

View File

@ -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);
}
}

View File

View File

@ -0,0 +1,48 @@
<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'">
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'">
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'">
Private
</button>
</div>
<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" (click)="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>
}
</div>

View File

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

View File

@ -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);
}
}

View File

@ -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();
});
});

46
src/app/stores/editor.ts Normal file
View File

@ -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<Flow | null>(null);
readonly isDirty = signal(false);
/** Derived state */
readonly hasFlow = computed(() => !!this.currentFlow());
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;
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);
}
}