Skip to content

Commit 5f922bf

Browse files
fix : doc view crashing when selecting enum value (#2859)
* fix : enum selector for try block * fix : special char enum values * fix : removed redundunt code * upgraded new version * fix : test cases failure
1 parent b8ec023 commit 5f922bf

File tree

7 files changed

+122
-26
lines changed

7 files changed

+122
-26
lines changed

packages/elements-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoplight/elements-core",
3-
"version": "9.0.14",
3+
"version": "9.0.15",
44
"sideEffects": [
55
"web-components.min.js",
66
"src/web-components/**",

packages/elements-core/src/components/TryIt/Parameters/ParameterEditor.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33

44
import { useUniqueId } from '../../../hooks/useUniqueId';
55
import {
6+
decodeSafeSelectorValue,
67
exampleOptions,
78
getPlaceholderForParameter,
89
getPlaceholderForSelectedParameter,
@@ -34,9 +35,25 @@ export const ParameterEditor: React.FC<ParameterProps> = ({
3435
const inputCheckId = useUniqueId(`id_${parameter.name}_checked`);
3536
const parameterValueOptions = parameterOptions(parameter);
3637
const examples = exampleOptions(parameter);
37-
const selectedExample = examples?.find(e => e.value === value) ?? selectExampleOption;
38+
// Find the encoded example value that matches the current (decoded) value
39+
const selectedExample = React.useMemo(() => {
40+
if (!examples) return selectExampleOption;
41+
const matchingExample = examples.find(e => {
42+
return String(decodeSafeSelectorValue(e.value as string | number)) === value;
43+
});
44+
return matchingExample ?? selectExampleOption;
45+
}, [examples, value]);
3846
const parameterDisplayName = `${parameter.name}${parameter.required ? '*' : ''}`;
3947

48+
// Find the encoded value that matches the current (decoded) value
49+
const encodedValue = React.useMemo(() => {
50+
if (!value || !parameterValueOptions) return value || '';
51+
const matchingOption = parameterValueOptions.find(opt => {
52+
return String(decodeSafeSelectorValue(opt.value as string | number)) === value;
53+
});
54+
return matchingOption ? String(matchingOption.value) : value;
55+
}, [value, parameterValueOptions]);
56+
4057
const requiredButEmpty = validate && parameter.required && !value;
4158

4259
return (
@@ -51,8 +68,8 @@ export const ParameterEditor: React.FC<ParameterProps> = ({
5168
flex={1}
5269
aria-label={parameter.name}
5370
options={parameterValueOptions}
54-
value={value || ''}
55-
onChange={onChange}
71+
value={encodedValue}
72+
onChange={val => onChange && onChange(String(decodeSafeSelectorValue(val as string | number)))}
5673
placeholder={getPlaceholderForSelectedParameter(parameter)}
5774
/>
5875
) : (
@@ -75,7 +92,7 @@ export const ParameterEditor: React.FC<ParameterProps> = ({
7592
flex={1}
7693
value={selectedExample.value}
7794
options={examples}
78-
onChange={onChange}
95+
onChange={val => onChange && onChange(String(decodeSafeSelectorValue(val as string | number)))}
7996
/>
8097
)}
8198
</Flex>

packages/elements-core/src/components/TryIt/Parameters/parameter-utils.ts

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,66 @@ const booleanOptions = [
1313
{ label: 'True', value: 'true' },
1414
];
1515

16+
/**
17+
* Encodes a value to be safe for use in CSS selectors (data-key attributes).
18+
* Special characters like quotes, brackets, etc. can break querySelector,
19+
* so we encode them using base64.
20+
*/
21+
export function encodeSafeSelectorValue(value: string | number): string | number {
22+
// Numbers are safe to use as-is
23+
if (typeof value === 'number') {
24+
return value;
25+
}
26+
// Check if the value contains characters that would break CSS selectors
27+
// This includes quotes, brackets, backslashes, etc.
28+
const hasSpecialChars = /["'\[\]\\(){}]/.test(value);
29+
if (!hasSpecialChars) {
30+
return value;
31+
}
32+
// Encode to base64 to make it safe for CSS selectors
33+
// We prefix with 'b64:' so we can decode it later if needed
34+
try {
35+
return 'b64:' + btoa(value);
36+
} catch (e) {
37+
// If btoa fails (e.g., with unicode), fallback to encodeURIComponent
38+
return 'enc:' + encodeURIComponent(value);
39+
}
40+
}
41+
42+
/**
43+
* Decodes a value that was encoded by encodeSafeSelectorValue
44+
*/
45+
export function decodeSafeSelectorValue(value: string | number): string | number {
46+
if (typeof value === 'number') {
47+
return value;
48+
}
49+
50+
if (value.startsWith('b64:')) {
51+
try {
52+
return atob(value.substring(4));
53+
} catch (e) {
54+
return value;
55+
}
56+
}
57+
if (value.startsWith('enc:')) {
58+
try {
59+
return decodeURIComponent(value.substring(4));
60+
} catch (e) {
61+
return value;
62+
}
63+
}
64+
return value;
65+
}
66+
1667
function enumOptions(enumValues: JSONSchema7Type[], required?: boolean) {
17-
const options = map(enumValues, v => ({ value: typeof v === 'number' ? v : String(v) }));
68+
const options = map(enumValues, v => {
69+
// Handle objects and arrays by stringifying them
70+
const stringValue =
71+
typeof v === 'object' && v !== null ? safeStringify(v) ?? String(v) : typeof v === 'number' ? v : String(v);
72+
// Encode the value to be safe for CSS selectors, but keep the original label
73+
const safeValue = encodeSafeSelectorValue(stringValue);
74+
return { value: safeValue, label: String(stringValue) };
75+
});
1876
return required ? options : [{ label: 'Not Set', value: '' }, ...options];
1977
}
2078

@@ -32,7 +90,10 @@ export function exampleOptions(parameter: ParameterSpec) {
3290
return parameter.examples?.length && parameter.examples.length > 1
3391
? [
3492
selectExampleOption,
35-
...parameter.examples.map(example => ({ label: example.key, value: exampleValue(example) })),
93+
...parameter.examples.map(example => ({
94+
label: example.key,
95+
value: encodeSafeSelectorValue(exampleValue(example)),
96+
})),
3697
]
3798
: null;
3899
}
@@ -48,18 +109,21 @@ export function parameterSupportsFileUpload(parameter?: Pick<ParameterSpec, 'sch
48109
}
49110

50111
function stringifyValue(value: unknown) {
51-
return typeof value === 'object' ? JSON.stringify(value) : escapeQuotes(String(value));
52-
}
53-
54-
function exampleValue(example: Omit<INodeExample, 'id'> | Omit<INodeExternalExample, 'id'>) {
55-
const value = 'value' in example ? example.value : example.externalValue;
56-
return stringifyValue(value);
112+
if (typeof value === 'object' && value !== null) {
113+
return safeStringify(value) ?? String(value);
114+
}
115+
return String(value);
57116
}
58117

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

122+
function exampleValue(example: Omit<INodeExample, 'id'> | Omit<INodeExternalExample, 'id'>) {
123+
const value = 'value' in example ? example.value : example.externalValue;
124+
return escapeQuotes(stringifyValue(value));
125+
}
126+
63127
export function getPlaceholderForParameter(parameter: ParameterSpec) {
64128
const { value: parameterValue, isDefault } = getValueForParameter(parameter);
65129

@@ -89,16 +153,18 @@ const getValueForParameter = (parameter: ParameterSpec) => {
89153
return { value: stringifyValue(defaultValue), isDefault: true };
90154
}
91155

92-
const examples = parameter.examples ?? [];
93-
if (examples.length > 0) {
94-
return { value: exampleValue(examples[0]) };
95-
}
96-
156+
// If the parameter has enums, prioritize using the first enum value
157+
// over examples, as examples might not match the enum values
97158
const enums = parameter.schema?.enum ?? [];
98159
if (enums.length > 0) {
99160
return { value: stringifyValue(enums[0]) };
100161
}
101162

163+
const examples = parameter.examples ?? [];
164+
if (examples.length > 0) {
165+
return { value: exampleValue(examples[0]) };
166+
}
167+
102168
return { value: '' };
103169
};
104170

packages/elements-core/src/components/TryIt/Servers/VariableEditor.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as React from 'react';
33

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

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

18+
// Find the encoded value that matches the current (decoded) value
19+
const encodedOptions = React.useMemo(
20+
() => (variable.enum ? variable.enum.map(s => ({ value: encodeSafeSelectorValue(s), label: String(s) })) : []),
21+
[variable.enum],
22+
);
23+
24+
const encodedValue = React.useMemo(() => {
25+
if (!value || !variable.enum) return value || variable.default;
26+
const matchingOption = encodedOptions.find(opt => decodeSafeSelectorValue(String(opt.value)) === value);
27+
return matchingOption ? String(matchingOption.value) : value;
28+
}, [value, variable.enum, variable.default, encodedOptions]);
29+
1730
return (
1831
<>
1932
<Text as="label" aria-hidden="true" data-testid="param-label" htmlFor={inputId} fontSize="base">
@@ -25,9 +38,9 @@ export const VariableEditor: React.FC<VariableProps> = ({ variable, value, onCha
2538
<Select
2639
flex={1}
2740
aria-label={variable.name}
28-
options={variable.enum.map(s => ({ value: s }))}
29-
value={value || variable.default}
30-
onChange={onChange}
41+
options={encodedOptions}
42+
value={encodedValue}
43+
onChange={val => onChange && onChange(decodeSafeSelectorValue(String(val)))}
3144
/>
3245
) : (
3346
<Flex flex={1}>

packages/elements-dev-portal/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoplight/elements-dev-portal",
3-
"version": "3.0.14",
3+
"version": "3.0.15",
44
"description": "UI components for composing beautiful developer documentation.",
55
"keywords": [],
66
"sideEffects": [
@@ -66,7 +66,7 @@
6666
"dependencies": {
6767
"@stoplight/markdown-viewer": "^5.7.1",
6868
"@stoplight/mosaic": "^1.53.5",
69-
"@stoplight/elements-core": "~9.0.14",
69+
"@stoplight/elements-core": "~9.0.15",
7070
"@stoplight/path": "^1.3.2",
7171
"@stoplight/types": "^14.0.0",
7272
"classnames": "^2.2.6",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// auto-updated during build
2-
export const appVersion = '3.0.14';
2+
export const appVersion = '3.0.15';

packages/elements/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stoplight/elements",
3-
"version": "9.0.14",
3+
"version": "9.0.15",
44
"description": "UI components for composing beautiful developer documentation.",
55
"keywords": [],
66
"sideEffects": [
@@ -63,7 +63,7 @@
6363
]
6464
},
6565
"dependencies": {
66-
"@stoplight/elements-core": "~9.0.14",
66+
"@stoplight/elements-core": "~9.0.15",
6767
"@stoplight/http-spec": "^7.1.0",
6868
"@stoplight/json": "^3.18.1",
6969
"@stoplight/mosaic": "^1.53.5",

0 commit comments

Comments
 (0)