Skip to content
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
94 changes: 92 additions & 2 deletions flowming/src/components/Flow/FlowContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { NodeBlock } from '../Toolbar/ToolbarTypes';
import ContextMenu from './ContextMenu';
import { useDnD } from '../../context/DnDContext';
import { useFlowExecutorState } from '../../context/FlowExecutorContext';
import { Expression, Variable } from '../../models';
import { Expression, Variable, ExpressionElement } from '../../models';
import { decisionEdgeLabels } from './Nodes/Conditional';
import FilenameEditor from '../FilenameEditor';
import { useCollaboration } from '../../context/CollaborationContext';
Expand Down Expand Up @@ -659,6 +659,40 @@ const FlowContent: React.FC = () => {

if (!needsUpdate) return;

// Helper to recursively update ExpressionElement lists (array index expressions)
const updateElements = (elements: any[]): ExpressionElement[] => {
return elements.flatMap((rawElem: any) => {
// Ensure we are working with an ExpressionElement instance (objects coming from Yjs/serialization won't have class methods)
const elem: ExpressionElement = rawElem instanceof ExpressionElement
? rawElem
: ExpressionElement.fromObject(rawElem);

if (elem.type !== 'variable') {
// Propagate into nested function calls, if any
if (elem.isFunction() && elem.nestedExpression) {
elem.nestedExpression.rightSide = updateElements(elem.nestedExpression.rightSide);
}
return [elem];
}

const match = variables.find(v => v.id === elem.variable?.id);
if (!match) return []; // Variable deleted -> drop element

const clonedVar = match.clone();
if (elem.variable?.indexExpression && clonedVar.type === 'array') {
const newIdx = updateElements(elem.variable.indexExpression as ExpressionElement[]);
if (newIdx.length > 0) clonedVar.indexExpression = newIdx;
}

// If index expression became empty, remove the whole variable element
if (elem.variable?.indexExpression && (!clonedVar.indexExpression || clonedVar.indexExpression.length === 0)) {
return [];
}

return [new ExpressionElement(elem.id, elem.type, clonedVar.toString(), clonedVar)];
});
};

setNodes(prevNodes => prevNodes.map(node => {
if ((node.type === 'AssignVariable' || node.type === 'Conditional' || node.type === 'Output') && node.data.expression) {
try {
Expand All @@ -668,13 +702,59 @@ const FlowContent: React.FC = () => {
const expression = Expression.fromObject(currentExprData);
expression.updateVariables(variables);

// Cleanup for left-side Variable (especially array element assignments)
if (expression.leftSide instanceof Variable) {
const leftSideId = expression.leftSide.id;
const leftMatch = variables.find(v => v.id === leftSideId);

if (!leftMatch) {
expression.leftSide = undefined; // Variable removed entirely
} else {
const leftClone = leftMatch.clone();

// Recursively update its index expression (if any)
if (expression.leftSide.indexExpression && expression.leftSide.indexExpression.length > 0) {
const updatedIdx = updateElements(expression.leftSide.indexExpression as ExpressionElement[]);
if (updatedIdx.length > 0) {
leftClone.indexExpression = updatedIdx;
}
}

// If after update the index expression is gone, drop the left side reference
if (expression.leftSide.indexExpression && (!leftClone.indexExpression || leftClone.indexExpression.length === 0)) {
expression.leftSide = undefined;
} else {
expression.leftSide = leftClone;
}
}
} else if (Array.isArray(expression.leftSide)) {
const cleanedLeft = updateElements(expression.leftSide as ExpressionElement[]);
if (cleanedLeft.length > 0) {
expression.leftSide = cleanedLeft;
} else {
expression.leftSide = undefined;
}
}

return { ...node, data: { ...node.data, expression: expression.toObject() } };
} catch {
return { ...node, data: { ...node.data, expression: null } };
}
} else if (node.type === 'Input' && node.data.variable) {
const updatedVariable = variables.find(v => v.id === node.data.variable?.id);
return { ...node, data: { ...node.data, variable: updatedVariable ? JSON.parse(JSON.stringify(updatedVariable)) : undefined } };
if (updatedVariable) {
const varCopy = updatedVariable.clone();
if (node.data.variable.indexExpression && node.data.variable.indexExpression.length > 0) {
const updatedIdx = updateElements(node.data.variable.indexExpression as ExpressionElement[]);
if (updatedIdx.length > 0) {
varCopy.indexExpression = updatedIdx;
} else {
return { ...node, data: { ...node.data, variable: undefined } };
}
}
return { ...node, data: { ...node.data, variable: varCopy } };
}
return { ...node, data: { ...node.data, variable: undefined } };
}

return node;
Expand Down Expand Up @@ -862,6 +942,16 @@ const FlowContent: React.FC = () => {
// TODO: cannot move draggable elements in expression builder if its >= 2 lines height (only horizontally)

// Timer: https://www.timeanddate.com/countdown/generic?iso=20250616T12&p0=%3A&font=cursive

// IDEA: Declare variable block could not be a block, and a simply display in some corner of the flow
// It would be displayed as a square with a form just like the one in the editor for Declare
// In this way, adding a "irrelevant" block wouldn't be needed and you would have a global, easy-to-access
// form to view and modify declared variables across the whole program
// (and also, it would be easier to add a new variable to the program)
// Does order of DeclareVariable nodes matter in the flow currently? I don't think so

// TODO: stricter type checking between float and integer?
// TODO: stricter display format for float (.0)
>
<Controls />
<Background variant={BackgroundVariant.Lines} gap={12} size={1} />
Expand Down
19 changes: 14 additions & 5 deletions flowming/src/components/Flow/Nodes/AssignVariable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,26 @@ export class AssignVariableProcessor implements NodeProcessor {
}

const AssignVariable = memo(function AssignVariableComponent({ data, id: _nodeId }: { data: AssignVariableNode; id: string }) {
const { isHovered, isSelected, isHighlighted, isCodeHighlighted, expression, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered, leftSideIndex } = data as AssignVariableNode & { leftSideIndex?: string };
const { isHovered, isSelected, isHighlighted, isCodeHighlighted, expression, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered } = data as AssignVariableNode;

const buildDisplayString = () => {
if (!expression) return null;
const exprInstance = expression instanceof Expression ? expression : Expression.fromObject(expression);

// When the stored expression is effectively empty (no left or right side)
if (exprInstance.isEmpty()) return null;

// For assign variable expressions, leftSide is a Variable
if (exprInstance.leftSide instanceof Variable) {
const leftName = exprInstance.leftSide.name;
const leftDisplay = leftSideIndex ? `${leftName}[${leftSideIndex}]` : leftName;
const leftVar = exprInstance.leftSide;
let leftDisplay = leftVar.name;
if (leftVar.indexExpression && leftVar.indexExpression.length > 0) {
const idxStr = new Expression(undefined, leftVar.indexExpression as any).toString();
leftDisplay = `${leftVar.name}[${idxStr}]`;
}
const right = exprInstance.rightSide.map((e: any) => (e as ExpressionElement).toString()).join(' ');
return `${leftDisplay} = ${right}`;
const str = `${leftDisplay} = ${right}`;
return str.trim() === '' ? null : str;
}
// Fallback
return exprInstance.toString();
Expand Down Expand Up @@ -124,7 +133,7 @@ const AssignVariable = memo(function AssignVariableComponent({ data, id: _nodeId
</div>
)}

{expression ? (
{displayString ? (
<div className="mb-1">
<Badge variant="outline" className="font-mono text-sm w-full justify-center">
{displayString}
Expand Down
93 changes: 82 additions & 11 deletions flowming/src/components/Flow/Nodes/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { Handle, Position, ReactFlowInstance } from '@xyflow/react';
import { memo } from 'react';
import { getNodeStyles } from '../../../utils/nodeStyles';
import { BaseNode, NodeProcessor } from './NodeTypes';
import { IVariable, VariableType, Variable } from '../../../models/Variable';
import { VariableType, Variable } from '../../../models/Variable';
import { IVariable } from '../../../models/IVariable';
import { IValuedVariable, ValuedVariable } from '../../../models/ValuedVariable';
import { Badge } from '@/components/ui/badge';
import BreakpointIndicator from './BreakpointIndicator';
import { Expression } from '../../../models/Expression';
import { ExpressionElement } from '../../../models/ExpressionElement';

interface InputNode extends BaseNode {
variable?: IVariable;
Expand Down Expand Up @@ -39,15 +42,16 @@ export class InputProcessor implements NodeProcessor {
if (data.variable) {
const input = await this.showInputDialog(
`Enter value for ${data.variable.name}`,
data.variable.type as 'string' | 'integer' | 'float' | 'boolean', // TODO: array
(data.variable.type === 'array' ? data.variable.arraySubtype : data.variable.type) as 'string' | 'integer' | 'float' | 'boolean',
`Please enter a ${data.variable.type} value for the variable "${data.variable.name}" (Block ID: ${data.visualId}).`,
`Enter ${data.variable.type} value...`
);

if (input !== null && data.variable) {
// Convert the raw string input to the correct type based on the variable definition
let convertedValue: any = input;
switch (data.variable.type) {
const valueType = data.variable.type === 'array' ? data.variable.arraySubtype : data.variable.type;
switch (valueType) {
case 'integer': {
const parsed = parseInt(input, 10);
if (isNaN(parsed)) {
Expand Down Expand Up @@ -80,14 +84,59 @@ export class InputProcessor implements NodeProcessor {

// Convert IVariable to Variable before passing to fromVariable
const variableObj = Variable.fromObject(data.variable);
const newValuedVariable = ValuedVariable.fromVariable(variableObj, convertedValue);

// Overwrite the existing variable
const existingIndex = currentValuedVariables.findIndex(v => v.id === newValuedVariable.id);
if (existingIndex !== -1) {
currentValuedVariables[existingIndex] = newValuedVariable;

// If variable is an array and has an index expression, handle element assignment
if (variableObj.type === 'array' && variableObj.indexExpression && variableObj.indexExpression.length > 0) {
try {
// Evaluate index expression (must resolve to integer)
const indexExpr = new Expression(undefined, variableObj.indexExpression);
const idx = indexExpr.calculateValue(variableObj.indexExpression, 'integer', currentValuedVariables);

if (typeof idx !== 'number' || !Number.isInteger(idx)) {
throw new Error(`Array index for variable "${variableObj.name}" must evaluate to an integer.`);
}

// Locate existing valued variable or create a default one
let existingVarIdx = currentValuedVariables.findIndex(v => v.id === variableObj.id);
let existingValuedVar: ValuedVariable<VariableType>;
if (existingVarIdx !== -1) {
existingValuedVar = currentValuedVariables[existingVarIdx];
} else {
existingValuedVar = ValuedVariable.fromVariable(variableObj, null);
}

// Ensure array is initialized with correct size
const arraySize = variableObj.arraySize || (Array.isArray(existingValuedVar.value) ? existingValuedVar.value.length : 0);
if (idx < 0 || idx >= arraySize) {
throw new Error(`Array index ${idx} is out of bounds for "${variableObj.name}" (size ${arraySize}).`);
}

// Create updated array value
const updatedArray = Array.isArray(existingValuedVar.value) ? [...existingValuedVar.value] : new Array(arraySize).fill(null);
updatedArray[idx] = convertedValue;

const newValuedVariable = new ValuedVariable(variableObj.id, variableObj.type, variableObj.name, variableObj.nodeId, updatedArray, variableObj.arraySubtype, variableObj.arraySize);

if (existingVarIdx !== -1) {
currentValuedVariables[existingVarIdx] = newValuedVariable;
} else {
currentValuedVariables.push(newValuedVariable);
}
} catch (err) {
console.error(err);
throw err;
}
} else {
currentValuedVariables.push(newValuedVariable);
// Scalar variable assignment
const newValuedVariable = ValuedVariable.fromVariable(variableObj, convertedValue);

// Overwrite the existing variable
const existingIndex = currentValuedVariables.findIndex(v => v.id === newValuedVariable.id);
if (existingIndex !== -1) {
currentValuedVariables[existingIndex] = newValuedVariable;
} else {
currentValuedVariables.push(newValuedVariable);
}
}
}
}
Expand All @@ -99,6 +148,20 @@ export class InputProcessor implements NodeProcessor {
const Input = memo(function InputComponent({ data, id: _nodeId }: { data: InputNode; id: string }) {
const { isHovered, isSelected, isHighlighted, isCodeHighlighted, variable, width, height, visualId, isError, hasBreakpoint, isBreakpointTriggered } = data;

// Helper to stringify index expression for display
const getIndexString = (indexExpr?: any[]): string | null => {
if (!indexExpr || indexExpr.length === 0) return null;
try {
// Ensure all elements are ExpressionElement instances
const elems = (indexExpr as any[]).map((e: any): ExpressionElement =>
e instanceof ExpressionElement ? e : ExpressionElement.fromObject(e)
);
return new Expression(undefined, elems as any).toString();
} catch {
return null;
}
};

return (
<div
className={`input-node`}
Expand Down Expand Up @@ -147,7 +210,15 @@ const Input = memo(function InputComponent({ data, id: _nodeId }: { data: InputN
{variable ? (
<div className="text-center mb-1">
<Badge variant="outline" className="font-mono text-sm">
{variable.name} = {variable.type}(👤)
{variable.name}
{variable.type === 'array' && (
<>
[
{getIndexString(variable.indexExpression) || ''}
]
</>
)}
{' '}= {(variable.type === 'array' ? variable.arraySubtype : variable.type)}(👤)
</Badge>
</div>
) : (
Expand Down
16 changes: 13 additions & 3 deletions flowming/src/components/ImportExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useVariables } from '../context/VariablesContext';
import { useFilename } from '../context/FilenameContext';
import { useFlowExecutorActions, useFlowExecutorState } from '../context/FlowExecutorContext';
import { SelectedNodeContext } from '../context/SelectedNodeContext';
import { Variable } from '../models';
import { Variable, ExpressionElement } from '../models';
import { Button } from './ui/button';
import { Download, Upload, FileX } from 'lucide-react';
import { useDebugger } from '../context/DebuggerContext';
Expand Down Expand Up @@ -117,6 +117,9 @@ const ImportExport: React.FC = () => {
// Stop execution before importing
stop();

// Reset selected node so DetailsTab shows System Settings during import
setSelectedNode(null);

const input = document.createElement('input');
input.type = 'file';
input.accept = '.flowming';
Expand Down Expand Up @@ -172,7 +175,14 @@ const ImportExport: React.FC = () => {
});
}

setNodes(flowData.nodes || []);
// Restore nodes and convert any indexExpression objects back to ExpressionElement instances
const restoredNodes = (flowData.nodes || []).map((n: any) => {
if (n.type === 'Input' && n.data?.variable?.indexExpression) {
n.data.variable.indexExpression = n.data.variable.indexExpression.map((elem: any) => ExpressionElement.fromObject(elem));
}
return n;
});
setNodes(restoredNodes);
setEdges(flowData.edges || []);

// Fit and center the view for the imported diagram
Expand All @@ -189,7 +199,7 @@ const ImportExport: React.FC = () => {
};

input.click();
}, [setNodes, setEdges, getNodes, updateNodeVariables, deleteNodeVariables, fitView, setFilename, stop]);
}, [setNodes, setEdges, getNodes, updateNodeVariables, deleteNodeVariables, fitView, setFilename, stop, setSelectedNode]);

return (
<div className="flex items-center gap-2">
Expand Down
20 changes: 18 additions & 2 deletions flowming/src/components/Panel/Tabs/DebuggerTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ const DebuggerTab = () => {
}
};

/**
* Formats a variable value for display.
* Arrays are shown with brackets and comma-separated elements (e.g., [1, 2, 3]).
* Non-arrays are stringified as-is.
*/
const formatValue = (value: any): string => {
if (Array.isArray(value)) {
return `[${value.map(v => formatValue(v)).join(', ')}]`;
}
// Properly format strings with quotes
if (typeof value === 'string') {
return JSON.stringify(value);
}
return String(value);
};

return (
<div className="h-full flex flex-col">
{/* Status header */}
Expand Down Expand Up @@ -111,7 +127,7 @@ const DebuggerTab = () => {
{variable.name}:
</span>
<span className="font-mono text-sm bg-muted px-2 py-1 rounded">
{variable.value}
{formatValue(variable.value)}
</span>
</div>
</div>
Expand Down Expand Up @@ -144,7 +160,7 @@ const DebuggerTab = () => {
</span>
<span className="font-mono text-sm bg-background px-2 py-1 rounded">
{change.value}
{formatValue(change.value)}
</span>
</div>
<span className="text-xs text-muted-foreground">
Expand Down
Loading
Loading