ui: make resource links visible and interactive
This commit is contained in:
parent
67d6bc6b71
commit
95ea013139
|
|
@ -33,7 +33,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy {
|
|||
|
||||
@HostBinding("style.width")
|
||||
get width() {
|
||||
return this.kind === "resource" ? "9px" : "8px";
|
||||
return this.kind === "resource" ? "7px" : "8px";
|
||||
}
|
||||
|
||||
@HostBinding("style.height")
|
||||
|
|
@ -42,7 +42,9 @@ export class CustomSocket implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
@HostBinding("style.display") d = "block";
|
||||
@HostBinding("style.borderRadius") br = "999px";
|
||||
@HostBinding("style.borderRadius") get borderRadius() {
|
||||
return this.kind === "resource" ? "4px" : "999px";
|
||||
}
|
||||
@HostBinding("style.border") border = "1px solid rgba(255,255,255,0.9)";
|
||||
@HostBinding("style.cursor") get cursor() {
|
||||
return this.data?.__readonly === true ? "default" : "crosshair";
|
||||
|
|
@ -55,7 +57,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy {
|
|||
@HostBinding("style.background")
|
||||
get bg() {
|
||||
if (this.kind === "resource") {
|
||||
return "linear-gradient(145deg, #2dd4bf 0%, #0f766e 100%)";
|
||||
return "linear-gradient(180deg, #5eead4 0%, #0f766e 100%)";
|
||||
}
|
||||
if (this.kind === "dependency") {
|
||||
return this.side === "input"
|
||||
|
|
@ -70,7 +72,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy {
|
|||
@HostBinding("style.boxShadow")
|
||||
get sh() {
|
||||
if (this.kind === "resource") {
|
||||
return "0 2px 6px rgba(15,118,110,0.45), 0 0 0 1px rgba(15,118,110,0.45)";
|
||||
return "0 1px 4px rgba(15,118,110,0.32), 0 0 0 1px rgba(13,148,136,0.42)";
|
||||
}
|
||||
if (this.kind === "dependency") {
|
||||
const c = this.side === "input" ? "rgba(217,119,6,0.45)" : "rgba(37,99,235,0.45)";
|
||||
|
|
@ -86,7 +88,7 @@ export class CustomSocket implements AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
private resolveSocketKind(): "data" | "dependency" | "resource" {
|
||||
const kind = this.data?.__hfKind;
|
||||
const kind = this.data?.__hfKind ?? this.data?.payload?.__hfKind;
|
||||
return kind === "dependency" || kind === "resource" ? kind : "data";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -595,6 +595,32 @@
|
|||
gap: 0;
|
||||
}
|
||||
|
||||
.llm-body-resource .llm-column {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* The Resource pill is the connection target itself, rather than having a separate tiny handle. */
|
||||
.llm-row-resource {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.llm-row-resource .llm-pill-resource {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
.llm-row-resource .llm-pill-resource ::ng-deep > * {
|
||||
position: absolute !important;
|
||||
inset: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.llm-socket {
|
||||
position: relative;
|
||||
left: -16px;
|
||||
|
|
@ -1024,7 +1050,26 @@
|
|||
}
|
||||
|
||||
.llm-pill-resource {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-color: #5eead4;
|
||||
background: #f0fdfa;
|
||||
background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%);
|
||||
color: #0f766e;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.llm-pill-resource--connected {
|
||||
border-color: #14b8a6;
|
||||
background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.14);
|
||||
}
|
||||
|
||||
.llm-pill-resource--connected .bi {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,12 +213,13 @@
|
|||
@if (resourceLinkInput) {
|
||||
<div class="llm-body llm-body-dependency llm-body-resource">
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title">Resource</div>
|
||||
@if (resourceLinkInput; as resourceLinkInput) {
|
||||
<div class="llm-row llm-row-input">
|
||||
<div
|
||||
<div class="llm-row llm-row-input llm-row-resource">
|
||||
<span
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
class="llm-pill llm-pill-input llm-pill-resource"
|
||||
[class.llm-pill-resource--connected]="resourceLinkIsConnected"
|
||||
[attr.title]="resourceLinkIsConnected ? 'Resource connected — drag another Resource here to replace it' : 'Drag a Resource here'"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
|
|
@ -228,8 +229,13 @@
|
|||
payload: resourceLinkInput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input llm-pill-resource">Works on</span>
|
||||
@if (resourceLinkIsConnected) {
|
||||
<i class="bi bi-link-45deg" aria-hidden="true"></i>
|
||||
Resource connected
|
||||
} @else {
|
||||
Resource
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -273,6 +273,14 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
return input ? { key: FLOW_RESOURCE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
/** A resource link is single-valued; the canvas stores the selected resource in this field. */
|
||||
get resourceLinkIsConnected(): boolean {
|
||||
const nodeData = this.data?.data as Record<string, any> | undefined;
|
||||
const field = nodeData?.['__resourceRefField'];
|
||||
const value = typeof field === 'string' ? nodeData?.['specificConfiguration']?.[field] : null;
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
get dependantOutput(): RenderedSocketPort | null {
|
||||
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -284,9 +284,9 @@
|
|||
|
||||
/* The socket sizes itself as a square; here it is stretched into the bar. */
|
||||
.resource-card__socket ::ng-deep > * {
|
||||
width: 9px !important;
|
||||
width: 7px !important;
|
||||
height: 100% !important;
|
||||
border-radius: 999px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.llm-spinner {
|
||||
|
|
|
|||
|
|
@ -504,6 +504,28 @@ button.llm-node-bias-summary:focus-visible {
|
|||
gap: 0;
|
||||
}
|
||||
|
||||
/* A storage Resource is selected through the whole labelled pill, not a separate handle. */
|
||||
.llm-row-resource {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.llm-row-resource .llm-pill-resource {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
.llm-row-resource .llm-pill-resource ::ng-deep > * {
|
||||
position: absolute !important;
|
||||
inset: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.llm-socket {
|
||||
position: relative;
|
||||
left: -16px;
|
||||
|
|
@ -846,7 +868,26 @@ button.llm-node-bias-summary:focus-visible {
|
|||
}
|
||||
|
||||
.llm-pill-resource {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-color: #5eead4;
|
||||
background: #f0fdfa;
|
||||
background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%);
|
||||
color: #0f766e;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.llm-pill-resource--connected {
|
||||
border-color: #14b8a6;
|
||||
background: linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.14);
|
||||
}
|
||||
|
||||
.llm-pill-resource--connected .bi {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,10 +254,12 @@
|
|||
<div class="llm-column">
|
||||
<div class="llm-column-title">Storage</div>
|
||||
@if (storageLinkInput; as storageLinkInput) {
|
||||
<div class="llm-row llm-row-input">
|
||||
<div
|
||||
<div class="llm-row llm-row-input llm-row-resource">
|
||||
<span
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
class="llm-pill llm-pill-input llm-pill-resource"
|
||||
[class.llm-pill-resource--connected]="storageLinkIsConnected"
|
||||
[attr.title]="storageLinkIsConnected ? 'Resource connected' : 'Resource'"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
|
|
@ -267,8 +269,13 @@
|
|||
payload: storageLinkInput.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input llm-pill-resource">Works on</span>
|
||||
@if (storageLinkIsConnected) {
|
||||
<i class="bi bi-link-45deg" aria-hidden="true"></i>
|
||||
Resource connected
|
||||
} @else {
|
||||
Resource
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -174,6 +174,13 @@ export class TaskStepNodeComponent {
|
|||
return input ? { key: FLOW_RESOURCE_LINK_PORT_KEY, socket: (input as any).socket as ClassicPreset.Socket } : null;
|
||||
}
|
||||
|
||||
get storageLinkIsConnected(): boolean {
|
||||
const nodeData = this.data?.data as Record<string, any> | undefined;
|
||||
const field = nodeData?.['__resourceRefField'];
|
||||
const value = typeof field === 'string' ? nodeData?.['specificConfiguration']?.[field] : null;
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
get dependantOutput(): { key: string; socket: ClassicPreset.Socket } | null {
|
||||
const output = this.data?.outputs?.[FLOW_DEPENDANT_PORT_KEY];
|
||||
return output
|
||||
|
|
|
|||
|
|
@ -215,6 +215,13 @@ export async function createEditor(
|
|||
return context;
|
||||
});
|
||||
|
||||
editor.addPipe(async (context) => {
|
||||
if (context.type === "connectioncreated" || context.type === "connectionremoved") {
|
||||
await syncResourceLinkPresentation(editor, area, runtime, context.data);
|
||||
}
|
||||
return context;
|
||||
});
|
||||
|
||||
area.addPipe((context: any) => {
|
||||
if (context?.type === 'nodetranslated') {
|
||||
// Any node moved can be one the way back has to clear, not only one of its two ends.
|
||||
|
|
@ -838,6 +845,34 @@ async function replaceResourceLink(editor: NodeEditor<HFSchemes>, stepNodeId: st
|
|||
}
|
||||
}
|
||||
|
||||
/** Keep a node's visible Resource state in step with the one allowed resource connection. */
|
||||
async function syncResourceLinkPresentation(
|
||||
editor: NodeEditor<HFSchemes>,
|
||||
area: AreaPlugin<HFSchemes, AreaExtra>,
|
||||
runtime: ReteRuntimeContext,
|
||||
connection: Pick<ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>, 'source' | 'sourceOutput' | 'target' | 'targetInput'>
|
||||
) {
|
||||
if (getGraphConnectionKind(connection.sourceOutput, connection.targetInput) !== 'resource') return;
|
||||
|
||||
const target = editor.getNode(connection.target) as HFNode | undefined;
|
||||
const reference = resourceRefOf(runtime, target);
|
||||
if (!target?.data || !reference) return;
|
||||
|
||||
const activeLink = editor.getConnections().find((candidate) =>
|
||||
candidate.target === target.id && candidate.targetInput === FLOW_RESOURCE_LINK_PORT_KEY);
|
||||
const source = activeLink ? editor.getNode(activeLink.source) as HFNode | undefined : undefined;
|
||||
const resourceId = typeof source?.data?.id === 'string' ? source.data.id : null;
|
||||
const targetData = target.data as Record<string, any>;
|
||||
const configuration = targetData['specificConfiguration'] as Record<string, unknown> | undefined;
|
||||
if (configuration?.[reference.field] === resourceId) return;
|
||||
|
||||
targetData['specificConfiguration'] = {
|
||||
...(configuration ?? {}),
|
||||
[reference.field]: resourceId
|
||||
};
|
||||
await area.update('node', target.id);
|
||||
}
|
||||
|
||||
async function withRestoredConnections<T>(runtime: ReteRuntimeContext | undefined, restore: () => Promise<T>): Promise<T> {
|
||||
if (!runtime) return restore();
|
||||
runtime.restoringConnections = (runtime.restoringConnections ?? 0) + 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue