// 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 Connection} from "@xyflow/react"; import {useEffect} from "react"; import useFlowStore from "./VisProgStores.tsx"; export type ConnectionContext = { connectionCount: number; source: { id: string; handleId: string; } target: { id: string; handleId: string; } } export type HandleRule = ( connection: Connection, context: ConnectionContext ) => RuleResult; /** * A RuleResult describes the outcome of validating a HandleRule * * if a rule is not satisfied, the RuleResult includes a message that is used inside a tooltip * that tells the user why their attempted connection is not possible */ export type RuleResult = | { isSatisfied: true } | { isSatisfied: false, message: string }; /** * default RuleResults, can be used to create more readable handleRule definitions */ export const ruleResult = { satisfied: { isSatisfied: true } as RuleResult, unknownError: {isSatisfied: false, message: "Unknown Error" } as RuleResult, notSatisfied: (message: string) : RuleResult => { return {isSatisfied: false, message: message } } } const evaluateRules = ( rules: HandleRule[], connection: Connection, context: ConnectionContext ) : RuleResult => { // evaluate the rules and check if there is at least one unsatisfied rule const failedRule = rules .map(rule => rule(connection, context)) .find(result => !result.isSatisfied); return failedRule ? ruleResult.notSatisfied(failedRule.message) : ruleResult.satisfied; } /** * !DOCUMENTATION NOT FINISHED! * * - The output is a single RuleResult, meaning we only show one error message. * Error messages are prioritised by listOrder; Thus, if multiple HandleRules evaluate to false, * we only send the error message of the first failed rule in the target's registered list of rules. * * @param {string} nodeId * @param {string} handleId * @param type * @param {HandleRule[]} rules * @returns {(c: Connection) => RuleResult} a function that validates an attempted connection */ export function useHandleRules( nodeId: string, handleId: string, type: "source" | "target", rules: HandleRule[], ) : (c: Connection) => RuleResult { const edges = useFlowStore.getState().edges; const registerRules = useFlowStore((state) => state.registerRules); useEffect(() => { registerRules(nodeId, handleId, rules); // the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement, // however this would result in an infinite loop because it would change one of its own dependencies // so we only use those dependencies that we don't change ourselves // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleId, nodeId, registerRules]); return (connection: Connection) => { // inside this function we consider the target to be the target of the isValidConnection event // and not the target in the actual connection const { target, targetHandle } = type === "source" ? connection : { target: connection.source, targetHandle: connection.sourceHandle }; if (!targetHandle) {throw new Error("No target handle was provided");} const targetConnections = edges.filter(edge => edge.target === target && edge.targetHandle === targetHandle); // we construct the connectionContext const context: ConnectionContext = { connectionCount: targetConnections.length, source: {id: nodeId, handleId: handleId}, target: {id: target, handleId: targetHandle}, }; const targetRules = useFlowStore.getState().getTargetRules(target, targetHandle); // finally we return a function that evaluates all rules using the created context return evaluateRules(targetRules, connection, context); }; } export function validateConnectionWithRules( connection: Connection, context: ConnectionContext ): RuleResult { const rules = useFlowStore.getState().getTargetRules( connection.target!, connection.targetHandle! ); return evaluateRules(rules,connection, context); }