fixed bug on item list
This commit is contained in:
parent
8e6084c32a
commit
958d3210c5
|
|
@ -1,4 +1,4 @@
|
|||
<div class="main-layout min-h-screen flex flex-col">
|
||||
<div class="main-layout h-screen flex flex-col">
|
||||
|
||||
<!-- Header fisso -->
|
||||
<header
|
||||
|
|
@ -33,9 +33,9 @@
|
|||
</header>
|
||||
|
||||
<!-- Wrapper per compensare header fisso -->
|
||||
<div class="flex-1 pt-16 flex flex-col">
|
||||
<div class="pt-16 flex flex-col flex-1 h-full overflow-hidden">
|
||||
|
||||
<ul class="nav sticky-top top-16 z-40
|
||||
<ul class="nav fixed w-full top-16 z-40
|
||||
flex gap-1 px-3
|
||||
bg-[#d8f4e7] border-b-none shadow-sm">
|
||||
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
|
||||
|
||||
<!-- Contenuto principale scrollabile -->
|
||||
<main class="main-content flex-1 overflow-auto p-4 bg-gray-50">
|
||||
<main class="main-content w-full h-full mt-12 overflow-hidden ml-2 bg-white pb-4 pr-4 rounded-md shadow-sm">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
<div class="app-layout min-h-screen flex flex-col">
|
||||
<div class="flex flex-col flex-1 h-full">
|
||||
|
||||
<!-- HEADER -->
|
||||
<!--<app-header></app-header> -->
|
||||
|
||||
<!-- BODY -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="flex flex-row flex-1 overflow-hidden">
|
||||
|
||||
<!-- LEFT SIDEBAR -->
|
||||
<app-editor-sidebar></app-editor-sidebar>
|
||||
|
||||
|
||||
<!-- CENTER EDITOR -->
|
||||
<main class="flex-1 bg-gray-100 overflow-auto">
|
||||
<main class="flex flex-col w-full bg-gray-100 overflow-hidden">
|
||||
@if (flow()) {
|
||||
<div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<app-title-toolbar></app-title-toolbar>
|
||||
<!-- editor canvas -->
|
||||
<div class="flex-1 bg-amber-200 h-full">
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="text-gray-400 text-center mt-10">
|
||||
<div class="text-gray-400 text-center mt-10 flex flex-col flex-1">
|
||||
Select a document
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
export class FlowData {
|
||||
|
||||
constructor(public nodes: any[] = [], public edges: any[] = []) {}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,55 +1,54 @@
|
|||
<aside
|
||||
class="relative h-full
|
||||
<aside class="h-full flex flex-row
|
||||
bg-white border-r border-gray-200
|
||||
transition-all duration-300
|
||||
overflow-hidden"
|
||||
[class.w-72]="!collapsed()"
|
||||
[class.w-14]="collapsed()">
|
||||
transition-all duration-300 p-1" [class.w-72]="!collapsed()" [class.w-14]="collapsed()">
|
||||
|
||||
@if (!collapsed()) {
|
||||
<button
|
||||
(click)="collapse()"
|
||||
class="absolute top-3 right-3
|
||||
<div class="flex flex-col h-full">
|
||||
|
||||
|
||||
<button (click)="collapse()" class="relative
|
||||
w-6 h-6 rounded-full
|
||||
bg-white border shadow
|
||||
flex items-center justify-center
|
||||
hover:bg-gray-50 z-10">
|
||||
hover:bg-gray-50 z-10" [class.invisible]="collapsed()">
|
||||
|
||||
<i class="bi bi-chevron-left text-xs transition-transform"></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- ICON MODE -->
|
||||
<div class="flex flex-col gap-4 items-center pt-6 flex-1">
|
||||
<i class="bi bi-lightning-charge-fill text-indigo-500 text-xl cursor-pointer enabled p-2"
|
||||
(click)="openSide('flows')"></i>
|
||||
<i [ngClass]="{'cursor-pointer text-gray-600 enabled' : !blockDisabled()}"
|
||||
class="bi bi-boxes text-gray-200 text-xl p-2" (click)="!blockDisabled() && openSide('blocks')"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!collapsed()) {
|
||||
<!-- CONTENT -->
|
||||
<div class="h-full p-4">
|
||||
@if(!collapsed()) {
|
||||
<div class="pt-6 space-y-6">
|
||||
<!-- ACCORDION -->
|
||||
<div class="h-full bg-white flex-1">
|
||||
|
||||
<div class="space-y-6 felx-1">
|
||||
@switch (open) {
|
||||
@case ('flows') {
|
||||
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
|
||||
<app-flows-list></app-flows-list>
|
||||
</app-group-holder>
|
||||
}
|
||||
@case ('blocks') {
|
||||
<app-group-holder title="Blocks" icon="bi-boxes">
|
||||
<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-group-holder>
|
||||
}
|
||||
@case ('flows') {
|
||||
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
|
||||
<i ngProjectAs="header-button"
|
||||
class="bi bi-plus-circle-fill text-blue-600 hover:cursor-pointer hover:text-blue-400 text-lg"
|
||||
(click)="createNewFlow()"></i>
|
||||
<app-flows-list></app-flows-list>
|
||||
</app-group-holder>
|
||||
}
|
||||
@case ('blocks') {
|
||||
<app-group-holder title="Blocks" icon="bi-boxes">
|
||||
<div class="space-y-2 overflow-auto">
|
||||
<div class="block-item">Input</div>
|
||||
<div class="block-item">Transform</div>
|
||||
<div class="block-item">Output</div>
|
||||
</div>
|
||||
</app-group-holder>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
} @else {
|
||||
<!-- ICON MODE -->
|
||||
<div class="flex flex-col gap-4 items-center pt-6">
|
||||
<i class="bi bi-lightning-charge-fill text-indigo-500 text-xl cursor-pointer enabled p-2" (click)="openSide('flows')"></i>
|
||||
<i [ngClass]="{'cursor-pointer text-gray-600 enabled' : !blockDisabled()}"
|
||||
class="bi bi-boxes text-gray-200 text-xl p-2" (click)="!blockDisabled() && openSide('blocks')"></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export abstract class FlowsCallServiceBase {
|
|||
|
||||
abstract updateFlow(flow: Flow) : Observable<void>;
|
||||
|
||||
abstract createNewFlow() : Observable<Flow>;
|
||||
abstract createNewFlow(name?: string) : Observable<Flow>;
|
||||
|
||||
abstract deleteFlow(flowId: string) : Observable<void>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Flow> {
|
||||
|
|
@ -11,9 +13,11 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
|
|||
return of(flow);
|
||||
}
|
||||
|
||||
authorizationService = inject(Authorization);
|
||||
|
||||
private data: Record<string, Flow> = {
|
||||
'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<Flow> {
|
||||
override createNewFlow(name?: string): 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() };
|
||||
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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export class FlowsCallService extends FlowsCallServiceBase {
|
|||
override getFlowById(flowId: string): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override createNewFlow(): Observable<Flow> {
|
||||
override createNewFlow(name?: string): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override deleteFlow(flowId: string): Observable<void> {
|
||||
|
|
|
|||
|
|
@ -54,12 +54,13 @@ export class FlowsService {
|
|||
|
||||
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 };
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
<div class="group 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" (click)="toggleDetails()" (dblclick)="open()">
|
||||
|
||||
<!-- Header -->
|
||||
<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>
|
||||
|
||||
<i class="bi bi-chevron-down text-xs text-indigo-400
|
||||
transition-transform duration-200" [class.rotate-180]="expanded()">
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<!-- Dettagli -->
|
||||
<div class="overflow-hidden transition-all duration-300" [class.max-h-0]="!expanded()"
|
||||
[class.max-h-36]="expanded()">
|
||||
|
||||
<div class="mt-2 pt-2 border-t border-indigo-200
|
||||
text-xs text-gray-600
|
||||
flex row items-center gap-1">
|
||||
|
||||
<!-- Author -->
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-[11px] tracking-wide text-gray-400">
|
||||
Author:
|
||||
</span>
|
||||
<span class="font-small text-gray-700">
|
||||
{{ flow().author }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Created -->
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-[11px] tracking-wide text-gray-400">
|
||||
Created:
|
||||
</span>
|
||||
<span class="font-small text-gray-700">
|
||||
{{ flow().createdAt | date:'dd/MM/yyyy' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="w-full h-px bg-linear-to-r from-transparent via-indigo-200 to-transparent my-3"></div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<button class="p-1 rounded enabled:hover:bg-indigo-100 enabled:text-indigo-600 disabled:text-indigo-200" title="Open"
|
||||
[disabled]="this.flow().id == this.openedFlowId()"
|
||||
(click)="$event.stopPropagation(); open()">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
|
||||
<button class="p-1 rounded enabled:hover:bg-indigo-100 enabled:text-indigo-600 disabled:text-indigo-200" title="Clone"
|
||||
(click)="$event.stopPropagation(); clone()" [disabled]="this.flow().visibility!=='private'">
|
||||
<i class="bi bi-files"></i>
|
||||
</button>
|
||||
|
||||
<button class="p-1 rounded enabled:hover:bg-red-100 enabled:text-red-600 disabled:text-red-200" title="Delete"
|
||||
(click)="$event.stopPropagation(); remove()" [disabled]="this.flow().visibility!=='private'">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlowItem } from './flow-item';
|
||||
|
||||
describe('FlowItem', () => {
|
||||
let component: FlowItem;
|
||||
let fixture: ComponentFixture<FlowItem>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FlowItem]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FlowItem);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<Flow>();
|
||||
|
||||
detailOpenedId = model<string | null>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,33 +1,41 @@
|
|||
|
||||
@if (loading()) {
|
||||
<div class="flex items-center justify-center h-32">
|
||||
<div class="spinner-border text-indigo-500" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (filteredFlows.length === 0) {
|
||||
<div class="btn-group w-full mb-3" role="group">
|
||||
} @else {
|
||||
<div class="h-screen flex flex-col">
|
||||
<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.set('all')">
|
||||
All
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'all'"
|
||||
[class.btn-outline-primary]="filter() !== 'all'" (click)="filter.set('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.set('public')">
|
||||
Public
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'public'"
|
||||
[class.btn-outline-primary]="filter() !== 'public'" (click)="filter.set('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.set('private')">
|
||||
Private
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'private'"
|
||||
[class.btn-outline-primary]="filter() !== 'private'" (click)="filter.set('private')">
|
||||
Private
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@for (flow of filteredFlows(); track flow.id) {
|
||||
<app-flow-item [flow]="flow"></app-flow-item>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
@if (filteredFlows().length === 0) {
|
||||
<div class="text-gray-400 text-center mt-10 flex flex-col flex-1 justify-center">
|
||||
No flows found
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex-1 overflow-y-auto h-full py-2 gap-2 flex flex-col items-center max-h-[calc(100vh-16rem)]">
|
||||
@for (flow of filteredFlows(); track flow.id) {
|
||||
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ export class FlowsList {
|
|||
|
||||
flows: Signal<Flow[]> | undefined;
|
||||
|
||||
detailOpenedId = signal<string | null>(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());
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<div
|
||||
class="rounded-xl border border-gray-200
|
||||
class=" border-gray-200
|
||||
bg-white shadow-sm
|
||||
transition overflow-hidden">
|
||||
transition flex-1 h-full">
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="w-full flex items-center justify-between
|
||||
px-4 py-3
|
||||
text-sm font-semibold text-gray-700
|
||||
text-sm font-semibold text-gray-700 user-select-none
|
||||
bg-gray-50 "
|
||||
>
|
||||
|
||||
|
|
@ -15,15 +15,14 @@
|
|||
<i class="bi {{ icon }} text-lg"></i>
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<span>
|
||||
<ng-content select="header-button"></ng-content>
|
||||
</span>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300 max-h-96"
|
||||
>
|
||||
|
||||
<div class="p-3 space-y-2">
|
||||
class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 overflow-hidden">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,36 +32,30 @@
|
|||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
enabled:hover:bg-green-200 text-green-600
|
||||
enabled:hover:bg-green-200 enabled:text-green-600 disabled:text-green-300
|
||||
p-1"
|
||||
title="Save" [disabled]="!notSaved()" (click)="save()">
|
||||
<i class="bi bi-save"></i>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-5 bg-gray-200 mx-2"></div>
|
||||
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
enabled:hover:bg-gray-100 text-gray-600 p-1"
|
||||
title="Undo">
|
||||
enabled:hover:bg-gray-100 enabled:text-gray-600 disabled:text-gray-300 p-1"
|
||||
title="Undo" [disabled]="!editorState.undoEnabled()" (click)="undo()">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
enabled:hover:bg-gray-100 text-gray-600 p-1"
|
||||
title="Redo">
|
||||
enabled:hover:bg-gray-100 enabled:text-gray-600 disabled:text-gray-300 p-1"
|
||||
[disabled]="!editorState.redoEnabled()"
|
||||
title="Redo" (click)="redo()">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
|
||||
<div class="w-px h-5 bg-gray-200 mx-2"></div>
|
||||
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
enabled:hover:bg-red-200 text-red-600 p-1"
|
||||
title="Clear">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<Flow | null>(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<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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue