Skip to content

Commit

Permalink
fix: unify redoc config (#2647)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Ivan Kropyvnytskyi <[email protected]>
  • Loading branch information
AlexVarchuk and ivankropyvnytskyi authored Jan 30, 2025
1 parent ae1ae79 commit 53a6afc
Show file tree
Hide file tree
Showing 27 changed files with 443 additions and 175 deletions.
2 changes: 1 addition & 1 deletion demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class DemoApp extends React.Component<
<RedocStandalone
spec={this.state.spec}
specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }}
options={{ scrollYOffset: 'nav', sanitize: true }}
/>
</>
);
Expand Down
6 changes: 6 additions & 0 deletions demo/museum.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ components:
enum:
- event
- general
x-enumDescriptions:
event: Special event ticket
general: General museum entry ticket
example: event
Date:
type: string
Expand Down Expand Up @@ -776,6 +779,9 @@ x-tagGroups:
- name: Purchases
tags:
- Tickets
- name: Entities
tags:
- Schemas

security:
- MuseumPlaceholderAuth: []
4 changes: 4 additions & 0 deletions demo/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,10 @@ components:
- available
- pending
- sold
x-enumDescriptions:
available: Available status
pending: Pending status
sold: Sold status
petType:
description: Type of a pet
type: string
Expand Down
6 changes: 5 additions & 1 deletion demo/playground/hmr-playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ const userUrl = window.location.search.match(/url=(.*)$/);
const specUrl =
(userUrl && userUrl[1]) || (swagger ? 'museum.yaml' : big ? 'big-openapi.json' : 'museum.yaml');

const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
const options: RedocRawOptions = {
nativeScrollbars: false,
maxDisplayedEnumValues: 3,
schemaDefinitionsTagName: 'schemas',
};

const container = document.getElementById('example');
const root = createRoot(container!);
Expand Down
36 changes: 17 additions & 19 deletions src/components/ApiInfo/ApiInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,13 @@ export interface ApiInfoProps {

@observer
export class ApiInfo extends React.Component<ApiInfoProps> {
handleDownloadClick = e => {
if (!e.target.href) {
e.target.href = this.props.store.spec.info.downloadLink;
}
};

render() {
const { store } = this.props;
const { info, externalDocs } = store.spec;
const hideDownloadButton = store.options.hideDownloadButton;

const downloadFilename = info.downloadFileName;
const downloadLink = info.downloadLink;
const hideDownloadButtons = store.options.hideDownloadButtons;

const downloadUrls = info.downloadUrls;
const downloadFileName = info.downloadFileName;
const license =
(info.license && (
<InfoSpan>
Expand Down Expand Up @@ -83,17 +76,22 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<ApiHeader>
{info.title} {version}
</ApiHeader>
{!hideDownloadButton && (
{!hideDownloadButtons && (
<p>
{l('downloadSpecification')}:
<DownloadButton
download={downloadFilename || true}
target="_blank"
href={downloadLink}
onClick={this.handleDownloadClick}
>
{l('download')}
</DownloadButton>
{downloadUrls?.map(({ title, url }) => {
return (
<DownloadButton
download={downloadFileName || true}
target="_blank"
href={url}
rel="noreferrer"
key={url}
>
{title}
</DownloadButton>
);
})}
</p>
)}
<StyledMarkdownBlock>
Expand Down
114 changes: 83 additions & 31 deletions src/components/Fields/EnumValues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
import styled from '../../styled-components';
import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
import { Markdown } from '../Markdown/Markdown';

export interface EnumValuesProps {
values: string[];
isArrayType: boolean;
values?: string[] | { [name: string]: string };
type: string | string[];
}

export interface EnumValuesState {
collapsed: boolean;
}

const DescriptionEnumsBlock = styled(StyledMarkdownBlock)`
table {
margin-bottom: 0.2em;
}
`;

export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesState> {
constructor(props: EnumValuesProps) {
super(props);
this.toggle = this.toggle.bind(this);
}
state: EnumValuesState = {
collapsed: true,
};
Expand All @@ -27,54 +39,94 @@ export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesS
}

render() {
const { values, isArrayType } = this.props;
const { values, type } = this.props;
const { collapsed } = this.state;
const isDescriptionEnum = !Array.isArray(values);
const enums =
(Array.isArray(values) && values) ||
Object.entries(values || {}).map(([value, description]) => ({
value,
description,
}));

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

if (!values.length) {
if (!enums.length) {
return null;
}

const displayedItems =
this.state.collapsed && maxDisplayedEnumValues
? values.slice(0, maxDisplayedEnumValues)
: values;
? enums.slice(0, maxDisplayedEnumValues)
: enums;

const showToggleButton = maxDisplayedEnumValues
? values.length > maxDisplayedEnumValues
: false;
const showToggleButton = maxDisplayedEnumValues ? enums.length > maxDisplayedEnumValues : false;

const toggleButtonText = maxDisplayedEnumValues
? collapsed
? `… ${values.length - maxDisplayedEnumValues} more`
? `… ${enums.length - maxDisplayedEnumValues} more`
: 'Hide'
: '';

return (
<div>
<FieldLabel>
{isArrayType ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '}
{displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{exampleValue}</ExampleValue>{' '}
</React.Fragment>
);
})}
{showToggleButton ? (
<ToggleButton
onClick={() => {
this.toggle();
}}
>
{toggleButtonText}
</ToggleButton>
) : null}
{isDescriptionEnum ? (
<>
<DescriptionEnumsBlock>
<table>
<thead>
<tr>
<th>
<FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '}
{enums.length === 1 ? l('enumSingleValue') : l('enum')}
</FieldLabel>{' '}
</th>
<th>
<strong>Description</strong>
</th>
</tr>
</thead>
<tbody>
{(displayedItems as { value: string; description: string }[]).map(
({ description, value }) => {
return (
<tr key={value}>
<td>{value}</td>
<td>
<Markdown source={description} compact inline />
</td>
</tr>
);
},
)}
</tbody>
</table>
</DescriptionEnumsBlock>
{showToggleButton ? (
<ToggleButton onClick={this.toggle}>{toggleButtonText}</ToggleButton>
) : null}
</>
) : (
<>
<FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '}
{displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{exampleValue}</ExampleValue>{' '}
</React.Fragment>
);
})}
{showToggleButton ? (
<ToggleButton onClick={this.toggle}>{toggleButtonText}</ToggleButton>
) : null}
</>
)}
</div>
);
}
Expand Down
20 changes: 17 additions & 3 deletions src/components/Fields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Schema } from '../Schema/Schema';

import type { SchemaOptions } from '../Schema/Schema';
import type { FieldModel } from '../../services/models';
import { OptionsContext } from '../OptionsProvider';
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';

export interface FieldProps extends SchemaOptions {
className?: string;
Expand All @@ -27,12 +29,15 @@ export interface FieldProps extends SchemaOptions {

field: FieldModel;
expandByDefault?: boolean;

fieldParentsName?: string[];
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
}

@observer
export class Field extends React.Component<FieldProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;

toggle = () => {
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
this.props.field.collapse();
Expand All @@ -49,12 +54,12 @@ export class Field extends React.Component<FieldProps> {
};

render() {
const { className = '', field, isLast, expandByDefault } = this.props;
const { hidePropertiesPrefix } = this.context;
const { className = '', field, isLast, expandByDefault, fieldParentsName = [] } = this.props;
const { name, deprecated, required, kind } = field;
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;

const expanded = field.expanded === undefined ? expandByDefault : field.expanded;

const labels = (
<>
{kind === 'additionalProperties' && <PropertyLabel>additional property</PropertyLabel>}
Expand All @@ -75,6 +80,10 @@ export class Field extends React.Component<FieldProps> {
onKeyPress={this.handleKeyPress}
aria-label={`expand ${name}`}
>
{!hidePropertiesPrefix &&
fieldParentsName.map(
name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking
)}
<span className="property-name">{name}</span>
<ShelfIcon direction={expanded ? 'down' : 'right'} />
</button>
Expand All @@ -83,6 +92,10 @@ export class Field extends React.Component<FieldProps> {
) : (
<PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}>
<PropertyBullet />
{!hidePropertiesPrefix &&
fieldParentsName.map(
name => name + '.\u200B', // zero-width space, a special character is used for correct line breaking
)}
<span className="property-name">{name}</span>
{labels}
</PropertyNameCell>
Expand All @@ -102,6 +115,7 @@ export class Field extends React.Component<FieldProps> {
<InnerPropertiesWrap>
<Schema
schema={field.schema}
fieldParentsName={[...(fieldParentsName || []), field.name]}
skipReadOnly={this.props.skipReadOnly}
skipWriteOnly={this.props.skipWriteOnly}
showTitle={this.props.showTitle}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Fields/FieldDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const FieldDetailsComponent = observer((props: FieldProps) => {
)}
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={defaultValue} />
{!renderDiscriminatorSwitch && (
<EnumValues isArrayType={isArrayType} values={schema.enum} />
<EnumValues type={schema.type} values={schema['x-enumDescriptions'] || schema.enum} />
)}{' '}
{renderedExamples}
<Extensions extensions={{ ...extensions, ...schema.extensions }} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/JsonViewer/JsonViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Json = (props: JsonProps) => {
// tslint:disable-next-line
ref={node => setNode(node!)}
dangerouslySetInnerHTML={{
__html: jsonToHTML(props.data, options.jsonSampleExpandLevel),
__html: jsonToHTML(props.data, options.jsonSamplesExpandLevel),
}}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Markdown/SanitizedMdBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const StyledMarkdownSpan = styled(StyledMarkdownBlock)`
display: inline;
`;

const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
const sanitize = (sanitize, html) => (sanitize ? DOMPurify.sanitize(html) : html);

export function SanitizedMarkdownHTML({
inline,
Expand All @@ -25,7 +25,7 @@ export function SanitizedMarkdownHTML({
<Wrap
className={'redoc-markdown ' + (rest.className || '')}
dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, rest.html),
__html: sanitize(options.sanitize, rest.html),
}}
data-role={rest['data-role']}
{...rest}
Expand Down
Loading

0 comments on commit 53a6afc

Please sign in to comment.