Add more functionality to Nodes (#7278)

updating node behaviour
pull/7275/head
Aarushi 2024-06-27 17:03:10 +01:00 committed by GitHub
parent 785a40ff9d
commit 6093acc813
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 363 additions and 37 deletions

View File

@ -1,15 +1,21 @@
import React, { useState, useEffect, FC, memo } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import 'reactflow/dist/style.css';
import './customnode.css';
import ModalComponent from './ModalComponent';
type Schema = {
type: string;
properties: { [key: string]: any };
required?: string[];
};
const CustomNode: FC<NodeProps> = ({ data }) => {
const CustomNode: FC<NodeProps> = ({ data, id }) => {
const [isPropertiesOpen, setIsPropertiesOpen] = useState(data.isPropertiesOpen || false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>('');
// Automatically open properties when output_data or status is updated
useEffect(() => {
if (data.output_data || data.status) {
setIsPropertiesOpen(true);
@ -24,7 +30,7 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
if (!schema?.properties) return null;
const keys = Object.keys(schema.properties);
return keys.map((key) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', position: 'relative', marginBottom: '5px' }}>
<div key={key} className="handle-container">
{type === 'target' && (
<>
<Handle
@ -33,12 +39,12 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span style={{ color: '#e0e0e0', marginLeft: '10px' }}>{key}</span>
<span className="handle-label">{key}</span>
</>
)}
{type === 'source' && (
<>
<span style={{ color: '#e0e0e0', marginRight: '10px' }}>{key}</span>
<span className="handle-label">{key}</span>
<Handle
type={type}
position={Position.Right}
@ -57,57 +63,160 @@ const CustomNode: FC<NodeProps> = ({ data }) => {
};
const isHandleConnected = (key: string) => {
return data.connections.some((conn: string) => {
const [, target] = conn.split(' -> ');
return data.connections && data.connections.some((conn: string) => {
const [source, target] = conn.split(' -> ');
return target.includes(key) && target.includes(data.title);
});
};
const hasDisconnectedHandle = (key: string) => {
return !isHandleConnected(key);
const handleInputClick = (key: string) => {
setActiveKey(key);
setModalValue(data.hardcodedValues[key] || '');
setIsModalOpen(true);
};
const handleModalSave = (value: string) => {
if (activeKey) {
handleInputChange(activeKey, value);
}
setIsModalOpen(false);
setActiveKey(null);
};
const renderInputField = (key: string, schema: any) => {
switch (schema.type) {
case 'string':
return schema.enum ? (
<div key={key} className="input-container">
<select
value={data.hardcodedValues[key] || ''}
onChange={(e) => handleInputChange(key, e.target.value)}
className="select-input"
>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
) : (
<div key={key} className="input-container">
<div className="clickable-input" onClick={() => handleInputClick(key)}>
{data.hardcodedValues[key] || `Enter ${key}`}
</div>
</div>
);
case 'boolean':
return (
<div key={key} className="input-container">
<label className="radio-label">
<input
type="radio"
value="true"
checked={data.hardcodedValues[key] === true}
onChange={() => handleInputChange(key, true)}
/>
True
</label>
<label className="radio-label">
<input
type="radio"
value="false"
checked={data.hardcodedValues[key] === false}
onChange={() => handleInputChange(key, false)}
/>
False
</label>
</div>
);
case 'integer':
case 'number':
return (
<div key={key} className="input-container">
<input
type="number"
value={data.hardcodedValues[key] || ''}
onChange={(e) => handleInputChange(key, parseFloat(e.target.value))}
className="number-input"
/>
</div>
);
case 'array':
if (schema.items && schema.items.type === 'string' && schema.items.enum) {
return (
<div key={key} className="input-container">
<select
value={data.hardcodedValues[key] || ''}
onChange={(e) => handleInputChange(key, e.target.value)}
className="select-input"
>
{schema.items.enum.map((option: string) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
);
}
return null;
default:
return null;
}
};
return (
<div style={{ padding: '20px', border: '2px solid #fff', borderRadius: '20px', background: '#333', color: '#e0e0e0', width: '250px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>
{data?.title.replace(/\d+/g, '')}
</div>
<button
onClick={toggleProperties}
style={{ background: 'transparent', border: 'none', cursor: 'pointer', color: '#e0e0e0' }}
>
<div className="custom-node">
<div className="node-header">
<div className="node-title">{data?.title.replace(/\d+/g, '')}</div>
<button onClick={toggleProperties} className="toggle-button">
&#9776;
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', gap: '20px' }}>
<div>
{data.inputSchema && generateHandles(data.inputSchema, 'target')}
{data.inputSchema && Object.keys(data.inputSchema.properties).map(key => (
hasDisconnectedHandle(key) && (
<div key={key} style={{ marginBottom: '5px' }}>
<input
type="text"
placeholder={`Enter ${key}`}
value={data.hardcodedValues[key] || ''}
onChange={(e) => handleInputChange(key, e.target.value)}
style={{ width: '100%', padding: '5px', borderRadius: '4px', border: '1px solid #555', background: '#444', color: '#e0e0e0' }}
/>
<div className="node-content">
<div className="input-section">
{data.inputSchema &&
Object.keys(data.inputSchema.properties).map((key) => (
<div key={key}>
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%' }}
/>
<span className="handle-label">{key}</span>
</div>
{!isHandleConnected(key) && renderInputField(key, data.inputSchema.properties[key])}
</div>
)
))}
))}
</div>
<div>
<div className="output-section">
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
</div>
</div>
{isPropertiesOpen && (
<div style={{ marginTop: '10px', background: '#444', padding: '10px', borderRadius: '10px' }}>
<div className="node-properties">
<h4>Node Output</h4>
<p><strong>Status:</strong> {typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}</p>
<p><strong>Output Data:</strong> {typeof data.output_data === 'object' ? JSON.stringify(data.output_data) : data.output_data || 'N/A'}</p>
<p>
<strong>Status:</strong>{' '}
{typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}
</p>
<p>
<strong>Output Data:</strong>{' '}
{typeof data.output_data === 'object'
? JSON.stringify(data.output_data)
: data.output_data || 'N/A'}
</p>
</div>
)}
<ModalComponent
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleModalSave}
value={modalValue}
/>
</div>
);
};

View File

@ -0,0 +1,40 @@
import React, { FC } from 'react';
import './modal.css';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (value: string) => void;
value: string;
}
const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
const [tempValue, setTempValue] = React.useState(value);
const handleSave = () => {
onSave(tempValue);
onClose();
};
if (!isOpen) {
return null;
}
return (
<div className="modal-overlay">
<div className="modal">
<textarea
className="modal-textarea"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
/>
<div className="modal-actions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
</div>
</div>
);
};
export default ModalComponent;

View File

@ -0,0 +1,143 @@
.custom-node {
padding: 20px;
border: 2px solid #fff;
border-radius: 12px;
background: #1e1e1e;
color: #e0e0e0;
width: 260px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
font-family: 'Arial', sans-serif;
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.node-title {
font-size: 20px;
font-weight: bold;
color: #61dafb;
}
.toggle-button {
background: transparent;
border: none;
cursor: pointer;
color: #e0e0e0;
font-size: 20px;
}
.node-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.input-section, .output-section {
flex: 1;
}
.handle-container {
display: flex;
align-items: center;
position: relative;
margin-bottom: 10px;
}
.handle-label {
color: #e0e0e0;
margin-left: 10px;
}
.input-container {
margin-bottom: 10px;
}
.clickable-input {
padding: 10px;
border-radius: 8px;
border: 1px solid #555;
background: #2a2a2a;
color: #e0e0e0;
cursor: pointer;
text-align: center;
transition: background 0.3s;
}
.clickable-input:hover {
background: #444;
}
.select-input {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #555;
background: #2a2a2a;
color: #e0e0e0;
cursor: pointer;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.radio-label {
color: #e0e0e0;
}
.radio-label input {
margin-right: 10px;
}
.number-input {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid #555;
background: #2a2a2a;
color: #e0e0e0;
}
.node-properties {
margin-top: 20px;
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
}
.node-properties h4 {
margin: 0 0 10px 0;
}
.node-properties p {
margin: 5px 0;
}
@media screen and (max-width: 768px) {
.custom-node {
width: 90%;
padding: 15px;
}
.node-content {
flex-direction: column;
}
.toggle-button {
font-size: 16px;
}
.node-title {
font-size: 18px;
}
.clickable-input {
padding: 8px;
}
}

View File

@ -0,0 +1,34 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background: #fff;
padding: 20px;
border-radius: 8px;
width: 500px;
max-width: 90%;
}
.modal-textarea {
width: 100%;
height: 200px;
padding: 10px;
border-radius: 4px;
border: 1px solid #ccc;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 10px;
}