Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: unify redoc config #2647

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
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
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
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}
>
{downloadUrls.length > 1 ? title : l('download')}
</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
14 changes: 12 additions & 2 deletions src/components/Schema/ArraySchema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@ export class ArraySchema extends React.PureComponent<SchemaProps> {
render() {
const schema = this.props.schema;
const itemsSchema = schema.items;
const fieldParentsName = this.props.fieldParentsName;

const minMaxItems =
schema.minItems === undefined && schema.maxItems === undefined
? ''
: `(${humanizeConstraints(schema)})`;

const updatedParentsArray = fieldParentsName
? [...fieldParentsName.slice(0, -1), fieldParentsName[fieldParentsName.length - 1] + '[]']
: fieldParentsName;
if (schema.fields) {
return <ObjectSchema {...(this.props as any)} level={this.props.level} />;
return (
<ObjectSchema
{...(this.props as any)}
level={this.props.level}
fieldParentsName={updatedParentsArray}
/>
);
}
if (schema.displayType && !itemsSchema && !minMaxItems.length) {
return (
Expand All @@ -37,7 +47,7 @@ export class ArraySchema extends React.PureComponent<SchemaProps> {
<div>
<ArrayOpenningLabel> Array {minMaxItems}</ArrayOpenningLabel>
<PaddedSchema>
<Schema {...this.props} schema={itemsSchema} />
<Schema {...this.props} schema={itemsSchema} fieldParentsName={updatedParentsArray} />
</PaddedSchema>
<ArrayClosingLabel />
</div>
Expand Down
Loading
Loading