Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow attaching Literals to their target nodes #340

Merged
merged 4 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/commands/NodeActionCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,11 @@ export function addNodeActionCommands(
});
return null;
}


const connections = Object.values(selected_node.ports)
.map((p: CustomPortModel) => Object.keys(p.links).length)
.reduce((a, b) => a+b);

const literalType = selected_node["extras"]["type"];
let oldValue = selected_node.getPorts()["out-0"].getOptions()["label"];

Expand All @@ -794,8 +798,8 @@ export function addNodeActionCommands(
}

const updateTitle = `Update ${literalType}`;
let nodeData: CustomNodeModelOptions = {color: selected_node["color"], type: selected_node["extras"]["type"]}
let updatedContent = await handleLiteralInput(selected_node["name"], nodeData, oldValue, literalType, updateTitle);
let nodeData: CustomNodeModelOptions = {color: selected_node["color"], type: selected_node["extras"]["type"], extras: {attached: selected_node["extras"]["attached"]}}
let updatedContent = await handleLiteralInput(selected_node["name"], nodeData, oldValue, literalType, updateTitle, connections);

if (!updatedContent) {
// handle case where Cancel was clicked or an error occurred
Expand Down
8 changes: 4 additions & 4 deletions src/components/link/CustomLinkFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ namespace S {

function addHover(model: TriangleLinkModel | ParameterLinkModel){
return (() => {
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList?.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList?.add("hover");
});
}

function removeHover(model: TriangleLinkModel | ParameterLinkModel){
return () => {
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid='${model.getSourcePort().getNode().getID()}'][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList?.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList?.remove("hover");
}
}

Expand Down
45 changes: 26 additions & 19 deletions src/components/node/CustomNodeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ const CommentNode = ({ node }) => {
);
};

const PortsComponent = ({node, engine, app}) => {
const renderPort = (port) => {
return <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} app={app} />
};
return (
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), renderPort)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), renderPort)}</S.PortsContainer>
</S.Ports>
)
}

