Fix Jinja2 template errors from invalid parameter/block names with special characters (SKY-7356) (#4644)
This commit is contained in:
@@ -104,6 +104,42 @@ function header(
|
||||
return `${prefix} Context Parameter`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a parameter key is a valid Python/Jinja2 identifier.
|
||||
* Parameter keys are used in Jinja2 templates, so they must be valid identifiers.
|
||||
* Returns an error message if invalid, or null if valid.
|
||||
*/
|
||||
function validateParameterKey(key: string): string | null {
|
||||
if (!key) return null; // Empty key is handled separately
|
||||
|
||||
// Check for whitespace
|
||||
if (/\s/.test(key)) {
|
||||
return "Key cannot contain whitespace characters. Consider using underscores (_) instead.";
|
||||
}
|
||||
|
||||
// Check if it's a valid Python identifier:
|
||||
// - Must start with a letter (a-z, A-Z) or underscore (_)
|
||||
// - Can only contain letters, digits (0-9), and underscores
|
||||
const validIdentifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
if (!validIdentifierRegex.test(key)) {
|
||||
if (/^[0-9]/.test(key)) {
|
||||
return "Key cannot start with a digit. Parameter keys must start with a letter or underscore.";
|
||||
}
|
||||
if (key.includes("/")) {
|
||||
return "Key cannot contain '/' characters. Use underscores instead (e.g., 'State_or_Province' instead of 'State/Province').";
|
||||
}
|
||||
if (key.includes("-")) {
|
||||
return "Key cannot contain '-' characters. Use underscores instead (e.g., 'my_parameter' instead of 'my-parameter').";
|
||||
}
|
||||
if (key.includes(".")) {
|
||||
return "Key cannot contain '.' characters. Use underscores instead.";
|
||||
}
|
||||
return "Key must be a valid identifier (only letters, digits, and underscores; cannot start with a digit).";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to detect initial credential data type from existing parameter
|
||||
function detectInitialCredentialDataType(
|
||||
initialValues: ParametersState[number] | undefined,
|
||||
@@ -149,7 +185,7 @@ function WorkflowParameterEditPanel({
|
||||
const isCloud = useContext(CloudContext);
|
||||
const isEditMode = !!initialValues;
|
||||
const [key, setKey] = useState(initialValues?.key ?? "");
|
||||
const hasWhitespace = /\s/.test(key);
|
||||
const keyValidationError = validateParameterKey(key);
|
||||
|
||||
// Detect initial values for backward compatibility
|
||||
const isBitwardenCredential =
|
||||
@@ -314,10 +350,8 @@ function WorkflowParameterEditPanel({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Key</Label>
|
||||
<Input value={key} onChange={(e) => setKey(e.target.value)} />
|
||||
{hasWhitespace && (
|
||||
<p className="text-xs text-destructive">
|
||||
Spaces are not allowed, consider using _
|
||||
</p>
|
||||
{keyValidationError && (
|
||||
<p className="text-xs text-destructive">{keyValidationError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
@@ -769,12 +803,11 @@ function WorkflowParameterEditPanel({
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (hasWhitespace) {
|
||||
if (keyValidationError) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to save parameter",
|
||||
description:
|
||||
"Key cannot contain whitespace characters. Consider using underscores (_) instead.",
|
||||
description: keyValidationError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,40 @@ import {
|
||||
} from "../editor/workflowEditorUtils";
|
||||
import { useState } from "react";
|
||||
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
|
||||
/**
|
||||
* Sanitizes a block label to be a valid Python/Jinja2 identifier.
|
||||
* Block labels are used to create output parameter keys (e.g., '{label}_output')
|
||||
* which are then used as Jinja2 template variable names.
|
||||
*/
|
||||
function sanitizeBlockLabel(value: string): {
|
||||
sanitized: string;
|
||||
wasModified: boolean;
|
||||
} {
|
||||
const original = value;
|
||||
|
||||
// Replace any character that's not a letter, digit, or underscore with underscore
|
||||
let sanitized = value.replace(/[^a-zA-Z0-9_]/g, "_");
|
||||
|
||||
// Collapse multiple consecutive underscores into one
|
||||
sanitized = sanitized.replace(/_+/g, "_");
|
||||
|
||||
// Remove leading/trailing underscores for cleaner labels
|
||||
sanitized = sanitized.replace(/^_+|_+$/g, "");
|
||||
|
||||
// If starts with a digit (after cleanup), prepend an underscore
|
||||
if (/^[0-9]/.test(sanitized)) {
|
||||
sanitized = "_" + sanitized;
|
||||
}
|
||||
|
||||
// If everything was stripped, provide a default
|
||||
if (!sanitized) {
|
||||
sanitized = "block";
|
||||
}
|
||||
|
||||
return { sanitized, wasModified: original !== sanitized };
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
@@ -24,13 +58,22 @@ function useNodeLabelChangeHandler({ id, initialValue }: Props) {
|
||||
|
||||
function handleLabelChange(value: string) {
|
||||
const existingLabels = nodes
|
||||
.filter(isWorkflowBlockNode)
|
||||
.filter((n) => isWorkflowBlockNode(n) && n.id !== id)
|
||||
.map((n) => n.data.label);
|
||||
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
|
||||
const newLabel = getUniqueLabelForExistingNode(
|
||||
labelWithoutWhitespace,
|
||||
existingLabels,
|
||||
);
|
||||
|
||||
// Sanitize the label to be a valid Python identifier
|
||||
const { sanitized, wasModified } = sanitizeBlockLabel(value);
|
||||
|
||||
// Show a toast if characters were modified
|
||||
if (wasModified) {
|
||||
toast({
|
||||
title: "Block label adjusted",
|
||||
description:
|
||||
"Block labels can only contain letters, numbers, and underscores. Invalid characters have been replaced.",
|
||||
});
|
||||
}
|
||||
|
||||
const newLabel = getUniqueLabelForExistingNode(sanitized, existingLabels);
|
||||
setLabel(newLabel);
|
||||
setNodes(
|
||||
getUpdatedNodesAfterLabelUpdateForParameterKeys(
|
||||
|
||||
Reference in New Issue
Block a user