// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { type NodeProps, Position, type Node } from '@xyflow/react'; import {useEffect} from "react"; import type {EditorWarning} from "../components/EditorWarnings.tsx"; import { Toolbar } from '../components/NodeComponents'; import styles from '../../VisProg.module.css'; import { TextField } from '../../../../components/TextField'; import {MultiConnectionHandle} from "../components/RuleBasedHandle.tsx"; import {allowOnlyConnectionsFromHandle, allowOnlyConnectionsFromType} from "../HandleRules.ts"; import useFlowStore from '../VisProgStores'; import {DoesPlanIterate, HasCheckingSubGoal, PlanReduce, type Plan } from '../components/Plan'; import PlanEditorDialog from '../components/PlanEditor'; import { MultilineTextField } from '../../../../components/MultilineTextField'; import { defaultPlan } from '../components/Plan.default.ts'; import { deleteGoalInPlanByID, insertGoalInPlan } from '../components/PlanEditingFunctions.tsx'; /** * The default data dot a phase node * @param label: the label of this phase * @param droppable: whether this node is droppable from the drop bar (initialized as true) * @param desciption: description of the goal - this will be checked for completion * @param hasReduce: whether this node has reducing functionality (true by default) * @param can_fail: whether this plan should be checked- this plan could possible fail * @param plan: The (possible) attached plan to this goal */ export type GoalNodeData = { label: string; name: string; description: string; droppable: boolean; achieved: boolean; hasReduce: boolean; can_fail: boolean; plan?: Plan; }; export type GoalNode = Node /** * Defines how a Goal node should be rendered * @param props NodeProps, like id, label, children * @returns React.JSX.Element */ export default function GoalNode({id, data}: NodeProps) { const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore(); const _nodes = useFlowStore().nodes; const text_input_id = `goal_${id}_text_input`; const checkbox_id = `goal_${id}_checkbox`; const planIterate = DoesPlanIterate(_nodes, data.plan); const hasCheckSubGoal = data.plan !== undefined && HasCheckingSubGoal(data.plan, _nodes) const setDescription = (value: string) => { updateNodeData(id, {...data, description: value}); } const setName= (value: string) => { updateNodeData(id, {...data, name: value}) } const setFailable = (value: boolean) => { updateNodeData(id, {...data, can_fail: value}); } //undefined plan warning useEffect(() => { const noPlanWarning : EditorWarning = { scope: { id: id, handleId: undefined }, type: 'PLAN_IS_UNDEFINED', severity: 'ERROR', description: "This goalNode is missing a plan, please make sure to create a plan by using the create plan button" }; if (!data.plan || data.plan.steps?.length === 0){ registerWarning(noPlanWarning); return; } unregisterWarning(id, noPlanWarning.type); },[data.plan, id, registerWarning, unregisterWarning]) //starts with number warning useEffect(() => { const name = data.name || ""; const startsWithNumberWarning: EditorWarning = { scope: { id: id }, type: 'ELEMENT_STARTS_WITH_NUMBER', severity: 'ERROR', description: "Norms are not allowed to start with a number." }; if (/^\d/.test(name)) { registerWarning(startsWithNumberWarning); } else { unregisterWarning(id, 'ELEMENT_STARTS_WITH_NUMBER'); } }, [data.name, id, registerWarning, unregisterWarning]); return <>
setName(val)} placeholder={"To ..."} />
{(data.can_fail || hasCheckSubGoal) && (
)}
{data.plan && (
{planIterate ? "" : } planIterate ? setFailable(e.target.checked) : setFailable(false)} />
)}
{ updateNodeData(id, { ...data, plan, }); }} description={data.name} />
; } /** * Reduces each Goal, including its children down into its relevant data. * @param node The Node Properties of this node. * @param _nodes all the nodes in the graph */ export function GoalReduce(node: Node, _nodes: Node[]) { const data = node.data as GoalNodeData; return { id: node.id, name: data.name, description: data.description, can_fail: data.can_fail || (data.plan && HasCheckingSubGoal(data.plan, _nodes)), plan: data.plan ? PlanReduce(_nodes, data.plan) : "", } } export const GoalTooltip = ` The goal node allows you to set goals that Pepper has to achieve before moving to the next phase of your program`; /** * This function is called whenever a connection is made with this node type as the target * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the received connection */ export function GoalConnectionTarget(_thisNode: Node, _sourceNodeId: string) { // Goals should only be targeted by other goals, for them to be part of our plan. const nodes = useFlowStore.getState().nodes; const otherNode = nodes.find((x) => x.id === _sourceNodeId) if (!otherNode || otherNode.type !== "goal") return; const data = _thisNode.data as GoalNodeData // First, let's see if we have a plan currently. If not, let's create a default plan with this goal inside.:) if (!data.plan) { data.plan = insertGoalInPlan({...structuredClone(defaultPlan), id: crypto.randomUUID()} as Plan, otherNode as GoalNode) } // Else, lets just insert this goal into our current plan. else { data.plan = insertGoalInPlan(structuredClone(data.plan), otherNode as GoalNode) } } /** * This function is called whenever a connection is made with this node type as the source * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the created connection */ export function GoalConnectionSource(_thisNode: Node, _targetNodeId: string) { // no additional connection logic exists yet } /** * This function is called whenever a connection is disconnected with this node type as the target * @param _thisNode the node of this node type which function is called * @param _sourceNodeId the source of the disconnected connection */ export function GoalDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) { // We should probably check if our disconnection was by a goal, since it would mean we have to remove it from our plan list. const data = _thisNode.data as GoalNodeData data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId) } /** * This function is called whenever a connection is disconnected with this node type as the source * @param _thisNode the node of this node type which function is called * @param _targetNodeId the target of the diconnected connection */ export function GoalDisconnectionSource(_thisNode: Node, _targetNodeId: string) { // no additional connection logic exists yet }