added details to flows
This commit is contained in:
parent
0fbe49f0f3
commit
8e6084c32a
|
|
@ -1,7 +1,7 @@
|
|||
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 { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { TitleToolbar } from "@shared/title-toolbar/title-toolbar";
|
||||
|
||||
@Component({
|
||||
|
|
@ -14,9 +14,5 @@ export class FlowEditor {
|
|||
|
||||
private editorState: EditorStateHolder = inject(EditorStateHolder);
|
||||
|
||||
|
||||
flow = this.editorState.currentFlow;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,7 @@ export type Flow = {
|
|||
name: string;
|
||||
visibility: 'public' | 'private';
|
||||
data: any;
|
||||
author: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
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 { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type OpenedId = 'flows' | 'blocks';
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ import { Observable } from "rxjs";
|
|||
|
||||
export abstract class FlowsCallServiceBase {
|
||||
|
||||
abstract getFlowById(flowId: string) : Observable<Flow>;
|
||||
|
||||
abstract retrieveAllFlows() : Observable<Flow[]>;
|
||||
|
||||
abstract updateFlow(flow: Flow) : Observable<void>;
|
||||
|
||||
abstract createNewFlow() : Observable<Flow>;
|
||||
|
||||
abstract deleteFlow(flowId: string) : Observable<void>;
|
||||
|
||||
}
|
||||
|
|
@ -1,20 +1,38 @@
|
|||
import { Flow } from "@models/flow";
|
||||
import { FlowsCallServiceBase } from "./flows-call.base";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
|
||||
export class FlowsCallServiceFake extends FlowsCallServiceBase {
|
||||
override getFlowById(flowId: string): Observable<Flow> {
|
||||
const flow = this.data[flowId];
|
||||
if (!flow) {
|
||||
throw new Error(`Flow with id ${flowId} not found`);
|
||||
}
|
||||
return of(flow);
|
||||
}
|
||||
|
||||
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' },
|
||||
'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() },
|
||||
};
|
||||
|
||||
retrieveAllFlows() {
|
||||
override retrieveAllFlows() {
|
||||
return of(Object.values(this.data));
|
||||
}
|
||||
|
||||
updateFlow(flow: Flow) {
|
||||
override updateFlow(flow: Flow) {
|
||||
this.data[flow.id] = flow;
|
||||
return of(void 0);
|
||||
}
|
||||
|
||||
override createNewFlow(): 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() };
|
||||
return of(this.data[newId]);
|
||||
}
|
||||
|
||||
override deleteFlow(flowId: string): Observable<void> {
|
||||
delete this.data[flowId];
|
||||
return of(void 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@ import { FlowsCallServiceBase } from "./flows-call.base";
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
export class FlowsCallService extends FlowsCallServiceBase {
|
||||
override getFlowById(flowId: string): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override createNewFlow(): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override deleteFlow(flowId: string): Observable<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override retrieveAllFlows(): Observable<Flow[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,27 @@ 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';
|
||||
import { BehaviorSubject, catchError, combineLatest, Observable, switchMap, 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');
|
||||
console.log('Flows signal accessed');
|
||||
if (this.toInit) {
|
||||
this.refresh();
|
||||
this.toInit = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return this._flows.asReadonly();
|
||||
}
|
||||
|
||||
|
|
@ -36,9 +36,49 @@ export class FlowsService {
|
|||
return this.flowsCallService.updateFlow(flow).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError(err => {
|
||||
console.error('Update flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
console.error('Update flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteFlow(flowId: string) {
|
||||
return this.flowsCallService.deleteFlow(flowId).pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError(err => {
|
||||
console.error('Delete flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
return this.flowsCallService.updateFlow(newFlow);
|
||||
}),
|
||||
tap(() => this.refresh()),
|
||||
catchError(err => {
|
||||
console.error('Cloning flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
createNewFlow() {
|
||||
return this.flowsCallService.createNewFlow().pipe(
|
||||
tap(() => this.refresh()),
|
||||
catchError(err => {
|
||||
console.error('Create new flow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,30 +26,7 @@
|
|||
|
||||
<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 select-none" (dblclick)="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>
|
||||
<app-flow-item [flow]="flow"></app-flow-item>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,20 +2,20 @@ import { CommonModule } from '@angular/common';
|
|||
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';
|
||||
import { FlowItem } from './flow-item/flow-item';
|
||||
|
||||
type FlowFilter = 'all' | 'public' | 'private';
|
||||
|
||||
@Component({
|
||||
selector: 'app-flows-list',
|
||||
imports: [CommonModule],
|
||||
imports: [FlowItem],
|
||||
templateUrl: './flows-list.html',
|
||||
styleUrl: './flows-list.css',
|
||||
})
|
||||
export class FlowsList {
|
||||
filter = signal<FlowFilter>('all');
|
||||
|
||||
private editorState = inject(EditorStateHolder);
|
||||
|
||||
private flowsService = inject(FlowsService);
|
||||
|
||||
loading: WritableSignal<boolean> = signal(true);
|
||||
|
|
@ -35,8 +35,5 @@ export class FlowsList {
|
|||
return this.flows().filter(f => f.visibility === this.filter());
|
||||
});
|
||||
|
||||
open(flow: Flow) {
|
||||
console.log('Opening flow:', flow);
|
||||
this.editorState.openDocument(flow);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
#titleInput
|
||||
/>
|
||||
</h2>
|
||||
@if (unsaved()) {
|
||||
@if (notSaved()) {
|
||||
<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>
|
||||
|
|
@ -32,16 +32,16 @@
|
|||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
hover:bg-green-200 text-green-600
|
||||
enabled:hover:bg-green-200 text-green-600
|
||||
p-1"
|
||||
title="Save" (click)="save()">
|
||||
title="Save" [disabled]="!notSaved()" (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"
|
||||
enabled:hover:bg-gray-100 text-gray-600 p-1"
|
||||
title="Undo">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
hover:bg-gray-100 text-gray-600 p-1"
|
||||
enabled:hover:bg-gray-100 text-gray-600 p-1"
|
||||
title="Redo">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
hover:bg-red-200 text-red-600 p-1"
|
||||
enabled:hover:bg-red-200 text-red-600 p-1"
|
||||
title="Clear">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, computed, ElementRef, inject, linkedSignal, ViewChild, WritableSignal } from '@angular/core';
|
||||
import { EditorStateHolder } from 'app/stores/editor';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
|
||||
@Component({
|
||||
selector: 'app-title-toolbar',
|
||||
|
|
@ -28,7 +28,7 @@ export class TitleToolbar {
|
|||
return Math.min(len, 20)-2;
|
||||
});
|
||||
|
||||
unsaved = computed(() => this.editorState.isDirty());
|
||||
notSaved = computed(() => this.editorState.isDirty());
|
||||
|
||||
|
||||
changeTitle(value: string) {
|
||||
|
|
@ -45,6 +45,7 @@ export class TitleToolbar {
|
|||
}
|
||||
|
||||
save() {
|
||||
if (!this.notSaved()) return;
|
||||
this.editorState.save().subscribe(
|
||||
err => console.error('Save failed', err)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
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 {
|
||||
|
||||
/** 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) { }
|
||||
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
updateFlow(updated: Flow) {
|
||||
this.currentFlow.set(updated);
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.flowsService.updateFlow(this.currentFlow()!).pipe(
|
||||
tap(() => this.markSaved())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,9 @@
|
|||
],
|
||||
"@assets/*": [
|
||||
"/assets/*"
|
||||
],
|
||||
"@stores/*": [
|
||||
"app/stores/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue