Link storage operations to a Storage node on the canvas

A Storage node has a 'Used by' handle and a storage operation a 'Works
on' one; the link drawn between them, teal and dotted, is what sets the
operation's storageNode. It is neither a connection nor a dependency: it
carries no value and orders nothing, and loops leave it out. An
operation works on one node, so a new link replaces the old one, and
removing the link empties the field. On load the links are drawn back
from what each operation says.

Fields the schema marks x-ui-hidden are left out of every form and chip
- storageNode is set by the link, not typed - and kept in the
configuration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-28 17:39:37 +02:00
parent 1a7d139535
commit ee2696814c
12 changed files with 261 additions and 28 deletions

View File

@ -278,6 +278,15 @@ export type GroupedFlowValidation = {
export const FLOW_DEPENDANT_PORT_KEY = '__dependant';
export const FLOW_DEPENDENCY_PORT_KEY = '__dependency';
export const FLOW_DEPENDENCY_SOCKET_TYPE = '__FLOW_DEPENDENCY__';
/** A Storage node's handle, and a StorageOperation's: the link between them is not a connection. */
export const FLOW_STORAGE_PORT_KEY = '__storage';
export const FLOW_STORAGE_LINK_PORT_KEY = '__storageNode';
export const FLOW_STORAGE_SOCKET_TYPE = '__FLOW_STORAGE__';
/** A block that works on a Storage node, linked to it rather than connected. */
export function isStorageOperationNode(node: { typeName?: string; specificConfiguration?: Record<string, unknown> | null }): boolean {
return node.typeName === 'StorageOperation' || node.specificConfiguration?.['type'] === 'StorageOperationBlockConfiguration';
}
export function normalizeFlowValidationErrors(raw: unknown): FlowValidationError[] {
if (!Array.isArray(raw)) return [];

View File

@ -8,6 +8,8 @@ import {
DEFAULT_LOOP_MAX_ITERATIONS,
FLOW_DEPENDANT_PORT_KEY,
FLOW_DEPENDENCY_PORT_KEY,
FLOW_STORAGE_LINK_PORT_KEY,
FLOW_STORAGE_PORT_KEY,
MAX_LOOP_MAX_ITERATIONS
} from '@models/flow';
import type { LoopAwareConnection } from '@utilities/rete-editor';
@ -199,6 +201,11 @@ export class CustomConnectionComponent {
@Input() end!: { x: number; y: number };
@Input() path = '';
/** A storage operation's link to the Storage node it works on: not a connection, orders nothing. */
get isStorageLink(): boolean {
return this.data?.sourceOutput === FLOW_STORAGE_PORT_KEY && this.data?.targetInput === FLOW_STORAGE_LINK_PORT_KEY;
}
get isDependencyConnection(): boolean {
return this.data?.sourceOutput === FLOW_DEPENDANT_PORT_KEY && this.data?.targetInput === FLOW_DEPENDENCY_PORT_KEY;
}
@ -278,17 +285,19 @@ export class CustomConnectionComponent {
if (this.isSelected) return '#f97316';
if (this.isBiasRoutingChange) return '#b45309';
if (this.isLoopBack) return '#7c3aed';
if (this.isStorageLink) return '#0f766e';
return this.isDependencyConnection ? '#7c8ba1' : '#4682b4';
}
get strokeWidth(): number {
if (this.isSelected) return this.isDependencyConnection ? 3.25 : 6;
if (this.isSelected) return this.isDependencyConnection || this.isStorageLink ? 3.25 : 6;
if (this.isBiasRoutingChange) return 6;
return this.isDependencyConnection ? 2.25 : 5;
return this.isDependencyConnection || this.isStorageLink ? 2.25 : 5;
}
get strokeDasharray(): string | null {
if (this.isLoopBack) return '12 7';
if (this.isStorageLink) return '2 6';
return this.isDependencyConnection ? '6 6' : null;
}

View File

@ -13,7 +13,7 @@ export class CustomSocket {
@Input() emit!: any;
@Input() rendered!: any;
private side: "input" | "output" = "input";
private kind: "data" | "dependency" = "data";
private kind: "data" | "dependency" | "storage" = "data";
@HostBinding("title") get title() {
return this.data.name;
@ -34,6 +34,9 @@ export class CustomSocket {
@HostBinding("style.background")
get bg() {
if (this.kind === "storage") {
return "linear-gradient(145deg, #2dd4bf 0%, #0f766e 100%)";
}
if (this.kind === "dependency") {
return this.side === "input"
? "linear-gradient(145deg, #fbbf24 0%, #d97706 100%)"
@ -46,6 +49,9 @@ export class CustomSocket {
@HostBinding("style.boxShadow")
get sh() {
if (this.kind === "storage") {
return "0 2px 6px rgba(15,118,110,0.45), 0 0 0 1px rgba(15,118,110,0.45)";
}
if (this.kind === "dependency") {
const c = this.side === "input" ? "rgba(217,119,6,0.45)" : "rgba(37,99,235,0.45)";
return `0 2px 6px ${c}, 0 0 0 1px ${c}`;
@ -59,8 +65,9 @@ export class CustomSocket {
return side === "output" ? "output" : "input";
}
private resolveSocketKind(): "data" | "dependency" {
return this.data?.__hfKind === "dependency" ? "dependency" : "data";
private resolveSocketKind(): "data" | "dependency" | "storage" {
const kind = this.data?.__hfKind;
return kind === "dependency" || kind === "storage" ? kind : "data";
}
ngOnChanges(): void {

View File

@ -999,3 +999,9 @@
background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);
border-bottom-color: #99f6e4;
}
.llm-pill-storage {
border-color: #5eead4;
background: #f0fdfa;
color: #0f766e;
}

View File

@ -217,6 +217,54 @@
</div>
</div>
@if (storageLinkOutput || storageLinkInput) {
<div class="llm-body llm-body-dependency llm-body-storage">
<div class="llm-column">
<div class="llm-column-title">Storage</div>
@if (storageLinkInput; as storageLinkInput) {
<div class="llm-row llm-row-input">
<div
refComponent
class="llm-socket"
[data]="{
type: 'socket',
side: 'input',
key: storageLinkInput.key,
nodeId: data.id,
__readonly: isReadonly,
payload: storageLinkInput.socket
}"
[emit]="emit">
</div>
<span class="llm-pill llm-pill-input llm-pill-storage">Works on</span>
</div>
}
</div>
<div class="llm-column">
<div class="llm-column-title llm-column-title-right">&nbsp;</div>
@if (storageLinkOutput; as storageLinkOutput) {
<div class="llm-row llm-row-output">
<span class="llm-pill llm-pill-output llm-pill-storage">Used by</span>
<div
refComponent
class="llm-socket llm-socket-right"
[data]="{
type: 'socket',
side: 'output',
key: storageLinkOutput.key,
nodeId: data.id,
__readonly: isReadonly,
payload: storageLinkOutput.socket
}"
[emit]="emit">
</div>
</div>
}
</div>
</div>
}
@if (hasExecutionDependencyPorts()) {
<div class="llm-body llm-body-dependency">
<div class="llm-column">

View File

@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, effect, ElementR
import { FormsModule } from '@angular/forms';
import { MatTooltipModule } from '@angular/material/tooltip';
import { environment } from '@environment';
import { BiasAnnotation, BiasAnnotationsDescriptor, BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowData, FlowGlobalInput, FlowPort, FlowValueKind, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, isProbeExecutable, normalizeFlowPortValueKinds } from '@models/flow';
import { BiasAnnotation, BiasAnnotationsDescriptor, BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowData, FlowGlobalInput, FlowPort, FlowValueKind, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY, isProbeExecutable, normalizeFlowPortValueKinds } from '@models/flow';
import { BiasAnnotationsComponent } from '../../bias-annotations/bias-annotations';
import { NodeFocusModalController } from '../node-focus-modal-controller';
import { ClassicPreset } from 'rete';
@ -245,7 +245,7 @@ export class GenericNodeComponent implements OnDestroy {
get outputs(): RenderedSocketPort[] {
return Object.entries(this.data?.outputs ?? {})
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY)
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY && key !== FLOW_STORAGE_PORT_KEY)
.map(([key, output]) => ({
key,
socket: (output as any).socket as ClassicPreset.Socket
@ -254,13 +254,25 @@ export class GenericNodeComponent implements OnDestroy {
get inputs(): RenderedSocketPort[] {
return Object.entries(this.data?.inputs ?? {})
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY)
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY && key !== FLOW_STORAGE_LINK_PORT_KEY)
.map(([key, input]) => ({
key,
socket: (input as any).socket as ClassicPreset.Socket
}));
}
/** A Storage node's handle, which storage operations are linked to. */
get storageLinkOutput(): { key: string; socket: ClassicPreset.Socket } | null {
const output = this.data?.outputs?.[FLOW_STORAGE_PORT_KEY];
return output ? { key: FLOW_STORAGE_PORT_KEY, socket: (output as any).socket as ClassicPreset.Socket } : null;
}
/** A storage operation's handle, linked to the Storage node it works on. */
get storageLinkInput(): { key: string; socket: ClassicPreset.Socket } | null {
const input = this.data?.inputs?.[FLOW_STORAGE_LINK_PORT_KEY];
return input ? { key: FLOW_STORAGE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
}
get dependantOutput(): RenderedSocketPort | null {
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
return output

View File

@ -177,6 +177,9 @@ export function orderedSchemaPropertyEntries(
isTechnical
};
})
// Said by something else on the canvas - a link drawn between two nodes - not a field. Left
// out of every form and chip, and kept in the configuration, which this does not touch.
.filter((entry) => entry.schema?.['x-ui-hidden'] !== true)
.sort((left, right) => {
const leftBucket = left.priorityIndex != null ? 0 : left.isTechnical ? 2 : 1;
const rightBucket = right.priorityIndex != null ? 0 : right.isTechnical ? 2 : 1;

View File

@ -841,3 +841,9 @@ button.llm-node-bias-summary:focus-visible {
background: linear-gradient(135deg, #14b8a6 0%, #0f766e 100%);
border-bottom-color: #99f6e4;
}
.llm-pill-storage {
border-color: #5eead4;
background: #f0fdfa;
color: #0f766e;
}

View File

@ -249,6 +249,54 @@
</div>
</div>
@if (storageLinkOutput || storageLinkInput) {
<div class="llm-body llm-body-dependency llm-body-storage">
<div class="llm-column">
<div class="llm-column-title">Storage</div>
@if (storageLinkInput; as storageLinkInput) {
<div class="llm-row llm-row-input">
<div
refComponent
class="llm-socket"
[data]="{
type: 'socket',
side: 'input',
key: storageLinkInput.key,
nodeId: data.id,
__readonly: true,
payload: storageLinkInput.socket
}"
[emit]="emit">
</div>
<span class="llm-pill llm-pill-input llm-pill-storage">Works on</span>
</div>
}
</div>
<div class="llm-column">
<div class="llm-column-title llm-column-title-right">&nbsp;</div>
@if (storageLinkOutput; as storageLinkOutput) {
<div class="llm-row llm-row-output">
<span class="llm-pill llm-pill-output llm-pill-storage">Used by</span>
<div
refComponent
class="llm-socket llm-socket-right"
[data]="{
type: 'socket',
side: 'output',
key: storageLinkOutput.key,
nodeId: data.id,
__readonly: true,
payload: storageLinkOutput.socket
}"
[emit]="emit">
</div>
</div>
}
</div>
</div>
}
@if (hasExecutionDependencyPorts()) {
<div class="llm-body llm-body-dependency">
<div class="llm-column">

View File

@ -7,7 +7,7 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, inject, viewChild } from '@angular/core';
import { ClassicPreset } from 'rete';
import { ReteModule } from 'rete-angular-plugin/21';
import { BiasAnnotation, BlockInteractionContract, BlockType, DEFAULT_NODE_CAPABILITIES, FlowData, FlowNode, FlowPort, isProbeExecutable, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, NodeTypeCapabilities } from '@models/flow';
import { BiasAnnotation, BlockInteractionContract, BlockType, DEFAULT_NODE_CAPABILITIES, FlowData, FlowNode, FlowPort, isProbeExecutable, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY, NodeTypeCapabilities } from '@models/flow';
import { BiasCapabilities } from '@models/bias-impact';
import { BlocksService } from '@services/blocks/blocks';
import { ContainersService } from '@services/containers/containers';
@ -152,16 +152,28 @@ export class TaskStepNodeComponent {
get outputs(): { key: string; socket: ClassicPreset.Socket }[] {
return Object.entries(this.data?.outputs ?? {})
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY)
.filter(([key]) => key !== FLOW_DEPENDANT_PORT_KEY && key !== FLOW_STORAGE_PORT_KEY)
.map(([key, output]) => ({ key, socket: (output as any).socket as ClassicPreset.Socket }));
}
get inputs(): { key: string; socket: ClassicPreset.Socket }[] {
return Object.entries(this.data?.inputs ?? {})
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY)
.filter(([key]) => key !== FLOW_DEPENDENCY_PORT_KEY && key !== FLOW_STORAGE_LINK_PORT_KEY)
.map(([key, input]) => ({ key, socket: (input as any).socket as ClassicPreset.Socket }));
}
/** A Storage node's handle, which storage operations are linked to. */
get storageLinkOutput(): { key: string; socket: ClassicPreset.Socket } | null {
const output = this.data?.outputs?.[FLOW_STORAGE_PORT_KEY];
return output ? { key: FLOW_STORAGE_PORT_KEY, socket: (output as any).socket as ClassicPreset.Socket } : null;
}
/** A storage operation's handle, linked to the Storage node it works on. */
get storageLinkInput(): { key: string; socket: ClassicPreset.Socket } | null {
const input = this.data?.inputs?.[FLOW_STORAGE_LINK_PORT_KEY];
return input ? { key: FLOW_STORAGE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
}
get dependantOutput(): { key: string; socket: ClassicPreset.Socket } | null {
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
return output

View File

@ -83,3 +83,34 @@ describe('isResourceNode', () => {
expect(isResourceNode(undefined, makeNode('Storage', 'block'))).toBe(false);
});
});
describe('the link between a Storage node and a storage operation', () => {
it('is what says which node the operation works on, and nothing when it is gone', async () => {
const { NodeEditor, ClassicPreset } = await import('rete');
const { exportGraph } = await import('./rete-editor');
const { FLOW_STORAGE_LINK_PORT_KEY, FLOW_STORAGE_PORT_KEY } = await import('@models/flow');
const editor = new NodeEditor<any>();
const socket = new ClassicPreset.Socket('__FLOW_STORAGE__');
const storage = new ClassicPreset.Node('Storage') as any;
storage.data = { id: 'store-1', typeName: 'Storage', nodeFamily: 'block', specificConfiguration: { type: 'StorageNodeConfiguration' } };
storage.addOutput(FLOW_STORAGE_PORT_KEY, new ClassicPreset.Output(socket));
const operation = new ClassicPreset.Node('StorageOperation') as any;
operation.data = { id: 'op-1', typeName: 'StorageOperation', nodeFamily: 'block',
specificConfiguration: { type: 'StorageOperationBlockConfiguration', storageNode: 'stale' } };
operation.addInput(FLOW_STORAGE_LINK_PORT_KEY, new ClassicPreset.Input(socket, undefined, true));
await editor.addNode(storage);
await editor.addNode(operation);
const link = new ClassicPreset.Connection(storage, FLOW_STORAGE_PORT_KEY, operation, FLOW_STORAGE_LINK_PORT_KEY);
await editor.addConnection(link);
let graph = exportGraph(editor);
expect((graph.blocks.find((block) => block.id === 'op-1')?.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode']).toBe('store-1');
expect(graph.connections).toEqual([]);
expect(graph.dependencies).toEqual([]);
await editor.removeConnection(link.id);
graph = exportGraph(editor);
expect((graph.blocks.find((block) => block.id === 'op-1')?.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode']).toBeNull();
});
});

View File

@ -22,6 +22,10 @@ import {
FLOW_DEPENDANT_PORT_KEY,
FLOW_DEPENDENCY_PORT_KEY,
FLOW_DEPENDENCY_SOCKET_TYPE,
FLOW_STORAGE_LINK_PORT_KEY,
FLOW_STORAGE_PORT_KEY,
FLOW_STORAGE_SOCKET_TYPE,
isStorageOperationNode,
FlowNode,
NodeTypeCapabilities,
normalizeFlowPortValueKinds
@ -53,7 +57,7 @@ export type ReteEditorInstance = {
area: AreaPlugin<HFSchemes, AreaExtra>;
};
type GraphConnectionKind = "data" | "dependency";
type GraphConnectionKind = "data" | "dependency" | "storage";
export type ReteRuntimeContext = {
blocksService: BlocksService;
@ -145,6 +149,13 @@ export async function createEditor(
const targetCapabilities = resolveNodeCapabilities(runtime, targetNode);
const connectionKind = getGraphConnectionKind(context.data.sourceOutput, context.data.targetInput);
if (connectionKind === "storage") {
// From a Storage node to a storage operation, and one node per operation: a new link
// replaces the operation's old one, the way a data input keeps to one connection.
if (!isResourceNode(runtime, sourceNode)) return;
if (!runtime.restoringConnections) await replaceStorageLink(editor, context.data.target);
return context;
}
if (connectionKind === "dependency") {
if (context.data.source === context.data.target) return;
if (!sourceCapabilities.canHaveDependentNodes || !targetCapabilities.canDependOnOtherNodes) return;
@ -281,6 +292,19 @@ export function exportGraph(editor: NodeEditor<HFSchemes>) {
};
});
// A storage operation says which Storage node it works on in its configuration; the link on the
// canvas is how that is set, so the configuration follows the links as they now stand.
const storageLinks = new Map(allConnections
.filter((connection) => connection.kind === 'storage')
.map((connection) => [connection.targetId, connection.sourceId] as const));
for (const node of nodes) {
if (!isStorageOperationNode(node as any)) continue;
node.specificConfiguration = {
...(node.specificConfiguration ?? {}),
storageNode: storageLinks.get(node.id) ?? null
};
}
return {
blocks: nodes.filter((node): node is FlowBlock => node.nodeFamily === 'block'),
containers: nodes.filter((node) => node.nodeFamily === 'container'),
@ -638,6 +662,12 @@ export async function addBlockToEditor(
};
const capabilities = resolveNodeCapabilities(resolvedRuntime, node);
if (capabilities.visualRole === 'RESOURCE') {
node.addOutput(FLOW_STORAGE_PORT_KEY, new ClassicPreset.Output(getSocket(editor, FLOW_STORAGE_SOCKET_TYPE)));
}
if (isStorageOperationNode(block as any)) {
node.addInput(FLOW_STORAGE_LINK_PORT_KEY, new ClassicPreset.Input(getSocket(editor, FLOW_STORAGE_SOCKET_TYPE), undefined, true));
}
if (capabilities.canHaveDependentNodes) {
node.addOutput(FLOW_DEPENDANT_PORT_KEY, new ClassicPreset.Output(getSocket(editor, FLOW_DEPENDENCY_SOCKET_TYPE)));
}
@ -718,9 +748,31 @@ async function loadFlowData(
new ClassicPreset.Connection(sourceNode, FLOW_DEPENDANT_PORT_KEY, targetNode, FLOW_DEPENDENCY_PORT_KEY)
);
}
// Drawn back from what each storage operation says it works on.
for (const block of flowData.blocks ?? []) {
if (isStale()) return;
const storageNodeId = (block.specificConfiguration as Record<string, unknown> | undefined)?.['storageNode'];
if (!isStorageOperationNode(block as any) || typeof storageNodeId !== 'string') continue;
if (!nodeMapping.has(storageNodeId) || !nodeMapping.has(block.id)) continue;
const sourceNode = editor.getNode(nodeMapping.get(storageNodeId)) as any;
const targetNode = editor.getNode(nodeMapping.get(block.id)) as any;
await editor.addConnection(
new ClassicPreset.Connection(sourceNode, FLOW_STORAGE_PORT_KEY, targetNode, FLOW_STORAGE_LINK_PORT_KEY)
);
}
});
}
/** An operation works on one Storage node: linking it to another drops the link it had. */
async function replaceStorageLink(editor: NodeEditor<HFSchemes>, operationNodeId: string) {
const existing = editor.getConnections().filter((connection) =>
connection.target === operationNodeId && connection.targetInput === FLOW_STORAGE_LINK_PORT_KEY);
for (const connection of existing) {
await editor.removeConnection(connection.id);
}
}
async function withRestoredConnections<T>(runtime: ReteRuntimeContext | undefined, restore: () => Promise<T>): Promise<T> {
if (!runtime) return restore();
runtime.restoringConnections = (runtime.restoringConnections ?? 0) + 1;
@ -743,11 +795,6 @@ type NewConnection = { source: string; sourceOutput: string; target: string; tar
* new one replaces it, as dropping onto a taken input always did.
*/
async function makeRoomOnInput(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, created: NewConnection) {
// A storage node's target takes every step that writes there. Its view parameters, named
// <view>.<parameter>, take one value like any input.
if (isResourceNode(runtime, editor.getNode(created.target) as HFNode | undefined) && !created.targetInput.includes('.')) {
return false;
}
const newLeadsBack = leadsBack(editor, runtime, created, null);
const existing = editor.getConnections().filter((connection) =>
connection.target === created.target && connection.targetInput === created.targetInput);
@ -766,8 +813,8 @@ function leadsBack(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, c
const outgoing = new Map<string, string[]>();
for (const candidate of editor.getConnections()) {
if (candidate.id === ignoredConnectionId) continue;
// A storage node is not a step: writing to it and reading from it is not a path round.
if (touchesResource(editor, runtime, candidate)) continue;
// A link to a Storage node orders nothing: it is not a path round.
if (getGraphConnectionKind(candidate.sourceOutput, candidate.targetInput) === 'storage') continue;
outgoing.set(candidate.source, [...(outgoing.get(candidate.source) ?? []), candidate.target]);
}
const seen = new Set<string>();
@ -862,7 +909,7 @@ function getSocket(editor: NodeEditor<HFSchemes>, type: string) {
const map = editorSockets.get(editor)!;
if (!map.has(type)) {
const socket = new ClassicPreset.Socket(type) as ClassicPreset.Socket & { __hfKind?: GraphConnectionKind };
socket.__hfKind = type === FLOW_DEPENDENCY_SOCKET_TYPE ? 'dependency' : 'data';
socket.__hfKind = type === FLOW_DEPENDENCY_SOCKET_TYPE ? 'dependency' : type === FLOW_STORAGE_SOCKET_TYPE ? 'storage' : 'data';
map.set(type, socket);
}
return map.get(type)!;
@ -872,11 +919,6 @@ export function isResourceNode(runtime: ReteRuntimeContext | undefined, node: HF
return resolveNodeCapabilities(runtime, node).visualRole === 'RESOURCE';
}
function touchesResource(editor: NodeEditor<HFSchemes>, runtime: ReteRuntimeContext, connection: { source: string; target: string }) {
return isResourceNode(runtime, editor.getNode(connection.source) as HFNode | undefined)
|| isResourceNode(runtime, editor.getNode(connection.target) as HFNode | undefined);
}
export function resolveNodeCapabilities(runtime: ReteRuntimeContext | undefined, node: HFNode | undefined): NodeTypeCapabilities {
if (node?.data?.capabilities) return node.data.capabilities;
@ -922,7 +964,7 @@ function cloneValue<T>(value: T): T {
}
function getGraphConnectionKind(sourceOutput: string, targetInput: string): GraphConnectionKind {
return sourceOutput === FLOW_DEPENDANT_PORT_KEY && targetInput === FLOW_DEPENDENCY_PORT_KEY
? 'dependency'
: 'data';
if (sourceOutput === FLOW_DEPENDANT_PORT_KEY && targetInput === FLOW_DEPENDENCY_PORT_KEY) return 'dependency';
if (sourceOutput === FLOW_STORAGE_PORT_KEY && targetInput === FLOW_STORAGE_LINK_PORT_KEY) return 'storage';
return 'data';
}