Fix Jinja2 template errors from invalid parameter/block names with special characters (SKY-7356) (#4644)

This commit is contained in:
Celal Zamanoglu
2026-02-06 00:58:36 +03:00
committed by GitHub
parent c35a744e27
commit 7bf1c721e4
8 changed files with 514 additions and 40 deletions

View File

@@ -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;
}

View File

@@ -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(