Skip to content

Commit 53a6afc

Browse files
fix: unify redoc config (#2647)
--------- Co-authored-by: Ivan Kropyvnytskyi <[email protected]>
1 parent ae1ae79 commit 53a6afc

27 files changed

+443
-175
lines changed

demo/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class DemoApp extends React.Component<
122122
<RedocStandalone
123123
spec={this.state.spec}
124124
specUrl={proxiedUrl}
125-
options={{ scrollYOffset: 'nav', untrustedSpec: true }}
125+
options={{ scrollYOffset: 'nav', sanitize: true }}
126126
/>
127127
</>
128128
);

demo/museum.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ components:
309309
enum:
310310
- event
311311
- general
312+
x-enumDescriptions:
313+
event: Special event ticket
314+
general: General museum entry ticket
312315
example: event
313316
Date:
314317
type: string
@@ -776,6 +779,9 @@ x-tagGroups:
776779
- name: Purchases
777780
tags:
778781
- Tickets
782+
- name: Entities
783+
tags:
784+
- Schemas
779785

780786
security:
781787
- MuseumPlaceholderAuth: []

demo/openapi.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,10 @@ components:
10831083
- available
10841084
- pending
10851085
- sold
1086+
x-enumDescriptions:
1087+
available: Available status
1088+
pending: Pending status
1089+
sold: Sold status
10861090
petType:
10871091
description: Type of a pet
10881092
type: string

demo/playground/hmr-playground.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ const userUrl = window.location.search.match(/url=(.*)$/);
1111
const specUrl =
1212
(userUrl && userUrl[1]) || (swagger ? 'museum.yaml' : big ? 'big-openapi.json' : 'museum.yaml');
1313

14-
const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
14+
const options: RedocRawOptions = {
15+
nativeScrollbars: false,
16+
maxDisplayedEnumValues: 3,
17+
schemaDefinitionsTagName: 'schemas',
18+
};
1519

1620
const container = document.getElementById('example');
1721
const root = createRoot(container!);

src/components/ApiInfo/ApiInfo.tsx

