added flow service
This commit is contained in:
parent
fbd34a0882
commit
0fbe49f0f3
|
|
@ -13,6 +13,7 @@
|
|||
"@angular/common": "^21.0.4",
|
||||
"@angular/compiler": "^21.0.4",
|
||||
"@angular/core": "^21.0.4",
|
||||
"@angular/elements": "^21.0.8",
|
||||
"@angular/forms": "^21.0.4",
|
||||
"@angular/platform-browser": "^21.0.4",
|
||||
"@angular/platform-browser-dynamic": "^21.0.4",
|
||||
|
|
@ -22,6 +23,8 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-angular": "^0.561.0",
|
||||
"rete-angular-plugin": "^2.6.0",
|
||||
"rete-render-utils": "^2.0.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
|
@ -597,6 +600,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/elements": {
|
||||
"version": "21.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/elements/-/elements-21.0.8.tgz",
|
||||
"integrity": "sha512-eOvxX5eQw5u5Dp1UT6hhv7OSH6P/xzWb4MwiKS8fZhAJs0CNY3WtJfKTJCFJ3vwIk0zIoqz52OV1lrT6fuozHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "21.0.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "21.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.5.tgz",
|
||||
|
|
@ -1001,6 +1020,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
|
|
@ -8207,6 +8235,62 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rete": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/rete/-/rete-2.0.6.tgz",
|
||||
"integrity": "sha512-kPmlKCGFES2VWtY7Y7SCB8ZeXRMsgX5deza9cu4OwmfM/ZUimd461kC3hRyccoyVxE4POlHUx0gg2jcGfusHFg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rete-angular-plugin": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/rete-angular-plugin/-/rete-angular-plugin-2.6.0.tgz",
|
||||
"integrity": "sha512-0GdYJbuo2V6mmN1ylzW/WUOetncAPWmZPpj+l2yw9sPUPpr/QtfBnXxUXje4cnY1ws7foq2gYeWsNnoClutUSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">= 12 < 22",
|
||||
"@angular/core": ">= 12 < 22",
|
||||
"@angular/elements": ">= 12 < 22",
|
||||
"rete": "^2.0.1",
|
||||
"rete-area-plugin": "^2.0.0",
|
||||
"rete-render-utils": "^2.0.0",
|
||||
"rxjs": ">=6.6.0",
|
||||
"zone.js": "~0.11.4 || ~0.12.0 || ~0.13.0 || ~0.14.0 || ~0.15.0 || ~0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rete-area-plugin": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/rete-area-plugin/-/rete-area-plugin-2.1.5.tgz",
|
||||
"integrity": "sha512-iquEvwkQlcsO4cmgM3Z37TG0AWaE536dfA+lCJAze5YJzVx4RBaViUCqdB4dUA/utSytpBCkiDC4D3ztM9akGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rete": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rete-render-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rete-render-utils/-/rete-render-utils-2.0.3.tgz",
|
||||
"integrity": "sha512-Oz4W2PNayHocRvlzadb5BCNf+tDzJ8RhTwB3ucBPCdCLKZ974wWDiTSCRfA287L2hmHVzRfBdyAwC03K9eP+4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rete": "^2.0.0",
|
||||
"rete-area-plugin": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
|
|
@ -10155,6 +10239,13 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zone.js": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
||||
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"@angular/common": "^21.0.4",
|
||||
"@angular/compiler": "^21.0.4",
|
||||
"@angular/core": "^21.0.4",
|
||||
"@angular/elements": "^21.0.8",
|
||||
"@angular/forms": "^21.0.4",
|
||||
"@angular/platform-browser": "^21.0.4",
|
||||
"@angular/platform-browser-dynamic": "^21.0.4",
|
||||
|
|
@ -37,6 +38,8 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-angular": "^0.561.0",
|
||||
"rete-angular-plugin": "^2.6.0",
|
||||
"rete-render-utils": "^2.0.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@
|
|||
<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>
|
||||
|
||||
<app-editor-sidebar></app-editor-sidebar>
|
||||
|
||||
<!-- CENTER EDITOR -->
|
||||
<main class="flex-1 bg-gray-100 overflow-auto">
|
||||
@if (flow()) {
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">{{ flow()?.name }}</h2>
|
||||
<app-title-toolbar></app-title-toolbar>
|
||||
<!-- editor canvas -->
|
||||
</div>
|
||||
} @else {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ 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 { TitleToolbar } from "@shared/title-toolbar/title-toolbar";
|
||||
|
||||
@Component({
|
||||
selector: 'app-flow-editor',
|
||||
imports: [EditorSidebar],
|
||||
imports: [EditorSidebar, TitleToolbar],
|
||||
templateUrl: './flow-editor.html',
|
||||
styleUrl: './flow-editor.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.enabled:hover {
|
||||
background-color: var(--color-gray-100) ;
|
||||
}
|
||||
|
|
@ -1,13 +1,55 @@
|
|||
<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>
|
||||
<aside
|
||||
class="relative h-full
|
||||
bg-white border-r border-gray-200
|
||||
transition-all duration-300
|
||||
overflow-hidden"
|
||||
[class.w-72]="!collapsed()"
|
||||
[class.w-14]="collapsed()">
|
||||
|
||||
@if (!collapsed()) {
|
||||
<button
|
||||
(click)="collapse()"
|
||||
class="absolute top-3 right-3
|
||||
w-6 h-6 rounded-full
|
||||
bg-white border shadow
|
||||
flex items-center justify-center
|
||||
hover:bg-gray-50 z-10">
|
||||
|
||||
<i class="bi bi-chevron-left text-xs transition-transform"></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="h-full p-4">
|
||||
@if(!collapsed()) {
|
||||
<div class="pt-6 space-y-6">
|
||||
<!-- ACCORDION -->
|
||||
@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>
|
||||
}
|
||||
}
|
||||
</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>
|
||||
}
|
||||
|
||||
<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>
|
||||
</aside>
|
||||
|
|
@ -1,26 +1,35 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Accordion } from '@shared/accordion/accordion';
|
||||
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 { CommonModule } from '@angular/common';
|
||||
|
||||
type AccordionId = 'flows' | 'blocks';
|
||||
type OpenedId = 'flows' | 'blocks';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor-sidebar',
|
||||
imports: [Accordion, FlowsList],
|
||||
imports: [GroupHolder, FlowsList, CommonModule],
|
||||
templateUrl: './editor-sidebar.html',
|
||||
styleUrl: './editor-sidebar.css',
|
||||
})
|
||||
export class EditorSidebar {
|
||||
|
||||
collapsed = false;
|
||||
flowState = inject(EditorStateHolder);
|
||||
|
||||
openAccordion: AccordionId | null = null;
|
||||
blockDisabled = computed(() => !this.flowState.hasFlow());
|
||||
|
||||
collapsed = signal(true);
|
||||
|
||||
open: OpenedId | null = null;
|
||||
|
||||
|
||||
openSide(id: OpenedId) {
|
||||
this.open = id;
|
||||
this.collapsed.set(false);
|
||||
}
|
||||
|
||||
collapse() {
|
||||
this.collapsed = !this.collapsed;
|
||||
}
|
||||
|
||||
toggle(id: AccordionId) {
|
||||
this.openAccordion = this.openAccordion === id ? null : id;
|
||||
this.collapsed.set(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { Flow } from "@models/flow";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class FlowsCallServiceBase {
|
||||
|
||||
abstract retrieveAllFlows() : Observable<Flow[]>;
|
||||
|
||||
abstract updateFlow(flow: Flow) : Observable<void>;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Flow } from "@models/flow";
|
||||
import { FlowsCallServiceBase } from "./flows-call.base";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
export class FlowsCallServiceFake extends FlowsCallServiceBase {
|
||||
|
||||
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' },
|
||||
};
|
||||
|
||||
retrieveAllFlows() {
|
||||
return of(Object.values(this.data));
|
||||
}
|
||||
|
||||
updateFlow(flow: Flow) {
|
||||
this.data[flow.id] = flow;
|
||||
return of(void 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Flow } from "@models/flow";
|
||||
import { FlowsCallServiceBase } from "./flows-call.base";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export class FlowsCallService extends FlowsCallServiceBase {
|
||||
override retrieveAllFlows(): Observable<Flow[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override updateFlow(flow: Flow): Observable<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Flows } from './flows';
|
||||
|
||||
describe('Flows', () => {
|
||||
let service: Flows;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(Flows);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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';
|
||||
|
||||
@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');
|
||||
if (this.toInit) {
|
||||
this.refresh();
|
||||
this.toInit = false;
|
||||
|
||||
}
|
||||
|
||||
return this._flows.asReadonly();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.flowsCallService.retrieveAllFlows().subscribe(flows => {
|
||||
this._flows.set(flows);
|
||||
});
|
||||
}
|
||||
|
||||
updateFlow(flow: Flow) {
|
||||
return this.flowsCallService.updateFlow(flow).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError(err => {
|
||||
console.error('Update flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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>();
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@if(!!this.state()) {
|
||||
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="fixed inset-0 z-9999 flex items-center justify-center">
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/40" (click)="close(false)">
|
||||
|
|
|
|||
|
|
@ -1,29 +1,36 @@
|
|||
@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">
|
||||
|
||||
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter === 'all'"
|
||||
[class.btn-outline-primary]="filter !== 'all'" (click)="filter = 'all'">
|
||||
<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 = 'public'">
|
||||
<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 = 'private'">
|
||||
<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 class="space-y-2">
|
||||
@for (flow of filteredFlows; track flow.id) {
|
||||
@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)">
|
||||
bg-indigo-50 border-indigo-200 select-none" (dblclick)="open(flow)">
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
|
||||
|
|
@ -45,4 +52,5 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
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';
|
||||
|
||||
type FlowFilter = 'all' | 'public' | 'private';
|
||||
|
|
@ -12,21 +13,27 @@ type FlowFilter = 'all' | 'public' | 'private';
|
|||
styleUrl: './flows-list.css',
|
||||
})
|
||||
export class FlowsList {
|
||||
filter: FlowFilter = 'all';
|
||||
filter = signal<FlowFilter>('all');
|
||||
|
||||
private editorState = inject(EditorStateHolder);
|
||||
private flowsService = inject(FlowsService);
|
||||
|
||||
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: {} },
|
||||
]
|
||||
loading: WritableSignal<boolean> = signal(true);
|
||||
|
||||
get filteredFlows() {
|
||||
if (this.filter === 'all') return this.flows;
|
||||
return this.flows.filter(f => f.visibility === this.filter);
|
||||
flows: Signal<Flow[]> | undefined;
|
||||
|
||||
ngOnInit() {
|
||||
this.flowsService.getAllFlows().then(flowsSignal => {
|
||||
this.flows = flowsSignal;
|
||||
this.loading.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
filteredFlows = computed(() => {
|
||||
if (!this.flows) return [];
|
||||
if (this.filter() === 'all') return this.flows();
|
||||
return this.flows().filter(f => f.visibility === this.filter());
|
||||
});
|
||||
|
||||
open(flow: Flow) {
|
||||
console.log('Opening flow:', flow);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<div
|
||||
class="rounded-xl border border-gray-200
|
||||
bg-white shadow-sm
|
||||
transition overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="w-full flex items-center justify-between
|
||||
px-4 py-3
|
||||
text-sm font-semibold text-gray-700
|
||||
bg-gray-50 "
|
||||
>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="bi {{ icon }} text-lg"></i>
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300 max-h-96"
|
||||
>
|
||||
|
||||
<div class="p-3 space-y-2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Accordion } from './accordion';
|
||||
import { GroupHolder } from './group-holder';
|
||||
|
||||
describe('Accordion', () => {
|
||||
let component: Accordion;
|
||||
let fixture: ComponentFixture<Accordion>;
|
||||
describe('GroupHolder', () => {
|
||||
let component: GroupHolder;
|
||||
let fixture: ComponentFixture<GroupHolder>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Accordion]
|
||||
imports: [GroupHolder]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Accordion);
|
||||
fixture = TestBed.createComponent(GroupHolder);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component, EventEmitter, input, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-holder',
|
||||
imports: [],
|
||||
templateUrl: './group-holder.html',
|
||||
styleUrl: './group-holder.css',
|
||||
})
|
||||
export class GroupHolder {
|
||||
@Input() title = '';
|
||||
@Input() icon = '';
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<div class="flex items-center justify-between
|
||||
px-4 py-2
|
||||
bg-white border-b border-gray-200
|
||||
shadow-sm">
|
||||
|
||||
<!-- Title -->
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="bi bi-pencil-square text-indigo-500"></i>
|
||||
<h2 class="m-0 text-sm font-semibold text-gray-800">
|
||||
<input type="text" class="border-0 text-sm font-semibold text-gray-800
|
||||
focus:ring-0 p-0 m-0 box-border
|
||||
min-w-[4ch] max-w-[16ch]
|
||||
bg-transparent"
|
||||
[value]="title()"
|
||||
(blur)="changeTitle(titleInput.value)"
|
||||
(keydown.enter)="changeTitle(titleInput.value); $event.target.blur()"
|
||||
[style.width.ch]="titleWidth()"
|
||||
maxlength="20"
|
||||
size="20"
|
||||
(input)="tempTitle.set(titleInput.value)"
|
||||
#titleInput
|
||||
/>
|
||||
</h2>
|
||||
@if (unsaved()) {
|
||||
<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>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2" >
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
hover:bg-green-200 text-green-600
|
||||
p-1"
|
||||
title="Save" (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"
|
||||
title="Undo">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
hover:bg-gray-100 text-gray-600 p-1"
|
||||
title="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
|
||||
hover:bg-red-200 text-red-600 p-1"
|
||||
title="Clear">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TitleToolbar } from './title-toolbar';
|
||||
|
||||
describe('TitleToolbar', () => {
|
||||
let component: TitleToolbar;
|
||||
let fixture: ComponentFixture<TitleToolbar>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TitleToolbar]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TitleToolbar);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { Component, computed, ElementRef, inject, linkedSignal, ViewChild, WritableSignal } from '@angular/core';
|
||||
import { EditorStateHolder } from 'app/stores/editor';
|
||||
|
||||
@Component({
|
||||
selector: 'app-title-toolbar',
|
||||
imports: [],
|
||||
templateUrl: './title-toolbar.html',
|
||||
styleUrl: './title-toolbar.css',
|
||||
})
|
||||
export class TitleToolbar {
|
||||
|
||||
@ViewChild('titleInput') myInputRef!: ElementRef;
|
||||
|
||||
private editorState: EditorStateHolder = inject(EditorStateHolder);
|
||||
title = computed(() => {
|
||||
const flow = this.editorState.currentFlow();
|
||||
return flow ? flow.name : 'No Flow Opened';
|
||||
});
|
||||
|
||||
tempTitle: WritableSignal<string | null> = linkedSignal(() =>null);
|
||||
|
||||
titleWidth = computed(() => {
|
||||
const temp = this.tempTitle();
|
||||
if (temp !== null) {
|
||||
return Math.max(temp.length-1, 4)
|
||||
}
|
||||
const len = this.title().length || 4;
|
||||
return Math.min(len, 20)-2;
|
||||
});
|
||||
|
||||
unsaved = computed(() => this.editorState.isDirty());
|
||||
|
||||
|
||||
changeTitle(value: string) {
|
||||
this.tempTitle.set(null);
|
||||
const trimmed = value.trim();
|
||||
if (value === this.title()) return;
|
||||
if (trimmed.length < 4) {
|
||||
this.myInputRef.nativeElement.value = this.title();
|
||||
return;
|
||||
}
|
||||
console.log('Updating flow title to:', trimmed);
|
||||
const flow = this.editorState.currentFlow()!;
|
||||
this.editorState.updateFlow({ ...flow, name: trimmed });
|
||||
}
|
||||
|
||||
save() {
|
||||
this.editorState.save().subscribe(
|
||||
err => console.error('Save failed', err)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import { computed, Injectable, signal } from '@angular/core';
|
||||
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 {
|
||||
|
|
@ -12,16 +15,20 @@ export class EditorStateHolder {
|
|||
/** Derived state */
|
||||
readonly hasFlow = computed(() => !!this.currentFlow());
|
||||
|
||||
flowsService: FlowsService = inject(FlowsService);
|
||||
|
||||
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;
|
||||
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);
|
||||
|
|
@ -29,12 +36,12 @@ export class EditorStateHolder {
|
|||
}
|
||||
|
||||
/** Intent: mark editor dirty */
|
||||
markDirty() {
|
||||
private markDirty() {
|
||||
this.isDirty.set(true);
|
||||
}
|
||||
|
||||
/** Intent: save document */
|
||||
markSaved() {
|
||||
private markSaved() {
|
||||
this.isDirty.set(false);
|
||||
}
|
||||
|
||||
|
|
@ -43,4 +50,15 @@ export class EditorStateHolder {
|
|||
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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake";
|
||||
import { FlowsCallServiceFake } from "@services/flows/flows-call.fake";
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiUrl: 'http://localhost:8080',
|
||||
authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here
|
||||
flowsCallService: FlowsCallServiceFake
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { AuthorizationCallService } from "@services/authorization/authorization-call";
|
||||
import { FlowsCallService } from "@services/flows/flows-call";
|
||||
|
||||
export const environment = {
|
||||
apiUrl: '/api',
|
||||
production: true,
|
||||
authorizationCallService: AuthorizationCallService,
|
||||
flowsCallService: FlowsCallService
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue