added store state for editor
This commit is contained in:
parent
f8cae973ff
commit
fbd34a0882
|
|
@ -1 +1,2 @@
|
|||
<router-outlet />
|
||||
<app-confirm-dialog-host></app-confirm-dialog-host>
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export type Flow = {
|
||||
id: string;
|
||||
name: string;
|
||||
visibility: 'public' | 'private';
|
||||
data: any;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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>();
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue