Split containers into dedicated services and sidebar list
This commit is contained in:
parent
25e133ac04
commit
533a1e7f43
|
|
@ -21,8 +21,22 @@
|
|||
[ngClass]="{'cursor-pointer enabled' : open != 'flows'}"
|
||||
class="bi bi-lightning-charge-fill text-indigo-500 text-xl p-2"
|
||||
(click)="open != 'flows' && 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>
|
||||
<i
|
||||
[ngClass]="{
|
||||
'cursor-pointer text-gray-600 enabled': !blockDisabled() && open != 'blocks',
|
||||
'cursor-none bg-gray-200 text-sky-700': open == 'blocks',
|
||||
'text-gray-200': blockDisabled()
|
||||
}"
|
||||
class="bi bi-boxes text-xl p-2"
|
||||
(click)="!blockDisabled() && open != 'blocks' && openSide('blocks')"></i>
|
||||
<i
|
||||
[ngClass]="{
|
||||
'cursor-pointer text-gray-600 enabled': !blockDisabled() && open != 'containers',
|
||||
'cursor-none bg-gray-200 text-cyan-700': open == 'containers',
|
||||
'text-gray-200': blockDisabled()
|
||||
}"
|
||||
class="bi bi-box-seam text-xl p-2"
|
||||
(click)="!blockDisabled() && open != 'containers' && openSide('containers')"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -51,6 +65,11 @@
|
|||
<app-blocks-list></app-blocks-list>
|
||||
</app-group-holder>
|
||||
}
|
||||
@case ('containers') {
|
||||
<app-group-holder title="Containers" icon="bi-box-seam">
|
||||
<app-containers-list></app-containers-list>
|
||||
</app-group-holder>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,18 @@ 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 { BlocksList } from '@shared/blocks-list/blocks-list';
|
||||
import { ContainersList } from '@shared/containers-list/containers-list';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FlowsService } from '@services/flows/flows';
|
||||
import { ListState } from '@stores/list-state';
|
||||
import { finalize } from 'rxjs';
|
||||
|
||||
type OpenedId = 'flows' | 'blocks';
|
||||
type OpenedId = 'flows' | 'blocks' | 'containers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor-sidebar',
|
||||
imports: [GroupHolder, FlowsList, BlocksList, CommonModule],
|
||||
imports: [GroupHolder, FlowsList, BlocksList, ContainersList, CommonModule],
|
||||
templateUrl: './editor-sidebar.html',
|
||||
styleUrl: './editor-sidebar.css',
|
||||
providers:[ListState]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import { BlockType, BlockTypeName, FlowBlock, FlowContainer, FlowData, FlowSubflowValidationResult } from "@models/flow";
|
||||
import { BlockType, BlockTypeName, FlowBlock } from "@models/flow";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class BlocksCallServiceBase {
|
||||
|
||||
abstract retrieveAllBlocksTypes() : Observable<BlockType[]>;
|
||||
|
||||
abstract retrieveAllContainerTypes() : Observable<BlockType[]>;
|
||||
|
||||
abstract createEmptyBlock(blockType: BlockTypeName) : Observable<FlowBlock>;
|
||||
|
||||
abstract createEmptyContainer(containerType: BlockTypeName) : Observable<FlowContainer>;
|
||||
|
||||
abstract updateBlock(blockId : string, configuration : any) : Observable<FlowBlock>;
|
||||
|
||||
abstract validateContainerSubflow(subFlow: FlowData) : Observable<FlowSubflowValidationResult>;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BlockType, FlowBlock, FlowContainer, FlowData, FlowSubflowValidationResult } from "@models/flow";
|
||||
import { BlockType, FlowBlock } from "@models/flow";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { BlocksCallServiceBase } from "./block-call.base";
|
||||
|
||||
|
|
@ -135,64 +135,10 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase {
|
|||
},
|
||||
];
|
||||
|
||||
private readonly containerTypes: BlockType[] = [
|
||||
{
|
||||
"type": "GenericContainer",
|
||||
"family": "container",
|
||||
"description": "Container block with an embedded validated subflow",
|
||||
"userInteractive": false,
|
||||
"configurationType": "GenericContainerConfiguration",
|
||||
"configurationClass": "it.cnr.isti.workflow.manager.blocks.configurations.GenericContainerConfiguration",
|
||||
"schema": {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "GenericContainerConfiguration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"GenericContainerConfiguration"
|
||||
],
|
||||
"default": "GenericContainerConfiguration"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": "Container"
|
||||
},
|
||||
"subFlow": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"blocks": [],
|
||||
"containers": [],
|
||||
"connections": []
|
||||
}
|
||||
},
|
||||
"publicInputs": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"publicOutputs": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
override retrieveAllBlocksTypes(): Observable<BlockType[]> {
|
||||
return of(this.blockTypes);
|
||||
}
|
||||
|
||||
override retrieveAllContainerTypes(): Observable<BlockType[]> {
|
||||
return of(this.containerTypes);
|
||||
}
|
||||
|
||||
override createEmptyBlock(blockType: string): Observable<FlowBlock> {
|
||||
const descriptor = this.blockTypes.find((b) => b.type === blockType);
|
||||
const typeName = descriptor?.type ?? blockType ?? "LLMBlock";
|
||||
|
|
@ -223,27 +169,6 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase {
|
|||
return of(block);
|
||||
}
|
||||
|
||||
override createEmptyContainer(containerType: string): Observable<FlowContainer> {
|
||||
const descriptor = this.containerTypes.find((b) => b.type === containerType);
|
||||
const typeName = descriptor?.type ?? containerType ?? "GenericContainer";
|
||||
const schema = descriptor?.schema as Record<string, any> | null;
|
||||
|
||||
const specificConfiguration = schema
|
||||
? this.buildObjectFromSchema(schema, schema)
|
||||
: {};
|
||||
|
||||
return of({
|
||||
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,
|
||||
name: String((specificConfiguration as any)?.name ?? typeName),
|
||||
position: undefined,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
specificConfiguration,
|
||||
typeName,
|
||||
nodeFamily: 'container'
|
||||
});
|
||||
}
|
||||
|
||||
override updateBlock(blockId: string, configuration: any): Observable<FlowBlock> {
|
||||
const typeName = configuration?.typeName ?? "LLMBlock";
|
||||
const io = this.defaultIOForBlockType(typeName);
|
||||
|
|
@ -260,35 +185,6 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase {
|
|||
return of(block);
|
||||
}
|
||||
|
||||
override validateContainerSubflow(subFlow: FlowData): Observable<FlowSubflowValidationResult> {
|
||||
const blocks = Array.isArray(subFlow?.blocks) ? subFlow.blocks : [];
|
||||
if (!blocks.length) {
|
||||
return of({
|
||||
valid: false,
|
||||
errors: [{ entity: 'flow', field: 'blocks', message: 'Subflow cannot be empty' }],
|
||||
openInputs: [],
|
||||
openOutputs: []
|
||||
});
|
||||
}
|
||||
|
||||
const nestedContainer = (subFlow?.containers ?? []).find((container) => container?.typeName === 'GenericContainer');
|
||||
if (nestedContainer) {
|
||||
return of({
|
||||
valid: false,
|
||||
errors: [{
|
||||
entity: 'block',
|
||||
id: nestedContainer.id,
|
||||
field: 'type',
|
||||
message: 'Nested GenericContainer blocks are not supported'
|
||||
}],
|
||||
openInputs: [],
|
||||
openOutputs: []
|
||||
});
|
||||
}
|
||||
|
||||
return of({ valid: true, errors: [], openInputs: [], openOutputs: [] });
|
||||
}
|
||||
|
||||
private defaultIOForBlockType(typeName: string) {
|
||||
if (typeName === "SourceBlock") {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BlockType, FlowBlock, FlowContainer, FlowData, FlowSubflowValidationResult, NodeFamily } from "@models/flow";
|
||||
import { BlockType, FlowBlock } from "@models/flow";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
import { environment } from "@environment";
|
||||
|
|
@ -8,13 +8,12 @@ import { BlocksCallServiceBase } from "./block-call.base";
|
|||
export class BlocksCallService extends BlocksCallServiceBase {
|
||||
private readonly http = inject(HttpClient);
|
||||
private blockTypesCache: BlockType[] | null = null;
|
||||
private containerTypesCache: BlockType[] | null = null;
|
||||
|
||||
override retrieveAllBlocksTypes(): Observable<BlockType[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/blocks/types`)
|
||||
.pipe(
|
||||
map((raw) => (Array.isArray(raw) ? raw.map((value) => this.blockTypeFromApi(value, 'block')) : [])),
|
||||
map((raw) => (Array.isArray(raw) ? raw.map((value) => this.blockTypeFromApi(value)) : [])),
|
||||
map((types) => {
|
||||
this.blockTypesCache = types;
|
||||
return types;
|
||||
|
|
@ -22,29 +21,17 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
);
|
||||
}
|
||||
|
||||
override retrieveAllContainerTypes(): Observable<BlockType[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/containers/types`)
|
||||
.pipe(
|
||||
map((raw) => (Array.isArray(raw) ? raw.map((value) => this.blockTypeFromApi(value, 'container')) : [])),
|
||||
map((types) => {
|
||||
this.containerTypesCache = types;
|
||||
return types;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override createEmptyBlock(blockType: string): Observable<FlowBlock> {
|
||||
return this.getBlockTypesForCreate().pipe(
|
||||
take(1),
|
||||
switchMap((types) => {
|
||||
const descriptor = types.find((type) => type.type === blockType);
|
||||
const exampleEndpoint = this.resolveExampleEndpoint('block', blockType, descriptor);
|
||||
const exampleEndpoint = this.resolveExampleEndpoint(blockType, descriptor);
|
||||
|
||||
if (exampleEndpoint) {
|
||||
return this.http
|
||||
.get<unknown>(exampleEndpoint)
|
||||
.pipe(map((raw) => this.flowNodeFromApi(raw, descriptor?.type ?? blockType, 'block') as FlowBlock));
|
||||
.pipe(map((raw) => this.flowBlockFromApi(raw, descriptor?.type ?? blockType)));
|
||||
}
|
||||
|
||||
const configuration = descriptor
|
||||
|
|
@ -54,21 +41,7 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
|
||||
return this.http
|
||||
.post<unknown>(`${environment.apiUrl}/blocks`, payload)
|
||||
.pipe(map((raw) => this.flowNodeFromApi(raw, descriptor?.type ?? blockType, 'block', payload) as FlowBlock));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override createEmptyContainer(containerType: string): Observable<FlowContainer> {
|
||||
return this.getContainerTypesForCreate().pipe(
|
||||
take(1),
|
||||
switchMap((types) => {
|
||||
const descriptor = types.find((type) => type.type === containerType);
|
||||
const exampleEndpoint = this.resolveExampleEndpoint('container', containerType, descriptor);
|
||||
|
||||
return this.http
|
||||
.get<unknown>(exampleEndpoint)
|
||||
.pipe(map((raw) => this.flowNodeFromApi(raw, descriptor?.type ?? containerType, 'container') as FlowContainer));
|
||||
.pipe(map((raw) => this.flowBlockFromApi(raw, descriptor?.type ?? blockType, payload)));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -104,15 +77,6 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
);
|
||||
}
|
||||
|
||||
override validateContainerSubflow(subFlow: FlowData): Observable<FlowSubflowValidationResult> {
|
||||
return this.http
|
||||
.post<unknown>(
|
||||
`${environment.apiUrl}/containers/types/GenericContainer/validate-subflow`,
|
||||
{ subFlow }
|
||||
)
|
||||
.pipe(map((raw) => this.subflowValidationFromApi(raw)));
|
||||
}
|
||||
|
||||
private getBlockTypesForCreate(): Observable<BlockType[]> {
|
||||
if (this.blockTypesCache) {
|
||||
return of(this.blockTypesCache);
|
||||
|
|
@ -120,18 +84,11 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
return this.retrieveAllBlocksTypes();
|
||||
}
|
||||
|
||||
private getContainerTypesForCreate(): Observable<BlockType[]> {
|
||||
if (this.containerTypesCache) {
|
||||
return of(this.containerTypesCache);
|
||||
}
|
||||
return this.retrieveAllContainerTypes();
|
||||
}
|
||||
|
||||
private blockTypeFromApi(raw: unknown, family: NodeFamily): BlockType {
|
||||
private blockTypeFromApi(raw: unknown): BlockType {
|
||||
const value = this.toRecord(raw);
|
||||
return {
|
||||
type: String(value["type"] ?? value["blockType"] ?? value["name"] ?? "LLMBlock"),
|
||||
family,
|
||||
family: 'block',
|
||||
description: String(value["description"] ?? ""),
|
||||
userInteractive: Boolean(value["userInteractive"] ?? value["interactive"] ?? false),
|
||||
hasExampleBlock: Boolean(value["hasExampleBlock"] ?? false),
|
||||
|
|
@ -142,49 +99,9 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
};
|
||||
}
|
||||
|
||||
private subflowValidationFromApi(raw: unknown): FlowSubflowValidationResult {
|
||||
const value = this.toRecord(raw);
|
||||
const rawErrors = Array.isArray(value['errors']) ? value['errors'] : [];
|
||||
|
||||
return {
|
||||
valid: Boolean(value['valid'] ?? false),
|
||||
errors: rawErrors
|
||||
.map((item) => this.toRecord(item))
|
||||
.map((item) => ({
|
||||
entity: this.toNullableString(item['entity']) ?? undefined,
|
||||
id: this.toNullableString(item['id']) ?? undefined,
|
||||
field: this.toNullableString(item['field']) ?? undefined,
|
||||
message: String(item['message'] ?? 'Invalid subflow')
|
||||
})),
|
||||
openInputs: this.toPorts(value['openInputs'], []).map((port) => ({
|
||||
...port,
|
||||
targetBlockId: this.toNullableString(this.toRecord(port)['targetBlockId']) ?? undefined,
|
||||
targetInputName: this.toNullableString(this.toRecord(port)['targetInputName']) ?? undefined,
|
||||
blockId: this.toNullableString(this.toRecord(port)['blockId']) ?? undefined,
|
||||
inputName: this.toNullableString(this.toRecord(port)['inputName']) ?? undefined
|
||||
})),
|
||||
openOutputs: this.toPorts(value['openOutputs'], []).map((port) => ({
|
||||
...port,
|
||||
sourceBlockId: this.toNullableString(this.toRecord(port)['sourceBlockId']) ?? undefined,
|
||||
sourceOutputName: this.toNullableString(this.toRecord(port)['sourceOutputName']) ?? undefined,
|
||||
blockId: this.toNullableString(this.toRecord(port)['blockId']) ?? undefined,
|
||||
outputName: this.toNullableString(this.toRecord(port)['outputName']) ?? undefined
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
private flowBlockFromApi(raw: unknown, fallbackTypeName = "LLMBlock", fallbackConfig?: Record<string, unknown>): FlowBlock {
|
||||
return this.flowNodeFromApi(raw, fallbackTypeName, 'block', fallbackConfig) as FlowBlock;
|
||||
}
|
||||
|
||||
private flowNodeFromApi(
|
||||
raw: unknown,
|
||||
fallbackTypeName = "LLMBlock",
|
||||
family: NodeFamily = 'block',
|
||||
fallbackConfig?: Record<string, unknown>
|
||||
) {
|
||||
const root = this.toRecord(raw);
|
||||
const value = this.toRecord(root[family] ?? root["node"] ?? root["block"] ?? root["container"] ?? root["data"] ?? root);
|
||||
const value = this.toRecord(root["block"] ?? root["node"] ?? root["data"] ?? root);
|
||||
const specificConfigurationRaw = value["specificConfiguration"] ?? value["configuration"] ?? value["blockConfiguration"] ?? fallbackConfig ?? {};
|
||||
const specificConfiguration = this.toRecord(specificConfigurationRaw);
|
||||
const typeName = String(value["typeName"] ?? value["blockType"] ?? specificConfiguration["typeName"] ?? fallbackTypeName);
|
||||
|
|
@ -198,7 +115,7 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
outputs: this.toPorts(value["outputs"], io.outputs),
|
||||
specificConfiguration,
|
||||
typeName,
|
||||
nodeFamily: family
|
||||
nodeFamily: 'block'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -274,12 +191,11 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
};
|
||||
}
|
||||
|
||||
private resolveExampleEndpoint(family: NodeFamily, typeName: string, descriptor?: BlockType): string {
|
||||
private resolveExampleEndpoint(typeName: string, descriptor?: BlockType): string {
|
||||
if (descriptor?.hasExampleBlock && descriptor.exampleBlockEndpoint) {
|
||||
return descriptor.exampleBlockEndpoint;
|
||||
}
|
||||
const base = family === 'container' ? 'containers' : 'blocks';
|
||||
return `${environment.apiUrl}/${base}/types/${encodeURIComponent(typeName)}/example`;
|
||||
return `${environment.apiUrl}/blocks/types/${encodeURIComponent(typeName)}/example`;
|
||||
}
|
||||
|
||||
private toUpdateBlockError(error: unknown, blockType: string): Error {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { environment } from '@environment';
|
||||
import { BlockType, BlockTypeName, FlowData, FlowNode, NodeFamily } from '@models/flow';
|
||||
import { BlockType, BlockTypeName, FlowBlock } from '@models/flow';
|
||||
import { BlocksCallServiceBase } from './block-call.base';
|
||||
import { catchError, finalize, firstValueFrom, forkJoin, map, Observable, of, shareReplay, throwError } from 'rxjs';
|
||||
import { catchError, finalize, firstValueFrom, map, Observable, of, shareReplay, throwError } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
@ -12,12 +12,17 @@ export class BlocksService {
|
|||
|
||||
toInit: boolean = true;
|
||||
private loadingPromise: Promise<void> | null = null;
|
||||
private readonly emptyBlockCache = new Map<string, FlowNode>();
|
||||
private readonly pendingEmptyBlockRequests = new Map<string, Observable<FlowNode>>();
|
||||
private readonly emptyBlockCache = new Map<string, FlowBlock>();
|
||||
private readonly pendingEmptyBlockRequests = new Map<string, Observable<FlowBlock>>();
|
||||
private readonly pendingServerSyncCount = signal(0);
|
||||
|
||||
private _blockTypes = signal<BlockType[]>([]);
|
||||
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
|
||||
readonly blockTypes = this._blockTypes.asReadonly();
|
||||
|
||||
hasLoadedBlockTypes() {
|
||||
return this._blockTypes().length > 0 || !this.toInit;
|
||||
}
|
||||
|
||||
async getAllBlocksTypes() {
|
||||
if (this.toInit) {
|
||||
|
|
@ -33,12 +38,9 @@ export class BlocksService {
|
|||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
this.loadingPromise = firstValueFrom(forkJoin({
|
||||
blocks: this.blocksCallService.retrieveAllBlocksTypes(),
|
||||
containers: this.blocksCallService.retrieveAllContainerTypes()
|
||||
}))
|
||||
.then(({ blocks, containers }) => {
|
||||
this._blockTypes.set([...blocks, ...containers]);
|
||||
this.loadingPromise = firstValueFrom(this.blocksCallService.retrieveAllBlocksTypes())
|
||||
.then((blockTypes) => {
|
||||
this._blockTypes.set(blockTypes);
|
||||
this.clearEmptyBlockCache();
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -56,31 +58,27 @@ export class BlocksService {
|
|||
const current = this._blockTypes().find((blockType) => blockType.type === typeName);
|
||||
if (current) return current;
|
||||
|
||||
const { blocks, containers } = await firstValueFrom(forkJoin({
|
||||
blocks: this.blocksCallService.retrieveAllBlocksTypes(),
|
||||
containers: this.blocksCallService.retrieveAllContainerTypes()
|
||||
}));
|
||||
const blockTypes = [...blocks, ...containers];
|
||||
const blockTypes = await firstValueFrom(this.blocksCallService.retrieveAllBlocksTypes());
|
||||
this._blockTypes.set(blockTypes);
|
||||
this.clearEmptyBlockCache();
|
||||
return blockTypes.find((blockType) => blockType.type === typeName);
|
||||
}
|
||||
|
||||
createEmptyBlock(blockType: BlockTypeName, family?: NodeFamily) {
|
||||
const cacheKey = `${family ?? 'auto'}:${String(blockType)}`;
|
||||
createEmptyBlock(blockType: BlockTypeName) {
|
||||
const cacheKey = String(blockType);
|
||||
const cached = this.emptyBlockCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return of(this.cloneEmptyNode(cached));
|
||||
return of(this.cloneEmptyBlock(cached));
|
||||
}
|
||||
|
||||
const pending = this.pendingEmptyBlockRequests.get(cacheKey);
|
||||
if (pending) {
|
||||
return pending.pipe(map((block) => this.cloneEmptyNode(block)));
|
||||
return pending.pipe(map((block) => this.cloneEmptyBlock(block)));
|
||||
}
|
||||
|
||||
const request = this.createEmptyNodeRequest(blockType, family).pipe(
|
||||
const request = this.blocksCallService.createEmptyBlock(blockType).pipe(
|
||||
map((block) => {
|
||||
this.emptyBlockCache.set(cacheKey, this.cloneEmptyNode(block));
|
||||
this.emptyBlockCache.set(cacheKey, this.cloneEmptyBlock(block));
|
||||
return block;
|
||||
}),
|
||||
finalize(() => {
|
||||
|
|
@ -92,7 +90,7 @@ export class BlocksService {
|
|||
this.pendingEmptyBlockRequests.set(cacheKey, request);
|
||||
|
||||
return request.pipe(
|
||||
map((block) => this.cloneEmptyNode(block)),
|
||||
map((block) => this.cloneEmptyBlock(block)),
|
||||
catchError((err) => {
|
||||
console.error('Create empty block failed', err);
|
||||
return throwError(() => err);
|
||||
|
|
@ -113,28 +111,12 @@ export class BlocksService {
|
|||
);
|
||||
}
|
||||
|
||||
validateContainerSubflow(subFlow: FlowData) {
|
||||
return this.blocksCallService.validateContainerSubflow(this.deepClone(subFlow)).pipe(
|
||||
catchError((err) => {
|
||||
console.error('Validate container subflow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private clearEmptyBlockCache() {
|
||||
this.emptyBlockCache.clear();
|
||||
this.pendingEmptyBlockRequests.clear();
|
||||
}
|
||||
|
||||
private createEmptyNodeRequest(blockType: BlockTypeName, family?: NodeFamily): Observable<FlowNode> {
|
||||
const normalizedFamily = family ?? this._blockTypes().find((type) => type.type === blockType)?.family ?? 'block';
|
||||
return normalizedFamily === 'container'
|
||||
? this.blocksCallService.createEmptyContainer(blockType)
|
||||
: this.blocksCallService.createEmptyBlock(blockType);
|
||||
}
|
||||
|
||||
private cloneEmptyNode(block: FlowNode): FlowNode {
|
||||
private cloneEmptyBlock(block: FlowBlock): FlowBlock {
|
||||
const clone = this.deepClone(block);
|
||||
return {
|
||||
...clone,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { BlockType, BlockTypeName, FlowContainer, FlowData, FlowSubflowValidationResult } from "@models/flow";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class ContainersCallServiceBase {
|
||||
abstract retrieveAllContainerTypes(): Observable<BlockType[]>;
|
||||
|
||||
abstract createEmptyContainer(containerType: BlockTypeName): Observable<FlowContainer>;
|
||||
|
||||
abstract validateContainerSubflow(subFlow: FlowData): Observable<FlowSubflowValidationResult>;
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { BlockType, FlowContainer, FlowData, FlowSubflowValidationResult } from "@models/flow";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { ContainersCallServiceBase } from "./container-call.base";
|
||||
|
||||
export class ContainersCallServiceFake extends ContainersCallServiceBase {
|
||||
private readonly containerTypes: BlockType[] = [
|
||||
{
|
||||
type: "GenericContainer",
|
||||
family: "container",
|
||||
description: "Container node with an embedded validated subflow",
|
||||
userInteractive: false,
|
||||
configurationType: "GenericContainerConfiguration",
|
||||
configurationClass: "it.cnr.isti.workflow.manager.blocks.configurations.GenericContainerConfiguration",
|
||||
schema: {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "GenericContainerConfiguration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["GenericContainerConfiguration"],
|
||||
"default": "GenericContainerConfiguration"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": "Container"
|
||||
},
|
||||
"subFlow": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"blocks": [],
|
||||
"containers": [],
|
||||
"connections": []
|
||||
}
|
||||
},
|
||||
"publicInputs": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"publicOutputs": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"required": ["type", "name"]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
override retrieveAllContainerTypes(): Observable<BlockType[]> {
|
||||
return of(this.containerTypes);
|
||||
}
|
||||
|
||||
override createEmptyContainer(containerType: string): Observable<FlowContainer> {
|
||||
const descriptor = this.containerTypes.find((container) => container.type === containerType);
|
||||
return of({
|
||||
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,
|
||||
name: String((descriptor?.schema as any)?.properties?.name?.default ?? containerType),
|
||||
position: undefined,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
specificConfiguration: {
|
||||
type: "GenericContainerConfiguration",
|
||||
name: "Container",
|
||||
subFlow: {
|
||||
blocks: [],
|
||||
containers: [],
|
||||
connections: []
|
||||
},
|
||||
publicInputs: [],
|
||||
publicOutputs: []
|
||||
},
|
||||
typeName: descriptor?.type ?? containerType,
|
||||
nodeFamily: 'container'
|
||||
});
|
||||
}
|
||||
|
||||
override validateContainerSubflow(subFlow: FlowData): Observable<FlowSubflowValidationResult> {
|
||||
const blocks = Array.isArray(subFlow?.blocks) ? subFlow.blocks : [];
|
||||
const containers = Array.isArray(subFlow?.containers) ? subFlow.containers : [];
|
||||
|
||||
if (!blocks.length && !containers.length) {
|
||||
return of({
|
||||
valid: false,
|
||||
errors: [{ entity: 'flow', field: 'blocks', message: 'Subflow cannot be empty' }],
|
||||
openInputs: [],
|
||||
openOutputs: []
|
||||
});
|
||||
}
|
||||
|
||||
const nestedContainer = containers.find((container) => container?.typeName === 'GenericContainer');
|
||||
if (nestedContainer) {
|
||||
return of({
|
||||
valid: false,
|
||||
errors: [{
|
||||
entity: 'container',
|
||||
id: nestedContainer.id,
|
||||
field: 'type',
|
||||
message: 'Nested GenericContainer nodes are not supported'
|
||||
}],
|
||||
openInputs: [],
|
||||
openOutputs: []
|
||||
});
|
||||
}
|
||||
|
||||
return of({
|
||||
valid: true,
|
||||
errors: [],
|
||||
openInputs: [],
|
||||
openOutputs: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
import { environment } from "@environment";
|
||||
import {
|
||||
BlockType,
|
||||
FlowContainer,
|
||||
FlowData,
|
||||
FlowSubflowValidationResult
|
||||
} from "@models/flow";
|
||||
import { map, Observable } from "rxjs";
|
||||
import { ContainersCallServiceBase } from "./container-call.base";
|
||||
|
||||
export class ContainersCallService extends ContainersCallServiceBase {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
override retrieveAllContainerTypes(): Observable<BlockType[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/containers/types`)
|
||||
.pipe(
|
||||
map((raw) => (Array.isArray(raw) ? raw.map((value) => this.containerTypeFromApi(value)) : []))
|
||||
);
|
||||
}
|
||||
|
||||
override createEmptyContainer(containerType: string): Observable<FlowContainer> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/containers/types/${encodeURIComponent(containerType)}/example`)
|
||||
.pipe(map((raw) => this.flowContainerFromApi(raw, containerType)));
|
||||
}
|
||||
|
||||
override validateContainerSubflow(subFlow: FlowData): Observable<FlowSubflowValidationResult> {
|
||||
return this.http
|
||||
.post<unknown>(
|
||||
`${environment.apiUrl}/containers/types/GenericContainer/validate-subflow`,
|
||||
{ subFlow }
|
||||
)
|
||||
.pipe(map((raw) => this.subflowValidationFromApi(raw)));
|
||||
}
|
||||
|
||||
private containerTypeFromApi(raw: unknown): BlockType {
|
||||
const value = this.toRecord(raw);
|
||||
return {
|
||||
type: String(value["type"] ?? value["containerType"] ?? value["name"] ?? "GenericContainer"),
|
||||
family: 'container',
|
||||
description: String(value["description"] ?? ""),
|
||||
userInteractive: Boolean(value["userInteractive"] ?? value["interactive"] ?? false),
|
||||
hasExampleBlock: Boolean(value["hasExampleBlock"] ?? false),
|
||||
exampleBlockEndpoint: this.toApiPath(value["exampleBlockEndpoint"]),
|
||||
configurationType: this.toNullableString(value["configurationType"]),
|
||||
configurationClass: this.toNullableString(value["configurationClass"]),
|
||||
schema: this.toSchema(value["schema"] ?? value["configurationSchema"] ?? null)
|
||||
};
|
||||
}
|
||||
|
||||
private flowContainerFromApi(raw: unknown, fallbackTypeName = "GenericContainer"): FlowContainer {
|
||||
const root = this.toRecord(raw);
|
||||
const value = this.toRecord(root["container"] ?? root["node"] ?? root["data"] ?? root);
|
||||
const specificConfigurationRaw = value["specificConfiguration"] ?? value["configuration"] ?? value["containerConfiguration"] ?? {};
|
||||
const specificConfiguration = this.toRecord(specificConfigurationRaw);
|
||||
const typeName = String(value["typeName"] ?? value["containerType"] ?? specificConfiguration["typeName"] ?? fallbackTypeName);
|
||||
|
||||
return {
|
||||
id: String(value["id"] ?? crypto.randomUUID()),
|
||||
name: String(value["name"] ?? specificConfiguration["name"] ?? typeName),
|
||||
position: this.toPosition(value["position"]),
|
||||
inputs: this.toPorts(value["inputs"]),
|
||||
outputs: this.toPorts(value["outputs"]),
|
||||
specificConfiguration,
|
||||
typeName,
|
||||
nodeFamily: 'container'
|
||||
};
|
||||
}
|
||||
|
||||
private subflowValidationFromApi(raw: unknown): FlowSubflowValidationResult {
|
||||
const value = this.toRecord(raw);
|
||||
const rawErrors = Array.isArray(value['errors']) ? value['errors'] : [];
|
||||
|
||||
return {
|
||||
valid: Boolean(value['valid'] ?? false),
|
||||
errors: rawErrors
|
||||
.map((item) => this.toRecord(item))
|
||||
.map((item) => ({
|
||||
entity: this.toNullableString(item['entity']) ?? undefined,
|
||||
id: this.toNullableString(item['id']) ?? undefined,
|
||||
field: this.toNullableString(item['field']) ?? undefined,
|
||||
message: String(item['message'] ?? 'Invalid subflow')
|
||||
})),
|
||||
openInputs: this.toPorts(value['openInputs']).map((port) => ({
|
||||
...port,
|
||||
targetBlockId: this.toNullableString(this.toRecord(port)['targetBlockId']) ?? undefined,
|
||||
targetInputName: this.toNullableString(this.toRecord(port)['targetInputName']) ?? undefined,
|
||||
blockId: this.toNullableString(this.toRecord(port)['blockId']) ?? undefined,
|
||||
inputName: this.toNullableString(this.toRecord(port)['inputName']) ?? undefined
|
||||
})),
|
||||
openOutputs: this.toPorts(value['openOutputs']).map((port) => ({
|
||||
...port,
|
||||
sourceBlockId: this.toNullableString(this.toRecord(port)['sourceBlockId']) ?? undefined,
|
||||
sourceOutputName: this.toNullableString(this.toRecord(port)['sourceOutputName']) ?? undefined,
|
||||
blockId: this.toNullableString(this.toRecord(port)['blockId']) ?? undefined,
|
||||
outputName: this.toNullableString(this.toRecord(port)['outputName']) ?? undefined
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
private toPorts(raw: unknown) {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw
|
||||
.map((port) => this.toRecord(port))
|
||||
.filter((port) => typeof port["name"] === "string" && (port["name"] as string).length > 0)
|
||||
.map((port) => {
|
||||
const type = String(port["type"] ?? "TEXT");
|
||||
const multiple = Boolean(port["multiple"] ?? false);
|
||||
return {
|
||||
...port,
|
||||
name: String(port["name"]),
|
||||
type,
|
||||
multiple,
|
||||
valueKinds: this.toValueKinds(port["valueKinds"], { type, multiple })
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private toValueKinds(raw: unknown, fallback: { type: string; multiple: boolean }) {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [{ type: fallback.type, multiple: fallback.multiple }];
|
||||
}
|
||||
|
||||
const kinds = raw
|
||||
.map((item) => this.toRecord(item))
|
||||
.filter((item) => typeof item["type"] === "string")
|
||||
.map((item) => ({
|
||||
type: String(item["type"] ?? fallback.type),
|
||||
multiple: Boolean(item["multiple"] ?? false)
|
||||
}));
|
||||
|
||||
return kinds.length ? kinds : [{ type: fallback.type, multiple: fallback.multiple }];
|
||||
}
|
||||
|
||||
private toPosition(raw: unknown): { x: number; y: number } | undefined {
|
||||
const value = this.toRecord(raw);
|
||||
const x = value["x"];
|
||||
const y = value["y"];
|
||||
if (typeof x !== "number" || typeof y !== "number") return undefined;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
private toSchema(raw: unknown): Record<string, unknown> | null {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
return raw as Record<string, unknown>;
|
||||
}
|
||||
|
||||
private toRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
private toNullableString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
private toApiPath(value: unknown): string | null {
|
||||
if (typeof value !== "string" || value.length === 0) return null;
|
||||
if (/^https?:\/\//.test(value)) return value;
|
||||
return `${environment.apiUrl}${value.startsWith("/") ? value : `/${value}`}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import { environment } from '@environment';
|
||||
import { BlockType, BlockTypeName, FlowData, FlowNode } from '@models/flow';
|
||||
import { catchError, finalize, firstValueFrom, map, Observable, of, shareReplay, throwError } from 'rxjs';
|
||||
import { ContainersCallServiceBase } from './container-call.base';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ContainersService {
|
||||
containersCallService: ContainersCallServiceBase = new environment.containersCallService();
|
||||
|
||||
toInit = true;
|
||||
private loadingPromise: Promise<void> | null = null;
|
||||
private readonly emptyContainerCache = new Map<string, FlowNode>();
|
||||
private readonly pendingEmptyContainerRequests = new Map<string, Observable<FlowNode>>();
|
||||
private readonly pendingServerSyncCount = signal(0);
|
||||
|
||||
private _containerTypes = signal<BlockType[]>([]);
|
||||
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
|
||||
readonly containerTypes = this._containerTypes.asReadonly();
|
||||
|
||||
hasLoadedContainerTypes() {
|
||||
return this._containerTypes().length > 0 || !this.toInit;
|
||||
}
|
||||
|
||||
async getAllContainerTypes() {
|
||||
if (this.toInit) {
|
||||
await this.refresh();
|
||||
this.toInit = false;
|
||||
}
|
||||
|
||||
return this._containerTypes.asReadonly();
|
||||
}
|
||||
|
||||
async refresh(force = false): Promise<void> {
|
||||
if (this.loadingPromise && !force) {
|
||||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
this.loadingPromise = firstValueFrom(this.containersCallService.retrieveAllContainerTypes())
|
||||
.then((containerTypes) => {
|
||||
this._containerTypes.set(containerTypes);
|
||||
this.clearEmptyContainerCache();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Retrieve container types failed', err);
|
||||
throw err;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingPromise = null;
|
||||
});
|
||||
|
||||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
async getContainerType(typeName: BlockTypeName) {
|
||||
const current = this._containerTypes().find((containerType) => containerType.type === typeName);
|
||||
if (current) return current;
|
||||
|
||||
const containerTypes = await firstValueFrom(this.containersCallService.retrieveAllContainerTypes());
|
||||
this._containerTypes.set(containerTypes);
|
||||
this.clearEmptyContainerCache();
|
||||
return containerTypes.find((containerType) => containerType.type === typeName);
|
||||
}
|
||||
|
||||
createEmptyContainer(containerType: BlockTypeName) {
|
||||
const cacheKey = String(containerType);
|
||||
const cached = this.emptyContainerCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return of(this.cloneEmptyNode(cached));
|
||||
}
|
||||
|
||||
const pending = this.pendingEmptyContainerRequests.get(cacheKey);
|
||||
if (pending) {
|
||||
return pending.pipe(map((container) => this.cloneEmptyNode(container)));
|
||||
}
|
||||
|
||||
const request = this.containersCallService.createEmptyContainer(containerType).pipe(
|
||||
map((container) => {
|
||||
this.emptyContainerCache.set(cacheKey, this.cloneEmptyNode(container));
|
||||
return container;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.pendingEmptyContainerRequests.delete(cacheKey);
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
this.pendingEmptyContainerRequests.set(cacheKey, request);
|
||||
|
||||
return request.pipe(
|
||||
map((container) => this.cloneEmptyNode(container)),
|
||||
catchError((err) => {
|
||||
console.error('Create empty container failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
validateContainerSubflow(subFlow: FlowData) {
|
||||
return this.containersCallService.validateContainerSubflow(this.deepClone(subFlow)).pipe(
|
||||
catchError((err) => {
|
||||
console.error('Validate container subflow failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private clearEmptyContainerCache() {
|
||||
this.emptyContainerCache.clear();
|
||||
this.pendingEmptyContainerRequests.clear();
|
||||
}
|
||||
|
||||
private cloneEmptyNode(node: FlowNode): FlowNode {
|
||||
const clone = this.deepClone(node);
|
||||
return {
|
||||
...clone,
|
||||
id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`,
|
||||
position: undefined
|
||||
};
|
||||
}
|
||||
|
||||
private deepClone<T>(value: T): T {
|
||||
if (typeof globalThis.structuredClone === 'function') {
|
||||
return globalThis.structuredClone(value);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
<div class="blocks-list-root">
|
||||
<div class="blocks-list-search">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search nodes</mat-label>
|
||||
<mat-label>Search blocks</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput placeholder="Search blocks and containers..." [(ngModel)]="searchTerm" />
|
||||
<input matInput placeholder="Search blocks..." [(ngModel)]="searchTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
(dragstart)="onDragStart($event, block)">
|
||||
<mat-card-header class="blocks-list-card-header">
|
||||
<div matCardAvatar class="blocks-list-card-avatar">
|
||||
<mat-icon [fontIcon]="block.family === 'container' ? 'inventory_2' : (block.userInteractive ? 'touch_app' : 'auto_awesome')"></mat-icon>
|
||||
<mat-icon [fontIcon]="block.userInteractive ? 'touch_app' : 'auto_awesome'"></mat-icon>
|
||||
</div>
|
||||
<div class="blocks-list-card-heading">
|
||||
<div class="blocks-list-card-title-row">
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<mat-card-subtitle class="blocks-list-card-subtitle">
|
||||
{{ block.family === 'container' ? 'Container' : (block.userInteractive ? 'Interactive' : 'Automatic') }}
|
||||
{{ block.userInteractive ? 'Interactive' : 'Automatic' }}
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
</mat-card-content>
|
||||
<mat-card-actions class="blocks-list-card-actions" align="end">
|
||||
<mat-chip class="blocks-list-card-chip" [class.blocks-list-card-chip-interactive]="block.userInteractive">
|
||||
{{ block.family === 'container' ? 'Container node' : (block.userInteractive ? 'Human step' : 'Automated step') }}
|
||||
{{ block.userInteractive ? 'Human step' : 'Automated step' }}
|
||||
</mat-chip>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@ export class BlocksList extends ListStateViewHolder<BlockType> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.blocksService.hasLoadedBlockTypes()) {
|
||||
this.blockTypes = this.blocksService.blockTypes;
|
||||
this.view.list = this.blockTypes;
|
||||
this.loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.blocksService.getAllBlocksTypes().then((blockTypesSignal) => {
|
||||
this.blockTypes = blockTypesSignal;
|
||||
this.view.list = this.blockTypes;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
:host {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.blocks-list-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.blocks-list-root {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.blocks-list-search {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.blocks-list-empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding-top: 2.5rem;
|
||||
margin-top: 2.5rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blocks-list-items {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0 20px;
|
||||
}
|
||||
|
||||
.blocks-list-card {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(56, 189, 248, 0.12), transparent 34%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
box-shadow:
|
||||
0 12px 26px rgba(15, 23, 42, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.blocks-list-card--container {
|
||||
border-color: #bae6fd;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(14, 165, 233, 0.16), transparent 34%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f0f9ff 100%);
|
||||
}
|
||||
|
||||
.blocks-list-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.blocks-list-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 18px 30px rgba(14, 116, 144, 0.14);
|
||||
}
|
||||
|
||||
.blocks-list-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 14px 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-heading {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.blocks-list-card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
border-radius: 12px;
|
||||
color: #075985;
|
||||
background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%);
|
||||
}
|
||||
|
||||
.blocks-list-card-avatar--container {
|
||||
color: #155e75;
|
||||
background: linear-gradient(180deg, #cffafe 0%, #a5f3fc 100%);
|
||||
}
|
||||
|
||||
.blocks-list-card-avatar .mat-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.blocks-list-card-title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.blocks-list-card-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(224, 242, 254, 0.95);
|
||||
color: #0369a1;
|
||||
cursor: default;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-info .mat-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.blocks-list-card-subtitle {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.blocks-list-card-content {
|
||||
padding: 0 14px 8px;
|
||||
}
|
||||
|
||||
.blocks-list-card-copy {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.blocks-list-card-actions {
|
||||
padding: 0 14px 14px !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-chip {
|
||||
min-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.blocks-list-card-chip-container {
|
||||
color: #0c4a6e !important;
|
||||
background: rgba(224, 242, 254, 0.95) !important;
|
||||
border: 1px solid rgba(56, 189, 248, 0.72);
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
@if (loading()) {
|
||||
<div class="blocks-list-loading">
|
||||
<mat-spinner diameter="32"></mat-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="blocks-list-root">
|
||||
<div class="blocks-list-search">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search containers</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput placeholder="Search containers..." [(ngModel)]="searchTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@if (filteredContainers().length === 0) {
|
||||
<div class="blocks-list-empty">
|
||||
No containers found
|
||||
</div>
|
||||
} @else {
|
||||
<div class="blocks-list-items">
|
||||
@for (container of filteredContainers(); track container.type) {
|
||||
<mat-card
|
||||
class="blocks-list-card blocks-list-card--container"
|
||||
draggable="true"
|
||||
(dragstart)="onDragStart($event, container)">
|
||||
<mat-card-header class="blocks-list-card-header">
|
||||
<div matCardAvatar class="blocks-list-card-avatar blocks-list-card-avatar--container">
|
||||
<mat-icon fontIcon="inventory_2"></mat-icon>
|
||||
</div>
|
||||
<div class="blocks-list-card-heading">
|
||||
<div class="blocks-list-card-title-row">
|
||||
<mat-card-title class="blocks-list-card-title">{{ container.type }}</mat-card-title>
|
||||
<button
|
||||
type="button"
|
||||
class="blocks-list-card-info"
|
||||
matTooltip="{{ container.description }}"
|
||||
aria-label="Show container description"
|
||||
title=""
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="$event.stopPropagation()">
|
||||
<mat-icon fontIcon="info"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-card-subtitle class="blocks-list-card-subtitle">
|
||||
Container
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="blocks-list-card-content">
|
||||
<p class="blocks-list-card-copy">{{ container.description }}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="blocks-list-card-actions" align="end">
|
||||
<mat-chip class="blocks-list-card-chip blocks-list-card-chip-container">
|
||||
Container node
|
||||
</mat-chip>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { Component, computed, inject, model, signal, Signal, WritableSignal } from '@angular/core';
|
||||
import { BlockType } from '@models/flow';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from '@shared/blocks-list/block-drag';
|
||||
|
||||
@Component({
|
||||
selector: 'app-containers-list',
|
||||
imports: [FormsModule, MatCardModule, MatChipsModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule],
|
||||
templateUrl: './containers-list.html',
|
||||
styleUrl: './containers-list.css',
|
||||
})
|
||||
export class ContainersList extends ListStateViewHolder<BlockType> {
|
||||
searchTerm = model<string>('');
|
||||
|
||||
private containersService = inject(ContainersService);
|
||||
|
||||
loading: WritableSignal<boolean> = signal(true);
|
||||
containerTypes?: Signal<BlockType[]>;
|
||||
|
||||
constructor() {
|
||||
super('containersList', {
|
||||
defaultOrder: { orderBy: 'name', orderDir: 'asc' } as OrderViewState
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const existingState = this.view;
|
||||
if (existingState.list) {
|
||||
this.containerTypes = existingState.list;
|
||||
this.loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.containersService.hasLoadedContainerTypes()) {
|
||||
this.containerTypes = this.containersService.containerTypes;
|
||||
this.view.list = this.containerTypes;
|
||||
this.loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.containersService.getAllContainerTypes().then((containerTypesSignal) => {
|
||||
this.containerTypes = containerTypesSignal;
|
||||
this.view.list = this.containerTypes;
|
||||
}).catch((err) => {
|
||||
console.error('Error loading container types', err);
|
||||
}).finally(() => {
|
||||
this.loading.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
filteredContainers = computed(() => {
|
||||
const containers = this.containerTypes ? this.containerTypes() : [];
|
||||
if (!containers) return [];
|
||||
|
||||
const term = this.searchTerm().toLowerCase();
|
||||
return containers.filter((container) =>
|
||||
container.type.toLowerCase().includes(term) || container.description.toLowerCase().includes(term)
|
||||
);
|
||||
});
|
||||
|
||||
onDragStart(event: DragEvent, container: BlockType) {
|
||||
if (!event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData(BLOCK_TYPE_DRAG_MIME, JSON.stringify(container));
|
||||
event.dataTransfer.setData('text/plain', container.type);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
.container-node {
|
||||
position: relative;
|
||||
width: 340px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 18px;
|
||||
|
|
@ -10,6 +11,15 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container-node__delete-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 12;
|
||||
border-radius: 18px;
|
||||
background: rgba(148, 163, 184, 0.38);
|
||||
backdrop-filter: grayscale(1) saturate(0.2);
|
||||
}
|
||||
|
||||
:host.selected .container-node {
|
||||
border-color: #0f766e;
|
||||
box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.18), 0 16px 36px rgba(15, 23, 42, 0.18);
|
||||
|
|
@ -66,6 +76,108 @@
|
|||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.container-node__header-actions {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container-node__warning-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container-node__warning {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #99f6e4;
|
||||
background: #14b8a6;
|
||||
color: #ecfeff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.22);
|
||||
}
|
||||
|
||||
.container-node__warning-tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
border: 1px solid #99f6e4;
|
||||
border-radius: 8px;
|
||||
background: #f0fdfa;
|
||||
color: #115e59;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2);
|
||||
padding: 8px;
|
||||
z-index: 13;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-2px);
|
||||
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease;
|
||||
}
|
||||
|
||||
.container-node__warning-wrap:hover .container-node__warning-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.container-node__warning-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.container-node__warning-item {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.container-node__delete-confirm {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 14;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #fecaca;
|
||||
background: #fff1f2;
|
||||
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.container-node__delete-confirm-text {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.container-node__delete-confirm-cancel,
|
||||
.container-node__delete-confirm-action {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.container-node__delete-confirm-cancel {
|
||||
background: #ffffff;
|
||||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.container-node__delete-confirm-action {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.container-node__ports {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<div class="container-node" [class.container-node-assigning]="isAssigning">
|
||||
<div class="container-node" [class.container-node-assigning]="isAssigning" [class.container-node-delete-pending]="deleteConfirmOpen">
|
||||
@if (deleteConfirmOpen) {
|
||||
<div class="container-node__delete-overlay"></div>
|
||||
}
|
||||
<div class="container-node__header">
|
||||
<div class="container-node__icon">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
|
|
@ -7,9 +10,31 @@
|
|||
<div class="container-node__eyebrow">Generic Container</div>
|
||||
<div class="container-node__name">{{ name }}</div>
|
||||
</div>
|
||||
<button type="button" class="container-node__delete" title="Delete node" (pointerdown)="$event.stopPropagation()" (click)="deleteNode($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="container-node__header-actions">
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="container-node__warning-wrap">
|
||||
<div class="container-node__warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="container-node__warning-tooltip">
|
||||
<div class="container-node__warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="container-node__warning-item">{{ missing }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (deleteConfirmOpen) {
|
||||
<div class="container-node__delete-confirm" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<span class="container-node__delete-confirm-text">Delete container?</span>
|
||||
<button type="button" class="container-node__delete-confirm-cancel" (click)="cancelDelete($event)">Cancel</button>
|
||||
<button type="button" class="container-node__delete-confirm-action" (click)="deleteNode($event)">Delete</button>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="container-node__delete" title="Delete node" (pointerdown)="$event.stopPropagation()" (click)="deleteNode($event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-node__ports">
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { CONTAINER_SUBFLOW_DRAG_MIME } from './container-node-drag';
|
|||
export class ContainerNodeComponent {
|
||||
private editorState = inject(EditorStateHolder);
|
||||
private subflowPreview = inject(SubflowPreviewDialogService);
|
||||
deleteConfirmOpen = false;
|
||||
|
||||
@Input() data!: any;
|
||||
@Input() emit!: (data: any) => void;
|
||||
|
|
@ -112,6 +113,24 @@ export class ContainerNodeComponent {
|
|||
return this.data?.data?.['__containerAssigning'] === true;
|
||||
}
|
||||
|
||||
get missingRequiredParams() {
|
||||
const missing: string[] = [];
|
||||
if (!this.name.trim()) {
|
||||
missing.push('Name');
|
||||
}
|
||||
if (!this.subFlow) {
|
||||
missing.push('Sub Flow');
|
||||
}
|
||||
const config = this.configuration ?? {};
|
||||
if (!Array.isArray(config['publicInputs'])) {
|
||||
missing.push('Public Inputs');
|
||||
}
|
||||
if (!Array.isArray(config['publicOutputs'])) {
|
||||
missing.push('Public Outputs');
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
inputDisplayLabel(inputKey: string) {
|
||||
return pathToLabel(inputKey);
|
||||
}
|
||||
|
|
@ -168,6 +187,10 @@ export class ContainerNodeComponent {
|
|||
deleteNode(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!this.deleteConfirmOpen) {
|
||||
this.deleteConfirmOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const remove = this.data?.data?.deleteNode;
|
||||
if (typeof remove === 'function') {
|
||||
|
|
@ -175,6 +198,12 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
cancelDelete(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.deleteConfirmOpen = false;
|
||||
}
|
||||
|
||||
openSubflowPreview(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, ElementRef, Injector, input, OnChanges, OnDestroy, output, signal, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { BlockType, FlowData, FlowNode } from '@models/flow';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from '@shared/blocks-list/block-drag';
|
||||
import { CONTAINER_SUBFLOW_DRAG_MIME } from '@shared/nodes/container-node/container-node-drag';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
|
|
@ -22,7 +23,8 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
constructor(
|
||||
private injector: Injector,
|
||||
private flowState: EditorStateHolder,
|
||||
private blocksService: BlocksService
|
||||
private blocksService: BlocksService,
|
||||
private containersService: ContainersService
|
||||
) {}
|
||||
|
||||
@ViewChild("editor") container!: ElementRef;
|
||||
|
|
@ -95,7 +97,9 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
this.creatingEmptyBlock = true;
|
||||
this.creatingEmptyBlockType = blockType.type;
|
||||
try {
|
||||
newBlock = await firstValueFrom(this.blocksService.createEmptyBlock(blockType.type, blockType.family));
|
||||
newBlock = blockType.family === 'container'
|
||||
? await firstValueFrom(this.containersService.createEmptyContainer(blockType.type))
|
||||
: await firstValueFrom(this.blocksService.createEmptyBlock(blockType.type));
|
||||
} catch (error) {
|
||||
console.error('Failed to create empty block', error);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
normalizeFlowPortValueKinds
|
||||
} from "@models/flow";
|
||||
import { BlocksService } from "@services/blocks/blocks";
|
||||
import { ContainersService } from "@services/containers/containers";
|
||||
import { NodeSettingsDialogService } from "@services/dialogs/node-settings-dialog";
|
||||
import { EditorStateHolder } from "@stores/flow-editor";
|
||||
import { ContainerNodeComponent } from "@shared/nodes/container-node/container-node";
|
||||
|
|
@ -37,6 +38,7 @@ export type ReteEditorInstance = {
|
|||
|
||||
type ReteRuntimeContext = {
|
||||
blocksService: BlocksService;
|
||||
containersService: ContainersService;
|
||||
flowState: EditorStateHolder;
|
||||
settingsDialog: NodeSettingsDialogService;
|
||||
};
|
||||
|
|
@ -55,6 +57,7 @@ export async function createEditor(
|
|||
const nodeView = options?.nodeView ?? "editor";
|
||||
const runtime: ReteRuntimeContext = {
|
||||
blocksService: injector.get(BlocksService),
|
||||
containersService: injector.get(ContainersService),
|
||||
flowState: injector.get(EditorStateHolder),
|
||||
settingsDialog: injector.get(NodeSettingsDialogService)
|
||||
};
|
||||
|
|
@ -255,7 +258,7 @@ export async function addBlockToEditor(
|
|||
};
|
||||
await area.update("node", node.id);
|
||||
|
||||
resolvedRuntime.blocksService.validateContainerSubflow(candidateSubFlow).subscribe({
|
||||
resolvedRuntime.containersService.validateContainerSubflow(candidateSubFlow).subscribe({
|
||||
next: async (result) => {
|
||||
const liveNode = editor.getNode(node.id) as HFNode | undefined;
|
||||
if (!liveNode?.data) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AssistantCallServiceFake } from "@services/assistant/assistant-call.fake";
|
||||
import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake";
|
||||
import { BlocksCallServiceFake } from "@services/blocks/blocks-call.fake";
|
||||
import { ContainersCallServiceFake } from "@services/containers/containers-call.fake";
|
||||
import { FlowsCallServiceFake } from "@services/flows/flows-call.fake";
|
||||
import { FieldRetrieverCallServiceFake } from "@services/retriever/field-retriever-call.fake";
|
||||
import { TaskExecutionsCallServiceFake } from "@services/task-executions/task-executions-call.fake";
|
||||
|
|
@ -13,6 +14,7 @@ export const environment = {
|
|||
assistantCallService: AssistantCallServiceFake,
|
||||
flowsCallService: FlowsCallServiceFake,
|
||||
blocksCallService: BlocksCallServiceFake,
|
||||
containersCallService: ContainersCallServiceFake,
|
||||
fieldRetrieverCallService: FieldRetrieverCallServiceFake,
|
||||
taskExecutionsCallService: TaskExecutionsCallServiceFake
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AssistantCallService } from "@services/assistant/assistant-call";
|
||||
import { AuthorizationCallService } from "@services/authorization/authorization-call";
|
||||
import { BlocksCallService } from "@services/blocks/blocks-call";
|
||||
import { ContainersCallService } from "@services/containers/containers-call";
|
||||
import { FlowsCallService } from "@services/flows/flows-call";
|
||||
import { FieldRetrieverCallService } from "@services/retriever/field-retriever-call";
|
||||
import { TaskExecutionsCallService } from "@services/task-executions/task-executions-call";
|
||||
|
|
@ -13,6 +14,7 @@ export const environment = {
|
|||
authorizationCallService: AuthorizationCallService,
|
||||
flowsCallService: FlowsCallService,
|
||||
blocksCallService: BlocksCallService,
|
||||
containersCallService: ContainersCallService,
|
||||
fieldRetrieverCallService: FieldRetrieverCallService,
|
||||
taskExecutionsCallService: TaskExecutionsCallService
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AssistantCallService } from "@services/assistant/assistant-call";
|
||||
import { AuthorizationCallService } from "@services/authorization/authorization-call";
|
||||
import { BlocksCallService } from "@services/blocks/blocks-call";
|
||||
import { ContainersCallService } from "@services/containers/containers-call";
|
||||
import { FlowsCallService } from "@services/flows/flows-call";
|
||||
import { FieldRetrieverCallService } from "@services/retriever/field-retriever-call";
|
||||
import { TaskExecutionsCallService } from "@services/task-executions/task-executions-call";
|
||||
|
|
@ -13,6 +14,7 @@ export const environment = {
|
|||
assistantCallService: AssistantCallService,
|
||||
flowsCallService: FlowsCallService,
|
||||
blocksCallService: BlocksCallService,
|
||||
containersCallService: ContainersCallService,
|
||||
fieldRetrieverCallService: FieldRetrieverCallService,
|
||||
taskExecutionsCallService: TaskExecutionsCallService
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue