125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
// 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);
|
|
} |