import _ from "underscore"; import { isDefined, isValidNumber } from "@/utils/validation"; /** Visits tool inputs. * @param{dict} inputs - Nested dictionary of input elements * @param{dict} callback - Called with the mapped dictionary object and corresponding model node */ export function visitInputs(inputs, callback, prefix = "", context = undefined) { context = Object.assign({}, context); _.each(inputs, (input) => { if (input && input.type && input.name) { context[input.name] = input; } }); for (var key in inputs) { var node = inputs[key]; node.name = node.name || key; var name = prefix ? `${prefix}|${node.name}` : node.name; switch (node.type) { case "repeat": _.each(node.cache, (cache, j) => { visitInputs(cache, callback, `${name}_${j}`, context); }); break; case "conditional": if (node.test_param) { callback(node.test_param, `${name}|${node.test_param.name}`, context); var selectedCase = matchCase(node, node.test_param.value); if (selectedCase != -1) { visitInputs(node.cases[selectedCase].inputs, callback, name, context); } else { console.debug(`Form.utilities::visitInputs() - Invalid case for ${name}.`); } } else { console.debug(`Form.utilities::visitInputs() - Conditional test parameter missing for ${name}.`); } break; case "section": visitInputs(node.inputs, callback, name, context); break; default: callback(node, name, context); } } } /** Visits ALL inputs including every conditional case (not just the active one). * Used for syncing server attributes to all conditional branches. * @param{Array} inputs - Nested array of input elements * @param{Function} callback - Called with each input node and its name * @param{String} prefix - Key prefix for nested param name construction */ export function visitAllInputs(inputs, callback, prefix = "") { for (var key in inputs) { var node = inputs[key]; var nodeName = node.name || key; var name = prefix ? `${prefix}|${nodeName}` : nodeName; switch (node.type) { case "repeat": _.each(node.cache, (cache, j) => { visitAllInputs(cache, callback, `${name}_${j}`); }); break; case "conditional": if (node.test_param) { callback(node.test_param, `${name}|${node.test_param.name}`); for (var i = 0; i < node.cases.length; i++) { visitAllInputs(node.cases[i].inputs, callback, name); } } break; case "section": visitAllInputs(node.inputs, callback, name); break; default: callback(node, name); } } } /** Matches conditional values to selected cases. * @param{dict} input - Definition of conditional input parameter * @param{dict} value - Current value */ export function matchCase(input, value) { if (input.test_param.type == "boolean") { if (["true", true].includes(value)) { if (input.test_param.truevalue !== undefined) { value = input.test_param.truevalue; } else { value = "true"; } } else { if (input.test_param.falsevalue !== undefined) { value = input.test_param.falsevalue; } else { value = "false"; } } } for (let i = 0; i < input.cases.length; i++) { if (input.cases[i].value == value) { return i; } } return -1; } /** Match server validation response to highlight inputs * @param{dict} index - Index of input elements * @param{dict} response - Nested dictionary with error/warning messages */ export function matchInputs(index, response) { var result = {}; function search(id, head) { if (typeof head === "string") { if (index[id]) { result[id] = head; } } else { for (var i in head) { var new_id = i; if (id !== "") { var separator = "|"; if (head instanceof Array) { separator = "_"; } new_id = id + separator + new_id; } search(new_id, head[i]); } } } search("", response); return result; } /** Validate value against a regular expression pattern * @param{object} validator - Validator definition * @param{*} value - Value to validate * @returns{object} Object with isValid boolean and message string */ function validateRegex(validator, value) { try { const regex = new RegExp(validator.expression); const matches = regex.test(String(value)); const isValid = validator.negate ? !matches : matches; return { isValid: isValid, message: isValid ? null : validator.message, }; } catch (error) { return { isValid: false, message: `Invalid validation pattern: ${error.message}`, }; } } /** Validate value length is within specified bounds * @param{object} validator - Validator definition * @param{*} value - Value to validate * @returns{object} Object with isValid boolean and message string */ function validateLength(validator, value) { const valueLength = String(value).length; let isValid = true; if (isValidNumber(validator.min) && valueLength < validator.min) { isValid = false; } if (isValidNumber(validator.max) && valueLength > validator.max) { isValid = false; } if (validator.negate) { isValid = !isValid; } return { isValid: isValid, message: isValid ? null : validator.message, }; } /** Validate numeric value is within specified range * @param{object} validator - Validator definition * @param{*} value - Value to validate * @returns{object} Object with isValid boolean and message string */ function validateInRange(validator, value) { const numericValue = Number(value); if (isNaN(numericValue)) { return { isValid: false, message: "Value must be numeric for range validation", }; } let isValid = true; if (isValidNumber(validator.min) && numericValue < validator.min) { isValid = false; } if (isValidNumber(validator.max) && numericValue > validator.max) { isValid = false; } if (validator.negate) { isValid = !isValid; } return { isValid: isValid, message: isValid ? null : validator.message, }; } // Map validator types to their validation functions const validatorFunctions = { regex: validateRegex, length: validateLength, in_range: validateInRange, }; /** Run a single validator * @param{object} validator - Validator definition with type, expression, message, etc. * @param{*} value - Value to validate * @returns{object} Object with isValid boolean and message string */ function runValidator(validator, value) { const validatorFunc = validatorFunctions[validator.type]; if (validatorFunc) { return validatorFunc(validator, value); } // Unknown validator type - consider valid by default return { isValid: true, message: null }; } /** Validates input parameters to identify issues before submitting a server request, where comprehensive validation is performed. * @param{dict} index - Index of input elements * @param{dict} values - Dictionary of parameter values */ export function validateInputs(index, values, rejectEmptyRequiredInputs = false) { let batchN = -1; let batchSrc = null; for (const inputId in index) { const inputDef = index[inputId]; const inputValue = values[inputId]; const isEmpty = !isDefined(inputValue) || inputValue === ""; const hasValue = !isEmpty; const isRequired = !inputDef.optional; if (!inputDef || inputDef.step_linked) { continue; } if (isRequired && inputDef.type != "hidden") { if (!isDefined(inputValue) || (rejectEmptyRequiredInputs && inputValue === "")) { return [inputId, "Please provide a value for this option."]; } } if (inputDef.wp_linked && inputDef.text_value == inputValue) { return [inputId, "Please provide a value for this workflow parameter."]; } if (inputValue && Array.isArray(inputValue.values) && inputValue.values.length == 0 && isRequired) { return [inputId, "Please provide data for this input."]; } if (inputValue) { if (inputValue.rules && inputValue.rules.length == 0) { return [inputId, "No rules defined, define at least one rule."]; } if (inputValue.mapping && inputValue.mapping.length == 0) { return [inputId, "No collection identifiers defined, specify at least one collection identifier."]; } if (inputValue.rules && inputValue.rules.length > 0) { for (const rule of inputValue.rules) { if (rule.error) { return [inputId, "Error detected in one or more rules."]; } } } } if (inputValue && inputValue.batch) { const n = inputValue.values.length; const src = n > 0 && inputValue.values[0] && inputValue.values[0].src; if (src) { if (batchSrc === null) { batchSrc = src; } else if (batchSrc !== src) { return [inputId, "Please select either dataset or dataset list fields for all batch mode fields."]; } } if (batchN === -1) { batchN = n; } else if (batchN !== n) { return [ inputId, `Please make sure that you select the same number of inputs for all batch mode fields. This field contains ${n} selection(s) while a previous field contains ${batchN}.`, ]; } } // Run custom validators if field is required or has a value if (inputDef.validators && (isRequired || hasValue)) { for (const validator of inputDef.validators) { const validationResult = runValidator(validator, inputValue); if (!validationResult.isValid) { return [inputId, validationResult.message]; } } } } return null; }