diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index a6f93a1..1f5e3bd 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -11,6 +11,8 @@ export type Flow = { description?: string; createdAt: Date; updatedAt: Date; + published?: boolean; + finalized?: boolean; }; export type FlowData = { diff --git a/src/app/services/flows/flow-mapper.ts b/src/app/services/flows/flow-mapper.ts new file mode 100644 index 0000000..281cbaa --- /dev/null +++ b/src/app/services/flows/flow-mapper.ts @@ -0,0 +1,49 @@ +import { Flow, FlowData, FlowVisibility } from '@models/flow'; + +function parseDate(value: unknown, fallback: Date): Date { + if (typeof value !== 'string' || !value) return fallback; + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? fallback : parsed; +} + +export function flowFromApi(raw: unknown): Flow { + const value = (raw ?? {}) as Record; + const now = new Date(); + const createdAt = parseDate(value['createdAt'], now); + const updatedAt = parseDate(value['updatedAt'] ?? value['lastUpdateAt'], createdAt); + const data = (value['data'] ?? value['flow'] ?? {}) as Partial; + const published = typeof value['published'] === 'boolean' + ? value['published'] + : ((value['visibility'] as FlowVisibility | undefined) === 'PUBLIC'); + const visibility: FlowVisibility = published ? 'PUBLIC' : 'PRIVATE'; + + return { + id: String(value['id'] ?? crypto.randomUUID()), + name: String(value['name'] ?? 'Untitled Flow'), + description: typeof value['description'] === 'string' ? value['description'] : undefined, + author: String(value['author'] ?? value['owner'] ?? 'unknown'), + createdAt, + updatedAt, + visibility, + published, + finalized: typeof value['finalized'] === 'boolean' ? value['finalized'] : undefined, + data: { + blocks: Array.isArray(data.blocks) ? data.blocks : [], + connections: Array.isArray(data.connections) ? data.connections : [] + } + }; +} + +export function toFlowCreateRequest(name: string) { + return { name }; +} + +export function toFlowUpdateRequest(flow: Flow) { + return { + name: flow.name, + description: flow.description, + published: flow.published ?? flow.visibility === 'PUBLIC', + finalized: flow.finalized ?? false, + flow: flow.data + }; +} diff --git a/src/app/services/flows/flows-call.fake.ts b/src/app/services/flows/flows-call.fake.ts index f1da227..f01cf0a 100644 --- a/src/app/services/flows/flows-call.fake.ts +++ b/src/app/services/flows/flows-call.fake.ts @@ -1,8 +1,9 @@ -import { Flow, FlowVisibility } from "@models/flow"; +import { Flow } from "@models/flow"; import { FlowsCallServiceBase } from "./flows-call.base"; import { Observable, of } from "rxjs"; import { Authorization } from "@services/authorization/authorization"; import { inject } from "@angular/core"; +import { flowFromApi } from "./flow-mapper"; export class FlowsCallServiceFake extends FlowsCallServiceBase { override getFlowById(flowId: string): Observable { @@ -18,7 +19,7 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { private data: Record = { '1': { id: '1', name: 'A Flow', data: { blocks: [], connections: [] }, visibility: 'PUBLIC', author: 'Alice', createdAt: new Date("December 17, 2023 03:24:00"), updatedAt: new Date("January 7, 2026 12:24:00") }, '2': { id: '2', name: 'Test Flow', data: { blocks: [], connections: [] }, visibility: 'PRIVATE', author: 'Bob', createdAt: new Date("April 25, 2025 12:24:00"), updatedAt: new Date("April 27, 2025 18:42:00") }, - 'testFlow': flowFromJson(testDataFlow) + 'testFlow': flowFromApi(testDataFlow) } override retrieveAllFlows() { @@ -41,35 +42,16 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { return of(void 0); } } - - - -export function flowFromJson(raw: any): Flow { - const flowId = raw.id ?? raw.name ?? crypto.randomUUID(); - return { - id: flowId, - author: raw.author, - visibility: raw.visibility as FlowVisibility, - createdAt: new Date(raw.createdAt), - updatedAt: new Date(raw.updatedAt), - name: raw.name, - description: raw.description, - data: { - blocks: raw.blocks, - connections: raw.connections - } - }; -} - - const testDataFlow ={ "id": "testFlow", "name" : "Test Flow", - "visibility": "PRIVATE", - "author": "lucio", + "owner": "lucio", + "published": false, + "finalized": false, "description" : "This is a test flow", "createdAt": "2023-12-17T03:24:00.000Z", - "updatedAt": "2026-01-07T12:24:00.000Z", + "lastUpdateAt": "2026-01-07T12:24:00.000Z", + "flow" : { "blocks" : [ { "id" : "cabd6f4e-5a05-41f8-9bf7-4de20391ac4e", "sink" : false, @@ -128,4 +110,5 @@ const testDataFlow ={ "targetId" : "0063a3ec-3863-4045-bd3b-61eaf87b4604", "targetName" : "input" } ] +} }; diff --git a/src/app/services/flows/flows-call.ts b/src/app/services/flows/flows-call.ts index 07963f6..704a313 100644 --- a/src/app/services/flows/flows-call.ts +++ b/src/app/services/flows/flows-call.ts @@ -1,22 +1,40 @@ -import { Flow } from "@models/flow"; -import { FlowsCallServiceBase } from "./flows-call.base"; -import { Observable } from "rxjs"; +import { HttpClient } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { environment } from '@environment'; +import { Flow } from '@models/flow'; +import { map, Observable } from 'rxjs'; +import { flowFromApi, toFlowCreateRequest, toFlowUpdateRequest } from './flow-mapper'; +import { FlowsCallServiceBase } from './flows-call.base'; export class FlowsCallService extends FlowsCallServiceBase { - override getFlowById(_flowId: string): Observable { - throw new Error("Method not implemented."); - } - override createNewFlow(_name?: string): Observable { - throw new Error("Method not implemented."); - } - override deleteFlow(_flowId: string): Observable { - throw new Error("Method not implemented."); - } - override retrieveAllFlows(): Observable { - throw new Error("Method not implemented."); - } - override updateFlow(_flow: Flow): Observable { - throw new Error("Method not implemented."); - } - + private readonly http = inject(HttpClient); + + override getFlowById(flowId: string): Observable { + const encodedId = encodeURIComponent(flowId); + return this.http + .get(`${environment.apiUrl}/flows/${encodedId}`) + .pipe(map((raw) => flowFromApi(raw))); + } + + override createNewFlow(name?: string): Observable { + return this.http + .post(`${environment.apiUrl}/flows`, toFlowCreateRequest(name ?? 'New Flow')) + .pipe(map((raw) => flowFromApi(raw))); + } + + override deleteFlow(flowId: string): Observable { + const encodedId = encodeURIComponent(flowId); + return this.http.delete(`${environment.apiUrl}/flows/${encodedId}`); + } + + override retrieveAllFlows(): Observable { + return this.http + .get(`${environment.apiUrl}/flows`) + .pipe(map((raw) => raw.map((flow) => flowFromApi(flow)))); + } + + override updateFlow(flow: Flow): Observable { + const encodedId = encodeURIComponent(flow.id); + return this.http.put(`${environment.apiUrl}/flows/${encodedId}`, toFlowUpdateRequest(flow)); + } } diff --git a/src/app/services/retreiver/field-retreiver-call.fake.ts b/src/app/services/retreiver/field-retreiver-call.fake.ts index 1b1678d..d3a7e45 100644 --- a/src/app/services/retreiver/field-retreiver-call.fake.ts +++ b/src/app/services/retreiver/field-retreiver-call.fake.ts @@ -3,8 +3,7 @@ import { FieldRetreiverCallServiceBase } from "./field-retreiver-call.base"; export class FieldRetreiverCallServiceFake extends FieldRetreiverCallServiceBase { private readonly providersByBlockType: Record = { - LLMBlock: ["OpenAI", "Anthropic", "OllamaTestProvider"], - HumanInteractionBlock: ["OpenAI", "OllamaTestProvider"] + LLM: ["OpenAI", "Anthropic", "OllamaTestProvider"], }; private readonly modelsByProvider: Record = { diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index b1f13ff..769ccf0 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -30,6 +30,7 @@ type EditableFieldDefinition = { path: string; label: string; type: FieldType; + retrieverBlockType: string | null; retrieverKey: string | null; retrieverDependsOn: RetrieverDependency[]; ui: { @@ -370,6 +371,7 @@ export class GenericNodeComponent { path, label: pathToLabel(path), type: this.toFieldType(childResolved?.type), + retrieverBlockType: this.toRetrieverBlockType(childResolved), retrieverKey: this.toRetrieverKey(childResolved), retrieverDependsOn: this.toRetrieverDependsOn(childResolved, pathPrefix), ui: this.toFieldUiMeta(childResolved) @@ -438,10 +440,34 @@ export class GenericNodeComponent { private toRetrieverKey(schema: Record | null | undefined): string | null { if (!schema || typeof schema !== 'object') return null; + + const fromUrl = this.parseRetrieverUrl(schema['x-retriever-url'])?.key; + if (fromUrl) return fromUrl; + const retrieverName = schema['x-retriever-name']; return typeof retrieverName === 'string' && retrieverName.length > 0 ? retrieverName : null; } + private toRetrieverBlockType(schema: Record | null | undefined): string | null { + if (!schema || typeof schema !== 'object') return null; + return this.parseRetrieverUrl(schema['x-retriever-url'])?.blockType ?? null; + } + + private parseRetrieverUrl(rawUrl: unknown): { blockType: string; key: string } | null { + if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) return null; + + const path = rawUrl.split('?')[0]; + const parts = path.split('/').filter(Boolean); + const retrieverIndex = parts.findIndex((part) => part === 'retriever'); + if (retrieverIndex < 0 || parts.length < retrieverIndex + 3) return null; + + const blockType = parts[retrieverIndex + 1]; + const key = parts[retrieverIndex + 2]; + if (!blockType || !key) return null; + + return { blockType, key }; + } + private toRetrieverDependsOn(schema: Record | null | undefined, pathPrefix: string): RetrieverDependency[] { if (!schema || typeof schema !== 'object') return []; @@ -458,7 +484,7 @@ export class GenericNodeComponent { } private async loadLocalEditorOptions(definition: EditableFieldDefinition) { - const blockType = this.blockType; + const blockType = definition.retrieverBlockType ?? this.blockType; if (!blockType || !definition.retrieverKey) { this.localEditorLoading = false; return; @@ -660,6 +686,7 @@ export class GenericNodeComponent { } private async fetchConditionalRequirement(blockType: string, field: ConditionalRequiredField) { + const retrieverBlockType = field.retrieverBlockType ?? blockType; const context: Record = {}; for (const dep of field.dependsOn) { const value = this.getByPath(this.blockConfiguration ?? {}, dep.path); @@ -668,7 +695,7 @@ export class GenericNodeComponent { try { return await firstValueFrom( - this.fieldRetriever.isFieldRequired(blockType, field.retrieverKey, context) + this.fieldRetriever.isFieldRequired(retrieverBlockType, field.retrieverKey, context) ); } catch { return false; diff --git a/src/app/shared/nodes/schema-requirements.ts b/src/app/shared/nodes/schema-requirements.ts index e442947..9f10ec5 100644 --- a/src/app/shared/nodes/schema-requirements.ts +++ b/src/app/shared/nodes/schema-requirements.ts @@ -8,6 +8,7 @@ export type RequiredField = { export type ConditionalRequiredField = { path: string; label: string; + retrieverBlockType: string | null; retrieverKey: string; dependsOn: Array<{ key: string; path: string }>; }; @@ -60,7 +61,9 @@ function walkSchema( const retrieverRequiredUrl = propertyResolved?.['x-retriever-required-url']; if (typeof retrieverRequiredUrl === 'string') { - const retrieverKey = String(propertyResolved?.['x-retriever-name'] ?? key); + const parsedRetriever = parseRetrieverUrl(retrieverRequiredUrl); + const retrieverKey = parsedRetriever?.key ?? String(propertyResolved?.['x-retriever-name'] ?? key); + const retrieverBlockType = parsedRetriever?.blockType ?? null; const rawDepends = Array.isArray(propertyResolved?.['x-retriever-required-depends-on']) ? (propertyResolved['x-retriever-required-depends-on'] as unknown[]) : []; @@ -78,6 +81,7 @@ function walkSchema( conditional.push({ path: propertyPath, label, + retrieverBlockType, retrieverKey, dependsOn }); @@ -96,3 +100,19 @@ function toLabel(key: string): string { .replace(/[_-]/g, ' ') .replace(/^./, (c) => c.toUpperCase()); } + +function parseRetrieverUrl(rawUrl: unknown): { blockType: string; key: string } | null { + if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) return null; + + const path = rawUrl.split('?')[0]; + const normalized = path.endsWith('/required') ? path.slice(0, -'/required'.length) : path; + const parts = normalized.split('/').filter(Boolean); + const retrieverIndex = parts.findIndex((part) => part === 'retriever'); + if (retrieverIndex < 0 || parts.length < retrieverIndex + 3) return null; + + const blockType = parts[retrieverIndex + 1]; + const key = parts[retrieverIndex + 2]; + if (!blockType || !key) return null; + + return { blockType, key }; +}