const ParameterNode = ({ node, engine, app }) => {
const handleEditParameter = () => {
const nodeName = node.getOptions()["name"];
Expand All @@ -201,6 +213,10 @@ const ParameterNode = ({ node, engine, app }) => {
app.commands.execute(commandIDs.editNode);
};

if(node.getOptions().extras['attached']){
return <></>;
}

return (
<S.Node
borderColor={node.getOptions().extras["borderColor"]}
Expand All @@ -214,15 +230,12 @@ const ParameterNode = ({ node, engine, app }) => {
{/* <S.IconContainer>{getNodeIcon('parameter')}</S.IconContainer> */}
<S.TitleName>{node.getOptions().name}</S.TitleName>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
);
};

const StartFinishNode = ({ node, engine, handleDeletableNode }) => (
const StartFinishNode = ({ node, engine, handleDeletableNode, app }) => (
<S.Node
borderColor={node.getOptions().extras["borderColor"]}
data-default-node-name={node.getOptions().name}
Expand All @@ -237,14 +250,11 @@ const StartFinishNode = ({ node, engine, handleDeletableNode }) => (
<Toggle className='lock' checked={node.isLocked() ?? false} onChange={event => handleDeletableNode('nodeDeletable', event)} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
);

const WorkflowNode = ({ node, engine, handleDeletableNode }) => {
const WorkflowNode = ({ node, engine, app, handleDeletableNode }) => {
const elementRef = React.useRef<HTMLElement>(null);
return (
<div style={{ position: "relative" }}>
Expand All @@ -265,16 +275,13 @@ const WorkflowNode = ({ node, engine, handleDeletableNode }) => {
<Toggle className='lock' checked={node.isLocked() ?? false} onChange={event => handleDeletableNode('nodeDeletable', event)} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.WorkflowNode>
</div>
);
};

const ComponentLibraryNode = ({ node, engine, shell, handleDeletableNode }) => {
const ComponentLibraryNode = ({ node, engine, shell, app, handleDeletableNode }) => {
const [showDescription, setShowDescription] = React.useState(false);
const [descriptionStr, setDescriptionStr] = React.useState("");
const elementRef = React.useRef<HTMLElement>(null);
Expand Down Expand Up @@ -330,10 +337,7 @@ const ComponentLibraryNode = ({ node, engine, shell, handleDeletableNode }) => {
<Toggle className='description' name='Description' checked={showDescription ?? false} onChange={handleDescription} />
</label>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(node.getInPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
<S.PortsContainer>{_.map(node.getOutPorts(), port => <CustomPortLabel engine={engine} port={port} key={port.getID()} node={node} />)}</S.PortsContainer>
</S.Ports>
<PortsComponent node={node} engine={engine} app={app}/>
</S.Node>
{(node.getOptions().extras["tip"] != undefined && node.getOptions().extras["tip"] != "") ?
<ReactTooltip
Expand Down Expand Up @@ -407,6 +411,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
return <WorkflowNode
node={node}
engine={engine}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand All @@ -415,6 +420,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
return <StartFinishNode
node={node}
engine={engine}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand All @@ -423,6 +429,7 @@ export class CustomNodeWidget extends React.Component<DefaultNodeProps> {
node={node}
engine={engine}
shell={shell}
app={app}
handleDeletableNode={this.handleDeletableNode}
/>;
}
Expand Down
130 changes: 113 additions & 17 deletions src/components/port/CustomPortLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import * as React from 'react';
import { DiagramEngine, PortWidget } from '@projectstorm/react-diagrams-core';
import { DefaultNodeModel, DefaultPortModel } from "@projectstorm/react-diagrams";
import styled from '@emotion/styled';
import { JupyterFrontEnd } from "@jupyterlab/application";
import { commandIDs } from "../../commands/CommandIDs";
import Color from "colorjs.io";

export interface CustomPortLabelProps {
port: DefaultPortModel;
engine: DiagramEngine;
node: DefaultNodeModel;
app: JupyterFrontEnd;
}

namespace S {
Expand All @@ -24,7 +28,7 @@ namespace S {
max-width: 40ch;
`;

export const SymbolContainer = styled.div<{ symbolType: string; selected: boolean; isOutPort: boolean }>`
export const SymbolContainer = styled.div<{ symbolType: string; selected: boolean; isOutPort: boolean; attachedColor?: string }>`
width: 15px;
height: 15px;
background: ${(p) => (p.selected ? 'oklch(1 0 0 / 0.5)' : 'oklch(50% 0 0 / 0.2)')};
Expand All @@ -39,6 +43,18 @@ namespace S {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.selected ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}

&.attached {
padding: 0 2px 0 3px;
border-radius: 20px;
border: 0;
background: ${(p) => p.attachedColor};

&:hover, &.hover {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.selected ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}
}
`;

export const Symbol = styled.div<{ selected: boolean; isOutPort: boolean }>`
Expand All @@ -49,7 +65,7 @@ namespace S {
padding:${(p) => (p.isOutPort ? '2px 0px 0px 2px' : '2px 2px 0px 0px')};
`;

export const Port = styled.div<{ isOutPort: boolean, hasLinks: boolean }>`
export const Port = styled.div<{ isOutPort: boolean, hasLinks: boolean; attachedColor?: string}>`
width: 15px;
height: 15px;
background: ${(p) => p.hasLinks ? 'oklch(1 0 0 / 0.5)' : 'oklch(50% 0 0 / 0.2)'};
Expand All @@ -73,9 +89,42 @@ namespace S {
stroke-linecap: round;
stroke-linejoin: round;
}
&.attached {
padding: 0 2px 0 3px;
border-radius: 20px;
border: 0;
background: ${(p) => p.attachedColor};
&:hover, &.hover {
background: rgb(192, 255, 0);
box-shadow: ${(p) => p.hasLinks ? '' : 'inset'} 0 4px 8px rgb(0 0 0 / 0.5);
}
}
`;
}

const PortLabel = ({nodeType, port}) => {
const LITERAL_SECRET = "Literal Secret";

let labelText = port.getOptions().label.replace('▶', '').trim();

let attached = false;
if(port.getOptions().in){
Object.values(port.links).forEach(link => {
if(link['sourcePort']['parent']['name'].startsWith('Literal ') && link['sourcePort']['parent']['extras']['attached']){
attached = true;
const label = link['sourcePort']['parent']['name'] === LITERAL_SECRET ? "*****" : link['sourcePort']['options']['label']
labelText += ": "+(label.length > 18 ? label.substring(0, 15)+"..." : label)
}
})
}

return (
<S.Label style={{ textAlign: (!port.getOptions().in && port.getOptions().label === '▶') ? 'right' : 'left', cursor: attached ? 'pointer' : 'inherit' }}>
{nodeType === LITERAL_SECRET ? "*****" : labelText}
</S.Label>
);
}

export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
render() {
let portName = this.props.port.getOptions().name;
Expand Down Expand Up @@ -123,8 +172,36 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
/* Workaround for Arguments being set up as triangle ports in other places */
!this.props.node['name'].match('Argument \(.+?\):');

let dblClickHandler = () => {};
let attachedColor = null;
if(this.props.port.getOptions().in){
Object.values(this.props.port.links).forEach(link => {
if(link['sourcePort']['parent']['name'].startsWith('Literal ') && link['sourcePort']['parent']['extras']['attached']){
attachedColor = link['sourcePort']['parent']['options']['color'];

dblClickHandler = () => {
this.props.engine.getModel().clearSelection();
link['sourcePort']['parent'].setSelected(true);
this.props.app.commands.execute(commandIDs.editNode);
}
}
})
}

if(attachedColor != null){
const color = new Color(attachedColor);
color.alpha = 0.75;
color.oklch.c *= 1.2;
const color1 = color.to('oklch').toString()
color.oklch.c *= 1.2;
color.oklch.l /= 2;
const color2 = color.to('oklch').toString()

attachedColor = `linear-gradient(${color1}, ${color2})`;
}

const port = (
<S.Port isOutPort={!isIn} hasLinks={hasLinks}>
<S.Port isOutPort={!isIn} hasLinks={hasLinks} className={attachedColor ? 'attached' : null} attachedColor={attachedColor}>
{!isTrianglePort ? null : (isIn ?
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" >
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
Expand All @@ -148,7 +225,7 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {


const symbol = (
<S.SymbolContainer symbolType={symbolLabel} selected={portHasLink} isOutPort={isOutPort}>
<S.SymbolContainer symbolType={symbolLabel} selected={portHasLink} isOutPort={isOutPort} className={attachedColor ? 'attached' : null} attachedColor={attachedColor}>
<S.Symbol isOutPort={isOutPort} selected={portHasLink}>
{symbolLabel}
</S.Symbol>
Expand All @@ -159,40 +236,59 @@ export class CustomPortLabel extends React.Component<CustomPortLabelProps> {
function addHover(port: DefaultPortModel) {
return (() => {
for (let linksKey in port.getLinks()) {
document.querySelector(`g[data-linkid='${linksKey}']`).classList.add("hover");
document.querySelector(`g[data-linkid='${linksKey}']`)?.classList.add("hover");
const model = port.getLinks()[linksKey]
if(model.getSourcePort() != null)
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList.add("hover");
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.add("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList.add("hover");
if(attachedColor != null){
if(model.getSourcePort() != null){
Object.values(model.getSourcePort().getNode().getPorts()).forEach(p => {
Object.values(p.getLinks()).forEach(l => {
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${l.getTargetPort().getNode().getID()}"][data-name='${l.getTargetPort().getName()}']>div>div`)?.classList.add("hover");
})
})
}
}
}
});
}

function removeHover(port: DefaultPortModel) {
return () => {
for (let linksKey in port.getLinks()) {
document.querySelector(`g[data-linkid='${linksKey}']`).classList.remove("hover");
document.querySelector(`g[data-linkid='${linksKey}']`)?.classList.remove("hover");
const model = port.getLinks()[linksKey]
if(model.getSourcePort() != null)
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getSourcePort().getNode().getID()}"][data-name='${model.getSourcePort().getName()}']>div>div`)?.classList.remove("hover");
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`).classList.remove("hover");
document.querySelector(`div.port[data-nodeid="${model.getTargetPort().getNode().getID()}"][data-name='${model.getTargetPort().getName()}']>div>div`)?.classList.remove("hover");
if(attachedColor != null){
if(model.getSourcePort() != null){
Object.values(model.getSourcePort().getNode().getPorts()).forEach(p => {
Object.values(p.getLinks()).forEach(l => {
if(model.getTargetPort() != null)
document.querySelector(`div.port[data-nodeid="${l.getTargetPort().getNode().getID()}"][data-name='${l.getTargetPort().getName()}']>div>div`)?.classList.remove("hover");
})
})
}
}
}
};
}

const label = (
<S.Label style={{ textAlign: (!this.props.port.getOptions().in && this.props.port.getOptions().label === '▶') ? 'right' : 'left' }}>
{nodeType === "Literal Secret" ? "*****" : this.props.port.getOptions().label.replace('▶', '').trim()}
</S.Label>);
const label = <PortLabel port={this.props.port} nodeType={nodeType} />

return (
<S.PortLabel>
<S.PortLabel onMouseOver={addHover(this.props.port)}
onMouseOut={removeHover(this.props.port)}
onDoubleClick={dblClickHandler}
>
{this.props.port.getOptions().in ? null : label}
<PortWidget engine={this.props.engine} port={this.props.port}>
<div onMouseOver={addHover(this.props.port)}
onMouseOut={removeHover(this.props.port)}>
<div>
{symbolLabel == null ? port : symbol}
</div>
</PortWidget>
Expand Down
3 changes: 3 additions & 0 deletions src/components/port/CustomPortModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export class CustomPortModel extends DefaultPortModel {
}

canLinkToPort(port: CustomPortModel): boolean {
// No self connections allowed
if(port === this) return false;

if (port instanceof DefaultPortModel) {
if(this.options.in === port.getOptions().in){
port.getNode().getOptions().extras["borderColor"]="red";
Expand Down
Loading