Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';

import { useUniqueId } from '../../../hooks/useUniqueId';
import {
decodeSafeSelectorValue,
exampleOptions,
getPlaceholderForParameter,
getPlaceholderForSelectedParameter,
Expand Down Expand Up @@ -37,6 +38,15 @@ export const ParameterEditor: React.FC<ParameterProps> = ({
const selectedExample = examples?.find(e => e.value === value) ?? selectExampleOption;
const parameterDisplayName = `${parameter.name}${parameter.required ? '*' : ''}`;

// Find the encoded value that matches the current (decoded) value
const encodedValue = React.useMemo(() => {
if (!value || !parameterValueOptions) return value || '';
const matchingOption = parameterValueOptions.find(opt => {
return String(decodeSafeSelectorValue(opt.value as string | number)) === value;
});
return matchingOption ? String(matchingOption.value) : value;
}, [value, parameterValueOptions]);

const requiredButEmpty = validate && parameter.required && !value;

return (
Expand All @@ -51,8 +61,8 @@ export const ParameterEditor: React.FC<ParameterProps> = ({
flex={1}
aria-label={parameter.name}
options={parameterValueOptions}
value={value || ''}
onChange={onChange}
value={encodedValue}
onChange={val => onChange && onChange(String(decodeSafeSelectorValue(val as string | number)))}
placeholder={getPlaceholderForSelectedParameter(parameter)}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,66 @@ const booleanOptions = [
{ label: 'True', value: 'true' },
];

/**
* Encodes a value to be safe for use in CSS selectors (data-key attributes).
* Special characters like quotes, brackets, etc. can break querySelector,
* so we encode them using base64.
*/
export function encodeSafeSelectorValue(value: string | number): string | number {
// Numbers are safe to use as-is
if (typeof value === 'number') {
return value;
}
// Check if the value contains characters that would break CSS selectors
// This includes quotes, brackets, backslashes, etc.
const hasSpecialChars = /["'\[\]\\(){}]/.test(value);
if (!hasSpecialChars) {
return value;
}
// Encode to base64 to make it safe for CSS selectors
// We prefix with 'b64:' so we can decode it later if needed
try {
return 'b64:' + btoa(value);
} catch (e) {
// If btoa fails (e.g., with unicode), fallback to encodeURIComponent
return 'enc:' + encodeURIComponent(value);
}
}

/**
* Decodes a value that was encoded by encodeSafeSelectorValue
*/
export function decodeSafeSelectorValue(value: string | number): string | number {
if (typeof value === 'number') {
return value;
}

if (value.startsWith('b64:')) {
try {
return atob(value.substring(4));
} catch (e) {
return value;
}
}
if (value.startsWith('enc:')) {
try {
return decodeURIComponent(value.substring(4));
} catch (e) {
return value;
}
}
return value;
}

function enumOptions(enumValues: JSONSchema7Type[], required?: boolean) {
const options = map(enumValues, v => ({ value: typeof v === 'number' ? v : String(v) }));
const options = map(enumValues, v => {
// Handle objects and arrays by stringifying them
const stringValue =
typeof v === 'object' && v !== null ? safeStringify(v) ?? String(v) : typeof v === 'number' ? v : String(v);
// Encode the value to be safe for CSS selectors, but keep the original label
const safeValue = encodeSafeSelectorValue(stringValue);
return { value: safeValue, label: String(stringValue) };
});
return required ? options : [{ label: 'Not Set', value: '' }, ...options];
}

Expand Down Expand Up @@ -48,18 +106,17 @@ export function parameterSupportsFileUpload(parameter?: Pick<ParameterSpec, 'sch
}

function stringifyValue(value: unknown) {
return typeof value === 'object' ? JSON.stringify(value) : escapeQuotes(String(value));
if (typeof value === 'object' && value !== null) {
return safeStringify(value) ?? String(value);
}
return String(value);
}

function exampleValue(example: Omit<INodeExample, 'id'> | Omit<INodeExternalExample, 'id'>) {
const value = 'value' in example ? example.value : example.externalValue;
return stringifyValue(value);
}

function escapeQuotes(value: string) {
return value.replace(/"/g, '\\"');
}

export function getPlaceholderForParameter(parameter: ParameterSpec) {
const { value: parameterValue, isDefault } = getValueForParameter(parameter);

Expand Down Expand Up @@ -89,16 +146,18 @@ const getValueForParameter = (parameter: ParameterSpec) => {
return { value: stringifyValue(defaultValue), isDefault: true };
}

const examples = parameter.examples ?? [];
if (examples.length > 0) {
return { value: exampleValue(examples[0]) };
}

// If the parameter has enums, prioritize using the first enum value
// over examples, as examples might not match the enum values
const enums = parameter.schema?.enum ?? [];
if (enums.length > 0) {
return { value: stringifyValue(enums[0]) };
}

const examples = parameter.examples ?? [];
if (examples.length > 0) {
return { value: exampleValue(examples[0]) };
}

return { value: '' };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';

import { useUniqueId } from '../../../hooks/useUniqueId';
import { ServerVariable } from '../../../utils/http-spec/IServer';
import { decodeSafeSelectorValue, encodeSafeSelectorValue } from '../Parameters/parameter-utils';

interface VariableProps {
variable: ServerVariable;
Expand All @@ -14,6 +15,18 @@ interface VariableProps {
export const VariableEditor: React.FC<VariableProps> = ({ variable, value, onChange }) => {
const inputId = useUniqueId(`id_${variable.name}_`);

// Find the encoded value that matches the current (decoded) value
const encodedOptions = React.useMemo(
() => (variable.enum ? variable.enum.map(s => ({ value: encodeSafeSelectorValue(s), label: String(s) })) : []),
[variable.enum],
);

const encodedValue = React.useMemo(() => {
if (!value || !variable.enum) return value || variable.default;
const matchingOption = encodedOptions.find(opt => decodeSafeSelectorValue(String(opt.value)) === value);
return matchingOption ? String(matchingOption.value) : value;
}, [value, variable.enum, variable.default, encodedOptions]);

return (
<>
<Text as="label" aria-hidden="true" data-testid="param-label" htmlFor={inputId} fontSize="base">
Expand All @@ -25,9 +38,9 @@ export const VariableEditor: React.FC<VariableProps> = ({ variable, value, onCha
<Select
flex={1}
aria-label={variable.name}
options={variable.enum.map(s => ({ value: s }))}
value={value || variable.default}
onChange={onChange}
options={encodedOptions}
value={encodedValue}
onChange={val => onChange && onChange(decodeSafeSelectorValue(String(val)))}
/>
) : (
<Flex flex={1}>
Expand Down
2 changes: 1 addition & 1 deletion packages/elements-dev-portal/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// auto-updated during build
export const appVersion = '3.0.10';
export const appVersion = '3.0.13';