Merge branch 'feat/add-naming-component-to-editor-nodes' into 'dev'
feat: added basic functionality for editable name bar See merge request ics/sp/2025/n25b/pepperplus-ui!17
This commit was merged in pull request #17.
This commit is contained in:
@@ -19,7 +19,28 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.node-text-input {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 5pt;
|
||||||
|
padding: 4px 8px;
|
||||||
|
outline: none;
|
||||||
|
background-color: white;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-text-input:focus {
|
||||||
|
border-color: gainsboro;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-text-input:read-only {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-text-input:read-only:hover {
|
||||||
|
border-color: gainsboro;
|
||||||
|
}
|
||||||
|
|
||||||
.dnd-panel {
|
.dnd-panel {
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
@@ -55,34 +76,22 @@
|
|||||||
filter: drop-shadow(0 0 0.75rem black);
|
filter: drop-shadow(0 0 0.75rem black);
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-node-norm {
|
.node-norm {
|
||||||
padding: 10px 15px;
|
|
||||||
background-color: canvas;
|
|
||||||
border-radius: 5pt;
|
|
||||||
outline: forestgreen solid 2pt;
|
outline: forestgreen solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem forestgreen);
|
filter: drop-shadow(0 0 0.25rem forestgreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-node-phase {
|
.node-phase {
|
||||||
padding: 10px 15px;
|
|
||||||
background-color: canvas;
|
|
||||||
border-radius: 5pt;
|
|
||||||
outline: dodgerblue solid 2pt;
|
outline: dodgerblue solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem dodgerblue);
|
filter: drop-shadow(0 0 0.25rem dodgerblue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-node-start {
|
.node-start {
|
||||||
padding: 10px 15px;
|
|
||||||
background-color: canvas;
|
|
||||||
border-radius: 5pt;
|
|
||||||
outline: orange solid 2pt;
|
outline: orange solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem orange);
|
filter: drop-shadow(0 0 0.25rem orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-node-end {
|
.node-end {
|
||||||
padding: 10px 15px;
|
|
||||||
background-color: canvas;
|
|
||||||
border-radius: 5pt;
|
|
||||||
outline: red solid 2pt;
|
outline: red solid 2pt;
|
||||||
filter: drop-shadow(0 0 0.25rem red);
|
filter: drop-shadow(0 0 0.25rem red);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
reconnectEdge, type Edge, type Connection
|
reconnectEdge, type Edge, type Connection
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import {type FlowState} from './VisProgTypes.tsx';
|
import {type AppNode, type FlowState} from './VisProgTypes.tsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* contains the nodes that are created when the editor is loaded,
|
* contains the nodes that are created when the editor is loaded,
|
||||||
@@ -110,6 +110,32 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
|||||||
setEdges: (edges) => {
|
setEdges: (edges) => {
|
||||||
set({edges});
|
set({edges});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* handles updating the data component of a node,
|
||||||
|
* if the provided data object contains entries that aren't present in the updated node's data component
|
||||||
|
* those entries are added to the data component,
|
||||||
|
* entries that do exist within the node's data component,
|
||||||
|
* are simply updated to contain the new value
|
||||||
|
*
|
||||||
|
* the data object
|
||||||
|
* @param {string} nodeId
|
||||||
|
* @param {object} data
|
||||||
|
*/
|
||||||
|
updateNodeData: (nodeId: string, data) => {
|
||||||
|
set({
|
||||||
|
nodes: get().nodes.map((node) : AppNode => {
|
||||||
|
if (node.id === nodeId) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else { return node; }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ export type FlowState = {
|
|||||||
deleteNode: (nodeId: string) => void;
|
deleteNode: (nodeId: string) => void;
|
||||||
setNodes: (nodes: AppNode[]) => void;
|
setNodes: (nodes: AppNode[]) => void;
|
||||||
setEdges: (edges: Edge[]) => void;
|
setEdges: (edges: Edge[]) => void;
|
||||||
|
updateNodeData: (nodeId: string, data: object) => void;
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import {Handle, type NodeProps, NodeToolbar, Position} from '@xyflow/react';
|
import {
|
||||||
|
Handle,
|
||||||
|
type NodeProps,
|
||||||
|
NodeToolbar,
|
||||||
|
Position
|
||||||
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import useFlowStore from "../VisProgStores.tsx";
|
import useFlowStore from "../VisProgStores.tsx";
|
||||||
@@ -9,7 +14,7 @@ import type {
|
|||||||
NormNode
|
NormNode
|
||||||
} from "../VisProgTypes.tsx";
|
} from "../VisProgTypes.tsx";
|
||||||
|
|
||||||
//
|
//Toolbar definitions
|
||||||
|
|
||||||
type ToolbarProps = {
|
type ToolbarProps = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -39,6 +44,56 @@ export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
|
|||||||
</NodeToolbar>);
|
</NodeToolbar>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renaming component
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a component that can be used to edit a node's label entry inside its Data
|
||||||
|
* can be added to any custom node that has a label inside its Data
|
||||||
|
*
|
||||||
|
* @param {string} nodeLabel
|
||||||
|
* @param {string} nodeId
|
||||||
|
* @returns {React.JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export function EditableName({nodeLabel = "new node", nodeId} : { nodeLabel : string, nodeId: string}) {
|
||||||
|
const {updateNodeData} = useFlowStore();
|
||||||
|
|
||||||
|
const updateData = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
const input = event.target.value;
|
||||||
|
updateNodeData(nodeId, {label: input});
|
||||||
|
event.currentTarget.setAttribute("readOnly", "true");
|
||||||
|
window.getSelection()?.empty();
|
||||||
|
event.currentTarget.classList.replace("nodrag", "drag"); // enable dragging of the node with cursor on the input box
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") (event.target as HTMLInputElement).blur(); };
|
||||||
|
|
||||||
|
const enableEditing = (event: React.MouseEvent<HTMLInputElement>) => {
|
||||||
|
if(event.currentTarget.hasAttribute("readOnly")) {
|
||||||
|
event.currentTarget.removeAttribute("readOnly"); // enable editing
|
||||||
|
event.currentTarget.select(); // select the text input
|
||||||
|
window.getSelection()?.collapseToEnd(); // move the caret to the end of the current value
|
||||||
|
event.currentTarget.classList.replace("drag", "nodrag"); // disable dragging using input box
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.NodeTextBar }>
|
||||||
|
<label>name: </label>
|
||||||
|
<input
|
||||||
|
className={`drag ${styles.nodeTextInput}`} // prevents dragging the component when user has focused the text input
|
||||||
|
type={"text"}
|
||||||
|
defaultValue={nodeLabel}
|
||||||
|
onKeyDown={updateOnEnter}
|
||||||
|
onBlur={updateData}
|
||||||
|
onClick={enableEditing}
|
||||||
|
maxLength={25}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Definitions of Nodes
|
// Definitions of Nodes
|
||||||
|
|
||||||
@@ -54,7 +109,7 @@ export const StartNodeComponent = ({id, data}: NodeProps<StartNode>) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={id} allowDelete={false}/>
|
<Toolbar nodeId={id} allowDelete={false}/>
|
||||||
<div className={styles.defaultNodeStart}>
|
<div className={`${styles.defaultNode} ${styles.nodeStart}`}>
|
||||||
<div> data test {data.label} </div>
|
<div> data test {data.label} </div>
|
||||||
<Handle type="source" position={Position.Right} id="start"/>
|
<Handle type="source" position={Position.Right} id="start"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +130,7 @@ export const EndNodeComponent = ({id, data}: NodeProps<EndNode>) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={id} allowDelete={false}/>
|
<Toolbar nodeId={id} allowDelete={false}/>
|
||||||
<div className={styles.defaultNodeEnd}>
|
<div className={`${styles.defaultNode} ${styles.nodeEnd}`}>
|
||||||
<div> {data.label} </div>
|
<div> {data.label} </div>
|
||||||
<Handle type="target" position={Position.Left} id="end"/>
|
<Handle type="target" position={Position.Left} id="end"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,8 +151,8 @@ export const PhaseNodeComponent = ({id, data}: NodeProps<PhaseNode>) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={id} allowDelete={true}/>
|
<Toolbar nodeId={id} allowDelete={true}/>
|
||||||
<div className={styles.defaultNodePhase}>
|
<div className={`${styles.defaultNode} ${styles.nodePhase}`}>
|
||||||
<div> phase {data.number} {data.label} </div>
|
<EditableName nodeLabel={data.label} nodeId={id}/>
|
||||||
<Handle type="target" position={Position.Left} id="target"/>
|
<Handle type="target" position={Position.Left} id="target"/>
|
||||||
<Handle type="target" position={Position.Bottom} id="norms"/>
|
<Handle type="target" position={Position.Bottom} id="norms"/>
|
||||||
<Handle type="source" position={Position.Right} id="source"/>
|
<Handle type="source" position={Position.Right} id="source"/>
|
||||||
@@ -119,8 +174,8 @@ export const NormNodeComponent = ({id, data}: NodeProps<NormNode>) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={id} allowDelete={true}/>
|
<Toolbar nodeId={id} allowDelete={true}/>
|
||||||
<div className={styles.defaultNodeNorm}>
|
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
||||||
<div> Norm {data.label} </div>
|
<EditableName nodeLabel={data.label} nodeId={id}/>
|
||||||
<Handle type="source" position={Position.Right} id="NormSource"/>
|
<Handle type="source" position={Position.Right} id="NormSource"/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -955,7 +955,7 @@ describe('Graph Reducer Tests', () => {
|
|||||||
state: onlyStartEnd,
|
state: onlyStartEnd,
|
||||||
expected: [],
|
expected: [],
|
||||||
}
|
}
|
||||||
])("`tests state: $state.name`", ({state, expected}) => {
|
])(`tests state: $state.name`, ({state, expected}) => {
|
||||||
useFlowStore.setState({nodes: state.nodes, edges: state.edges});
|
useFlowStore.setState({nodes: state.nodes, edges: state.edges});
|
||||||
const output = graphReducer(); // uses default reducers
|
const output = graphReducer(); // uses default reducers
|
||||||
expect(output).toEqual(expected);
|
expect(output).toEqual(expected);
|
||||||
|
|||||||
@@ -221,4 +221,132 @@ describe('FlowStore Functionality', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('ReactFlow updateNodeData', () => {
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
name: 'updateName',
|
||||||
|
nodes: [{
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2'}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
id: 'phase-1',
|
||||||
|
changedData: {label: 'new name'}
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
node: {
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'new name', number: '2'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
name: 'updateNumber',
|
||||||
|
nodes: [{
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2'}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
id: 'phase-1',
|
||||||
|
changedData: {number: '3'}
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
node: {
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '3'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
name: 'updateNameAndNumber',
|
||||||
|
nodes: [{
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2'}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
id: 'phase-1',
|
||||||
|
changedData: {label: 'new name', number: '3'}
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
node: {
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'new name', number: '3'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
name: 'AddNewEntry',
|
||||||
|
nodes: [{
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2'}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
id: 'phase-1',
|
||||||
|
changedData: {newEntry: 20}
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
node: {
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2', newEntry: 20}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: {
|
||||||
|
name: 'AddNewEntryAndUpdateOneValue_UnorderedInput',
|
||||||
|
nodes: [{
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '2'}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
id: 'phase-1',
|
||||||
|
changedData: {newEntry: 20, number: '3'}
|
||||||
|
},
|
||||||
|
expected: {
|
||||||
|
node: {
|
||||||
|
id: 'phase-1',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'name', number: '3', newEntry: 20}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])(`tests state: $state.name`, ({state, input,expected}) => {
|
||||||
|
useFlowStore.setState({ nodes: state.nodes })
|
||||||
|
const {updateNodeData} = useFlowStore.getState();
|
||||||
|
act(() => {
|
||||||
|
updateNodeData(input.id, input.changedData);
|
||||||
|
})
|
||||||
|
const updatedState = useFlowStore.getState();
|
||||||
|
expect(updatedState.nodes).toHaveLength(1);
|
||||||
|
expect(updatedState.nodes[0]).toMatchObject(expected.node);
|
||||||
|
})
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user