+17-19
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,13 @@ export interface ApiInfoProps {
2222

2323
@observer
2424
export class ApiInfo extends React.Component<ApiInfoProps> {
25-
handleDownloadClick = e => {
26-
if (!e.target.href) {
27-
e.target.href = this.props.store.spec.info.downloadLink;
28-
}
29-
};
30-
3125
render() {
3226
const { store } = this.props;
3327
const { info, externalDocs } = store.spec;
34-
const hideDownloadButton = store.options.hideDownloadButton;
35-
36-
const downloadFilename = info.downloadFileName;
37-
const downloadLink = info.downloadLink;
28+
const hideDownloadButtons = store.options.hideDownloadButtons;
3829

30+
const downloadUrls = info.downloadUrls;
31+
const downloadFileName = info.downloadFileName;
3932
const license =
4033
(info.license && (
4134
<InfoSpan>
@@ -83,17 +76,22 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
8376
<ApiHeader>
8477
{info.title} {version}
8578
</ApiHeader>
86-
{!hideDownloadButton && (
79+
{!hideDownloadButtons && (
8780
<p>
8881
{l('downloadSpecification')}:
89-
<DownloadButton
90-
download={downloadFilename || true}
91-
target="_blank"
92-
href={downloadLink}
93-
onClick={this.handleDownloadClick}
94-
>
95-
{l('download')}
96-
</DownloadButton>
82+
{downloadUrls?.map(({ title, url }) => {
83+
return (
84+
<DownloadButton
85+
download={downloadFileName || true}
86+
target="_blank"
87+
href={url}
88+
rel="noreferrer"
89+
key={url}
90+
>
91+
{title}
92+
</DownloadButton>
93+
);
94+
})}
9795
</p>
9896
)}
9997
<StyledMarkdownBlock>

src/components/Fields/EnumValues.tsx

+83-31
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ import { l } from '../../services/Labels';
55
import { OptionsContext } from '../OptionsProvider';
66
import styled from '../../styled-components';
77
import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
8+
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
9+
import { Markdown } from '../Markdown/Markdown';
810

911
export interface EnumValuesProps {
10-
values: string[];
11-
isArrayType: boolean;
12+
values?: string[] | { [name: string]: string };
13+
type: string | string[];
1214
}
1315

1416
export interface EnumValuesState {
1517
collapsed: boolean;
1618
}
1719

20+
const DescriptionEnumsBlock = styled(StyledMarkdownBlock)`
21+
table {
22+
margin-bottom: 0.2em;
23+
}
24+
`;
25+
1826
export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesState> {
27+
constructor(props: EnumValuesProps) {
28+
super(props);
29+
this.toggle = this.toggle.bind(this);
30+
}
1931
state: EnumValuesState = {
2032
collapsed: true,
2133
};
@@ -27,54 +39,94 @@ export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesS
2739
}
2840

2941
render() {
30-
const { values, isArrayType } = this.props;
42+
const { values, type } = this.props;
3143
const { collapsed } = this.state;
44+
const isDescriptionEnum = !Array.isArray(values);
45+
const enums =
46+
(Array.isArray(values) && values) ||
47+
Object.entries(values || {}).map(([value, description]) => ({
48+
value,
49+
description,
50+
}));
3251

3352
// TODO: provide context interface in more elegant way
3453
const { enumSkipQuotes, maxDisplayedEnumValues } = this.context as RedocRawOptions;
3554

36-
if (!values.length) {
55+
if (!enums.length) {
3756
return null;
3857
}
3958

4059
const displayedItems =
4160
this.state.collapsed && maxDisplayedEnumValues
42-
? values.slice(0, maxDisplayedEnumValues)
43-
: values;
61+
? enums.slice(0, maxDisplayedEnumValues)
62+
: enums;
4463

45-
const showToggleButton = maxDisplayedEnumValues
46-
? values.length > maxDisplayedEnumValues
47-
: false;
64+
const showToggleButton = maxDisplayedEnumValues ? enums.length > maxDisplayedEnumValues : false;
4865

4966
const toggleButtonText = maxDisplayedEnumValues
5067
? collapsed
51-
? `… ${values.length - maxDisplayedEnumValues} more`
68+
? `… ${enums.length - maxDisplayedEnumValues} more`
5269
: 'Hide'
5370
: '';
5471

5572
return (
5673
<div>
57-
<FieldLabel>
58-
{isArrayType ? l('enumArray') : ''}{' '}
59-
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
60-
</FieldLabel>{' '}
61-
{displayedItems.map((value, idx) => {
62-
const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
63-
return (
64-
<React.Fragment key={idx}>
65-
<ExampleValue>{exampleValue}</ExampleValue>{' '}
66-
</React.Fragment>
67-
);
68-
})}
69-
{showToggleButton ? (
70-
<ToggleButton
71-
onClick={() => {
72-
this.toggle();
73-
}}
74-
>
75-
{toggleButtonText}
76-
</ToggleButton>
77-
) : null}
74+
{isDescriptionEnum ? (
75+
<>
76+
<DescriptionEnumsBlock>
77+
<table>
78+
<thead>
79+
<tr>
80+
<th>
81+
<FieldLabel>
82+
{type === 'array' ? l('enumArray') : ''}{' '}
83+
{enums.length === 1 ? l('enumSingleValue') : l('enum')}
84+
</FieldLabel>{' '}
85+
</th>
86+
<th>
87+
<strong>Description</strong>
88+
</th>
89+
</tr>
90+
</thead>
91+
<tbody>
92+
{(displayedItems as { value: string; description: string }[]).map(
93+
({ description, value }) => {
94+
return (
95+
<tr key={value}>
96+
<td>{value}</td>
97+
<td>
98+
<Markdown source={description} compact inline />
99+
</td>
100+
</tr>
101+
);
102+
},
103+
)}
104+
</tbody>
105+
</table>
106+
</DescriptionEnumsBlock>
107+
{showToggleButton ? (
108+
<ToggleButton onClick={this.toggle}>{toggleButtonText}</ToggleButton>
109+
) : null}
110+
</>
111+
) : (
112+
<>
113+
<FieldLabel>
114+
{type === 'array' ? l('enumArray') : ''}{' '}
115+
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
116+
</FieldLabel>{' '}
117+
{displayedItems.map((value, idx) => {
118+
const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
119+
return (
120+
<React.Fragment key={idx}>
121+
<ExampleValue>{exampleValue}</ExampleValue>{' '}
122+
</React.Fragment>
123+
);
124+
})}
125+
{showToggleButton ? (
126+
<ToggleButton onClick={this.toggle}>{toggleButtonText}</ToggleButton>
127+
) : null}
128+
</>
129+
)}
78130
</div>
79131
);
80132
}

src/components/Fields/Field.tsx

+17-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Schema } from '../Schema/Schema';
1919

2020
import type { SchemaOptions } from '../Schema/Schema';
2121
import type { FieldModel } from '../../services/models';
22+
import { OptionsContext } from '../OptionsProvider';
23+
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
2224

2325
export interface FieldProps extends SchemaOptions {
2426
className?: string;
@@ -27,12 +29,15 @@ export interface FieldProps extends SchemaOptions {
2729

2830
field: FieldModel;
2931
expandByDefault?: boolean;
30-
32+
fieldParentsName?: string[];
3133
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
3234
}
3335

3436
@observer
3537
export class Field extends React.Component<FieldProps> {
38+
static contextType = OptionsContext;
39+
context: RedocNormalizedOptions;
40+
3641
toggle = () => {
3742
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
3843
this.props.field.collapse();
@@ -49,12 +54,12 @@ export class Field extends React.Component<FieldProps> {
4954
};
5055

5156
render() {
52-
const { className = '', field, isLast, expandByDefault } = this.props;
57+
const { hidePropertiesPrefix } = this.context;
58+
const { className = '', field, isLast, expandByDefault, fieldParentsName = [] } = this.props;
5359
const { name, deprecated, required, kind } = field;
5460
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;
5561

5662
const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
57-
5863
const labels = (
5964
<>
6065
{kind === 'additionalProperties' && <PropertyLabel>additional property</PropertyLabel>}
@@ -75,6 +80,10 @@ export class Field extends React.Component<FieldProps> {
7580
onKeyPress={this.handleKeyPress}
7681
aria-label={`expand ${name}`}
7782
>
83+
{!hidePropertiesPrefix &&
84+
fieldParentsName.map(
85+
name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking
86+
)}
7887
<span className="property-name">{name}</span>
7988
<ShelfIcon direction={expanded ? 'down' : 'right'} />
8089
</button>
@@ -83,6 +92,10 @@ export class Field extends React.Component<FieldProps> {
8392
) : (
8493
<PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}>
8594
<PropertyBullet />
95+
{!hidePropertiesPrefix &&
96+
fieldParentsName.map(
97+
name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking
98+
)}
8699
<span className="property-name">{name}</span>
87100
{labels}
88101
</PropertyNameCell>
@@ -102,6 +115,7 @@ export class Field extends React.Component<FieldProps> {
102115
<InnerPropertiesWrap>
103116
<Schema
104117
schema={field.schema}
118+
fieldParentsName={[...(fieldParentsName || []), field.name]}
105119
skipReadOnly={this.props.skipReadOnly}
106120
skipWriteOnly={this.props.skipWriteOnly}
107121
showTitle={this.props.showTitle}

src/components/Fields/FieldDetails.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const FieldDetailsComponent = observer((props: FieldProps) => {
9999
)}
100100
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={defaultValue} />
101101
{!renderDiscriminatorSwitch && (
102-
<EnumValues isArrayType={isArrayType} values={schema.enum} />
102+
<EnumValues type={schema.type} values={schema['x-enumDescriptions'] || schema.enum} />
103103
)}{' '}
104104
{renderedExamples}
105105
<Extensions extensions={{ ...extensions, ...schema.extensions }} />

src/components/JsonViewer/JsonViewer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const Json = (props: JsonProps) => {
4545
// tslint:disable-next-line
4646
ref={node => setNode(node!)}
4747
dangerouslySetInnerHTML={{
48-
__html: jsonToHTML(props.data, options.jsonSampleExpandLevel),
48+
__html: jsonToHTML(props.data, options.jsonSamplesExpandLevel),
4949
}}
5050
/>
5151
)}

src/components/Markdown/SanitizedMdBlock.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const StyledMarkdownSpan = styled(StyledMarkdownBlock)`
1010
display: inline;
1111
`;
1212

13-
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
13+
const sanitize = (sanitize, html) => (sanitize ? DOMPurify.sanitize(html) : html);
1414

1515
export function SanitizedMarkdownHTML({
1616
inline,
@@ -25,7 +25,7 @@ export function SanitizedMarkdownHTML({
2525
<Wrap
2626
className={'redoc-markdown ' + (rest.className || '')}
2727
dangerouslySetInnerHTML={{
28-
__html: sanitize(options.untrustedSpec, rest.html),
28+
__html: sanitize(options.sanitize, rest.html),
2929
}}
3030
data-role={rest['data-role']}
3131
{...rest}

0 commit comments

Comments
 (0)